import { emitCompatSsrBoundaryHelper } from "./emit-compat-ssr-boundary.js"; import type { AttributeIr, ComponentPropIr, ComponentIr, JsxNodeIr, ModuleIr } from "./ir.js"; import type { RuntimeImport, ServerEscapeOptions } from "./types.js"; import { emitEscapeHtmlHelper } from "./emit-escape-helper.js"; import { createCodeBuilder } from "./emit-code-builder.js"; import { escapeHtmlAttribute as escapeHtml } from "@reckona/mreact-shared/html-escape"; import { emitOptionSelectedAttributeCode, emitSelectSelectionValueCode, htmlAttributeNameForElement, isBooleanishStringAttribute, isDangerousHtmlAttribute, isStaticUrlValueUnsafe, isUrlAttribute, isVoidHtmlElement, parseStaticStyleObjectLiteral, parseStyleLiteralValue, simpleSideEffectFreeExpression, type OptionSelectedLocalNames, } from "./emit-server-shared.js"; import { emitOxcCompatObjectChildren, oxcServerStringReactNodeRenderHelperPlaceholder, setOxcServerStringUrlSafeHelperName, } from "./oxc-runtime-emit.js"; export interface EmitResult { code: string; imports: RuntimeImport[]; } export interface EmitServerOptions { dynamicAttributes?: "drop" | "emit"; escape?: ServerEscapeOptions | undefined; serverHydration?: boolean; } // Module-local handle to the URL-safety helper name for the current emit // call. Used by deeply-nested attribute emitters to avoid threading the // name through every signature. Reset at the top of `emitServer`. let currentRouterLinkComponentNames: ReadonlySet = new Set(); let currentUrlSafeHelperName: string = "_urlAttrSafe"; let currentClientBoundaryHelperName: string | undefined; let currentCompatChildHelperName: string | undefined; let currentSpreadAttributesHelperName: string = "_renderSpreadAttributes"; let currentSpreadPropsName: string = "_renderSpreadAttributes$props"; let currentSpreadSelectedValueName: string = "_renderSpreadAttributes$selected"; let currentMarkServerRenderValueHelperName: string = "_registerServerRenderValue"; let currentMarkServerRenderThunkHelperName = "_registerServerRenderThunk"; let currentRenderServerValueHelperName: string = "_renderServerValue"; let currentContainsServerRenderValueHelperName: string = "_containsServerRenderValue"; let currentServerRenderAttributeValueName: string = "_serverRenderAttributeValue"; let currentRenderServerChildHelperName: string = "_renderServerChild"; let currentJoinServerHtmlHelperName = "_joinServerHtml"; let currentAppendServerHtmlHelperName = "_appendServerHtml"; let currentPromiseAwareComposition = false; let currentSelectionParameterName: string = "_selectedValue"; let currentSelectionMultipleParameterName: string = "_selectedMultiple"; let currentOptionSelectedLocalNames: OptionSelectedLocalNames = { selected: "_selected", optionValue: "_optionValue", boundOptionValue: "_boundOptionValue", textValue: "_optionText", textParts: "_optionTextParts", textBody: "_optionTextBody", textHasValue: "_optionTextHasValue", selectValue: "_selectValue", selectValueAttribute: "_selectValueAttribute", selectDefaultValue: "_selectDefaultValue", selectMultiple: "_selectMultiple", attributes: "_optionAttributes", index: "_i", candidate: "_candidate", }; const serverSelectionContextKey = "mreact.server.selected-value"; const serverSelectionMultipleContextKey = "mreact.server.select-multiple"; const serverSelectionRenderValueKey = "mreact.server.selection-render-value"; /** * Selection expression of the nearest enclosing `` saves and restores it * around its own children. Threading it through the walker's parameter list would * mean touching ~25 recursion sites, and the stream emitter already carries the * same value on its `CollectHtmlState`. */ let currentSelectedValueCode: string | undefined; let currentSelectedMultipleCode: string | undefined; let currentSelectionContextActive = false; function withSelectedValueCode( selectedValueCode: string | undefined, selectedMultipleCode: string | undefined, emit: () => T, ): T { const previous = currentSelectedValueCode; const previousMultiple = currentSelectedMultipleCode; const previousActive = currentSelectionContextActive; currentSelectedValueCode = selectedValueCode; currentSelectedMultipleCode = selectedMultipleCode; currentSelectionContextActive = selectedValueCode !== undefined || selectedMultipleCode !== undefined; try { return emit(); } finally { currentSelectedValueCode = previous; currentSelectedMultipleCode = previousMultiple; currentSelectionContextActive = previousActive; } } export function emitServer(ir: ModuleIr, options: EmitServerOptions = {}): EmitResult { currentRouterLinkComponentNames = new Set(ir.routerLinkComponentNames ?? []); currentSelectedValueCode = undefined; currentSelectedMultipleCode = undefined; currentSelectionContextActive = false; const escapeHelperName = allocateEscapeHelperName(ir); const escapeBatchHelperName = options.escape === undefined ? undefined : allocateHelperName(ir, "_escapeHtmlBatch"); const contextProviderHelperName = usesContextProvider(ir) ? allocateHelperName(ir, "_renderContextProviderToString") : undefined; const contextConsumerHelperName = usesContextConsumer(ir) ? allocateHelperName(ir, "_renderContextConsumerToString") : undefined; const reactNodeRenderHelperName = usesReactNodeRender(ir) ? allocateHelperName(ir, "_renderReactNodeToString") : undefined; const clientBoundaryHelperName = usesClientBoundary(ir) ? allocateHelperFamilyName(ir, "_renderClientBoundary", [ "$hasNonSerializableProps", "$markChildren", "$compat", ]) : undefined; const compatChildHelperName = usesCompatChildRender(ir) ? allocateHelperName(ir, "_renderCompatChild") : undefined; const spreadAttributesHelperName = allocateHelperName(ir, "_renderSpreadAttributes"); const spreadPropsName = allocateNestedBindingSafeName(ir, `${spreadAttributesHelperName}$props`); const spreadSelectedValueName = allocateNestedBindingSafeName( ir, `${spreadAttributesHelperName}$selected`, ); const markServerRenderValueHelperName = allocateNestedBindingSafeName( ir, "_registerServerRenderValue", ); currentMarkServerRenderThunkHelperName = allocateNestedBindingSafeName( ir, "_registerServerRenderThunk", ); const renderServerValueHelperName = allocateNestedBindingSafeName(ir, "_renderServerValue"); const isServerRenderValueHelperName = allocateNestedBindingSafeName(ir, "_isServerRenderValue"); const readServerRenderValueHelperName = allocateNestedBindingSafeName( ir, "_readServerRenderValue", ); const containsServerRenderValueHelperName = allocateNestedBindingSafeName( ir, "_containsServerRenderValue", ); const serverRenderAttributeValueName = allocateNestedBindingSafeName( ir, "_serverRenderAttributeValue", ); const renderServerChildHelperName = allocateNestedBindingSafeName(ir, "_renderServerChild"); currentJoinServerHtmlHelperName = allocateNestedBindingSafeName(ir, "_joinServerHtml"); currentAppendServerHtmlHelperName = allocateNestedBindingSafeName(ir, "_appendServerHtml"); currentPromiseAwareComposition = false; const selectionParameterName = allocateNestedBindingSafeName(ir, "_selectedValue"); const selectionMultipleParameterName = allocateNestedBindingSafeName(ir, "_selectedMultiple"); currentSelectionParameterName = selectionParameterName; currentSelectionMultipleParameterName = selectionMultipleParameterName; currentOptionSelectedLocalNames = { selected: allocateNestedBindingSafeName(ir, "_selected"), optionValue: allocateNestedBindingSafeName(ir, "_optionValue"), boundOptionValue: allocateNestedBindingSafeName(ir, "_boundOptionValue"), textValue: allocateNestedBindingSafeName(ir, "_optionText"), textParts: allocateNestedBindingSafeName(ir, "_optionTextParts"), textBody: allocateNestedBindingSafeName(ir, "_optionTextBody"), textHasValue: allocateNestedBindingSafeName(ir, "_optionTextHasValue"), selectValue: allocateNestedBindingSafeName(ir, "_selectValue"), selectValueAttribute: allocateNestedBindingSafeName(ir, "_selectValueAttribute"), selectDefaultValue: allocateNestedBindingSafeName(ir, "_selectDefaultValue"), selectMultiple: allocateNestedBindingSafeName(ir, "_selectMultiple"), attributes: allocateNestedBindingSafeName(ir, "_optionAttributes"), index: allocateNestedBindingSafeName(ir, "_i"), candidate: allocateNestedBindingSafeName(ir, "_candidate"), }; const outAccumulatorName = allocateHelperName(ir, "_out"); const urlSafeHelperName = allocateHelperName(ir, "_urlAttrSafe"); currentUrlSafeHelperName = urlSafeHelperName; setOxcServerStringUrlSafeHelperName(urlSafeHelperName); currentClientBoundaryHelperName = clientBoundaryHelperName; currentCompatChildHelperName = compatChildHelperName; currentSpreadAttributesHelperName = spreadAttributesHelperName; currentSpreadPropsName = spreadPropsName; currentSpreadSelectedValueName = spreadSelectedValueName; currentMarkServerRenderValueHelperName = markServerRenderValueHelperName; currentRenderServerValueHelperName = renderServerValueHelperName; currentContainsServerRenderValueHelperName = containsServerRenderValueHelperName; currentServerRenderAttributeValueName = serverRenderAttributeValueName; currentRenderServerChildHelperName = renderServerChildHelperName; const helper = emitEscapeHtmlHelper(escapeHelperName); // Inline URL-scheme guard mirroring packages/server/src/url-safety.ts. // Returns the original value when safe to emit and undefined when the // attribute should be dropped. Inlined so compiler output stays free // of cross-package runtime imports. // Mirrors packages/server/src/url-safety.ts. Issue 078: in-scheme // tab/CR/LF must be stripped anywhere in the value, not just at the // start, to match the browser's URL parser. const urlSafeHelper = [ `function ${urlSafeHelperName}(name, value) {`, ` name = name.toLowerCase();`, ` value = String(value);`, ` if (name === "srcset" || name === "imagesrcset") {`, ` const _canonicalSet = value.replace(/^[\\x00-\\x20]+/u, "").replace(/[\\t\\r\\n]/g, "");`, ` for (const _candidate of _canonicalSet.split(",")) {`, ` const _url = (_candidate.trim().split(/\\s+/)[0] || "");`, ` if (_url !== "" && ${urlSafeHelperName}("src", _url) === undefined) return undefined;`, ` }`, ` return value;`, ` }`, ` const _canonical = value`, ` .replace(/^[\\x00-\\x20]+/u, "")`, ` .replace(/[\\t\\r\\n]/g, "");`, ` const _match = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(_canonical);`, ` if (_match === null) return value;`, ` const _scheme = _match[1].toLowerCase();`, ` if (_scheme !== "javascript" && _scheme !== "vbscript" && _scheme !== "livescript" && _scheme !== "mhtml" && _scheme !== "file" && _scheme !== "data") return value;`, ` if (_scheme === "data" && (name === "src" || name === "poster") && /^data:image\\/(?!svg\\+xml\\s*(?:[;,]|$))/i.test(_canonical)) return value;`, ` return undefined;`, `}`, ].join("\n"); const asyncComponentNames = collectAsyncServerComponentNames(ir.components); const components = ir.components .map((component) => { const emitted = emitComponent( component, escapeHelperName, escapeBatchHelperName, outAccumulatorName, options, asyncComponentNames, options.dynamicAttributes ?? "emit", contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName, selectionParameterName, selectionMultipleParameterName, ); return component.serverRenderValuePlaceholder === undefined ? emitted : emitted.replaceAll( component.serverRenderValuePlaceholder, markServerRenderValueHelperName, ); }) .join("\n\n"); const rawModuleStatements = emitModuleStatements(ir); const moduleStatements = ir.serverRenderValuePlaceholder === undefined ? rawModuleStatements : rawModuleStatements.replaceAll( ir.serverRenderValuePlaceholder, markServerRenderValueHelperName, ); const emittedServerCode = `${moduleStatements}\n${components}`; // Tree-shake the URL-safety helper when it is not referenced by any // component output. Same shape as the existing escapeImport check. const needsSpreadAttributesHelper = components.includes(spreadAttributesHelperName); const urlSafeBlock = components.includes(urlSafeHelperName) || needsSpreadAttributesHelper ? urlSafeHelper : ""; const needsServerRenderValue = emittedServerCode.includes(renderServerChildHelperName) || emittedServerCode.includes(currentMarkServerRenderThunkHelperName) || emittedServerCode.includes(markServerRenderValueHelperName) || emittedServerCode.includes(renderServerValueHelperName) || emittedServerCode.includes(isServerRenderValueHelperName) || emittedServerCode.includes(containsServerRenderValueHelperName); const clientBoundaryBlock = clientBoundaryHelperName === undefined || !components.includes(clientBoundaryHelperName) ? "" : emitClientBoundaryHelper( clientBoundaryHelperName, needsServerRenderValue ? isServerRenderValueHelperName : undefined, ); const spreadAttributesBlock = needsSpreadAttributesHelper ? emitSpreadAttributesHelper( spreadAttributesHelperName, escapeHelperName, urlSafeHelperName, needsServerRenderValue ? containsServerRenderValueHelperName : undefined, ) : ""; const serverRenderValueBlock = needsServerRenderValue ? emitServerRenderValueHelpers( isServerRenderValueHelperName, readServerRenderValueHelperName, containsServerRenderValueHelperName, renderServerValueHelperName, escapeHelperName, ) : ""; const serverChildBlock = emittedServerCode.includes(renderServerChildHelperName) ? emitServerChildHelper( renderServerChildHelperName, escapeHelperName, needsServerRenderValue ? isServerRenderValueHelperName : undefined, needsServerRenderValue ? readServerRenderValueHelperName : undefined, needsServerRenderValue ? renderServerValueHelperName : undefined, ) : ""; const serverRenderValueImport = serverRenderValueBlock === "" ? "" : `import { isServerRenderValue as ${isServerRenderValueHelperName}, readServerRenderValue as ${readServerRenderValueHelperName}, registerServerRenderValue as ${markServerRenderValueHelperName}, registerServerRenderThunk as ${currentMarkServerRenderThunkHelperName} } from "@reckona/mreact-shared/server-render-value-internal";`; // Emit batch escape import only when the helper is actually referenced // by the generated component code (issue 048: dead-import elimination). // Helper names are uniquely allocated, so a literal substring check is // both correct and inexpensive. const escapeImport = options.escape === undefined || escapeBatchHelperName === undefined || !components.includes(escapeBatchHelperName) ? "" : `import { ${options.escape.batchImportName} as ${escapeBatchHelperName} } from ${stringLiteral(options.escape.batchImportSource)};`; const userImports = emitUserImports(ir); const contextImport = emitContextImport( contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName, compatChildHelperName, ); const code = createCodeBuilder(); code.section(userImports); code.section(escapeImport); code.section(contextImport); code.section(serverRenderValueImport); code.section(moduleStatements); code.section(helper); code.section(urlSafeBlock); code.section(clientBoundaryBlock); code.section(spreadAttributesBlock); code.section(serverRenderValueBlock); code.section(serverChildBlock); if ( `${components}\n${serverChildBlock}\n${serverRenderValueBlock}`.includes( currentJoinServerHtmlHelperName, ) || components.includes(currentAppendServerHtmlHelperName) ) code.section(emitJoinServerHtmlHelper(currentJoinServerHtmlHelperName)); code.section(components); return { code: code.toString(), imports: [ ...collectContextImports( contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName, compatChildHelperName, ), ...(serverRenderValueBlock === "" ? [] : [ { source: "@reckona/mreact-shared/server-render-value-internal", specifiers: [ "isServerRenderValue", "readServerRenderValue", "registerServerRenderValue", "registerServerRenderThunk", ], }, ]), ], }; } function emitContextImport( contextProviderHelperName: string | undefined, contextConsumerHelperName: string | undefined, reactNodeRenderHelperName: string | undefined, compatChildHelperName?: string | undefined, ): string { const specifiers = [ reactNodeRenderHelperName === undefined ? undefined : `renderToString as ${reactNodeRenderHelperName}`, compatChildHelperName === undefined ? undefined : `renderChildToString as ${compatChildHelperName}`, contextProviderHelperName === undefined ? undefined : `renderContextProviderToString as ${contextProviderHelperName}`, contextConsumerHelperName === undefined ? undefined : `renderContextConsumerToString as ${contextConsumerHelperName}`, ].filter((specifier): specifier is string => specifier !== undefined); return specifiers.length === 0 ? "" : `import { ${specifiers.join(", ")} } from "@reckona/mreact-compat";`; } function collectContextImports( contextProviderHelperName: string | undefined, contextConsumerHelperName: string | undefined, reactNodeRenderHelperName?: string, compatChildHelperName?: string, ): RuntimeImport[] { const specifiers = [ reactNodeRenderHelperName === undefined ? undefined : "renderToString", contextProviderHelperName === undefined ? undefined : "renderContextProviderToString", contextConsumerHelperName === undefined ? undefined : "renderContextConsumerToString", compatChildHelperName === undefined ? undefined : "renderChildToString", ].filter((specifier): specifier is string => specifier !== undefined); return specifiers.length === 0 ? [] : [{ source: "@reckona/mreact-compat", specifiers }]; } function emitUserImports(ir: ModuleIr): string { return ir.userImports.join("\n"); } function emitModuleStatements(ir: ModuleIr): string { return ir.moduleStatements.join("\n"); } function emitComponent( component: ComponentIr, escapeHelperName: string, escapeBatchHelperName: string | undefined, outAccumulatorName: string, options: EmitServerOptions, asyncComponentNames: ReadonlySet, dynamicAttributes: "drop" | "emit", contextProviderHelperName?: string, contextConsumerHelperName?: string, reactNodeRenderHelperName?: string, selectionParameterName?: string, selectionMultipleParameterName?: string, ): string { const body = component.bodyStatements.map( (statement) => ` ${replaceOxcServerStringReactNodeRenderHelper(statement, reactNodeRenderHelperName)}`, ); const parameters = component.parameters.join(", "); const selectionContextDeclaration = [ ` const ${selectionParameterName} = arguments[0]?.[Symbol.for(${JSON.stringify(serverSelectionContextKey)})];`, ` const ${selectionMultipleParameterName} = arguments[0]?.[Symbol.for(${JSON.stringify(serverSelectionMultipleContextKey)})];`, ]; const promiseAware = mayRenderDeferredChildren(component.root); const previousPromiseAware = currentPromiseAwareComposition; currentPromiseAwareComposition = promiseAware; const collect = () => collectHtmlStatements( component.root, outAccumulatorName, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName, ); const previousSelectedValueCode = currentSelectedValueCode; const previousSelectedMultipleCode = currentSelectedMultipleCode; const previousSelectionContextActive = currentSelectionContextActive; currentSelectedValueCode = selectionParameterName; currentSelectedMultipleCode = selectionMultipleParameterName; currentSelectionContextActive = false; let htmlStatements: string[]; try { htmlStatements = collect(); } finally { currentSelectedValueCode = previousSelectedValueCode; currentSelectedMultipleCode = previousSelectedMultipleCode; currentSelectionContextActive = previousSelectionContextActive; currentPromiseAwareComposition = previousPromiseAware; } const markerStart = stringLiteral(``); const markerEnd = stringLiteral(``); const hydrationOpenStatements = options.serverHydration === true ? [` ${emitHtmlAppend(outAccumulatorName, markerStart, promiseAware)}`] : []; const hydrationCloseStatements = options.serverHydration === true ? [` ${emitHtmlAppend(outAccumulatorName, markerEnd, promiseAware)}`] : []; const functionKeyword = `${component.exportDefault === true ? "export default " : component.exported === false ? "" : "export "}${ asyncComponentNames.has(component.name) ? "async " : "" }function`; return [ `${functionKeyword} ${component.name}(${parameters}) {`, ...selectionContextDeclaration, ...body, ` let ${outAccumulatorName} = "";`, ...hydrationOpenStatements, ...htmlStatements.map((statement) => ` ${statement}`), ...hydrationCloseStatements, ` return ${outAccumulatorName};`, `}`, ].join("\n"); } function emitHtmlExpression( node: JsxNodeIr, escapeHelperName: string, escapeBatchHelperName: string | undefined, asyncComponentNames: ReadonlySet, dynamicAttributes: "drop" | "emit", contextProviderHelperName?: string, contextConsumerHelperName?: string, reactNodeRenderHelperName?: string, ): string { const parts = collectHtmlParts( node, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName, ); if (parts.length === 0) { return '""'; } return joinHtmlExpressions(parts, mayRenderDeferredChildren(node)); } /** * Statement-list IR walker (issue 046 followup). Produces a sequence of * statements that each append to a shared accumulator variable instead of * a single concat expression. Used at the top of component bodies; sub * callbacks (`renderContextProviderToString`, async list renderers, etc.) * still use the expression form via `emitHtmlExpression`. * * The benefits over expression mode: * - intermediate string allocations from `+ +` chains disappear * - conditional branches lower to `if/else` (no ternary expression spaghetti) * - sync list rendering inlines the for-loop append without an IIFE wrapper * - debugger / source maps step naturally over the generated statements */ function collectHtmlStatements( node: JsxNodeIr, outVar: string, escapeHelperName: string, escapeBatchHelperName: string | undefined, asyncComponentNames: ReadonlySet, dynamicAttributes: "drop" | "emit", contextProviderHelperName?: string, contextConsumerHelperName?: string, reactNodeRenderHelperName?: string, ): string[] { if (node.kind === "text") { const literal = escapeHtml(node.value); if (literal === "") { return []; } return [emitHtmlAppend(outVar, `${stringLiteral(literal)}`)]; } if (node.kind === "expr") { if (isChildrenExpressionCode(node.code)) { return [ emitHtmlAppend( outVar, `${currentRenderServerChildHelperName}(${node.code}, ${currentSelectedValueCode ?? "undefined"}, ${currentSelectedMultipleCode ?? "undefined"})`, ), ]; } if (node.renderMode === "html") { return [emitHtmlAppend(outVar, `${rawHtmlExpression(node.code)}`)]; } if (node.renderMode === "react-node" && reactNodeRenderHelperName !== undefined) { return [emitHtmlAppend(outVar, `${reactNodeRenderHelperName}(() => (${node.code}))`)]; } if (node.renderMode === "compat-child" && currentCompatChildHelperName !== undefined) { return [emitHtmlAppend(outVar, `${currentCompatChildHelperName}(${node.code})`)]; } if (node.renderMode === "server-render-value") { return [ emitHtmlAppend( outVar, `${currentRenderServerValueHelperName}(${node.code}, 0, ${currentSelectedValueCode ?? "undefined"}, ${currentSelectedMultipleCode ?? "undefined"})`, ), ]; } return [emitHtmlAppend(outVar, `${escapeHelperName}(${node.code})`)]; } if (node.kind === "conditional") { const conditionCode = node.conditionValueName === undefined ? node.conditionCode : (node.conditionTestCode ?? node.conditionValueName); const whenTrueStatements = node.whenTrue.flatMap((child) => collectHtmlStatements( child, outVar, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName, ), ); const whenFalseStatements = node.whenFalse.flatMap((child) => collectHtmlStatements( child, outVar, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName, ), ); if (whenTrueStatements.length === 0 && whenFalseStatements.length === 0) { return []; } let statements: string[]; if (whenFalseStatements.length === 0) { statements = [ `if (${conditionCode}) {`, ...whenTrueStatements.map((statement) => ` ${statement}`), `}`, ]; } else if (whenTrueStatements.length === 0) { statements = [ `if (!(${conditionCode})) {`, ...whenFalseStatements.map((statement) => ` ${statement}`), `}`, ]; } else { statements = [ `if (${conditionCode}) {`, ...whenTrueStatements.map((statement) => ` ${statement}`), `} else {`, ...whenFalseStatements.map((statement) => ` ${statement}`), `}`, ]; } if (node.conditionValueName === undefined) { return statements; } return [ `{`, ` const ${node.conditionValueName} = (${node.conditionCode});`, ...statements.map((statement) => ` ${statement}`), `}`, ]; } if (node.kind === "list") { const isAsync = containsAsyncServerOperationInChildren(node.children, asyncComponentNames); if (isAsync) { // Parallel async path keeps the existing renderer + Promise.all + join // form to preserve concurrent resolution semantics. const parameters = emitListParameters(node); const renderer = emitListRenderer( node, parameters, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName, ); const mapped = `(${node.itemsCode}).map(${renderer})`; return [emitHtmlAppend(outVar, `(await Promise.all(${mapped})).join("")`)]; } // Sync list — inline for-loop appending to the caller's accumulator. // No inner IIFE wrapper and no intermediate string concat per iteration. const itemPattern = node.parameterPatterns === undefined ? node.itemName : node.parameterPatterns[0]; const itemBinding = itemPattern === undefined ? undefined : `const ${itemPattern} = _arr[_i];`; const indexPattern = node.parameterPatterns?.[1] ?? node.indexName; const arrayPattern = node.parameterPatterns?.[2] ?? node.arrayName; const indexBinding = indexPattern === undefined ? undefined : `const ${indexPattern} = _i;`; const arrayBinding = arrayPattern === undefined ? undefined : `const ${arrayPattern} = _arr;`; const bodyStatements = node.bodyStatements ?? []; const childStatements = node.children.flatMap((child) => collectHtmlStatements( child, outVar, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName, ), ); return [ `{`, ` const _arr = (${node.itemsCode});`, ` for (let _i = 0, _len = _arr.length; _i < _len; _i++) {`, ...(itemBinding === undefined ? [] : [` ${itemBinding}`]), ...(indexBinding === undefined ? [] : [` ${indexBinding}`]), ...(arrayBinding === undefined ? [] : [` ${arrayBinding}`]), ...bodyStatements.map((statement) => ` ${statement}`), ...childStatements.map((statement) => ` ${statement}`), ` }`, `}`, ]; } if (node.kind === "fragment") { return node.children.flatMap((child) => collectHtmlStatements( child, outVar, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName, ), ); } if (node.kind === "component") { if (node.name === "Suspense") { return [ emitHtmlAppend(outVar, `""`), ...node.children.flatMap((child) => collectHtmlStatements( child, outVar, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName, ), ), emitHtmlAppend(outVar, `""`), ]; } if (contextProviderHelperName !== undefined && node.name.endsWith(".Provider")) { // Provider helper takes a string-returning callback. Use the // expression form inside the callback to preserve the existing // helper contract. const valueCode = findComponentPropCode(node.props, "value") ?? "undefined"; return [ emitHtmlAppend( outVar, `${contextProviderHelperName}(${node.name}, ${valueCode}, () => ${emitHtmlExpressionFromChildren(node.children, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName)})`, ), ]; } if (contextConsumerHelperName !== undefined && node.name.endsWith(".Consumer")) { const renderProp = findComponentRenderProp(node.props, "children"); if (renderProp !== undefined) { const valueName = renderProp.valueName ?? "_value"; return [ emitHtmlAppend( outVar, `${contextConsumerHelperName}(${node.name}, (${valueName}) => ${emitHtmlExpressionFromChildren(renderProp.children, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName)})`, ), ]; } } if (isClientBoundaryPlaceholder(node)) { const helperName = currentClientBoundaryHelperName; if (helperName !== undefined) { const hasComponentFallback = shouldRenderClientBoundaryFallback(node); const boundaryProps = emitPropsObject( node.props, [], escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName, undefined, undefined, false, ); const fallbackHtml = node.clientReference?.compatSsr === true ? `(_childrenHtml, _identifierPrefix, _props) => ${reactNodeRenderHelperName}(${node.name}, _props, { identifierPrefix: _identifierPrefix, stringResult: "text" })` : hasComponentFallback ? `(_childrenHtml) => ${emitComponentCallExpression( node.name, emitPropsObject( node.props, node.children, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName, node.name, "_childrenHtml", false, ), asyncComponentNames, )}` : emitHtmlExpressionFromChildren( node.children, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName, ); const originalChildrenHtml = hasComponentFallback ? emitHtmlExpressionFromChildren( node.children, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName, ) : undefined; return [ emitHtmlAppend( outVar, `${helperName}(${stringLiteral(node.name)}, ${boundaryProps}, ${fallbackHtml}${originalChildrenHtml === undefined ? "" : `, true, ${originalChildrenHtml}, ${node.children.length > 0}${node.clientReference?.compatSsr === true ? ", true" : ""}`})`, ), ]; } return [emitHtmlAppend(outVar, `${stringLiteral(clientBoundaryPlaceholder(node))}`)]; } if (node.runtime === "compat" && reactNodeRenderHelperName !== undefined) { return [ emitHtmlAppend( outVar, `${reactNodeRenderHelperName}(${node.name}, ${emitCompatRuntimePropsObject( node.props, node.children, currentSelectedValueCode, currentSelectedMultipleCode, )})`, ), ]; } return [ emitHtmlAppend( outVar, `${emitComponentCallExpression( node.name, emitPropsObject( node.props, node.children, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName, node.name, ), asyncComponentNames, )}`, ), ]; } if (node.kind === "async-boundary") { return []; } // element const statements: string[] = []; if (node.tagName === "textarea") { const attributeScan = scanElementAttributes(node.tagName, node.attributes); statements.push(emitHtmlAppend(outVar, `${stringLiteral(""`)); for (const valuePart of collectTextareaValueParts( node, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName, attributeScan, )) { statements.push(emitHtmlAppend(outVar, `${valuePart}`)); } statements.push(emitHtmlAppend(outVar, `""`)); return statements; } const attributeScan = scanElementAttributes(node.tagName, node.attributes); if (hasDynamicSelectSelectionAttribute(node)) { return [ emitHtmlAppend( outVar, `${emitBoundSelectExpression( node, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName, attributeScan, )}`, ), ]; } if ( dynamicAttributes === "emit" && !isVoidHtmlElement(node.tagName) && node.attributes.some((attr) => attr.kind === "spread-attr") ) { const capturedOptionText = currentSelectedValueCode === undefined || node.attributes.some( (attr) => attr.kind !== "spread-attr" && attr.name === "dangerouslySetInnerHTML", ) ? undefined : findCapturedOptionText(node, escapeHelperName); const selectedAttributePart = collectOptionSelectedAttributePart( node, true, capturedOptionText?.valueCode, ); statements.push( emitHtmlAppend( outVar, `${emitMergedSpreadElementExpression( node.tagName, node.attributes, attributeScan, withSelectedValueCode( selectedValueCodeForChildren(node, attributeScan), selectedMultipleCodeForChildren(node, attributeScan), () => capturedOptionText === undefined ? emitHtmlExpressionFromChildren( node.children, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName, ) : capturedOptionText.bodyCode, ), selectedAttributePart, containsAsyncServerOperationInChildren(node.children, asyncComponentNames), capturedOptionText, )}`, ), ); return statements; } const dynamicOptionValueAttribute = findDynamicOptionValueAttribute(node); if (dynamicOptionValueAttribute !== undefined && currentSelectedValueCode !== undefined) { return [ emitHtmlAppend( outVar, `${emitBoundOptionValueExpression( node, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName, attributeScan, dynamicOptionValueAttribute, )}`, ), ]; } const capturedOptionExpression = emitCapturedOptionExpression( node, escapeHelperName, escapeBatchHelperName, dynamicAttributes, attributeScan, ); if (capturedOptionExpression !== undefined) { return [emitHtmlAppend(outVar, `${capturedOptionExpression}`)]; } statements.push(emitHtmlAppend(outVar, `${stringLiteral(`<${node.tagName}`)}`)); for (const attributePart of collectElementAttributeParts( node.tagName, node.attributes, escapeHelperName, escapeBatchHelperName, dynamicAttributes, attributeScan, )) { statements.push(emitHtmlAppend(outVar, `${attributePart}`)); } const selectedAttributePart = collectOptionSelectedAttributePart(node); if (selectedAttributePart !== undefined) { statements.push(emitHtmlAppend(outVar, `${selectedAttributePart}`)); } statements.push(emitHtmlAppend(outVar, `">"`)); if (isVoidHtmlElement(node.tagName)) { return statements; } const childSelectedValueCode = selectedValueCodeForChildren(node, attributeScan); const childSelectedMultipleCode = selectedMultipleCodeForChildren(node, attributeScan); const dangerousInnerHtml = emitDangerouslySetInnerHtmlExpression( node.attributes, emitHtmlExpressionFromChildren( node.children, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName, ), ); if (dangerousInnerHtml !== undefined) { statements.push(emitHtmlAppend(outVar, `${dangerousInnerHtml}`)); statements.push(emitHtmlAppend(outVar, `${stringLiteral(``)}`)); return statements; } const childrenExpression = emitTextSeparatedSimpleChildrenExpression( node.children, escapeHelperName, escapeBatchHelperName, ); if ( childrenExpression !== undefined && !(node.tagName === "select" && attributeScan.formValueAttributeCode !== undefined) ) { statements.push(emitHtmlAppend(outVar, `${childrenExpression}`)); } else { withSelectedValueCode(childSelectedValueCode, childSelectedMultipleCode, () => { for (const child of node.children) { statements.push( ...collectHtmlStatements( child, outVar, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName, ), ); } }); } statements.push(emitHtmlAppend(outVar, `${stringLiteral(``)}`)); return statements; } function hasDynamicSelectSelectionAttribute( node: Extract, ): boolean { return ( node.tagName === "select" && !node.attributes.some((attr) => attr.kind === "spread-attr") && node.attributes.some( (attr) => attr.kind === "dynamic-attr" && (attr.name === "value" || attr.name === "defaultValue"), ) ); } function findDynamicOptionValueAttribute( node: Extract, ): Extract | undefined { if (node.tagName !== "option") return undefined; const valueAttribute = node.attributes.find( (attr) => attr.kind !== "spread-attr" && attr.name === "value", ); return valueAttribute?.kind === "dynamic-attr" ? valueAttribute : undefined; } function emitBoundSelectExpression( node: Extract, escapeHelperName: string, escapeBatchHelperName: string | undefined, asyncComponentNames: ReadonlySet, dynamicAttributes: "drop" | "emit", contextProviderHelperName: string | undefined, contextConsumerHelperName: string | undefined, reactNodeRenderHelperName: string | undefined, attributeScan: ElementAttributeScan, ): string { if (attributeScan.formValueAttributeCode === undefined) { return '""'; } const selectValueName = currentOptionSelectedLocalNames.selectValue; const selectionCode = emitSelectSelectionValueCode( currentOptionSelectedLocalNames.selectValueAttribute, currentOptionSelectedLocalNames.selectDefaultValue, ) ?? "undefined"; const attributeSetup = emitBoundSelectAttributeSetup( node, escapeHelperName, escapeBatchHelperName, dynamicAttributes, ); const selectedMultipleCode = attributeScan.multipleAttributeCode === undefined ? undefined : currentOptionSelectedLocalNames.selectMultiple; const childrenHtml = withSelectedValueCode(selectValueName, selectedMultipleCode, () => emitHtmlExpressionFromChildren( node.children, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName, ), ); const innerHtml = emitDangerouslySetInnerHtmlExpression(node.attributes, childrenHtml) ?? childrenHtml; const isAsync = containsAsyncServerOperationInChildren(node.children, asyncComponentNames); const invocation = `${isAsync ? "(async () =>" : "(() =>"} { ${attributeSetup} const ${selectValueName} = (${selectionCode}); return ${joinHtmlExpressions([`${stringLiteral(""`, `(${innerHtml})`, stringLiteral("")], mayRenderDeferredChildren(node))}; })()`; return isAsync ? `(await ${invocation})` : invocation; } function emitBoundSelectAttributeSetup( node: Extract, escapeHelperName: string, escapeBatchHelperName: string | undefined, dynamicAttributes: "drop" | "emit", ): string { const attributesName = currentOptionSelectedLocalNames.attributes; const valueAttributeName = currentOptionSelectedLocalNames.selectValueAttribute; const defaultValueName = currentOptionSelectedLocalNames.selectDefaultValue; const multipleName = currentOptionSelectedLocalNames.selectMultiple; const statements = [ `let ${attributesName} = "";`, `let ${valueAttributeName};`, `let ${defaultValueName};`, `let ${multipleName};`, ]; for (const attr of node.attributes) { if (attr.kind !== "spread-attr" && (attr.name === "value" || attr.name === "defaultValue")) { const valueCode = readFormValueAttributeCode(attr); if (valueCode !== undefined) { statements.push( `${attr.name === "value" ? valueAttributeName : defaultValueName} = ${valueCode};`, ); } continue; } if (attr.kind !== "spread-attr" && attr.name === "multiple") { const multipleCode = readBooleanAttributeCode(attr); const parts = collectHtmlAttributeParts( node.tagName, attr, escapeHelperName, escapeBatchHelperName, dynamicAttributes, attr.kind === "dynamic-attr" ? `(${multipleName} = (${attr.code}))` : undefined, ); statements.push(...parts.map((part) => `${attributesName} += ${part};`)); if ( multipleCode !== undefined && (dynamicAttributes === "drop" || attr.kind !== "dynamic-attr") ) { statements.push(`${multipleName} = ${multipleCode};`); } continue; } const parts = collectHtmlAttributeParts( node.tagName, attr, escapeHelperName, escapeBatchHelperName, dynamicAttributes, ); statements.push(...parts.map((part) => `${attributesName} += ${part};`)); } return statements.join(" "); } function emitBoundOptionValueExpression( node: Extract, escapeHelperName: string, escapeBatchHelperName: string | undefined, asyncComponentNames: ReadonlySet, dynamicAttributes: "drop" | "emit", contextProviderHelperName: string | undefined, contextConsumerHelperName: string | undefined, reactNodeRenderHelperName: string | undefined, attributeScan: ElementAttributeScan, valueAttribute: Extract, ): string { const boundValueName = currentOptionSelectedLocalNames.boundOptionValue; const capturedOptionText = node.attributes.some( (attr) => attr.kind !== "spread-attr" && attr.name === "dangerouslySetInnerHTML", ) ? undefined : findCapturedOptionText(node, escapeHelperName); const attributes = collectElementAttributeParts( node.tagName, node.attributes, escapeHelperName, escapeBatchHelperName, dynamicAttributes, attributeScan, valueAttribute, `(${boundValueName} = (${valueAttribute.code}))`, ); const attributesCode = attributes.length === 0 ? '""' : attributes.join(" + "); const boundValueInitialization = dynamicAttributes === "drop" ? `${boundValueName} = (${valueAttribute.code});` : ""; const selectedAttribute = collectOptionSelectedAttributePart( node, false, capturedOptionText?.valueCode, boundValueName, ) ?? '""'; const innerHtml = capturedOptionText?.bodyCode ?? emitDangerouslySetInnerHtmlExpression( node.attributes, emitHtmlExpressionFromChildren( node.children, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName, ), ) ?? emitHtmlExpressionFromChildren( node.children, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName, ); const optionTextDeclaration = capturedOptionText?.declaration ?? ""; const isAsync = containsAsyncServerOperationInChildren(node.children, asyncComponentNames); const invocation = `${isAsync ? "(async () =>" : "(() =>"} { let ${boundValueName}; const ${currentOptionSelectedLocalNames.attributes} = ${attributesCode}; ${boundValueInitialization} ${optionTextDeclaration} return ${joinHtmlExpressions([stringLiteral(""), `(${innerHtml})`, stringLiteral("")], mayRenderDeferredChildren(node))}; })()`; return isAsync ? `(await ${invocation})` : invocation; } /** * Selection expression that this element's descendants compare against. A * `` * has none, so its attribute is dropped from the normal attribute list (see * `isSuppressedOptionSelectedAttribute`) and re-emitted here as the fallback * branch. Without that, a stale `selected` would survive next to the match. */ function emitOwnSelectedFallbackCode(node: Extract): string { const selectedAttr = node.attributes.find( (attr) => attr.kind !== "spread-attr" && attr.name === "selected", ); if (selectedAttr === undefined || selectedAttr.kind === "spread-attr") { return '""'; } if (selectedAttr.kind === "static-attr") { return stringLiteral(' selected=""'); } if (selectedAttr.kind !== "dynamic-attr") { return '""'; } return `((_own) => _own == null || _own === false ? "" : ${stringLiteral(' selected=""')})(${selectedAttr.code})`; } /** * True while an `