diff --git a/examples/snowpack/.gitignore b/examples/snowpack/.gitignore
index f03556e8d..888e5be06 100644
--- a/examples/snowpack/.gitignore
+++ b/examples/snowpack/.gitignore
@@ -1,3 +1,4 @@
.DS_Store
build
node_modules
+_site
\ No newline at end of file
diff --git a/examples/snowpack/astro/components/Nav.astro b/examples/snowpack/astro/components/Nav.astro
index 8ca95cb2a..31ef33e3d 100644
--- a/examples/snowpack/astro/components/Nav.astro
+++ b/examples/snowpack/astro/components/Nav.astro
@@ -245,8 +245,8 @@ export let version: string = '3.1.2';
Snowpack
-
+
Press
⌘
diff --git a/examples/snowpack/astro/components/NewsAssets.svelte b/examples/snowpack/astro/components/NewsAssets.svelte
index fc968ee99..f53078e79 100644
--- a/examples/snowpack/astro/components/NewsAssets.svelte
+++ b/examples/snowpack/astro/components/NewsAssets.svelte
@@ -1,7 +1,7 @@
----
+
Assets
diff --git a/examples/snowpack/astro/components/PluginSearchPage.jsx b/examples/snowpack/astro/components/PluginSearchPage.jsx
index 5fb00c9db..8f3cd98d6 100644
--- a/examples/snowpack/astro/components/PluginSearchPage.jsx
+++ b/examples/snowpack/astro/components/PluginSearchPage.jsx
@@ -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([
diff --git a/examples/snowpack/astro/pages/proof-of-concept-dynamic/[slug].astro b/examples/snowpack/astro/pages/proof-of-concept-dynamic/[slug].astro.ignore
similarity index 100%
rename from examples/snowpack/astro/pages/proof-of-concept-dynamic/[slug].astro
rename to examples/snowpack/astro/pages/proof-of-concept-dynamic/[slug].astro.ignore
diff --git a/examples/snowpack/snowpack.config.js b/examples/snowpack/snowpack.config.js
index 821552181..eb9310e80 100644
--- a/examples/snowpack/snowpack.config.js
+++ b/examples/snowpack/snowpack.config.js
@@ -15,7 +15,7 @@ module.exports = {
out: '_site',
},
optimize: {
- bundle: true,
+ bundle: false,
minify: true,
target: 'es2018',
},
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;
extensions?: Record;
}
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 {
+ 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) => {
+ 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): string {
interface ComponentInfo {
type: string;
url: string;
+ plugin: string | undefined;
}
const defaultExtensions: Readonly> = {
@@ -109,13 +111,14 @@ const defaultExtensions: Readonly> = {
'.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, resolve: (s: string) => Promise): Promise {
+ 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 {
+ 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 = new Set();
- const components: Record = {};
+ const components: Record = {};
+ const componentPlugins = new Set();
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;
}
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> = [];
- 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, ...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 {
- 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;
extensions?: Record;
- } = { 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(),
};