@remix-run/node-hmr 0.0.0 → 0.1.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 +306 -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 +99 -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 +1046 -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 +123 -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 +1427 -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,1427 @@
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
+ timestamp: event.timestamp,
899
+ type: 'browser:update',
900
+ updates: event.updates,
901
+ })
902
+ }
903
+ }
904
+ return 'events'
905
+ }
906
+
907
+ function markBrowserHmrEventServerPathsChecked(filePaths: readonly string[]): void {
908
+ for (let filePath of filePaths) {
909
+ pendingBrowserHmrEventServerPaths.delete(filePath)
910
+ }
911
+ }
912
+
913
+ function sendHotUpdate(moduleInfo: NodeHmrUpdate): boolean {
914
+ if (child === undefined || child.send === undefined || !child.connected) return false
915
+
916
+ let shouldWaitForEntryServerReady =
917
+ serverReadyCount > 0 &&
918
+ (moduleInfo.acceptedUrl ?? moduleInfo.url) === pathToFileURL(getEntryPath()).href
919
+ if (shouldWaitForEntryServerReady) {
920
+ waitingForEntryServerReady = true
921
+ }
922
+
923
+ return child.send({
924
+ acceptedUrl: moduleInfo.acceptedUrl,
925
+ invalidatedUrls: moduleInfo.invalidatedUrls,
926
+ type: 'node-hmr:parent:hot-module-changed',
927
+ url: moduleInfo.url,
928
+ timestamp: Date.now(),
929
+ })
930
+ }
931
+
932
+ function ready(): Promise<void> {
933
+ if (isReady()) return Promise.resolve()
934
+
935
+ return new Promise((resolvePromise) => {
936
+ readyWaiters.push(resolvePromise)
937
+ })
938
+ }
939
+
940
+ function resolveReadyWaiters(options: { force?: boolean } = {}): void {
941
+ if (!options.force && !isReady()) return
942
+
943
+ let waiters = readyWaiters
944
+ readyWaiters = []
945
+ for (let waiter of waiters) {
946
+ waiter()
947
+ }
948
+ }
949
+
950
+ let watcher = watch([], {
951
+ cwd: options.cwd,
952
+ ...resolveChokidarWatchOptions(options.watch),
953
+ })
954
+
955
+ watcher.on('all', handleWatchEvent)
956
+
957
+ let stopPromise: Promise<void> | undefined
958
+
959
+ async function stop(signal: NodeJS.Signals = 'SIGTERM'): Promise<void> {
960
+ stopping = true
961
+ if (restartTimer !== undefined) {
962
+ clearTimeout(restartTimer)
963
+ }
964
+ if (restartSettleTimer !== undefined) {
965
+ clearTimeout(restartSettleTimer)
966
+ }
967
+ if (browserHmrEventFlushTimer !== undefined) {
968
+ clearTimeout(browserHmrEventFlushTimer)
969
+ }
970
+ clearPendingHotUpdates()
971
+ resolveReadyWaiters({ force: true })
972
+ resolvePendingBrowserHmrRequests([])
973
+
974
+ stopPromise ??= Promise.resolve()
975
+ .then(() => watcher.close())
976
+ .then(() => stopChild(child, { force: true, signal }))
977
+ .then(async () => {
978
+ let channel = await browserHmrEventChannelPromise
979
+ await channel?.close()
980
+ })
981
+ .then(() => {
982
+ resolveRun?.()
983
+ })
984
+
985
+ await stopPromise
986
+ }
987
+
988
+ let supervisor = {
989
+ get generation() {
990
+ return serverGeneration
991
+ },
992
+
993
+ ready,
994
+
995
+ async start() {
996
+ return await new Promise<void>((resolvePromise) => {
997
+ resolveRun = resolvePromise
998
+ start()
999
+
1000
+ process.once('SIGINT', () => {
1001
+ stop('SIGINT').catch((error: unknown) => {
1002
+ console.error(error)
1003
+ })
1004
+ })
1005
+ process.once('SIGTERM', () => {
1006
+ stop('SIGTERM').catch((error: unknown) => {
1007
+ console.error(error)
1008
+ })
1009
+ })
1010
+ })
1011
+ },
1012
+
1013
+ stop,
1014
+ }
1015
+ chokidarWatcherBySupervisor.set(supervisor, watcher)
1016
+ return supervisor
1017
+ }
1018
+
1019
+ interface BrowserHmrEventChannel {
1020
+ close(): Promise<void>
1021
+ send(payload: HmrEventPayload): void
1022
+ url: string
1023
+ }
1024
+
1025
+ interface HmrEventClient {
1026
+ close(): void
1027
+ send(payload: HmrEventPayload): void
1028
+ }
1029
+
1030
+ async function createBrowserHmrEventChannel(
1031
+ options: BrowserHmrChannelOptions,
1032
+ ): Promise<BrowserHmrEventChannel> {
1033
+ let host = options.host ?? '127.0.0.1'
1034
+ let pathname = options.pathname ?? defaultBrowserHmrPathname
1035
+ let port = options.port ?? 0
1036
+ let clients = new Set<HmrEventClient>()
1037
+ let server: Server
1038
+
1039
+ server = createServer((request, response) => {
1040
+ if (request.url === undefined) {
1041
+ response.writeHead(404).end()
1042
+ return
1043
+ }
1044
+
1045
+ let requestUrl = new URL(request.url, `http://${host}`)
1046
+
1047
+ if (request.method === 'OPTIONS' && requestUrl.pathname === pathname) {
1048
+ writeCorsHeaders(response, 204)
1049
+ response.end()
1050
+ return
1051
+ }
1052
+
1053
+ if (request.method !== 'GET' || requestUrl.pathname !== pathname) {
1054
+ response.writeHead(404).end()
1055
+ return
1056
+ }
1057
+
1058
+ response.writeHead(200, {
1059
+ 'Access-Control-Allow-Origin': '*',
1060
+ 'Cache-Control': 'no-cache',
1061
+ Connection: 'keep-alive',
1062
+ 'Content-Type': 'text/event-stream; charset=utf-8',
1063
+ 'X-Accel-Buffering': 'no',
1064
+ })
1065
+ response.flushHeaders()
1066
+
1067
+ let client: HmrEventClient = {
1068
+ close() {
1069
+ response.end()
1070
+ clients.delete(client)
1071
+ },
1072
+ send(payload) {
1073
+ response.write(formatServerSentEvent(payload))
1074
+ },
1075
+ }
1076
+
1077
+ clients.add(client)
1078
+ response.once('close', () => {
1079
+ clients.delete(client)
1080
+ })
1081
+ response.write(': connected\n\n')
1082
+ })
1083
+
1084
+ let url = await listen(server, {
1085
+ host,
1086
+ pathname,
1087
+ port,
1088
+ serverName: 'node HMR browser channel',
1089
+ })
1090
+
1091
+ return {
1092
+ async close() {
1093
+ for (let client of clients) {
1094
+ client.close()
1095
+ }
1096
+ clients.clear()
1097
+
1098
+ await closeServer(server)
1099
+ },
1100
+
1101
+ send(payload) {
1102
+ for (let client of clients) {
1103
+ client.send(payload)
1104
+ }
1105
+ },
1106
+
1107
+ url,
1108
+ }
1109
+ }
1110
+
1111
+ async function listen(
1112
+ server: Server,
1113
+ options: {
1114
+ host: string
1115
+ pathname?: string
1116
+ port: number
1117
+ serverName: string
1118
+ },
1119
+ ): Promise<string> {
1120
+ return await new Promise<string>((resolvePromise, reject) => {
1121
+ server.once('error', reject)
1122
+ server.listen(options.port, options.host, () => {
1123
+ server.off('error', reject)
1124
+ let address = server.address()
1125
+ if (!isAddressInfo(address)) {
1126
+ reject(new Error(`Failed to start ${options.serverName}.`))
1127
+ return
1128
+ }
1129
+
1130
+ resolvePromise(`http://${options.host}:${address.port}${options.pathname ?? ''}`)
1131
+ })
1132
+ })
1133
+ }
1134
+
1135
+ async function closeServer(server: Server): Promise<void> {
1136
+ await new Promise<void>((resolvePromise, reject) => {
1137
+ server.close((error) => {
1138
+ if (error) {
1139
+ reject(error)
1140
+ return
1141
+ }
1142
+ resolvePromise()
1143
+ })
1144
+ })
1145
+ }
1146
+
1147
+ function writeCorsHeaders(response: ServerResponse, status: number): void {
1148
+ response.writeHead(status, {
1149
+ 'Access-Control-Allow-Headers': 'Cache-Control',
1150
+ 'Access-Control-Allow-Origin': '*',
1151
+ 'Access-Control-Allow-Methods': 'GET, OPTIONS',
1152
+ })
1153
+ }
1154
+
1155
+ function formatServerSentEvent(payload: HmrEventPayload): string {
1156
+ return `data: ${JSON.stringify(payload)}\n\n`
1157
+ }
1158
+
1159
+ export function resolveChokidarWatchOptions(
1160
+ options: NodeHmrWatchOptions = {},
1161
+ ): ResolvedChokidarWatchOptions {
1162
+ return {
1163
+ awaitWriteFinish: {
1164
+ pollInterval: 10,
1165
+ stabilityThreshold: 10,
1166
+ },
1167
+ depth: 0,
1168
+ ignorePermissionErrors: true,
1169
+ ignored: ['**/.git/**', ...(options.ignore ?? [])],
1170
+ ignoreInitial: true,
1171
+ interval: options.pollInterval ?? 100,
1172
+ usePolling: options.poll ?? process.platform === 'win32',
1173
+ }
1174
+ }
1175
+
1176
+ export function buildChildProcessArgs(options: {
1177
+ browserEventUrl?: string
1178
+ entry: string
1179
+ entryArgs: Array<string>
1180
+ nodeArgs: Array<string>
1181
+ registerPath: string
1182
+ rootPath?: string
1183
+ }): Array<string> {
1184
+ let registerUrl = pathToFileURL(options.registerPath)
1185
+ if (options.browserEventUrl !== undefined) {
1186
+ registerUrl.searchParams.set('browserEventUrl', options.browserEventUrl)
1187
+ }
1188
+ if (options.rootPath !== undefined) {
1189
+ registerUrl.searchParams.set('rootPath', options.rootPath)
1190
+ }
1191
+
1192
+ return [
1193
+ ...options.nodeArgs,
1194
+ `--conditions=${nodeHmrCondition}`,
1195
+ '--import',
1196
+ registerUrl.href,
1197
+ options.entry,
1198
+ ...options.entryArgs,
1199
+ ]
1200
+ }
1201
+
1202
+ export function buildChildProcessEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
1203
+ return {
1204
+ ...env,
1205
+ [nodeHmrEnvVar]: '1',
1206
+ }
1207
+ }
1208
+
1209
+ function isAddressInfo(value: string | AddressInfo | null): value is AddressInfo {
1210
+ return typeof value === 'object' && value !== null && typeof value.port === 'number'
1211
+ }
1212
+
1213
+ function logHotUpdate(reason: string): void {
1214
+ console.log(`${styles.green('hmr update')} ${reason}`)
1215
+ }
1216
+
1217
+ function logRestart(reason: string): void {
1218
+ console.log(`${styles.yellow('restart')} ${reason}`)
1219
+ }
1220
+
1221
+ function formatChangedPaths(paths: string[], cwd: string): string {
1222
+ return [...new Set(paths.map((path) => formatChangedPath(path, cwd)))].join(', ')
1223
+ }
1224
+
1225
+ function formatChangedPath(path: string, cwd: string): string {
1226
+ return (relative(cwd, path) || path).replace(/\\/g, '/')
1227
+ }
1228
+
1229
+ async function stopChild(
1230
+ child: ChildProcess | undefined,
1231
+ options: { force: boolean; signal: NodeJS.Signals },
1232
+ ) {
1233
+ if (child === undefined) return
1234
+ if (child.exitCode !== null || child.signalCode !== null) return
1235
+
1236
+ await new Promise<void>((resolvePromise) => {
1237
+ let timeout = options.force
1238
+ ? setTimeout(() => child.kill('SIGKILL'), shutdownTimeoutMs)
1239
+ : undefined
1240
+
1241
+ child.once('exit', () => {
1242
+ if (timeout !== undefined) clearTimeout(timeout)
1243
+ resolvePromise()
1244
+ })
1245
+
1246
+ child.kill(options.signal)
1247
+ })
1248
+ }
1249
+
1250
+ function isChildMessage(message: unknown): message is ChildMessage {
1251
+ if (typeof message !== 'object' || message === null || !('type' in message)) return false
1252
+
1253
+ if (message.type === 'node-hmr:child:browser-event-emitted') {
1254
+ return 'payload' in message && isHmrEventPayload(message.payload)
1255
+ }
1256
+
1257
+ if (message.type === 'node-hmr:child:browser-hmr-channel-requested') {
1258
+ return 'requestId' in message && typeof message.requestId === 'number'
1259
+ }
1260
+
1261
+ if (message.type === 'node-hmr:child:browser-hmr-watch-files-changed') {
1262
+ return (
1263
+ 'id' in message &&
1264
+ typeof message.id === 'number' &&
1265
+ 'delta' in message &&
1266
+ isWatchFileDelta(message.delta)
1267
+ )
1268
+ }
1269
+
1270
+ if (message.type === 'node-hmr:child:browser-hmr-file-events-handled') {
1271
+ return (
1272
+ 'requestId' in message &&
1273
+ typeof message.requestId === 'number' &&
1274
+ 'events' in message &&
1275
+ Array.isArray(message.events) &&
1276
+ message.events.every(isBrowserHmrEvent) &&
1277
+ (!('error' in message) || typeof message.error === 'string')
1278
+ )
1279
+ }
1280
+
1281
+ if (message.type === 'node-hmr:child:hot-module-updated') {
1282
+ return (
1283
+ 'filePath' in message &&
1284
+ typeof message.filePath === 'string' &&
1285
+ 'timestamp' in message &&
1286
+ typeof message.timestamp === 'number' &&
1287
+ 'url' in message &&
1288
+ typeof message.url === 'string' &&
1289
+ (!('acceptedUrl' in message) || typeof message.acceptedUrl === 'string')
1290
+ )
1291
+ }
1292
+
1293
+ if (message.type === 'node-hmr:child:hot-module-invalidated') {
1294
+ return (
1295
+ 'acceptedUrl' in message &&
1296
+ typeof message.acceptedUrl === 'string' &&
1297
+ (!('message' in message) || typeof message.message === 'string') &&
1298
+ 'timestamp' in message &&
1299
+ typeof message.timestamp === 'number' &&
1300
+ 'url' in message &&
1301
+ typeof message.url === 'string'
1302
+ )
1303
+ }
1304
+
1305
+ if (message.type === 'node-hmr:child:server-ready') return true
1306
+
1307
+ if (message.type === 'node-hmr:child:restart-requested') {
1308
+ return !('message' in message) || typeof message.message === 'string'
1309
+ }
1310
+
1311
+ if (message.type === 'node-hmr:child:module-imported') {
1312
+ return (
1313
+ 'depFilePath' in message &&
1314
+ typeof message.depFilePath === 'string' &&
1315
+ 'depUrl' in message &&
1316
+ typeof message.depUrl === 'string' &&
1317
+ 'importerFilePath' in message &&
1318
+ typeof message.importerFilePath === 'string' &&
1319
+ 'importerUrl' in message &&
1320
+ typeof message.importerUrl === 'string'
1321
+ )
1322
+ }
1323
+
1324
+ if (message.type === 'node-hmr:child:accepted-deps-resolved') {
1325
+ return (
1326
+ 'url' in message &&
1327
+ typeof message.url === 'string' &&
1328
+ 'acceptedDeps' in message &&
1329
+ Array.isArray(message.acceptedDeps) &&
1330
+ message.acceptedDeps.every((dep) => typeof dep === 'string')
1331
+ )
1332
+ }
1333
+
1334
+ if (message.type !== 'node-hmr:child:module-analyzed') return false
1335
+
1336
+ return (
1337
+ 'filePath' in message &&
1338
+ typeof message.filePath === 'string' &&
1339
+ 'url' in message &&
1340
+ typeof message.url === 'string' &&
1341
+ 'hmr' in message &&
1342
+ isHmrInfo(message.hmr)
1343
+ )
1344
+ }
1345
+
1346
+ function isBrowserHmrEvent(event: unknown): event is BrowserHmrEvent {
1347
+ if (typeof event !== 'object' || event === null || !('type' in event)) return false
1348
+
1349
+ if (event.type === 'update') {
1350
+ return (
1351
+ isOptionalStringArrayProperty(event, 'files') &&
1352
+ 'timestamp' in event &&
1353
+ typeof event.timestamp === 'number' &&
1354
+ 'updates' in event &&
1355
+ Array.isArray(event.updates) &&
1356
+ event.updates.every(isHmrBrowserUpdate)
1357
+ )
1358
+ }
1359
+
1360
+ if (event.type === 'reload') {
1361
+ return isOptionalStringArrayProperty(event, 'files')
1362
+ }
1363
+
1364
+ return false
1365
+ }
1366
+
1367
+ function isHmrBrowserUpdate(update: unknown): boolean {
1368
+ if (typeof update !== 'object' || update === null || !('type' in update)) return false
1369
+
1370
+ if (update.type === 'js') {
1371
+ return (
1372
+ 'path' in update &&
1373
+ typeof update.path === 'string' &&
1374
+ (!('acceptedPath' in update) || typeof update.acceptedPath === 'string')
1375
+ )
1376
+ }
1377
+
1378
+ if (update.type === 'css') {
1379
+ return 'path' in update && typeof update.path === 'string'
1380
+ }
1381
+
1382
+ return false
1383
+ }
1384
+
1385
+ function isOptionalStringArrayProperty(value: object, property: string): boolean {
1386
+ if (!(property in value)) return true
1387
+
1388
+ let candidate = (value as Record<string, unknown>)[property]
1389
+ return Array.isArray(candidate) && candidate.every((item) => typeof item === 'string')
1390
+ }
1391
+
1392
+ function isWatchFileDelta(value: unknown): value is { add: string[]; remove: string[] } {
1393
+ return (
1394
+ typeof value === 'object' &&
1395
+ value !== null &&
1396
+ 'add' in value &&
1397
+ Array.isArray(value.add) &&
1398
+ value.add.every((item) => typeof item === 'string') &&
1399
+ 'remove' in value &&
1400
+ Array.isArray(value.remove) &&
1401
+ value.remove.every((item) => typeof item === 'string')
1402
+ )
1403
+ }
1404
+
1405
+ function isHmrInfo(value: unknown): value is ModuleRecord['hmr'] {
1406
+ return (
1407
+ typeof value === 'object' &&
1408
+ value !== null &&
1409
+ 'acceptedDeps' in value &&
1410
+ Array.isArray(value.acceptedDeps) &&
1411
+ value.acceptedDeps.every((dep) => typeof dep === 'string') &&
1412
+ 'selfAccepting' in value &&
1413
+ typeof value.selfAccepting === 'boolean' &&
1414
+ 'usesImportMetaHot' in value &&
1415
+ typeof value.usesImportMetaHot === 'boolean'
1416
+ )
1417
+ }
1418
+
1419
+ function formatUnknownError(error: unknown): string {
1420
+ return error instanceof Error ? error.message : String(error)
1421
+ }
1422
+
1423
+ function isHmrEventPayload(value: unknown): value is HmrEventPayload {
1424
+ return (
1425
+ typeof value === 'object' && value !== null && 'type' in value && typeof value.type === 'string'
1426
+ )
1427
+ }