browser-metro 1.0.7 → 1.0.8

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.
@@ -3,6 +3,29 @@ import { typescriptTransformer } from "./typescript.js";
3
3
  function isJsxFile(filename) {
4
4
  return filename.endsWith(".tsx") || filename.endsWith(".jsx");
5
5
  }
6
+ /**
7
+ * Extract a hook signature string from source.
8
+ *
9
+ * Returns the full ordered sequence of hook calls (with duplicates), joined by
10
+ * newline. This is passed to $RefreshSig$ so React Refresh can detect ANY change
11
+ * that affects the hook fiber chain — including adding a second call to an
12
+ * already-present hook (e.g. a second useMutation).
13
+ *
14
+ * Why NOT deduplicate: if we used a Set, adding a second `useMutation` would
15
+ * leave the signature unchanged → React Refresh would attempt an in-place update
16
+ * → "Rendered more hooks than during the previous render" crash. The full
17
+ * ordered sequence changes whenever hook count or order changes, so React
18
+ * Refresh always forces a clean remount instead.
19
+ */
20
+ function extractHookSignature(src) {
21
+ const hooks = [];
22
+ const re = /\buse[A-Z][a-zA-Z0-9]*/g;
23
+ let m;
24
+ while ((m = re.exec(src)) !== null) {
25
+ hooks.push(m[0]);
26
+ }
27
+ return hooks.join('\n');
28
+ }
6
29
  /**
7
30
  * Detect React component names from source code.
8
31
  * Heuristic: any function or const/let with an uppercase first letter.
@@ -46,8 +69,12 @@ export function createReactRefreshTransformer(base) {
46
69
  if (components.length === 0) {
47
70
  return result;
48
71
  }
49
- // Preamble: set up refresh hooks scoped to this module
50
- const preamble = 'var _prevRefreshReg = window.$RefreshReg$;\n' +
72
+ // Compute hook signature for this module changes when hooks are added/removed
73
+ const hookSig = extractHookSignature(params.src + result.code);
74
+ // Check if module uses createContext (needs HMR identity preservation)
75
+ const usesCreateContext = params.src.includes('createContext') || result.code.includes('createContext');
76
+ // Preamble: set up refresh hooks scoped to this module + signature vars per component
77
+ let preamble = 'var _prevRefreshReg = window.$RefreshReg$;\n' +
51
78
  'var _prevRefreshSig = window.$RefreshSig$;\n' +
52
79
  'var _refreshModuleId = ' + JSON.stringify(params.filename) + ';\n' +
53
80
  'window.$RefreshReg$ = function(type, id) {\n' +
@@ -61,14 +88,49 @@ export function createReactRefreshTransformer(base) {
61
88
  ' }\n' +
62
89
  ' return function(type) { return type; };\n' +
63
90
  '};\n';
91
+ // One signature function per component — enables hooks-change detection
92
+ for (const name of components) {
93
+ preamble += 'var _s_' + name + ' = $RefreshSig$();\n';
94
+ }
95
+ // Context identity preservation: patch React.createContext so that on HMR
96
+ // re-executions the same context object is returned instead of a new one.
97
+ // Without this, Provider uses a new context reference while consumers still
98
+ // hold the old one → useContext() returns null → "must be used within Provider".
99
+ // Contexts are keyed by moduleId + call-order index and stored in
100
+ // window.__HMR_CONTEXTS__ which persists across re-executions.
101
+ if (usesCreateContext) {
102
+ preamble +=
103
+ 'var _hmrCtxIdx = 0;\n' +
104
+ 'var _hmrOrigCC;\n' +
105
+ 'try {\n' +
106
+ ' var _hmrReact = require("react");\n' +
107
+ ' _hmrOrigCC = _hmrReact.createContext;\n' +
108
+ ' if (!window.__HMR_CONTEXTS__) window.__HMR_CONTEXTS__ = {};\n' +
109
+ ' _hmrReact.createContext = function(defaultValue) {\n' +
110
+ ' var key = _refreshModuleId + ":ctx:" + (_hmrCtxIdx++);\n' +
111
+ ' if (window.__HMR_CONTEXTS__[key]) return window.__HMR_CONTEXTS__[key];\n' +
112
+ ' var ctx = _hmrOrigCC(defaultValue);\n' +
113
+ ' window.__HMR_CONTEXTS__[key] = ctx;\n' +
114
+ ' return ctx;\n' +
115
+ ' };\n' +
116
+ '} catch(_e) {}\n';
117
+ }
64
118
  // Postamble: register each component and accept HMR
65
119
  let postamble = '\n';
66
120
  for (const name of components) {
67
121
  postamble +=
68
122
  'if (typeof ' + name + ' === "function") {\n' +
123
+ ' _s_' + name + '(' + name + ', ' + JSON.stringify(hookSig) + ');\n' +
69
124
  ' $RefreshReg$(' + name + ', ' + JSON.stringify(name) + ');\n' +
70
125
  '}\n';
71
126
  }
127
+ // Restore original React.createContext after module body runs
128
+ if (usesCreateContext) {
129
+ postamble +=
130
+ 'if (_hmrOrigCC) {\n' +
131
+ ' try { require("react").createContext = _hmrOrigCC; } catch(_e) {}\n' +
132
+ '}\n';
133
+ }
72
134
  postamble +=
73
135
  'window.$RefreshReg$ = _prevRefreshReg;\n' +
74
136
  'window.$RefreshSig$ = _prevRefreshSig;\n' +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "browser-metro",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "description": "A browser-based JavaScript/TypeScript bundler with HMR support, inspired by Metro. Runs entirely client-side.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -31,7 +31,7 @@
31
31
  },
32
32
  "license": "MIT",
33
33
  "dependencies": {
34
- "sucrase": "^3.35.0"
34
+ "sucrase": "^3.35.1"
35
35
  },
36
36
  "devDependencies": {
37
37
  "typescript": "^5.7.0"