import type { AttributeIr, ComponentPropIr, ComponentIr, JsxNodeIr, ModuleIr } from "./ir.js"; import { emitSelectBindingLine, isSelectControlAttributeName, provenNativeCellTextBinding, specializedElementProperty, usesBranchInsertion, usesDedicatedSelectBinding, withClientSpecializations, } from "./emit-client-specialization.js"; import type { ClientSpecializationFlags, RuntimeImport } from "./types.js"; import { listReadsNestedItemObject } from "./ir-nested-object-read.js"; import { OXC_BIND_DOM_REF_PLACEHOLDER } from "./oxc-dom-lowering.js"; import { OXC_COMPUTED_REACTIVE_ALIAS_PLACEHOLDER, OXC_UNTRACK_REACTIVE_ALIAS_PLACEHOLDER, } from "./oxc-render-values.js"; import { getCompatInlineMemo, type CompatInlineMemo } from "./compat-inline-memo.js"; import { escapeHtmlAttribute as escapeHtml } from "@reckona/mreact-shared/html-escape"; import { htmlAttributeNameForElement, isStaticUrlValueUnsafe, isUrlAttribute, } from "./emit-server-shared.js"; export interface EmitResult { code: string; imports: RuntimeImport[]; } export function emitClient( ir: ModuleIr, options: { dev?: boolean; filename?: string; specializations?: Partial | undefined; } = {}, ): EmitResult { return withClientSpecializations(options.specializations, () => emitClientModule(ir, options)); } function emitClientModule( ir: ModuleIr, options: { dev?: boolean; filename?: string }, ): EmitResult { const imports = collectImports(ir); const helperNames = allocateRuntimeHelperNames( ir, imports.flatMap((entry) => entry.specifiers), ); const importLines = imports .filter((entry) => entry.specifiers.length > 0) .map((entry) => emitRuntimeImportLine(entry, helperNames)) .join("\n"); const userImports = emitUserImports(ir); const moduleStatements = emitModuleStatements(ir); const moduleAllocator = createNameAllocator([]); const clientBoundaryHelperName = hasClientReferenceNodes(ir) ? moduleAllocator("__mreactClientBoundary", ir.moduleBindingNames) : undefined; const clientBoundaryHelper = clientBoundaryHelperName === undefined ? "" : emitClientBoundaryHelper(clientBoundaryHelperName); const inlineMemoComponents = new Map( ir.components.flatMap((component) => { const inlineMemo = getCompatInlineMemo(component); return inlineMemo === undefined ? [] : ([[component.name, inlineMemo]] as const); }), ); // Every component this emitter lowers returns a DOM node or a fragment, so a // call site can skip the generic nullish render-value guard for it. const nonNullishComponents = new Set( ir.components .filter( (component) => component.root.kind !== "component" && component.async !== true && component.reassigned !== true && getCompatInlineMemo(component) === undefined, ) .map((component) => component.name), ); const components = ir.components .map((component) => emitComponent( component, moduleAllocator, helperNames, clientBoundaryHelperName, inlineMemoComponents, nonNullishComponents, options, ), ) .join("\n\n") .replaceAll(OXC_BIND_DOM_REF_PLACEHOLDER, helperNames.bindDomRef) .replaceAll(OXC_UNTRACK_REACTIVE_ALIAS_PLACEHOLDER, helperNames.untrack) .replaceAll(OXC_COMPUTED_REACTIVE_ALIAS_PLACEHOLDER, helperNames.deferredComputed); return { code: `${[importLines, userImports, moduleStatements, clientBoundaryHelper] .filter(Boolean) .join("\n")}\n\n${components}\n`, imports, }; } type RuntimeHelperName = | "bindList" | "bindListWithRenderArity" | "bindDomRef" | "bindEvent" | "bindProp" | "bindElementProperty" | "bindSelectValue" | "bindSpreadProps" | "bindCellText" | "bindText" | "createListWithRenderArity" | "createMemo" | "createSvgTemplate" | "createSvgTemplateElement" | "createTemplate" | "createTemplateElement" | "computed" | "deferredComputed" | "createCompilerListBindingCache" | "insertBranch" | "insertDynamic" | "insertRenderValue" | "insertMemo" | "insertMemoDynamic" | "installMemoRenderValueNormalizer" | "bindCompilerKeyedCellText" | "bindCompilerKeyedSingleNodeList" | "bindCompilerKeyedPropertyText" | "bindCompilerKeyedText" | "markCompilerKeyedEventSlot" | "trackCompilerKeyedItem" | "untrack"; type RuntimeHelperNames = Record; function allocateRuntimeHelperNames( ir: ModuleIr, specifiers: readonly string[], ): RuntimeHelperNames { const bindingNames = [ ...ir.moduleBindingNames, ...ir.components.flatMap((component) => [ component.name, component.exportName, ...component.bindingNames, ]), ]; const allocator = createNameAllocator(bindingNames); const occupiedNames = new Set(bindingNames); const helperNames: RuntimeHelperNames = { bindList: "bindList", bindListWithRenderArity: "bindListWithRenderArity", bindDomRef: "bindDomRef", bindEvent: "bindEvent", bindProp: "bindProp", bindElementProperty: "bindElementProperty", bindSelectValue: "bindSelectValue", bindSpreadProps: "bindSpreadProps", bindCellText: "bindCellText", bindText: "bindText", createListWithRenderArity: "createListWithRenderArity", createMemo: "createMemo", createSvgTemplate: "createSvgTemplate", createSvgTemplateElement: "createSvgTemplateElement", createTemplate: "createTemplate", createTemplateElement: "createTemplateElement", computed: "computed", deferredComputed: "deferredComputed", createCompilerListBindingCache: "createCompilerListBindingCache", insertBranch: "insertBranch", insertDynamic: "insertDynamic", insertRenderValue: "insertRenderValue", insertMemo: "insertMemo", insertMemoDynamic: "insertMemoDynamic", installMemoRenderValueNormalizer: "installMemoRenderValueNormalizer", bindCompilerKeyedCellText: "bindCompilerKeyedCellText", bindCompilerKeyedSingleNodeList: "bindCompilerKeyedSingleNodeList", bindCompilerKeyedPropertyText: "bindCompilerKeyedPropertyText", bindCompilerKeyedText: "bindCompilerKeyedText", markCompilerKeyedEventSlot: "markCompilerKeyedEventSlot", trackCompilerKeyedItem: "trackCompilerKeyedItem", untrack: "untrack", }; for (const specifier of specifiers) { const helper = specifier as RuntimeHelperName; helperNames[helper] = allocator(occupiedNames.has(helper) ? `_${helper}` : helper); } return helperNames; } function emitRuntimeImportLine( runtimeImport: RuntimeImport, helperNames: RuntimeHelperNames, ): string { const specifiers = runtimeImport.specifiers; const importedNames = specifiers.map((specifier) => { const helper = specifier as RuntimeHelperName; const localName = helperNames[helper]; return localName === specifier ? specifier : `${specifier} as ${localName}`; }); return `import { ${importedNames.join(", ")} } from ${JSON.stringify(runtimeImport.source)};`; } function emitUserImports(ir: ModuleIr): string { return ir.components.length === 0 ? "" : ir.userImports.join("\n"); } function emitModuleStatements(ir: ModuleIr): string { return ir.components.length === 0 ? "" : ir.moduleStatements.join("\n"); } function collectImports(ir: ModuleIr): RuntimeImport[] { if (ir.components.length === 0) { return []; } const specifiers = new Set(["createTemplate"]); const internalSpecifiers = new Set(); const reactiveCoreSpecifiers = new Set(); const inlineMemoComponentNames = new Set( ir.components .filter((component) => getCompatInlineMemo(component) !== undefined) .map((component) => component.name), ); const usesLightweightMemoInsertion = ir.components.some((component) => treeUsesOwnerScopedMemo(component.root, inlineMemoComponentNames, false), ); const usesListCapableMemoInsertion = ir.components.some((component) => treeUsesOwnerScopedMemo(component.root, inlineMemoComponentNames, true), ); if (usesLightweightMemoInsertion || usesListCapableMemoInsertion) { internalSpecifiers.add("createMemo"); } if (usesLightweightMemoInsertion) { internalSpecifiers.add("insertMemo"); } if (usesListCapableMemoInsertion) { internalSpecifiers.add("insertMemoDynamic"); } if (ir.components.some((component) => treeUsesDeferredComponentRenderValues(component.root))) { internalSpecifiers.add("createMemo"); internalSpecifiers.add("installMemoRenderValueNormalizer"); } if (JSON.stringify(ir).includes(OXC_BIND_DOM_REF_PLACEHOLDER)) { specifiers.add("bindDomRef"); } if (JSON.stringify(ir).includes(OXC_UNTRACK_REACTIVE_ALIAS_PLACEHOLDER)) { reactiveCoreSpecifiers.add("untrack"); } for (const component of ir.components) { if (component.root.kind === "element" && component.root.namespace === "svg") { internalSpecifiers.add("createSvgTemplate"); } visitForClientImports(component.root, "setup", (node, context) => { if (node.kind === "expr") { if (isClientDynamicExpression(node)) { specifiers.add( node.renderMode === "render-value" ? "insertRenderValue" : "insertDynamic", ); } else if (node.renderMode === "compiler-keyed-cell-text") { internalSpecifiers.add("bindCompilerKeyedCellText"); } else if (node.renderMode === "compiler-keyed-text") { internalSpecifiers.add( node.compilerKeyedProperty === undefined ? "bindCompilerKeyedText" : "bindCompilerKeyedPropertyText", ); } else if (context === "setup" && node.renderMode !== "compiler-keyed-initial-text") { // Render-value expressions are inlined into the branch expression, so // they never produce a text binding of their own. if (provenNativeCellTextBinding(node) === undefined) { specifiers.add("bindText"); } else { internalSpecifiers.add("bindCellText"); } } } if (node.kind === "conditional") { if (usesBranchInsertion(node)) { internalSpecifiers.add("insertBranch"); } else { specifiers.add("insertDynamic"); } } if (node.kind === "list") { if (node.parameterBinding !== undefined) { reactiveCoreSpecifiers.add("computed"); if (node.keyCode !== undefined) { internalSpecifiers.add("createCompilerListBindingCache"); internalSpecifiers.add("trackCompilerKeyedItem"); } } if (node.compiledSingleNode === undefined) { if (requiresExplicitListRenderArity(node)) { internalSpecifiers.add("bindListWithRenderArity"); } else { specifiers.add("bindList"); } } else { if (node.compiledSingleNode.root.namespace === "svg") { internalSpecifiers.add("createSvgTemplateElement"); } else { specifiers.add("createTemplateElement"); } internalSpecifiers.add("bindCompilerKeyedSingleNodeList"); } } if (node.kind === "element") { if (node.namespace === "svg" && context === "render-value") { internalSpecifiers.add("createSvgTemplate"); } const dedicatedSelect = usesDedicatedSelectBinding(node); for (const attr of node.attributes) { const selectControlAttribute = node.tagName === "select" && (attr.kind === "static-attr" || attr.kind === "dynamic-attr") && isSelectControlAttributeName(attr.name); if (selectControlAttribute) { if (dedicatedSelect) { internalSpecifiers.add("bindSelectValue"); } else { specifiers.add("bindSpreadProps"); } continue; } if (attr.kind === "dynamic-attr") { if (specializedElementProperty(node, attr.name) === undefined) { specifiers.add("bindProp"); } else { internalSpecifiers.add("bindElementProperty"); } } if (attr.kind === "dom-ref") { specifiers.add("bindDomRef"); } if (attr.kind === "spread-attr") { specifiers.add("bindSpreadProps"); } if (attr.kind === "event" && attr.compilerKeyedSlot === undefined) { specifiers.add("bindEvent"); } } } }); if (componentUsesCreateList(component.root)) { internalSpecifiers.add("createListWithRenderArity"); } } const imports: RuntimeImport[] = [ { source: "@reckona/mreact-reactive-dom", specifiers: Array.from(specifiers).sort(), }, ]; if (internalSpecifiers.size > 0) { imports.push({ source: "@reckona/mreact-reactive-dom/internal", specifiers: Array.from(internalSpecifiers).sort(), }); } if (reactiveCoreSpecifiers.size > 0) { imports.push({ source: "@reckona/mreact-reactive-core", specifiers: Array.from(reactiveCoreSpecifiers).sort(), }); } if (JSON.stringify(ir).includes(OXC_COMPUTED_REACTIVE_ALIAS_PLACEHOLDER)) { imports.push({ source: "@reckona/mreact-reactive-core/internal", specifiers: ["deferredComputed"], }); } return imports; } function componentUsesCreateList(node: JsxNodeIr): boolean { if (node.kind === "conditional" || node.kind === "list") { return renderValueNodeUsesCreateList(node); } if (node.kind === "component") { return componentCallUsesCreateList(node); } return setupUsesCreateList(node); } function setupUsesCreateList(node: JsxNodeIr): boolean { if (node.kind === "component") { return componentCallUsesCreateList(node); } if (node.kind === "list") { return renderValueChildrenUseCreateList(node.children); } if (node.kind === "conditional") { return renderValueNodeUsesCreateList(node); } if (node.kind === "async-boundary") { return ( renderValueChildrenUseCreateList(node.children) || renderValueChildrenUseCreateList(node.placeholderChildren ?? []) || renderValueChildrenUseCreateList(node.catchChildren ?? []) ); } if (node.kind === "element" || node.kind === "fragment") { return node.children.some(setupUsesCreateList); } return false; } function renderValueChildrenUseCreateList(children: readonly JsxNodeIr[]): boolean { return children.some(renderValueNodeUsesCreateList); } function renderValueNodeUsesCreateList(node: JsxNodeIr): boolean { if (node.kind === "list") { return true; } if (node.kind === "conditional") { return ( renderValueChildrenUseCreateList(node.whenTrue) || renderValueChildrenUseCreateList(node.whenFalse) ); } if (node.kind === "fragment") { return renderValueChildrenUseCreateList(node.children); } if (node.kind === "component") { return componentCallUsesCreateList(node); } if (node.kind === "element") { return setupUsesCreateList(node); } return false; } function componentCallUsesCreateList(node: Extract): boolean { return ( node.props.some( (prop) => prop.kind === "render-prop" && renderValueChildrenUseCreateList(prop.children), ) || renderValueChildrenUseCreateList(node.children) ); } function hasClientReferenceNodes(ir: ModuleIr): boolean { return ir.components.some((component) => { let found = false; visit(component.root, (node) => { if ( node.kind === "component" && node.clientReference !== undefined && isCompatClientReferenceModuleId(node.clientReference.moduleId) ) { found = true; } }); return found; }); } function emitClientBoundaryHelper(name: string): string { return `function ${name}(name, props) { const fragment = document.createDocumentFragment(); const placeholder = document.createElement("template"); placeholder.setAttribute("data-mreact-client-boundary", name); const propsElement = document.createElement("script"); propsElement.type = "application/json"; propsElement.setAttribute("data-mreact-client-boundary-props", name); try { propsElement.textContent = JSON.stringify(props ?? {}) .replaceAll("&", "\\\\u0026") .replaceAll("<", "\\\\u003c") .replaceAll(">", "\\\\u003e") .replaceAll("\\u2028", "\\\\u2028") .replaceAll("\\u2029", "\\\\u2029"); } catch { placeholder.setAttribute("data-mreact-client-boundary-nonserializable", "true"); propsElement.textContent = "{}"; } fragment.append(placeholder, propsElement); return fragment; }`; } function emitComponent( component: ComponentIr, moduleAllocator: NameAllocator, helperNames: RuntimeHelperNames, clientBoundaryHelperName: string | undefined, inlineMemoComponents: ReadonlyMap, nonNullishComponents: ReadonlySet, options: { dev?: boolean; filename?: string }, ): string { const templateName = moduleAllocator("_tmpl_" + component.name, component.bindingNames); const allocator = createNameAllocator([...component.bindingNames, templateName]); // Installing the normalizer from inside the component that needs it keeps the // dependency out of a module whose only deferred-render-value component is // unreachable. The runtime installer is idempotent, so repeated renders do no // redundant setup. const body = [ ...(treeUsesDeferredComponentRenderValues(component.root) ? [` ${helperNames.installMemoRenderValueNormalizer}();`] : []), ...component.bodyStatements.map((statement) => ` ${statement}`), ]; const parameters = component.parameters.join(", "); const functionKeyword = emitFunctionKeyword(component); const debugLabel = options.dev === true && options.filename !== undefined ? `${options.filename}#${component.name}` : undefined; if (component.root.kind === "component") { const state: EmitSetupState = { allocateName: allocator, textIndex: 0, helperNames, clientBoundaryHelperName, inlineMemoComponents, nonNullishComponents, debugLabel, ownerDeclarations: [], listBindingCaches: new Map(), }; const componentCall = emitComponentCall( component.root.name, component.root.props, component.root.children, state, component.root.clientReference === undefined ? undefined : { moduleId: component.root.clientReference.moduleId, name: component.root.name }, ); return [ `${functionKeyword} ${component.name}(${parameters}) {`, ...body, ...state.ownerDeclarations.map((declaration) => ` ${declaration}`), ` return ${componentCall};`, `}`, ].join("\n"); } if (component.root.kind === "conditional") { const state: EmitSetupState = { allocateName: allocator, textIndex: 0, helperNames, clientBoundaryHelperName, inlineMemoComponents, nonNullishComponents, debugLabel, ownerDeclarations: [], listBindingCaches: new Map(), }; const fragmentName = allocator("_fragment"); const markerName = allocator("_marker"); const ownerScopedMemoHelper = ownerScopedMemoInsertionHelper(component.root, state); const renderValue = emitNodeRenderValueExpression(component.root, state); return [ `${functionKeyword} ${component.name}(${parameters}) {`, ...body, ...state.ownerDeclarations.map((declaration) => ` ${declaration}`), ` const ${fragmentName} = document.createDocumentFragment();`, ` const ${markerName} = document.createComment("");`, ` ${fragmentName}.append(${markerName});`, ` ${ownerScopedMemoHelper ?? (usesBranchInsertion(component.root) ? helperNames.insertBranch : helperNames.insertDynamic)}(${fragmentName}, ${markerName}, () => ${renderValue}${emitDynamicOptions(debugLabel)});`, ` return ${fragmentName};`, `}`, ].join("\n"); } const fragmentName = allocator("_fragment"); const rootName = allocator("_root"); const templateHtml = JSON.stringify(renderStaticHtml(component.root)); const state: EmitSetupState = { allocateName: allocator, textIndex: 0, helperNames, clientBoundaryHelperName, inlineMemoComponents, nonNullishComponents, debugLabel, ownerDeclarations: [], listBindingCaches: new Map(), }; const setup = emitSetup(component.root, rootName, state); const createTemplateHelper = component.root.kind === "element" && component.root.namespace === "svg" ? helperNames.createSvgTemplate : helperNames.createTemplate; return [ // The template is created on first render rather than at module load, so a // bundler can drop an unused export together with its HTML payload, and so // importing the module never requires a DOM. `let ${templateName};`, `${functionKeyword} ${component.name}(${parameters}) {`, ...body, ...state.ownerDeclarations.map((declaration) => ` ${declaration}`), ` const ${fragmentName} = (${templateName} ??= ${createTemplateHelper}(${templateHtml}))();`, component.root.kind === "fragment" ? ` const ${rootName} = ${fragmentName};` : ` const ${rootName} = ${fragmentName}.firstChild;`, setup, ` return ${rootName};`, `}`, ] .filter(Boolean) .join("\n"); } function emitFunctionKeyword(component: ComponentIr): string { return `${component.exportDefault === true ? "export default " : component.exported === false ? "" : "export "}${ component.async === true ? "async " : "" }function`; } function renderStaticHtml(node: JsxNodeIr): string { if (node.kind === "text") { return escapeHtml(node.value); } if (node.kind === "expr") { return ""; } if (node.kind === "conditional" || node.kind === "list") { return ""; } if (node.kind === "fragment") { return renderStaticChildren(node.children); } if (node.kind === "component") { return ""; } if (node.kind === "async-boundary") { return ""; } // Hydration compares this template against the server markup attribute by // attribute, so the template has to name attributes the way the server does. // The JSX prop name is not that name: HTML parsing would lowercase it, and // attribute synchronisation would then drop the server's attribute and copy // the lowercased JSX name across. const textareaSeed = readStaticTextareaSeed(node); const attrs = node.attributes .flatMap((attr) => { if (attr.kind !== "static-attr" || attr === textareaSeed) { return []; } const htmlName = htmlAttributeNameForElement(node.tagName, attr.name); return isUrlAttribute(htmlName) && isStaticUrlValueUnsafe(htmlName, attr.value) ? [] : [` ${htmlName}="${escapeHtml(attr.value)}"`]; }) .join(""); const children = textareaSeed === undefined ? hasDirectDangerouslySetInnerHtml(node) ? "" : renderStaticChildren(node.children) : escapeHtml(textareaSeed.value); return `<${node.tagName}${attrs}>${children}`; } /** * Reads the static attribute a textarea seeds its content from. * * A textarea has no value attribute in HTML: the server writes `value` or * `defaultValue` between the tags and lets it win over any children. Mapping the * name could not reconcile an attribute with content, so the template writes the * seed as content too. */ function readStaticTextareaSeed( node: Extract, ): Extract | undefined { if (node.tagName !== "textarea") { return undefined; } return node.attributes.find( (attr): attr is Extract => attr.kind === "static-attr" && (attr.name === "value" || attr.name === "defaultValue"), ); } function renderStaticChildren(children: readonly JsxNodeIr[]): string { return children .map((child, index) => canReuseTemplateTextNode(children, index) ? " " : renderStaticHtml(child), ) .join(""); } function canReuseTemplateTextNode(children: readonly JsxNodeIr[], index: number): boolean { const child = children[index]; if (child?.kind !== "expr" || isClientDynamicExpression(child)) { return false; } return ( !isMergeableTemplateText(children[index - 1]) && !isMergeableTemplateText(children[index + 1]) ); } function isMergeableTemplateText(node: JsxNodeIr | undefined): boolean { return node?.kind === "text" || (node?.kind === "expr" && !isClientDynamicExpression(node)); } function isClientDynamicExpression(node: Extract): boolean { return ( node.renderMode === "dynamic" || node.renderMode === "render-value" || node.renderMode === "server-render-value" ); } interface EmitSetupState { allocateName: (baseName: string) => string; textIndex: number; helperNames: RuntimeHelperNames; clientBoundaryHelperName?: string | undefined; inlineMemoComponents: ReadonlyMap; nonNullishComponents: ReadonlySet; debugLabel?: string | undefined; compilerKeyedEventSlotKeys?: ReadonlyMap | undefined; compilerKeyedElementPath?: string | undefined; compilerKeyedRowContext?: string | undefined; ownerDeclarations: string[]; listBindingCaches: Map, string>; } /** Reports whether a component call is proven to return a DOM node rather than a render value. */ function returnsRenderedNode( node: Extract, state: EmitSetupState, ): boolean { return ( node.clientReference === undefined && node.runtime !== "compat" && node.async !== true && !state.inlineMemoComponents.has(node.name) && state.nonNullishComponents.has(node.name) ); } function emitDynamicOptions(debugLabel: string | undefined, memo = false): string { const entries = [ ...(debugLabel === undefined ? [] : [`debugLabel: ${JSON.stringify(debugLabel)}`]), ...(memo ? ["memo: true"] : []), ]; return entries.length === 0 ? "" : `, { ${entries.join(", ")} }`; } function emitSetup( node: JsxNodeIr, path: string, state: EmitSetupState, initialChildIndex = 0, inheritedStableChildrenName?: string, inheritedLiveChildrenName?: string, ): string { const lines: string[] = []; if (node.kind !== "element" && node.kind !== "fragment" && node.kind !== "component") { return ""; } if (node.kind === "component") { const componentCall = emitComponentCall( node.name, node.props, node.children, state, node.clientReference === undefined ? undefined : { moduleId: node.clientReference.moduleId, name: node.name }, ); // A same-module component the emitter itself lowers always returns a node, // so the nullish and boolean render-value guard around its call is dead. if (returnsRenderedNode(node, state)) { lines.push(` ${path}.replaceWith(${componentCall});`); return lines.join("\n"); } const componentVar = state.allocateName("_component"); lines.push(` const ${componentVar} = ${componentCall};`); lines.push(` if (${componentVar} == null || typeof ${componentVar} === "boolean") {`); lines.push(` ${path}.remove();`); lines.push(` } else {`); lines.push(` ${path}.replaceWith(${componentVar});`); lines.push(` }`); return lines.join("\n"); } const currentPath = node.kind === "element" && shouldCacheCompilerKeyedElementPath(node, path, state) ? state.allocateName("_keyedElement") : path; if (currentPath !== path) { lines.push(` const ${currentPath} = ${path};`); } const postChildBindingLines: string[] = []; const selectBindingSources: string[] = []; const selectControlEntries: string[] = []; const dedicatedSelectBinding = node.kind === "element" && usesDedicatedSelectBinding(node); if (node.kind === "element") { for (const attr of node.attributes) { if ( node.tagName === "select" && attr.kind === "static-attr" && isSelectControlAttributeName(attr.name) ) { if (!dedicatedSelectBinding) { selectBindingSources.push( `{ ${JSON.stringify(attr.name)}: ${JSON.stringify(attr.value)} }`, ); } else if (attr.name !== "multiple") { // A static multiple attribute is already in the emitted template, so // the dedicated binding never has to reapply it. selectControlEntries.push(`${attr.name}: ${JSON.stringify(attr.value)}`); } continue; } if (attr.kind === "dynamic-attr") { if (node.tagName === "select" && isSelectControlAttributeName(attr.name)) { if (dedicatedSelectBinding) { selectControlEntries.push(`${attr.name}: (${attr.code})`); } else { selectBindingSources.push(`{ ${JSON.stringify(attr.name)}: (${attr.code}) }`); } continue; } const specializedProperty = specializedElementProperty(node, attr.name); const line = specializedProperty === undefined ? ` ${state.helperNames.bindProp}(${currentPath}, ${JSON.stringify(attr.name)}, () => (${attr.code}));` : ` ${state.helperNames.bindElementProperty}(${currentPath}, ${JSON.stringify(specializedProperty.property)}, ${JSON.stringify(specializedProperty.attribute)}, () => (${attr.code}));`; if (shouldDeferSelectBinding(node, attr)) { postChildBindingLines.push(line); } else { lines.push(line); } } if (attr.kind === "dom-ref") { lines.push(` ${state.helperNames.bindDomRef}(${currentPath}, ${attr.code});`); } if (attr.kind === "spread-attr") { if (node.tagName === "select") { selectBindingSources.push(`(${attr.code})`); continue; } const line = ` ${state.helperNames.bindSpreadProps}(${currentPath}, () => (${attr.code}));`; if (shouldDeferSelectBinding(node, attr)) { postChildBindingLines.push(line); } else { lines.push(line); } } if (attr.kind === "event") { if (state.compilerKeyedEventSlotKeys && attr.compilerKeyedSlot !== undefined) { const slotKey = state.compilerKeyedEventSlotKeys.get(attr.eventName); if (slotKey === undefined) { throw new Error(`Missing compiler keyed event slot for ${attr.eventName}.`); } lines.push(` ${currentPath}[${slotKey}] = ${attr.compilerKeyedSlot};`); } else { lines.push( ` ${state.helperNames.bindEvent}(${currentPath}, "${attr.eventName}", ${attr.code});`, ); } } } const selectBindingLine = emitSelectBindingLine( currentPath, selectControlEntries, selectBindingSources, state, ); if (selectBindingLine !== undefined) { if (hasDirectDangerouslySetInnerHtml(node)) { lines.push(selectBindingLine); return lines.join("\n"); } postChildBindingLines.push(selectBindingLine); } else if (hasDirectDangerouslySetInnerHtml(node)) { return lines.join("\n"); } } const children = node.children; const stableChildrenName = inheritedStableChildrenName ?? (needsStableChildrenSnapshot(children) ? state.allocateName("_children") : undefined); const liveChildrenName = stableChildrenName === undefined ? (inheritedLiveChildrenName ?? (state.compilerKeyedRowContext !== undefined && needsCompilerKeyedLiveChildrenAlias(children) ? state.allocateName("_keyedChildren") : undefined)) : undefined; let childIndex = initialChildIndex; if (stableChildrenName !== undefined && inheritedStableChildrenName === undefined) { lines.push(` const ${stableChildrenName} = Array.from(${currentPath}.childNodes);`); } else if (liveChildrenName !== undefined && inheritedLiveChildrenName === undefined) { lines.push(` const ${liveChildrenName} = ${currentPath}.childNodes;`); } let sawStaticText = false; let sawComponentMutation = false; const ownsStableChildrenSnapshot = inheritedStableChildrenName === undefined; for (let sourceChildIndex = 0; sourceChildIndex < children.length; sourceChildIndex += 1) { const child = children[sourceChildIndex] as JsxNodeIr; if (child.kind === "text") { sawStaticText = true; childIndex += 1; continue; } const usesLiveChildPath = stableChildrenName === undefined || (ownsStableChildrenSnapshot && child.kind !== "component" && !sawComponentMutation && usesLiveInsertionAnchor(child) && !sawStaticText); const childPath = usesLiveChildPath ? liveChildrenName !== undefined ? `${liveChildrenName}[${childIndex}]` : state.compilerKeyedRowContext !== undefined && stableChildrenName === undefined && childIndex === 0 ? `${currentPath}.firstChild` : `${currentPath}.childNodes[${childIndex}]` : `${stableChildrenName}[${childIndex}]`; if (usesLiveInsertionAnchor(child)) sawComponentMutation = true; if (child.kind === "expr") { if (isClientDynamicExpression(child)) { const markerPath = child.renderMode === "render-value" ? state.allocateName(`_renderValueMarker_${state.textIndex++}`) : childPath; if (child.renderMode === "render-value") { lines.push(` const ${markerPath} = document.createTextNode("");`); lines.push(` ${childPath}.replaceWith(${markerPath});`); } lines.push( ` ${child.renderMode === "render-value" ? state.helperNames.insertRenderValue : state.helperNames.insertDynamic}(${currentPath}, ${markerPath}, () => (${child.code})${emitDynamicOptions(state.debugLabel)});`, ); childIndex += 1; continue; } const textVar = state.allocateName(`_text_${state.textIndex}`); const initialTextValueVar = child.renderMode === "compiler-keyed-initial-text" ? state.allocateName(`_textValue_${state.textIndex}`) : undefined; const reuseTemplateTextNode = canReuseTemplateTextNode(children, sourceChildIndex); state.textIndex += 1; if (reuseTemplateTextNode) { lines.push(` const ${textVar} = ${childPath};`); } else if (initialTextValueVar === undefined) { lines.push(` const ${textVar} = document.createTextNode("");`); } else { lines.push(` const ${initialTextValueVar} = (${child.code});`); lines.push( ` const ${textVar} = document.createTextNode(typeof ${initialTextValueVar} === "string" ? ${initialTextValueVar} : ${initialTextValueVar} == null ? "" : String(${initialTextValueVar}));`, ); } if (!reuseTemplateTextNode) { lines.push(` ${childPath}.replaceWith(${textVar});`); } if (reuseTemplateTextNode && initialTextValueVar !== undefined) { lines.push(` const ${initialTextValueVar} = (${child.code});`); lines.push( ` ${textVar}.data = typeof ${initialTextValueVar} === "string" ? ${initialTextValueVar} : ${initialTextValueVar} == null ? "" : String(${initialTextValueVar});`, ); } if (child.renderMode === "compiler-keyed-cell-text") { if ( state.compilerKeyedRowContext === undefined || child.compilerKeyedProperty === undefined ) { throw new Error("Missing compiler keyed row context for optimized cell text."); } lines.push( ` ${state.helperNames.bindCompilerKeyedCellText}(${state.compilerKeyedRowContext}, ${textVar}, ${JSON.stringify(child.compilerKeyedProperty)});`, ); } else if (child.renderMode === "compiler-keyed-text") { if (state.compilerKeyedRowContext === undefined) { throw new Error("Missing compiler keyed row context for optimized text."); } if (child.compilerKeyedProperty === undefined) { lines.push( ` ${state.helperNames.bindCompilerKeyedText}(${state.compilerKeyedRowContext}, ${textVar}, () => (${child.code}));`, ); } else { lines.push( ` ${state.helperNames.bindCompilerKeyedPropertyText}(${state.compilerKeyedRowContext}, ${textVar}, ${JSON.stringify(child.compilerKeyedProperty)});`, ); } } else if (child.renderMode !== "compiler-keyed-initial-text") { const provenCell = provenNativeCellTextBinding(child); lines.push( provenCell === undefined ? ` ${state.helperNames.bindText}(${textVar}, () => (${child.code}));` : ` ${state.helperNames.bindCellText}(${textVar}, ${provenCell});`, ); } childIndex += 1; continue; } if (child.kind === "conditional") { const ownerScopedMemoHelper = ownerScopedMemoInsertionHelper(child, state); const insertionHelper = ownerScopedMemoHelper ?? (usesBranchInsertion(child) ? state.helperNames.insertBranch : state.helperNames.insertDynamic); lines.push( ` ${insertionHelper}(${currentPath}, ${childPath}, () => ${emitConditionalRenderValueExpression(child, state)}${emitDynamicOptions(state.debugLabel)});`, ); childIndex += 1; continue; } if (child.kind === "list") { const parameters = emitListParameters(child); const optionEntries: string[] = []; const eventPrograms = child.compiledSingleNode?.eventPrograms; const eventSlotKeys = eventPrograms?.map(() => state.allocateName("_keyedEventSlot")); if (eventSlotKeys !== undefined) { for (const slotKey of eventSlotKeys) { lines.push(` const ${slotKey} = Symbol();`); } } if (child.keyCode !== undefined) { optionEntries.push(emitListKeyOption(child, state)); } if ( child.keyCode !== undefined && listReadsNestedItemObject(child, child.itemName, child.compiledSingleNode?.root) ) { optionEntries.push("nestedObjectFallback: true"); } if (child.compiledSingleNode?.selectedClass !== undefined) { optionEntries.push( `compilerSelectedClass: { className: ${JSON.stringify(child.compiledSingleNode.selectedClass.className)}, initialClassValue: "", source: ${child.compiledSingleNode.selectedClass.sourceCode} }`, ); } if (eventPrograms !== undefined && eventSlotKeys !== undefined) { optionEntries.push( `compilerEvents: ${emitCompilerKeyedEventPrograms(eventPrograms, child.itemName, eventSlotKeys)}`, ); optionEntries.push("deferEventPromotion: false"); } if (child.compiledSingleNode?.ownsTextCleanup === true) { optionEntries.push("compilerOwnsTextCleanup: true"); } const options = optionEntries.length === 0 ? "" : `, { ${optionEntries.join(", ")} }`; if (child.compiledSingleNode === undefined) { const explicitRenderArity = requiresExplicitListRenderArity(child); const listOptions = explicitRenderArity && options === "" ? ", undefined" : options; lines.push( ` ${explicitRenderArity ? state.helperNames.bindListWithRenderArity : state.helperNames.bindList}(${currentPath}, ${childPath}, ${emitListItems(child, state)}, ${emitListRenderer(child, parameters, state)}${listOptions}${explicitRenderArity ? `, ${emitListRenderArity(child)}` : ""});`, ); } else { const templateName = state.allocateName("_keyedTemplate"); lines.push( ` const ${templateName} = ${child.compiledSingleNode.root.namespace === "svg" ? state.helperNames.createSvgTemplateElement : state.helperNames.createTemplateElement}(${JSON.stringify(renderStaticHtml(child.compiledSingleNode.root))});`, ); lines.push( ` ${state.helperNames.bindCompilerKeyedSingleNodeList}(${currentPath}, ${childPath}, () => (${child.itemsCode}), ${emitCompilerKeyedSingleNodeRenderer(child, templateName, state, eventSlotKeys)}${options});`, ); } childIndex += 1; continue; } if (child.kind === "async-boundary") { lines.push(emitAsyncBoundarySetup(child, childPath, state)); childIndex += 1; continue; } if (child.kind === "fragment") { const previousCompilerKeyedElementPath = state.compilerKeyedElementPath; state.compilerKeyedElementPath = undefined; lines.push( emitSetup(child, currentPath, state, childIndex, stableChildrenName, liveChildrenName), ); state.compilerKeyedElementPath = previousCompilerKeyedElementPath; sawComponentMutation ||= hasLiveChildListMutation(child.children); childIndex += renderedChildNodeCount(child); continue; } const previousCompilerKeyedElementPath = state.compilerKeyedElementPath; state.compilerKeyedElementPath = state.compilerKeyedRowContext !== undefined && usesLiveChildPath ? childPath : undefined; lines.push(emitSetup(child, childPath, state)); state.compilerKeyedElementPath = previousCompilerKeyedElementPath; if (child.kind === "component") { sawComponentMutation = true; } childIndex += 1; } lines.push(...postChildBindingLines); return lines.filter(Boolean).join("\n"); } function shouldDeferSelectBinding( node: Extract, attribute: Extract, ): boolean { return ( node.tagName === "select" && (attribute.kind === "spread-attr" || (attribute.kind === "dynamic-attr" && (attribute.name === "value" || attribute.name === "defaultValue"))) ); } function renderedChildNodeCount(node: JsxNodeIr): number { return node.kind === "fragment" ? node.children.reduce((count, child) => count + renderedChildNodeCount(child), 0) : 1; } function hasDirectDangerouslySetInnerHtml(node: Extract): boolean { return node.attributes.some( (attribute) => attribute.kind === "dynamic-attr" && attribute.name === "dangerouslySetInnerHTML", ); } function shouldCacheCompilerKeyedElementPath( node: Extract, path: string, state: EmitSetupState, ): boolean { if (state.compilerKeyedRowContext === undefined || state.compilerKeyedElementPath !== path) { return false; } let pathUses = 0; for (const attr of node.attributes) { if (attr.kind === "event" && attr.compilerKeyedSlot !== undefined) { pathUses += 1; } } for (const child of node.children) { if (child.kind === "expr" && !isClientDynamicExpression(child)) { pathUses += 1; } } return pathUses > 1; } function needsCompilerKeyedLiveChildrenAlias(children: readonly JsxNodeIr[]): boolean { let setupChildCount = 0; for (const child of children) { if (compilerKeyedNodeHasSetup(child)) { setupChildCount += 1; if (setupChildCount > 1) { return true; } } } return false; } function compilerKeyedNodeHasSetup(node: JsxNodeIr): boolean { if (node.kind === "text") { return false; } if (node.kind === "element") { return ( node.attributes.some((attribute) => attribute.kind !== "static-attr") || node.children.some(compilerKeyedNodeHasSetup) ); } if (node.kind === "fragment") { return node.children.some(compilerKeyedNodeHasSetup); } return true; } function usesLiveInsertionAnchor(child: JsxNodeIr): boolean { return ( child.kind === "component" || (child.kind === "expr" && isClientDynamicExpression(child)) || child.kind === "conditional" || child.kind === "list" || child.kind === "async-boundary" ); } function hasLiveChildListMutation(children: readonly JsxNodeIr[]): boolean { return children.some( (child) => usesLiveInsertionAnchor(child) || (child.kind === "fragment" && hasLiveChildListMutation(child.children)), ); } function needsStableChildrenSnapshot(children: readonly JsxNodeIr[]): boolean { if (!hasLiveChildListMutation(children)) { return false; } if (children.filter(usesLiveInsertionAnchor).length > 1) return true; let sawStaticText = false; for (const child of children) { if (child.kind === "text") { sawStaticText = true; continue; } const usesDirectLivePath = child.kind !== "component" && usesLiveInsertionAnchor(child) && !sawStaticText; if (!usesDirectLivePath) { return true; } } return false; } function emitRenderValueExpression( children: JsxNodeIr[], state: EmitSetupState, ownerScopedMemo = false, ): string { if (children.length === 0) { return "null"; } if (children.length === 1) { return emitNodeRenderValueExpression(children[0] as JsxNodeIr, state, ownerScopedMemo); } return `[${children .map((child) => emitNodeRenderValueExpression(child, state, ownerScopedMemo)) .join(", ")}]`; } /** * Render values handed to a component (children and render props) are evaluated * once at the call site, so a reactive conditional inside them would never * subscribe and would never mount. Give each such branch its own dynamic owner * so the subscription, the DOM range, and the branch cleanup scope all live * with the branch instead of with the caller. */ function emitComponentRenderValueExpression(children: JsxNodeIr[], state: EmitSetupState): string { const parts = children.map((child) => emitComponentRenderValueNode(child, state)); return parts.length === 1 ? (parts[0] as string) : `[${parts.join(", ")}]`; } function emitComponentRenderValueNode(node: JsxNodeIr, state: EmitSetupState): string { if (needsDeferredComponentRenderValue(node)) { const expression = emitNodeRenderValueExpression(node, state); return `${state.helperNames.createMemo}(null, null, () => ${expression}, () => false)`; } if (shouldDeferComponentRenderValue(node)) { const expression = emitNodeRenderValueExpression(node, state); return `${state.helperNames.createMemo}(null, null, () => ${expression}, () => false)`; } if (node.kind === "fragment") { const valueExpression = emitComponentRenderValueExpression(node.children, state); if (node.bodyStatements !== undefined && node.bodyStatements.length > 0) { return [ "(() => {", ...node.bodyStatements.map((statement) => ` ${statement}`), ` return ${valueExpression};`, "})()", ].join("\n"); } return valueExpression; } return emitNodeRenderValueExpression(node, state); } function shouldDeferComponentRenderValue(node: JsxNodeIr): boolean { if (node.kind === "expr") { return node.deferRenderValue === true; } if (node.kind === "conditional") { return ( needsDeferredComponentRenderValue(node) || [...node.whenTrue, ...node.whenFalse].some( (child) => child.kind === "list" || shouldDeferComponentRenderValue(child), ) ); } if (node.kind === "component") { return true; } if (node.kind === "element") { return node.children.some(shouldDeferComponentRenderValue); } if (node.kind === "fragment") { return node.children.some(shouldDeferComponentRenderValue); } return false; } function needsDeferredComponentRenderValue(node: JsxNodeIr): boolean { return ( node.kind === "conditional" && (needsOwnedDynamicRenderValue(node) || [...node.whenTrue, ...node.whenFalse].some((child) => child.kind === "list")) ); } function needsOwnedDynamicRenderValue(node: JsxNodeIr): boolean { return ( node.kind === "conditional" && readsReactiveSourceCode(node.conditionCode) && [...node.whenTrue, ...node.whenFalse].some(rendersDomNode) ); } function rendersDomNode(node: JsxNodeIr): boolean { if ( node.kind === "element" || node.kind === "component" || node.kind === "list" || node.kind === "async-boundary" ) { return true; } if (node.kind === "fragment") { return node.children.some(rendersDomNode); } if (node.kind === "conditional") { return [...node.whenTrue, ...node.whenFalse].some(rendersDomNode); } return false; } function readsReactiveSourceCode(code: string): boolean { return /\.\s*get\s*\(/.test(code); } function treeUsesDeferredComponentRenderValues(node: JsxNodeIr): boolean { if (node.kind === "component") { if ( node.children.some(shouldDeferComponentRenderValue) || node.props.some( (prop) => prop.kind === "render-prop" && prop.children.some(shouldDeferComponentRenderValue), ) ) { return true; } return ( node.children.some(treeUsesDeferredComponentRenderValues) || node.props.some( (prop) => prop.kind === "render-prop" && prop.children.some(treeUsesDeferredComponentRenderValues), ) ); } if (node.kind === "conditional") { return [...node.whenTrue, ...node.whenFalse].some(treeUsesDeferredComponentRenderValues); } if (node.kind === "list") { return node.children.some(treeUsesDeferredComponentRenderValues); } if (node.kind === "element" || node.kind === "fragment") { return node.children.some(treeUsesDeferredComponentRenderValues); } return false; } function emitAsyncBoundarySetup( node: Extract, childPath: string, state: EmitSetupState, ): string { // Without a stable id the server has no way to tell the client what it // resolved. Leave the placeholder comment in place so the server-rendered // subtree (preserved via the hydration marker skip) remains the source of // truth. The resolved buttons inside it stay non-interactive in that case. if (node.awaitId === undefined) { return ""; } const valueName = node.valueName; const renderChildren = emitRenderValueExpression(node.children, state); const awaitIdLiteral = JSON.stringify(node.awaitId); return [ ` {`, ` const _awaitStore = globalThis.__mreactAwaitData;`, ` const _awaitEntry = _awaitStore === undefined ? undefined : _awaitStore[${awaitIdLiteral}];`, ` if (_awaitEntry !== undefined) {`, ` const ${valueName} = _awaitEntry.value;`, ` const _resolvedAwaitContent = ${renderChildren};`, ` if (_resolvedAwaitContent != null) {`, ` ${childPath}.replaceWith(_resolvedAwaitContent);`, ` }`, ` }`, ` }`, ].join("\n"); } function emitNodeRenderValueExpression( node: JsxNodeIr, state: EmitSetupState, ownerScopedMemo = false, ): string { if (node.kind === "text") { return JSON.stringify(node.value); } if (node.kind === "expr") { return ownerScopedMemo ? `${state.helperNames.createMemo}(null, null, () => (${node.code}), () => false)` : `(${node.code})`; } if (node.kind === "component") { const inlineMemo = state.inlineMemoComponents.get(node.name); if (ownerScopedMemo && inlineMemo !== undefined) { const memoProps = state.allocateName("_memoProps"); const propsCode = emitPropsObject(node.props, node.children, state, false); const compareArgument = inlineMemo.compareCode === undefined ? "" : `, ${inlineMemo.compareCode}`; return `${state.helperNames.createMemo}(${node.name}, ${propsCode}, (${memoProps}) => ${node.name}(${memoProps})${compareArgument})`; } return emitComponentCall( node.name, node.props, node.children, state, node.clientReference === undefined ? undefined : { moduleId: node.clientReference.moduleId, name: node.name }, ); } if (node.kind === "fragment") { if (node.bodyStatements !== undefined && node.bodyStatements.length > 0) { const valueExpression = emitRenderValueExpression(node.children, state); return [ "(() => {", ...node.bodyStatements.map((statement) => ` ${statement}`), ` return ${valueExpression};`, "})()", ].join("\n"); } return emitRenderValueExpression(node.children, state); } if (node.kind === "conditional") { return emitConditionalRenderValueExpression(node, state); } if (node.kind === "list") { const parameters = emitListParameters(node); const options = emitListOptions(node, state); return `${state.helperNames.createListWithRenderArity}(${emitListItems(node, state)}, ${emitListRenderer(node, parameters, state)}, ${emitListRenderArity(node)}${options})`; } if (node.kind === "async-boundary") { return "null"; } const templateName = state.allocateName("_dynamicTemplate"); const fragmentName = state.allocateName("_dynamicFragment"); const rootName = state.allocateName("_dynamicRoot"); const templateHtml = JSON.stringify(renderStaticHtml(node)); const setup = emitSetup(node, rootName, state); const setupLines = setup === "" ? [] : setup.split("\n"); return [ "(() => {", ` const ${templateName} = ${node.kind === "element" && node.namespace === "svg" ? state.helperNames.createSvgTemplate : state.helperNames.createTemplate}(${templateHtml});`, ` const ${fragmentName} = ${templateName}();`, ` const ${rootName} = ${fragmentName}.firstChild;`, ...setupLines, ` return ${rootName};`, "})()", ].join("\n"); } function isOwnerScopedMemoConditional( node: Extract, state: EmitSetupState, ): boolean { const branches = [node.whenTrue, node.whenFalse]; return ( branches.some( (branch) => branch.length === 1 && branch[0]?.kind === "component" && state.inlineMemoComponents.has(branch[0].name), ) && branches.every( (branch) => branch.length === 0 || (branch.length === 1 && branch[0]?.kind === "component" && state.inlineMemoComponents.has(branch[0].name)) || (branch.length === 1 && (branch[0]?.kind === "expr" || branch[0]?.kind === "text" || branch[0]?.kind === "list")), ) ); } function ownerScopedMemoInsertionHelper( node: Extract, state: EmitSetupState, ): string | undefined { if (!isOwnerScopedMemoConditional(node, state)) { return undefined; } return ownerScopedMemoBranchesNeedListSupport(node.whenTrue, node.whenFalse) ? state.helperNames.insertMemoDynamic : state.helperNames.insertMemo; } function treeUsesOwnerScopedMemo( node: JsxNodeIr, inlineMemoComponentNames: ReadonlySet, requiresListSupport: boolean, ): boolean { if ( node.kind === "conditional" && isOwnerScopedMemoBranches(node.whenTrue, node.whenFalse, inlineMemoComponentNames) && ownerScopedMemoBranchesNeedListSupport(node.whenTrue, node.whenFalse) === requiresListSupport ) { return true; } if (node.kind === "conditional") { return [...node.whenTrue, ...node.whenFalse].some((child) => treeUsesOwnerScopedMemo(child, inlineMemoComponentNames, requiresListSupport), ); } if (node.kind === "list" || node.kind === "element" || node.kind === "fragment") { return node.children.some((child) => treeUsesOwnerScopedMemo(child, inlineMemoComponentNames, requiresListSupport), ); } if (node.kind === "component") { return ( node.props.some( (prop) => prop.kind === "render-prop" && prop.children.some((child) => treeUsesOwnerScopedMemo(child, inlineMemoComponentNames, requiresListSupport), ), ) || node.children.some((child) => treeUsesOwnerScopedMemo(child, inlineMemoComponentNames, requiresListSupport), ) ); } if (node.kind === "async-boundary") { return ( node.children.some((child) => treeUsesOwnerScopedMemo(child, inlineMemoComponentNames, requiresListSupport), ) || node.placeholderChildren?.some((child) => treeUsesOwnerScopedMemo(child, inlineMemoComponentNames, requiresListSupport), ) === true || node.catchChildren?.some((child) => treeUsesOwnerScopedMemo(child, inlineMemoComponentNames, requiresListSupport), ) === true ); } return false; } function ownerScopedMemoBranchesNeedListSupport( whenTrue: readonly JsxNodeIr[], whenFalse: readonly JsxNodeIr[], ): boolean { return [whenTrue, whenFalse].some( (branch) => branch.length === 1 && (branch[0]?.kind === "expr" || branch[0]?.kind === "list"), ); } function isOwnerScopedMemoBranches( whenTrue: readonly JsxNodeIr[], whenFalse: readonly JsxNodeIr[], inlineMemoComponentNames: ReadonlySet, ): boolean { const branches = [whenTrue, whenFalse]; return ( branches.some( (branch) => branch.length === 1 && branch[0]?.kind === "component" && inlineMemoComponentNames.has(branch[0].name), ) && branches.every( (branch) => branch.length === 0 || (branch.length === 1 && branch[0]?.kind === "component" && inlineMemoComponentNames.has(branch[0].name)) || (branch.length === 1 && (branch[0]?.kind === "expr" || branch[0]?.kind === "text" || branch[0]?.kind === "list")), ) ); } function emitConditionalRenderValueExpression( node: Extract, state: EmitSetupState, ): string { const ownerScopedMemo = isOwnerScopedMemoConditional(node, state); const whenTrue = emitRenderValueExpression(node.whenTrue, state, ownerScopedMemo); const whenFalse = emitRenderValueExpression(node.whenFalse, state, ownerScopedMemo); if (node.conditionValueName === undefined) { return `((${node.conditionCode}) ? ${whenTrue} : ${whenFalse})`; } return `(() => { const ${node.conditionValueName} = (${node.conditionCode}); return ${node.conditionTestCode ?? node.conditionValueName} ? ${whenTrue} : ${whenFalse}; })()`; } function emitListRenderer( node: Extract, parameters: string, state: EmitSetupState, ): string { const rendererState: EmitSetupState = { ...state, ownerDeclarations: [], listBindingCaches: new Map(), }; const valueExpression = emitRenderValueExpression(node.children, rendererState); const ownerDeclarations = rendererState.ownerDeclarations.map( (declaration) => ` ${declaration}`, ); const parameterBinding = node.parameterBinding; if (parameterBinding !== undefined) { const sourceParameters = parameterBinding.sourcePatterns.join(", "); const boundValues = parameterBinding.bindingNames.join(", "); const argumentsCode = parameterBinding.argumentNames.join(", "); const cacheName = getListBindingCache(node, state); if (cacheName !== undefined) { const indexName = parameterBinding.argumentNames[1] as string; const itemCellName = state.allocateName("_listItemCell"); const bindingCellName = state.allocateName("_listBindingCell"); return `(${parameters}, ${itemCellName}) => { ${ownerDeclarations.join("\n")}${ownerDeclarations.length === 0 ? "" : "\n"} const ${bindingCellName} = ${cacheName}.binding(${indexName}); const ${parameterBinding.cellName} = ${state.helperNames.computed}(() => { ${state.helperNames.trackCompilerKeyedItem}(${itemCellName}); return ${bindingCellName}.get(); }); ${parameterBinding.cellName}.get(); return ${valueExpression}; }`; } return `(${parameters}) => { ${ownerDeclarations.join("\n")}${ownerDeclarations.length === 0 ? "" : "\n"} const ${parameterBinding.cellName} = ${state.helperNames.computed}(() => ((${sourceParameters}) => [${boundValues}])(${argumentsCode})); ${parameterBinding.cellName}.get(); return ${valueExpression}; }`; } if (node.bodyStatements === undefined || node.bodyStatements.length === 0) { return ownerDeclarations.length === 0 ? `(${parameters}) => ${valueExpression}` : `(${parameters}) => {\n${ownerDeclarations.join("\n")}\n return ${valueExpression};\n }`; } return `(${parameters}) => {\n${ownerDeclarations.join("\n")}${ownerDeclarations.length === 0 ? "" : "\n"}${node.bodyStatements.map((statement) => ` ${statement}`).join("\n")}\n return ${valueExpression};\n }`; } function emitCompilerKeyedSingleNodeRenderer( node: Extract, templateName: string, state: EmitSetupState, eventSlotKeys: readonly string[] | undefined, ): string { const root = node.compiledSingleNode?.root; if (root === undefined) { throw new Error("Missing compiled single-node root."); } const rootName = state.allocateName("_keyedRoot"); const eventPrograms = node.compiledSingleNode?.eventPrograms; const setup = emitSetup(root, rootName, { ...state, compilerKeyedEventSlotKeys: eventPrograms === undefined || eventSlotKeys === undefined ? undefined : new Map( eventPrograms.map((program, index) => [ program.eventName, eventSlotKeys[index] as string, ]), ), compilerKeyedRowContext: node.itemName, }); const setupLines = setup === "" ? [] : setup.split("\n"); return [ `(${node.itemName}) => {`, ` const ${rootName} = ${templateName}();`, ...setupLines, ` return ${rootName};`, "}", ].join("\n"); } function emitCompilerKeyedEventPrograms( programs: NonNullable< Extract["compiledSingleNode"] >["eventPrograms"], rowName: string, eventSlotKeys: readonly string[], ): string { if (programs === undefined) { return "[]"; } return `[${programs .map( (program, programIndex) => `{ type: ${JSON.stringify(program.eventName)}, slotKey: ${eventSlotKeys[programIndex]}, dispatch: (slot, ${rowName}, event, currentTarget) => { switch (slot) { ${program.handlers .map((handler, slot) => `case ${slot}: return (${handler}).call(currentTarget, event);`) .join(" ")} } } }`, ) .join(", ")}]`; } function emitListOptions( node: Extract, state: EmitSetupState, ): string { const optionEntries: string[] = []; if (node.keyCode !== undefined) { optionEntries.push(emitListKeyOption(node, state)); } if (node.keyCode !== undefined && listReadsNestedItemObject(node, node.itemName)) { optionEntries.push("nestedObjectFallback: true"); } return optionEntries.length === 0 ? "" : `, { ${optionEntries.join(", ")} }`; } function emitListKeyOption( node: Extract, state: EmitSetupState, ): string { const parameterBinding = node.parameterBinding; const cacheName = getListBindingCache(node, state); if (parameterBinding === undefined || cacheName === undefined) { return `key: (${emitListParameters(node)}) => (${node.keyCode})`; } const parameters = parameterBinding.argumentNames.join(", "); const indexName = parameterBinding.argumentNames[1] as string; return `key: (${parameters}) => ${cacheName}.key(${indexName})`; } function emitListItems(node: Extract, state: EmitSetupState): string { const cacheName = getListBindingCache(node, state); if ( cacheName === undefined || node.parameterBinding === undefined || node.keyCode === undefined ) { return `() => (${node.itemsCode})`; } const sourceParameters = node.parameterBinding.sourcePatterns.join(", "); const boundValues = node.parameterBinding.bindingNames.join(", "); const keyName = state.allocateName("_listKey"); return `() => ${cacheName}.prepare((${node.itemsCode}), (${sourceParameters}) => { const ${keyName} = (${node.keyCode}); return [${keyName}, [${boundValues}]]; })`; } function getListBindingCache( node: Extract, state: EmitSetupState, ): string | undefined { if (node.parameterBinding === undefined || node.keyCode === undefined) { return undefined; } const existing = state.listBindingCaches.get(node); if (existing !== undefined) { return existing; } const cacheName = state.allocateName("_listBindingCache"); state.listBindingCaches.set(node, cacheName); state.ownerDeclarations.push( `const ${cacheName} = ${state.helperNames.createCompilerListBindingCache}();`, ); return cacheName; } function emitListParameters(node: Extract): string { if (node.parameterPatterns !== undefined) { return node.parameterPatterns.join(", "); } return [node.itemName, node.indexName, node.arrayName] .filter((name): name is string => name !== undefined) .join(", "); } function emitListRenderArity(node: Extract): number { if (node.parameterBinding !== undefined) { return node.parameterBinding.sourcePatterns.some((pattern) => pattern.trimStart().startsWith("..."), ) ? 3 : Math.min(node.parameterBinding.sourcePatterns.length, 3); } const patterns = node.parameterPatterns; if (patterns !== undefined) { if (patterns.some((pattern) => pattern.trimStart().startsWith("..."))) { return 3; } if (patterns[0] !== undefined && !/^[A-Za-z_$][\w$]*$/u.test(patterns[0])) { return 3; } return Math.min(patterns.length, 3); } return Math.min( [node.itemName, node.indexName, node.arrayName].filter((name) => name !== undefined).length, 3, ); } function requiresExplicitListRenderArity(node: Extract): boolean { return ( node.parameterBinding !== undefined || node.parameterPatterns?.some((pattern) => !/^[A-Za-z_$][\w$]*$/u.test(pattern)) === true ); } function emitComponentCall( name: string, props: ComponentPropIr[], children: JsxNodeIr[], state: EmitSetupState, clientReference?: { moduleId: string; name: string } | undefined, ): string { if ( clientReference !== undefined && state.clientBoundaryHelperName !== undefined && isCompatClientReferenceModuleId(clientReference.moduleId) ) { return `${state.clientBoundaryHelperName}(${JSON.stringify(clientReference.name)}, ${emitPropsObject(props, children, state)})`; } return `${name}(${emitPropsObject(props, children, state)})`; } function emitPropsObject( props: ComponentPropIr[], children: JsxNodeIr[], state: EmitSetupState, reactiveGetters = true, ): string { const entries = props.map((prop) => { if (prop.kind === "spread-prop") { return `...(${prop.code})`; } if (prop.kind === "render-prop") { const renderValue = emitComponentRenderValueExpression(prop.children, state); // A call expression prop is analyzed as a render value because it may return // markup, but a plain reactive read inside it still has to stay lazy or the // component receives a value frozen at its first render. A reactive // conditional is a different IR kind and is handled by the owned dynamic // value above, so the two never apply to the same prop. return reactiveGetters && shouldEmitReactiveRenderPropGetter(prop.children) ? `get ${emitGetterPropName(prop.name)}() { return ${renderValue}; }` : `${emitPropName(prop.name)}: ${renderValue}`; } if (reactiveGetters && shouldEmitReactiveComponentPropGetter(prop.code)) { return `get ${emitGetterPropName(prop.name)}() { return (${prop.code}); }`; } return `${emitPropName(prop.name)}: (${prop.code})`; }); if (children.length > 0) { entries.push(`children: ${emitComponentRenderValueExpression(children, state)}`); } return `{ ${entries.join(", ")} }`; } function emitPropName(name: string): string { return /^[A-Za-z_$][\w$]*$/.test(name) ? name : JSON.stringify(name); } function emitGetterPropName(name: string): string { return /^[A-Za-z_$][\w$]*$/.test(name) ? name : `[${JSON.stringify(name)}]`; } function shouldEmitReactiveComponentPropGetter(code: string): boolean { if (!/\.\s*get\s*\(/.test(code)) { return false; } return !/^\s*(?:async\s*)?(?:function\b|(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>)/.test(code); } function shouldEmitReactiveRenderPropGetter(children: readonly JsxNodeIr[]): boolean { const child = children[0]; return ( children.length === 1 && child?.kind === "expr" && shouldEmitReactiveComponentPropGetter(child.code) ); } function createNameAllocator(reservedNames: readonly string[]): NameAllocator { const usedNames = new Set(reservedNames); return (baseName: string, extraReservedNames: readonly string[] = []): string => { const reservedNames = new Set(extraReservedNames); let name = baseName; let index = 1; while (usedNames.has(name) || reservedNames.has(name)) { name = `${baseName}$${index}`; index += 1; } usedNames.add(name); return name; }; } function isCompatClientReferenceModuleId(moduleId: string): boolean { return /\.compat(?:\.mreact)?(?:\.[cm]?[jt]sx?)?$/.test(moduleId); } type NameAllocator = (baseName: string, extraReservedNames?: readonly string[]) => string; function visit(node: JsxNodeIr, fn: (node: JsxNodeIr) => void): void { fn(node); if (node.kind === "conditional") { for (const child of [...node.whenTrue, ...node.whenFalse]) { visit(child, fn); } } if (node.kind === "list") { if (node.compiledSingleNode === undefined) { for (const child of node.children) { visit(child, fn); } } else { visit(node.compiledSingleNode.root, fn); } } if (node.kind === "component") { for (const prop of node.props) { if (prop.kind === "render-prop") { for (const child of prop.children) { visit(child, fn); } } } for (const child of node.children) { visit(child, fn); } } if (node.kind === "element" || node.kind === "fragment") { for (const child of node.children) { visit(child, fn); } } // Async-boundary children participate in client-side rendering when the // boundary has an awaitId (hydration data path). Traverse them so their // runtime imports (bindList / bindText / bindEvent / etc.) are included. if (node.kind === "async-boundary") { for (const child of node.children) { visit(child, fn); } if (node.placeholderChildren !== undefined) { for (const child of node.placeholderChildren) { visit(child, fn); } } if (node.catchChildren !== undefined) { for (const child of node.catchChildren) { visit(child, fn); } } } } function visitForClientImports( node: JsxNodeIr, context: "render-value" | "setup", fn: (node: JsxNodeIr, context: "render-value" | "setup") => void, ): void { fn(node, context); if (node.kind === "conditional") { for (const child of [...node.whenTrue, ...node.whenFalse]) { visitForClientImports(child, "render-value", fn); } return; } if (node.kind === "list") { if (context === "setup" && node.compiledSingleNode !== undefined) { visitForClientImports(node.compiledSingleNode.root, "setup", fn); } else { for (const child of node.children) { visitForClientImports(child, "render-value", fn); } } return; } if (node.kind === "component") { for (const prop of node.props) { if (prop.kind === "render-prop") { for (const child of prop.children) { visitForClientImports(child, "render-value", fn); } } } for (const child of node.children) { visitForClientImports(child, "render-value", fn); } return; } if (node.kind === "element") { for (const child of node.children) { visitForClientImports(child, "setup", fn); } return; } if (node.kind === "fragment") { for (const child of node.children) { visitForClientImports(child, context, fn); } return; } if (node.kind === "async-boundary") { for (const child of node.children) { visitForClientImports(child, "render-value", fn); } for (const child of node.placeholderChildren ?? []) { visitForClientImports(child, "render-value", fn); } for (const child of node.catchChildren ?? []) { visitForClientImports(child, "render-value", fn); } } }