Compiler cleanup (#64)

* Compiler cleanup

This is general compiler cleanup, especially around the codegen part. Goals here were too:

1. Make it possible to compile HTML recursively (needed for future astro-in-expressions work) by moving that work into its own function.
1. Get rid of collectionItems and have compiling the HTML return just a source string.

Also not planned, this change gets rid of the different between components and pages. All Astro components compile to the same JavaScript.

* Remove unused node types
This commit is contained in:
Matthew Phillips 2021-04-06 16:14:42 -04:00 committed by GitHub
parent 7240f0d677
commit 72d9ece6db
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 129 additions and 83 deletions

View file

@ -24,7 +24,7 @@ export interface JsxItem {
export interface TransformResult { export interface TransformResult {
script: string; script: string;
imports: string[]; imports: string[];
items: JsxItem[]; html: string;
css?: string; css?: string;
} }

View file

@ -1,6 +1,6 @@
import type { CompileOptions } from '../@types/compiler'; import type { CompileOptions } from '../@types/compiler';
import type { AstroConfig, ValidExtensionPlugins } from '../@types/astro'; import type { AstroConfig, ValidExtensionPlugins } from '../@types/astro';
import type { Ast, TemplateNode } from '../parser/interfaces'; import type { Ast, Script, Style, TemplateNode } from '../parser/interfaces';
import type { JsxItem, TransformResult } from '../@types/astro'; import type { JsxItem, TransformResult } from '../@types/astro';
import eslexer from 'es-module-lexer'; import eslexer from 'es-module-lexer';
@ -262,17 +262,18 @@ async function acquireDynamicComponentImports(plugins: Set<ValidExtensionPlugins
return importMap; return importMap;
} }
/** type Components = Record<string, { type: string; url: string; plugin: string | undefined }>;
* Codegen
* Step 3/3 in Astro SSR. interface CodegenState {
* This is the final pass over a document AST before its converted to an h() function filename: string;
* and handed off to Snowpack to build. components: Components;
* @param {Ast} AST The parsed AST to crawl css: string[];
* @param {object} CodeGenOptions importExportStatements: Set<string>;
*/ dynamicImports: DynamicImportMap;
export async function codegen(ast: Ast, { compileOptions, filename }: CodeGenOptions): Promise<TransformResult> { }
const { extensions = defaultExtensions, astroConfig } = compileOptions;
await eslexer.init; function compileModule(module: Script, state: CodegenState, compileOptions: CompileOptions) {
const { extensions = defaultExtensions } = compileOptions;
const componentImports: ImportDeclaration[] = []; const componentImports: ImportDeclaration[] = [];
const componentProps: VariableDeclarator[] = []; const componentProps: VariableDeclarator[] = [];
@ -280,12 +281,10 @@ export async function codegen(ast: Ast, { compileOptions, filename }: CodeGenOpt
let script = ''; let script = '';
let propsStatement = ''; let propsStatement = '';
const importExportStatements: Set<string> = new Set();
const components: Record<string, { type: string; url: string; plugin: string | undefined }> = {};
const componentPlugins = new Set<ValidExtensionPlugins>(); const componentPlugins = new Set<ValidExtensionPlugins>();
if (ast.module) { if (module) {
const program = babelParser.parse(ast.module.content, { const program = babelParser.parse(module.content, {
sourceType: 'module', sourceType: 'module',
plugins: ['jsx', 'typescript', 'topLevelAwait'], plugins: ['jsx', 'typescript', 'topLevelAwait'],
}).program; }).program;
@ -317,7 +316,7 @@ export async function codegen(ast: Ast, { compileOptions, filename }: CodeGenOpt
const componentType = path.posix.extname(importUrl); const componentType = path.posix.extname(importUrl);
const componentName = path.posix.basename(importUrl, componentType); const componentName = path.posix.basename(importUrl, componentType);
const plugin = extensions[componentType] || defaultExtensions[componentType]; const plugin = extensions[componentType] || defaultExtensions[componentType];
components[componentName] = { state.components[componentName] = {
type: componentType, type: componentType,
plugin, plugin,
url: importUrl, url: importUrl,
@ -325,10 +324,10 @@ export async function codegen(ast: Ast, { compileOptions, filename }: CodeGenOpt
if (plugin) { if (plugin) {
componentPlugins.add(plugin); componentPlugins.add(plugin);
} }
importExportStatements.add(ast.module.content.slice(componentImport.start!, componentImport.end!)); state.importExportStatements.add(module.content.slice(componentImport.start!, componentImport.end!));
} }
for (const componentImport of componentExports) { for (const componentImport of componentExports) {
importExportStatements.add(ast.module.content.slice(componentImport.start!, componentImport.end!)); state.importExportStatements.add(module.content.slice(componentImport.start!, componentImport.end!));
} }
if (componentProps.length > 0) { if (componentProps.length > 0) {
@ -345,18 +344,14 @@ export async function codegen(ast: Ast, { compileOptions, filename }: CodeGenOpt
script = propsStatement + babelGenerator(program).code; script = propsStatement + babelGenerator(program).code;
} }
const dynamicImports = await acquireDynamicComponentImports(componentPlugins, compileOptions.resolve); return { script, componentPlugins };
}
let items: JsxItem[] = []; function compileCss(style: Style, state: CodegenState) {
let collectionItem: JsxItem | undefined; walk(style, {
let currentItemName: string | undefined;
let currentDepth = 0;
let css: string[] = [];
walk(ast.css, {
enter(node: TemplateNode) { enter(node: TemplateNode) {
if (node.type === 'Style') { if (node.type === 'Style') {
css.push(node.content.styles); // if multiple <style> tags, combine together state.css.push(node.content.styles); // if multiple <style> tags, combine together
this.skip(); this.skip();
} }
}, },
@ -366,8 +361,14 @@ export async function codegen(ast: Ast, { compileOptions, filename }: CodeGenOpt
} }
}, },
}); });
}
walk(ast.html, { function compileHtml(enterNode: TemplateNode, state: CodegenState, compileOptions: CompileOptions) {
const { components, css, importExportStatements, dynamicImports, filename } = state;
const { astroConfig } = compileOptions;
let outSource = '';
walk(enterNode, {
enter(node: TemplateNode) { enter(node: TemplateNode) {
switch (node.type) { switch (node.type) {
case 'MustacheTag': case 'MustacheTag':
@ -394,19 +395,13 @@ export async function codegen(ast: Ast, { compileOptions, filename }: CodeGenOpt
code = code.slice(0, astroComponent.index + 2) + wrapper + code.slice(astroComponent.index + astroComponent[0].length - 1); code = code.slice(0, astroComponent.index + 2) + wrapper + code.slice(astroComponent.index + astroComponent[0].length - 1);
} }
} }
collectionItem!.jsx += `,(${code.trim().replace(/\;$/, '')})`; outSource += `,(${code.trim().replace(/\;$/, '')})`;
this.skip(); this.skip();
return; return;
case 'Comment': case 'Comment':
return; return;
case 'Fragment': case 'Fragment':
// Ignore if its the top level fragment
// This should be cleaned up, but right now this is how the old thing worked
if (!collectionItem) {
return;
}
break; break;
case 'Slot': case 'Slot':
case 'Head': case 'Head':
case 'InlineComponent': case 'InlineComponent':
@ -417,20 +412,15 @@ export async function codegen(ast: Ast, { compileOptions, filename }: CodeGenOpt
throw new Error('AHHHH'); throw new Error('AHHHH');
} }
const attributes = getAttributes(node.attributes); const attributes = getAttributes(node.attributes);
currentDepth++;
currentItemName = name; outSource += outSource === '' ? '' : ',';
if (!collectionItem) {
collectionItem = { name, jsx: '' };
items.push(collectionItem);
}
collectionItem.jsx += collectionItem.jsx === '' ? '' : ',';
if (node.type === 'Slot') { if (node.type === 'Slot') {
collectionItem.jsx += `(children`; outSource += `(children`;
return; return;
} }
const COMPONENT_NAME_SCANNER = /^[A-Z]/; const COMPONENT_NAME_SCANNER = /^[A-Z]/;
if (!COMPONENT_NAME_SCANNER.test(name)) { if (!COMPONENT_NAME_SCANNER.test(name)) {
collectionItem.jsx += `h("${name}", ${attributes ? generateAttributes(attributes) : 'null'}`; outSource += `h("${name}", ${attributes ? generateAttributes(attributes) : 'null'}`;
return; return;
} }
const [componentName, componentKind] = name.split(':'); const [componentName, componentKind] = name.split(':');
@ -443,7 +433,7 @@ export async function codegen(ast: Ast, { compileOptions, filename }: CodeGenOpt
importExportStatements.add(wrapperImport); importExportStatements.add(wrapperImport);
} }
collectionItem.jsx += `h(${wrapper}, ${attributes ? generateAttributes(attributes) : 'null'}`; outSource += `h(${wrapper}, ${attributes ? generateAttributes(attributes) : 'null'}`;
return; return;
} }
case 'Attribute': { case 'Attribute': {
@ -460,14 +450,7 @@ export async function codegen(ast: Ast, { compileOptions, filename }: CodeGenOpt
if (!text.trim()) { if (!text.trim()) {
return; return;
} }
if (!collectionItem) { outSource += ',' + JSON.stringify(text);
throw new Error('Not possible! TEXT:' + text);
}
if (currentItemName === 'script' || currentItemName === 'code') {
collectionItem.jsx += ',' + JSON.stringify(text);
return;
}
collectionItem.jsx += ',' + JSON.stringify(text);
return; return;
} }
default: default:
@ -482,23 +465,14 @@ export async function codegen(ast: Ast, { compileOptions, filename }: CodeGenOpt
case 'Comment': case 'Comment':
return; return;
case 'Fragment': case 'Fragment':
if (!collectionItem) { return;
return;
}
case 'Slot': case 'Slot':
case 'Head': case 'Head':
case 'Body': case 'Body':
case 'Title': case 'Title':
case 'Element': case 'Element':
case 'InlineComponent': case 'InlineComponent':
if (!collectionItem) { outSource += ')';
throw new Error('Not possible! CLOSE ' + node.name);
}
collectionItem.jsx += ')';
currentDepth--;
if (currentDepth === 0) {
collectionItem = undefined;
}
return; return;
case 'Style': { case 'Style': {
this.remove(); // this will be optimized in a global CSS file; remove so its not accidentally inlined this.remove(); // this will be optimized in a global CSS file; remove so its not accidentally inlined
@ -510,10 +484,39 @@ export async function codegen(ast: Ast, { compileOptions, filename }: CodeGenOpt
}, },
}); });
return outSource;
}
/**
* Codegen
* Step 3/3 in Astro SSR.
* This is the final pass over a document AST before its converted to an h() function
* and handed off to Snowpack to build.
* @param {Ast} AST The parsed AST to crawl
* @param {object} CodeGenOptions
*/
export async function codegen(ast: Ast, { compileOptions, filename }: CodeGenOptions): Promise<TransformResult> {
await eslexer.init;
const state: CodegenState = {
filename,
components: {},
css: [],
importExportStatements: new Set(),
dynamicImports: new Map()
};
const { script, componentPlugins } = compileModule(ast.module, state, compileOptions);
state.dynamicImports = await acquireDynamicComponentImports(componentPlugins, compileOptions.resolve);
compileCss(ast.css, state);
const html = compileHtml(ast.html, state, compileOptions);
return { return {
script: script, script: script,
imports: Array.from(importExportStatements), imports: Array.from(state.importExportStatements),
items, html,
css: css.length ? css.join('\n\n') : undefined, css: state.css.length ? state.css.join('\n\n') : undefined,
}; };
} }

View file

@ -115,27 +115,23 @@ export async function compileComponent(
source: string, source: string,
{ compileOptions, filename, projectRoot }: { compileOptions: CompileOptions; filename: string; projectRoot: string } { compileOptions, filename, projectRoot }: { compileOptions: CompileOptions; filename: string; projectRoot: string }
): Promise<CompileResult> { ): Promise<CompileResult> {
const sourceJsx = await transformFromSource(source, { compileOptions, filename, projectRoot }); const result = await transformFromSource(source, { compileOptions, filename, projectRoot });
const isPage = path.extname(filename) === '.md' || sourceJsx.items.some((item) => item.name === 'html');
// return template // return template
let modJsx = ` let modJsx = `
import fetch from 'node-fetch'; import fetch from 'node-fetch';
// <script astro></script> // <script astro></script>
${sourceJsx.imports.join('\n')} ${result.imports.join('\n')}
// \`__render()\`: Render the contents of the Astro module. // \`__render()\`: Render the contents of the Astro module.
import { h, Fragment } from '${internalImport('h.js')}'; import { h, Fragment } from '${internalImport('h.js')}';
async function __render(props, ...children) { async function __render(props, ...children) {
${sourceJsx.script} ${result.script}
return h(Fragment, null, ${sourceJsx.items.map(({ jsx }) => jsx).join(',')}); return h(Fragment, null, ${result.html});
} }
export default __render; export default __render;
`;
if (isPage) {
modJsx += `
// \`__renderPage()\`: Render the contents of the Astro module as a page. This is a special flow, // \`__renderPage()\`: Render the contents of the Astro module as a page. This is a special flow,
// triggered by loading a component directly by URL. // triggered by loading a component directly by URL.
export async function __renderPage({request, children, props}) { export async function __renderPage({request, children, props}) {
@ -159,14 +155,10 @@ export async function __renderPage({request, children, props}) {
return childBodyResult; return childBodyResult;
};\n`; };\n`;
} else {
modJsx += `
export async function __renderPage() { throw new Error("No <html> page element found!"); }\n`;
}
return { return {
result: sourceJsx, result,
contents: modJsx, contents: modJsx,
css: sourceJsx.css, css: result.css,
}; };
} }

18
test/astro-expr.test.js Normal file
View file

@ -0,0 +1,18 @@
import { suite } from 'uvu';
import * as assert from 'uvu/assert';
import { doc } from './test-utils.js';
import { setup } from './helpers.js';
const Expressions = suite('Expressions');
setup(Expressions, './fixtures/astro-expr');
Expressions('Can load page', async ({ runtime }) => {
const result = await runtime.load('/');
console.log(result)
assert.equal(result.statusCode, 200);
console.log(result.contents);
});
Expressions.run();

View file

@ -0,0 +1,6 @@
export default {
extensions: {
'.jsx': 'preact'
}
}

View file

@ -0,0 +1,5 @@
import { h } from 'preact';
export default function({ name }) {
return <div>{name}</div>
}

View file

@ -0,0 +1,22 @@
---
import Color from '../components/Color.jsx';
let title = 'My Site';
const colors = ['red', 'yellow', 'blue']
---
<html lang="en">
<head>
<title>My site</title>
</head>
<body>
<h1>{title}</h1>
{colors.map(color => (
<div>
<Color name={color} />
</div>
))}
</body>
</html>