rsnext/packages/next/build/webpack/plugins/nextjs-require-cache-hot-reloader.ts

88 lines
2.7 KiB
TypeScript
Raw Normal View History

import type { webpack5 } from 'next/dist/compiled/webpack/webpack'
Refactor sandbox module cache (#31822) To run middleware we are using a **sandbox** that emulates the web runtime and keeps a module cache. This cache is shared for all of the modules that we run using the sandbox while there are some module-level APIs that must be scoped depending on the module we are running. One example of this is `fetch` where we want to always inject a special header that indicate the module that is performing the fetch and use it to avoid getting into infinite loops for middleware. For those cases the cached implementation will be the first one that instantiates the module and therefore we can actually get into infinite loops. This is the reason why #31800 is failing. With this PR we refactor the sandbox so that the module cache is scoped per module name. This means that one execution of a middleware will preserve its cache only for that module so that each execution will still have its own `fetch` implementation, fixing this issue. Also, with this refactor the code is more clear and we also provide an option to avoid using the cache. ## Bug - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Errors have helpful link attached, see `contributing.md` ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have helpful link attached, see `contributing.md` ## Documentation / Examples - [ ] Make sure the linting passes by running `yarn lint`
2021-11-26 13:06:41 +01:00
import { clearModuleContext } from '../../../server/web/sandbox'
import { realpathSync } from 'fs'
import path from 'path'
import isError from '../../../lib/is-error'
type Compiler = webpack5.Compiler
type WebpackPluginInstance = webpack5.WebpackPluginInstance
const originModules = [
require.resolve('../../../server/require'),
require.resolve('../../../server/load-components'),
]
const RUNTIME_NAMES = ['webpack-runtime', 'webpack-api-runtime']
function deleteCache(filePath: string) {
try {
filePath = realpathSync(filePath)
} catch (e) {
if (isError(e) && e.code !== 'ENOENT') throw e
}
const module = require.cache[filePath]
if (module) {
// remove the child reference from the originModules
for (const originModule of originModules) {
const parent = require.cache[originModule]
if (parent) {
const idx = parent.children.indexOf(module)
if (idx >= 0) parent.children.splice(idx, 1)
}
}
// remove parent references from external modules
for (const child of module.children) {
child.parent = null
}
}
delete require.cache[filePath]
}
const PLUGIN_NAME = 'NextJsRequireCacheHotReloader'
// This plugin flushes require.cache after emitting the files. Providing 'hot reloading' of server files.
export class NextJsRequireCacheHotReloader implements WebpackPluginInstance {
prevAssets: any = null
previousOutputPathsWebpack5: Set<string> = new Set()
currentOutputPathsWebpack5: Set<string> = new Set()
apply(compiler: Compiler) {
compiler.hooks.assetEmitted.tap(
PLUGIN_NAME,
(_file, { targetPath, content }) => {
this.currentOutputPathsWebpack5.add(targetPath)
deleteCache(targetPath)
Refactor sandbox module cache (#31822) To run middleware we are using a **sandbox** that emulates the web runtime and keeps a module cache. This cache is shared for all of the modules that we run using the sandbox while there are some module-level APIs that must be scoped depending on the module we are running. One example of this is `fetch` where we want to always inject a special header that indicate the module that is performing the fetch and use it to avoid getting into infinite loops for middleware. For those cases the cached implementation will be the first one that instantiates the module and therefore we can actually get into infinite loops. This is the reason why #31800 is failing. With this PR we refactor the sandbox so that the module cache is scoped per module name. This means that one execution of a middleware will preserve its cache only for that module so that each execution will still have its own `fetch` implementation, fixing this issue. Also, with this refactor the code is more clear and we also provide an option to avoid using the cache. ## Bug - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Errors have helpful link attached, see `contributing.md` ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have helpful link attached, see `contributing.md` ## Documentation / Examples - [ ] Make sure the linting passes by running `yarn lint`
2021-11-26 13:06:41 +01:00
clearModuleContext(targetPath, content.toString('utf-8'))
}
)
compiler.hooks.afterEmit.tap(PLUGIN_NAME, (compilation) => {
RUNTIME_NAMES.forEach((name) => {
const runtimeChunkPath = path.join(
compilation.outputOptions.path!,
`${name}.js`
)
deleteCache(runtimeChunkPath)
})
// we need to make sure to clear all server entries from cache
// since they can have a stale webpack-runtime cache
// which needs to always be in-sync
const entries = [...compilation.entries.keys()].filter((entry) =>
entry.toString().startsWith('pages/')
)
entries.forEach((page) => {
const outputPath = path.join(
compilation.outputOptions.path!,
page + '.js'
)
deleteCache(outputPath)
})
})
this.previousOutputPathsWebpack5 = new Set(this.currentOutputPathsWebpack5)
this.currentOutputPathsWebpack5.clear()
}
}