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:
parent
7240f0d677
commit
72d9ece6db
7 changed files with 129 additions and 83 deletions
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 it‘s 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 it‘s not accidentally inlined
|
this.remove(); // this will be optimized in a global CSS file; remove so it‘s 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 it‘s 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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
18
test/astro-expr.test.js
Normal 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();
|
6
test/fixtures/astro-expr/astro.config.mjs
vendored
Normal file
6
test/fixtures/astro-expr/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
|
||||||
|
export default {
|
||||||
|
extensions: {
|
||||||
|
'.jsx': 'preact'
|
||||||
|
}
|
||||||
|
}
|
5
test/fixtures/astro-expr/astro/components/Color.jsx
vendored
Normal file
5
test/fixtures/astro-expr/astro/components/Color.jsx
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import { h } from 'preact';
|
||||||
|
|
||||||
|
export default function({ name }) {
|
||||||
|
return <div>{name}</div>
|
||||||
|
}
|
22
test/fixtures/astro-expr/astro/pages/index.astro
vendored
Normal file
22
test/fixtures/astro-expr/astro/pages/index.astro
vendored
Normal 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>
|
Loading…
Reference in a new issue