First pass at the build (#27)

This updates `astro build` to do a production build. It works! No optimizations yet.
This commit is contained in:
Matthew Phillips 2021-03-25 14:06:08 -04:00 committed by GitHub
parent 18e7cc5af9
commit 3db5959377
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 186 additions and 115 deletions

View file

@ -1,3 +1,4 @@
.DS_Store
build
node_modules
_site

View file

@ -245,8 +245,8 @@ export let version: string = '3.1.2';
<span class="logo-type">Snowpack</span>
</a>
<div class="search">
<input type="text" name="search" placeholder="Search documentation..." class="search-form-input"
id="search-input">
<input type="text" name="search" placeholder="Search documentation..." class="search-input"
id="search-form-input">
<span class="search-hint">
<span class="sr-only">Press </span>
<kbd class="font-sans"><abbr title="Command" style="text-decoration: none;">⌘</abbr></kbd>

View file

@ -1,7 +1,7 @@
---
<script>
//let name = 'world';
// TODO make this dynamic?
---
</script>
<h3>Assets</h3>

View file

@ -1,6 +1,6 @@
import { h, Fragment } from 'preact';
import { useEffect, useState } from 'preact/hooks';
import * as Styles from './PluginSearchPage.css';
import * as Styles from './PluginSearchPage.module.css';
async function searchPlugins(val) {
const params3 = new URLSearchParams([

View file

@ -15,7 +15,7 @@ module.exports = {
out: '_site',
},
optimize: {
bundle: true,
bundle: false,
minify: true,
target: 'es2018',
},

View file

@ -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>;
}

83
src/build.ts Normal file
View file

@ -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;
}

View file

@ -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],
]);

View file

@ -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);
}

View file

@ -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) {

View file

@ -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);

View file

@ -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);

View file

@ -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,

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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(),
};