import { describe, expect, it } from "vitest"; import type { JSXElement } from "@/transform/jsx/implementation"; import { createInitialState } from "@/transform/state/implementation"; import type { NestedTransformers } from "@/transform/tree/implementation"; import { transformJSXNode } from "./implementation"; const nested: NestedTransformers = { transformJSXNode, transformJSXForSSRNode: () => ({ type: "CallExpression", callee: { type: "Identifier", name: "renderToString" }, arguments: [], optional: false, }), }; function makeElement( attributes: JSXElement["openingElement"]["attributes"] = [], ): JSXElement { return { type: "JSXElement", openingElement: { type: "JSXOpeningElement", name: { type: "JSXIdentifier", name: "div" }, attributes, selfClosing: false, }, children: [{ type: "JSXText", value: "Hello" }], closingElement: null, }; } describe("transform/mode-csr", () => { it("creates template and fromTree import", () => { const state = createInitialState("csr"); const output = transformJSXNode(makeElement(), state, nested); expect(output.type).toBe("CallExpression"); expect(state.templates).toHaveLength(1); expect(state.runtimeImports.has("fromTree")).toBe(true); }); it("passes a compiled template descriptor to fromTree", () => { const state = createInitialState("csr"); transformJSXNode(makeElement(), state, nested); const template = state.templates[0] as unknown as { declarations?: Array<{ init?: { arguments?: Array<{ type: string; properties?: Array<{ key?: { name?: string }; value?: { value?: unknown }; }>; }>; }; }>; }; const descriptor = template.declarations?.[0]?.init?.arguments?.[0] as { type: string; properties?: Array<{ key?: { name?: string }; value?: { value?: unknown }; }>; }; expect(descriptor.type).toBe("ObjectExpression"); expect( descriptor.properties?.some( (property) => property.key?.name === "kind" && property.value?.value === "compiled", ), ).toBe(true); expect( descriptor.properties?.some( (property) => property.key?.name === "markup" && property.value?.value === "
Hello
", ), ).toBe(true); }); it("adds templateEffect for reactive text", () => { const state = createInitialState("csr"); const node = { ...makeElement(), children: [ { type: "JSXExpressionContainer", expression: { type: "MemberExpression", object: { type: "Identifier", name: "count" }, property: { type: "Identifier", name: "value" }, computed: false, optional: false, }, }, ], } as JSXElement; transformJSXNode(node, state, nested); expect(state.runtimeImports.has("templateEffect")).toBe(true); expect(state.runtimeImports.has("setText")).toBe(true); }); it("adds event import for event handler attribute", () => { const state = createInitialState("csr"); const node = makeElement([ { type: "JSXAttribute", name: { type: "JSXIdentifier", name: "onClick" }, value: { type: "JSXExpressionContainer", expression: { type: "Identifier", name: "handler" }, }, }, ]); transformJSXNode(node, state, nested); expect(state.runtimeImports.has("event")).toBe(true); }); it("initializes static expression attributes with setAttr without templateEffect", () => { const state = createInitialState("csr"); const node = makeElement([ { type: "JSXAttribute", name: { type: "JSXIdentifier", name: "data-render-mode" }, value: { type: "JSXExpressionContainer", expression: { type: "Identifier", name: "modeLabel" }, }, }, ]); const output = transformJSXNode(node, state, nested) as unknown as { callee: { body: { body: Array<{ type: string; expression?: { callee?: { name?: string } }; }>; }; }; }; const statements = output.callee.body.body; expect(state.runtimeImports.has("setAttr")).toBe(true); expect(state.runtimeImports.has("templateEffect")).toBe(false); expect( statements.some( (stmt) => stmt.type === "ExpressionStatement" && stmt.expression?.callee?.name === "setAttr", ), ).toBe(true); }); it("wraps reactive attributes in templateEffect", () => { const state = createInitialState("csr"); const node = makeElement([ { type: "JSXAttribute", name: { type: "JSXIdentifier", name: "title" }, value: { type: "JSXExpressionContainer", expression: { type: "MemberExpression", object: { type: "Identifier", name: "count" }, property: { type: "Identifier", name: "value" }, computed: false, optional: false, }, }, }, ]); const output = JSON.stringify(transformJSXNode(node, state, nested)); expect(state.runtimeImports.has("templateEffect")).toBe(true); expect(state.runtimeImports.has("setAttr")).toBe(true); expect(output).toContain('"name":"setAttr"'); }); it("does not wrap component inserts in templateEffect", () => { const state = createInitialState("csr"); const node = { ...makeElement(), children: [ { type: "JSXElement", openingElement: { type: "JSXOpeningElement", name: { type: "JSXIdentifier", name: "Counter" }, attributes: [], selfClosing: true, }, children: [], closingElement: null, }, ], } as JSXElement; const output = JSON.stringify(transformJSXNode(node, state, nested)); expect(state.runtimeImports.has("templateEffect")).toBe(false); expect(output).not.toContain('"name":"templateEffect"'); }); it("wraps dynamic inserts in templateEffect", () => { const state = createInitialState("csr"); const node = { ...makeElement(), children: [ { type: "JSXExpressionContainer", expression: { type: "LogicalExpression", operator: "&&", left: { type: "Literal", value: true, raw: "true" }, right: { type: "JSXElement", openingElement: { type: "JSXOpeningElement", name: { type: "JSXIdentifier", name: "span" }, attributes: [], selfClosing: false, }, children: [{ type: "JSXText", value: "A" }], closingElement: null, }, }, }, ], } as JSXElement; const output = JSON.stringify(transformJSXNode(node, state, nested)); expect(state.runtimeImports.has("insert")).toBe(true); expect(state.runtimeImports.has("templateEffect")).toBe(true); expect(output).toContain('"name":"templateEffect"'); expect(output).toContain('"name":"insert"'); }); it("wraps spread attributes in templateEffect", () => { const state = createInitialState("csr"); const node = makeElement([ { type: "JSXSpreadAttribute", argument: { type: "Identifier", name: "props" }, } as never, ]); const output = JSON.stringify(transformJSXNode(node, state, nested)); expect(state.runtimeImports.has("spread")).toBe(true); expect(state.runtimeImports.has("templateEffect")).toBe(true); expect(output).toContain('"name":"spread"'); }); it("declares insert anchors before templateEffect expressions", () => { const state = createInitialState("csr"); const node = { ...makeElement(), children: [ { type: "JSXExpressionContainer", expression: { type: "LogicalExpression", operator: "&&", left: { type: "Literal", value: true, raw: "true" }, right: { type: "JSXElement", openingElement: { type: "JSXOpeningElement", name: { type: "JSXIdentifier", name: "span" }, attributes: [], selfClosing: false, }, children: [{ type: "JSXText", value: "A" }], closingElement: null, }, }, }, { type: "JSXExpressionContainer", expression: { type: "ObjectExpression", properties: [ { type: "Property", key: { type: "Identifier", name: "node" }, value: { type: "JSXElement", openingElement: { type: "JSXOpeningElement", name: { type: "JSXIdentifier", name: "em" }, attributes: [], selfClosing: false, }, children: [{ type: "JSXText", value: "B" }], closingElement: null, }, kind: "init", computed: false, method: false, shorthand: false, }, ], }, }, ], } as JSXElement; const output = transformJSXNode(node, state, nested) as unknown as { callee: { body: { body: Array<{ type: string; declarations?: Array<{ id: { name: string } }>; }>; }; }; }; const statements = output.callee.body.body; const firstTemplateEffectIndex = statements.findIndex( (stmt) => stmt.type === "ExpressionStatement" && (stmt as unknown as { expression?: { callee?: { name?: string } } }) .expression?.callee?.name === "templateEffect", ); const insertNodeDeclarationIndexes = statements .map((stmt, index) => ({ index, name: stmt.type === "VariableDeclaration" ? (stmt.declarations?.[0]?.id.name ?? "") : "", })) .filter(({ name }) => name.startsWith("_n0_")); expect(firstTemplateEffectIndex).toBeGreaterThan(-1); expect(insertNodeDeclarationIndexes.length).toBeGreaterThanOrEqual(2); expect( insertNodeDeclarationIndexes.every( ({ index }) => index < firstTemplateEffectIndex, ), ).toBe(true); }); });