Skip to content

Commit

Permalink
feat: use webpack esm server build (#474)
Browse files Browse the repository at this point in the history
  • Loading branch information
danielroe authored Sep 5, 2021
1 parent cb3affa commit 009579a
Show file tree
Hide file tree
Showing 4 changed files with 163 additions and 59 deletions.
8 changes: 8 additions & 0 deletions src/compat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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',
Expand Down
15 changes: 7 additions & 8 deletions src/rollup/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
67 changes: 16 additions & 51 deletions src/rollup/plugins/dynamic-require.ts
Original file line number Diff line number Diff line change
@@ -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
}
Expand All @@ -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: ')'
Expand All @@ -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, '/'),
Expand All @@ -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")
}
}
}
}
Expand All @@ -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) {
Expand Down
132 changes: 132 additions & 0 deletions src/webpack/wp4.ts
Original file line number Diff line number Diff line change
@@ -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);'
]),
'}'
]),
'}'
])
})
})
}
}

0 comments on commit 009579a

Please sign in to comment.