From 193d7bf8bc24618b63e1f67ffcf6d9d0e4e7d10e Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Sun, 5 Sep 2021 21:33:24 +0100 Subject: [PATCH] feat: use webpack esm server build (#474) --- packages/nitro/src/compat.ts | 8 ++ packages/nitro/src/rollup/config.ts | 15 +- .../src/rollup/plugins/dynamic-require.ts | 67 +++------ packages/nitro/src/webpack/wp4.ts | 132 ++++++++++++++++++ packages/webpack/package.json | 2 +- packages/webpack/src/configs/server.ts | 2 +- packages/webpack/src/plugins/vue/server.ts | 8 -- packages/webpack/src/plugins/vue/util.ts | 5 - packages/webpack/src/presets/base.ts | 1 + packages/webpack/src/presets/esbuild.ts | 2 +- packages/webpack/src/presets/node.ts | 18 ++- yarn.lock | 10 +- 12 files changed, 187 insertions(+), 83 deletions(-) create mode 100644 packages/nitro/src/webpack/wp4.ts diff --git a/packages/nitro/src/compat.ts b/packages/nitro/src/compat.ts index d53c8b141b..84708f2804 100644 --- a/packages/nitro/src/compat.ts +++ b/packages/nitro/src/compat.ts @@ -7,6 +7,7 @@ import { getNitroContext, NitroContext } from './context' import { createDevServer } from './server/dev' import { wpfs } from './utils/wpfs' import { resolveMiddleware } from './server/middleware' +import AsyncLoadingPlugin from './webpack/wp4' export default function nuxt2CompatModule (this: ModuleContainer) { const { nuxt } = this @@ -64,6 +65,13 @@ export default function nuxt2CompatModule (this: ModuleContainer) { } }) + // Set up webpack plugin for node async loading + nuxt.hook('webpack:config', (webpackConfigs) => { + const serverConfig = webpackConfigs.find(config => config.name === 'server') + serverConfig.plugins = serverConfig.plugins || [] + serverConfig.plugins.push(new AsyncLoadingPlugin()) + }) + // Nitro client plugin this.addPlugin({ fileName: 'nitro.client.mjs', diff --git a/packages/nitro/src/rollup/config.ts b/packages/nitro/src/rollup/config.ts index 4ecfdd0807..d6bd1387c1 100644 --- a/packages/nitro/src/rollup/config.ts +++ b/packages/nitro/src/rollup/config.ts @@ -161,14 +161,13 @@ export const getRollupConfig = (nitroContext: NitroContext) => { rollupConfig.plugins.push(dynamicRequire({ dir: resolve(nitroContext._nuxt.buildDir, 'dist/server'), inline: nitroContext.node === false || nitroContext.inlineDynamicImports, - globbyOptions: { - ignore: [ - 'client.manifest.mjs', - 'server.cjs', - 'server.mjs', - 'server.manifest.mjs' - ] - } + ignore: [ + 'client.manifest.mjs', + 'server.js', + 'server.cjs', + 'server.mjs', + 'server.manifest.mjs' + ] })) // Assets diff --git a/packages/nitro/src/rollup/plugins/dynamic-require.ts b/packages/nitro/src/rollup/plugins/dynamic-require.ts index 7182ff4747..8f7e0bc60a 100644 --- a/packages/nitro/src/rollup/plugins/dynamic-require.ts +++ b/packages/nitro/src/rollup/plugins/dynamic-require.ts @@ -1,15 +1,15 @@ import { resolve } from 'upath' -import globby, { GlobbyOptions } from 'globby' +import globby from 'globby' import type { Plugin } from 'rollup' const PLUGIN_NAME = 'dynamic-require' const HELPER_DYNAMIC = `\0${PLUGIN_NAME}.js` -const DYNAMIC_REQUIRE_RE = /require\("\.\/" ?\+/g +const DYNAMIC_REQUIRE_RE = /import\("\.\/" ?\+(.*)\).then/g interface Options { dir: string inline: boolean - globbyOptions: GlobbyOptions + ignore: string[] outDir?: string prefix?: string } @@ -29,19 +29,19 @@ interface TemplateContext { chunks: Chunk[] } -export function dynamicRequire ({ dir, globbyOptions, inline }: Options): Plugin { +export function dynamicRequire ({ dir, ignore, inline }: Options): Plugin { return { name: PLUGIN_NAME, transform (code: string, _id: string) { return { - code: code.replace(DYNAMIC_REQUIRE_RE, `require('${HELPER_DYNAMIC}')(`), + code: code.replace(DYNAMIC_REQUIRE_RE, `import('${HELPER_DYNAMIC}').then(r => r.default || r).then(dynamicRequire => dynamicRequire($1)).then`), map: null } }, resolveId (id: string) { return id === HELPER_DYNAMIC ? id : null }, - // TODO: Async chunk loading over netwrok! + // TODO: Async chunk loading over network! // renderDynamicImport () { // return { // left: 'fetch(', right: ')' @@ -53,7 +53,13 @@ export function dynamicRequire ({ dir, globbyOptions, inline }: Options): Plugin } // Scan chunks - const files = await globby('**/*.{cjs,mjs,js}', { cwd: dir, absolute: false, ...globbyOptions }) + let files = [] + try { + const wpManifest = resolve(dir, './server.manifest.json') + files = await import(wpManifest).then(r => Object.keys(r.files).filter(file => !ignore.includes(file))) + } catch { + files = await globby('**/*.{cjs,mjs,js}', { cwd: dir, absolute: false, ignore }) + } const chunks = files.map(id => ({ id, src: resolve(dir, id).replace(/\\/g, '/'), @@ -62,20 +68,6 @@ export function dynamicRequire ({ dir, globbyOptions, inline }: Options): Plugin })) return inline ? TMPL_INLINE({ chunks }) : TMPL_LAZY({ chunks }) - }, - renderChunk (code) { - if (inline) { - return { - map: null, - code - } - } - return { - map: null, - code: code.replace( - /Promise.resolve\(\).then\(function \(\) \{ return require\('([^']*)' \/\* webpackChunk \*\/\); \}\).then\(function \(n\) \{ return n.([_a-zA-Z0-9]*); \}\)/g, - "require('$1').$2") - } } } } @@ -91,47 +83,20 @@ function getWebpackChunkMeta (src: string) { } function TMPL_INLINE ({ chunks }: TemplateContext) { - return `${chunks.map(i => `import ${i.name} from '${i.src}'`).join('\n')} + return `${chunks.map(i => `import * as ${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]; + return Promise.resolve(dynamicChunks[id]); };` } function TMPL_LAZY ({ chunks }: TemplateContext) { return ` -function dynamicWebpackModule(id, getChunk, ids) { - return function (module, exports, require) { - const r = getChunk() - if (typeof r.then === 'function') { - module.exports = r.then(r => { - const realModule = { exports: {}, require }; - r.modules[id](realModule, realModule.exports, realModule.require); - for (const _id of ids) { - if (_id === id) continue; - r.modules[_id](realModule, realModule.exports, realModule.require); - } - return realModule.exports; - }); - } else if (r && typeof r.modules[id] === 'function') { - r.modules[id](module, exports, require); - } - }; -}; - -function webpackChunk (meta, getChunk) { - const chunk = { ...meta, modules: {} }; - for (const id of meta.moduleIds) { - chunk.modules[id] = dynamicWebpackModule(id, getChunk, meta.moduleIds); - }; - return chunk; -}; - const dynamicChunks = { -${chunks.map(i => ` ['${i.id}']: () => webpackChunk(${JSON.stringify(i.meta)}, () => import('${i.src}' /* webpackChunk */))`).join(',\n')} +${chunks.map(i => ` ['${i.id}']: () => import('${i.src}')`).join(',\n')} }; export default function dynamicRequire(id) { diff --git a/packages/nitro/src/webpack/wp4.ts b/packages/nitro/src/webpack/wp4.ts new file mode 100644 index 0000000000..607a348def --- /dev/null +++ b/packages/nitro/src/webpack/wp4.ts @@ -0,0 +1,132 @@ +// Based on https://github.com/webpack/webpack/blob/v4.46.0/lib/node/NodeMainTemplatePlugin.js#L81-L191 + +import { Compiler } from 'webpack' +import Template from 'webpack/lib/Template' + +export default class AsyncLoadingPlugin { + apply (compiler: Compiler) { + compiler.hooks.compilation.tap('AsyncLoading', (compilation) => { + const mainTemplate = compilation.mainTemplate + mainTemplate.hooks.requireEnsure.tap( + 'AsyncLoading', + (_source, chunk, hash) => { + const chunkFilename = mainTemplate.outputOptions.chunkFilename + const chunkMaps = chunk.getChunkMaps() + const insertMoreModules = [ + 'var moreModules = chunk.modules, chunkIds = chunk.ids;', + 'for(var moduleId in moreModules) {', + Template.indent( + mainTemplate.renderAddModule( + hash, + chunk, + 'moduleId', + 'moreModules[moduleId]' + ) + ), + '}' + ] + return Template.asString([ + '// Async chunk loading for Nitro', + '', + 'var installedChunkData = installedChunks[chunkId];', + 'if(installedChunkData !== 0) { // 0 means "already installed".', + Template.indent([ + '// array of [resolve, reject, promise] means "currently loading"', + 'if(installedChunkData) {', + Template.indent(['promises.push(installedChunkData[2]);']), + '} else {', + Template.indent([ + '// load the chunk and return promise to it', + 'var promise = new Promise(function(resolve, reject) {', + Template.indent([ + 'installedChunkData = installedChunks[chunkId] = [resolve, reject];', + 'import(' + + mainTemplate.getAssetPath( + JSON.stringify(`./${chunkFilename}`), + { + hash: `" + ${mainTemplate.renderCurrentHashCode( + hash + )} + "`, + hashWithLength: length => + `" + ${mainTemplate.renderCurrentHashCode( + hash, + length + )} + "`, + chunk: { + id: '" + chunkId + "', + hash: `" + ${JSON.stringify( + chunkMaps.hash + )}[chunkId] + "`, + hashWithLength: (length) => { + const shortChunkHashMap = {} + for (const chunkId of Object.keys(chunkMaps.hash)) { + if (typeof chunkMaps.hash[chunkId] === 'string') { + shortChunkHashMap[chunkId] = chunkMaps.hash[ + chunkId + ].substr(0, length) + } + } + return `" + ${JSON.stringify( + shortChunkHashMap + )}[chunkId] + "` + }, + contentHash: { + javascript: `" + ${JSON.stringify( + chunkMaps.contentHash.javascript + )}[chunkId] + "` + }, + contentHashWithLength: { + javascript: (length) => { + const shortContentHashMap = {} + const contentHash = + chunkMaps.contentHash.javascript + for (const chunkId of Object.keys(contentHash)) { + if (typeof contentHash[chunkId] === 'string') { + shortContentHashMap[chunkId] = contentHash[ + chunkId + ].substr(0, length) + } + } + return `" + ${JSON.stringify( + shortContentHashMap + )}[chunkId] + "` + } + }, + name: `" + (${JSON.stringify( + chunkMaps.name + )}[chunkId]||chunkId) + "` + }, + contentHashType: 'javascript' + } + ) + + ').then(chunk => {', + Template.indent( + insertMoreModules + .concat([ + 'var callbacks = [];', + 'for(var i = 0; i < chunkIds.length; i++) {', + Template.indent([ + 'if(installedChunks[chunkIds[i]])', + Template.indent([ + 'callbacks = callbacks.concat(installedChunks[chunkIds[i]][0]);' + ]), + 'installedChunks[chunkIds[i]] = 0;' + ]), + '}', + 'for(i = 0; i < callbacks.length; i++)', + Template.indent('callbacks[i]();') + ]) + ), + '});' + ]), + '});', + 'promises.push(installedChunkData[2] = promise);' + ]), + '}' + ]), + '}' + ]) + }) + }) + } +} diff --git a/packages/webpack/package.json b/packages/webpack/package.json index f148fbbef3..40a7a94822 100644 --- a/packages/webpack/package.json +++ b/packages/webpack/package.json @@ -40,7 +40,7 @@ "vue": "3.2.2", "vue-loader": "^16.5.0", "vue-style-loader": "^4.1.3", - "webpack": "^5.50.0", + "webpack": "^5.52.0", "webpack-bundle-analyzer": "^4.4.2", "webpack-dev-middleware": "^5.0.0", "webpack-hot-middleware": "^2.25.0", diff --git a/packages/webpack/src/configs/server.ts b/packages/webpack/src/configs/server.ts index 3668c28196..8a11dfb6e9 100644 --- a/packages/webpack/src/configs/server.ts +++ b/packages/webpack/src/configs/server.ts @@ -22,7 +22,7 @@ export function server (ctx: WebpackConfigContext) { function serverPreset (ctx: WebpackConfigContext) { const { config } = ctx - config.output.filename = 'server.cjs' + config.output.filename = 'server.mjs' config.devtool = 'cheap-module-source-map' config.optimization = { diff --git a/packages/webpack/src/plugins/vue/server.ts b/packages/webpack/src/plugins/vue/server.ts index f2c1072cc5..1ef59f5a96 100644 --- a/packages/webpack/src/plugins/vue/server.ts +++ b/packages/webpack/src/plugins/vue/server.ts @@ -81,14 +81,6 @@ export default class VueSSRServerPlugin { size: () => mjsSrc.length } - // TODO: Workaround for webpack - const serverJS = 'export { default } from "./server.cjs"' - assets['server.mjs'] = { - source: () => serverJS, - map: () => null, - size: () => serverJS.length - } - cb() }) }) diff --git a/packages/webpack/src/plugins/vue/util.ts b/packages/webpack/src/plugins/vue/util.ts index d419f11e93..3cdf60dc99 100644 --- a/packages/webpack/src/plugins/vue/util.ts +++ b/packages/webpack/src/plugins/vue/util.ts @@ -10,11 +10,6 @@ export const validate = (compiler) => { consola.warn('webpack config `target` should be "node".') } - const libraryType = compiler.options.output.library.type - if (libraryType !== 'commonjs2') { - consola.warn('webpack config `output.libraryTarget` should be "commonjs2".') - } - if (!compiler.options.externals) { consola.info( 'It is recommended to externalize dependencies in the server build for ' + diff --git a/packages/webpack/src/presets/base.ts b/packages/webpack/src/presets/base.ts index 240d615cc3..fe6625299d 100644 --- a/packages/webpack/src/presets/base.ts +++ b/packages/webpack/src/presets/base.ts @@ -31,6 +31,7 @@ function baseConfig (ctx: WebpackConfigContext) { ...options.build.optimization as any, minimizer: [] }, + experiments: {}, mode: ctx.isDev ? 'development' : 'production', cache: getCache(ctx), output: getOutput(ctx), diff --git a/packages/webpack/src/presets/esbuild.ts b/packages/webpack/src/presets/esbuild.ts index 44d117aba3..f720cbc141 100644 --- a/packages/webpack/src/presets/esbuild.ts +++ b/packages/webpack/src/presets/esbuild.ts @@ -7,7 +7,7 @@ export function esbuild (ctx: WebpackConfigContext) { // https://esbuild.github.io/getting-started/#bundling-for-the-browser // https://gs.statcounter.com/browser-version-market-share // https://nodejs.org/en/ - const target = ctx.isServer ? 'node14' : 'chrome85' + const target = ctx.isServer ? 'es2019' : 'chrome85' config.optimization.minimizer.push(new ESBuildMinifyPlugin()) diff --git a/packages/webpack/src/presets/node.ts b/packages/webpack/src/presets/node.ts index 9f515e0dd1..e98c987302 100644 --- a/packages/webpack/src/presets/node.ts +++ b/packages/webpack/src/presets/node.ts @@ -6,13 +6,25 @@ export function node (ctx: WebpackConfigContext) { config.target = 'node' config.node = false - config.resolve.mainFields = ['main', 'module'] + config.experiments.outputModule = true config.output = { ...config.output, - chunkFilename: '[name].cjs', + chunkFilename: '[name].mjs', + chunkFormat: 'module', + chunkLoading: 'import', + module: true, + environment: { + module: true, + arrowFunction: true, + bigIntLiteral: true, + const: true, + destructuring: true, + dynamicImport: true, + forOf: true + }, library: { - type: 'commonjs2' + type: 'module' } } diff --git a/yarn.lock b/yarn.lock index 26daa3acd9..a612ae9bcf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1456,7 +1456,7 @@ __metadata: vue: 3.2.2 vue-loader: ^16.5.0 vue-style-loader: ^4.1.3 - webpack: ^5.50.0 + webpack: ^5.52.0 webpack-bundle-analyzer: ^4.4.2 webpack-dev-middleware: ^5.0.0 webpack-hot-middleware: ^2.25.0 @@ -12195,9 +12195,9 @@ typescript@^4.3.5: languageName: node linkType: hard -"webpack@npm:^5, webpack@npm:^5.1.0, webpack@npm:^5.38.1, webpack@npm:^5.50.0": - version: 5.50.0 - resolution: "webpack@npm:5.50.0" +"webpack@npm:^5, webpack@npm:^5.1.0, webpack@npm:^5.38.1, webpack@npm:^5.52.0": + version: 5.52.0 + resolution: "webpack@npm:5.52.0" dependencies: "@types/eslint-scope": ^3.7.0 "@types/estree": ^0.0.50 @@ -12228,7 +12228,7 @@ typescript@^4.3.5: optional: true bin: webpack: bin/webpack.js - checksum: 293bed1d9101ac127605f35a225a5cbc1bc89eac68d6e09e7feb3e284ec2693b3db7c1dd7710fadf6852f89ad39ed09413c35befa1cfc9738074b33299ac2b9e + checksum: fe7cbb761b251a6885d67971f8763a9675ca4777ff863be4cbe76a6ab22a3f810be2728fe7b9c31f74259001859a3915ad581f0e4aca5255cdb13ccab3472f00 languageName: node linkType: hard