Skip to content

Commit 6b013bb

Browse files
authored
Merge pull request #611 from preactjs/account-for-different-transform
Account for different behaviour of refresh transform in Oxc vs Babel
2 parents 985af13 + ba9db17 commit 6b013bb

3 files changed

Lines changed: 183 additions & 1 deletion

File tree

.changeset/four-buckets-beam.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@prefresh/vite': patch
3+
---
4+
5+
Add workaround for Oxc transforming differently than babel
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import assert from 'node:assert/strict';
2+
import test from 'node:test';
3+
import { rolldown } from 'rolldown';
4+
5+
import prefreshPlugin from '../src/index.js';
6+
7+
const MEMO_FIXTURE = `
8+
import { memo } from "preact/compat";
9+
import { useMemo } from "preact/hooks";
10+
11+
const Button = memo(({ name, className, onClick }) => {
12+
const classes = useMemo(() => {
13+
const classList = ["btn"];
14+
if (className) classList.push(className);
15+
return classList.join(" ");
16+
}, [className]);
17+
18+
return <button name={name} className={classes} onClick={onClick}>Click</button>;
19+
});
20+
21+
export default Button;
22+
`;
23+
24+
test('issue #610: Oxc emits one $RefreshSig$ factory for memo-wrapped component', async () => {
25+
const output = await transform(MEMO_FIXTURE);
26+
27+
assert.match(output, /\$RefreshSig\$/, 'should contain $RefreshSig$');
28+
assert.match(output, /\$RefreshReg\$/, 'should contain $RefreshReg$');
29+
30+
const sigFactoryCalls = output.match(/\$RefreshSig\$\(\)/g);
31+
assert.equal(sigFactoryCalls.length, 1, 'exactly one factory');
32+
33+
assert.match(
34+
output,
35+
/_s\(memo\(_c\s*=\s*_s\(/,
36+
'same _s wraps both inner function and outer memo()'
37+
);
38+
});
39+
40+
test('issue #610: $RefreshSig$ wrapper must not crash for memo-wrapped components', async () => {
41+
const signaturesForType = new Map();
42+
43+
function sign(type, key, forceReset, getCustomHooks, status) {
44+
if (type) {
45+
let signature = signaturesForType.get(type);
46+
if (status === 'begin') {
47+
signaturesForType.set(type, {
48+
type,
49+
key,
50+
forceReset,
51+
getCustomHooks: getCustomHooks || (() => []),
52+
});
53+
return 'needsHooks';
54+
} else if (status === 'needsHooks') {
55+
signature.fullKey = signature.key;
56+
}
57+
}
58+
}
59+
60+
// Fixed wrapper: calls with a key always use 'begin'
61+
function $RefreshSig$() {
62+
let status = 'begin';
63+
let savedType;
64+
return (type, key, forceReset, getCustomHooks) => {
65+
if (!savedType) savedType = type;
66+
status = sign(
67+
type || savedType,
68+
key,
69+
forceReset,
70+
getCustomHooks,
71+
key ? 'begin' : status
72+
);
73+
return type;
74+
};
75+
}
76+
77+
const _s = $RefreshSig$();
78+
const innerFn = function Button() {};
79+
const memo = (fn) => ({ type: fn, $$typeof: Symbol.for('preact.memo') });
80+
const key = 'WQ9WH5eCVGcEPdUJDepp+VlX1/c=';
81+
82+
// _s(innerFn, key) — registers the inner function
83+
assert.equal(_s(innerFn, key), innerFn);
84+
assert.ok(signaturesForType.has(innerFn), 'innerFn registered');
85+
86+
// _s(memoResult, key) — must register the memo wrapper without crashing
87+
const memoResult = memo(innerFn);
88+
assert.doesNotThrow(() => _s(memoResult, key));
89+
assert.ok(signaturesForType.has(memoResult), 'memo wrapper registered');
90+
});
91+
92+
test('issue #610: Babel path (single _s call) still works with the fix', () => {
93+
const signaturesForType = new Map();
94+
95+
function sign(type, key, forceReset, getCustomHooks, status) {
96+
if (type) {
97+
let signature = signaturesForType.get(type);
98+
if (status === 'begin') {
99+
signaturesForType.set(type, {
100+
type,
101+
key,
102+
forceReset,
103+
getCustomHooks: getCustomHooks || (() => []),
104+
});
105+
return 'needsHooks';
106+
} else if (status === 'needsHooks') {
107+
signature.fullKey = signature.key;
108+
}
109+
}
110+
}
111+
112+
function $RefreshSig$() {
113+
let status = 'begin';
114+
let savedType;
115+
return (type, key, forceReset, getCustomHooks) => {
116+
if (!savedType) savedType = type;
117+
status = sign(
118+
type || savedType,
119+
key,
120+
forceReset,
121+
getCustomHooks,
122+
key ? 'begin' : status
123+
);
124+
return type;
125+
};
126+
}
127+
128+
// Babel pattern: memo(_c = _s(innerFn, key)) — only innerFn gets _s
129+
// Then _s() in the component body during render
130+
const _s = $RefreshSig$();
131+
const innerFn = function Button() {};
132+
const key = 'WQ9WH5eCVGcEPdUJDepp+VlX1/c=';
133+
134+
assert.equal(_s(innerFn, key), innerFn);
135+
assert.ok(signaturesForType.has(innerFn), 'innerFn registered');
136+
137+
// Body call _s() — no key, triggers needsHooks
138+
assert.doesNotThrow(() => _s());
139+
const sig = signaturesForType.get(innerFn);
140+
assert.equal(sig.fullKey, key, 'fullKey computed via needsHooks');
141+
});
142+
143+
async function transform(code) {
144+
const bundle = await rolldown({
145+
input: 'virtual:entry.jsx',
146+
plugins: [
147+
{
148+
name: 'virtual',
149+
resolveId(id) {
150+
if (id === 'virtual:entry.jsx') return id;
151+
return { id, external: true };
152+
},
153+
load(id) {
154+
if (id === 'virtual:entry.jsx') return code;
155+
},
156+
},
157+
prefreshPlugin({ enabled: true }),
158+
],
159+
transform: {
160+
jsx: {
161+
runtime: 'automatic',
162+
importSource: 'preact',
163+
refresh: true,
164+
},
165+
},
166+
});
167+
168+
const { output } = await bundle.generate({ format: 'esm' });
169+
return stripRolldownRuntime(output[0].code);
170+
}
171+
172+
function stripRolldownRuntime(code) {
173+
return code.replace(
174+
/\/\/#region \\0rolldown\/runtime\.js[\s\S]*?\/\/#endregion\n*/g,
175+
''
176+
);
177+
}

packages/vite/src/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ function prefreshWrapperPlugin(options = {}) {
293293
let savedType;
294294
return (type, key, forceReset, getCustomHooks) => {
295295
if (!savedType) savedType = type;
296-
status = self.__PREFRESH__.sign(type || savedType, key, forceReset, getCustomHooks, status);
296+
status = self.__PREFRESH__.sign(type || savedType, key, forceReset, getCustomHooks, key ? 'begin' : status);
297297
return type;
298298
};
299299
};

0 commit comments

Comments
 (0)