Extract Astro styles to external stylesheets (#43)

* Extract Astro styles to external stylesheets

* Require relative URLs in Markdown layouts
This commit is contained in:
Drew Powers 2021-03-31 13:04:18 -06:00 committed by GitHub
parent a3b20a9aff
commit 3fa6396a7b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
54 changed files with 201 additions and 164 deletions

View file

@ -3,12 +3,15 @@ module.exports = {
extends: ['plugin:@typescript-eslint/recommended', 'prettier'], extends: ['plugin:@typescript-eslint/recommended', 'prettier'],
plugins: ['@typescript-eslint', 'prettier'], plugins: ['@typescript-eslint', 'prettier'],
rules: { rules: {
'@typescript-eslint/ban-ts-comment': 'warn',
'@typescript-eslint/camelcase': 'off', '@typescript-eslint/camelcase': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-use-before-define': 'off', '@typescript-eslint/no-use-before-define': 'off',
'@typescript-eslint/no-var-requires': 'off', '@typescript-eslint/no-var-requires': 'off',
'no-shadow': 'warn',
'prettier/prettier': 'error', 'prettier/prettier': 'error',
'prefer-const': 'off', 'prefer-const': 'off',
'prefer-rest-params': 'off', 'prefer-rest-params': 'off',
'require-jsdoc': 'warn',
}, },
}; };

View file

@ -27,3 +27,10 @@ jobs:
npm test npm test
env: env:
CI: true CI: true
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/setup-node@v1
- run: npm ci
- run: npm run lint

View file

@ -1,5 +1,5 @@
--- ---
layout: layouts/content.astro layout: ../../layouts/content.astro
title: The Build Pipeline title: The Build Pipeline
description: Snowpack Build creates a production-ready website with or without a bundler description: Snowpack Build creates a production-ready website with or without a bundler
--- ---

View file

@ -1,5 +1,5 @@
--- ---
layout: layouts/content.astro layout: ../../layouts/content.astro
title: The Dev Server title: The Dev Server
description: Snowpack's dev server is fast because it only rebuilds the files you change. Powered by ESM (ES modules). description: Snowpack's dev server is fast because it only rebuilds the files you change. Powered by ESM (ES modules).
--- ---

View file

@ -1,5 +1,5 @@
--- ---
layout: layouts/content.astro layout: ../../layouts/content.astro
title: HMR + Fast Refresh title: HMR + Fast Refresh
description: Snowpack's ESM-powered unbundled development means near-instant single file builds that only take 10-25ms to load and update in the browser. description: Snowpack's ESM-powered unbundled development means near-instant single file builds that only take 10-25ms to load and update in the browser.
--- ---

View file

@ -1,5 +1,5 @@
--- ---
layout: layouts/content.astro layout: ../../layouts/content.astro
title: How Snowpack Works title: How Snowpack Works
description: Snowpack serves your application unbundled during development. Each file is built only once and is cached until it changes. description: Snowpack serves your application unbundled during development. Each file is built only once and is cached until it changes.
--- ---

View file

@ -1,5 +1,5 @@
--- ---
layout: layouts/content.astro layout: ../../layouts/content.astro
title: 'Babel' title: 'Babel'
tags: communityGuide tags: communityGuide
published: true published: true

View file

@ -1,5 +1,5 @@
--- ---
layout: layouts/content.astro layout: ../../layouts/content.astro
title: The Snowpack Guide to connecting your favorite tools title: The Snowpack Guide to connecting your favorite tools
description: 'How do you use your favorite tools in Snowpack? This Guide will help you get started' description: 'How do you use your favorite tools in Snowpack? This Guide will help you get started'
published: true published: true

View file

@ -1,5 +1,5 @@
--- ---
layout: layouts/content.astro layout: ../../layouts/content.astro
title: Hot Module Replacement (HMR) title: Hot Module Replacement (HMR)
description: Enable Snowpack's Hot Module Replacement (HMR) on your development server. description: Enable Snowpack's Hot Module Replacement (HMR) on your development server.
published: false published: false

View file

@ -1,5 +1,5 @@
--- ---
layout: layouts/content.astro layout: ../../layouts/content.astro
title: SSL Certificates title: SSL Certificates
description: How to use HTTPs during development and generate SSL certifcates for your Snowpack build. description: How to use HTTPs during development and generate SSL certifcates for your Snowpack build.
--- ---

View file

@ -1,5 +1,5 @@
--- ---
layout: layouts/content.astro layout: ../../layouts/content.astro
title: 'Jest' title: 'Jest'
tags: communityGuide tags: communityGuide
img: '/img/logos/jest.svg' img: '/img/logos/jest.svg'

View file

@ -1,5 +1,5 @@
--- ---
layout: layouts/content.astro layout: ../../layouts/content.astro
title: Optimize & Bundle for Production title: Optimize & Bundle for Production
published: true published: true
description: How to optimize your Snowpack build for production, with or without a bundler. description: How to optimize your Snowpack build for production, with or without a bundler.

