diff --git a/src/js_parser.zig b/src/js_parser.zig index dad2a9eb5e..e55e9227f2 100644 --- a/src/js_parser.zig +++ b/src/js_parser.zig @@ -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()) { diff --git a/test/bake/bake-harness.ts b/test/bake/bake-harness.ts index 9d71c8c01a..b44f64d090 100644 --- a/test/bake/bake-harness.ts +++ b/test/bake/bake-harness.ts @@ -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( 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(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(description: string, options: T): T { const callerLocation = snapshotCallerLocation(); const caller = stackTraceFileName(callerLocation); diff --git a/test/bake/dev/react-spa.test.ts b/test/bake/dev/react-spa.test.ts index 8944361666..9e951c0165 100644 --- a/test/bake/dev/react-spa.test.ts +++ b/test/bake/dev/react-spa.test.ts @@ -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 && + } + + function B() { + const [value, setValue] = useFakeState(42); + useFakeEffect(() => {}); + return + } + + // 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: `
`, + }), + }, + async test(dev) { + await using c = await dev.client("/", {}); + await c.expectMessage( + "ComponentWithConst:", + "helper:", + "ComponentWithLet:", + "getCounter:", + "ComponentWithVar:", + "getGlobalState:", + "MathComponent:", + "utilityFunction:", + "ProcessorComponent:", + "DataProcessor:", + "PASS", + ); + }, +});