Skip to content

Commit

Permalink
Extract Astro styles to external stylesheets
Browse files Browse the repository at this point in the history
  • Loading branch information
drwpow committed Mar 30, 2021
1 parent da6ad7a commit 0fa772b
Show file tree
Hide file tree
Showing 9 changed files with 130 additions and 89 deletions.
2 changes: 2 additions & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ module.exports = {
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-use-before-define': 'off',
'@typescript-eslint/no-var-requires': 'off',
'no-shadow': 'error',
'prettier/prettier': 'error',
'prefer-const': 'off',
'prefer-rest-params': 'off',
'require-jsdoc': 'warn',
},
};
7 changes: 7 additions & 0 deletions .github/workflows/nodejs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,10 @@ jobs:
npm test
env:
CI: true
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/setup-node@v1
- run: npm ci
- run: npm run lint
8 changes: 6 additions & 2 deletions snowpack-plugin.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ module.exports = function (snowpackConfig, { resolve, extensions, astroConfig }
knownEntrypoints: ['deepmerge'],
resolve: {
input: ['.astro', '.md'],
output: ['.js'],
output: ['.js', '.css'],
},
async load({ filePath }) {
const { compileComponent } = await transformPromise;
Expand All @@ -21,7 +21,11 @@ module.exports = function (snowpackConfig, { resolve, extensions, astroConfig }
extensions,
};
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;
},
};
};
2 changes: 2 additions & 0 deletions src/@types/astro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@ export interface TransformResult {
script: string;
imports: string[];
items: JsxItem[];
css?: string;
}

export interface CompileResult {
result: TransformResult;
contents: string;
css?: string;
}
34 changes: 26 additions & 8 deletions src/compiler/codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ interface Attribute {
end: number;
type: 'Attribute';
name: string;
value: any;
value: TemplateNode[] | boolean;
}

