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 .DS_Store
build build
node_modules node_modules
_site

View file

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

View file

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

View file

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

View file

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

View file

@ -3,6 +3,6 @@ import type { ValidExtensionPlugins } from './astro';
export interface CompileOptions { export interface CompileOptions {
logging: LogOptions; logging: LogOptions;
resolve: (p: string) => string; resolve: (p: string) => Promise<string>;
extensions?: Record<string, ValidExtensionPlugins>; 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 yargs from 'yargs-parser';
import { loadConfig } from './config.js'; import { loadConfig } from './config.js';
import generate from './generate.js'; import {build} from './build.js';
import devServer from './dev.js'; import devServer from './dev.js';
const { readFile } = fsPromises; const { readFile } = fsPromises;
const buildAndExit = async (...args: Parameters<typeof build>) => {
const ret = await build(...args);
process.exit(ret);
}
type Arguments = yargs.Arguments; type Arguments = yargs.Arguments;
type cliState = 'help' | 'version' | 'dev' | 'build'; type cliState = 'help' | 'version' | 'dev' | 'build';
@ -61,7 +65,7 @@ async function runCommand(rawRoot: string, cmd: (a: AstroConfig) => Promise<void
} }
const cmdMap = new Map([ const cmdMap = new Map([
['build', generate], ['build', buildAndExit],
['dev', devServer], ['dev', devServer],
]); ]);

View file

@ -11,6 +11,7 @@ import babelParser from '@babel/parser';
import _babelGenerator from '@babel/generator'; import _babelGenerator from '@babel/generator';
import traverse from '@babel/traverse'; import traverse from '@babel/traverse';
import { ImportDeclaration,ExportNamedDeclaration, VariableDeclarator, Identifier, VariableDeclaration } from '@babel/types'; import { ImportDeclaration,ExportNamedDeclaration, VariableDeclarator, Identifier, VariableDeclaration } from '@babel/types';
import { type } from 'node:os';
const babelGenerator: typeof _babelGenerator = const babelGenerator: typeof _babelGenerator =
// @ts-ignore // @ts-ignore
@ -100,6 +101,7 @@ function generateAttributes(attrs: Record<string, string>): string {
interface ComponentInfo { interface ComponentInfo {
type: string; type: string;
url: string; url: string;
plugin: string | undefined;
} }
const defaultExtensions: Readonly<Record<string, ValidExtensionPlugins>> = { const defaultExtensions: Readonly<Record<string, ValidExtensionPlugins>> = {
@ -109,13 +111,14 @@ const defaultExtensions: Readonly<Record<string, ValidExtensionPlugins>> = {
'.svelte': 'svelte', '.svelte': 'svelte',
}; };
function getComponentWrapper(_name: string, { type, url }: ComponentInfo, compileOptions: CompileOptions) { type DynamicImportMap = Map<
const { resolve, extensions = defaultExtensions } = compileOptions; 'vue' | 'react' | 'react-dom' | 'preact',
string
>;
function getComponentWrapper(_name: string, { type, plugin, url }: ComponentInfo, dynamicImports: DynamicImportMap) {
const [name, kind] = _name.split(':'); const [name, kind] = _name.split(':');
const plugin = extensions[type] || defaultExtensions[type];
if (!plugin) { if (!plugin) {
throw new Error(`No supported plugin found for extension ${type}`); throw new Error(`No supported plugin found for extension ${type}`);
} }
@ -133,7 +136,7 @@ function getComponentWrapper(_name: string, { type, url }: ComponentInfo, compil
case 'preact': { case 'preact': {
if (kind === 'dynamic') { if (kind === 'dynamic') {
return { 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')}';`, wrapperImport: `import {__preact_dynamic} from '${internalImport('render/preact.js')}';`,
}; };
} else { } else {
@ -146,9 +149,9 @@ function getComponentWrapper(_name: string, { type, url }: ComponentInfo, compil
case 'react': { case 'react': {
if (kind === 'dynamic') { if (kind === 'dynamic') {
return { 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' 'react'
)}', '${resolve('react-dom')}')`, )!}', '${dynamicImports.get('react-dom')!}')`,
wrapperImport: `import {__react_dynamic} from '${internalImport('render/react.js')}';`, wrapperImport: `import {__react_dynamic} from '${internalImport('render/react.js')}';`,
}; };
} else { } else {
@ -174,7 +177,7 @@ function getComponentWrapper(_name: string, { type, url }: ComponentInfo, compil
case 'vue': { case 'vue': {
if (kind === 'dynamic') { if (kind === 'dynamic') {
return { 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')}';`, wrapperImport: `import {__vue_dynamic} from '${internalImport('render/vue.js')}';`,
}; };
} else { } else {
@ -186,23 +189,10 @@ function getComponentWrapper(_name: string, { type, url }: ComponentInfo, compil
}; };
} }
} }
} default: {
throw new Error('Unknown Component Type: ' + name); throw new Error(`Unknown component type`);
}
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;
} }
} }
return compiledCode;
} }
function compileExpressionSafe(raw: string): string { function compileExpressionSafe(raw: string): string {
@ -215,7 +205,30 @@ function compileExpressionSafe(raw: string): string {
return code; 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> { export async function codegen(ast: Ast, { compileOptions }: CodeGenOptions): Promise<TransformResult> {
const { extensions = defaultExtensions } = compileOptions;
await eslexer.init; await eslexer.init;
const componentImports: ImportDeclaration[] = []; const componentImports: ImportDeclaration[] = [];
@ -225,7 +238,8 @@ export async function codegen(ast: Ast, { compileOptions }: CodeGenOptions): Pro
let script = ''; let script = '';
let propsStatement: string = ''; let propsStatement: string = '';
const importExportStatements: Set<string> = new Set(); 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) { if (ast.module) {
const program = babelParser.parse(ast.module.content, { const program = babelParser.parse(ast.module.content, {
@ -259,7 +273,15 @@ export async function codegen(ast: Ast, { compileOptions }: CodeGenOptions): Pro
const importUrl = componentImport.source.value; const importUrl = componentImport.source.value;
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);
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!)); importExportStatements.add(ast.module.content.slice(componentImport.start!, componentImport.end!));
} }
for (const componentImport of componentExports) { for (const componentImport of componentExports) {
@ -280,6 +302,8 @@ export async function codegen(ast: Ast, { compileOptions }: CodeGenOptions): Pro
script = propsStatement + babelGenerator(program).code; script = propsStatement + babelGenerator(program).code;
} }
const dynamicImports = await acquireDynamicComponentImports(componentPlugins, compileOptions.resolve);
let items: JsxItem[] = []; let items: JsxItem[] = [];
let collectionItem: JsxItem | undefined; let collectionItem: JsxItem | undefined;
let currentItemName: string | undefined; let currentItemName: string | undefined;
@ -304,7 +328,7 @@ export async function codegen(ast: Ast, { compileOptions }: CodeGenOptions): Pro
if (!components[componentName]) { if (!components[componentName]) {
throw new Error(`Unknown Component: ${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) { if (wrapperImport) {
importExportStatements.add(wrapperImport); importExportStatements.add(wrapperImport);
} }
@ -356,7 +380,7 @@ export async function codegen(ast: Ast, { compileOptions }: CodeGenOptions): Pro
if (!componentImportData) { if (!componentImportData) {
throw new Error(`Unknown Component: ${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) { if (wrapperImport) {
importExportStatements.add(wrapperImport); importExportStatements.add(wrapperImport);
} }

View file

@ -15,12 +15,12 @@ import { codegen } from './codegen.js';
interface CompileOptions { interface CompileOptions {
logging: LogOptions; logging: LogOptions;
resolve: (p: string) => string; resolve: (p: string) => Promise<string>;
} }
const defaultCompileOptions: CompileOptions = { const defaultCompileOptions: CompileOptions = {
logging: defaultLogOptions, logging: defaultLogOptions,
resolve: (p: string) => p, resolve: (p: string) => Promise.resolve(p),
}; };
function internalImport(internalPath: string) { function internalImport(internalPath: string) {

View file

@ -73,7 +73,13 @@ export async function optimize(ast: Ast, opts: OptimizeOptions) {
const cssVisitors = createVisitorCollection(); const cssVisitors = createVisitorCollection();
const finalizers: Array<() => Promise<void>> = []; 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.css, cssVisitors);
walkAstWithVisitors(ast.html, htmlVisitors); walkAstWithVisitors(ast.html, htmlVisitors);

View file

@ -21,7 +21,7 @@ const logging: LogOptions = {
export default async function (astroConfig: AstroConfig) { export default async function (astroConfig: AstroConfig) {
const { projectRoot } = 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 server = http.createServer(async (req, res) => {
const result = await runtime.load(req.url); const result = await runtime.load(req.url);

View file

@ -1,10 +1,13 @@
import render from 'preact-render-to-string'; import renderToString from 'preact-render-to-string';
import { h } from 'preact'; import { h, render } from 'preact';
import type { Component } from 'preact'; import type { Component } from 'preact';
// This prevents tree-shaking of render.
Function.prototype(render);
export function __preact_static(PreactComponent: Component) { export function __preact_static(PreactComponent: Component) {
return (attrs: Record<string, any>, ...children: any): string => { return (attrs: Record<string, any>, ...children: any): string => {
let html = render( let html = renderToString(
h( h(
PreactComponent as any, // Preact's types seem wrong... PreactComponent as any, // Preact's types seem wrong...
attrs, 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 { parse_expression_at } from '../acorn.js';
import { Parser } from '../index.js'; import { Parser } from '../index.js';
import { whitespace } from '../../utils/patterns.js'; import { whitespace } from '../../utils/patterns.js';
// import { Node } from 'estree';
// @ts-ignore // @ts-ignore
export default function read_expression(parser: Parser): string { export default function read_expression(parser: Parser): string {
@ -35,7 +34,6 @@ export default function read_expression(parser: Parser): string {
parser.index = index; parser.index = index;
return parser.template.substring(start, index); return parser.template.substring(start, index);
// return node as Node;
} catch (err) { } catch (err) {
parser.acorn_error(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 { AstroConfig } from './@types/astro';
import type { LogOptions } from './logger'; import type { LogOptions } from './logger';
import type { CompileError } from './parser/utils/error.js'; import type { CompileError } from './parser/utils/error.js';
@ -14,6 +14,7 @@ interface RuntimeConfig {
logging: LogOptions; logging: LogOptions;
snowpack: SnowpackDevServer; snowpack: SnowpackDevServer;
snowpackRuntime: SnowpackServerRuntime; snowpackRuntime: SnowpackServerRuntime;
snowpackConfig: SnowpackConfig;
} }
type LoadResultSuccess = { 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 { projectRoot, astroRoot, extensions } = astroConfig;
const internalPath = new URL('./frontend/', import.meta.url); const internalPath = new URL('./frontend/', import.meta.url);
// Workaround for SKY-251 let snowpack: SnowpackDevServer;
const astroPlugOptions: { const astroPlugOptions: {
resolve?: (s: string) => string; resolve?: (s: string) => Promise<string>;
extensions?: Record<string, 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 pkgLockStr = await readFile(new URL('./package-lock.json', projectRoot), 'utf-8');
const pkgLock = JSON.parse(pkgLockStr); const pkgLock = JSON.parse(pkgLockStr);
astroPlugOptions.resolve = (pkgName: string) => { astroPlugOptions.resolve = (pkgName: string) => {
const ver = pkgLock.dependencies[pkgName].version; const ver = pkgLock.dependencies[pkgName].version;
return `/_snowpack/pkg/${pkgName}.v${ver}.js`; return `/_snowpack/pkg/${pkgName}.v${ver}.js`;
}; };
} }*/
const snowpackConfig = await loadConfiguration({ const snowpackConfig = await loadConfiguration({
root: projectRoot.pathname, root: projectRoot.pathname,
@ -132,7 +143,7 @@ export async function createRuntime(astroConfig: AstroConfig, logging: LogOption
external: ['@vue/server-renderer', 'node-fetch'], external: ['@vue/server-renderer', 'node-fetch'],
}, },
}); });
const snowpack = await startSnowpackServer({ snowpack = await startSnowpackServer({
config: snowpackConfig, config: snowpackConfig,
lockfile: null, lockfile: null,
}); });
@ -143,9 +154,11 @@ export async function createRuntime(astroConfig: AstroConfig, logging: LogOption
logging, logging,
snowpack, snowpack,
snowpackRuntime, snowpackRuntime,
snowpackConfig,
}; };
return { return {
runtimeConfig,
load: load.bind(null, runtimeConfig), load: load.bind(null, runtimeConfig),
shutdown: () => snowpack.shutdown(), shutdown: () => snowpack.shutdown(),
}; };