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
|
-
//
|
|
50
|
-
const
|
|
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.
|
|
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.
|
|
34
|
+
"sucrase": "^3.35.1"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"typescript": "^5.7.0"
|