@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,401 @@
1
+ import { registerHooks } from 'node:module'
2
+ import { isAbsolute, relative } from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+ import { SourceMapConsumer, SourceMapGenerator } from 'source-map-js/source-map.js'
5
+
6
+ import {
7
+ analyzeNodeHmrSource,
8
+ type NodeHmrAnalysis,
9
+ type ResolvedNodeHmrAnalysis,
10
+ } from './lib/hmr-analysis.ts'
11
+ import { markNodeHmrParentProcess } from './lib/process-state.ts'
12
+ import { installNodeHmrRuntime } from './lib/runtime.ts'
13
+
14
+ markNodeHmrParentProcess()
15
+ const runtime = installNodeHmrRuntime({
16
+ browserEventUrl: getBrowserEventUrl(),
17
+ })
18
+ const rootPath = getRegisterUrlParam('rootPath')
19
+ let invalidatedUrlTimestamps = new Map<string, number>()
20
+ let updateQueue = Promise.resolve()
21
+
22
+ registerHooks({
23
+ resolve(specifier, context, nextResolve) {
24
+ let result = nextResolve(specifier, context)
25
+ reportModuleImport(context.parentURL, result.url)
26
+ return result
27
+ },
28
+
29
+ load(url, context, nextLoad) {
30
+ let result = nextLoad(url, context)
31
+ let source = result.source
32
+
33
+ if (!shouldTransformModule(url, result.format, source)) return result
34
+
35
+ let canonicalUrl = getCanonicalUrl(url)
36
+ let transformedSource = transformSource(canonicalUrl, source)
37
+ transformedSource = rewriteInvalidatedImports(canonicalUrl, transformedSource)
38
+ let hmrAnalysis = analyzeNodeHmrSource(canonicalUrl, transformedSource)
39
+
40
+ if (!hmrAnalysis.usesImportMetaHot) {
41
+ reportModuleUpdate(canonicalUrl, {
42
+ acceptedDeps: [],
43
+ selfAccepting: false,
44
+ usesImportMetaHot: false,
45
+ })
46
+
47
+ return {
48
+ ...result,
49
+ source: transformedSource,
50
+ }
51
+ }
52
+
53
+ reportModuleUpdate(canonicalUrl, {
54
+ acceptedDeps: [],
55
+ selfAccepting: hmrAnalysis.selfAccepting,
56
+ usesImportMetaHot: true,
57
+ })
58
+
59
+ return {
60
+ ...result,
61
+ source: injectHotContext(canonicalUrl, transformedSource, hmrAnalysis),
62
+ }
63
+ },
64
+ })
65
+
66
+ function getRegisterUrlParam(name: string): string | undefined {
67
+ let value = new URL(import.meta.url).searchParams.get(name)
68
+ return value ?? undefined
69
+ }
70
+
71
+ function getBrowserEventUrl(): string | undefined {
72
+ let eventUrl = getRegisterUrlParam('browserEventUrl')
73
+ if (eventUrl === undefined) return undefined
74
+ return isHttpUrl(eventUrl) ? eventUrl : undefined
75
+ }
76
+
77
+ function isHttpUrl(value: string): boolean {
78
+ try {
79
+ let url = new URL(value)
80
+ return url.protocol === 'http:' || url.protocol === 'https:'
81
+ } catch {
82
+ return false
83
+ }
84
+ }
85
+
86
+ process.on('message', (message: unknown) => {
87
+ if (isBrowserHmrFileEventsMessage(message)) {
88
+ runtime.handleBrowserHmrFileEvents(message.requestId, message.events)
89
+ return
90
+ }
91
+
92
+ if (!isHmrUpdateMessage(message)) return
93
+
94
+ updateQueue = updateQueue.then(() => handleHotUpdateMessage(message))
95
+ void updateQueue
96
+ })
97
+
98
+ process.once('SIGINT', () => disposeOnSignal('SIGINT'))
99
+ process.once('SIGTERM', () => disposeOnSignal('SIGTERM'))
100
+
101
+ function shouldTransformModule(
102
+ url: string,
103
+ format: string | null | undefined,
104
+ source: unknown,
105
+ ): source is string {
106
+ if (!url.startsWith('file:')) return false
107
+ if (format !== 'module') return false
108
+ if (typeof source !== 'string') return false
109
+
110
+ return true
111
+ }
112
+
113
+ function injectHotContext(url: string, source: string, hmr: NodeHmrAnalysis): string {
114
+ let resolveDependencyExpression = `(specifier) => { let url = new URL(import.meta.resolve(specifier)); url.search = ''; url.hash = ''; return url.href }`
115
+ let sourceWithMap = extractInlineSourceMap(source)
116
+ let prelude = [
117
+ `const __remixNodeHmrResolveDependency = ${resolveDependencyExpression};`,
118
+ `globalThis.__remixNodeHmr.reportAcceptedDependencies(${JSON.stringify(url)}, ${getAcceptedDependencyExpression(hmr)});`,
119
+ `import.meta.hot = globalThis.__remixNodeHmr.createHotContext(${JSON.stringify(url)}, __remixNodeHmrResolveDependency);`,
120
+ ].join('\n')
121
+ let injectedSource = `${prelude}\n${sourceWithMap.code}`
122
+ let injectionSourceMap = createLineOffsetSourceMap(url, sourceWithMap.code, getLineCount(prelude))
123
+ let sourceMap =
124
+ sourceWithMap.sourceMap === null
125
+ ? injectionSourceMap
126
+ : composeSourceMaps(injectionSourceMap, sourceWithMap.sourceMap)
127
+
128
+ return appendInlineSourceMap(injectedSource, sourceMap)
129
+ }
130
+
131
+ function getAcceptedDependencyExpression(hmr: NodeHmrAnalysis): string {
132
+ return `[${hmr.acceptedDeps
133
+ .map(
134
+ (acceptedDep) => `__remixNodeHmrResolveDependency(${JSON.stringify(acceptedDep.specifier)})`,
135
+ )
136
+ .join(', ')}]`
137
+ }
138
+
139
+ function transformSource(url: string, source: string): string {
140
+ let filePath = fileURLToPath(url)
141
+ if (
142
+ url.includes('/node_modules/') ||
143
+ (rootPath !== undefined && !isInsideRoot(filePath, rootPath))
144
+ ) {
145
+ return source
146
+ }
147
+
148
+ return source
149
+ }
150
+
151
+ function rewriteInvalidatedImports(url: string, source: string): string {
152
+ if (invalidatedUrlTimestamps.size === 0) return source
153
+
154
+ let replacements: Array<{ end: number; specifier: string; start: number }> = []
155
+ let staticSpecifierPattern =
156
+ /\b(?:import\s+(?:[^'"()]*?\s+from\s*)?|export\s+[^'"()]*?\s+from\s*)(["'])([^"']+)\1/g
157
+
158
+ for (let match of source.matchAll(staticSpecifierPattern)) {
159
+ let quote = match[1]
160
+ let specifier = match[2]
161
+ if (quote === undefined || specifier === undefined || match.index === undefined) continue
162
+
163
+ let resolvedUrl = new URL(specifier, url).href
164
+ let timestamp = invalidatedUrlTimestamps.get(getCanonicalUrl(resolvedUrl))
165
+ if (timestamp === undefined) continue
166
+
167
+ let specifierStart = match.index + match[0].length - specifier.length - quote.length
168
+ replacements.push({
169
+ end: specifierStart + specifier.length,
170
+ specifier: addTimestampQuery(specifier, timestamp),
171
+ start: specifierStart,
172
+ })
173
+ }
174
+
175
+ if (replacements.length === 0) return source
176
+
177
+ let rewrittenSource = ''
178
+ let position = 0
179
+ for (let replacement of replacements) {
180
+ rewrittenSource += source.slice(position, replacement.start)
181
+ rewrittenSource += replacement.specifier
182
+ position = replacement.end
183
+ }
184
+ rewrittenSource += source.slice(position)
185
+ return rewrittenSource
186
+ }
187
+
188
+ function reportModuleUpdate(url: string, hmr: ResolvedNodeHmrAnalysis): void {
189
+ process.send?.({
190
+ type: 'node-hmr:child:module-analyzed',
191
+ url,
192
+ filePath: fileURLToPath(url),
193
+ hmr,
194
+ })
195
+ }
196
+
197
+ function reportModuleImport(parentUrl: string | undefined, url: string): void {
198
+ if (parentUrl === undefined) return
199
+
200
+ let canonicalParentUrl = getCanonicalUrl(parentUrl)
201
+ let canonicalUrl = getCanonicalUrl(url)
202
+ if (!canonicalParentUrl.startsWith('file:') || !canonicalUrl.startsWith('file:')) return
203
+
204
+ process.send?.({
205
+ type: 'node-hmr:child:module-imported',
206
+ importerFilePath: fileURLToPath(canonicalParentUrl),
207
+ importerUrl: canonicalParentUrl,
208
+ depFilePath: fileURLToPath(canonicalUrl),
209
+ depUrl: canonicalUrl,
210
+ })
211
+ }
212
+
213
+ function getCanonicalUrl(url: string): string {
214
+ let parsedUrl = new URL(url)
215
+ parsedUrl.search = ''
216
+ parsedUrl.hash = ''
217
+ return parsedUrl.href
218
+ }
219
+
220
+ function addTimestampQuery(specifier: string, timestamp: number): string {
221
+ return `${specifier}${specifier.includes('?') ? '&' : '?'}t=${timestamp}`
222
+ }
223
+
224
+ function isInsideRoot(filePath: string, root: string): boolean {
225
+ let relativePath = relative(root, filePath)
226
+ return relativePath === '' || (!relativePath.startsWith('..') && !isAbsolute(relativePath))
227
+ }
228
+
229
+ function getLineCount(source: string): number {
230
+ return source.split('\n').length
231
+ }
232
+
233
+ function extractInlineSourceMap(source: string): {
234
+ code: string
235
+ sourceMap: string | null
236
+ } {
237
+ let sourceMapPattern =
238
+ /(?:\/\/# sourceMappingURL=data:application\/json;base64,([A-Za-z0-9+/=]+)|\/\*# sourceMappingURL=data:application\/json;base64,([A-Za-z0-9+/=]+) \*\/)\s*$/g
239
+ let sourceMap: string | null = null
240
+ let code = source.replace(sourceMapPattern, (_match, lineComment, blockComment) => {
241
+ sourceMap = Buffer.from(lineComment ?? blockComment, 'base64').toString('utf-8')
242
+ return ''
243
+ })
244
+
245
+ return { code: code.trimEnd(), sourceMap }
246
+ }
247
+
248
+ function appendInlineSourceMap(source: string, sourceMap: string): string {
249
+ let encoded = Buffer.from(sourceMap).toString('base64')
250
+ return `${source}\n//# sourceMappingURL=data:application/json;base64,${encoded}`
251
+ }
252
+
253
+ function createLineOffsetSourceMap(url: string, source: string, lineOffset: number): string {
254
+ let generator = new SourceMapGenerator({ file: url })
255
+ let lines = source.split('\n')
256
+
257
+ for (let index = 0; index < lines.length; index++) {
258
+ generator.addMapping({
259
+ generated: {
260
+ column: 0,
261
+ line: index + lineOffset + 1,
262
+ },
263
+ original: {
264
+ column: 0,
265
+ line: index + 1,
266
+ },
267
+ source: url,
268
+ })
269
+ }
270
+
271
+ generator.setSourceContent(url, source)
272
+ return JSON.stringify(generator.toJSON())
273
+ }
274
+
275
+ function composeSourceMaps(rewriteSourceMap: string, transformSourceMap: string): string {
276
+ let rewriteConsumer = new SourceMapConsumer(JSON.parse(rewriteSourceMap))
277
+ let transformConsumer = new SourceMapConsumer(JSON.parse(transformSourceMap))
278
+ let generator = new SourceMapGenerator()
279
+
280
+ rewriteConsumer.eachMapping((mapping) => {
281
+ if (
282
+ mapping.originalLine == null ||
283
+ mapping.originalColumn == null ||
284
+ mapping.generatedLine == null ||
285
+ mapping.generatedColumn == null
286
+ ) {
287
+ return
288
+ }
289
+
290
+ let original = transformConsumer.originalPositionFor({
291
+ line: mapping.originalLine,
292
+ column: mapping.originalColumn,
293
+ })
294
+ if (original.line == null || original.column == null || original.source == null) return
295
+
296
+ generator.addMapping({
297
+ generated: {
298
+ line: mapping.generatedLine,
299
+ column: mapping.generatedColumn,
300
+ },
301
+ original: {
302
+ line: original.line,
303
+ column: original.column,
304
+ },
305
+ source: original.source,
306
+ name: original.name ?? mapping.name ?? undefined,
307
+ })
308
+ })
309
+
310
+ for (let source of transformConsumer.sources) {
311
+ let sourceContent = transformConsumer.sourceContentFor(source, true)
312
+ if (sourceContent !== null) {
313
+ generator.setSourceContent(source, sourceContent)
314
+ }
315
+ }
316
+
317
+ return JSON.stringify(generator.toJSON())
318
+ }
319
+
320
+ async function handleHotUpdateMessage(message: {
321
+ acceptedUrl?: string
322
+ invalidatedUrls?: Record<string, number>
323
+ timestamp: number
324
+ type: 'node-hmr:parent:hot-module-changed'
325
+ url: string
326
+ }): Promise<void> {
327
+ invalidatedUrlTimestamps = new Map(Object.entries(message.invalidatedUrls ?? {}))
328
+ try {
329
+ await runtime.update(message.url, message.timestamp, message.acceptedUrl)
330
+ } catch (error: unknown) {
331
+ process.send?.({
332
+ type: 'node-hmr:child:restart-requested',
333
+ message: error instanceof Error ? error.message : String(error),
334
+ })
335
+ }
336
+ }
337
+
338
+ function isHmrUpdateMessage(message: unknown): message is {
339
+ acceptedUrl?: string
340
+ invalidatedUrls?: Record<string, number>
341
+ timestamp: number
342
+ type: 'node-hmr:parent:hot-module-changed'
343
+ url: string
344
+ } {
345
+ return (
346
+ typeof message === 'object' &&
347
+ message !== null &&
348
+ 'type' in message &&
349
+ message.type === 'node-hmr:parent:hot-module-changed' &&
350
+ 'url' in message &&
351
+ typeof message.url === 'string' &&
352
+ 'timestamp' in message &&
353
+ typeof message.timestamp === 'number' &&
354
+ (!('acceptedUrl' in message) || typeof message.acceptedUrl === 'string') &&
355
+ (!('invalidatedUrls' in message) || isInvalidatedUrls(message.invalidatedUrls))
356
+ )
357
+ }
358
+
359
+ function isBrowserHmrFileEventsMessage(message: unknown): message is {
360
+ events: Array<{ event: 'add' | 'change' | 'unlink'; filePath: string }>
361
+ requestId: number
362
+ type: 'node-hmr:parent:browser-hmr-file-events'
363
+ } {
364
+ return (
365
+ typeof message === 'object' &&
366
+ message !== null &&
367
+ 'type' in message &&
368
+ message.type === 'node-hmr:parent:browser-hmr-file-events' &&
369
+ 'requestId' in message &&
370
+ typeof message.requestId === 'number' &&
371
+ 'events' in message &&
372
+ Array.isArray(message.events) &&
373
+ message.events.every(
374
+ (event) =>
375
+ typeof event === 'object' &&
376
+ event !== null &&
377
+ 'filePath' in event &&
378
+ typeof event.filePath === 'string' &&
379
+ 'event' in event &&
380
+ (event.event === 'add' || event.event === 'change' || event.event === 'unlink'),
381
+ )
382
+ )
383
+ }
384
+
385
+ function isInvalidatedUrls(value: unknown): value is Record<string, number> {
386
+ if (typeof value !== 'object' || value === null) return false
387
+
388
+ for (let timestamp of Object.values(value)) {
389
+ if (typeof timestamp !== 'number') return false
390
+ }
391
+
392
+ return true
393
+ }
394
+
395
+ function disposeOnSignal(signal: NodeJS.Signals) {
396
+ runtime.disposeAll().finally(() => {
397
+ if (process.listenerCount(signal) === 0) {
398
+ process.exit(signal === 'SIGINT' ? 130 : 143)
399
+ }
400
+ })
401
+ }
@@ -0,0 +1,40 @@
1
+ import { emitServerReady, getNodeHmrRuntime } from './lib/runtime.ts'
2
+ import type { NodeHmrRuntimeApi } from './lib/runtime-api.ts'
3
+ import { nodeHmrRuntimeUnavailableError } from './lib/runtime-api.ts'
4
+
5
+ export type { BrowserHmrChannel } from './lib/browser-events.ts'
6
+
7
+ const maybeNodeHmrRuntime = getNodeHmrRuntime()
8
+ if (maybeNodeHmrRuntime === undefined) {
9
+ throw new Error(nodeHmrRuntimeUnavailableError)
10
+ }
11
+ const nodeHmrRuntime = maybeNodeHmrRuntime
12
+
13
+ /**
14
+ * Connects browser asset tooling in this child process to the browser HMR event stream and file
15
+ * watcher owned by its `node-hmr` parent process.
16
+ *
17
+ * Pass this function as the `hmr` factory for `createAssetServer()`. Each call creates an
18
+ * independent channel that must be closed when its owner shuts down. The returned promise rejects
19
+ * when browser HMR is disabled for the runner.
20
+ *
21
+ * The `remix/node-hmr/runtime` module itself can only be imported by a process supervised by
22
+ * `node-hmr`. Use a dynamic import guarded by `process.env.REMIX_NODE_HMR` when the same entry module
23
+ * also runs without HMR supervision.
24
+ *
25
+ * @returns A child-scoped channel for watching browser source files and publishing HMR events.
26
+ */
27
+ export const createBrowserHmrChannel: NodeHmrRuntimeApi['createBrowserHmrChannel'] =
28
+ async function createBrowserHmrChannel() {
29
+ return await nodeHmrRuntime.createBrowserHmrChannel()
30
+ }
31
+
32
+ /**
33
+ * Notifies the `node-hmr` parent that this child process is ready to serve requests.
34
+ *
35
+ * Call this after the app server starts listening. After a restart, `node-hmr` waits for this
36
+ * signal before publishing the browser `server:update` event, preventing clients from refreshing
37
+ * against a server that is not ready yet.
38
+ */
39
+ const emitRuntimeServerReady: NodeHmrRuntimeApi['emitServerReady'] = emitServerReady
40
+ export { emitRuntimeServerReady as emitServerReady }
package/src/runtime.ts ADDED
@@ -0,0 +1,40 @@
1
+ import type { NodeHmrRuntimeApi } from './lib/runtime-api.ts'
2
+ import { nodeHmrRuntimeUnavailableError } from './lib/runtime-api.ts'
3
+
4
+ export type { BrowserHmrChannel } from './lib/browser-events.ts'
5
+
6
+ /**
7
+ * Connects browser asset tooling in this child process to the browser HMR event stream and file
8
+ * watcher owned by its `node-hmr` parent process.
9
+ *
10
+ * Pass this function as the `hmr` factory for `createAssetServer()`. Each call creates an
11
+ * independent channel that must be closed when its owner shuts down. The returned promise rejects
12
+ * when browser HMR is disabled for the runner.
13
+ *
14
+ * The `remix/node-hmr/runtime` module itself can only be imported by a process supervised by
15
+ * `node-hmr`. Use a dynamic import guarded by `process.env.REMIX_NODE_HMR` when the same entry module
16
+ * also runs without HMR supervision.
17
+ *
18
+ * @returns A child-scoped channel for watching browser source files and publishing HMR events.
19
+ */
20
+ export const createBrowserHmrChannel: NodeHmrRuntimeApi['createBrowserHmrChannel'] =
21
+ async function createBrowserHmrChannel() {
22
+ throwNodeHmrRuntimeUnavailable()
23
+ }
24
+
25
+ /**
26
+ * Notifies the `node-hmr` parent that this child process is ready to serve requests.
27
+ *
28
+ * Call this after the app server starts listening. After a restart, `node-hmr` waits for this
29
+ * signal before publishing the browser `server:update` event, preventing clients from refreshing
30
+ * against a server that is not ready yet.
31
+ */
32
+ export const emitServerReady: NodeHmrRuntimeApi['emitServerReady'] = function emitServerReady() {
33
+ throwNodeHmrRuntimeUnavailable()
34
+ }
35
+
36
+ throwNodeHmrRuntimeUnavailable()
37
+
38
+ function throwNodeHmrRuntimeUnavailable(): never {
39
+ throw new Error(nodeHmrRuntimeUnavailableError)
40
+ }
package/src/types.d.ts ADDED
@@ -0,0 +1,36 @@
1
+ interface ImportMetaHot {
2
+ /** Mutable state preserved for this module across accepted updates and passed to dispose handlers. */
3
+ readonly data: Record<string, unknown>
4
+ /** Accepts updates to this module, optionally receiving its newly evaluated namespace. */
5
+ accept(callback?: (module: HotModule) => HotCallbackResult): void
6
+ /** Accepts updates from one dependency, optionally receiving its newly evaluated namespace. */
7
+ accept(dep: string, callback?: (module: HotModule) => HotCallbackResult): void
8
+ /**
9
+ * Accepts updates from multiple dependencies. The callback array preserves `deps` order and
10
+ * contains the updated namespace only at the position of the dependency that changed.
11
+ */
12
+ accept(
13
+ deps: readonly string[],
14
+ callback?: (modules: Array<HotModule | undefined>) => HotCallbackResult,
15
+ ): void
16
+ /** Registers cleanup that runs before this module is re-evaluated or the runtime is disposed. */
17
+ dispose(callback: (data: Record<string, unknown>) => HotCallbackResult): void
18
+ /** Declines the current update and asks the runner to restart the child process. */
19
+ invalidate(message?: string): void
20
+ /** Registers a custom-event listener for API compatibility. Server modules receive no events. */
21
+ on(event: string, callback: (data: unknown) => void | Promise<void>): void
22
+ }
23
+
24
+ type HotModule = Readonly<Record<string, unknown>> & {
25
+ readonly [Symbol.toStringTag]: 'Module'
26
+ }
27
+
28
+ type HotCallbackResult = void | Promise<void>
29
+
30
+ declare global {
31
+ interface ImportMeta {
32
+ readonly hot?: ImportMetaHot
33
+ }
34
+ }
35
+
36
+ export {}