diff --git a/src/compat.ts b/src/compat.ts index d53c8b141b..84708f2804 100644 --- a/src/compat.ts +++ b/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/src/rollup/config.ts b/src/rollup/config.ts index 4ecfdd0807..d6bd1387c1 100644 --- a/src/rollup/config.ts +++ b/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/src/rollup/plugins/dynamic-require.ts b/src/rollup/plugins/dynamic-require.ts index 7182ff4747..8f7e0bc60a 100644 --- a/src/rollup/plugins/dynamic-require.ts +++ b/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/src/webpack/wp4.ts b/src/webpack/wp4.ts new file mode 100644 index 0000000000..607a348def --- /dev/null +++ b/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);' + ]), + '}' + ]), + '}' + ]) + }) + }) + } +}