diff --git a/packages/nitro/src/build.ts b/packages/nitro/src/build.ts index 08a0293027..1f00c10969 100644 --- a/packages/nitro/src/build.ts +++ b/packages/nitro/src/build.ts @@ -5,24 +5,27 @@ import Hookable from 'hookable' import prettyBytes from 'pretty-bytes' import gzipSize from 'gzip-size' import chalk from 'chalk' -import { readFile } from 'fs-extra' +import { readFile, emptyDir } from 'fs-extra' import { getRollupConfig } from './rollup/config' import { hl, prettyPath, serializeTemplate, writeFile } from './utils' import { SLSOptions } from './config' export async function build (options: SLSOptions) { - console.log('\n') consola.info(`Generating bundle for ${hl(options.target)}`) const hooks = new Hookable() hooks.addHooks(options.hooks) + if (options.cleanTargetDir) { + await emptyDir(options.targetDir) + } + // Compile html template const htmlSrc = resolve(options.buildDir, `views/${{ 2: 'app', 3: 'document' }[2]}.template.html`) const htmlTemplate = { src: htmlSrc, contents: '', dst: '', compiled: '' } htmlTemplate.dst = htmlTemplate.src.replace(/.html$/, '.js').replace('app.', 'document.') htmlTemplate.contents = await readFile(htmlTemplate.src, 'utf-8') - htmlTemplate.compiled = serializeTemplate(htmlTemplate.contents) + htmlTemplate.compiled = 'module.exports = ' + serializeTemplate(htmlTemplate.contents) await hooks.callHook('template:document', htmlTemplate) await writeFile(htmlTemplate.dst, htmlTemplate.compiled) @@ -36,13 +39,11 @@ export async function build (options: SLSOptions) { const { output } = await build.write(options.rollupConfig.output as OutputOptions) const size = prettyBytes(output[0].code.length) const zSize = prettyBytes(await gzipSize(output[0].code)) - consola.success('Generated', prettyPath((options.rollupConfig.output as any).file), - chalk.gray(`(Size: ${size} Gzip: ${zSize})`) - ) + consola.success('Generated bundle in', prettyPath(options.targetDir), chalk.gray(`(Size: ${size} Gzip: ${zSize})`)) await hooks.callHook('done', options) return { - entry: options.rollupConfig.output.file + entry: resolve(options.rollupConfig.output.dir, options.rollupConfig.output.entryFileNames) } } diff --git a/packages/nitro/src/config.ts b/packages/nitro/src/config.ts index b9c047afa4..6a2c05f7ab 100644 --- a/packages/nitro/src/config.ts +++ b/packages/nitro/src/config.ts @@ -15,6 +15,7 @@ export interface Nuxt extends Hookable{ export interface ServerMiddleware { route: string handle: string + lazy?: boolean } export interface SLSOptions { @@ -34,11 +35,13 @@ export interface SLSOptions { node: false | true target: string minify: boolean + externals: boolean rollupConfig?: any logStartup: boolean inlineChunks: boolean renderer: string analyze: boolean + cleanTargetDir: boolean runtimeDir: string slsDir: string @@ -72,6 +75,8 @@ export function getoptions (nuxtOptions: Nuxt['options'], serverless: SLSConfig) logStartup: true, inlineChunks: true, minify: false, + externals: false, + cleanTargetDir: true, runtimeDir: resolve(__dirname, '../runtime'), slsDir: '{{ rootDir }}/.nuxt/serverless', diff --git a/packages/nitro/src/index.ts b/packages/nitro/src/index.ts index 0e0c7dc49d..19f20170df 100644 --- a/packages/nitro/src/index.ts +++ b/packages/nitro/src/index.ts @@ -13,9 +13,7 @@ export default function slsModule () { const options = getoptions(nuxt.options, nuxt.options.serverless || {}) // Tune webpack config - if (options.minify !== false) { - nuxt.options.build._minifyServer = true - } + nuxt.options.build._minifyServer = options.minify !== false nuxt.options.build.standalone = true // Tune generator @@ -46,7 +44,7 @@ export default function slsModule () { continue } - options.serverMiddleware.push({ route, handle }) + options.serverMiddleware.push({ ...m, route, handle }) } if (unsupported.length) { console.warn('[serverless] Unsupported Server middleware used: ', unsupported) @@ -72,12 +70,16 @@ export default function slsModule () { }) nuxt.hook('generate:before', async () => { + console.info('Building light version for `nuxt generate`') const { entry } = await build(getoptions(nuxt.options, { - target: 'node', + target: 'cjs', serverMiddleware: options.serverMiddleware })) + console.info('Loading lambda') require(entry) }) - nuxt.hook('generate:done', () => build(options)) + nuxt.hook('generate:done', async () => { + await build(options) + }) } diff --git a/packages/nitro/src/rollup/config.ts b/packages/nitro/src/rollup/config.ts index c131d7a440..5a2e41d41d 100644 --- a/packages/nitro/src/rollup/config.ts +++ b/packages/nitro/src/rollup/config.ts @@ -1,5 +1,5 @@ import Module from 'module' -import { basename, dirname, extname, resolve } from 'path' +import { join, resolve } from 'path' import { InputOptions, OutputOptions } from 'rollup' import { terser } from 'rollup-plugin-terser' import commonjs from '@rollup/plugin-commonjs' @@ -13,23 +13,18 @@ import analyze from 'rollup-plugin-analyzer' import hasha from 'hasha' import { SLSOptions } from '../config' -import { resolvePath } from '../utils' -import dynamicRequire from './dynamic-require' +import { resolvePath, MODULE_DIR } from '../utils' +import { dynamicRequire } from './dynamic-require' +import { externals } from './externals' const mapArrToVal = (val, arr) => arr.reduce((p, c) => ({ ...p, [c]: val }), {}) export type RollupConfig = InputOptions & { output: OutputOptions } -export const getRollupConfig = (config: SLSOptions) => { - const providedDeps = [ - '@nuxt/devalue', - 'vue-bundle-renderer', - '@cloudflare/kv-asset-handler' - ] - +export const getRollupConfig = (options: SLSOptions) => { const extensions: string[] = ['.ts', '.mjs', '.js', '.json', '.node'] - const external: string[] = [] + const external: InputOptions['external'] = [] const injects:{ [key: string]: string| string[] } = {} @@ -53,7 +48,10 @@ export const getRollupConfig = (config: SLSOptions) => { '@vue/compiler-ssr' ])) - if (config.node === false) { + // Uses eval + aliases.depd = '~mocks/custom/depd' + + if (options.node === false) { // Globals // injects.Buffer = ['buffer', 'Buffer'] <-- TODO: Make it opt-in injects.process = '~mocks/node/process' @@ -73,7 +71,6 @@ export const getRollupConfig = (config: SLSOptions) => { // Custom 'node-fetch': '~mocks/custom/node-fetch', - depd: '~mocks/custom/depd', etag: '~mocks/generic/noop', // Express @@ -91,13 +88,15 @@ export const getRollupConfig = (config: SLSOptions) => { external.push(...Module.builtinModules) } - const outFile = resolve(config.targetDir, config.outName) - - const options: RollupConfig = { - input: resolvePath(config, config.entry), + const rollupConfig: RollupConfig = { + input: resolvePath(options, options.entry), output: { - file: outFile, + dir: options.targetDir, + entryFileNames: options.outName, + chunkFileNames: 'chunks/_[name].js', + inlineDynamicImports: options.inlineChunks, format: 'cjs', + exports: 'auto', intro: '', outro: '', preferConst: true @@ -106,31 +105,32 @@ export const getRollupConfig = (config: SLSOptions) => { plugins: [] } - if (config.logStartup) { - options.output.intro += 'global._startTime = global.process.hrtime();' + if (options.logStartup) { + rollupConfig.output.intro += 'global._startTime = global.process.hrtime();' // eslint-disable-next-line no-template-curly-in-string - options.output.outro += 'global._endTime = global.process.hrtime(global._startTime); global._coldstart = ((global._endTime[0] * 1e9) + global._endTime[1]) / 1e6; console.log(`λ Cold start took: ${global._coldstart}ms`);' + rollupConfig.output.outro += 'global._endTime = global.process.hrtime(global._startTime); global._coldstart = ((global._endTime[0] * 1e9) + global._endTime[1]) / 1e6; console.log(`λ Cold start took: ${global._coldstart}ms (${typeof __filename !== "undefined" ? __filename.replace(process.cwd(), "") : ""})`);' } // https://github.com/rollup/plugins/tree/master/packages/replace - options.plugins.push(replace({ + rollupConfig.plugins.push(replace({ values: { 'process.env.NODE_ENV': '"production"', 'typeof window': '"undefined"', - 'process.env.ROUTER_BASE': JSON.stringify(config.routerBase), - 'process.env.PUBLIC_PATH': JSON.stringify(config.publicPath), - 'process.env.NUXT_STATIC_BASE': JSON.stringify(config.staticAssets.base), - 'process.env.NUXT_STATIC_VERSION': JSON.stringify(config.staticAssets.version), + 'process.env.ROUTER_BASE': JSON.stringify(options.routerBase), + 'process.env.PUBLIC_PATH': JSON.stringify(options.publicPath), + 'process.env.NUXT_STATIC_BASE': JSON.stringify(options.staticAssets.base), + 'process.env.NUXT_STATIC_VERSION': JSON.stringify(options.staticAssets.version), // @ts-ignore - 'process.env.NUXT_FULL_STATIC': config.fullStatic + 'process.env.NUXT_FULL_STATIC': options.fullStatic } })) // Dynamic Require Support - options.plugins.push(dynamicRequire({ - dir: resolve(config.buildDir, 'dist/server'), - outDir: (config.node === false || config.inlineChunks) ? undefined : dirname(outFile), - chunksDir: '_' + basename(outFile, extname(outFile)), + rollupConfig.plugins.push(dynamicRequire({ + dir: resolve(options.buildDir, 'dist/server'), + inline: options.node === false || options.inlineChunks, + outDir: join(options.targetDir, 'chunks'), + prefix: './', globbyOptions: { ignore: [ 'server.js' @@ -140,7 +140,7 @@ export const getRollupConfig = (config: SLSOptions) => { // https://github.com/rollup/plugins/tree/master/packages/replace // TODO: better fix for node-fetch issue - options.plugins.push(replace({ + rollupConfig.plugins.push(replace({ delimiters: ['', ''], values: { 'require(\'encoding\')': '{}' @@ -149,59 +149,77 @@ export const getRollupConfig = (config: SLSOptions) => { // Provide serverMiddleware const getImportId = p => '_' + hasha(p).substr(0, 6) - options.plugins.push(virtual({ + rollupConfig.plugins.push(virtual({ '~serverMiddleware': ` - ${config.serverMiddleware.map(m => `import ${getImportId(m.handle)} from '${m.handle}';`).join('\n')} + ${options.serverMiddleware.filter(m => !m.lazy).map(m => `import ${getImportId(m.handle)} from '${m.handle}';`).join('\n')} + + ${options.serverMiddleware.filter(m => m.lazy).map(m => `const ${getImportId(m.handle)} = () => import('${m.handle}');`).join('\n')} export default [ - ${config.serverMiddleware.map(m => `{ route: '${m.route}', handle: ${getImportId(m.handle)} }`).join(',\n')} + ${options.serverMiddleware.map(m => `{ route: '${m.route}', handle: ${getImportId(m.handle)}, lazy: ${m.lazy || false} }`).join(',\n')} ]; ` })) // https://github.com/rollup/plugins/tree/master/packages/alias - const renderer = config.renderer || 'vue2' - options.plugins.push(alias({ + const renderer = options.renderer || 'vue2' + rollupConfig.plugins.push(alias({ entries: { - '~runtime': config.runtimeDir, - '~mocks': resolve(config.runtimeDir, 'mocks'), - '~renderer': require.resolve(resolve(config.runtimeDir, 'ssr', renderer)), - '~build': config.buildDir, - '~mock': require.resolve(resolve(config.runtimeDir, 'mocks/generic')), - ...providedDeps.reduce((p, c) => ({ ...p, [c]: require.resolve(c) }), {}), + '~runtime': options.runtimeDir, + '~mocks': resolve(options.runtimeDir, 'mocks'), + '~renderer': require.resolve(resolve(options.runtimeDir, 'ssr', renderer)), + '~build': options.buildDir, + '~mock': require.resolve(resolve(options.runtimeDir, 'mocks/generic')), ...aliases } })) + // External Plugin + if (options.externals) { + rollupConfig.plugins.push(externals({ + relativeTo: options.targetDir, + include: [ + options.runtimeDir, + ...options.serverMiddleware.map(m => m.handle) + ] + })) + } + // https://github.com/rollup/plugins/tree/master/packages/node-resolve - options.plugins.push(nodeResolve({ + rollupConfig.plugins.push(nodeResolve({ extensions, preferBuiltins: true, - rootDir: config.rootDir, + rootDir: options.rootDir, // https://www.npmjs.com/package/resolve - customResolveOptions: { basedir: config.rootDir }, + customResolveOptions: { + basedir: options.rootDir, + paths: [ + resolve(options.rootDir, 'node_modukes'), + resolve(MODULE_DIR, 'node_modules') + ] + }, mainFields: ['main'] // Force resolve CJS (@vue/runtime-core ssrUtils) })) // https://github.com/rollup/plugins/tree/master/packages/commonjs - options.plugins.push(commonjs({ + rollupConfig.plugins.push(commonjs({ extensions: extensions.filter(ext => ext !== '.json') })) // https://github.com/rollup/plugins/tree/master/packages/json - options.plugins.push(json()) + rollupConfig.plugins.push(json()) // https://github.com/rollup/plugins/tree/master/packages/inject - options.plugins.push(inject(injects)) + rollupConfig.plugins.push(inject(injects)) - if (config.analyze) { + if (options.analyze) { // https://github.com/doesdev/rollup-plugin-analyzer - options.plugins.push(analyze()) + rollupConfig.plugins.push(analyze()) } - if (config.minify !== false) { - options.plugins.push(terser()) + if (options.minify !== false) { + rollupConfig.plugins.push(terser()) } - return options + return rollupConfig } diff --git a/packages/nitro/src/rollup/dynamic-require.ts b/packages/nitro/src/rollup/dynamic-require.ts index d33082f123..487d940baa 100644 --- a/packages/nitro/src/rollup/dynamic-require.ts +++ b/packages/nitro/src/rollup/dynamic-require.ts @@ -1,4 +1,4 @@ -import { basename, resolve, dirname } from 'path' +import { resolve, dirname } from 'path' import globby, { GlobbyOptions } from 'globby' import { copyFile, mkdirp } from 'fs-extra' @@ -6,43 +6,26 @@ const PLUGIN_NAME = 'dynamic-require' const HELPER_DYNAMIC = `\0${PLUGIN_NAME}.js` const DYNAMIC_REQUIRE_RE = /require\("\.\/" ?\+/g -interface Import { - name: string - id: string - import: string -} - -const TMPL_ESM_INLINE = ({ imports }: { imports: Import[]}) => - `${imports.map(i => `import ${i.name} from '${i.import.replace(/\\/g, '/')}'`).join('\n')} -const dynamicChunks = { - ${imports.map(i => ` ['${i.id}']: ${i.name}`).join(',\n')} -}; - -export default function dynamicRequire(id) { - return dynamicChunks[id]; -};` - -const TMPL_CJS_LAZY = ({ imports, chunksDir }) => - `const dynamicChunks = { -${imports.map(i => ` ['${i.id}']: () => require('./${chunksDir}/${i.id}')`).join(',\n')} -}; - -export default function dynamicRequire(id) { - return dynamicChunks[id](); -};` - -// const TMPL_CJS = ({ chunksDir }) => `export default function dynamicRequire(id) { -// return require('./${chunksDir}/' + id); -// };` - interface Options { dir: string + inline: boolean globbyOptions: GlobbyOptions outDir?: string - chunksDir?: string + prefix?: string } -export default function dynamicRequire ({ dir, globbyOptions, outDir, chunksDir }: Options) { +interface Chunk { + name: string + id: string + src: string +} + +interface TemplateContext { + chunks: Chunk[] + prefix: string +} + +export function dynamicRequire ({ dir, globbyOptions, inline, outDir, prefix = '' }: Options) { return { name: PLUGIN_NAME, transform (code: string, _id: string) { @@ -51,30 +34,60 @@ export default function dynamicRequire ({ dir, globbyOptions, outDir, chunksDir resolveId (id: string) { return id === HELPER_DYNAMIC ? id : null }, - async load (id: string) { - if (id === HELPER_DYNAMIC) { - const files = await globby('**/*.js', { cwd: dir, absolute: false, ...globbyOptions }) - - const imports = files.map(id => ({ - id, - import: resolve(dir, id), - name: '_' + id.replace(/[\\/.]/g, '_') - })) - - if (!outDir) { - return TMPL_ESM_INLINE({ imports }) - } - - // Write chunks - chunksDir = chunksDir || basename(dir) - await Promise.all(imports.map(async (i) => { - const dst = resolve(outDir, chunksDir, i.id) - await mkdirp(dirname(dst)) - await copyFile(i.import, dst) - })) - return TMPL_CJS_LAZY({ chunksDir, imports }) + async load (_id: string) { + if (_id !== HELPER_DYNAMIC) { + return null } - return null + + // Scan chunks + const files = await globby('**/*.js', { cwd: dir, absolute: false, ...globbyOptions }) + const chunks = files.map(id => ({ + id, + src: resolve(dir, id).replace(/\\/g, '/'), + out: prefix + id, + name: '_' + id.replace(/[\\/.]/g, '_') + })) + + // Inline mode + if (inline) { + return TMPL_ESM_INLINE({ chunks, prefix }) + } + + // Write chunks + await Promise.all(chunks.map(async (chunk) => { + const dst = resolve(outDir, prefix + chunk.id) + await mkdirp(dirname(dst)) + await copyFile(chunk.src, dst) + })) + + return TMPL_CJS_LAZY({ chunks, prefix }) } } } + +function TMPL_ESM_INLINE ({ chunks }: TemplateContext) { + return `${chunks.map(i => `import ${i.name} from '${i.src}'`).join('\n')} +const dynamicChunks = { + ${chunks.map(i => ` ['${i.id}']: ${i.name}`).join(',\n')} +}; + +export default function dynamicRequire(id) { + return dynamicChunks[id]; +};` +} + +function TMPL_CJS_LAZY ({ chunks, prefix }: TemplateContext) { + return `const dynamicChunks = { +${chunks.map(i => ` ['${i.id}']: () => require('${prefix}${i.id}')`).join(',\n')} +}; + +export default function dynamicRequire(id) { + return dynamicChunks[id](); +};` +} + +// function TMPL_CJS ({ prefix }: TemplateContext) { +// return `export default function dynamicRequire(id) { +// return require('${prefix}' + id); +// };` +// } diff --git a/packages/nitro/src/rollup/externals.ts b/packages/nitro/src/rollup/externals.ts new file mode 100644 index 0000000000..f0515d73a9 --- /dev/null +++ b/packages/nitro/src/rollup/externals.ts @@ -0,0 +1,24 @@ +import { isAbsolute, relative } from 'path' + +export function externals ({ include = [], relativeTo }) { + return { + name: 'externals', + resolveId (source) { + if ( + source[0] === '.' || // Compile relative imports + source[0] === '\x00' || // Skip helpers + source.includes('?') || // Skip helpers + include.find(i => source.startsWith(i)) + ) { return null } + + if (!isAbsolute(source)) { + source = require.resolve(source) + } + + return { + id: relative(relativeTo, source), + external: true + } + } + } +} diff --git a/packages/nitro/src/targets/browser.ts b/packages/nitro/src/targets/browser.ts index 26baff5f78..deac5e57f2 100644 --- a/packages/nitro/src/targets/browser.ts +++ b/packages/nitro/src/targets/browser.ts @@ -18,6 +18,7 @@ if ('serviceWorker' in navigator) { entry: '{{ runtimeDir }}/targets/service-worker', targetDir: '{{ publicDir }}', outName: '_nuxt.js', + cleanTargetDir: false, nuxtHooks: { 'vue-renderer:ssr:templateParams' (params) { params.APP += script diff --git a/packages/nitro/src/targets/cjs.ts b/packages/nitro/src/targets/cjs.ts new file mode 100644 index 0000000000..4a6a8aaa00 --- /dev/null +++ b/packages/nitro/src/targets/cjs.ts @@ -0,0 +1,10 @@ +import { extendTarget } from '../utils' +import { SLSTarget } from '../config' +import { node } from './node' + +export const cjs: SLSTarget = extendTarget(node, { + entry: '{{ runtimeDir }}/targets/cjs', + minify: false, + externals: true, + inlineChunks: true +}) diff --git a/packages/nitro/src/targets/index.ts b/packages/nitro/src/targets/index.ts index d7d4eb3c83..cba2c91749 100644 --- a/packages/nitro/src/targets/index.ts +++ b/packages/nitro/src/targets/index.ts @@ -3,5 +3,6 @@ export * from './cloudflare' export * from './lambda' export * from './netlify' export * from './node' +export * from './cjs' export * from './vercel' export * from './worker' diff --git a/packages/nitro/src/targets/lambda.ts b/packages/nitro/src/targets/lambda.ts index d151019663..2c151980ed 100644 --- a/packages/nitro/src/targets/lambda.ts +++ b/packages/nitro/src/targets/lambda.ts @@ -1,16 +1,8 @@ -import { relative } from 'path' -import consola from 'consola' import { SLSTarget } from '../config' export const lambda: SLSTarget = { entry: '{{ runtimeDir }}/targets/lambda', outName: '_nuxt.js', - inlineChunks: false, - hooks: { - 'done' ({ rollupConfig }) { - const entry = relative(process.cwd(), rollupConfig.output.file).replace(/\.js$/, '') - consola.info(`Ready to deploy lambda: \`${entry}\``) - } - } + inlineChunks: false } diff --git a/packages/nitro/src/targets/node.ts b/packages/nitro/src/targets/node.ts index f1a009917e..71025fd15a 100644 --- a/packages/nitro/src/targets/node.ts +++ b/packages/nitro/src/targets/node.ts @@ -1,18 +1,7 @@ -import { relative } from 'path' -import consola from 'consola' import { SLSTarget } from '../config' export const node: SLSTarget = { entry: '{{ runtimeDir }}/targets/node', outName: 'index.js', - inlineChunks: false, - hooks: { - 'done' ({ rollupConfig }) { - const entry = relative(process.cwd(), rollupConfig.output.file) - .replace(/\.js$/, '') - .replace(/\/index$/, '') - consola.info(`Ready to deploy node entrypoint: \`${entry}\``) - consola.info(`You can try using \`node ${entry} [path]\``) - } - } + inlineChunks: false } diff --git a/packages/nitro/src/utils.ts b/packages/nitro/src/utils.ts index d228a8047c..83246a8ee0 100644 --- a/packages/nitro/src/utils.ts +++ b/packages/nitro/src/utils.ts @@ -6,6 +6,8 @@ import Hookable from 'hookable' import consola from 'consola' import { SLSOptions, UnresolvedPath, SLSTarget, SLSTargetFn, SLSConfig } from './config' +export const MODULE_DIR = resolve(__dirname, '..') + export function hl (str: string) { return '`' + str + '`' } @@ -21,7 +23,7 @@ export function compileTemplate (contents: string) { export function serializeTemplate (contents: string) { // eslint-disable-next-line no-template-curly-in-string - return `export default (params) => \`${contents.replace(/{{ (\w+) }}/g, '${params.$1}')}\`` + return `(params) => \`${contents.replace(/{{ (\w+) }}/g, '${params.$1}')}\`` } export function jitiImport (dir: string, path: string) { @@ -80,3 +82,22 @@ export function extendTarget (base: SLSTarget, target: SLSTarget): SLSTargetFn { }, target, base) } } + +const _getDependenciesMode = { + dev: ['devDependencies'], + prod: ['dependencies'], + all: ['devDependencies', 'dependencies'] +} +export function getDependencies (dir: string, mode: keyof typeof _getDependenciesMode = 'all') { + const fields = _getDependenciesMode[mode] + const pkg = require(resolve(dir, 'package.json')) + const dependencies = [] + for (const field of fields) { + if (pkg[field]) { + for (const name in pkg[field]) { + dependencies.push(name) + } + } + } + return dependencies +}