feat: support dynamic chunks, lazy middleware and cjs target

This commit is contained in:
Pooya Parsa 2020-11-14 14:05:09 +01:00
parent e3609b6d8a
commit 1e34041e8d
12 changed files with 221 additions and 144 deletions

View File

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

View File

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

View File

@ -13,9 +13,7 @@ export default <Module> 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 <Module> 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 <Module> 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)
})
}

View File

@ -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(), "") : "<entry>"})`);'
}
// 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
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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