@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.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +305 -2
  3. package/dist/index.d.ts +128 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +107 -0
  6. package/dist/lib/browser-events.d.ts +87 -0
  7. package/dist/lib/browser-events.d.ts.map +1 -0
  8. package/dist/lib/browser-events.js +11 -0
  9. package/dist/lib/events.d.ts +29 -0
  10. package/dist/lib/events.d.ts.map +1 -0
  11. package/dist/lib/events.js +32 -0
  12. package/dist/lib/hmr-analysis.d.ts +17 -0
  13. package/dist/lib/hmr-analysis.d.ts.map +1 -0
  14. package/dist/lib/hmr-analysis.js +130 -0
  15. package/dist/lib/module-store.d.ts +27 -0
  16. package/dist/lib/module-store.d.ts.map +1 -0
  17. package/dist/lib/module-store.js +161 -0
  18. package/dist/lib/process-state.d.ts +3 -0
  19. package/dist/lib/process-state.d.ts.map +1 -0
  20. package/dist/lib/process-state.js +7 -0
  21. package/dist/lib/runner.d.ts +62 -0
  22. package/dist/lib/runner.d.ts.map +1 -0
  23. package/dist/lib/runner.js +1050 -0
  24. package/dist/lib/runtime-api.d.ts +7 -0
  25. package/dist/lib/runtime-api.d.ts.map +1 -0
  26. package/dist/lib/runtime-api.js +1 -0
  27. package/dist/lib/runtime.d.ts +46 -0
  28. package/dist/lib/runtime.d.ts.map +1 -0
  29. package/dist/lib/runtime.js +374 -0
  30. package/dist/register.d.ts +2 -0
  31. package/dist/register.d.ts.map +1 -0
  32. package/dist/register.js +317 -0
  33. package/dist/runtime.d.ts +26 -0
  34. package/dist/runtime.d.ts.map +1 -0
  35. package/dist/runtime.js +32 -0
  36. package/dist/runtime.node-hmr.d.ts +27 -0
  37. package/dist/runtime.node-hmr.d.ts.map +1 -0
  38. package/dist/runtime.node-hmr.js +33 -0
  39. package/dist/types.d.ts +36 -0
  40. package/package.json +55 -5
  41. package/src/index.ts +244 -0
  42. package/src/lib/browser-events.ts +113 -0
  43. package/src/lib/events.ts +61 -0
  44. package/src/lib/hmr-analysis.ts +178 -0
  45. package/src/lib/module-store.ts +228 -0
  46. package/src/lib/process-state.ts +9 -0
  47. package/src/lib/runner.ts +1429 -0
  48. package/src/lib/runtime-api.ts +9 -0
  49. package/src/lib/runtime.ts +534 -0
  50. package/src/register.ts +401 -0
  51. package/src/runtime.node-hmr.ts +40 -0
  52. package/src/runtime.ts +40 -0
  53. 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
+ }