@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,317 @@
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
+ import { analyzeNodeHmrSource, } from './lib/hmr-analysis.js';
6
+ import { markNodeHmrParentProcess } from './lib/process-state.js';
7
+ import { installNodeHmrRuntime } from './lib/runtime.js';
8
+ markNodeHmrParentProcess();
9
+ const runtime = installNodeHmrRuntime({
10
+ browserEventUrl: getBrowserEventUrl(),
11
+ });
12
+ const rootPath = getRegisterUrlParam('rootPath');
13
+ let invalidatedUrlTimestamps = new Map();
14
+ let updateQueue = Promise.resolve();
15
+ registerHooks({
16
+ resolve(specifier, context, nextResolve) {
17
+ let result = nextResolve(specifier, context);
18
+ reportModuleImport(context.parentURL, result.url);
19
+ return result;
20
+ },
21
+ load(url, context, nextLoad) {
22
+ let result = nextLoad(url, context);
23
+ let source = result.source;
24
+ if (!shouldTransformModule(url, result.format, source))
25
+ return result;
26
+ let canonicalUrl = getCanonicalUrl(url);
27
+ let transformedSource = transformSource(canonicalUrl, source);
28
+ transformedSource = rewriteInvalidatedImports(canonicalUrl, transformedSource);
29
+ let hmrAnalysis = analyzeNodeHmrSource(canonicalUrl, transformedSource);
30
+ if (!hmrAnalysis.usesImportMetaHot) {
31
+ reportModuleUpdate(canonicalUrl, {
32
+ acceptedDeps: [],
33
+ selfAccepting: false,
34
+ usesImportMetaHot: false,
35
+ });
36
+ return {
37
+ ...result,
38
+ source: transformedSource,
39
+ };
40
+ }
41
+ reportModuleUpdate(canonicalUrl, {
42
+ acceptedDeps: [],
43
+ selfAccepting: hmrAnalysis.selfAccepting,
44
+ usesImportMetaHot: true,
45
+ });
46
+ return {
47
+ ...result,
48
+ source: injectHotContext(canonicalUrl, transformedSource, hmrAnalysis),
49
+ };
50
+ },
51
+ });
52
+ function getRegisterUrlParam(name) {
53
+ let value = new URL(import.meta.url).searchParams.get(name);
54
+ return value ?? undefined;
55
+ }
56
+ function getBrowserEventUrl() {
57
+ let eventUrl = getRegisterUrlParam('browserEventUrl');
58
+ if (eventUrl === undefined)
59
+ return undefined;
60
+ return isHttpUrl(eventUrl) ? eventUrl : undefined;
61
+ }
62
+ function isHttpUrl(value) {
63
+ try {
64
+ let url = new URL(value);
65
+ return url.protocol === 'http:' || url.protocol === 'https:';
66
+ }
67
+ catch {
68
+ return false;
69
+ }
70
+ }
71
+ process.on('message', (message) => {
72
+ if (isBrowserHmrFileEventsMessage(message)) {
73
+ runtime.handleBrowserHmrFileEvents(message.requestId, message.events);
74
+ return;
75
+ }
76
+ if (!isHmrUpdateMessage(message))
77
+ return;
78
+ updateQueue = updateQueue.then(() => handleHotUpdateMessage(message));
79
+ void updateQueue;
80
+ });
81
+ process.once('SIGINT', () => disposeOnSignal('SIGINT'));
82
+ process.once('SIGTERM', () => disposeOnSignal('SIGTERM'));
83
+ function shouldTransformModule(url, format, source) {
84
+ if (!url.startsWith('file:'))
85
+ return false;
86
+ if (format !== 'module')
87
+ return false;
88
+ if (typeof source !== 'string')
89
+ return false;
90
+ return true;
91
+ }
92
+ function injectHotContext(url, source, hmr) {
93
+ let resolveDependencyExpression = `(specifier) => { let url = new URL(import.meta.resolve(specifier)); url.search = ''; url.hash = ''; return url.href }`;
94
+ let sourceWithMap = extractInlineSourceMap(source);
95
+ let prelude = [
96
+ `const __remixNodeHmrResolveDependency = ${resolveDependencyExpression};`,
97
+ `globalThis.__remixNodeHmr.reportAcceptedDependencies(${JSON.stringify(url)}, ${getAcceptedDependencyExpression(hmr)});`,
98
+ `import.meta.hot = globalThis.__remixNodeHmr.createHotContext(${JSON.stringify(url)}, __remixNodeHmrResolveDependency);`,
99
+ ].join('\n');
100
+ let injectedSource = `${prelude}\n${sourceWithMap.code}`;
101
+ let injectionSourceMap = createLineOffsetSourceMap(url, sourceWithMap.code, getLineCount(prelude));
102
+ let sourceMap = sourceWithMap.sourceMap === null
103
+ ? injectionSourceMap
104
+ : composeSourceMaps(injectionSourceMap, sourceWithMap.sourceMap);
105
+ return appendInlineSourceMap(injectedSource, sourceMap);
106
+ }
107
+ function getAcceptedDependencyExpression(hmr) {
108
+ return `[${hmr.acceptedDeps
109
+ .map((acceptedDep) => `__remixNodeHmrResolveDependency(${JSON.stringify(acceptedDep.specifier)})`)
110
+ .join(', ')}]`;
111
+ }
112
+ function transformSource(url, source) {
113
+ let filePath = fileURLToPath(url);
114
+ if (url.includes('/node_modules/') ||
115
+ (rootPath !== undefined && !isInsideRoot(filePath, rootPath))) {
116
+ return source;
117
+ }
118
+ return source;
119
+ }
120
+ function rewriteInvalidatedImports(url, source) {
121
+ if (invalidatedUrlTimestamps.size === 0)
122
+ return source;
123
+ let replacements = [];
124
+ let staticSpecifierPattern = /\b(?:import\s+(?:[^'"()]*?\s+from\s*)?|export\s+[^'"()]*?\s+from\s*)(["'])([^"']+)\1/g;
125
+ for (let match of source.matchAll(staticSpecifierPattern)) {
126
+ let quote = match[1];
127
+ let specifier = match[2];
128
+ if (quote === undefined || specifier === undefined || match.index === undefined)
129
+ continue;
130
+ let resolvedUrl = new URL(specifier, url).href;
131
+ let timestamp = invalidatedUrlTimestamps.get(getCanonicalUrl(resolvedUrl));
132
+ if (timestamp === undefined)
133
+ continue;
134
+ let specifierStart = match.index + match[0].length - specifier.length - quote.length;
135
+ replacements.push({
136
+ end: specifierStart + specifier.length,
137
+ specifier: addTimestampQuery(specifier, timestamp),
138
+ start: specifierStart,
139
+ });
140
+ }
141
+ if (replacements.length === 0)
142
+ return source;
143
+ let rewrittenSource = '';
144
+ let position = 0;
145
+ for (let replacement of replacements) {
146
+ rewrittenSource += source.slice(position, replacement.start);
147
+ rewrittenSource += replacement.specifier;
148
+ position = replacement.end;
149
+ }
150
+ rewrittenSource += source.slice(position);
151
+ return rewrittenSource;
152
+ }
153
+ function reportModuleUpdate(url, hmr) {
154
+ process.send?.({
155
+ type: 'node-hmr:child:module-analyzed',
156
+ url,
157
+ filePath: fileURLToPath(url),
158
+ hmr,
159
+ });
160
+ }
161
+ function reportModuleImport(parentUrl, url) {
162
+ if (parentUrl === undefined)
163
+ return;
164
+ let canonicalParentUrl = getCanonicalUrl(parentUrl);
165
+ let canonicalUrl = getCanonicalUrl(url);
166
+ if (!canonicalParentUrl.startsWith('file:') || !canonicalUrl.startsWith('file:'))
167
+ return;
168
+ process.send?.({
169
+ type: 'node-hmr:child:module-imported',
170
+ importerFilePath: fileURLToPath(canonicalParentUrl),
171
+ importerUrl: canonicalParentUrl,
172
+ depFilePath: fileURLToPath(canonicalUrl),
173
+ depUrl: canonicalUrl,
174
+ });
175
+ }
176
+ function getCanonicalUrl(url) {
177
+ let parsedUrl = new URL(url);
178
+ parsedUrl.search = '';
179
+ parsedUrl.hash = '';
180
+ return parsedUrl.href;
181
+ }
182
+ function addTimestampQuery(specifier, timestamp) {
183
+ return `${specifier}${specifier.includes('?') ? '&' : '?'}t=${timestamp}`;
184
+ }
185
+ function isInsideRoot(filePath, root) {
186
+ let relativePath = relative(root, filePath);
187
+ return relativePath === '' || (!relativePath.startsWith('..') && !isAbsolute(relativePath));
188
+ }
189
+ function getLineCount(source) {
190
+ return source.split('\n').length;
191
+ }
192
+ function extractInlineSourceMap(source) {
193
+ let sourceMapPattern = /(?:\/\/# sourceMappingURL=data:application\/json;base64,([A-Za-z0-9+/=]+)|\/\*# sourceMappingURL=data:application\/json;base64,([A-Za-z0-9+/=]+) \*\/)\s*$/g;
194
+ let sourceMap = null;
195
+ let code = source.replace(sourceMapPattern, (_match, lineComment, blockComment) => {
196
+ sourceMap = Buffer.from(lineComment ?? blockComment, 'base64').toString('utf-8');
197
+ return '';
198
+ });
199
+ return { code: code.trimEnd(), sourceMap };
200
+ }
201
+ function appendInlineSourceMap(source, sourceMap) {
202
+ let encoded = Buffer.from(sourceMap).toString('base64');
203
+ return `${source}\n//# sourceMappingURL=data:application/json;base64,${encoded}`;
204
+ }
205
+ function createLineOffsetSourceMap(url, source, lineOffset) {
206
+ let generator = new SourceMapGenerator({ file: url });
207
+ let lines = source.split('\n');
208
+ for (let index = 0; index < lines.length; index++) {
209
+ generator.addMapping({
210
+ generated: {
211
+ column: 0,
212
+ line: index + lineOffset + 1,
213
+ },
214
+ original: {
215
+ column: 0,
216
+ line: index + 1,
217
+ },
218
+ source: url,
219
+ });
220
+ }
221
+ generator.setSourceContent(url, source);
222
+ return JSON.stringify(generator.toJSON());
223
+ }
224
+ function composeSourceMaps(rewriteSourceMap, transformSourceMap) {
225
+ let rewriteConsumer = new SourceMapConsumer(JSON.parse(rewriteSourceMap));
226
+ let transformConsumer = new SourceMapConsumer(JSON.parse(transformSourceMap));
227
+ let generator = new SourceMapGenerator();
228
+ rewriteConsumer.eachMapping((mapping) => {
229
+ if (mapping.originalLine == null ||
230
+ mapping.originalColumn == null ||
231
+ mapping.generatedLine == null ||
232
+ mapping.generatedColumn == null) {
233
+ return;
234
+ }
235
+ let original = transformConsumer.originalPositionFor({
236
+ line: mapping.originalLine,
237
+ column: mapping.originalColumn,
238
+ });
239
+ if (original.line == null || original.column == null || original.source == null)
240
+ return;
241
+ generator.addMapping({
242
+ generated: {
243
+ line: mapping.generatedLine,
244
+ column: mapping.generatedColumn,
245
+ },
246
+ original: {
247
+ line: original.line,
248
+ column: original.column,
249
+ },
250
+ source: original.source,
251
+ name: original.name ?? mapping.name ?? undefined,
252
+ });
253
+ });
254
+ for (let source of transformConsumer.sources) {
255
+ let sourceContent = transformConsumer.sourceContentFor(source, true);
256
+ if (sourceContent !== null) {
257
+ generator.setSourceContent(source, sourceContent);
258
+ }
259
+ }
260
+ return JSON.stringify(generator.toJSON());
261
+ }
262
+ async function handleHotUpdateMessage(message) {
263
+ invalidatedUrlTimestamps = new Map(Object.entries(message.invalidatedUrls ?? {}));
264
+ try {
265
+ await runtime.update(message.url, message.timestamp, message.acceptedUrl);
266
+ }
267
+ catch (error) {
268
+ process.send?.({
269
+ type: 'node-hmr:child:restart-requested',
270
+ message: error instanceof Error ? error.message : String(error),
271
+ });
272
+ }
273
+ }
274
+ function isHmrUpdateMessage(message) {
275
+ return (typeof message === 'object' &&
276
+ message !== null &&
277
+ 'type' in message &&
278
+ message.type === 'node-hmr:parent:hot-module-changed' &&
279
+ 'url' in message &&
280
+ typeof message.url === 'string' &&
281
+ 'timestamp' in message &&
282
+ typeof message.timestamp === 'number' &&
283
+ (!('acceptedUrl' in message) || typeof message.acceptedUrl === 'string') &&
284
+ (!('invalidatedUrls' in message) || isInvalidatedUrls(message.invalidatedUrls)));
285
+ }
286
+ function isBrowserHmrFileEventsMessage(message) {
287
+ return (typeof message === 'object' &&
288
+ message !== null &&
289
+ 'type' in message &&
290
+ message.type === 'node-hmr:parent:browser-hmr-file-events' &&
291
+ 'requestId' in message &&
292
+ typeof message.requestId === 'number' &&
293
+ 'events' in message &&
294
+ Array.isArray(message.events) &&
295
+ message.events.every((event) => typeof event === 'object' &&
296
+ event !== null &&
297
+ 'filePath' in event &&
298
+ typeof event.filePath === 'string' &&
299
+ 'event' in event &&
300
+ (event.event === 'add' || event.event === 'change' || event.event === 'unlink')));
301
+ }
302
+ function isInvalidatedUrls(value) {
303
+ if (typeof value !== 'object' || value === null)
304
+ return false;
305
+ for (let timestamp of Object.values(value)) {
306
+ if (typeof timestamp !== 'number')
307
+ return false;
308
+ }
309
+ return true;
310
+ }
311
+ function disposeOnSignal(signal) {
312
+ runtime.disposeAll().finally(() => {
313
+ if (process.listenerCount(signal) === 0) {
314
+ process.exit(signal === 'SIGINT' ? 130 : 143);
315
+ }
316
+ });
317
+ }
@@ -0,0 +1,26 @@
1
+ import type { NodeHmrRuntimeApi } from './lib/runtime-api.ts';
2
+ export type { BrowserHmrChannel } from './lib/browser-events.ts';
3
+ /**
4
+ * Connects browser asset tooling in this child process to the browser HMR event stream and file
5
+ * watcher owned by its `node-hmr` parent process.
6
+ *
7
+ * Pass this function as the `hmr` factory for `createAssetServer()`. Each call creates an
8
+ * independent channel that must be closed when its owner shuts down. The returned promise rejects
9
+ * when browser HMR is disabled for the runner.
10
+ *
11
+ * The `remix/node-hmr/runtime` module itself can only be imported by a process supervised by
12
+ * `node-hmr`. Use a dynamic import guarded by `process.env.REMIX_NODE_HMR` when the same entry module
13
+ * also runs without HMR supervision.
14
+ *
15
+ * @returns A child-scoped channel for watching browser source files and publishing HMR events.
16
+ */
17
+ export declare const createBrowserHmrChannel: NodeHmrRuntimeApi['createBrowserHmrChannel'];
18
+ /**
19
+ * Notifies the `node-hmr` parent that this child process is ready to serve requests.
20
+ *
21
+ * Call this after the app server starts listening. After a restart, `node-hmr` waits for this
22
+ * signal before publishing the browser `server:update` event, preventing clients from refreshing
23
+ * against a server that is not ready yet.
24
+ */
25
+ export declare const emitServerReady: NodeHmrRuntimeApi['emitServerReady'];
26
+ //# sourceMappingURL=runtime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AAG7D,YAAY,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAA;AAEhE;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,uBAAuB,EAAE,iBAAiB,CAAC,yBAAyB,CAG9E,CAAA;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,EAAE,iBAAiB,CAAC,iBAAiB,CAEhE,CAAA"}
@@ -0,0 +1,32 @@
1
+ import { nodeHmrRuntimeUnavailableError } from './lib/runtime-api.js';
2
+ /**
3
+ * Connects browser asset tooling in this child process to the browser HMR event stream and file
4
+ * watcher owned by its `node-hmr` parent process.
5
+ *
6
+ * Pass this function as the `hmr` factory for `createAssetServer()`. Each call creates an
7
+ * independent channel that must be closed when its owner shuts down. The returned promise rejects
8
+ * when browser HMR is disabled for the runner.
9
+ *
10
+ * The `remix/node-hmr/runtime` module itself can only be imported by a process supervised by
11
+ * `node-hmr`. Use a dynamic import guarded by `process.env.REMIX_NODE_HMR` when the same entry module
12
+ * also runs without HMR supervision.
13
+ *
14
+ * @returns A child-scoped channel for watching browser source files and publishing HMR events.
15
+ */
16
+ export const createBrowserHmrChannel = async function createBrowserHmrChannel() {
17
+ throwNodeHmrRuntimeUnavailable();
18
+ };
19
+ /**
20
+ * Notifies the `node-hmr` parent that this child process is ready to serve requests.
21
+ *
22
+ * Call this after the app server starts listening. After a restart, `node-hmr` waits for this
23
+ * signal before publishing the browser `server:update` event, preventing clients from refreshing
24
+ * against a server that is not ready yet.
25
+ */
26
+ export const emitServerReady = function emitServerReady() {
27
+ throwNodeHmrRuntimeUnavailable();
28
+ };
29
+ throwNodeHmrRuntimeUnavailable();
30
+ function throwNodeHmrRuntimeUnavailable() {
31
+ throw new Error(nodeHmrRuntimeUnavailableError);
32
+ }
@@ -0,0 +1,27 @@
1
+ import type { NodeHmrRuntimeApi } from './lib/runtime-api.ts';
2
+ export type { BrowserHmrChannel } from './lib/browser-events.ts';
3
+ /**
4
+ * Connects browser asset tooling in this child process to the browser HMR event stream and file
5
+ * watcher owned by its `node-hmr` parent process.
6
+ *
7
+ * Pass this function as the `hmr` factory for `createAssetServer()`. Each call creates an
8
+ * independent channel that must be closed when its owner shuts down. The returned promise rejects
9
+ * when browser HMR is disabled for the runner.
10
+ *
11
+ * The `remix/node-hmr/runtime` module itself can only be imported by a process supervised by
12
+ * `node-hmr`. Use a dynamic import guarded by `process.env.REMIX_NODE_HMR` when the same entry module
13
+ * also runs without HMR supervision.
14
+ *
15
+ * @returns A child-scoped channel for watching browser source files and publishing HMR events.
16
+ */
17
+ export declare const createBrowserHmrChannel: NodeHmrRuntimeApi['createBrowserHmrChannel'];
18
+ /**
19
+ * Notifies the `node-hmr` parent that this child process is ready to serve requests.
20
+ *
21
+ * Call this after the app server starts listening. After a restart, `node-hmr` waits for this
22
+ * signal before publishing the browser `server:update` event, preventing clients from refreshing
23
+ * against a server that is not ready yet.
24
+ */
25
+ declare const emitRuntimeServerReady: NodeHmrRuntimeApi['emitServerReady'];
26
+ export { emitRuntimeServerReady as emitServerReady };
27
+ //# sourceMappingURL=runtime.node-hmr.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.node-hmr.d.ts","sourceRoot":"","sources":["../src/runtime.node-hmr.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AAG7D,YAAY,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAA;AAQhE;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,uBAAuB,EAAE,iBAAiB,CAAC,yBAAyB,CAG9E,CAAA;AAEH;;;;;;GAMG;AACH,QAAA,MAAM,sBAAsB,EAAE,iBAAiB,CAAC,iBAAiB,CAAmB,CAAA;AACpF,OAAO,EAAE,sBAAsB,IAAI,eAAe,EAAE,CAAA"}
@@ -0,0 +1,33 @@
1
+ import { emitServerReady, getNodeHmrRuntime } from './lib/runtime.js';
2
+ import { nodeHmrRuntimeUnavailableError } from './lib/runtime-api.js';
3
+ const maybeNodeHmrRuntime = getNodeHmrRuntime();
4
+ if (maybeNodeHmrRuntime === undefined) {
5
+ throw new Error(nodeHmrRuntimeUnavailableError);
6
+ }
7
+ const nodeHmrRuntime = maybeNodeHmrRuntime;
8
+ /**
9
+ * Connects browser asset tooling in this child process to the browser HMR event stream and file
10
+ * watcher owned by its `node-hmr` parent process.
11
+ *
12
+ * Pass this function as the `hmr` factory for `createAssetServer()`. Each call creates an
13
+ * independent channel that must be closed when its owner shuts down. The returned promise rejects
14
+ * when browser HMR is disabled for the runner.
15
+ *
16
+ * The `remix/node-hmr/runtime` module itself can only be imported by a process supervised by
17
+ * `node-hmr`. Use a dynamic import guarded by `process.env.REMIX_NODE_HMR` when the same entry module
18
+ * also runs without HMR supervision.
19
+ *
20
+ * @returns A child-scoped channel for watching browser source files and publishing HMR events.
21
+ */
22
+ export const createBrowserHmrChannel = async function createBrowserHmrChannel() {
23
+ return await nodeHmrRuntime.createBrowserHmrChannel();
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
+ const emitRuntimeServerReady = emitServerReady;
33
+ export { emitRuntimeServerReady as emitServerReady };
@@ -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 {}
package/package.json CHANGED
@@ -1,14 +1,64 @@
1
1
  {
2
2
  "name": "@remix-run/node-hmr",
3
- "version": "0.0.0",
4
- "description": "Placeholder package for Remix CI/OIDC setup",
3
+ "version": "0.1.0",
4
+ "description": "Run Node.js applications with Hot Module Reloading",
5
+ "author": "Michael Jackson <mjijackson@gmail.com>",
5
6
  "license": "MIT",
6
7
  "repository": {
7
8
  "type": "git",
8
9
  "url": "git+https://github.com/remix-run/remix.git",
9
10
  "directory": "packages/node-hmr"
10
11
  },
11
- "publishConfig": {
12
- "access": "public"
12
+ "homepage": "https://github.com/remix-run/remix/tree/main/packages/node-hmr#readme",
13
+ "files": [
14
+ "LICENSE",
15
+ "README.md",
16
+ "dist",
17
+ "src",
18
+ "!src/**/*.test.ts"
19
+ ],
20
+ "type": "module",
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/index.d.ts",
24
+ "default": "./dist/index.js"
25
+ },
26
+ "./runtime": {
27
+ "node-hmr": {
28
+ "types": "./dist/runtime.node-hmr.d.ts",
29
+ "default": "./dist/runtime.node-hmr.js"
30
+ },
31
+ "types": "./dist/runtime.d.ts",
32
+ "default": "./dist/runtime.js"
33
+ },
34
+ "./types": {
35
+ "types": "./dist/types.d.ts"
36
+ },
37
+ "./package.json": "./package.json"
38
+ },
39
+ "dependencies": {
40
+ "chokidar": "^5.0.0",
41
+ "oxc-parser": "^0.121.0",
42
+ "source-map-js": "^1.2.1",
43
+ "@remix-run/terminal": "^0.1.1"
44
+ },
45
+ "devDependencies": {
46
+ "@types/node": "^24.6.0",
47
+ "typescript": "^7.0.2",
48
+ "@remix-run/assert": "^0.3.0",
49
+ "@remix-run/node-tsx": "^0.1.1",
50
+ "@remix-run/test": "^0.6.0"
51
+ },
52
+ "keywords": [
53
+ "remix",
54
+ "node",
55
+ "hmr",
56
+ "watch"
57
+ ],
58
+ "scripts": {
59
+ "build": "tsc -p tsconfig.build.json && node ../../scripts/copy-package-type-only-exports.ts .",
60
+ "clean": "git clean -fdX",
61
+ "test": "remix test",
62
+ "typecheck": "tsc --noEmit"
13
63
  }
14
- }
64
+ }