View file

@ -1,5 +1,5 @@
--- ---
layout: layouts/content.astro layout: ../../layouts/content.astro
title: Creating Your Own Plugin title: Creating Your Own Plugin
description: Learn the basics of our Plugin API through working examples. description: Learn the basics of our Plugin API through working examples.
--- ---

View file

@ -1,5 +1,5 @@
--- ---
layout: layouts/content.astro layout: ../../layouts/content.astro
title: 'PostCSS' title: 'PostCSS'
tags: communityGuide tags: communityGuide
published: true published: true

View file

@ -1,5 +1,5 @@
--- ---
layout: layouts/content.astro layout: ../../layouts/content.astro
title: Preact title: Preact
tags: communityGuide tags: communityGuide
img: '/img/logos/preact.svg' img: '/img/logos/preact.svg'

View file

@ -1,5 +1,5 @@
--- ---
layout: layouts/content.astro layout: ../../layouts/content.astro
title: React + babel-plugin-import-global title: React + babel-plugin-import-global
published: false published: false
--- ---

View file

@ -1,5 +1,5 @@
--- ---
layout: layouts/content.astro layout: ../../layouts/content.astro
title: React + Loadable Components title: React + Loadable Components
published: false published: false
--- ---

View file

@ -1,5 +1,5 @@
--- ---
layout: layouts/content.astro layout: ../../layouts/content.astro
title: Routing title: Routing
published: true published: true
description: This guide will walk you through some common routing scenarios and how to configure the routes option to support them in development. description: This guide will walk you through some common routing scenarios and how to configure the routes option to support them in development.

View file

@ -1,5 +1,5 @@
--- ---
layout: layouts/content.astro layout: ../../layouts/content.astro
title: 'Sass' title: 'Sass'
tags: communityGuide tags: communityGuide
published: true published: true

View file

@ -1,5 +1,5 @@
--- ---
layout: layouts/content.astro layout: ../../layouts/content.astro
title: Server-Side Rendering (SSR) title: Server-Side Rendering (SSR)
description: This guide will walk you through three different options for setting up Snowpack with your own custom server. description: This guide will walk you through three different options for setting up Snowpack with your own custom server.
published: true published: true

View file

@ -1,5 +1,5 @@
--- ---
layout: layouts/content.astro layout: ../../layouts/content.astro
title: Streaming Imports title: Streaming Imports
published: true published: true
stream: Fetch your npm dependencies on-demand from a remote ESM CDN. stream: Fetch your npm dependencies on-demand from a remote ESM CDN.

View file

@ -1,5 +1,5 @@
--- ---
layout: layouts/content.astro layout: ../../layouts/content.astro
title: 'Tailwind CSS' title: 'Tailwind CSS'
tags: communityGuide tags: communityGuide
published: true published: true

View file

@ -1,5 +1,5 @@
--- ---
layout: layouts/content.astro layout: ../../layouts/content.astro
title: Testing title: Testing
published: true published: true
description: How to choose and use a JavaScript test runner for your Snowpack site. description: How to choose and use a JavaScript test runner for your Snowpack site.

View file

@ -1,5 +1,5 @@
--- ---
layout: layouts/content.astro layout: ../../layouts/content.astro
title: Snowpack Upgrade Guide title: Snowpack Upgrade Guide
published: true published: true
description: How to upgrade to Snowpack v3 from older versions of Snowpack. description: How to upgrade to Snowpack v3 from older versions of Snowpack.

View file

@ -1,5 +1,5 @@
--- ---
layout: layouts/content.astro layout: ../../layouts/content.astro
title: Vue title: Vue
tags: communityGuide tags: communityGuide
img: '/img/logos/vue.png' img: '/img/logos/vue.png'

View file

@ -1,5 +1,5 @@
--- ---
layout: layouts/content.astro layout: ../../layouts/content.astro
title: 'WASM' title: 'WASM'
tags: communityGuide tags: communityGuide
published: true published: true

View file

@ -1,5 +1,5 @@
--- ---
layout: layouts/content.astro layout: ../../layouts/content.astro
title: '@web/test-runner' title: '@web/test-runner'
tags: communityGuide tags: communityGuide
img: '/img/logos/modern-web.svg' img: '/img/logos/modern-web.svg'

View file

@ -1,5 +1,5 @@
--- ---
layout: layouts/content.astro layout: ../../layouts/content.astro
title: 'Web Workers' title: 'Web Workers'
tags: communityGuide tags: communityGuide
published: true published: true

View file

@ -1,5 +1,5 @@
--- ---
layout: layouts/content.astro layout: ../../layouts/content.astro
title: Workbox title: Workbox
tags: communityGuide tags: communityGuide
description: The Workbox CLI integrates well with Snowpack. description: The Workbox CLI integrates well with Snowpack.

