@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.
- package/LICENSE +21 -0
- package/README.md +306 -2
- package/dist/index.d.ts +128 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +107 -0
- package/dist/lib/browser-events.d.ts +99 -0
- package/dist/lib/browser-events.d.ts.map +1 -0
- package/dist/lib/browser-events.js +11 -0
- package/dist/lib/events.d.ts +29 -0
- package/dist/lib/events.d.ts.map +1 -0
- package/dist/lib/events.js +32 -0
- package/dist/lib/hmr-analysis.d.ts +17 -0
- package/dist/lib/hmr-analysis.d.ts.map +1 -0
- package/dist/lib/hmr-analysis.js +130 -0
- package/dist/lib/module-store.d.ts +27 -0
- package/dist/lib/module-store.d.ts.map +1 -0
- package/dist/lib/module-store.js +161 -0
- package/dist/lib/process-state.d.ts +3 -0
- package/dist/lib/process-state.d.ts.map +1 -0
- package/dist/lib/process-state.js +7 -0
- package/dist/lib/runner.d.ts +62 -0
- package/dist/lib/runner.d.ts.map +1 -0
- package/dist/lib/runner.js +1046 -0
- package/dist/lib/runtime-api.d.ts +7 -0
- package/dist/lib/runtime-api.d.ts.map +1 -0
- package/dist/lib/runtime-api.js +1 -0
- package/dist/lib/runtime.d.ts +46 -0
- package/dist/lib/runtime.d.ts.map +1 -0
- package/dist/lib/runtime.js +374 -0
- package/dist/register.d.ts +2 -0
- package/dist/register.d.ts.map +1 -0
- package/dist/register.js +317 -0
- package/dist/runtime.d.ts +26 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +32 -0
- package/dist/runtime.node-hmr.d.ts +27 -0
- package/dist/runtime.node-hmr.d.ts.map +1 -0
- package/dist/runtime.node-hmr.js +33 -0
- package/dist/types.d.ts +36 -0
- package/package.json +55 -5
- package/src/index.ts +244 -0
- package/src/lib/browser-events.ts +123 -0
- package/src/lib/events.ts +61 -0
- package/src/lib/hmr-analysis.ts +178 -0
- package/src/lib/module-store.ts +228 -0
- package/src/lib/process-state.ts +9 -0
- package/src/lib/runner.ts +1427 -0
- package/src/lib/runtime-api.ts +9 -0
- package/src/lib/runtime.ts +534 -0
- package/src/register.ts +401 -0
- package/src/runtime.node-hmr.ts +40 -0
- package/src/runtime.ts +40 -0
- package/src/types.d.ts +36 -0
package/src/register.ts
ADDED
|
@@ -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 {}
|