feat: use webpack esm server build (#474)

This commit is contained in:
Daniel Roe 2021-09-05 21:33:24 +01:00 committed by GitHub
parent 7527d30bed
commit 193d7bf8bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 187 additions and 83 deletions

View File

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

View File

@ -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.js',
'server.cjs',
'server.mjs',
'server.manifest.mjs'
]
}
}))
// Assets

View File

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

View File

@ -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);'
]),
'}'
]),
'}'
])
})
})
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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