@remix-run/node-hmr 0.0.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +305 -2
- package/dist/index.d.ts +128 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +107 -0
- package/dist/lib/browser-events.d.ts +87 -0
- package/dist/lib/browser-events.d.ts.map +1 -0
- package/dist/lib/browser-events.js +11 -0
- package/dist/lib/events.d.ts +29 -0
- package/dist/lib/events.d.ts.map +1 -0
- package/dist/lib/events.js +32 -0
- package/dist/lib/hmr-analysis.d.ts +17 -0
- package/dist/lib/hmr-analysis.d.ts.map +1 -0
- package/dist/lib/hmr-analysis.js +130 -0
- package/dist/lib/module-store.d.ts +27 -0
- package/dist/lib/module-store.d.ts.map +1 -0
- package/dist/lib/module-store.js +161 -0
- package/dist/lib/process-state.d.ts +3 -0
- package/dist/lib/process-state.d.ts.map +1 -0
- package/dist/lib/process-state.js +7 -0
- package/dist/lib/runner.d.ts +62 -0
- package/dist/lib/runner.d.ts.map +1 -0
- package/dist/lib/runner.js +1050 -0
- package/dist/lib/runtime-api.d.ts +7 -0
- package/dist/lib/runtime-api.d.ts.map +1 -0
- package/dist/lib/runtime-api.js +1 -0
- package/dist/lib/runtime.d.ts +46 -0
- package/dist/lib/runtime.d.ts.map +1 -0
- package/dist/lib/runtime.js +374 -0
- package/dist/register.d.ts +2 -0
- package/dist/register.d.ts.map +1 -0
- package/dist/register.js +317 -0
- package/dist/runtime.d.ts +26 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +32 -0
- package/dist/runtime.node-hmr.d.ts +27 -0
- package/dist/runtime.node-hmr.d.ts.map +1 -0
- package/dist/runtime.node-hmr.js +33 -0
- package/dist/types.d.ts +36 -0
- package/package.json +55 -5
- package/src/index.ts +244 -0
- package/src/lib/browser-events.ts +113 -0
- package/src/lib/events.ts +61 -0
- package/src/lib/hmr-analysis.ts +178 -0
- package/src/lib/module-store.ts +228 -0
- package/src/lib/process-state.ts +9 -0
- package/src/lib/runner.ts +1429 -0
- package/src/lib/runtime-api.ts +9 -0
- package/src/lib/runtime.ts +534 -0
- package/src/register.ts +401 -0
- package/src/runtime.node-hmr.ts +40 -0
- package/src/runtime.ts +40 -0
- package/src/types.d.ts +36 -0
|
@@ -0,0 +1,1429 @@
|
|
|
1
|
+
import { spawn, type ChildProcess } from 'node:child_process'
|
|
2
|
+
import type { Stats } from 'node:fs'
|
|
3
|
+
import { createServer, type Server, type ServerResponse } from 'node:http'
|
|
4
|
+
import type { AddressInfo } from 'node:net'
|
|
5
|
+
import process from 'node:process'
|
|
6
|
+
import { pathToFileURL } from 'node:url'
|
|
7
|
+
import { dirname, relative, resolve } from 'node:path'
|
|
8
|
+
|
|
9
|
+
import { createStyles } from '@remix-run/terminal'
|
|
10
|
+
import { watch } from 'chokidar'
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
type BrowserHmrFileEvent,
|
|
14
|
+
defaultBrowserHmrPathname,
|
|
15
|
+
type BrowserHmrEvent,
|
|
16
|
+
type HmrEventPayload,
|
|
17
|
+
} from './browser-events.ts'
|
|
18
|
+
import { createModuleStore, type ModuleRecord } from './module-store.ts'
|
|
19
|
+
|
|
20
|
+
interface NodeHmrUpdate {
|
|
21
|
+
acceptedUrl: string
|
|
22
|
+
filePath: string
|
|
23
|
+
invalidatedUrls: Record<string, number>
|
|
24
|
+
url: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type ChildMessage =
|
|
28
|
+
| {
|
|
29
|
+
requestId: number
|
|
30
|
+
type: 'node-hmr:child:browser-hmr-channel-requested'
|
|
31
|
+
}
|
|
32
|
+
| {
|
|
33
|
+
payload: HmrEventPayload
|
|
34
|
+
type: 'node-hmr:child:browser-event-emitted'
|
|
35
|
+
}
|
|
36
|
+
| {
|
|
37
|
+
delta: { add: string[]; remove: string[] }
|
|
38
|
+
id: number
|
|
39
|
+
type: 'node-hmr:child:browser-hmr-watch-files-changed'
|
|
40
|
+
}
|
|
41
|
+
| {
|
|
42
|
+
error?: string
|
|
43
|
+
events: BrowserHmrEvent[]
|
|
44
|
+
requestId: number
|
|
45
|
+
type: 'node-hmr:child:browser-hmr-file-events-handled'
|
|
46
|
+
}
|
|
47
|
+
| {
|
|
48
|
+
type: 'node-hmr:child:restart-requested'
|
|
49
|
+
message?: string
|
|
50
|
+
}
|
|
51
|
+
| {
|
|
52
|
+
type: 'node-hmr:child:module-imported'
|
|
53
|
+
depFilePath: string
|
|
54
|
+
depUrl: string
|
|
55
|
+
importerFilePath: string
|
|
56
|
+
importerUrl: string
|
|
57
|
+
}
|
|
58
|
+
| {
|
|
59
|
+
type: 'node-hmr:child:accepted-deps-resolved'
|
|
60
|
+
acceptedDeps: string[]
|
|
61
|
+
url: string
|
|
62
|
+
}
|
|
63
|
+
| {
|
|
64
|
+
type: 'node-hmr:child:module-analyzed'
|
|
65
|
+
filePath: string
|
|
66
|
+
hmr: ModuleRecord['hmr']
|
|
67
|
+
url: string
|
|
68
|
+
}
|
|
69
|
+
| {
|
|
70
|
+
acceptedUrl?: string
|
|
71
|
+
filePath: string
|
|
72
|
+
timestamp: number
|
|
73
|
+
type: 'node-hmr:child:hot-module-updated'
|
|
74
|
+
url: string
|
|
75
|
+
}
|
|
76
|
+
| {
|
|
77
|
+
acceptedUrl: string
|
|
78
|
+
message?: string
|
|
79
|
+
timestamp: number
|
|
80
|
+
type: 'node-hmr:child:hot-module-invalidated'
|
|
81
|
+
url: string
|
|
82
|
+
}
|
|
83
|
+
| {
|
|
84
|
+
type: 'node-hmr:child:server-ready'
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const restartDelayMs = 50
|
|
88
|
+
const restartSettleDelayMs = 150
|
|
89
|
+
const browserHmrEventFlushDelayMs = 75
|
|
90
|
+
const browserHmrRequestTimeoutMs = 1_000
|
|
91
|
+
const shutdownTimeoutMs = 5_000
|
|
92
|
+
const nodeHmrCondition = 'node-hmr'
|
|
93
|
+
const nodeHmrEnvVar = 'REMIX_NODE_HMR'
|
|
94
|
+
const styles = createStyles()
|
|
95
|
+
const windowsDriveLetterRE = /^[A-Za-z]:\//
|
|
96
|
+
|
|
97
|
+
export function normalizeBrowserHmrFilePath(filePath: string): string {
|
|
98
|
+
return filePath
|
|
99
|
+
.replace(/\\/g, '/')
|
|
100
|
+
.replace(windowsDriveLetterRE, (prefix) => `${prefix[0]!.toUpperCase()}${prefix.slice(1)}`)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function getBrowserHmrFileEventsForWatchedFiles(options: {
|
|
104
|
+
changedPaths: readonly string[]
|
|
105
|
+
restartPathEvents: ReadonlyMap<string, 'add' | 'unlink'>
|
|
106
|
+
watchedFiles: ReadonlySet<string>
|
|
107
|
+
}): BrowserHmrFileEvent[] {
|
|
108
|
+
return [
|
|
109
|
+
...options.changedPaths
|
|
110
|
+
.filter((filePath) => options.watchedFiles.has(normalizeBrowserHmrFilePath(filePath)))
|
|
111
|
+
.map((filePath) => ({ event: 'change' as const, filePath })),
|
|
112
|
+
...[...options.restartPathEvents]
|
|
113
|
+
.filter(([filePath]) => options.watchedFiles.has(normalizeBrowserHmrFilePath(filePath)))
|
|
114
|
+
.map(([filePath, event]) => ({ event, filePath })),
|
|
115
|
+
]
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function getWatchedDirectoriesForFiles(filePaths: Iterable<string>): Set<string> {
|
|
119
|
+
let watchedDirectories = new Set<string>()
|
|
120
|
+
for (let filePath of filePaths) {
|
|
121
|
+
watchedDirectories.add(dirname(filePath))
|
|
122
|
+
}
|
|
123
|
+
return watchedDirectories
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
type FileChangeStats = Pick<Stats, 'birthtimeMs' | 'ctimeMs' | 'ino' | 'mtimeMs' | 'size'>
|
|
127
|
+
|
|
128
|
+
export function createFileChangeEventDeduper(): (
|
|
129
|
+
filePath: string,
|
|
130
|
+
stats: FileChangeStats | undefined,
|
|
131
|
+
) => boolean {
|
|
132
|
+
let previousStatsByFilePath = new Map<string, FileChangeStats>()
|
|
133
|
+
|
|
134
|
+
return (filePath, stats) => {
|
|
135
|
+
if (stats === undefined) return false
|
|
136
|
+
|
|
137
|
+
let previousStats = previousStatsByFilePath.get(filePath)
|
|
138
|
+
let currentStats: FileChangeStats = {
|
|
139
|
+
birthtimeMs: stats.birthtimeMs,
|
|
140
|
+
ctimeMs: stats.ctimeMs,
|
|
141
|
+
ino: stats.ino,
|
|
142
|
+
mtimeMs: stats.mtimeMs,
|
|
143
|
+
size: stats.size,
|
|
144
|
+
}
|
|
145
|
+
previousStatsByFilePath.set(filePath, currentStats)
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
previousStats !== undefined &&
|
|
149
|
+
previousStats.birthtimeMs === currentStats.birthtimeMs &&
|
|
150
|
+
previousStats.ctimeMs === currentStats.ctimeMs &&
|
|
151
|
+
previousStats.ino === currentStats.ino &&
|
|
152
|
+
previousStats.mtimeMs === currentStats.mtimeMs &&
|
|
153
|
+
previousStats.size === currentStats.size
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const chokidarWatcherBySupervisor = new WeakMap<object, ReturnType<typeof watch>>()
|
|
159
|
+
|
|
160
|
+
export function getSupervisorChokidarWatcher(
|
|
161
|
+
supervisor: object,
|
|
162
|
+
): ReturnType<typeof watch> | undefined {
|
|
163
|
+
return chokidarWatcherBySupervisor.get(supervisor)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
interface BrowserHmrChannelOptions {
|
|
167
|
+
host?: string
|
|
168
|
+
port?: number
|
|
169
|
+
pathname?: string
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
interface NodeHmrWatchOptions {
|
|
173
|
+
ignore?: readonly string[]
|
|
174
|
+
poll?: boolean
|
|
175
|
+
pollInterval?: number
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
interface ResolvedChokidarWatchOptions {
|
|
179
|
+
awaitWriteFinish: {
|
|
180
|
+
pollInterval: number
|
|
181
|
+
stabilityThreshold: number
|
|
182
|
+
}
|
|
183
|
+
depth: number
|
|
184
|
+
ignorePermissionErrors: boolean
|
|
185
|
+
ignored: string[]
|
|
186
|
+
ignoreInitial: boolean
|
|
187
|
+
interval: number
|
|
188
|
+
usePolling: boolean
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function createHmrSupervisor(options: {
|
|
192
|
+
browserHmrChannel: BrowserHmrChannelOptions | null
|
|
193
|
+
cwd: string
|
|
194
|
+
entry: string
|
|
195
|
+
entryArgs: string[]
|
|
196
|
+
env: NodeJS.ProcessEnv
|
|
197
|
+
nodeArgs: string[]
|
|
198
|
+
registerPath: string
|
|
199
|
+
watch?: NodeHmrWatchOptions
|
|
200
|
+
}): {
|
|
201
|
+
readonly generation: number
|
|
202
|
+
ready(): Promise<void>
|
|
203
|
+
start(): Promise<void>
|
|
204
|
+
stop(signal?: NodeJS.Signals): Promise<void>
|
|
205
|
+
} {
|
|
206
|
+
let browserHmrEventChannel: BrowserHmrEventChannel | null = null
|
|
207
|
+
let browserHmrEventChannelPromise: Promise<BrowserHmrEventChannel | null> | undefined
|
|
208
|
+
let child: ChildProcess | undefined
|
|
209
|
+
let restartTimer: NodeJS.Timeout | undefined
|
|
210
|
+
let resolveRun: (() => void) | undefined
|
|
211
|
+
let moduleStore = createModuleStore()
|
|
212
|
+
let watchedFilePaths = new Set<string>()
|
|
213
|
+
let watchedDirectoryRefCounts = new Map<string, number>()
|
|
214
|
+
let browserWatchedFileRefCountsByRuntime = new Map<number, Map<string, number>>()
|
|
215
|
+
let browserWatchedFilePaths = new Set<string>()
|
|
216
|
+
let activeWatchedDirectories = new Set<string>()
|
|
217
|
+
let pendingChangedPaths = new Set<string>()
|
|
218
|
+
let pendingRestartPathEvents = new Map<string, 'add' | 'unlink'>()
|
|
219
|
+
let activeWatchEventFlushCount = 0
|
|
220
|
+
let pendingHotUpdateCount = 0
|
|
221
|
+
let acceptedHotUpdateCount = 0
|
|
222
|
+
let pendingBrowserHmrEvents: BrowserHmrEvent[] = []
|
|
223
|
+
let pendingBrowserHmrEventServerPaths = new Set<string>()
|
|
224
|
+
let browserHmrEventFlushTimer: NodeJS.Timeout | undefined
|
|
225
|
+
let restartSettleTimer: NodeJS.Timeout | undefined
|
|
226
|
+
let serverGeneration = 0
|
|
227
|
+
let restartGeneration = 0
|
|
228
|
+
let pendingRestartGeneration = 0
|
|
229
|
+
let readyGeneration = -1
|
|
230
|
+
let serverReadyCount = 0
|
|
231
|
+
let waitingForEntryServerReady = false
|
|
232
|
+
let readyWaiters: Array<() => void> = []
|
|
233
|
+
let browserHmrRequestId = 0
|
|
234
|
+
let pendingBrowserHmrRequests = new Map<
|
|
235
|
+
number,
|
|
236
|
+
{
|
|
237
|
+
resolve(events: BrowserHmrEvent[]): void
|
|
238
|
+
timer: NodeJS.Timeout
|
|
239
|
+
}
|
|
240
|
+
>()
|
|
241
|
+
let restarting = false
|
|
242
|
+
let stopping = false
|
|
243
|
+
let waitingForFileChangeAfterExit = false
|
|
244
|
+
let pendingServerUpdateEvent = false
|
|
245
|
+
let isDuplicateFileChangeEvent = createFileChangeEventDeduper()
|
|
246
|
+
|
|
247
|
+
function setReadyGeneration(generation: number): void {
|
|
248
|
+
readyGeneration = generation
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function isReady(): boolean {
|
|
252
|
+
return (
|
|
253
|
+
readyGeneration === pendingRestartGeneration &&
|
|
254
|
+
restartTimer === undefined &&
|
|
255
|
+
activeWatchEventFlushCount === 0 &&
|
|
256
|
+
!waitingForEntryServerReady &&
|
|
257
|
+
pendingHotUpdateCount === acceptedHotUpdateCount
|
|
258
|
+
)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function markRestartPending(): number {
|
|
262
|
+
if (pendingRestartGeneration === restartGeneration) {
|
|
263
|
+
pendingRestartGeneration += 1
|
|
264
|
+
serverGeneration += 1
|
|
265
|
+
}
|
|
266
|
+
waitingForEntryServerReady = false
|
|
267
|
+
setReadyGeneration(-1)
|
|
268
|
+
return pendingRestartGeneration
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function commitRestartGeneration(): number {
|
|
272
|
+
restartGeneration = pendingRestartGeneration
|
|
273
|
+
return restartGeneration
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function getEntryPath(): string {
|
|
277
|
+
return resolve(options.cwd, options.entry)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function start() {
|
|
281
|
+
setReadyGeneration(-1)
|
|
282
|
+
moduleStore.reset()
|
|
283
|
+
browserWatchedFileRefCountsByRuntime = new Map()
|
|
284
|
+
browserWatchedFilePaths = new Set()
|
|
285
|
+
unwatchKnownModuleFiles()
|
|
286
|
+
watchKnownModuleFile(getEntryPath())
|
|
287
|
+
waitingForFileChangeAfterExit = false
|
|
288
|
+
let childRestartGeneration = restartGeneration
|
|
289
|
+
|
|
290
|
+
let entry = getEntryPath()
|
|
291
|
+
let nextChild = spawn(
|
|
292
|
+
process.execPath,
|
|
293
|
+
buildChildProcessArgs({
|
|
294
|
+
entry,
|
|
295
|
+
browserEventUrl: browserHmrEventChannel?.url,
|
|
296
|
+
entryArgs: options.entryArgs,
|
|
297
|
+
nodeArgs: options.nodeArgs,
|
|
298
|
+
registerPath: options.registerPath,
|
|
299
|
+
rootPath: options.cwd,
|
|
300
|
+
}),
|
|
301
|
+
{
|
|
302
|
+
cwd: options.cwd,
|
|
303
|
+
env: buildChildProcessEnv(options.env),
|
|
304
|
+
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
|
|
305
|
+
},
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
child = nextChild
|
|
309
|
+
|
|
310
|
+
nextChild.on('message', (message: unknown) => {
|
|
311
|
+
if (child !== nextChild) return
|
|
312
|
+
handleChildMessage(message, childRestartGeneration)
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
nextChild.once('exit', (code, signal) => {
|
|
316
|
+
if (stopping || restarting || child !== nextChild) return
|
|
317
|
+
resolvePendingBrowserHmrRequests([])
|
|
318
|
+
waitingForFileChangeAfterExit = true
|
|
319
|
+
pendingServerUpdateEvent = true
|
|
320
|
+
child = undefined
|
|
321
|
+
console.log(`Failed running ${options.entry}. Waiting for file changes before restarting...`)
|
|
322
|
+
})
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async function restart() {
|
|
326
|
+
if (stopping) return
|
|
327
|
+
commitRestartGeneration()
|
|
328
|
+
restarting = true
|
|
329
|
+
await stopChild(child, { force: false, signal: 'SIGTERM' })
|
|
330
|
+
restarting = false
|
|
331
|
+
if (stopping) return
|
|
332
|
+
start()
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function scheduleRestart(): void {
|
|
336
|
+
markRestartPending()
|
|
337
|
+
|
|
338
|
+
if (restartSettleTimer !== undefined) {
|
|
339
|
+
clearTimeout(restartSettleTimer)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
restartSettleTimer = setTimeout(() => {
|
|
343
|
+
restartSettleTimer = undefined
|
|
344
|
+
restart().catch((error: unknown) => {
|
|
345
|
+
console.error(error)
|
|
346
|
+
})
|
|
347
|
+
}, restartSettleDelayMs)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function handleChildMessage(message: unknown, childRestartGeneration: number) {
|
|
351
|
+
if (!isChildMessage(message)) return
|
|
352
|
+
|
|
353
|
+
if (message.type === 'node-hmr:child:browser-event-emitted') {
|
|
354
|
+
browserHmrEventChannel?.send(message.payload)
|
|
355
|
+
return
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (message.type === 'node-hmr:child:browser-hmr-channel-requested') {
|
|
359
|
+
respondToBrowserHmrChannelRequest(message.requestId).catch((error: unknown) => {
|
|
360
|
+
console.warn(`Failed to create browser HMR channel: ${formatUnknownError(error)}`)
|
|
361
|
+
sendBrowserHmrChannelResponse(message.requestId, undefined)
|
|
362
|
+
})
|
|
363
|
+
return
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (message.type === 'node-hmr:child:browser-hmr-watch-files-changed') {
|
|
367
|
+
updateBrowserWatchedFiles(message.id, message.delta)
|
|
368
|
+
return
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (message.type === 'node-hmr:child:browser-hmr-file-events-handled') {
|
|
372
|
+
let request = pendingBrowserHmrRequests.get(message.requestId)
|
|
373
|
+
if (request !== undefined) {
|
|
374
|
+
clearTimeout(request.timer)
|
|
375
|
+
pendingBrowserHmrRequests.delete(message.requestId)
|
|
376
|
+
request.resolve(message.events)
|
|
377
|
+
}
|
|
378
|
+
if (message.error !== undefined) {
|
|
379
|
+
console.warn(`Browser HMR runtime failed: ${message.error}`)
|
|
380
|
+
}
|
|
381
|
+
return
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (message.type === 'node-hmr:child:hot-module-updated') {
|
|
385
|
+
acceptedHotUpdateCount += 1
|
|
386
|
+
serverGeneration += 1
|
|
387
|
+
if (!waitingForEntryServerReady) {
|
|
388
|
+
flushAcceptedHotUpdateBrowserEvent()
|
|
389
|
+
}
|
|
390
|
+
resolveReadyWaiters()
|
|
391
|
+
return
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (message.type === 'node-hmr:child:hot-module-invalidated') {
|
|
395
|
+
propagateInvalidatedHotUpdate(message.url, message.timestamp, message.message).catch(
|
|
396
|
+
(error: unknown) => {
|
|
397
|
+
console.error(error)
|
|
398
|
+
},
|
|
399
|
+
)
|
|
400
|
+
return
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (message.type === 'node-hmr:child:server-ready') {
|
|
404
|
+
if (childRestartGeneration !== pendingRestartGeneration) return
|
|
405
|
+
serverReadyCount += 1
|
|
406
|
+
waitingForEntryServerReady = false
|
|
407
|
+
setReadyGeneration(childRestartGeneration)
|
|
408
|
+
resolveReadyWaiters()
|
|
409
|
+
flushPendingServerUpdateEvent()
|
|
410
|
+
flushAcceptedHotUpdateBrowserEvent()
|
|
411
|
+
return
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (message.type === 'node-hmr:child:restart-requested') {
|
|
415
|
+
if (message.message !== undefined) {
|
|
416
|
+
console.warn(message.message)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
pendingServerUpdateEvent = true
|
|
420
|
+
clearPendingHotUpdates()
|
|
421
|
+
logRestart(message.message ?? 'import.meta.hot.invalidate()')
|
|
422
|
+
scheduleRestart()
|
|
423
|
+
return
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (message.type === 'node-hmr:child:module-imported') {
|
|
427
|
+
moduleStore.addDependency(message.importerUrl, message.depUrl)
|
|
428
|
+
return
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (message.type === 'node-hmr:child:accepted-deps-resolved') {
|
|
432
|
+
moduleStore.setAcceptedDependencies(message.url, message.acceptedDeps)
|
|
433
|
+
return
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
moduleStore.setModule({
|
|
437
|
+
filePath: message.filePath,
|
|
438
|
+
hmr: message.hmr,
|
|
439
|
+
url: message.url,
|
|
440
|
+
})
|
|
441
|
+
syncWatchedModuleFiles()
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function watchKnownModuleFile(filePath: string): void {
|
|
445
|
+
syncWatchedModuleFiles(new Set([resolve(filePath)]))
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function syncWatchedModuleFiles(requiredFilePaths: Set<string> = new Set()): void {
|
|
449
|
+
let nextWatchedFilePaths = new Set(requiredFilePaths)
|
|
450
|
+
|
|
451
|
+
let entryUrl = pathToFileURL(getEntryPath()).href
|
|
452
|
+
for (let filePath of moduleStore.getReachableFilePaths(entryUrl)) {
|
|
453
|
+
nextWatchedFilePaths.add(filePath)
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
let nextWatchedDirectoryRefCounts = new Map<string, number>()
|
|
457
|
+
for (let filePath of nextWatchedFilePaths) {
|
|
458
|
+
let directory = dirname(filePath)
|
|
459
|
+
nextWatchedDirectoryRefCounts.set(
|
|
460
|
+
directory,
|
|
461
|
+
(nextWatchedDirectoryRefCounts.get(directory) ?? 0) + 1,
|
|
462
|
+
)
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
watchedFilePaths = nextWatchedFilePaths
|
|
466
|
+
watchedDirectoryRefCounts = nextWatchedDirectoryRefCounts
|
|
467
|
+
syncWatchedDirectories()
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function unwatchKnownModuleFiles(): void {
|
|
471
|
+
watchedFilePaths = new Set()
|
|
472
|
+
watchedDirectoryRefCounts = new Map()
|
|
473
|
+
syncWatchedDirectories()
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function updateBrowserWatchedFiles(
|
|
477
|
+
runtimeId: number,
|
|
478
|
+
delta: { add: readonly string[]; remove: readonly string[] },
|
|
479
|
+
): void {
|
|
480
|
+
let refCounts = browserWatchedFileRefCountsByRuntime.get(runtimeId)
|
|
481
|
+
if (refCounts === undefined) {
|
|
482
|
+
refCounts = new Map()
|
|
483
|
+
browserWatchedFileRefCountsByRuntime.set(runtimeId, refCounts)
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
for (let file of delta.add) {
|
|
487
|
+
let filePath = normalizeBrowserHmrFilePath(resolve(options.cwd, file))
|
|
488
|
+
refCounts.set(filePath, (refCounts.get(filePath) ?? 0) + 1)
|
|
489
|
+
}
|
|
490
|
+
for (let file of delta.remove) {
|
|
491
|
+
let filePath = normalizeBrowserHmrFilePath(resolve(options.cwd, file))
|
|
492
|
+
let count = refCounts.get(filePath)
|
|
493
|
+
if (count === undefined) continue
|
|
494
|
+
if (count <= 1) {
|
|
495
|
+
refCounts.delete(filePath)
|
|
496
|
+
} else {
|
|
497
|
+
refCounts.set(filePath, count - 1)
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (refCounts.size === 0) {
|
|
502
|
+
browserWatchedFileRefCountsByRuntime.delete(runtimeId)
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
syncWatchedDirectories()
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function syncWatchedDirectories(): void {
|
|
509
|
+
let nextBrowserWatchedFilePaths = new Set<string>()
|
|
510
|
+
for (let refCounts of browserWatchedFileRefCountsByRuntime.values()) {
|
|
511
|
+
for (let file of refCounts.keys()) {
|
|
512
|
+
nextBrowserWatchedFilePaths.add(file)
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
let nextWatchedDirectories = new Set([
|
|
516
|
+
...watchedDirectoryRefCounts.keys(),
|
|
517
|
+
...getWatchedDirectoriesForFiles(nextBrowserWatchedFilePaths),
|
|
518
|
+
])
|
|
519
|
+
|
|
520
|
+
let directoriesToAdd = [...nextWatchedDirectories].filter(
|
|
521
|
+
(directory) => !activeWatchedDirectories.has(directory),
|
|
522
|
+
)
|
|
523
|
+
let directoriesToRemove = [...activeWatchedDirectories].filter(
|
|
524
|
+
(directory) => !nextWatchedDirectories.has(directory),
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
if (directoriesToRemove.length > 0) {
|
|
528
|
+
watcher.unwatch(directoriesToRemove)
|
|
529
|
+
}
|
|
530
|
+
if (directoriesToAdd.length > 0) {
|
|
531
|
+
watcher.add(directoriesToAdd)
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
activeWatchedDirectories = nextWatchedDirectories
|
|
535
|
+
browserWatchedFilePaths = nextBrowserWatchedFilePaths
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function handleWatchEvent(event: string, changedPath: string, stats?: Stats) {
|
|
539
|
+
let filePath = resolve(options.cwd, changedPath)
|
|
540
|
+
|
|
541
|
+
if (event === 'change') {
|
|
542
|
+
let normalizedFilePath = normalizeBrowserHmrFilePath(filePath)
|
|
543
|
+
let isRelevantFile =
|
|
544
|
+
waitingForFileChangeAfterExit ||
|
|
545
|
+
watchedFilePaths.has(filePath) ||
|
|
546
|
+
browserWatchedFilePaths.has(normalizedFilePath)
|
|
547
|
+
if (!isRelevantFile || isDuplicateFileChangeEvent(normalizedFilePath, stats)) return
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (restartTimer !== undefined) {
|
|
551
|
+
clearTimeout(restartTimer)
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (event === 'change') {
|
|
555
|
+
pendingChangedPaths.add(filePath)
|
|
556
|
+
} else {
|
|
557
|
+
pendingRestartPathEvents.set(filePath, event === 'add' ? 'add' : 'unlink')
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
restartTimer = setTimeout(() => {
|
|
561
|
+
flushPendingWatchEvents().catch((error: unknown) => {
|
|
562
|
+
console.error(error)
|
|
563
|
+
})
|
|
564
|
+
}, restartDelayMs)
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
async function flushPendingWatchEvents() {
|
|
568
|
+
activeWatchEventFlushCount += 1
|
|
569
|
+
try {
|
|
570
|
+
let changedPaths = [...pendingChangedPaths]
|
|
571
|
+
pendingChangedPaths = new Set()
|
|
572
|
+
|
|
573
|
+
let restartPathEvents = new Map(pendingRestartPathEvents)
|
|
574
|
+
let restartPaths = [...restartPathEvents.keys()]
|
|
575
|
+
pendingRestartPathEvents = new Map()
|
|
576
|
+
restartTimer = undefined
|
|
577
|
+
|
|
578
|
+
let browserFileEvents = getBrowserHmrFileEvents(changedPaths, restartPathEvents)
|
|
579
|
+
let browserHmrEvents = await requestBrowserHmrEvents(browserFileEvents)
|
|
580
|
+
for (let event of browserHmrEvents) {
|
|
581
|
+
queueBrowserHmrEvent(event, { schedule: false })
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (waitingForFileChangeAfterExit) {
|
|
585
|
+
logRestart(formatChangedPaths([...restartPaths, ...changedPaths], options.cwd))
|
|
586
|
+
pendingServerUpdateEvent = true
|
|
587
|
+
start()
|
|
588
|
+
return
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
restartPaths = restartPaths.filter((changedPath) => watchedFilePaths.has(changedPath))
|
|
592
|
+
|
|
593
|
+
if (restartPaths.length > 0) {
|
|
594
|
+
markBrowserHmrEventServerPathsChecked(restartPaths)
|
|
595
|
+
forceBrowserFullReloadIfBrowserWorkPending()
|
|
596
|
+
logRestart(formatChangedPaths(restartPaths, options.cwd))
|
|
597
|
+
pendingServerUpdateEvent = true
|
|
598
|
+
scheduleRestart()
|
|
599
|
+
return
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
let hotUpdates: NodeHmrUpdate[] = []
|
|
603
|
+
let checkedChangedPaths: string[] = []
|
|
604
|
+
for (let changedPath of changedPaths) {
|
|
605
|
+
if (!watchedFilePaths.has(changedPath)) continue
|
|
606
|
+
checkedChangedPaths.push(changedPath)
|
|
607
|
+
|
|
608
|
+
let modules = moduleStore.getModulesForFile(changedPath)
|
|
609
|
+
if (modules.length === 0) {
|
|
610
|
+
markBrowserHmrEventServerPathsChecked(checkedChangedPaths)
|
|
611
|
+
forceBrowserFullReloadIfBrowserWorkPending()
|
|
612
|
+
logRestart(formatChangedPath(changedPath, options.cwd))
|
|
613
|
+
pendingServerUpdateEvent = true
|
|
614
|
+
scheduleRestart()
|
|
615
|
+
return
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
for (let moduleInfo of modules) {
|
|
619
|
+
let hotUpdateBoundaries = moduleStore.findHotUpdateBoundaries(moduleInfo.url, Date.now())
|
|
620
|
+
if (!hotUpdateBoundaries) {
|
|
621
|
+
markBrowserHmrEventServerPathsChecked(checkedChangedPaths)
|
|
622
|
+
forceBrowserFullReloadIfBrowserWorkPending()
|
|
623
|
+
logRestart(formatChangedPath(changedPath, options.cwd))
|
|
624
|
+
pendingServerUpdateEvent = true
|
|
625
|
+
scheduleRestart()
|
|
626
|
+
return
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
hotUpdates.push(
|
|
630
|
+
...hotUpdateBoundaries.map((boundary) => ({
|
|
631
|
+
acceptedUrl: boundary.acceptedDependencyUrl,
|
|
632
|
+
filePath: moduleInfo.filePath,
|
|
633
|
+
invalidatedUrls: boundary.invalidatedUrls,
|
|
634
|
+
url: boundary.updateHandlerUrl,
|
|
635
|
+
})),
|
|
636
|
+
)
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
markBrowserHmrEventServerPathsChecked(checkedChangedPaths)
|
|
641
|
+
|
|
642
|
+
pendingHotUpdateCount += hotUpdates.length
|
|
643
|
+
if (hotUpdates.length === 0) {
|
|
644
|
+
flushBrowserHmrEvents({ serverReady: !pendingServerUpdateEvent })
|
|
645
|
+
return
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
for (let moduleInfo of hotUpdates) {
|
|
649
|
+
if (!sendHotUpdate(moduleInfo)) {
|
|
650
|
+
clearPendingHotUpdates()
|
|
651
|
+
forceBrowserFullReloadIfBrowserWorkPending()
|
|
652
|
+
logRestart(formatChangedPath(moduleInfo.filePath, options.cwd))
|
|
653
|
+
pendingServerUpdateEvent = true
|
|
654
|
+
scheduleRestart()
|
|
655
|
+
return
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (hotUpdates.length > 0) {
|
|
660
|
+
logHotUpdate(
|
|
661
|
+
formatChangedPaths(
|
|
662
|
+
hotUpdates.map((moduleInfo) => moduleInfo.filePath),
|
|
663
|
+
options.cwd,
|
|
664
|
+
),
|
|
665
|
+
)
|
|
666
|
+
}
|
|
667
|
+
} finally {
|
|
668
|
+
activeWatchEventFlushCount -= 1
|
|
669
|
+
resolveReadyWaiters()
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function flushPendingServerUpdateEvent(): void {
|
|
674
|
+
if (!pendingServerUpdateEvent) return
|
|
675
|
+
|
|
676
|
+
clearPendingHotUpdates()
|
|
677
|
+
forceBrowserFullReloadIfBrowserWorkPending()
|
|
678
|
+
if (flushBrowserHmrEvents({ serverReady: true }) === 'reload') {
|
|
679
|
+
pendingServerUpdateEvent = false
|
|
680
|
+
return
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
browserHmrEventChannel?.send({
|
|
684
|
+
type: 'server:update',
|
|
685
|
+
})
|
|
686
|
+
pendingServerUpdateEvent = false
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function flushAcceptedHotUpdateBrowserEvent(): void {
|
|
690
|
+
if (pendingServerUpdateEvent) return
|
|
691
|
+
if (pendingHotUpdateCount === 0 || acceptedHotUpdateCount < pendingHotUpdateCount) return
|
|
692
|
+
|
|
693
|
+
clearPendingHotUpdates()
|
|
694
|
+
if (flushBrowserHmrEvents({ serverReady: true }) === 'reload') {
|
|
695
|
+
return
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
browserHmrEventChannel?.send({
|
|
699
|
+
type: 'server:update',
|
|
700
|
+
})
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
async function propagateInvalidatedHotUpdate(
|
|
704
|
+
url: string,
|
|
705
|
+
timestamp: number,
|
|
706
|
+
message: string | undefined,
|
|
707
|
+
): Promise<void> {
|
|
708
|
+
let moduleInfo = moduleStore.getModule(url)
|
|
709
|
+
let hotUpdateBoundaries = moduleStore.findHotUpdateBoundariesFromImporters(url, timestamp)
|
|
710
|
+
if (!hotUpdateBoundaries || hotUpdateBoundaries.length === 0) {
|
|
711
|
+
clearPendingHotUpdates()
|
|
712
|
+
forceBrowserFullReloadIfBrowserWorkPending()
|
|
713
|
+
logRestart(
|
|
714
|
+
message ?? (moduleInfo ? formatChangedPath(moduleInfo.filePath, options.cwd) : url),
|
|
715
|
+
)
|
|
716
|
+
pendingServerUpdateEvent = true
|
|
717
|
+
scheduleRestart()
|
|
718
|
+
return
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
pendingHotUpdateCount = Math.max(0, pendingHotUpdateCount - 1) + hotUpdateBoundaries.length
|
|
722
|
+
|
|
723
|
+
for (let boundary of hotUpdateBoundaries) {
|
|
724
|
+
let filePath = moduleInfo?.filePath ?? boundary.acceptedDependencyUrl
|
|
725
|
+
if (
|
|
726
|
+
!sendHotUpdate({
|
|
727
|
+
acceptedUrl: boundary.acceptedDependencyUrl,
|
|
728
|
+
filePath,
|
|
729
|
+
invalidatedUrls: boundary.invalidatedUrls,
|
|
730
|
+
url: boundary.updateHandlerUrl,
|
|
731
|
+
})
|
|
732
|
+
) {
|
|
733
|
+
clearPendingHotUpdates()
|
|
734
|
+
forceBrowserFullReloadIfBrowserWorkPending()
|
|
735
|
+
logRestart(moduleInfo ? formatChangedPath(moduleInfo.filePath, options.cwd) : url)
|
|
736
|
+
pendingServerUpdateEvent = true
|
|
737
|
+
scheduleRestart()
|
|
738
|
+
return
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
if (moduleInfo) {
|
|
743
|
+
logHotUpdate(formatChangedPath(moduleInfo.filePath, options.cwd))
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function clearPendingHotUpdates(): void {
|
|
748
|
+
pendingHotUpdateCount = 0
|
|
749
|
+
acceptedHotUpdateCount = 0
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function resolvePendingBrowserHmrRequests(events: BrowserHmrEvent[]): void {
|
|
753
|
+
for (let [requestId, request] of pendingBrowserHmrRequests) {
|
|
754
|
+
clearTimeout(request.timer)
|
|
755
|
+
request.resolve(events)
|
|
756
|
+
pendingBrowserHmrRequests.delete(requestId)
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function queueBrowserHmrEvent(
|
|
761
|
+
event: BrowserHmrEvent,
|
|
762
|
+
queueOptions: { schedule: boolean } = { schedule: true },
|
|
763
|
+
): void {
|
|
764
|
+
pendingBrowserHmrEvents.push(event)
|
|
765
|
+
for (let file of event.files ?? []) {
|
|
766
|
+
let filePath = resolve(options.cwd, file)
|
|
767
|
+
if (watchedFilePaths.has(filePath)) {
|
|
768
|
+
pendingBrowserHmrEventServerPaths.add(filePath)
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
if (queueOptions.schedule) scheduleBrowserHmrEventFlush()
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function scheduleBrowserHmrEventFlush(): void {
|
|
775
|
+
if (browserHmrEventFlushTimer !== undefined) return
|
|
776
|
+
|
|
777
|
+
browserHmrEventFlushTimer = setTimeout(() => {
|
|
778
|
+
browserHmrEventFlushTimer = undefined
|
|
779
|
+
flushBrowserHmrEvents({
|
|
780
|
+
serverReady:
|
|
781
|
+
!pendingServerUpdateEvent &&
|
|
782
|
+
pendingBrowserHmrEventServerPaths.size === 0 &&
|
|
783
|
+
(pendingHotUpdateCount === 0 || acceptedHotUpdateCount >= pendingHotUpdateCount),
|
|
784
|
+
})
|
|
785
|
+
}, browserHmrEventFlushDelayMs)
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function forceBrowserFullReloadIfBrowserWorkPending(): void {
|
|
789
|
+
if (pendingBrowserHmrEvents.length === 0) return
|
|
790
|
+
|
|
791
|
+
let reloadEvent = pendingBrowserHmrEvents.find((event) => event.type === 'reload')
|
|
792
|
+
pendingBrowserHmrEvents = [
|
|
793
|
+
reloadEvent ?? {
|
|
794
|
+
type: 'reload',
|
|
795
|
+
},
|
|
796
|
+
]
|
|
797
|
+
pendingBrowserHmrEventServerPaths.clear()
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function getBrowserHmrFileEvents(
|
|
801
|
+
changedPaths: readonly string[],
|
|
802
|
+
restartPathEvents: ReadonlyMap<string, 'add' | 'unlink'>,
|
|
803
|
+
): BrowserHmrFileEvent[] {
|
|
804
|
+
return getBrowserHmrFileEventsForWatchedFiles({
|
|
805
|
+
changedPaths,
|
|
806
|
+
restartPathEvents,
|
|
807
|
+
watchedFiles: browserWatchedFilePaths,
|
|
808
|
+
})
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
async function requestBrowserHmrEvents(
|
|
812
|
+
events: readonly BrowserHmrFileEvent[],
|
|
813
|
+
): Promise<BrowserHmrEvent[]> {
|
|
814
|
+
if (events.length === 0) return []
|
|
815
|
+
if (child === undefined || child.send === undefined || !child.connected) return []
|
|
816
|
+
|
|
817
|
+
let requestId = browserHmrRequestId++
|
|
818
|
+
let browserHmrEvents = await new Promise<BrowserHmrEvent[]>((resolvePromise) => {
|
|
819
|
+
let timer = setTimeout(() => {
|
|
820
|
+
pendingBrowserHmrRequests.delete(requestId)
|
|
821
|
+
resolvePromise([])
|
|
822
|
+
}, browserHmrRequestTimeoutMs)
|
|
823
|
+
|
|
824
|
+
pendingBrowserHmrRequests.set(requestId, {
|
|
825
|
+
resolve: resolvePromise,
|
|
826
|
+
timer,
|
|
827
|
+
})
|
|
828
|
+
|
|
829
|
+
if (
|
|
830
|
+
!child?.send?.({
|
|
831
|
+
events,
|
|
832
|
+
requestId,
|
|
833
|
+
type: 'node-hmr:parent:browser-hmr-file-events',
|
|
834
|
+
})
|
|
835
|
+
) {
|
|
836
|
+
clearTimeout(timer)
|
|
837
|
+
pendingBrowserHmrRequests.delete(requestId)
|
|
838
|
+
resolvePromise([])
|
|
839
|
+
}
|
|
840
|
+
})
|
|
841
|
+
|
|
842
|
+
return browserHmrEvents
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
async function respondToBrowserHmrChannelRequest(requestId: number): Promise<void> {
|
|
846
|
+
let channel = await getBrowserHmrEventChannel()
|
|
847
|
+
sendBrowserHmrChannelResponse(requestId, channel?.url)
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function sendBrowserHmrChannelResponse(requestId: number, url: string | undefined): void {
|
|
851
|
+
if (child === undefined || child.send === undefined || !child.connected) return
|
|
852
|
+
|
|
853
|
+
child.send({
|
|
854
|
+
requestId,
|
|
855
|
+
type: 'node-hmr:parent:browser-hmr-channel',
|
|
856
|
+
url,
|
|
857
|
+
})
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
async function getBrowserHmrEventChannel(): Promise<BrowserHmrEventChannel | null> {
|
|
861
|
+
if (options.browserHmrChannel === null) return null
|
|
862
|
+
if (browserHmrEventChannel) return browserHmrEventChannel
|
|
863
|
+
|
|
864
|
+
browserHmrEventChannelPromise ??= createBrowserHmrEventChannel(options.browserHmrChannel).then(
|
|
865
|
+
(channel) => {
|
|
866
|
+
browserHmrEventChannel = channel
|
|
867
|
+
return channel
|
|
868
|
+
},
|
|
869
|
+
)
|
|
870
|
+
|
|
871
|
+
return browserHmrEventChannelPromise
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function flushBrowserHmrEvents(options: { serverReady: boolean }): 'events' | 'reload' | 'none' {
|
|
875
|
+
if (pendingBrowserHmrEvents.length === 0) return 'none'
|
|
876
|
+
if (!options.serverReady) return 'none'
|
|
877
|
+
if (pendingBrowserHmrEventServerPaths.size > 0) return 'none'
|
|
878
|
+
|
|
879
|
+
if (browserHmrEventFlushTimer !== undefined) {
|
|
880
|
+
clearTimeout(browserHmrEventFlushTimer)
|
|
881
|
+
browserHmrEventFlushTimer = undefined
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
let reloadEvent = pendingBrowserHmrEvents.find((event) => event.type === 'reload')
|
|
885
|
+
if (reloadEvent) {
|
|
886
|
+
pendingBrowserHmrEvents = []
|
|
887
|
+
browserHmrEventChannel?.send({
|
|
888
|
+
type: 'browser:reload',
|
|
889
|
+
})
|
|
890
|
+
return 'reload'
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
let events = pendingBrowserHmrEvents
|
|
894
|
+
pendingBrowserHmrEvents = []
|
|
895
|
+
for (let event of events) {
|
|
896
|
+
if (event.type === 'update') {
|
|
897
|
+
browserHmrEventChannel?.send({
|
|
898
|
+
data: event.data,
|
|
899
|
+
type: 'browser:update',
|
|
900
|
+
})
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
return 'events'
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
function markBrowserHmrEventServerPathsChecked(filePaths: readonly string[]): void {
|
|
907
|
+
for (let filePath of filePaths) {
|
|
908
|
+
pendingBrowserHmrEventServerPaths.delete(filePath)
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
function sendHotUpdate(moduleInfo: NodeHmrUpdate): boolean {
|
|
913
|
+
if (child === undefined || child.send === undefined || !child.connected) return false
|
|
914
|
+
|
|
915
|
+
let shouldWaitForEntryServerReady =
|
|
916
|
+
serverReadyCount > 0 &&
|
|
917
|
+
(moduleInfo.acceptedUrl ?? moduleInfo.url) === pathToFileURL(getEntryPath()).href
|
|
918
|
+
if (shouldWaitForEntryServerReady) {
|
|
919
|
+
waitingForEntryServerReady = true
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
return child.send({
|
|
923
|
+
acceptedUrl: moduleInfo.acceptedUrl,
|
|
924
|
+
invalidatedUrls: moduleInfo.invalidatedUrls,
|
|
925
|
+
type: 'node-hmr:parent:hot-module-changed',
|
|
926
|
+
url: moduleInfo.url,
|
|
927
|
+
timestamp: Date.now(),
|
|
928
|
+
})
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
function ready(): Promise<void> {
|
|
932
|
+
if (isReady()) return Promise.resolve()
|
|
933
|
+
|
|
934
|
+
return new Promise((resolvePromise) => {
|
|
935
|
+
readyWaiters.push(resolvePromise)
|
|
936
|
+
})
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
function resolveReadyWaiters(options: { force?: boolean } = {}): void {
|
|
940
|
+
if (!options.force && !isReady()) return
|
|
941
|
+
|
|
942
|
+
let waiters = readyWaiters
|
|
943
|
+
readyWaiters = []
|
|
944
|
+
for (let waiter of waiters) {
|
|
945
|
+
waiter()
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
let watcher = watch([], {
|
|
950
|
+
cwd: options.cwd,
|
|
951
|
+
...resolveChokidarWatchOptions(options.watch),
|
|
952
|
+
})
|
|
953
|
+
|
|
954
|
+
watcher.on('all', handleWatchEvent)
|
|
955
|
+
|
|
956
|
+
let stopPromise: Promise<void> | undefined
|
|
957
|
+
|
|
958
|
+
async function stop(signal: NodeJS.Signals = 'SIGTERM'): Promise<void> {
|
|
959
|
+
stopping = true
|
|
960
|
+
if (restartTimer !== undefined) {
|
|
961
|
+
clearTimeout(restartTimer)
|
|
962
|
+
}
|
|
963
|
+
if (restartSettleTimer !== undefined) {
|
|
964
|
+
clearTimeout(restartSettleTimer)
|
|
965
|
+
}
|
|
966
|
+
if (browserHmrEventFlushTimer !== undefined) {
|
|
967
|
+
clearTimeout(browserHmrEventFlushTimer)
|
|
968
|
+
}
|
|
969
|
+
clearPendingHotUpdates()
|
|
970
|
+
resolveReadyWaiters({ force: true })
|
|
971
|
+
resolvePendingBrowserHmrRequests([])
|
|
972
|
+
|
|
973
|
+
stopPromise ??= Promise.resolve()
|
|
974
|
+
.then(() => watcher.close())
|
|
975
|
+
.then(() => stopChild(child, { force: true, signal }))
|
|
976
|
+
.then(async () => {
|
|
977
|
+
let channel = await browserHmrEventChannelPromise
|
|
978
|
+
await channel?.close()
|
|
979
|
+
})
|
|
980
|
+
.then(() => {
|
|
981
|
+
resolveRun?.()
|
|
982
|
+
})
|
|
983
|
+
|
|
984
|
+
await stopPromise
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
let supervisor = {
|
|
988
|
+
get generation() {
|
|
989
|
+
return serverGeneration
|
|
990
|
+
},
|
|
991
|
+
|
|
992
|
+
ready,
|
|
993
|
+
|
|
994
|
+
async start() {
|
|
995
|
+
return await new Promise<void>((resolvePromise) => {
|
|
996
|
+
resolveRun = resolvePromise
|
|
997
|
+
start()
|
|
998
|
+
|
|
999
|
+
process.once('SIGINT', () => {
|
|
1000
|
+
stop('SIGINT').catch((error: unknown) => {
|
|
1001
|
+
console.error(error)
|
|
1002
|
+
})
|
|
1003
|
+
})
|
|
1004
|
+
process.once('SIGTERM', () => {
|
|
1005
|
+
stop('SIGTERM').catch((error: unknown) => {
|
|
1006
|
+
console.error(error)
|
|
1007
|
+
})
|
|
1008
|
+
})
|
|
1009
|
+
})
|
|
1010
|
+
},
|
|
1011
|
+
|
|
1012
|
+
stop,
|
|
1013
|
+
}
|
|
1014
|
+
chokidarWatcherBySupervisor.set(supervisor, watcher)
|
|
1015
|
+
return supervisor
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
interface BrowserHmrEventChannel {
|
|
1019
|
+
close(): Promise<void>
|
|
1020
|
+
send(payload: HmrEventPayload): void
|
|
1021
|
+
url: string
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
interface HmrEventClient {
|
|
1025
|
+
close(): void
|
|
1026
|
+
send(payload: HmrEventPayload): void
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
async function createBrowserHmrEventChannel(
|
|
1030
|
+
options: BrowserHmrChannelOptions,
|
|
1031
|
+
): Promise<BrowserHmrEventChannel> {
|
|
1032
|
+
let host = options.host ?? '127.0.0.1'
|
|
1033
|
+
let pathname = options.pathname ?? defaultBrowserHmrPathname
|
|
1034
|
+
let port = options.port ?? 0
|
|
1035
|
+
let clients = new Set<HmrEventClient>()
|
|
1036
|
+
let server: Server
|
|
1037
|
+
|
|
1038
|
+
server = createServer((request, response) => {
|
|
1039
|
+
if (request.url === undefined) {
|
|
1040
|
+
response.writeHead(404).end()
|
|
1041
|
+
return
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
let requestUrl = new URL(request.url, `http://${host}`)
|
|
1045
|
+
|
|
1046
|
+
if (request.method === 'OPTIONS' && requestUrl.pathname === pathname) {
|
|
1047
|
+
writeCorsHeaders(response, 204)
|
|
1048
|
+
response.end()
|
|
1049
|
+
return
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
if (request.method !== 'GET' || requestUrl.pathname !== pathname) {
|
|
1053
|
+
response.writeHead(404).end()
|
|
1054
|
+
return
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
response.writeHead(200, {
|
|
1058
|
+
'Access-Control-Allow-Origin': '*',
|
|
1059
|
+
'Cache-Control': 'no-cache',
|
|
1060
|
+
Connection: 'keep-alive',
|
|
1061
|
+
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
1062
|
+
'X-Accel-Buffering': 'no',
|
|
1063
|
+
})
|
|
1064
|
+
response.flushHeaders()
|
|
1065
|
+
|
|
1066
|
+
let client: HmrEventClient = {
|
|
1067
|
+
close() {
|
|
1068
|
+
response.end()
|
|
1069
|
+
clients.delete(client)
|
|
1070
|
+
},
|
|
1071
|
+
send(payload) {
|
|
1072
|
+
response.write(formatServerSentEvent(payload))
|
|
1073
|
+
},
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
clients.add(client)
|
|
1077
|
+
response.once('close', () => {
|
|
1078
|
+
clients.delete(client)
|
|
1079
|
+
})
|
|
1080
|
+
response.write(': connected\n\n')
|
|
1081
|
+
})
|
|
1082
|
+
|
|
1083
|
+
let url = await listen(server, {
|
|
1084
|
+
host,
|
|
1085
|
+
pathname,
|
|
1086
|
+
port,
|
|
1087
|
+
serverName: 'node HMR browser channel',
|
|
1088
|
+
})
|
|
1089
|
+
|
|
1090
|
+
return {
|
|
1091
|
+
async close() {
|
|
1092
|
+
for (let client of clients) {
|
|
1093
|
+
client.close()
|
|
1094
|
+
}
|
|
1095
|
+
clients.clear()
|
|
1096
|
+
|
|
1097
|
+
await closeServer(server)
|
|
1098
|
+
},
|
|
1099
|
+
|
|
1100
|
+
send(payload) {
|
|
1101
|
+
for (let client of clients) {
|
|
1102
|
+
client.send(payload)
|
|
1103
|
+
}
|
|
1104
|
+
},
|
|
1105
|
+
|
|
1106
|
+
url,
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
async function listen(
|
|
1111
|
+
server: Server,
|
|
1112
|
+
options: {
|
|
1113
|
+
host: string
|
|
1114
|
+
pathname?: string
|
|
1115
|
+
port: number
|
|
1116
|
+
serverName: string
|
|
1117
|
+
},
|
|
1118
|
+
): Promise<string> {
|
|
1119
|
+
return await new Promise<string>((resolvePromise, reject) => {
|
|
1120
|
+
server.once('error', reject)
|
|
1121
|
+
server.listen(options.port, options.host, () => {
|
|
1122
|
+
server.off('error', reject)
|
|
1123
|
+
let address = server.address()
|
|
1124
|
+
if (!isAddressInfo(address)) {
|
|
1125
|
+
reject(new Error(`Failed to start ${options.serverName}.`))
|
|
1126
|
+
return
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
resolvePromise(`http://${options.host}:${address.port}${options.pathname ?? ''}`)
|
|
1130
|
+
})
|
|
1131
|
+
})
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
async function closeServer(server: Server): Promise<void> {
|
|
1135
|
+
await new Promise<void>((resolvePromise, reject) => {
|
|
1136
|
+
server.close((error) => {
|
|
1137
|
+
if (error) {
|
|
1138
|
+
reject(error)
|
|
1139
|
+
return
|
|
1140
|
+
}
|
|
1141
|
+
resolvePromise()
|
|
1142
|
+
})
|
|
1143
|
+
})
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
function writeCorsHeaders(response: ServerResponse, status: number): void {
|
|
1147
|
+
response.writeHead(status, {
|
|
1148
|
+
'Access-Control-Allow-Headers': 'Cache-Control',
|
|
1149
|
+
'Access-Control-Allow-Origin': '*',
|
|
1150
|
+
'Access-Control-Allow-Methods': 'GET, OPTIONS',
|
|
1151
|
+
})
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
function formatServerSentEvent(payload: HmrEventPayload): string {
|
|
1155
|
+
return `data: ${JSON.stringify(payload)}\n\n`
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
export function resolveChokidarWatchOptions(
|
|
1159
|
+
options: NodeHmrWatchOptions = {},
|
|
1160
|
+
): ResolvedChokidarWatchOptions {
|
|
1161
|
+
return {
|
|
1162
|
+
awaitWriteFinish: {
|
|
1163
|
+
pollInterval: 10,
|
|
1164
|
+
stabilityThreshold: 10,
|
|
1165
|
+
},
|
|
1166
|
+
depth: 0,
|
|
1167
|
+
ignorePermissionErrors: true,
|
|
1168
|
+
ignored: ['**/.git/**', ...(options.ignore ?? [])],
|
|
1169
|
+
ignoreInitial: true,
|
|
1170
|
+
interval: options.pollInterval ?? 100,
|
|
1171
|
+
usePolling: options.poll ?? process.platform === 'win32',
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
export function buildChildProcessArgs(options: {
|
|
1176
|
+
browserEventUrl?: string
|
|
1177
|
+
entry: string
|
|
1178
|
+
entryArgs: Array<string>
|
|
1179
|
+
nodeArgs: Array<string>
|
|
1180
|
+
registerPath: string
|
|
1181
|
+
rootPath?: string
|
|
1182
|
+
}): Array<string> {
|
|
1183
|
+
let registerUrl = pathToFileURL(options.registerPath)
|
|
1184
|
+
if (options.browserEventUrl !== undefined) {
|
|
1185
|
+
registerUrl.searchParams.set('browserEventUrl', options.browserEventUrl)
|
|
1186
|
+
}
|
|
1187
|
+
if (options.rootPath !== undefined) {
|
|
1188
|
+
registerUrl.searchParams.set('rootPath', options.rootPath)
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
return [
|
|
1192
|
+
...options.nodeArgs,
|
|
1193
|
+
`--conditions=${nodeHmrCondition}`,
|
|
1194
|
+
'--import',
|
|
1195
|
+
registerUrl.href,
|
|
1196
|
+
options.entry,
|
|
1197
|
+
...options.entryArgs,
|
|
1198
|
+
]
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
export function buildChildProcessEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
|
1202
|
+
return {
|
|
1203
|
+
...env,
|
|
1204
|
+
[nodeHmrEnvVar]: '1',
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
function isAddressInfo(value: string | AddressInfo | null): value is AddressInfo {
|
|
1209
|
+
return typeof value === 'object' && value !== null && typeof value.port === 'number'
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
function logHotUpdate(reason: string): void {
|
|
1213
|
+
console.log(`${styles.green('hmr update')} ${reason}`)
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
function logRestart(reason: string): void {
|
|
1217
|
+
console.log(`${styles.yellow('restart')} ${reason}`)
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
function formatChangedPaths(paths: string[], cwd: string): string {
|
|
1221
|
+
return [...new Set(paths.map((path) => formatChangedPath(path, cwd)))].join(', ')
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
function formatChangedPath(path: string, cwd: string): string {
|
|
1225
|
+
return (relative(cwd, path) || path).replace(/\\/g, '/')
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
async function stopChild(
|
|
1229
|
+
child: ChildProcess | undefined,
|
|
1230
|
+
options: { force: boolean; signal: NodeJS.Signals },
|
|
1231
|
+
) {
|
|
1232
|
+
if (child === undefined) return
|
|
1233
|
+
if (child.exitCode !== null || child.signalCode !== null) return
|
|
1234
|
+
|
|
1235
|
+
await new Promise<void>((resolvePromise) => {
|
|
1236
|
+
let timeout = options.force
|
|
1237
|
+
? setTimeout(() => child.kill('SIGKILL'), shutdownTimeoutMs)
|
|
1238
|
+
: undefined
|
|
1239
|
+
|
|
1240
|
+
child.once('exit', () => {
|
|
1241
|
+
if (timeout !== undefined) clearTimeout(timeout)
|
|
1242
|
+
resolvePromise()
|
|
1243
|
+
})
|
|
1244
|
+
|
|
1245
|
+
child.kill(options.signal)
|
|
1246
|
+
})
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
function isChildMessage(message: unknown): message is ChildMessage {
|
|
1250
|
+
if (typeof message !== 'object' || message === null || !('type' in message)) return false
|
|
1251
|
+
|
|
1252
|
+
if (message.type === 'node-hmr:child:browser-event-emitted') {
|
|
1253
|
+
return 'payload' in message && isHmrEventPayload(message.payload)
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
if (message.type === 'node-hmr:child:browser-hmr-channel-requested') {
|
|
1257
|
+
return 'requestId' in message && typeof message.requestId === 'number'
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
if (message.type === 'node-hmr:child:browser-hmr-watch-files-changed') {
|
|
1261
|
+
return (
|
|
1262
|
+
'id' in message &&
|
|
1263
|
+
typeof message.id === 'number' &&
|
|
1264
|
+
'delta' in message &&
|
|
1265
|
+
isWatchFileDelta(message.delta)
|
|
1266
|
+
)
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
if (message.type === 'node-hmr:child:browser-hmr-file-events-handled') {
|
|
1270
|
+
return (
|
|
1271
|
+
'requestId' in message &&
|
|
1272
|
+
typeof message.requestId === 'number' &&
|
|
1273
|
+
'events' in message &&
|
|
1274
|
+
Array.isArray(message.events) &&
|
|
1275
|
+
message.events.every(isBrowserHmrEvent) &&
|
|
1276
|
+
(!('error' in message) || typeof message.error === 'string')
|
|
1277
|
+
)
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
if (message.type === 'node-hmr:child:hot-module-updated') {
|
|
1281
|
+
return (
|
|
1282
|
+
'filePath' in message &&
|
|
1283
|
+
typeof message.filePath === 'string' &&
|
|
1284
|
+
'timestamp' in message &&
|
|
1285
|
+
typeof message.timestamp === 'number' &&
|
|
1286
|
+
'url' in message &&
|
|
1287
|
+
typeof message.url === 'string' &&
|
|
1288
|
+
(!('acceptedUrl' in message) || typeof message.acceptedUrl === 'string')
|
|
1289
|
+
)
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
if (message.type === 'node-hmr:child:hot-module-invalidated') {
|
|
1293
|
+
return (
|
|
1294
|
+
'acceptedUrl' in message &&
|
|
1295
|
+
typeof message.acceptedUrl === 'string' &&
|
|
1296
|
+
(!('message' in message) || typeof message.message === 'string') &&
|
|
1297
|
+
'timestamp' in message &&
|
|
1298
|
+
typeof message.timestamp === 'number' &&
|
|
1299
|
+
'url' in message &&
|
|
1300
|
+
typeof message.url === 'string'
|
|
1301
|
+
)
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
if (message.type === 'node-hmr:child:server-ready') return true
|
|
1305
|
+
|
|
1306
|
+
if (message.type === 'node-hmr:child:restart-requested') {
|
|
1307
|
+
return !('message' in message) || typeof message.message === 'string'
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
if (message.type === 'node-hmr:child:module-imported') {
|
|
1311
|
+
return (
|
|
1312
|
+
'depFilePath' in message &&
|
|
1313
|
+
typeof message.depFilePath === 'string' &&
|
|
1314
|
+
'depUrl' in message &&
|
|
1315
|
+
typeof message.depUrl === 'string' &&
|
|
1316
|
+
'importerFilePath' in message &&
|
|
1317
|
+
typeof message.importerFilePath === 'string' &&
|
|
1318
|
+
'importerUrl' in message &&
|
|
1319
|
+
typeof message.importerUrl === 'string'
|
|
1320
|
+
)
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
if (message.type === 'node-hmr:child:accepted-deps-resolved') {
|
|
1324
|
+
return (
|
|
1325
|
+
'url' in message &&
|
|
1326
|
+
typeof message.url === 'string' &&
|
|
1327
|
+
'acceptedDeps' in message &&
|
|
1328
|
+
Array.isArray(message.acceptedDeps) &&
|
|
1329
|
+
message.acceptedDeps.every((dep) => typeof dep === 'string')
|
|
1330
|
+
)
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
if (message.type !== 'node-hmr:child:module-analyzed') return false
|
|
1334
|
+
|
|
1335
|
+
return (
|
|
1336
|
+
'filePath' in message &&
|
|
1337
|
+
typeof message.filePath === 'string' &&
|
|
1338
|
+
'url' in message &&
|
|
1339
|
+
typeof message.url === 'string' &&
|
|
1340
|
+
'hmr' in message &&
|
|
1341
|
+
isHmrInfo(message.hmr)
|
|
1342
|
+
)
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
function isBrowserHmrEvent(event: unknown): event is BrowserHmrEvent {
|
|
1346
|
+
if (typeof event !== 'object' || event === null || !('type' in event)) return false
|
|
1347
|
+
|
|
1348
|
+
if (event.type === 'update') {
|
|
1349
|
+
return (
|
|
1350
|
+
'data' in event && isJsonObject(event.data) && isOptionalStringArrayProperty(event, 'files')
|
|
1351
|
+
)
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
if (event.type === 'reload') {
|
|
1355
|
+
return isOptionalStringArrayProperty(event, 'files')
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
return false
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
function isJsonObject(value: unknown): value is Record<string, unknown> {
|
|
1362
|
+
return isJsonValue(value) && typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
function isJsonValue(value: unknown, ancestors = new Set<object>()): boolean {
|
|
1366
|
+
if (
|
|
1367
|
+
value === null ||
|
|
1368
|
+
typeof value === 'string' ||
|
|
1369
|
+
typeof value === 'boolean' ||
|
|
1370
|
+
(typeof value === 'number' && Number.isFinite(value))
|
|
1371
|
+
) {
|
|
1372
|
+
return true
|
|
1373
|
+
}
|
|
1374
|
+
if (typeof value !== 'object') return false
|
|
1375
|
+
|
|
1376
|
+
let prototype = Object.getPrototypeOf(value)
|
|
1377
|
+
if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null) return false
|
|
1378
|
+
if (ancestors.has(value)) return false
|
|
1379
|
+
|
|
1380
|
+
ancestors.add(value)
|
|
1381
|
+
let values = Array.isArray(value) ? value : Object.values(value)
|
|
1382
|
+
let valid = values.every((item) => isJsonValue(item, ancestors))
|
|
1383
|
+
ancestors.delete(value)
|
|
1384
|
+
return valid
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
function isOptionalStringArrayProperty(value: object, property: string): boolean {
|
|
1388
|
+
if (!(property in value)) return true
|
|
1389
|
+
|
|
1390
|
+
let candidate = (value as Record<string, unknown>)[property]
|
|
1391
|
+
return Array.isArray(candidate) && candidate.every((item) => typeof item === 'string')
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
function isWatchFileDelta(value: unknown): value is { add: string[]; remove: string[] } {
|
|
1395
|
+
return (
|
|
1396
|
+
typeof value === 'object' &&
|
|
1397
|
+
value !== null &&
|
|
1398
|
+
'add' in value &&
|
|
1399
|
+
Array.isArray(value.add) &&
|
|
1400
|
+
value.add.every((item) => typeof item === 'string') &&
|
|
1401
|
+
'remove' in value &&
|
|
1402
|
+
Array.isArray(value.remove) &&
|
|
1403
|
+
value.remove.every((item) => typeof item === 'string')
|
|
1404
|
+
)
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
function isHmrInfo(value: unknown): value is ModuleRecord['hmr'] {
|
|
1408
|
+
return (
|
|
1409
|
+
typeof value === 'object' &&
|
|
1410
|
+
value !== null &&
|
|
1411
|
+
'acceptedDeps' in value &&
|
|
1412
|
+
Array.isArray(value.acceptedDeps) &&
|
|
1413
|
+
value.acceptedDeps.every((dep) => typeof dep === 'string') &&
|
|
1414
|
+
'selfAccepting' in value &&
|
|
1415
|
+
typeof value.selfAccepting === 'boolean' &&
|
|
1416
|
+
'usesImportMetaHot' in value &&
|
|
1417
|
+
typeof value.usesImportMetaHot === 'boolean'
|
|
1418
|
+
)
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
function formatUnknownError(error: unknown): string {
|
|
1422
|
+
return error instanceof Error ? error.message : String(error)
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
function isHmrEventPayload(value: unknown): value is HmrEventPayload {
|
|
1426
|
+
return (
|
|
1427
|
+
typeof value === 'object' && value !== null && 'type' in value && typeof value.type === 'string'
|
|
1428
|
+
)
|
|
1429
|
+
}
|