View file

@ -1,5 +1,5 @@
--- ---
layout: layouts/post.astro layout: ../../layouts/post.astro
bannerVideo: '/img/extra-space-4.mp4' bannerVideo: '/img/extra-space-4.mp4'
permalink: '/posts/2020-05-26-snowpack-2-0-release/' permalink: '/posts/2020-05-26-snowpack-2-0-release/'
title: Snowpack v2.0 title: Snowpack v2.0

View file

@ -1,5 +1,5 @@
--- ---
layout: layouts/post.astro layout: ../../layouts/post.astro
title: Snowpack 2.7 title: Snowpack 2.7
description: 'A new plugin API plus smaller, faster production builds.' description: 'A new plugin API plus smaller, faster production builds.'
tagline: v2.7.0 release post tagline: v2.7.0 release post

View file

@ -1,5 +1,5 @@
--- ---
layout: layouts/post.astro layout: ../../layouts/post.astro
title: 'Snowpack v3.0 Release Candidate' title: 'Snowpack v3.0 Release Candidate'
tagline: New features to change the way you build for the web. tagline: New features to change the way you build for the web.
description: 'New features to change the way you build for the web. Snowpack v3.0 will release on January 6th, 2021 (the one-year anniversary of its original launch post). This is our biggest release yet with some serious new features, including a new way to load npm packages on-demand that lets you skip the `npm install` step entirely.' description: 'New features to change the way you build for the web. Snowpack v3.0 will release on January 6th, 2021 (the one-year anniversary of its original launch post). This is our biggest release yet with some serious new features, including a new way to load npm packages on-demand that lets you skip the `npm install` step entirely.'

View file

@ -1,5 +1,5 @@
--- ---
layout: layouts/post.astro layout: ../../layouts/post.astro
title: 'Snowpack v3.0' title: 'Snowpack v3.0'
description: Snowpack v3.0 is here! Our biggest release yet with some serious new features, including pre-bundled streaming imports, built-in bundling & optimizations, new JavaScript APIs, and more.' description: Snowpack v3.0 is here! Our biggest release yet with some serious new features, including pre-bundled streaming imports, built-in bundling & optimizations, new JavaScript APIs, and more.'
date: 2021-01-13 date: 2021-01-13

View file

@ -1,5 +1,5 @@
--- ---
layout: layouts/content.astro layout: ../../layouts/content.astro
title: Command Line API title: Command Line API
description: The Snowpack Command Line tool's API, commands, and flags. description: The Snowpack Command Line tool's API, commands, and flags.
--- ---

View file

@ -1,5 +1,5 @@
--- ---
layout: layouts/content.astro layout: ../../layouts/content.astro
title: Common Error Details title: Common Error Details
description: How to troubleshoot common issues and error messagesm, plus our resources for getting help. description: How to troubleshoot common issues and error messagesm, plus our resources for getting help.
--- ---

View file

@ -1,5 +1,5 @@
--- ---
layout: layouts/content.astro layout: ../../layouts/content.astro
title: snowpack.config.js title: snowpack.config.js
description: The Snowpack configuration API reference. description: The Snowpack configuration API reference.
--- ---

View file

@ -1,5 +1,5 @@
--- ---
layout: layouts/content.astro layout: ../../layouts/content.astro
title: Environment Variables title: Environment Variables
description: Using environment variables with Snowpack description: Using environment variables with Snowpack
--- ---

View file

@ -1,5 +1,5 @@
--- ---
layout: layouts/content.astro layout: ../../layouts/content.astro
title: Hot Module Replacement (HMR) API title: Hot Module Replacement (HMR) API
description: Snowpack implements HMR via the esm-hmr spec, an attempted standard for ESM-based Hot Module Replacement (HMR). description: Snowpack implements HMR via the esm-hmr spec, an attempted standard for ESM-based Hot Module Replacement (HMR).
--- ---

View file

@ -1,5 +1,5 @@
--- ---
layout: layouts/content.astro layout: ../../layouts/content.astro
title: JavaScript API title: JavaScript API
description: Snowpack's JavaScript API is for anyone who wants to integrate with some custom build pipeline or server-side rendering engine. description: Snowpack's JavaScript API is for anyone who wants to integrate with some custom build pipeline or server-side rendering engine.
--- ---

View file

@ -1,5 +1,5 @@
--- ---
layout: layouts/content.astro layout: ../../layouts/content.astro
title: Plugin API title: Plugin API
description: The Snowpack Plugin API and how to use it. description: The Snowpack Plugin API and how to use it.
--- ---

View file

@ -1,5 +1,5 @@
--- ---
layout: layouts/content.astro layout: ../../layouts/content.astro
title: Supported Files title: Supported Files
description: Snowpack ships with built-in support for many file types including json, js, ts, jsx, css, css modules, and images. description: Snowpack ships with built-in support for many file types including json, js, ts, jsx, css, css modules, and images.
--- ---

