Fix React HMR duplicate identifier error for named default exports (#20812)

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Jarred Sumner
2025-07-04 03:06:00 -07:00
committed by GitHub
parent 068997b529
commit eabbd5cbfb
3 changed files with 358 additions and 6 deletions

View File

@@ -19382,22 +19382,36 @@ fn NewParser_(
// > export default default_export;
// > $RefreshReg(default_export, "App.tsx:default")
const ref = if (data.value == .expr) emit_temp_var: {
const temp_id = p.generateTempRef("default_export");
try p.current_scope.generated.push(p.allocator, temp_id);
const ref_to_use = brk: {
if (func.func.name) |*loc_ref| {
// Input:
//
// export default function Foo() {}
//
// Output:
//
// const Foo = _s(function Foo() {})
// export default Foo;
if (loc_ref.ref) |ref| break :brk ref;
}
const temp_id = p.generateTempRef("default_export");
try p.current_scope.generated.push(p.allocator, temp_id);
break :brk temp_id;
};
stmts.append(Stmt.alloc(S.Local, .{
.kind = .k_const,
.decls = try G.Decl.List.fromSlice(p.allocator, &.{
.{
.binding = Binding.alloc(p.allocator, B.Identifier{ .ref = temp_id }, stmt.loc),
.binding = Binding.alloc(p.allocator, B.Identifier{ .ref = ref_to_use }, stmt.loc),
.value = data.value.expr,
},
}),
}, stmt.loc)) catch bun.outOfMemory();
data.value = .{ .expr = .initIdentifier(temp_id, stmt.loc) };
data.value = .{ .expr = .initIdentifier(ref_to_use, stmt.loc) };
break :emit_temp_var temp_id;
break :emit_temp_var ref_to_use;
} else data.default_name.ref.?;
if (p.options.features.server_components.wrapsExports()) {

View File

@@ -129,6 +129,10 @@ export interface DevServerTest {
mainDir?: string;
skip?: ("win32" | "darwin" | "linux" | "ci")[];
/**
* Only run this test.
*/
only?: boolean;
}
let interactive = false;
@@ -1813,7 +1817,8 @@ function testImpl<T extends DevServerTest>(
jest.test.todo(name, run);
return options;
}
jest.test(
(options.only ? jest.test.only : jest.test)(
name,
run,
isStressTest
@@ -1929,6 +1934,17 @@ export function devTest<T extends DevServerTest>(description: string, options: T
return testImpl(description, options, "development", caller);
}
devTest.only = function (description: string, options: DevServerTest) {
// Capture the caller name as part of the test tempdir
const callerLocation = snapshotCallerLocation();
const caller = stackTraceFileName(callerLocation);
assert(
caller.startsWith(devTestRoot) || caller.includes("dev-and-prod"),
"dev server tests must be in test/bake/dev, not " + caller,
);
return testImpl(description, { ...options, only: true }, "development", caller);
};
export function prodTest<T extends DevServerTest>(description: string, options: T): T {
const callerLocation = snapshotCallerLocation();
const caller = stackTraceFileName(callerLocation);

View File

@@ -421,8 +421,330 @@ devTest("custom hook tracking", {
}
`,
},
async test(dev) {
await using c = await dev.client("/", {});
await c.expectMessage("PASS");
},
});
devTest("react component with hooks and mutual recursion renders without error", {
files: {
...reactAndRefreshStub,
"index.tsx": `
import ComponentWithConst, { helper } from './component-with-const';
import ComponentWithLet, { getCounter } from './component-with-let';
import ComponentWithVar, { getGlobalState } from './component-with-var';
import MathComponent, { utilityFunction } from './component-with-function';
import ProcessorComponent, { DataProcessor } from './component-with-class';
function useThis() {
return null;
}
function useFakeState(initial) {
return [initial, () => {}];
}
function useFakeEffect(fn) {
fn();
}
export default function AA({ depth = 0 }: { depth: number }) {
const [count, setCount] = useFakeState(0);
useThis();
useFakeEffect(() => {});
return depth === 0 && <B />
}
function B() {
const [value, setValue] = useFakeState(42);
useFakeEffect(() => {});
return <AA depth={1} />
}
// Call B outside the function body to test statement -> expression transform
B();
// Call all imported default functions outside their bodies
ComponentWithConst();
ComponentWithLet();
ComponentWithVar();
MathComponent({ input: 10 });
ProcessorComponent({ text: "test" });
// Use all the imported components and their non-default exports
console.log("ComponentWithConst:", ComponentWithConst());
console.log("helper:", helper());
console.log("ComponentWithLet:", ComponentWithLet());
console.log("getCounter:", getCounter());
console.log("ComponentWithVar:", ComponentWithVar());
console.log("getGlobalState:", getGlobalState());
console.log("MathComponent:", MathComponent({ input: 10 }));
console.log("utilityFunction:", utilityFunction(15));
console.log("ProcessorComponent:", ProcessorComponent({ text: "test" }));
const processor = new DataProcessor();
console.log("DataProcessor:", processor.process("world"));
console.log("PASS");
`,
"component-with-const.tsx": `
const helperValue = "helper-result";
function useFakeState(initial) {
return [initial, () => {}];
}
function useFakeCallback(fn) {
return fn;
}
export default function Component() {
const [state, setState] = useFakeState(helperValue);
const [count, setCount] = useFakeState(0);
const callback = useFakeCallback(() => {});
return helperValue;
}
export const helper = () => helperValue;
// Call Component outside its body to test statement -> expression transform
Component();
const result1 = Component();
helper();
`,
"component-with-let.tsx": `
let counter = 0;
function useFakeState(initial) {
return [initial, () => {}];
}
function useFakeEffect(fn, deps) {
fn();
}
function useFakeMemo(fn, deps) {
return fn();
}
export default function Counter() {
const [localCount, setLocalCount] = useFakeState(0);
const [multiplier, setMultiplier] = useFakeState(1);
useFakeEffect(() => {
setLocalCount(counter * multiplier);
}, [multiplier]);
const memoized = useFakeMemo(() => counter * 2, [counter]);
return ++counter;
}
export const getCounter = () => counter;
// Call Counter outside its body multiple times
Counter();
Counter();
const currentCount = Counter();
getCounter();
// Test with different call patterns
[1, 2, 3].forEach(() => Counter());
const counters = [Counter, Counter, Counter].map(fn => fn());
`,
"component-with-var.tsx": `
var globalState = { value: 42 };
function useFakeState(initial) {
return [initial, () => {}];
}
function useFakeMemo(fn, deps) {
return fn();
}
function useFakeRef(initial) {
return { current: initial };
}
export default function StateComponent() {
const [localState, setLocalState] = useFakeState(globalState.value);
const [factor, setFactor] = useFakeState(2);
const computed = useFakeMemo(() => localState * factor, [localState, factor]);
const ref = useFakeRef(null);
return globalState.value;
}
export const getGlobalState = () => globalState;
// Call StateComponent outside its body
StateComponent();
const state1 = StateComponent();
const state2 = StateComponent();
getGlobalState();
// Test with object method calls
const obj = { fn: StateComponent };
obj.fn();
// Test with array of functions
const fns = [StateComponent, getGlobalState];
fns[0]();
fns[1]();
`,
"component-with-function.tsx": `
function multiply(x: number) {
return x * 2;
}
function useFakeState(initial) {
return [initial, () => {}];
}
function useFakeCallback(fn, deps) {
return fn;
}
function useFakeReducer(reducer, initial) {
return [initial, () => {}];
}
export default function MathComponent({ input }: { input: number }) {
const [result, setResult] = useFakeState(0);
const [operations, setOperations] = useFakeState(0);
const [state, dispatch] = useFakeReducer((s, a) => s, {});
const calculate = useFakeCallback(() => {
const value = multiply(input);
setResult(value);
setOperations(prev => prev + 1);
return value;
}, [input]);
return multiply(input);
}
export const utilityFunction = multiply;
// Call MathComponent outside its body with various patterns
MathComponent({ input: 5 });
MathComponent({ input: 10 });
const result1 = MathComponent({ input: 15 });
utilityFunction(20);
// Test with function composition
const compose = (fn: Function) => fn({ input: 25 });
compose(MathComponent);
// Test with conditional calls
const shouldCall = true;
if (shouldCall) {
MathComponent({ input: 30 });
}
// Test with ternary
const ternaryResult = true ? MathComponent({ input: 35 }) : null;
// Test with logical operators
true && MathComponent({ input: 40 });
false || MathComponent({ input: 45 });
`,
"component-with-class.tsx": `
class Processor {
process(data: string) {
return data.toUpperCase();
}
}
function useFakeState(initial) {
return [initial, () => {}];
}
function useFakeReducer(reducer, initial) {
return [initial, () => {}];
}
function useFakeRef(initial) {
return { current: initial };
}
function useFakeContext() {
return {};
}
const reducer = (state: any, action: any) => {
switch (action.type) {
case 'process':
return { ...state, processed: action.payload };
default:
return state;
}
};
export default function ProcessorComponent({ text }: { text: string }) {
const [state, setState] = useFakeState({ text, processed: '' });
const [history, dispatch] = useFakeReducer(reducer, { processed: [] });
const processorRef = useFakeRef(new Processor());
const context = useFakeContext();
const processor = new Processor();
const result = processor.process(text);
dispatch({ type: 'process', payload: result });
return processor.process(text);
}
export const DataProcessor = Processor;
// Call ProcessorComponent outside its body
ProcessorComponent({ text: "hello" });
ProcessorComponent({ text: "world" });
const processed1 = ProcessorComponent({ text: "test1" });
const processed2 = ProcessorComponent({ text: "test2" });
// Test with new DataProcessor
const proc1 = new DataProcessor();
const proc2 = new DataProcessor();
proc1.process("data1");
proc2.process("data2");
// Test with function binding
const boundProcessor = ProcessorComponent.bind(null);
boundProcessor({ text: "bound" });
// Test with apply/call
ProcessorComponent.call(null, { text: "called" });
ProcessorComponent.apply(null, [{ text: "applied" }]);
// Test with destructuring
const { process } = new DataProcessor();
// Test with spread operator
const args = [{ text: "spread" }];
ProcessorComponent(...args);
`,
"index.html": emptyHtmlFile({
scripts: ["index.tsx"],
body: `<div id="root"></div>`,
}),
},
async test(dev) {
await using c = await dev.client("/", {});
await c.expectMessage(
"ComponentWithConst:",
"helper:",
"ComponentWithLet:",
"getCounter:",
"ComponentWithVar:",
"getGlobalState:",
"MathComponent:",
"utilityFunction:",
"ProcessorComponent:",
"DataProcessor:",
"PASS",
);
},
});