diff options
author | 2021-03-25 14:06:08 -0400 | |
---|---|---|
committer | 2021-03-25 14:06:08 -0400 | |
commit | 3db595937719b89956c594e4a76ee68ae8de098a (patch) | |
tree | e463889925f71539f28730f957b195b1806b3cb0 /src | |
parent | 18e7cc5af903543ac6f46780bfea67c13c6517df (diff) | |
download | astro-3db595937719b89956c594e4a76ee68ae8de098a.tar.gz astro-3db595937719b89956c594e4a76ee68ae8de098a.tar.zst astro-3db595937719b89956c594e4a76ee68ae8de098a.zip |
First pass at the build (#27)
This updates `astro build` to do a production build. It works! No optimizations yet.
Diffstat (limited to 'src')
-rw-r--r-- | src/@types/compiler.ts | 2 | ||||
-rw-r--r-- | src/build.ts | 83 | ||||
-rw-r--r-- | src/cli.ts | 8 | ||||
-rw-r--r-- | src/compiler/codegen.ts | 80 | ||||
-rw-r--r-- | src/compiler/index.ts | 4 | ||||
-rw-r--r-- | src/compiler/optimize/index.ts | 8 | ||||
-rw-r--r-- | src/dev.ts | 2 | ||||
-rw-r--r-- | src/frontend/render/preact.ts | 9 | ||||
-rw-r--r-- | src/generate.ts | 61 | ||||
-rw-r--r-- | src/parser/parse/read/expression.ts | 2 | ||||
-rw-r--r-- | src/runtime.ts | 29 |
11 files changed, 179 insertions, 109 deletions
diff --git a/src/@types/compiler.ts b/src/@types/compiler.ts index 4e0ee6250..916be22cb 100644 --- a/src/@types/compiler.ts +++ b/src/@types/compiler.ts @@ -3,6 +3,6 @@ import type { ValidExtensionPlugins } from './astro'; export interface CompileOptions { logging: LogOptions; - resolve: (p: string) => string; + resolve: (p: string) => Promise<string>; extensions?: Record<string, ValidExtensionPlugins>; } diff --git a/src/build.ts b/src/build.ts new file mode 100644 index 000000000..63cdea87d --- /dev/null +++ b/src/build.ts @@ -0,0 +1,83 @@ +import type { AstroConfig } from './@types/astro'; +import { defaultLogOptions, LogOptions } from './logger'; + +import { + loadConfiguration, + startServer as startSnowpackServer, + build as snowpackBuild } from 'snowpack'; +import { promises as fsPromises } from 'fs'; +import { relative as pathRelative } from 'path'; +import { defaultLogDestination, error } from './logger.js'; +import { createRuntime } from './runtime.js'; + +const { mkdir, readdir, stat, writeFile } = fsPromises; + +const logging: LogOptions = { + level: 'debug', + dest: defaultLogDestination, +}; + +async function* allPages(root: URL): AsyncGenerator<URL, void, unknown> { + for (const filename of await readdir(root)) { + const fullpath = new URL(filename, root); + const info = await stat(fullpath); + + if (info.isDirectory()) { + yield* allPages(new URL(fullpath + '/')); + } else { + if(/\.(astro|md)$/.test(fullpath.pathname)) { + yield fullpath; + } + } + } +} + +export async function build(astroConfig: AstroConfig): Promise<0 | 1> { + const { projectRoot, astroRoot } = astroConfig; + const pageRoot = new URL('./pages/', astroRoot); + const dist = new URL(astroConfig.dist + '/', projectRoot); + + const runtimeLogging: LogOptions = { + level: 'error', + dest: defaultLogDestination + }; + + const runtime = await createRuntime(astroConfig, { logging: runtimeLogging, env: 'build' }); + const { snowpackConfig } = runtime.runtimeConfig; + + try { + const result = await snowpackBuild({ + config: snowpackConfig, + lockfile: null + }); + + } catch(err) { + error(logging, 'build', err); + return 1; + } + + for await (const filepath of allPages(pageRoot)) { + const rel = pathRelative(astroRoot.pathname + '/pages', filepath.pathname); // pages/index.astro + const pagePath = `/${rel.replace(/\.(astro|md)/, '')}`; + + try { + const outPath = new URL('./' + rel.replace(/\.(astro|md)/, '.html'), dist); + const outFolder = new URL('./', outPath); + const result = await runtime.load(pagePath); + + if(result.statusCode !== 200) { + error(logging, 'generate', result.error || result.statusCode); + //return 1; + } else { + await mkdir(outFolder, { recursive: true }); + await writeFile(outPath, result.contents, 'utf-8'); + } + } catch (err) { + error(logging, 'generate', err); + return 1; + } + } + + await runtime.shutdown(); + return 0; +}
\ No newline at end of file diff --git a/src/cli.ts b/src/cli.ts index 0a5c9612d..9d1815256 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -5,10 +5,14 @@ import { promises as fsPromises } from 'fs'; import yargs from 'yargs-parser'; import { loadConfig } from './config.js'; -import generate from './generate.js'; +import {build} from './build.js'; import devServer from './dev.js'; const { readFile } = fsPromises; +const buildAndExit = async (...args: Parameters<typeof build>) => { + const ret = await build(...args); + process.exit(ret); +} type Arguments = yargs.Arguments; type cliState = 'help' | 'version' | 'dev' | 'build'; @@ -61,7 +65,7 @@ async function runCommand(rawRoot: string, cmd: (a: AstroConfig) => Promise<void } const cmdMap = new Map([ - ['build', generate], + ['build', buildAndExit], ['dev', devServer], ]); diff --git a/src/compiler/codegen.ts b/src/compiler/codegen.ts index 52249fd77..58c7a6c9d 100644 --- a/src/compiler/codegen.ts +++ b/src/compiler/codegen.ts @@ -11,6 +11,7 @@ import babelParser from '@babel/parser'; import _babelGenerator from '@babel/generator'; import traverse from '@babel/traverse'; import { ImportDeclaration,ExportNamedDeclaration, VariableDeclarator, Identifier, VariableDeclaration } from '@babel/types'; +import { type } from 'node:os'; const babelGenerator: typeof _babelGenerator = // @ts-ignore @@ -100,6 +101,7 @@ function generateAttributes(attrs: Record<string, string>): string { interface ComponentInfo { type: string; url: string; + plugin: string | undefined; } const defaultExtensions: Readonly<Record<string, ValidExtensionPlugins>> = { @@ -109,13 +111,14 @@ const defaultExtensions: Readonly<Record<string, ValidExtensionPlugins>> = { '.svelte': 'svelte', }; -function getComponentWrapper(_name: string, { type, url }: ComponentInfo, compileOptions: CompileOptions) { - const { resolve, extensions = defaultExtensions } = compileOptions; +type DynamicImportMap = Map< + 'vue' | 'react' | 'react-dom' | 'preact', + string +>; +function getComponentWrapper(_name: string, { type, plugin, url }: ComponentInfo, dynamicImports: DynamicImportMap) { const [name, kind] = _name.split(':'); - const plugin = extensions[type] || defaultExtensions[type]; - if (!plugin) { throw new Error(`No supported plugin found for extension ${type}`); } @@ -133,7 +136,7 @@ function getComponentWrapper(_name: string, { type, url }: ComponentInfo, compil case 'preact': { if (kind === 'dynamic') { return { - wrapper: `__preact_dynamic(${name}, new URL(${JSON.stringify(url.replace(/\.[^.]+$/, '.js'))}, \`http://TEST\${import.meta.url}\`).pathname, '${resolve('preact')}')`, + wrapper: `__preact_dynamic(${name}, new URL(${JSON.stringify(url.replace(/\.[^.]+$/, '.js'))}, \`http://TEST\${import.meta.url}\`).pathname, '${dynamicImports.get('preact')!}')`, wrapperImport: `import {__preact_dynamic} from '${internalImport('render/preact.js')}';`, }; } else { @@ -146,9 +149,9 @@ function getComponentWrapper(_name: string, { type, url }: ComponentInfo, compil case 'react': { if (kind === 'dynamic') { return { - wrapper: `__react_dynamic(${name}, new URL(${JSON.stringify(url.replace(/\.[^.]+$/, '.js'))}, \`http://TEST\${import.meta.url}\`).pathname, '${resolve( + wrapper: `__react_dynamic(${name}, new URL(${JSON.stringify(url.replace(/\.[^.]+$/, '.js'))}, \`http://TEST\${import.meta.url}\`).pathname, '${dynamicImports.get( 'react' - )}', '${resolve('react-dom')}')`, + )!}', '${dynamicImports.get('react-dom')!}')`, wrapperImport: `import {__react_dynamic} from '${internalImport('render/react.js')}';`, }; } else { @@ -174,7 +177,7 @@ function getComponentWrapper(_name: string, { type, url }: ComponentInfo, compil case 'vue': { if (kind === 'dynamic') { return { - wrapper: `__vue_dynamic(${name}, new URL(${JSON.stringify(url.replace(/\.[^.]+$/, '.vue.js'))}, \`http://TEST\${import.meta.url}\`).pathname, '${resolve('vue')}')`, + wrapper: `__vue_dynamic(${name}, new URL(${JSON.stringify(url.replace(/\.[^.]+$/, '.vue.js'))}, \`http://TEST\${import.meta.url}\`).pathname, '${dynamicImports.get('vue')!}')`, wrapperImport: `import {__vue_dynamic} from '${internalImport('render/vue.js')}';`, }; } else { @@ -186,23 +189,10 @@ function getComponentWrapper(_name: string, { type, url }: ComponentInfo, compil }; } } - } - throw new Error('Unknown Component Type: ' + name); -} - -function compileScriptSafe(raw: string): string { - let compiledCode = compileExpressionSafe(raw); - // esbuild treeshakes unused imports. In our case these are components, so let's keep them. - const imports = eslexer - .parse(raw)[0] - .filter(({ d }) => d === -1) - .map((i) => raw.substring(i.ss, i.se)); - for (let importStatement of imports) { - if (!compiledCode.includes(importStatement)) { - compiledCode = importStatement + '\n' + compiledCode; + default: { + throw new Error(`Unknown component type`); } } - return compiledCode; } function compileExpressionSafe(raw: string): string { @@ -215,7 +205,30 @@ function compileExpressionSafe(raw: string): string { return code; } +async function acquireDynamicComponentImports(plugins: Set<ValidExtensionPlugins>, resolve: (s: string) => Promise<string>): Promise<DynamicImportMap> { + const importMap: DynamicImportMap = new Map(); + for(let plugin of plugins) { + switch(plugin) { + case 'vue': { + importMap.set('vue', await resolve('vue')); + break; + } + case 'react': { + importMap.set('react', await resolve('react')); + importMap.set('react-dom', await resolve('react-dom')); + break; + } + case 'preact': { + importMap.set('preact', await resolve('preact')); + break; + } + } + } + return importMap; +} + export async function codegen(ast: Ast, { compileOptions }: CodeGenOptions): Promise<TransformResult> { + const { extensions = defaultExtensions } = compileOptions; await eslexer.init; const componentImports: ImportDeclaration[] = []; @@ -225,7 +238,8 @@ export async function codegen(ast: Ast, { compileOptions }: CodeGenOptions): Pro let script = ''; let propsStatement: string = ''; const importExportStatements: Set<string> = new Set(); - const components: Record<string, { type: string; url: string }> = {}; + const components: Record<string, { type: string; url: string, plugin: string | undefined }> = {}; + const componentPlugins = new Set<ValidExtensionPlugins>(); if (ast.module) { const program = babelParser.parse(ast.module.content, { @@ -245,7 +259,7 @@ export async function codegen(ast: Ast, { compileOptions }: CodeGenOptions): Pro if (node.type === 'ExportNamedDeclaration' && node.declaration?.type === 'VariableDeclaration') { const declaration = node.declaration.declarations[0]; if ((declaration.id as Identifier).name === '__layout' || (declaration.id as Identifier).name === '__content') { - componentExports.push(node); + componentExports.push(node); } else { componentProps.push(declaration); } @@ -259,7 +273,15 @@ export async function codegen(ast: Ast, { compileOptions }: CodeGenOptions): Pro const importUrl = componentImport.source.value; const componentType = path.posix.extname(importUrl); const componentName = path.posix.basename(importUrl, componentType); - components[componentName] = { type: componentType, url: importUrl }; + const plugin = extensions[componentType] || defaultExtensions[componentType]; + components[componentName] = { + type: componentType, + plugin, + url: importUrl + }; + if(plugin) { + componentPlugins.add(plugin); + } importExportStatements.add(ast.module.content.slice(componentImport.start!, componentImport.end!)); } for (const componentImport of componentExports) { @@ -280,6 +302,8 @@ export async function codegen(ast: Ast, { compileOptions }: CodeGenOptions): Pro script = propsStatement + babelGenerator(program).code; } + const dynamicImports = await acquireDynamicComponentImports(componentPlugins, compileOptions.resolve); + let items: JsxItem[] = []; let collectionItem: JsxItem | undefined; let currentItemName: string | undefined; @@ -304,7 +328,7 @@ export async function codegen(ast: Ast, { compileOptions }: CodeGenOptions): Pro if (!components[componentName]) { throw new Error(`Unknown Component: ${componentName}`); } - const { wrapper, wrapperImport } = getComponentWrapper(name, components[componentName], compileOptions); + const { wrapper, wrapperImport } = getComponentWrapper(name, components[componentName], dynamicImports); if (wrapperImport) { importExportStatements.add(wrapperImport); } @@ -356,7 +380,7 @@ export async function codegen(ast: Ast, { compileOptions }: CodeGenOptions): Pro if (!componentImportData) { throw new Error(`Unknown Component: ${componentName}`); } - const { wrapper, wrapperImport } = getComponentWrapper(name, components[componentName], compileOptions); + const { wrapper, wrapperImport } = getComponentWrapper(name, components[componentName], dynamicImports); if (wrapperImport) { importExportStatements.add(wrapperImport); } diff --git a/src/compiler/index.ts b/src/compiler/index.ts index e09664a19..fea6b8a29 100644 --- a/src/compiler/index.ts +++ b/src/compiler/index.ts @@ -15,12 +15,12 @@ import { codegen } from './codegen.js'; interface CompileOptions { logging: LogOptions; - resolve: (p: string) => string; + resolve: (p: string) => Promise<string>; } const defaultCompileOptions: CompileOptions = { logging: defaultLogOptions, - resolve: (p: string) => p, + resolve: (p: string) => Promise.resolve(p), }; function internalImport(internalPath: string) { diff --git a/src/compiler/optimize/index.ts b/src/compiler/optimize/index.ts index 4f6e54fa5..a0291954b 100644 --- a/src/compiler/optimize/index.ts +++ b/src/compiler/optimize/index.ts @@ -73,7 +73,13 @@ export async function optimize(ast: Ast, opts: OptimizeOptions) { const cssVisitors = createVisitorCollection(); const finalizers: Array<() => Promise<void>> = []; - collectVisitors(optimizeStyles(opts), htmlVisitors, cssVisitors, finalizers); + const optimizers = [ + optimizeStyles(opts) + ]; + + for(const optimizer of optimizers) { + collectVisitors(optimizer, htmlVisitors, cssVisitors, finalizers); + } walkAstWithVisitors(ast.css, cssVisitors); walkAstWithVisitors(ast.html, htmlVisitors); diff --git a/src/dev.ts b/src/dev.ts index efa7f1f6c..19bfa6530 100644 --- a/src/dev.ts +++ b/src/dev.ts @@ -21,7 +21,7 @@ const logging: LogOptions = { export default async function (astroConfig: AstroConfig) { const { projectRoot } = astroConfig; - const runtime = await createRuntime(astroConfig, logging); + const runtime = await createRuntime(astroConfig, { logging, env: 'dev' }); const server = http.createServer(async (req, res) => { const result = await runtime.load(req.url); diff --git a/src/frontend/render/preact.ts b/src/frontend/render/preact.ts index 3b9e1e6d8..50bb9344e 100644 --- a/src/frontend/render/preact.ts +++ b/src/frontend/render/preact.ts @@ -1,10 +1,13 @@ -import render from 'preact-render-to-string'; -import { h } from 'preact'; +import renderToString from 'preact-render-to-string'; +import { h, render } from 'preact'; import type { Component } from 'preact'; +// This prevents tree-shaking of render. +Function.prototype(render); + export function __preact_static(PreactComponent: Component) { return (attrs: Record<string, any>, ...children: any): string => { - let html = render( + let html = renderToString( h( PreactComponent as any, // Preact's types seem wrong... attrs, diff --git a/src/generate.ts b/src/generate.ts deleted file mode 100644 index 4a31cc291..000000000 --- a/src/generate.ts +++ /dev/null @@ -1,61 +0,0 @@ -import type { AstroConfig } from './@types/astro'; -import { loadConfiguration, startServer as startSnowpackServer } from 'snowpack'; -import { promises as fsPromises } from 'fs'; -import { relative as pathRelative } from 'path'; - -const { mkdir, readdir, stat, writeFile } = fsPromises; - -async function* allPages(root: URL): AsyncGenerator<URL, void, unknown> { - for (const filename of await readdir(root)) { - const fullpath = new URL(filename, root); - const info = await stat(fullpath); - - if (info.isDirectory()) { - yield* allPages(new URL(fullpath + '/')); - } else { - yield fullpath; - } - } -} - -export default async function (astroConfig: AstroConfig) { - const { projectRoot, astroRoot } = astroConfig; - const pageRoot = new URL('./pages/', astroRoot); - const dist = new URL(astroConfig.dist + '/', projectRoot); - - const configPath = new URL('./snowpack.config.js', projectRoot).pathname; - const config = await loadConfiguration( - { - root: projectRoot.pathname, - devOptions: { open: 'none', output: 'stream' }, - }, - configPath - ); - const snowpack = await startSnowpackServer({ - config, - lockfile: null, // TODO should this be required? - }); - - const runtime = snowpack.getServerRuntime(); - - for await (const filepath of allPages(pageRoot)) { - const rel = pathRelative(astroRoot.pathname, filepath.pathname); // pages/index.astro - const pagePath = `/_astro/${rel.replace(/\.(astro|md)/, '.js')}`; - - try { - const outPath = new URL('./' + rel.replace(/\.(astro|md)/, '.html'), dist); - const outFolder = new URL('./', outPath); - const mod = await runtime.importModule(pagePath); - const html = await mod.exports.default({}); - - await mkdir(outFolder, { recursive: true }); - await writeFile(outPath, html, 'utf-8'); - } catch (err) { - console.error('Unable to generate page', rel); - console.error(err); - } - } - - await snowpack.shutdown(); - process.exit(0); -} diff --git a/src/parser/parse/read/expression.ts b/src/parser/parse/read/expression.ts index f691f4772..580c5d62b 100644 --- a/src/parser/parse/read/expression.ts +++ b/src/parser/parse/read/expression.ts @@ -1,7 +1,6 @@ import { parse_expression_at } from '../acorn.js'; import { Parser } from '../index.js'; import { whitespace } from '../../utils/patterns.js'; -// import { Node } from 'estree'; // @ts-ignore export default function read_expression(parser: Parser): string { @@ -35,7 +34,6 @@ export default function read_expression(parser: Parser): string { parser.index = index; return parser.template.substring(start, index); - // return node as Node; } catch (err) { parser.acorn_error(err); } diff --git a/src/runtime.ts b/src/runtime.ts index 4b5d51f07..5dd391fdd 100644 --- a/src/runtime.ts +++ b/src/runtime.ts @@ -1,4 +1,4 @@ -import type { SnowpackDevServer, ServerRuntime as SnowpackServerRuntime, LoadResult as SnowpackLoadResult } from 'snowpack'; +import type { SnowpackDevServer, ServerRuntime as SnowpackServerRuntime, LoadResult as SnowpackLoadResult, SnowpackConfig } from 'snowpack'; import type { AstroConfig } from './@types/astro'; import type { LogOptions } from './logger'; import type { CompileError } from './parser/utils/error.js'; @@ -14,6 +14,7 @@ interface RuntimeConfig { logging: LogOptions; snowpack: SnowpackDevServer; snowpackRuntime: SnowpackServerRuntime; + snowpackConfig: SnowpackConfig; } type LoadResultSuccess = { @@ -96,24 +97,34 @@ async function load(config: RuntimeConfig, rawPathname: string | undefined): Pro } } -export async function createRuntime(astroConfig: AstroConfig, logging: LogOptions) { +interface RuntimeOptions { + logging: LogOptions; + env: 'dev' | 'build' +} + +export async function createRuntime(astroConfig: AstroConfig, { env, logging }: RuntimeOptions) { const { projectRoot, astroRoot, extensions } = astroConfig; const internalPath = new URL('./frontend/', import.meta.url); - // Workaround for SKY-251 + let snowpack: SnowpackDevServer; const astroPlugOptions: { - resolve?: (s: string) => string; + resolve?: (s: string) => Promise<string>; extensions?: Record<string, string>; - } = { extensions }; - if (existsSync(new URL('./package-lock.json', projectRoot))) { + } = { + extensions, + resolve: env === 'dev' ? + async (pkgName: string) => snowpack.getUrlForPackage(pkgName) : + async (pkgName: string) => `/_snowpack/pkg/${pkgName}.js` + }; + /*if (existsSync(new URL('./package-lock.json', projectRoot))) { const pkgLockStr = await readFile(new URL('./package-lock.json', projectRoot), 'utf-8'); const pkgLock = JSON.parse(pkgLockStr); astroPlugOptions.resolve = (pkgName: string) => { const ver = pkgLock.dependencies[pkgName].version; return `/_snowpack/pkg/${pkgName}.v${ver}.js`; }; - } + }*/ const snowpackConfig = await loadConfiguration({ root: projectRoot.pathname, @@ -132,7 +143,7 @@ export async function createRuntime(astroConfig: AstroConfig, logging: LogOption external: ['@vue/server-renderer', 'node-fetch'], }, }); - const snowpack = await startSnowpackServer({ + snowpack = await startSnowpackServer({ config: snowpackConfig, lockfile: null, }); @@ -143,9 +154,11 @@ export async function createRuntime(astroConfig: AstroConfig, logging: LogOption logging, snowpack, snowpackRuntime, + snowpackConfig, }; return { + runtimeConfig, load: load.bind(null, runtimeConfig), shutdown: () => snowpack.shutdown(), }; |