react-x11 2.48.0 → 2.50.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.
@@ -31,11 +31,22 @@ import { markHotReloadSession } from '../registry.js';
31
31
  const RUNTIME_URL = new URL('./index.js', import.meta.url).href;
32
32
  const SRC_DIR = fileURLToPath(new URL('../', import.meta.url));
33
33
 
34
- const TOOLCHAIN = [
35
- '@babel/core',
36
- '@babel/plugin-transform-react-jsx',
37
- 'react-refresh',
38
- 'hot-module-replacement',
34
+ /**
35
+ * The hot-reload toolchain: each specifier this loader imports, and the
36
+ * package it comes from. Optional peer dependencies, so `react-x11 dev`
37
+ * checks the same list before it starts (src/cli/dev.js).
38
+ */
39
+ export const TOOLCHAIN = [
40
+ { specifier: '@babel/core', name: '@babel/core' },
41
+ {
42
+ specifier: '@babel/plugin-transform-react-jsx',
43
+ name: '@babel/plugin-transform-react-jsx',
44
+ },
45
+ { specifier: 'react-refresh/babel', name: 'react-refresh' },
46
+ {
47
+ specifier: 'hot-module-replacement/register',
48
+ name: 'hot-module-replacement',
49
+ },
39
50
  ];
40
51
 
41
52
  async function loadToolchain() {
@@ -54,13 +65,22 @@ async function loadToolchain() {
54
65
  '@babel/plugin-transform-react-jsx',
55
66
  );
56
67
  const refreshTransform = await load('react-refresh/babel', 'react-refresh');
68
+ // Resolved rather than imported: importing it registers its hooks, which
69
+ // has to wait until ours are in. Checked here all the same, so a missing
70
+ // one is this message and not a bare ERR_MODULE_NOT_FOUND after the
71
+ // babel hooks already went in.
72
+ try {
73
+ import.meta.resolve('hot-module-replacement/register');
74
+ } catch {
75
+ missing.push('hot-module-replacement');
76
+ }
57
77
  if (missing.length > 0) {
58
78
  throw new Error(
59
79
  `react-x11/refresh: the hot-reload toolchain is not installed ` +
60
80
  `(missing: ${missing.join(', ')}). It is a set of optional peer ` +
61
81
  `dependencies — an app that never hot-reloads does not carry them. ` +
62
82
  `Install them where the dev loop runs:\n\n` +
63
- ` npm install --save-dev ${TOOLCHAIN.join(' ')}\n`,
83
+ ` npm install --save-dev ${TOOLCHAIN.map((t) => t.name).join(' ')}\n`,
64
84
  );
65
85
  }
66
86
  return {
@@ -285,7 +305,8 @@ function buildTransformer({ babel, jsxTransform, refreshTransform }, options) {
285
305
  `const $RefreshSig$ = __ReactX11Refresh.createSignatureFunctionForTransform`,
286
306
  ...prelude,
287
307
  ];
288
- // Stacks through hot modules are off by these lines; dev-only.
308
+ // One statement a line, above the module's own first line: the source map
309
+ // below starts that many lines down to match.
289
310
  const preludeSource = preludeLines.join(';\n') + ';';
290
311
 
291
312
  let lastModuleInfo = null;
@@ -300,7 +321,7 @@ function buildTransformer({ babel, jsxTransform, refreshTransform }, options) {
300
321
  },
301
322
  transform(source, filename) {
302
323
  lastModuleInfo = null;
303
- const { code } = babel.transformSync(source, {
324
+ const { code, map } = babel.transformSync(source, {
304
325
  filename,
305
326
  babelrc: false,
306
327
  configFile: false,
@@ -311,7 +332,7 @@ function buildTransformer({ babel, jsxTransform, refreshTransform }, options) {
311
332
  guardPlugin,
312
333
  ],
313
334
  retainLines: true,
314
- sourceMaps: 'inline',
335
+ sourceMaps: true,
315
336
  });
316
337
  const { opaque, names } = lastModuleInfo;
317
338
  const exportsArg =
@@ -319,13 +340,55 @@ function buildTransformer({ babel, jsxTransform, refreshTransform }, options) {
319
340
  ? 'null'
320
341
  : `{ ${names.map((n) => `${JSON.stringify(n)}: ${n}`).join(', ')} }`;
321
342
  const footer = `\n;import.meta.hot && __ReactX11Refresh.moduleReady(import.meta.hot, import.meta.url, ${exportsArg});`;
322
- return { code: `${preludeSource}\n${code}${footer}` };
343
+ return {
344
+ code:
345
+ `${preludeSource}\n${code}${footer}\n` +
346
+ inlineSourceMap(map, preludeLines.length),
347
+ };
323
348
  },
324
349
  };
325
350
  }
326
351
 
352
+ const transformListeners = new Set();
353
+
354
+ /**
355
+ * Called for every hot module this loader compiles, with `{ filename, error }`
356
+ * — `error` null when it compiled. The seam a host needs for a save that does
357
+ * not compile: hot-module-replacement catches the error of a reload and only
358
+ * prints it, and the app keeps running the module's last version, so nothing
359
+ * else tells the host. `react-x11 dev` shows it in a window. Returns an
360
+ * unsubscribe.
361
+ */
362
+ export function onTransform(listener) {
363
+ transformListeners.add(listener);
364
+ return () => transformListeners.delete(listener);
365
+ }
366
+
367
+ function reportTransform(event) {
368
+ for (const listener of [...transformListeners]) {
369
+ try {
370
+ listener(event);
371
+ } catch (err) {
372
+ console.error('react-x11/refresh: an onTransform listener threw', err);
373
+ }
374
+ }
375
+ }
376
+
327
377
  let registered = false;
328
378
 
379
+ /**
380
+ * Babel's map for a module, as the comment that carries it, moved `lines`
381
+ * down: babel describes its own output, and the prelude goes in above that.
382
+ * An empty line in a map's mappings is a `;`, so the shift is that many of
383
+ * them. hot-module-replacement, the layer above, composes this map into its
384
+ * own — which is what a stack trace through a hot module is read through.
385
+ */
386
+ export function inlineSourceMap(map, lines) {
387
+ const shifted = { ...map, mappings: ';'.repeat(lines) + map.mappings };
388
+ const payload = Buffer.from(JSON.stringify(shifted)).toString('base64');
389
+ return `//# sourceMappingURL=data:application/json;charset=utf-8;base64,${payload}\n`;
390
+ }
391
+
329
392
  /**
330
393
  * Which files stay out of the hot graph: every package in node_modules,
331
394
  * react-x11's own src/, and whatever the app's `ignore` names.
@@ -406,10 +469,17 @@ export async function registerRefresh(options = {}) {
406
469
  if (hotIgnore(filename)) {
407
470
  return nextLoad(url, context);
408
471
  }
409
- const { code } = transformer.transform(
410
- readFileSync(filename, 'utf8'),
411
- filename,
412
- );
472
+ let code;
473
+ try {
474
+ ({ code } = transformer.transform(
475
+ readFileSync(filename, 'utf8'),
476
+ filename,
477
+ ));
478
+ } catch (error) {
479
+ reportTransform({ filename, error });
480
+ throw error;
481
+ }
482
+ reportTransform({ filename, error: null });
413
483
  return { format: 'module', source: code, shortCircuit: true };
414
484
  },
415
485
  });
@@ -1,11 +1,14 @@
1
1
  // react-x11/refresh/register — hot reload with the defaults, for --import:
2
2
  //
3
- // node --enable-source-maps --import react-x11/refresh/register app.jsx
3
+ // node --enable-source-maps --import react-x11/refresh/register main.jsx
4
4
  //
5
- // The app's entry needs no changes: the loader injects the runtime wiring
6
- // into every hot module, edits to components apply in place with hook
7
- // state intact, and `onReload` (from react-x11/refresh) is there when a
8
- // tool wants to observe the reloads. A tool that needs the seams —
5
+ // The app writes no wiring of its own: the loader injects the runtime into
6
+ // every hot module, edits to components apply in place with hook state
7
+ // intact, and `onReload` (from react-x11/refresh) is there when a tool wants
8
+ // to observe the reloads. What the app does owe is a split: main.jsx is
9
+ // never watched — hot-module-replacement watches a module when another
10
+ // imports it — so the components it mounts live in modules it imports, and
11
+ // an app in one file never reloads. A tool that needs the seams —
9
12
  // extensions, ignore, an injected prelude — writes its own two-line
10
13
  // --import module calling registerRefresh(options) instead.
11
14
  import { registerRefresh } from './loader.js';