/
nextjs-require-cache-hot-reloader.ts
123 lines (107 loc) · 3.8 KB
/
nextjs-require-cache-hot-reloader.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
import type { webpack } from 'next/dist/compiled/webpack/webpack'
import { clearModuleContext } from '../../../server/web/sandbox'
import { realpathSync } from '../../../lib/realpath'
import path from 'path'
import isError from '../../../lib/is-error'
import { clearManifestCache } from '../../../server/load-manifest'
type Compiler = webpack.Compiler
type WebpackPluginInstance = webpack.WebpackPluginInstance
const originModules = [
require.resolve('../../../server/require'),
require.resolve('../../../server/load-components'),
require.resolve('../../../server/next-server'),
require.resolve('../../../server/app-render/use-flight-response'),
require.resolve('../../../compiled/react-server-dom-webpack/client.edge'),
require.resolve(
'../../../compiled/react-server-dom-webpack-experimental/client.edge'
),
]
const RUNTIME_NAMES = ['webpack-runtime', 'webpack-api-runtime']
function deleteFromRequireCache(filePath: string) {
try {
filePath = realpathSync(filePath)
} catch (e) {
if (isError(e) && e.code !== 'ENOENT') throw e
}
const mod = require.cache[filePath]
if (mod) {
// remove the child reference from the originModules
for (const originModule of originModules) {
const parent = require.cache[originModule]
if (parent) {
const idx = parent.children.indexOf(mod)
if (idx >= 0) parent.children.splice(idx, 1)
}
}
// remove parent references from external modules
for (const child of mod.children) {
child.parent = null
}
delete require.cache[filePath]
return true
}
return false
}
export function deleteAppClientCache() {
// ensure we reset the cache for rsc components
// loaded via react-server-dom-webpack
const reactServerDomModId = require.resolve(
'react-server-dom-webpack/client.edge'
)
const reactServerDomMod = require.cache[reactServerDomModId]
if (reactServerDomMod) {
for (const child of [...reactServerDomMod.children]) {
deleteFromRequireCache(child.id)
}
deleteFromRequireCache(reactServerDomModId)
}
}
export function deleteCache(filePath: string) {
// try to clear it from the fs cache
clearManifestCache(filePath)
deleteFromRequireCache(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
hasServerComponents: boolean
constructor(opts: { hasServerComponents: boolean }) {
this.hasServerComponents = opts.hasServerComponents
}
apply(compiler: Compiler) {
compiler.hooks.assetEmitted.tap(PLUGIN_NAME, (_file, { targetPath }) => {
// Clear module context in this process
clearModuleContext(targetPath)
deleteCache(targetPath)
})
compiler.hooks.afterEmit.tapPromise(PLUGIN_NAME, async (compilation) => {
for (const name of RUNTIME_NAMES) {
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
let hasAppEntry = false
const entries = [...compilation.entries.keys()].filter((entry) => {
const isAppPath = entry.toString().startsWith('app/')
if (isAppPath) hasAppEntry = true
return entry.toString().startsWith('pages/') || isAppPath
})
if (hasAppEntry) {
deleteAppClientCache()
}
for (const page of entries) {
const outputPath = path.join(
compilation.outputOptions.path!,
page + '.js'
)
deleteCache(outputPath)
}
})
}
}