View file

@ -1,5 +1,5 @@
--- ---
layout: layouts/content.astro layout: ../../layouts/content.astro
title: 'Starting a New Project' title: 'Starting a New Project'
description: This guide shows you how to set up Snowpack from scratch in a Node.js project. Along the way learn key concepts of Snowpack and unbundled development. description: This guide shows you how to set up Snowpack from scratch in a Node.js project. Along the way learn key concepts of Snowpack and unbundled development.
--- ---

View file

@ -1,5 +1,5 @@
--- ---
layout: layouts/content.astro layout: ../../layouts/content.astro
title: Quick Start title: Quick Start
description: A very basic guide for developers who want to run Snowpack as quickly as possible. description: A very basic guide for developers who want to run Snowpack as quickly as possible.
--- ---

View file

@ -1,5 +1,5 @@
--- ---
layout: layouts/content-with-cover.astro layout: ../../layouts/content-with-cover.astro
title: 'Getting Started with React' title: 'Getting Started with React'
description: 'Get started with this in-depth tutorial on how to build React applications and websites with Snowpack and developer tools like React Fast Refresh' description: 'Get started with this in-depth tutorial on how to build React applications and websites with Snowpack and developer tools like React Fast Refresh'
date: 2020-12-01 date: 2020-12-01

View file

@ -1,5 +1,5 @@
--- ---
layout: layouts/content-with-cover.astro layout: ../../layouts/content-with-cover.astro
title: 'Getting Started with Svelte' title: 'Getting Started with Svelte'
description: 'Get started with this in-depth tutorial on how to build Svelte applications and websites with Snowpack' description: 'Get started with this in-depth tutorial on how to build Svelte applications and websites with Snowpack'
date: 2020-12-01 date: 2020-12-01

View file

@ -9,7 +9,7 @@ module.exports = function (snowpackConfig, { resolve, extensions, astroConfig }
knownEntrypoints: ['deepmerge'], knownEntrypoints: ['deepmerge'],
resolve: { resolve: {
input: ['.astro', '.md'], input: ['.astro', '.md'],
output: ['.js'], output: ['.js', '.css'],
}, },
async load({ filePath }) { async load({ filePath }) {
const { compileComponent } = await transformPromise; const { compileComponent } = await transformPromise;
@ -21,7 +21,11 @@ module.exports = function (snowpackConfig, { resolve, extensions, astroConfig }
extensions, extensions,
}; };
const result = await compileComponent(contents, { compileOptions, filename: filePath, projectRoot }); const result = await compileComponent(contents, { compileOptions, filename: filePath, projectRoot });
return result.contents; const output = {
'.js': result.contents,
};
if (result.css) output['.css'] = result.css;
return output;
}, },
}; };
}; };

View file

@ -25,9 +25,11 @@ export interface TransformResult {
script: string; script: string;
imports: string[]; imports: string[];
items: JsxItem[]; items: JsxItem[];
css?: string;
} }
export interface CompileResult { export interface CompileResult {
result: TransformResult; result: TransformResult;
contents: string; contents: string;
css?: string;
} }

View file

