From a29e60914766abf27c3a49891a76edc9c4015ab5 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 3 Dec 2020 13:43:54 -0500 Subject: [PATCH] Faster build (#222) * create separate svelte-kit adapt command (#62) * transform server and client in parallel * remove unused import --- packages/kit/src/api/build/index.js | 494 +++++++++++++++------------- 1 file changed, 259 insertions(+), 235 deletions(-) diff --git a/packages/kit/src/api/build/index.js b/packages/kit/src/api/build/index.js index 152c13b54fa9..9c4a5e1cb325 100644 --- a/packages/kit/src/api/build/index.js +++ b/packages/kit/src/api/build/index.js @@ -2,13 +2,13 @@ import fs from 'fs'; import path from 'path'; import child_process from 'child_process'; import { promisify } from 'util'; -import colors from 'kleur'; +import { green, gray, bold, cyan } from 'kleur/colors'; import { mkdirp } from '@sveltejs/app-utils/files'; import create_manifest_data from '../../core/create_manifest_data'; import { rollup } from 'rollup'; import { terser } from 'rollup-plugin-terser'; import css_chunks from 'rollup-plugin-css-chunks'; -import { copy_assets, logger } from '../utils'; +import { copy_assets } from '../utils'; import { create_app } from '../../core/create_app'; import { css_injection } from './css_injection'; @@ -28,280 +28,304 @@ const onwarn = (warning, handler) => { handler(warning); }; +const DIR = '.svelte'; +const ASSETS = `${DIR}/assets;` +const UNOPTIMIZED = `${DIR}/build/unoptimized`; +const OPTIMIZED = `${DIR}/build/optimized`; + +const s = JSON.stringify; + export async function build(config) { const manifest = create_manifest_data(config.paths.routes); - mkdirp('.svelte/assets'); + mkdirp(ASSETS); + await rimraf(UNOPTIMIZED); + await rimraf(OPTIMIZED); + create_app({ manifest_data: manifest, output: '.svelte/assets' }); - const header = (msg) => console.log(colors.bold().cyan(`> ${msg}`)); - - const log = logger(); - - const unoptimized = '.svelte/build/unoptimized'; - - { - // phase one — build with Snowpack - header('Transforming...'); - await rimraf('.svelte/build/unoptimized'); - - copy_assets(); - - const setup_file = `${unoptimized}/server/_app/setup/index.js`; - if (!fs.existsSync(setup_file)) { - mkdirp(path.dirname(setup_file)); - fs.writeFileSync(setup_file, ''); - } - - const mount = `--mount.${config.paths.routes}=/_app/routes --mount.${config.paths.setup}=/_app/setup`; - - await exec(`node ${snowpack_bin} build ${mount} --out=${unoptimized}/client`); - log.success('client'); - - await exec(`node ${snowpack_bin} build ${mount} --out=${unoptimized}/server --ssr`); - log.success('server'); - } + copy_assets(); + + const progress = { + transformed_client: false, + transformed_server: false, + optimized_client: false, + optimized_server: false + }; + + process.stdout.write('\x1b[s'); + + const tick = bold(green('✔')); + const render = () => process.stdout.write('\x1b[u' + ` + ${bold(cyan('Transforming...'))} + ${progress.transformed_client ? `${tick} client` : gray('⧗ client')} + ${progress.transformed_server ? `${tick} server` : gray('⧗ server')} + ${bold(cyan('Optimizing...'))} + ${progress.optimized_client ? `${tick} client ` : gray('⧗ client')} + ${progress.optimized_server ? `${tick} server ` : gray('⧗ server')} + `.replace(/^\t/gm, '').trimStart()); + + render(); + + const mount = `--mount.${config.paths.routes}=/_app/routes --mount.${config.paths.setup}=/_app/setup`; + + const promises = { + transform_client: exec(`node ${snowpack_bin} build ${mount} --out=${UNOPTIMIZED}/client`).then(() => { + progress.transformed_client = true; + render(); + }), + transform_server: exec(`node ${snowpack_bin} build ${mount} --out=${UNOPTIMIZED}/server --ssr`).then(() => { + progress.transformed_server = true; + render(); + }) + }; + + // we await this promise because we can't start optimizing the server + // until client optimization is complete + await promises.transform_client; + + const client = { + entry: null, + deps: {} + }; + + const entry = path.resolve(`${UNOPTIMIZED}/client/_app/assets/runtime/internal/start.js`); + + const client_chunks = await rollup({ + input: { + entry + }, + plugins: [ + { + name: 'deproxy-css', + async resolveId(importee, importer) { + if (/\.css\.proxy\.js$/.test(importee)) { + const deproxied = importee.replace(/\.css\.proxy\.js$/, '.css'); + const resolved = await this.resolve(deproxied, importer); + return resolved.id; + } + } + }, + css_chunks({ + injectImports: true, + sourcemap: true + }), + css_injection, + { + name: 'generate-client-manifest', + generateBundle(_options, bundle) { + const reverse_lookup = new Map(); + + const routes = path.resolve(`${UNOPTIMIZED}/client/_app/routes`); + + let inject_styles; + + for (const key in bundle) { + const chunk = bundle[key]; + + if (chunk.facadeModuleId === entry) { + client.entry = key; + } else if (chunk.facadeModuleId === 'inject_styles.js') { + inject_styles = key; + } else if (chunk.modules) { + for (const id in chunk.modules) { + if (id.startsWith(routes) && id.endsWith('.js')) { + const file = id.slice(routes.length + 1); + reverse_lookup.set(file, key); + } + } + } + } - { - // phase two — optimise - header('Optimizing...'); - await rimraf('.svelte/build/optimized'); + const find_deps = (key, js, css) => { + if (js.has(key)) return; - const s = JSON.stringify; + js.add(key); - const client = { - entry: null, - deps: {} - }; + const chunk = bundle[key]; - const entry = path.resolve(`${unoptimized}/client/_app/assets/runtime/internal/start.js`); + if (chunk) { + const imports = chunk.imports; - const client_chunks = await rollup({ - input: { - entry - }, - plugins: [ - { - name: 'deproxy-css', - async resolveId(importee, importer) { - if (/\.css\.proxy\.js$/.test(importee)) { - const deproxied = importee.replace(/\.css\.proxy\.js$/, '.css'); - const resolved = await this.resolve(deproxied, importer); - return resolved.id; - } - } - }, - css_chunks({ - injectImports: true, - sourcemap: true - }), - css_injection, - { - name: 'generate-client-manifest', - generateBundle(_options, bundle) { - const reverse_lookup = new Map(); - - const routes = path.resolve(`${unoptimized}/client/_app/routes`); - - let inject_styles; - - for (const key in bundle) { - const chunk = bundle[key]; - - if (chunk.facadeModuleId === entry) { - client.entry = key; - } else if (chunk.facadeModuleId === 'inject_styles.js') { - inject_styles = key; - } else if (chunk.modules) { - for (const id in chunk.modules) { - if (id.startsWith(routes) && id.endsWith('.js')) { - const file = id.slice(routes.length + 1); - reverse_lookup.set(file, key); + if (imports) { + imports.forEach((key) => { + if (key.endsWith('.css')) { + js.add(inject_styles); + css.add(key); + } else { + find_deps(key, js, css); } - } + }); } + } else { + this.error(`'${key}' is imported but could not be bundled`); } - const find_deps = (key, js, css) => { - if (js.has(key)) return; + return { js, css }; + }; - js.add(key); + const get_deps = (key) => { + const js = new Set(); + const css = new Set(); - const chunk = bundle[key]; + find_deps(key, js, css); - if (chunk) { - const imports = chunk.imports; + return { + js: Array.from(js), + css: Array.from(css) + }; + }; - if (imports) { - imports.forEach((key) => { - if (key.endsWith('.css')) { - js.add(inject_styles); - css.add(key); - } else { - find_deps(key, js, css); - } - }); - } - } else { - this.error(`'${key}' is imported but could not be bundled`); - } + client.deps.__entry__ = get_deps(client.entry); - return { js, css }; - }; + manifest.components.forEach((component) => { + const file = path.normalize(component.file.replace(/\.svelte$/, '.js')); + const key = reverse_lookup.get(file); + + client.deps[component.name] = get_deps(key); + }); + } + }, + terser() + ], - const get_deps = (key) => { - const js = new Set(); - const css = new Set(); + onwarn, - find_deps(key, js, css); + // TODO ensure this works with external node modules (on server) + external: (id) => id[0] !== '.' && !path.isAbsolute(id) + }); - return { - js: Array.from(js), - css: Array.from(css) - }; - }; + await client_chunks.write({ + dir: `${OPTIMIZED}/client/_app`, + entryFileNames: '[name]-[hash].js', + chunkFileNames: '[name]-[hash].js', + assetFileNames: '[name]-[hash].js', // TODO CSS filenames aren't hashed? + format: 'esm', + sourcemap: true + }); - client.deps.__entry__ = get_deps(client.entry); + progress.optimized_client = true; + render(); - manifest.components.forEach((component) => { - const file = path.normalize(component.file.replace(/\.svelte$/, '.js')); - const key = reverse_lookup.get(file); + // just in case the server is still transforming... + await promises.transform_server; - client.deps[component.name] = get_deps(key); - }); - } - }, - terser() + const setup_file = `${UNOPTIMIZED}/server/_app/setup/index.js`; + if (!fs.existsSync(setup_file)) { + mkdirp(path.dirname(setup_file)); + fs.writeFileSync(setup_file, ''); + } + + const stringified_manifest = ` + { + layout: ${s(manifest.layout)}, + error: ${s(manifest.error)}, + components: ${s(manifest.components)}, + pages: [ + ${manifest.pages + .map(({ pattern, parts: json_parts }) => { + const parts = JSON.stringify(json_parts); + return `{ pattern: ${pattern}, parts: ${parts} }`; + }) + .join(',')} ], + endpoints: [ + ${manifest.endpoints + .map(({ name, pattern, file, params: json_params }) => { + const params = JSON.stringify(json_params); + return `{ name: '${name}', pattern: ${pattern}, file: '${file}', params: ${params} }`; + }) + .join(',')} + ] + } + `.replace(/^\t{3}/gm, '').trim(); - onwarn, + fs.writeFileSync('.svelte/build/manifest.js', `export default ${stringified_manifest};`); + fs.writeFileSync('.svelte/build/manifest.cjs', `module.exports = ${stringified_manifest};`); - // TODO ensure this works with external node modules (on server) - external: (id) => id[0] !== '.' && !path.isAbsolute(id) - }); + fs.writeFileSync(`${UNOPTIMIZED}/server/app.js`, ` + import * as renderer from '@sveltejs/kit/assets/renderer'; + import root from './_app/assets/generated/root.js'; + import * as setup from './_app/setup/index.js'; + import manifest from '../../manifest.js'; - await client_chunks.write({ - dir: '.svelte/build/optimized/client/_app', - entryFileNames: '[name]-[hash].js', - chunkFileNames: '[name]-[hash].js', - assetFileNames: '[name]-[hash].js', // TODO CSS filenames aren't hashed? - format: 'esm', - sourcemap: true - }); + const template = ({ head, body }) => ${s(fs.readFileSync(config.paths.template, 'utf-8')) + .replace('%svelte.head%', '" + head + "') + .replace('%svelte.body%', '" + body + "')}; - log.success('client'); + const client = ${s(client)}; - const stringified_manifest = ` - { - layout: ${s(manifest.layout)}, - error: ${s(manifest.error)}, - components: ${s(manifest.components)}, - pages: [ - ${manifest.pages - .map(({ pattern, parts: json_parts }) => { - const parts = JSON.stringify(json_parts); - return `{ pattern: ${pattern}, parts: ${parts} }`; - }) - .join(',')} - ], - endpoints: [ - ${manifest.endpoints - .map(({ name, pattern, file, params: json_params }) => { - const params = JSON.stringify(json_params); - return `{ name: '${name}', pattern: ${pattern}, file: '${file}', params: ${params} }`; - }) - .join(',')} - ] - } - `.replace(/^\t{3}/gm, '').trim(); - - fs.writeFileSync('.svelte/build/manifest.js', `export default ${stringified_manifest};`); - fs.writeFileSync('.svelte/build/manifest.cjs', `module.exports = ${stringified_manifest};`); - - fs.writeFileSync(`${unoptimized}/server/app.js`, ` - import * as renderer from '@sveltejs/kit/assets/renderer'; - import root from './_app/assets/generated/root.js'; - import * as setup from './_app/setup/index.js'; - import manifest from '../../manifest.js'; - - const template = ({ head, body }) => ${s(fs.readFileSync(config.paths.template, 'utf-8')) - .replace('%svelte.head%', '" + head + "') - .replace('%svelte.body%', '" + body + "')}; - - const client = ${s(client)}; - - export const paths = { - static: ${s(config.paths.static)} - }; - - export function render(request, { only_prerender = false } = {}) { - return renderer.render(request, { - static_dir: paths.static, - template, - manifest, - target: ${s(config.target)},${config.startGlobal ? `\n\t\t\t\t\tstart_global: ${s(config.startGlobal)},` : ''} - client, - root, - setup, - load: (route) => require(\`./routes/\${route.name}.js\`), - dev: false, - only_prerender - }); - } - `.replace(/^\t{3}/gm, '').trim()); - - const server_input = { - app: `${unoptimized}/server/app.js` + export const paths = { + static: ${s(config.paths.static)} }; - [ - manifest.layout, - manifest.error, - ...manifest.components, - ...manifest.endpoints - ].forEach((item) => { - server_input[`routes/${item.name}`] = `${unoptimized}/server${item.url.replace( - /\.\w+$/, - '.js' - )}`; - }); - - const server_chunks = await rollup({ - input: server_input, - plugins: [ - { - name: 'remove-css', - load(id) { - if (/\.css\.proxy\.js$/.test(id)) return ''; - } - }, - // TODO add server manifest generation so we can prune - // imports before zipping for cloud functions - terser() - ], + export function render(request, { only_prerender = false } = {}) { + return renderer.render(request, { + static_dir: paths.static, + template, + manifest, + target: ${s(config.target)},${config.startGlobal ? `\n\t\t\t\t\tstart_global: ${s(config.startGlobal)},` : ''} + client, + root, + setup, + load: (route) => require(\`./routes/\${route.name}.js\`), + dev: false, + only_prerender + }); + } + `.replace(/^\t{3}/gm, '').trim()); + + const server_input = { + app: `${UNOPTIMIZED}/server/app.js` + }; + + [ + manifest.layout, + manifest.error, + ...manifest.components, + ...manifest.endpoints + ].forEach((item) => { + server_input[`routes/${item.name}`] = `${UNOPTIMIZED}/server${item.url.replace( + /\.\w+$/, + '.js' + )}`; + }); - onwarn, + const server_chunks = await rollup({ + input: server_input, + plugins: [ + { + name: 'remove-css', + load(id) { + if (/\.css\.proxy\.js$/.test(id)) return ''; + } + }, + // TODO add server manifest generation so we can prune + // imports before zipping for cloud functions + terser() + ], - // TODO ensure this works with external node modules (on server) - external: (id) => id[0] !== '.' && !path.isAbsolute(id) - }); + onwarn, - await server_chunks.write({ - dir: '.svelte/build/optimized/server', - format: 'cjs', // TODO some adapters might want ESM? - exports: 'named', - entryFileNames: '[name].js', - chunkFileNames: 'chunks/[name].js', - assetFileNames: 'assets/[name].js', - sourcemap: true - }); + // TODO ensure this works with external node modules (on server) + external: (id) => id[0] !== '.' && !path.isAbsolute(id) + }); - log.success('server'); - } + await server_chunks.write({ + dir: `${OPTIMIZED}/server`, + format: 'cjs', // TODO some adapters might want ESM? + exports: 'named', + entryFileNames: '[name].js', + chunkFileNames: 'chunks/[name].js', + assetFileNames: 'assets/[name].js', + sourcemap: true + }); + progress.optimized_server = true; + render(); console.log(); }