interface CodeGenOptions {
Expand All @@ -41,7 +41,8 @@ function getAttributes(attrs: Attribute[]): Record<string, string> {
result[attr.name] = JSON.stringify(attr.value);
continue;
}
if (attr.value === false) {
if (attr.value === false || attr.value === undefined) {
// note: attr.value shouldn’t be `undefined`, but a bad transform would cause a compile error here, so prevent that
continue;
}
if (attr.value.length > 1) {
Expand All @@ -59,7 +60,7 @@ function getAttributes(attrs: Attribute[]): Record<string, string> {
')';
continue;
}
const val: TemplateNode = attr.value[0];
const val = attr.value[0];
if (!val) {
result[attr.name] = '(' + val + ')';
continue;
Expand All @@ -72,7 +73,7 @@ function getAttributes(attrs: Attribute[]): Record<string, string> {
result[attr.name] = JSON.stringify(getTextFromAttribute(val));
continue;
default:
throw new Error('UNKNOWN V');
throw new Error(`UNKNOWN: ${val.type}`);
}
}
return result;
Expand Down Expand Up @@ -253,7 +254,7 @@ async function acquireDynamicComponentImports(plugins: Set<ValidExtensionPlugins
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;
await eslexer.init;

Expand Down Expand Up @@ -334,6 +335,21 @@ export async function codegen(ast: Ast, { compileOptions, filename }: CodeGenOpt
let collectionItem: JsxItem | undefined;
let currentItemName: string | undefined;
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 it‘s not accidentally inlined
}
},
});

walk(ast.html, {
enter(node: TemplateNode) {
Expand Down Expand Up @@ -419,9 +435,9 @@ export async function codegen(ast: Ast, { compileOptions, filename }: CodeGenOpt
return;
}
case 'Style': {
const attributes = getAttributes(node.attributes);
items.push({ name: 'style', jsx: `h("style", ${attributes ? generateAttributes(attributes) : 'null'}, ${JSON.stringify(node.content.styles)})` });
break;
css.push(node.content.styles); // if multiple <style> tags, combine together
this.skip();
return;
}
case 'Text': {
const text = getTextFromAttribute(node);
Expand Down Expand Up @@ -469,6 +485,7 @@ export async function codegen(ast: Ast, { compileOptions, filename }: CodeGenOpt
}
return;
case 'Style': {
this.remove(); // this will be optimized in a global CSS file; remove so it‘s not accidentally inlined
return;
}
default:
Expand All @@ -481,5 +498,6 @@ export async function codegen(ast: Ast, { compileOptions, filename }: CodeGenOpt
script: script,
imports: Array.from(importExportStatements),
items,
css: css.length ? css.join('\n\n') : undefined,
};
}
15 changes: 6 additions & 9 deletions src/compiler/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { CompileResult, TransformResult } from '../@types/astro';
import { parse } from '../parser/index.js';
import { createMarkdownHeadersCollector } from '../micromark-collect-headers.js';
import { encodeMarkdown } from '../micromark-encode.js';
import { defaultLogOptions } from '../logger.js';
import { optimize } from './optimize/index.js';
import { codegen } from './codegen.js';

Expand Down Expand Up @@ -75,14 +74,14 @@ async function convertMdToJsx(

const raw = `---
${imports}
${frontmatterData.layout ? `export const __layout = ${JSON.stringify(frontmatterData.layout)};` : ''}
${frontmatterData.layout ? `import {__renderPage as __layout} from ${JSON.stringify('../../' + frontmatterData.layout)};` : 'const __layout = undefined;'}
export const __content = ${stringifiedSetupContext};
---
<section>${mdHtml}</section>`;

const convertOptions = { compileOptions, filename, fileID };

return convertAstroToJsx(raw, convertOptions);
return await convertAstroToJsx(raw, convertOptions);
}

type SupportedExtensions = '.astro' | '.md';
Expand All @@ -94,9 +93,9 @@ async function transformFromSource(
const fileID = path.relative(projectRoot, filename);
switch (path.extname(filename) as SupportedExtensions) {
case '.astro':
return convertAstroToJsx(contents, { compileOptions, filename, fileID });
return await convertAstroToJsx(contents, { compileOptions, filename, fileID });
case '.md':
return convertMdToJsx(contents, { compileOptions, filename, fileID });
return await convertMdToJsx(contents, { compileOptions, filename, fileID });
default:
throw new Error('Not Supported!');
}
Expand All @@ -108,8 +107,6 @@ export async function compileComponent(
): Promise<CompileResult> {
const sourceJsx = await transformFromSource(source, { compileOptions, filename, projectRoot });
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
let modJsx = `
Expand Down Expand Up @@ -144,8 +141,7 @@ export async function __renderPage({request, children, props}) {
// find layout, if one was given.
if (currentChild.layout) {
const layoutComponent = (await import('/_astro/layouts/' + currentChild.layout.replace(/.*layouts\\//, "").replace(/\.astro$/, '.js')));
return layoutComponent.__renderPage({
return currentChild.layout({
request,
props: {content: currentChild.content},
children: [childBodyResult],
Expand All @@ -162,5 +158,6 @@ export async function __renderPage() { throw new Error("No <html> page element f
return {
result: sourceJsx,
contents: modJsx,
css: sourceJsx.css,
};
}
36 changes: 9 additions & 27 deletions src/compiler/optimize/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const getStyleType: Map<string, StyleType> = new Map([
]);

const SASS_OPTIONS: Partial<sass.Options> = {
outputStyle: 'compressed',
outputStyle: process.env.NODE_ENV === 'production' ? 'compressed' : undefined,
};
/** HTML tags that should never get scoped classes */
const NEVER_SCOPED_TAGS = new Set<string>(['html', 'head', 'body', 'script', 'style', 'link', 'meta']);
Expand Down Expand Up @@ -95,11 +95,10 @@ async function transformStyle(code: string, { type, filename, scopedClass }: { t
return { css, type: styleType };
}

/** Style optimizer */
export default function ({ filename, fileID }: { filename: string; fileID: string }): Optimizer {
const styleNodes: TemplateNode[] = []; // <style> tags to be updated
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

return {
Expand All @@ -124,15 +123,7 @@ export default function ({ filename, fileID }: { filename: string; fileID: strin
return;
}

// 2. find the root node to inject the <style> tag in later
// 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 won’t for Component subtrees).
} else if (!rootNode) {
rootNode = node; // If no <head> (yet), then take the first element we come to and assume it‘s the “root” (but if we find a <head> later, then override this per the above)
}

// 3. add scoped HTML classes
// 2. add scoped HTML classes
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?

Expand Down Expand Up @@ -175,25 +166,16 @@ export default function ({ filename, fileID }: { filename: string; fileID: strin
scopedClass,
})
);

// TODO: we should delete the old untransformed <style> node after we’re 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 we’ll need to actually delete the node (or transform it to the processed version)
},
},
},
},
async finalize() {
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 there’s a bug in our code
}

// 1. transform <style> tags
styleTransforms.forEach((result, n) => {
if (styleNodes[n].attributes) {
// 1b. Inject final CSS
// 1. Replace with final CSS
const isHeadStyle = !styleNodes[n].content;
if (isHeadStyle) {
// Note: <style> tags in <head> have different attributes/rules, because of the parser. Unknown why
Expand All @@ -202,22 +184,22 @@ export default function ({ filename, fileID }: { filename: string; fileID: strin
styleNodes[n].content.styles = result.css;
}

// 3b. Update <style> attributes
// 2. Update <style> attributes
const styleTypeIndex = styleNodes[n].attributes.findIndex(({ name }: any) => name === 'type');
// add type="text/css"
if (styleTypeIndex !== -1) {
styleNodes[n].attributes[styleTypeIndex].value[0].raw = 'text/css';
styleNodes[n].attributes[styleTypeIndex].value[0].data = 'text/css';
} else {
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');
if (styleLangIndex !== -1) styleNodes[n].attributes.splice(styleLangIndex, 1);
// 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 || [])];
},
};
}
Loading

0 comments on commit 0fa772b

Please sign in to comment.