@ -21,7 +21,7 @@ interface Attribute {
end: number; end: number;
type: 'Attribute'; type: 'Attribute';
name: string; name: string;
value: any; value: TemplateNode[] | boolean;
} }
interface CodeGenOptions { interface CodeGenOptions {
@ -41,7 +41,8 @@ function getAttributes(attrs: Attribute[]): Record<string, string> {
result[attr.name] = JSON.stringify(attr.value); result[attr.name] = JSON.stringify(attr.value);
continue; continue;
} }
if (attr.value === false) { if (attr.value === false || attr.value === undefined) {
// note: attr.value shouldnt be `undefined`, but a bad transform would cause a compile error here, so prevent that
continue; continue;
} }
if (attr.value.length > 1) { if (attr.value.length > 1) {
@ -59,7 +60,7 @@ function getAttributes(attrs: Attribute[]): Record<string, string> {
')'; ')';
continue; continue;
} }
const val: TemplateNode = attr.value[0]; const val = attr.value[0];
if (!val) { if (!val) {
result[attr.name] = '(' + val + ')'; result[attr.name] = '(' + val + ')';
continue; continue;
@ -72,7 +73,7 @@ function getAttributes(attrs: Attribute[]): Record<string, string> {
result[attr.name] = JSON.stringify(getTextFromAttribute(val)); result[attr.name] = JSON.stringify(getTextFromAttribute(val));
continue; continue;
default: default:
throw new Error('UNKNOWN V'); throw new Error(`UNKNOWN: ${val.type}`);
} }
} }
return result; return result;
@ -253,7 +254,7 @@ async function acquireDynamicComponentImports(plugins: Set<ValidExtensionPlugins
return importMap; return importMap;
} }
export async function codegen(ast: Ast, { compileOptions, filename }: CodeGenOptions): Promise<TransformResult> { export async function codegen(ast: Ast, { compileOptions, filename, fileID }: CodeGenOptions): Promise<TransformResult> {
const { extensions = defaultExtensions, astroConfig } = compileOptions; const { extensions = defaultExtensions, astroConfig } = compileOptions;
await eslexer.init; await eslexer.init;
@ -334,6 +335,21 @@ export async function codegen(ast: Ast, { compileOptions, filename }: CodeGenOpt
let collectionItem: JsxItem | undefined; let collectionItem: JsxItem | undefined;
let currentItemName: string | undefined; let currentItemName: string | undefined;
let currentDepth = 0; let currentDepth = 0;
let css: string[] = [];
walk(ast.css, {
enter(node: TemplateNode) {
if (node.type === 'Style') {
css.push(node.content.styles); // if multiple <style> tags, combine together
this.skip();
}
},
leave(node: TemplateNode) {
if (node.type === 'Style') {
this.remove(); // this will be optimized in a global CSS file; remove so its not accidentally inlined
}
},
});
walk(ast.html, { walk(ast.html, {
enter(node: TemplateNode) { enter(node: TemplateNode) {
@ -419,9 +435,9 @@ export async function codegen(ast: Ast, { compileOptions, filename }: CodeGenOpt
return; return;
} }
case 'Style': { case 'Style': {
const attributes = getAttributes(node.attributes); css.push(node.content.styles); // if multiple <style> tags, combine together
items.push({ name: 'style', jsx: `h("style", ${attributes ? generateAttributes(attributes) : 'null'}, ${JSON.stringify(node.content.styles)})` }); this.skip();
break; return;
} }
case 'Text': { case 'Text': {
const text = getTextFromAttribute(node); const text = getTextFromAttribute(node);
@ -469,6 +485,7 @@ export async function codegen(ast: Ast, { compileOptions, filename }: CodeGenOpt
} }
return; return;
case 'Style': { case 'Style': {
this.remove(); // this will be optimized in a global CSS file; remove so its not accidentally inlined
return; return;
} }
default: default:
@ -481,5 +498,6 @@ export async function codegen(ast: Ast, { compileOptions, filename }: CodeGenOpt
script: script, script: script,
imports: Array.from(importExportStatements), imports: Array.from(importExportStatements),
items, items,
css: css.length ? css.join('\n\n') : undefined,
}; };
} }

View file

@ -10,7 +10,6 @@ import { CompileResult, TransformResult } from '../@types/astro';
import { parse } from '../parser/index.js'; import { parse } from '../parser/index.js';
import { createMarkdownHeadersCollector } from '../micromark-collect-headers.js'; import { createMarkdownHeadersCollector } from '../micromark-collect-headers.js';
import { encodeMarkdown } from '../micromark-encode.js'; import { encodeMarkdown } from '../micromark-encode.js';
import { defaultLogOptions } from '../logger.js';
import { optimize } from './optimize/index.js'; import { optimize } from './optimize/index.js';
import { codegen } from './codegen.js'; import { codegen } from './codegen.js';
@ -75,14 +74,14 @@ async function convertMdToJsx(
const raw = `--- const raw = `---
${imports} ${imports}
${frontmatterData.layout ? `export const __layout = ${JSON.stringify(frontmatterData.layout)};` : ''} ${frontmatterData.layout ? `import {__renderPage as __layout} from '${frontmatterData.layout}';` : 'const __layout = undefined;'}
export const __content = ${stringifiedSetupContext}; export const __content = ${stringifiedSetupContext};
--- ---
<section>${mdHtml}</section>`; <section>${mdHtml}</section>`;
const convertOptions = { compileOptions, filename, fileID }; const convertOptions = { compileOptions, filename, fileID };
return convertAstroToJsx(raw, convertOptions); return await convertAstroToJsx(raw, convertOptions);
} }
type SupportedExtensions = '.astro' | '.md'; type SupportedExtensions = '.astro' | '.md';
@ -94,9 +93,9 @@ async function transformFromSource(
const fileID = path.relative(projectRoot, filename); const fileID = path.relative(projectRoot, filename);
switch (path.extname(filename) as SupportedExtensions) { switch (path.extname(filename) as SupportedExtensions) {
case '.astro': case '.astro':
return convertAstroToJsx(contents, { compileOptions, filename, fileID }); return await convertAstroToJsx(contents, { compileOptions, filename, fileID });
case '.md': case '.md':
return convertMdToJsx(contents, { compileOptions, filename, fileID }); return await convertMdToJsx(contents, { compileOptions, filename, fileID });
default: default:
throw new Error('Not Supported!'); throw new Error('Not Supported!');
} }
@ -108,8 +107,6 @@ export async function compileComponent(
): Promise<CompileResult> { ): Promise<CompileResult> {
const sourceJsx = await transformFromSource(source, { compileOptions, filename, projectRoot }); const sourceJsx = await transformFromSource(source, { compileOptions, filename, projectRoot });
const isPage = path.extname(filename) === '.md' || sourceJsx.items.some((item) => item.name === 'html'); const isPage = path.extname(filename) === '.md' || sourceJsx.items.some((item) => item.name === 'html');
// sort <style> tags first
sourceJsx.items.sort((a, b) => (a.name === 'style' && b.name !== 'style' ? -1 : 0));
// return template // return template
let modJsx = ` let modJsx = `
@ -144,8 +141,7 @@ export async function __renderPage({request, children, props}) {
// find layout, if one was given. // find layout, if one was given.
if (currentChild.layout) { if (currentChild.layout) {
const layoutComponent = (await import('/_astro/layouts/' + currentChild.layout.replace(/.*layouts\\//, "").replace(/\.astro$/, '.js'))); return currentChild.layout({
return layoutComponent.__renderPage({
request, request,
props: {content: currentChild.content}, props: {content: currentChild.content},
children: [childBodyResult], children: [childBodyResult],
@ -162,5 +158,6 @@ export async function __renderPage() { throw new Error("No <html> page element f
return { return {
result: sourceJsx, result: sourceJsx,
contents: modJsx, contents: modJsx,
css: sourceJsx.css,
}; };
} }

View file

@ -26,7 +26,7 @@ const getStyleType: Map<string, StyleType> = new Map([
]); ]);
const SASS_OPTIONS: Partial<sass.Options> = { const SASS_OPTIONS: Partial<sass.Options> = {
outputStyle: 'compressed', outputStyle: process.env.NODE_ENV === 'production' ? 'compressed' : undefined,
}; };
/** HTML tags that should never get scoped classes */ /** HTML tags that should never get scoped classes */
const NEVER_SCOPED_TAGS = new Set<string>(['html', 'head', 'body', 'script', 'style', 'link', 'meta']); const NEVER_SCOPED_TAGS = new Set<string>(['html', 'head', 'body', 'script', 'style', 'link', 'meta']);
@ -95,11 +95,10 @@ async function transformStyle(code: string, { type, filename, scopedClass }: { t
return { css, type: styleType }; return { css, type: styleType };
} }
/** Style optimizer */
export default function ({ filename, fileID }: { filename: string; fileID: string }): Optimizer { export default function ({ filename, fileID }: { filename: string; fileID: string }): Optimizer {
const styleNodes: TemplateNode[] = []; // <style> tags to be updated const styleNodes: TemplateNode[] = []; // <style> tags to be updated
const styleTransformPromises: Promise<StyleTransformResult>[] = []; // async style transform results to be finished in finalize(); const styleTransformPromises: Promise<StyleTransformResult>[] = []; // async style transform results to be finished in finalize();
let rootNode: TemplateNode; // root node which needs <style> tags
const scopedClass = `astro-${hashFromFilename(fileID)}`; // this *should* generate same hash from fileID every time const scopedClass = `astro-${hashFromFilename(fileID)}`; // this *should* generate same hash from fileID every time
return { return {
@ -124,15 +123,7 @@ export default function ({ filename, fileID }: { filename: string; fileID: strin
return; return;
} }
// 2. find the root node to inject the <style> tag in later // 2. add scoped HTML classes
// TODO: remove this when we are injecting <link> tags into <head>
if (node.name === 'head') {
rootNode = node; // If this is <head>, this is what we want. Always take this if found. However, this may not always exist (it wont for Component subtrees).
} else if (!rootNode) {
rootNode = node; // If no <head> (yet), then take the first element we come to and assume its the “root” (but if we find a <head> later, then override this per the above)
}
// 3. add scoped HTML classes
if (NEVER_SCOPED_TAGS.has(node.name)) return; // only continue if this is NOT a <script> tag, etc. if (NEVER_SCOPED_TAGS.has(node.name)) return; // only continue if this is NOT a <script> tag, etc.
// Note: currently we _do_ scope web components/custom elements. This seems correct? // Note: currently we _do_ scope web components/custom elements. This seems correct?
@ -175,10 +166,6 @@ export default function ({ filename, fileID }: { filename: string; fileID: strin
scopedClass, scopedClass,
}) })
); );
// TODO: we should delete the old untransformed <style> node after were done.
// However, the svelte parser left it in ast.css, not ast.html. At the final step, this just gets ignored, so it will be deleted, in a sense.
// If we ever end up scanning ast.css for something else, then well need to actually delete the node (or transform it to the processed version)
}, },
}, },
}, },
@ -186,14 +173,9 @@ export default function ({ filename, fileID }: { filename: string; fileID: strin
async finalize() { async finalize() {
const styleTransforms = await Promise.all(styleTransformPromises); const styleTransforms = await Promise.all(styleTransformPromises);
if (!rootNode) {
throw new Error(`No root node found`); // TODO: remove this eventually; we should always find it, but for now alert if theres a bug in our code
}
// 1. transform <style> tags
styleTransforms.forEach((result, n) => { styleTransforms.forEach((result, n) => {
if (styleNodes[n].attributes) { if (styleNodes[n].attributes) {
// 1b. Inject final CSS // 1. Replace with final CSS
const isHeadStyle = !styleNodes[n].content; const isHeadStyle = !styleNodes[n].content;
if (isHeadStyle) { if (isHeadStyle) {
// Note: <style> tags in <head> have different attributes/rules, because of the parser. Unknown why // Note: <style> tags in <head> have different attributes/rules, because of the parser. Unknown why
@ -202,22 +184,22 @@ export default function ({ filename, fileID }: { filename: string; fileID: strin
styleNodes[n].content.styles = result.css; styleNodes[n].content.styles = result.css;
} }
// 3b. Update <style> attributes // 2. Update <style> attributes
const styleTypeIndex = styleNodes[n].attributes.findIndex(({ name }: any) => name === 'type'); const styleTypeIndex = styleNodes[n].attributes.findIndex(({ name }: any) => name === 'type');
// add type="text/css"
if (styleTypeIndex !== -1) { if (styleTypeIndex !== -1) {
styleNodes[n].attributes[styleTypeIndex].value[0].raw = 'text/css'; styleNodes[n].attributes[styleTypeIndex].value[0].raw = 'text/css';
styleNodes[n].attributes[styleTypeIndex].value[0].data = 'text/css'; styleNodes[n].attributes[styleTypeIndex].value[0].data = 'text/css';
} else { } else {
styleNodes[n].attributes.push({ name: 'type', type: 'Attribute', value: [{ type: 'Text', raw: 'text/css', data: 'text/css' }] }); styleNodes[n].attributes.push({ name: 'type', type: 'Attribute', value: [{ type: 'Text', raw: 'text/css', data: 'text/css' }] });
} }
// remove lang="*"
const styleLangIndex = styleNodes[n].attributes.findIndex(({ name }: any) => name === 'lang'); const styleLangIndex = styleNodes[n].attributes.findIndex(({ name }: any) => name === 'lang');
if (styleLangIndex !== -1) styleNodes[n].attributes.splice(styleLangIndex, 1); if (styleLangIndex !== -1) styleNodes[n].attributes.splice(styleLangIndex, 1);
// TODO: add data-astro for later
// styleNodes[n].attributes.push({ name: 'data-astro', type: 'Attribute', value: true });
} }
}); });
// 2. inject finished <style> tags into root node
// TODO: pull out into <link> tags for deduping
rootNode.children = [...styleNodes, ...(rootNode.children || [])];
}, },
}; };
} }

View file

@ -53,7 +53,7 @@ export interface Parser {
html: Node; html: Node;
css: Node; css: Node;
js: Node; js: Node;
meta_tags: {}; meta_tags: Map<string, string>;
} }
export interface Script extends BaseNode { export interface Script extends BaseNode {

View file

@ -1,8 +1,8 @@
import type { SnowpackDevServer, ServerRuntime as SnowpackServerRuntime, LoadResult as SnowpackLoadResult, SnowpackConfig } from 'snowpack'; import type { SnowpackDevServer, ServerRuntime as SnowpackServerRuntime, SnowpackConfig } from 'snowpack';
import type { AstroConfig } from './@types/astro'; import type { AstroConfig } from './@types/astro';
import type { LogOptions } from './logger'; import type { LogOptions } from './logger';
import type { CompileError } from './parser/utils/error.js'; import type { CompileError } from './parser/utils/error.js';
import { info } from './logger.js'; import { debug, info } from './logger.js';
import { existsSync } from 'fs'; import { existsSync } from 'fs';
import { loadConfiguration, logger as snowpackLogger, startServer as startSnowpackServer } from 'snowpack'; import { loadConfiguration, logger as snowpackLogger, startServer as startSnowpackServer } from 'snowpack';
@ -39,7 +39,6 @@ async function load(config: RuntimeConfig, rawPathname: string | undefined): Pro
const selectedPageLoc = new URL(`./pages/${selectedPage}.astro`, astroRoot); const selectedPageLoc = new URL(`./pages/${selectedPage}.astro`, astroRoot);
const selectedPageMdLoc = new URL(`./pages/${selectedPage}.md`, astroRoot); const selectedPageMdLoc = new URL(`./pages/${selectedPage}.md`, astroRoot);
const selectedPageUrl = `/_astro/pages/${selectedPage}.js`;
// Non-Astro pages (file resources) // Non-Astro pages (file resources)
if (!existsSync(selectedPageLoc) && !existsSync(selectedPageMdLoc)) { if (!existsSync(selectedPageLoc) && !existsSync(selectedPageMdLoc)) {
@ -62,8 +61,10 @@ async function load(config: RuntimeConfig, rawPathname: string | undefined): Pro
} }
} }
for (const url of [`/_astro/pages/${selectedPage}.astro.js`, `/_astro/pages/${selectedPage}.md.js`]) {
try { try {
const mod = await snowpackRuntime.importModule(selectedPageUrl); const mod = await snowpackRuntime.importModule(url);
debug(logging, 'resolve', `${reqPath} -> ${url}`);
let html = (await mod.exports.__renderPage({ let html = (await mod.exports.__renderPage({
request: { request: {
host: fullurl.hostname, host: fullurl.hostname,
@ -76,7 +77,7 @@ async function load(config: RuntimeConfig, rawPathname: string | undefined): Pro
// inject styles // inject styles
// TODO: handle this in compiler // TODO: handle this in compiler
const styleTags = Array.isArray(mod.css) && mod.css.length ? mod.css.reduce((markup, url) => `${markup}\n<link rel="stylesheet" type="text/css" href="${url}" />`, '') : ``; const styleTags = Array.isArray(mod.css) && mod.css.length ? mod.css.reduce((markup, href) => `${markup}\n<link rel="stylesheet" type="text/css" href="${href}" />`, '') : ``;
if (html.indexOf('</head>') !== -1) { if (html.indexOf('</head>') !== -1) {
html = html.replace('</head>', `${styleTags}</head>`); html = html.replace('</head>', `${styleTags}</head>`);
} else { } else {
@ -88,15 +89,19 @@ async function load(config: RuntimeConfig, rawPathname: string | undefined): Pro
contents: html, contents: html,
}; };
} catch (err) { } catch (err) {
switch (err.code) { // if this is a 404, try the next URL (will be caught at the end)
case 'parse-error': { const notFoundError = err.toString().startsWith('Error: Not Found');
if (notFoundError) {
continue;
}
if (err.code === 'parse-error') {
return { return {
statusCode: 500, statusCode: 500,
type: 'parse-error', type: 'parse-error',
error: err, error: err,
}; };
} }
default: {
return { return {
statusCode: 500, statusCode: 500,
type: 'unknown', type: 'unknown',
@ -104,7 +109,13 @@ async function load(config: RuntimeConfig, rawPathname: string | undefined): Pro
}; };
} }
} }
}
// couldnt find match; 404
return {
statusCode: 404,
type: 'unknown',
error: new Error(`Could not locate ${selectedPage}`),
};
} }
export interface AstroRuntime { export interface AstroRuntime {

View file

@ -8,6 +8,16 @@ const StylesSSR = suite('Styles SSR');
let runtime; let runtime;
/** Basic CSS minification; removes some flakiness in testing CSS */
function cssMinify(css) {
return css
.trim() // remove whitespace
.replace(/\n\s*/g, '') // collapse lines
.replace(/\s*\{/g, '{') // collapse selectors
.replace(/:\s*/g, ':') // collapse attributes
.replace(/;}/g, '}'); // collapse block
}
StylesSSR.before(async () => { StylesSSR.before(async () => {
const astroConfig = await loadConfig(new URL('./fixtures/astro-styles-ssr', import.meta.url).pathname); const astroConfig = await loadConfig(new URL('./fixtures/astro-styles-ssr', import.meta.url).pathname);
@ -54,12 +64,15 @@ StylesSSR('CSS Module support in .astro', async () => {
let scopedClass; let scopedClass;
// test 1: <style> tag in <head> is transformed // test 1: <style> tag in <head> is transformed
const css = $('style') const css = cssMinify(
$('style')
.html() .html()
.replace(/\.astro-[A-Za-z0-9-]+/, (match) => { .replace(/\.astro-[A-Za-z0-9-]+/, (match) => {
scopedClass = match; scopedClass = match; // get class hash from result
return match; return match;
}); // remove class hash (should be deterministic / the same every time, but even still dont cause this test to flake) })
);
assert.equal(css, `.wrapper${scopedClass}{margin-left:auto;margin-right:auto;max-width:1200px}`); assert.equal(css, `.wrapper${scopedClass}{margin-left:auto;margin-right:auto;max-width:1200px}`);
// test 2: element received .astro-XXXXXX class (this selector will succeed if transformed correctly) // test 2: element received .astro-XXXXXX class (this selector will succeed if transformed correctly)

View file

@ -1,5 +1,5 @@
--- ---
layout: layouts/content.astro layout: ../layouts/content.astro
title: My Blog Post title: My Blog Post
description: This is a post about some stuff. description: This is a post about some stuff.
import: import: