import { emitCompatSsrBoundaryHelper } from "./emit-compat-ssr-boundary.js"; import type { AsyncBoundaryIr, AttributeIr, ComponentPropIr, ComponentIr, JsxNodeIr, ModuleIr, } from "./ir.js"; import type { RuntimeImport, ServerBootstrapMode, ServerEscapeOptions } from "./types.js"; import { emitEscapeHtmlHelper } from "./emit-escape-helper.js"; import { createCodeBuilder } from "./emit-code-builder.js"; import { emitAsyncBoundary as emitLoweredAsyncBoundary, emitOutOfOrderBoundary as emitLoweredOutOfOrderBoundary, emitReactSuspenseBoundary as emitLoweredReactSuspenseBoundary, emitReactSuspenseOutOfOrderBoundary as emitLoweredReactSuspenseOutOfOrderBoundary, } from "./emit-boundary-lowering.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 EmitServerStreamResult { code: string; imports: RuntimeImport[]; } export interface EmitServerStreamOptions { dynamicAttributes?: "drop" | "emit"; serverBootstrap?: ServerBootstrapMode; serverBootstrapNonce?: string; serverBootstrapSrc?: string; serverHydration?: boolean; serverAwaitHydration?: boolean; escape?: ServerEscapeOptions | undefined; reactSuspenseRevealScriptSrc?: string; } let currentRouterLinkComponentNames: ReadonlySet = new Set(); let currentUrlSafeHelperName: string = "_urlAttrSafe"; let currentClientBoundaryHelperName: string | undefined; let currentClientBoundaryFallbackSinkName: string = "_clientBoundaryFallbackSink"; let currentSpreadAttributesHelperName: string = "_renderSpreadAttributes"; let currentSpreadPropsName: string = "_renderSpreadAttributes$props"; let currentSpreadSelectedValueName: string = "_renderSpreadAttributes$selected"; let currentStreamNodeHelperName: string = "_renderStreamNode"; let currentAsyncBoundaryHelperName: string = "_renderAsyncBoundary"; let currentOutOfOrderBoundaryHelperName: string = "_renderOutOfOrderBoundary"; let currentReactSuspenseBoundaryHelperName: string = "_renderReactSuspenseBoundary"; let currentReactSuspenseOutOfOrderBoundaryHelperName: string = "_renderReactSuspenseOutOfOrderBoundary"; let currentCompatRenderToStringHelperName: string = "_renderCompatToString"; let currentCompatChildHelperName: string | undefined; let currentPropChildrenCollectState: CollectHtmlState | undefined; let currentMarkServerRenderValueHelperName: string = "_registerServerRenderValue"; let currentMarkServerRenderThunkHelperName = "_registerServerRenderThunk"; let currentRenderServerValueHelperName: string = "_renderServerValue"; let currentContainsServerRenderValueHelperName: string = "_containsServerRenderValue"; let currentServerRenderValueSinkName: string = "$sink"; let currentServerRenderAttributeValueName: string = "_serverRenderAttributeValue"; 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"; export function emitServerStream( ir: ModuleIr, options: EmitServerStreamOptions = {}, ): EmitServerStreamResult { currentRouterLinkComponentNames = new Set(ir.routerLinkComponentNames ?? []); const serverBootstrap = options.serverBootstrap ?? "none"; const escapeHelperName = allocateHelperName(ir, "_escapeHtml"); const escapeBatchHelperName = options.escape === undefined ? undefined : allocateHelperName(ir, "_escapeHtmlBatch"); const asyncBoundaryHelperName = allocateHelperName(ir, "_renderAsyncBoundary"); const outOfOrderBoundaryHelperName = allocateHelperName(ir, "_renderOutOfOrderBoundary"); const reorderScriptHelperName = allocateHelperName(ir, "_renderOutOfOrderReorderScript"); const reactSuspenseBoundaryHelperName = allocateHelperName(ir, "_renderReactSuspenseBoundary"); const reactSuspenseOutOfOrderBoundaryHelperName = allocateHelperName( ir, "_renderReactSuspenseOutOfOrderBoundary", ); const compatRenderToStringHelperName = allocateHelperName(ir, "_renderCompatToString"); const compatChildHelperName = usesCompatChildRender(ir) ? allocateHelperName(ir, "_renderCompatChild") : undefined; const streamNodeHelperName = allocateHelperName(ir, "_renderStreamNode"); const clientBoundaryHelperName = usesClientBoundary(ir, options.serverHydration === true) ? allocateHelperFamilyName(ir, "_renderClientBoundary", [ "$hasNonSerializableProps", "$markChildren", "$compat", "$renderHtml", ]) : undefined; const clientBoundaryFallbackSinkName = allocateNestedBindingSafeName( ir, "_clientBoundaryFallbackSink", ); 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 serverRenderValueSinkName = allocateNestedBindingSafeName(ir, "$sink"); const serverRenderAttributeValueName = allocateNestedBindingSafeName( ir, "_serverRenderAttributeValue", ); 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 selectionParameterName = allocateNestedBindingSafeName(ir, "_selectedValue"); const selectionMultipleParameterName = allocateNestedBindingSafeName(ir, "_selectedMultiple"); currentSelectionParameterName = selectionParameterName; currentSelectionMultipleParameterName = selectionMultipleParameterName; const urlSafeHelperName = allocateHelperName(ir, "_urlAttrSafe"); currentUrlSafeHelperName = urlSafeHelperName; setOxcServerStringUrlSafeHelperName(urlSafeHelperName); currentClientBoundaryHelperName = clientBoundaryHelperName; currentClientBoundaryFallbackSinkName = clientBoundaryFallbackSinkName; currentSpreadAttributesHelperName = spreadAttributesHelperName; currentSpreadPropsName = spreadPropsName; currentSpreadSelectedValueName = spreadSelectedValueName; currentStreamNodeHelperName = streamNodeHelperName; currentAsyncBoundaryHelperName = asyncBoundaryHelperName; currentOutOfOrderBoundaryHelperName = outOfOrderBoundaryHelperName; currentReactSuspenseBoundaryHelperName = reactSuspenseBoundaryHelperName; currentReactSuspenseOutOfOrderBoundaryHelperName = reactSuspenseOutOfOrderBoundaryHelperName; currentCompatRenderToStringHelperName = compatRenderToStringHelperName; currentCompatChildHelperName = compatChildHelperName; currentMarkServerRenderValueHelperName = markServerRenderValueHelperName; currentRenderServerValueHelperName = renderServerValueHelperName; currentContainsServerRenderValueHelperName = containsServerRenderValueHelperName; currentServerRenderValueSinkName = serverRenderValueSinkName; currentServerRenderAttributeValueName = serverRenderAttributeValueName; const helper = emitEscapeHtmlHelper(escapeHelperName); 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 components = ir.components .map((component) => { const emitted = emitComponent( component, escapeHelperName, asyncBoundaryHelperName, outOfOrderBoundaryHelperName, reorderScriptHelperName, reactSuspenseBoundaryHelperName, reactSuspenseOutOfOrderBoundaryHelperName, compatRenderToStringHelperName, { serverBootstrap, ...(options.serverBootstrapNonce === undefined ? {} : { serverBootstrapNonce: options.serverBootstrapNonce }), ...(options.serverBootstrapSrc === undefined ? {} : { serverBootstrapSrc: options.serverBootstrapSrc }), ...(options.serverHydration === undefined ? {} : { serverHydration: options.serverHydration }), ...(options.serverAwaitHydration === undefined ? {} : { serverAwaitHydration: options.serverAwaitHydration }), ...(options.reactSuspenseRevealScriptSrc === undefined ? {} : { reactSuspenseRevealScriptSrc: options.reactSuspenseRevealScriptSrc }), dynamicAttributes: options.dynamicAttributes ?? "emit", ...(escapeBatchHelperName === undefined ? {} : { escapeBatchHelperName }), ...(selectionParameterName === undefined ? {} : { selectionParameterName }), ...(selectionMultipleParameterName === undefined ? {} : { 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}`; // Emit batch escape import only when the helper is actually referenced // (issue 048: dead-import elimination). const escapeImport = options.escape === undefined || escapeBatchHelperName === undefined || !components.includes(escapeBatchHelperName) ? "" : `import { ${options.escape.batchImportName} as ${escapeBatchHelperName} } from ${stringLiteral(options.escape.batchImportSource)};`; const imports = collectImports(ir, serverBootstrap); const importAliases: Record = { renderAsyncBoundary: asyncBoundaryHelperName, renderOutOfOrderBoundary: outOfOrderBoundaryHelperName, renderOutOfOrderReorderScript: reorderScriptHelperName, renderReactSuspenseBoundary: reactSuspenseBoundaryHelperName, renderReactSuspenseOutOfOrderBoundary: reactSuspenseOutOfOrderBoundaryHelperName, renderToString: compatRenderToStringHelperName, ...(compatChildHelperName === undefined ? {} : { renderChildToString: compatChildHelperName }), }; const importLine = imports .map( (runtimeImport) => `import { ${runtimeImport.specifiers .map((specifier) => `${specifier} as ${importAliases[specifier]}`) .join(", ")} } from "${runtimeImport.source}";`, ) .join("\n"); const userImports = emitUserImports(ir); const importsBlock = [importLine, escapeImport, userImports, moduleStatements] .filter(Boolean) .join("\n"); const needsSpreadAttributesHelper = components.includes(spreadAttributesHelperName); const urlSafeBlock = components.includes(urlSafeHelperName) || needsSpreadAttributesHelper ? urlSafeHelper : ""; const needsServerRenderValue = components.includes(streamNodeHelperName) || 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 streamNodeBlock = components.includes(streamNodeHelperName) ? emitStreamNodeHelper( streamNodeHelperName, isServerRenderValueHelperName, renderServerValueHelperName, ) : ""; const serverRenderValueBlock = needsServerRenderValue ? emitServerRenderValueHelpers( isServerRenderValueHelperName, readServerRenderValueHelperName, containsServerRenderValueHelperName, renderServerValueHelperName, ) : ""; const serverRenderValueImport = serverRenderValueBlock === "" ? "" : `import { isServerRenderValue as ${isServerRenderValueHelperName}, readServerRenderValue as ${readServerRenderValueHelperName}, registerServerRenderValue as ${markServerRenderValueHelperName}, registerServerRenderThunk as ${currentMarkServerRenderThunkHelperName} } from "@reckona/mreact-shared/server-render-value-internal";`; const code = createCodeBuilder(); code.section(serverRenderValueImport); code.section(importsBlock); code.section(helper); code.section(urlSafeBlock); code.section(clientBoundaryBlock); code.section(spreadAttributesBlock); code.section(serverRenderValueBlock); code.section(streamNodeBlock); code.section(components); return { code: code.toString(), imports: [ ...imports, ...(serverRenderValueBlock === "" ? [] : [ { source: "@reckona/mreact-shared/server-render-value-internal", specifiers: [ "isServerRenderValue", "readServerRenderValue", "registerServerRenderValue", "registerServerRenderThunk", ], }, ]), ], }; } 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, serverBootstrap: ServerBootstrapMode): RuntimeImport[] { const serverSpecifiers = [ ...(hasInOrderAsyncBoundary(ir) ? ["renderAsyncBoundary"] : []), ...(hasOutOfOrderAsyncBoundary(ir) ? ["renderOutOfOrderBoundary"] : []), ...(serverBootstrap === "out-of-order-reorder" && hasOutOfOrderAsyncBoundary(ir) ? ["renderOutOfOrderReorderScript"] : []), ...(hasReactSuspenseBoundary(ir) ? ["renderReactSuspenseBoundary"] : []), ...(hasReactSuspenseOutOfOrderBoundary(ir) ? ["renderReactSuspenseOutOfOrderBoundary"] : []), ]; const imports: RuntimeImport[] = []; if (serverSpecifiers.length > 0) { imports.push({ source: "@reckona/mreact-server", specifiers: serverSpecifiers, }); } const compatSpecifiers = [ ...(hasCompatComponentReference(ir) || hasReactNodeRender(ir) || hasRawJsxDynamicRender(ir) ? ["renderToString"] : []), ...(usesCompatChildRender(ir) ? ["renderChildToString"] : []), ]; if (compatSpecifiers.length > 0) { imports.push({ source: "@reckona/mreact-compat", specifiers: compatSpecifiers, }); } return imports; } function hasInOrderAsyncBoundary(ir: ModuleIr): boolean { return ir.components.some( (component) => containsAsyncBoundary(component.root, false) || containsForcedClientBoundaryAsyncBoundary(component.root), ); } function containsForcedClientBoundaryAsyncBoundary(node: JsxNodeIr): boolean { if (node.kind === "component") { if ( isClientBoundaryPlaceholder(node) && shouldRenderClientBoundaryFallback(node) && node.children.some(containsAwaitBoundary) ) { return true; } return node.children.some(containsForcedClientBoundaryAsyncBoundary); } if (node.kind === "conditional") { return [...node.whenTrue, ...node.whenFalse].some(containsForcedClientBoundaryAsyncBoundary); } if (node.kind === "list" || node.kind === "element" || node.kind === "fragment") { return node.children.some(containsForcedClientBoundaryAsyncBoundary); } if (node.kind === "async-boundary") { return [ ...node.children, ...(node.placeholderChildren ?? []), ...(node.catchChildren ?? []), ].some(containsForcedClientBoundaryAsyncBoundary); } return false; } function containsAwaitBoundary(node: JsxNodeIr): boolean { if (node.kind === "async-boundary") { return true; } if (node.kind === "conditional") { return [...node.whenTrue, ...node.whenFalse].some(containsAwaitBoundary); } if (node.kind === "list" || node.kind === "element" || node.kind === "fragment") { return node.children.some(containsAwaitBoundary); } if (node.kind === "component") { return node.children.some(containsAwaitBoundary); } return false; } function hasOutOfOrderAsyncBoundary(ir: ModuleIr): boolean { return ir.components.some((component) => containsAsyncBoundary(component.root, true)); } function hasReactSuspenseBoundary(ir: ModuleIr): boolean { return ir.components.some((component) => containsReactSuspense(component.root, false)); } function hasReactSuspenseOutOfOrderBoundary(ir: ModuleIr): boolean { return ir.components.some((component) => containsReactSuspense(component.root, true)); } function hasCompatComponentReference(ir: ModuleIr): boolean { return ir.components.some((component) => containsCompatComponent(component.root)); } function hasReactNodeRender(ir: ModuleIr): boolean { return ir.components.some((component) => containsReactNodeRender(component.root)); } function hasRawJsxDynamicRender(ir: ModuleIr): boolean { return ir.components.some((component) => containsRawJsxDynamicRender(component.root)); } function usesClientBoundary(ir: ModuleIr, serverHydration: boolean): boolean { return ir.components.some((component) => containsClientBoundary(component.root, serverHydration)); } function emitClientBoundaryHelper(name: string, isServerRenderValueHelperName?: string): string { const propsHelperName = `${name}$hasNonSerializableProps`; const markChildrenHelperName = `${name}$markChildren`; const renderHelperName = `${name}$renderHtml`; return [ `function ${propsHelperName}(value) {`, ` const pending = [value];`, ` const seen = new Set();`, ` while (pending.length > 0) {`, ` const current = pending.pop();`, ...(isServerRenderValueHelperName === undefined ? [] : [` if (${isServerRenderValueHelperName}(current)) return true;`]), ` if (typeof current === "function" || typeof current === "symbol" || typeof current === "bigint") return true;`, ` if (current === null || typeof current !== "object") continue;`, ` if (seen.has(current)) continue;`, ` seen.add(current);`, ` if (Array.isArray(current)) {`, ` try {`, ` for (let index = 0; index < current.length; index += 1) pending.push(current[index]);`, ` } catch { return true; }`, ` continue;`, ` }`, ` let descriptors;`, ` try { descriptors = Object.getOwnPropertyDescriptors(current); } catch { return true; }`, ` for (const descriptor of Object.values(descriptors)) {`, ` if (descriptor.enumerable !== true) continue;`, ` if (!("value" in descriptor)) return true;`, ` pending.push(descriptor.value);`, ` }`, ` }`, ` return false;`, `}`, `function ${markChildrenHelperName}(fallbackHtml, childrenHtml, startMarker, endMarker) {`, ` if (childrenHtml === "") return undefined;`, ` const _start = fallbackHtml.indexOf(childrenHtml);`, ` if (_start === -1 || fallbackHtml.indexOf(childrenHtml, _start + childrenHtml.length) !== -1) return undefined;`, ` const _end = _start + childrenHtml.length;`, ` const _opening = /<([a-z][a-z0-9:-]*)(?:\\s[^<>]*)?>$/i.exec(fallbackHtml.slice(0, _start));`, ` const _closing = /^<\\/([a-z][a-z0-9:-]*)\\s*>/i.exec(fallbackHtml.slice(_end));`, ` if (_opening === null || _closing === null || _opening[1].toLowerCase() !== _closing[1].toLowerCase()) return undefined;`, ` if (["iframe", "noembed", "noframes", "noscript", "plaintext", "script", "style", "template", "textarea", "title", "xmp"].includes(_opening[1].toLowerCase())) return undefined;`, ` return fallbackHtml.slice(0, _start) + startMarker + childrenHtml + endMarker + fallbackHtml.slice(_end);`, `}`, `async function ${renderHelperName}(value) {`, ` if (typeof value !== "function") return value ?? "";`, ` let _out = "";`, ` const _tasks = [];`, ` const _sink = {`, ` __mreactForceInOrder: true,`, ` append(chunk) { _out += chunk; },`, ` defer(task) { _tasks.push(Promise.resolve(task)); },`, ` };`, ` await value(_sink);`, ` while (_tasks.length > 0) await Promise.all(_tasks.splice(0));`, ` return _out;`, `}`, emitCompatSsrBoundaryHelper(name), `function ${name}(name, props, fallbackHtml = "", componentFallback = false, originalChildrenHtml = "", hasOriginalChildren = false, compatSsr = false) {`, ` if (compatSsr) return Promise.resolve(${renderHelperName}(originalChildrenHtml)).then((children) => ${name}$compat(name, props, fallbackHtml, children, hasOriginalChildren));`, ` const _name = String(name);`, ` const _escapedName = _name.replaceAll("&", "&").replaceAll('"', """).replaceAll("<", "<").replaceAll(">", ">");`, ` const _props = props ?? {};`, ` let _nonSerializable = ${propsHelperName}(_props);`, ` let _jsonValue = "{}";`, ` if (!_nonSerializable) { try { _jsonValue = JSON.stringify(_props) ?? "{}"; } catch { _nonSerializable = true; _jsonValue = "{}"; } }`, ` const _nonSerializableAttr = _nonSerializable ? ' data-mreact-client-boundary-nonserializable="true"' : "";`, ` const _json = _jsonValue`, ` .replaceAll("&", "\\\\u0026")`, ` .replaceAll("<", "\\\\u003c")`, ` .replaceAll(">", "\\\\u003e")`, ` .replaceAll("\\u2028", "\\\\u2028")`, ` .replaceAll("\\u2029", "\\\\u2029");`, ` if (!componentFallback) return \`\${fallbackHtml}\`;`, ` return (async () => {`, ` const _originalChildrenHtml = await ${renderHelperName}(originalChildrenHtml);`, ` const _startMarker = "";`, ` const _endMarker = "";`, ` const _fallbackValue = typeof fallbackHtml === "function" ? await fallbackHtml(_originalChildrenHtml) : fallbackHtml;`, ` const _fallbackHtml = String(await ${renderHelperName}(_fallbackValue));`, ` const _markedFallbackHtml = ${markChildrenHelperName}(_fallbackHtml, _originalChildrenHtml, _startMarker, _endMarker);`, ` const _visibleHtml = _markedFallbackHtml ?? _fallbackHtml;`, ` const _childrenArchive = hasOriginalChildren && _markedFallbackHtml === undefined ? '' : "";`, ` return \`\${_visibleHtml}\${_childrenArchive}\`;`, ` })();`, `}`, ].join("\n"); } function emitSpreadAttributesHelper( name: string, escapeHelperName: string, urlSafeHelperName: string, isServerRenderValueHelperName?: string, ): string { const aliases = JSON.stringify({ acceptCharset: "accept-charset", autoFocus: "autofocus", autoPlay: "autoplay", charSet: "charset", className: "class", colSpan: "colspan", contentEditable: "contenteditable", crossOrigin: "crossorigin", encType: "enctype", formAction: "formaction", frameBorder: "frameborder", htmlFor: "for", httpEquiv: "http-equiv", maxLength: "maxlength", minLength: "minlength", noValidate: "novalidate", playsInline: "playsinline", readOnly: "readonly", rowSpan: "rowspan", spellCheck: "spellcheck", imageSrcSet: "imagesrcset", srcDoc: "srcdoc", srcSet: "srcset", tabIndex: "tabindex", useMap: "usemap", }); const urlAttributes = JSON.stringify([ "href", "src", "action", "formaction", "xlink:href", "ping", "poster", "background", "manifest", "data", "codebase", "srcset", "imagesrcset", ]); const dangerousAttributes = JSON.stringify(["srcdoc"]); return [ `const ${name}$aliases = ${aliases};`, `const ${name}$urlAttributes = new Set(${urlAttributes});`, `const ${name}$dangerousAttributes = new Set(${dangerousAttributes});`, `function ${name}$html(value) {`, ` try {`, ` if (typeof value !== "object" || value === null) return undefined;`, ` const _descriptor = Object.getOwnPropertyDescriptor(value, "__html");`, ` return _descriptor !== undefined && "value" in _descriptor && typeof _descriptor.value === "string" ? _descriptor.value : undefined;`, ` } catch { return undefined; }`, `}`, `function ${name}$assign(target, source) {`, ` for (const _rawName of Object.keys(source)) {`, ` if (_rawName === "key" || _rawName === "ref" || _rawName === "domRef" || _rawName === "children" || /^on/i.test(_rawName)) continue;`, ` if (_rawName === "__proto__") Object.defineProperty(target, _rawName, { configurable: true, enumerable: true, value: source[_rawName], writable: true });`, ` else target[_rawName] = source[_rawName];`, ` }`, `}`, `function ${name}$style(value) {`, ` if (value == null || value === false) return "";`, ` if (typeof value === "string") return value;`, ` let _style = "";`, ` for (const _styleName of Object.keys(value)) {`, ` const _styleValue = value[_styleName];`, ` if (_styleValue == null || _styleValue === false) continue;`, ` const _cssName = String(_styleName).startsWith("--") ? String(_styleName) : String(_styleName).replace(/[A-Z]/g, (_char) => "-" + _char.toLowerCase());`, ` _style += (_style === "" ? "" : ";") + _cssName + ":" + (_styleValue === true ? "" : String(_styleValue));`, ` }`, ` return _style;`, `}`, `function ${name}(tagName, props, omitSelected) {`, ` if (props == null || props === false) return "";`, ` let _out = "";`, ` for (const _rawName of Object.keys(props)) {`, ` if (_rawName === "key" || _rawName === "ref" || _rawName === "domRef" || _rawName === "children" || _rawName === "dangerouslySetInnerHTML") continue;`, ` if (/^on/i.test(_rawName)) continue;`, ` if (omitSelected && tagName === "option" && _rawName === "selected") continue;`, ` if (tagName === "select" && (_rawName === "value" || _rawName === "defaultValue")) continue;`, ` let _value = props[_rawName];`, ` if (_value == null) continue;`, ...(isServerRenderValueHelperName === undefined ? [] : [` if (${isServerRenderValueHelperName}(_value)) continue;`]), ` let _name = tagName === "input" && _rawName === "defaultValue" ? "value" : tagName === "input" && _rawName === "defaultChecked" ? "checked" : (Object.hasOwn(${name}$aliases, _rawName) ? ${name}$aliases[_rawName] : _rawName);`, ` if (!/^[A-Za-z_:][A-Za-z0-9:_.-]*$/.test(_name)) continue;`, ` const _lowerName = _name.toLowerCase();`, ` const _booleanish = _lowerName.startsWith("aria-") || _lowerName.startsWith("data-") || _lowerName === "autocapitalize" || _lowerName === "contenteditable" || _lowerName === "draggable" || _lowerName === "spellcheck" || _lowerName === "translate";`, ` if (_value === false && !_booleanish) continue;`, ` if (_name === "style") {`, ` const _style = ${name}$style(_value);`, ` if (_style !== "") _out += " style=\\"" + ${escapeHelperName}(_style) + "\\"";`, ` continue;`, ` }`, ` if (${name}$dangerousAttributes.has(_lowerName)) {`, ` const _html = ${name}$html(_value);`, ` if (_html !== undefined) {`, ` _out += " " + _name + "=\\"" + ${escapeHelperName}(_html) + "\\"";`, ` }`, ` continue;`, ` }`, ` if (${name}$urlAttributes.has(_lowerName)) {`, ` _value = ${urlSafeHelperName}(_name, _value === true ? "" : _value);`, ` if (_value === undefined) continue;`, ` }`, ` _out += " " + _name + "=\\"" + ${escapeHelperName}(_value === true && !_booleanish ? "" : _value) + "\\"";`, ` }`, ` return _out;`, `}`, ].join("\n"); } function emitStreamNodeHelper( name: string, isRenderValueName: string, renderValueName: string, ): string { return [ `async function ${name}($sink, value, escapeHtml, selectedValue, selectedMultiple) {`, ` if (value == null || value === false) return;`, ` if (${isRenderValueName}(value)) { await ${renderValueName}($sink, value, escapeHtml, 0, selectedValue, selectedMultiple); return; }`, ` if (typeof value === "function") {`, ` if (value[Symbol.for(${JSON.stringify(serverSelectionRenderValueKey)})] === true) { await value($sink, selectedValue, selectedMultiple); return; }`, ` await value($sink); return;`, ` }`, ` if (Array.isArray(value)) { await ${renderValueName}($sink, value, escapeHtml, 0, selectedValue, selectedMultiple); return; }`, ` if (typeof value === "string") { $sink.append(value); return; }`, ` $sink.append(escapeHtml(value === true ? "" : value));`, `}`, ].join("\n"); } function emitStreamSelectionArguments(part?: Extract): string { const selectedValue = part?.selectedValueCode ?? currentPropChildrenCollectState?.selectedValueCode ?? currentSelectionParameterName ?? "undefined"; const selectedMultiple = part?.selectedMultipleCode ?? currentPropChildrenCollectState?.selectedMultipleCode ?? currentSelectionMultipleParameterName ?? "undefined"; return `, ${selectedValue}, ${selectedMultiple}`; } function emitComponent( component: ComponentIr, escapeHelperName: string, asyncBoundaryHelperName: string, outOfOrderBoundaryHelperName: string, reorderScriptHelperName: string, reactSuspenseBoundaryHelperName: string, reactSuspenseOutOfOrderBoundaryHelperName: string, compatRenderToStringHelperName: string, options: Required> & Omit & { dynamicAttributes: "drop" | "emit"; escapeBatchHelperName?: string; selectionParameterName?: string; selectionMultipleParameterName?: string; }, ): string { const { serverBootstrap, serverBootstrapNonce, serverBootstrapSrc } = options; const sinkName = allocateComponentSinkName(component); const parameters = [sinkName, ...component.parameters].join(", "); const selectionContextDeclaration = [ ` const ${options.selectionParameterName} = arguments[1]?.[Symbol.for(${JSON.stringify(serverSelectionContextKey)})];`, ` const ${options.selectionMultipleParameterName} = arguments[1]?.[Symbol.for(${JSON.stringify(serverSelectionMultipleContextKey)})];`, ]; const body = component.bodyStatements.map( (statement) => ` ${statement.replaceAll( oxcServerStringReactNodeRenderHelperPlaceholder, compatRenderToStringHelperName, )}`, ); const markerId = encodeURIComponent(component.name); const hydrationStartStatements = options.serverHydration === true ? [` ${sinkName}.append(${stringLiteral(``)});`] : []; const appendStatements = emitAppendStatements( component.root, sinkName, escapeHelperName, asyncBoundaryHelperName, outOfOrderBoundaryHelperName, reactSuspenseBoundaryHelperName, reactSuspenseOutOfOrderBoundaryHelperName, compatRenderToStringHelperName, options.serverBootstrapNonce, options.reactSuspenseRevealScriptSrc, options.serverHydration === true, options.serverAwaitHydration === true, options.dynamicAttributes, options.escapeBatchHelperName, options.selectionParameterName, options.selectionMultipleParameterName, ); const bootstrapStatements = serverBootstrap === "out-of-order-reorder" && containsAsyncBoundary(component.root, true) ? [ ` ${reorderScriptHelperName}(${sinkName}${emitBootstrapOptions(serverBootstrapNonce, serverBootstrapSrc)});`, ] : []; const hydrationEndStatements = options.serverHydration === true ? [` ${sinkName}.append(${stringLiteral(``)});`] : []; const exportPrefix = component.exportDefault === true ? "export default " : component.exported === false ? "" : "export "; const asyncPrefix = component.async === true || containsAnyAsyncBoundary(component.root) || containsServerRenderValue(component.root) ? "async " : ""; const functionKeyword = `${exportPrefix}${asyncPrefix}function`; return [ `${functionKeyword} ${component.name}(${parameters}) {`, ...selectionContextDeclaration, ...body, ...hydrationStartStatements, ...appendStatements, ...hydrationEndStatements, ...bootstrapStatements, `}`, ].join("\n"); } function emitBootstrapOptions(nonce?: string, src?: string): string { const entries = [ ...(nonce === undefined ? [] : [`nonce: ${stringLiteral(nonce)}`]), ...(src === undefined ? [] : [`src: ${stringLiteral(src)}`]), ]; return entries.length === 0 ? "" : `, { ${entries.join(", ")} }`; } function emitAppendStatements( node: JsxNodeIr, sinkName: string, escapeHelperName: string, asyncBoundaryHelperName: string, outOfOrderBoundaryHelperName: string, reactSuspenseBoundaryHelperName: string, reactSuspenseOutOfOrderBoundaryHelperName: string, compatRenderToStringHelperName: string, reactSuspenseRevealScriptNonce: string | undefined, reactSuspenseRevealScriptSrc: string | undefined, hydration: boolean, awaitHydration: boolean, dynamicAttributes: "drop" | "emit", escapeBatchHelperName: string | undefined, selectionParameterName: string | undefined, selectionMultipleParameterName: string | undefined, ): string[] { if (node.kind === "conditional") { const emitBranch = (children: readonly JsxNodeIr[]): string[] => children.flatMap((child) => emitAppendStatements( child, sinkName, escapeHelperName, asyncBoundaryHelperName, outOfOrderBoundaryHelperName, reactSuspenseBoundaryHelperName, reactSuspenseOutOfOrderBoundaryHelperName, compatRenderToStringHelperName, reactSuspenseRevealScriptNonce, reactSuspenseRevealScriptSrc, hydration, awaitHydration, dynamicAttributes, escapeBatchHelperName, selectionParameterName, selectionMultipleParameterName, ), ); const indentBranch = (line: string) => ` ${line}`; const whenTrue = emitBranch(node.whenTrue).map(indentBranch); const whenFalse = emitBranch(node.whenFalse).map(indentBranch); if (whenTrue.length === 0 && whenFalse.length === 0) { return []; } const conditionCode = node.conditionValueName === undefined ? node.conditionCode : (node.conditionTestCode ?? node.conditionValueName); let statements: string[]; if (whenFalse.length === 0) { statements = [` if (${conditionCode}) {`, ...whenTrue, ` }`]; } else { statements = [` if (${conditionCode}) {`, ...whenTrue, ` } else {`, ...whenFalse, ` }`]; } if (node.conditionValueName === undefined) { return statements; } return [ ` {`, ` const ${node.conditionValueName} = (${node.conditionCode});`, ...statements.map((statement) => ` ${statement}`), ` }`, ]; } const collectState: CollectHtmlState = { dynamicAttributes, ...(escapeBatchHelperName === undefined ? {} : { escapeBatchHelperName }), hydration, awaitHydration, nextFragmentId: 0, ...(reactSuspenseRevealScriptNonce === undefined ? {} : { reactSuspenseRevealScriptNonce }), ...(reactSuspenseRevealScriptSrc === undefined ? {} : { reactSuspenseRevealScriptSrc }), ...(selectionParameterName === undefined ? {} : { selectedValueCode: selectionParameterName }), ...(selectionMultipleParameterName === undefined ? {} : { selectedMultipleCode: selectionMultipleParameterName }), }; const collected = collectHtmlParts( node, escapeHelperName, asyncBoundaryHelperName, outOfOrderBoundaryHelperName, reactSuspenseBoundaryHelperName, reactSuspenseOutOfOrderBoundaryHelperName, collectState, ); const previousPropChildrenCollectState = currentPropChildrenCollectState; currentPropChildrenCollectState = collectState; try { return coalesceAdjacentStaticParts(collected).map((part) => { if (part.kind === "async-boundary") { return emitLoweredAsyncBoundary(part, { asyncBoundaryHelperName, compatRenderToStringHelperName, emitNestedAppendStatements, sinkName, }); } if (part.kind === "out-of-order-boundary") { return emitLoweredOutOfOrderBoundary(part, { compatRenderToStringHelperName, emitNestedAppendStatements, outOfOrderBoundaryHelperName, sinkName, }); } if (part.kind === "react-suspense-boundary") { return emitLoweredReactSuspenseBoundary(part, { compatRenderToStringHelperName, emitNestedAppendStatements, reactSuspenseBoundaryHelperName, sinkName, }); } if (part.kind === "react-suspense-out-of-order-boundary") { return emitLoweredReactSuspenseOutOfOrderBoundary(part, { compatRenderToStringHelperName, emitNestedAppendStatements, reactSuspenseOutOfOrderBoundaryHelperName, sinkName, }); } if (part.kind === "component") { if (part.runtime === "compat") { return emitCompatComponentAppendStatements( part, sinkName, compatRenderToStringHelperName, " ", ); } return ` await ${part.name}(${sinkName}, ${emitPropsObject(part.props, part.children, part.escapeHelperName, part.name, undefined, part.selectedValueCode, part.selectedMultipleCode, part.selectionContextActive)});`; } if (part.kind === "react-node") { return ` ${sinkName}.append(${compatRenderToStringHelperName}(() => (${part.code})));`; } if (part.kind === "stream-node") { return ` await ${currentStreamNodeHelperName}(${sinkName}, (${part.code}), ${part.escapeHelperName}${emitStreamSelectionArguments(part)});`; } if (part.kind === "list") { return emitListPart(part, sinkName, compatRenderToStringHelperName, " "); } if (part.kind === "dynamic" && looksLikeRawJsxExpression(part.code)) { return emitDynamicHtmlAppendStatement( part.code, sinkName, escapeHelperName, compatRenderToStringHelperName, " ", ); } const expression = part.kind === "static" ? stringLiteral(part.value) : part.kind === "dynamic" ? `${escapeHelperName}(${part.code})` : part.code; return ` ${sinkName}.append(${expression});`; }); } finally { currentPropChildrenCollectState = previousPropChildrenCollectState; } } function isHtmlSyncPart(part: HtmlPart): part is HtmlSyncPart { return ( part.kind !== "async-boundary" && part.kind !== "out-of-order-boundary" && part.kind !== "react-suspense-boundary" && part.kind !== "react-suspense-out-of-order-boundary" ); } // Issue 085: collapse runs of adjacent `static` parts into a single // `static` part. Each part becomes one `sink.append(...)` call at emit // time and `sink.append` goes through 2-3 function frames, so merging // `[""]` into `""` halves the per-iteration call count // for tag-heavy lists. // // Only adjacent static-kind parts are merged; dynamic / boundary / // component / list / react-node parts stay where they are. function coalesceAdjacentStaticParts(parts: T[]): T[] { if (parts.length < 2) return parts; const result: T[] = []; let pending: { kind: "static"; value: string } | undefined; for (const part of parts) { if (part.kind === "static") { pending = pending === undefined ? { kind: "static", value: part.value } : { kind: "static", value: pending.value + part.value }; continue; } if (pending !== undefined) { result.push(pending as T); pending = undefined; } result.push(part); } if (pending !== undefined) { result.push(pending as T); } return result; } function emitSyncPartAsAppendStatement( part: HtmlSyncPart, sinkName: string, compatRenderToStringHelperName: string, indent: string, ): string { if (part.kind === "component") { if (part.runtime === "compat") { return emitCompatComponentAppendStatements( part, sinkName, compatRenderToStringHelperName, indent, ); } return `${indent}await ${part.name}(${sinkName}, ${emitPropsObject(part.props, part.children, part.escapeHelperName, part.name, undefined, part.selectedValueCode, part.selectedMultipleCode, part.selectionContextActive)});`; } if (part.kind === "react-node") { return `${indent}${sinkName}.append(${compatRenderToStringHelperName}(() => (${part.code})));`; } if (part.kind === "stream-node") { return `${indent}await ${currentStreamNodeHelperName}(${sinkName}, (${part.code}), ${part.escapeHelperName}${emitStreamSelectionArguments(part)});`; } if (part.kind === "list") { return emitListPart(part, sinkName, compatRenderToStringHelperName, indent); } if (part.kind === "dynamic" && looksLikeRawJsxExpression(part.code)) { return emitDynamicHtmlAppendStatement( part.code, sinkName, part.escapeHelperName, compatRenderToStringHelperName, indent, ); } const expression = part.kind === "static" ? stringLiteral(part.value) : part.kind === "dynamic" ? `${part.escapeHelperName}(${part.code})` : part.code; return `${indent}${sinkName}.append(${expression});`; } function emitListPart( part: Extract, sinkName: string, compatRenderToStringHelperName: string, indent: string, ): string { const innerIndent = indent + " "; const itemBinding = part.bindItem ? `${innerIndent}const ${part.itemPattern ?? part.itemName} = _arr[_i];` : undefined; const indexPattern = part.indexPattern ?? part.indexName; const arrayPattern = part.arrayPattern ?? part.arrayName; const indexBinding = indexPattern === undefined ? undefined : `${innerIndent}const ${indexPattern} = _i;`; const arrayBinding = arrayPattern === undefined ? undefined : `${innerIndent}const ${arrayPattern} = _arr;`; const bodyLines = part.bodyStatements.map((statement) => `${innerIndent}${statement}`); const coalescedParts = coalesceAdjacentStaticParts(part.parts); // Issue 085 follow-up: if every child part can be expressed as a // pure string expression (no `sink.append`/`await` required), build // up a local ConsString accumulator and emit a single // `sink.append(_listOut)` at the end of the iteration. This matches // the string backend's `_out +=` pattern, which V8 turns into a // shallow cons-string tree (~3 ns per append). Otherwise (the list // contains components / nested lists with components / etc) fall // back to per-part `sink.append` inside the loop. const syncCoalescedParts = coalescedParts.every(isHtmlSyncPart) ? coalescedParts : undefined; const stringExpressions = syncCoalescedParts?.map((child) => tryEmitPartAsStringExpression(child, compatRenderToStringHelperName), ) ?? []; const allStringSafe = syncCoalescedParts !== undefined && stringExpressions.every((expr) => expr !== undefined); if (allStringSafe) { const accumulatorName = "_listOut"; const concatLines = stringExpressions.map( (expr) => `${innerIndent}${accumulatorName} += ${expr};`, ); return [ `${indent}{`, `${indent} const _arr = (${part.itemsCode});`, `${indent} let ${accumulatorName} = "";`, `${indent} for (let _i = 0, _len = _arr.length; _i < _len; _i++) {`, ...(itemBinding === undefined ? [] : [itemBinding]), ...(indexBinding === undefined ? [] : [indexBinding]), ...(arrayBinding === undefined ? [] : [arrayBinding]), ...bodyLines, ...concatLines, `${indent} }`, `${indent} ${sinkName}.append(${accumulatorName});`, `${indent}}`, ].join("\n"); } const childLines = syncCoalescedParts === undefined ? [emitNestedStreamAppendStatements(coalescedParts, sinkName, compatRenderToStringHelperName)] : syncCoalescedParts.map((child) => emitSyncPartAsAppendStatement( child, sinkName, compatRenderToStringHelperName, innerIndent, ), ); return [ `${indent}{`, `${indent} const _arr = (${part.itemsCode});`, `${indent} for (let _i = 0, _len = _arr.length; _i < _len; _i++) {`, ...(itemBinding === undefined ? [] : [itemBinding]), ...(indexBinding === undefined ? [] : [indexBinding]), ...(arrayBinding === undefined ? [] : [arrayBinding]), ...bodyLines, ...childLines, `${indent} }`, `${indent}}`, ].join("\n"); } // Returns a string-typed expression for `part` if it can be evaluated // synchronously without writing to the sink, otherwise undefined. // Used by `emitListPart` to choose between the cons-string accumulator // path and the per-part `sink.append` path. function tryEmitPartAsStringExpression( part: HtmlSyncPart, compatRenderToStringHelperName: string, ): string | undefined { if (part.kind === "static") return stringLiteral(part.value); if (part.kind === "dynamic") { return looksLikeRawJsxExpression(part.code) ? undefined : `${part.escapeHelperName}(${part.code})`; } if (part.kind === "raw-dynamic") return `(${part.code})`; if (part.kind === "react-node") { return `${compatRenderToStringHelperName}(() => (${part.code}))`; } if (part.kind === "stream-node") { return undefined; } if (part.kind === "list" && part.parts.every(isHtmlSyncPart)) { return emitListPartAsStringExpression(part, compatRenderToStringHelperName); } if (part.kind === "component" && part.runtime === "compat") { const rendered = `${compatRenderToStringHelperName}(${part.name}, ${emitCompatRuntimePropsObject(part.props, part.children, part.selectedValueCode, part.selectedMultipleCode, part.selectionContextActive)})`; if (part.hydrationId === undefined) { return rendered; } return `${stringLiteral(``)} + ${rendered} + ${stringLiteral(``)}`; } // The router Link keeps a single-argument overload that returns its markup as // a string, so it is the one component this emitter can inline. Every other // component in stream output is compiled as `Name($sink, props)`: it writes to // the sink and returns nothing, so calling it with the string convention would // hand the props object over as the sink and leave `props` undefined. // // Neither `async` nor `hydrationId` needs a check here. Only a module-local // declaration is ever marked async and the router Link is imported, and // `hydrationId` is only set for the compat runtime, which the branch above // already returned for. if (part.kind === "component" && isRouterLinkComponentName(part.name)) { return emitRenderableHtmlExpression( `${part.name}(${emitPropsObject(part.props, part.children, part.escapeHelperName, part.name, undefined, part.selectedValueCode, part.selectedMultipleCode, part.selectionContextActive)})`, ); } // Non-compat component parts require `await sink-write`; lists with // sink-needing children also can't collapse. Signal fallback. return undefined; } function emitListPartAsStringExpression( part: Extract, compatRenderToStringHelperName: string, ): string | undefined { const coalescedParts = coalesceAdjacentStaticParts(part.parts); if (!coalescedParts.every(isHtmlSyncPart)) { return undefined; } const stringExpressions = coalescedParts.map((child) => tryEmitPartAsStringExpression(child, compatRenderToStringHelperName), ); if (stringExpressions.some((expr) => expr === undefined)) { return undefined; } const concatLines = stringExpressions.map((expr) => `_listOut += ${expr};`); const itemBinding = part.bindItem ? ` const ${part.itemPattern ?? part.itemName} = _arr[_i];` : ""; return `(() => { const _arr = (${part.itemsCode}); let _listOut = ""; for (let _i = 0, _len = _arr.length; _i < _len; _i++) {${itemBinding}${(part.indexPattern ?? part.indexName) === undefined ? "" : ` const ${part.indexPattern ?? part.indexName} = _i;`}${(part.arrayPattern ?? part.arrayName) === undefined ? "" : ` const ${part.arrayPattern ?? part.arrayName} = _arr;`}${part.bodyStatements.length === 0 ? "" : ` ${part.bodyStatements.join(" ")}`} ${concatLines.join(" ")} } return _listOut; })()`; } function emitNestedAppendStatements( parts: readonly HtmlSyncPart[], sinkName: string, compatRenderToStringHelperName: string, ): string { return coalesceAdjacentStaticParts([...parts]) .map((part) => emitSyncPartAsAppendStatement(part, sinkName, compatRenderToStringHelperName, " "), ) .join("\n"); } function emitNestedStreamAppendStatements( parts: HtmlPart[], sinkName: string, compatRenderToStringHelperName: string, ): string { return coalesceAdjacentStaticParts(parts) .map((part) => { if (part.kind === "async-boundary") { return emitLoweredAsyncBoundary(part, { asyncBoundaryHelperName: currentAsyncBoundaryHelperName, compatRenderToStringHelperName, emitNestedAppendStatements, sinkName, }).replace(/^/gm, " "); } if (part.kind === "out-of-order-boundary") { return emitLoweredOutOfOrderBoundary(part, { compatRenderToStringHelperName, emitNestedAppendStatements, outOfOrderBoundaryHelperName: currentOutOfOrderBoundaryHelperName, sinkName, }).replace(/^/gm, " "); } if (part.kind === "react-suspense-boundary") { return emitLoweredReactSuspenseBoundary(part, { compatRenderToStringHelperName, emitNestedAppendStatements, reactSuspenseBoundaryHelperName: currentReactSuspenseBoundaryHelperName, sinkName, }).replace(/^/gm, " "); } if (part.kind === "react-suspense-out-of-order-boundary") { return emitLoweredReactSuspenseOutOfOrderBoundary(part, { compatRenderToStringHelperName, emitNestedAppendStatements, reactSuspenseOutOfOrderBoundaryHelperName: currentReactSuspenseOutOfOrderBoundaryHelperName, sinkName, }).replace(/^/gm, " "); } return emitSyncPartAsAppendStatement(part, sinkName, compatRenderToStringHelperName, " "); }) .join("\n"); } function emitCompatComponentAppendStatements( part: Extract, sinkName: string, compatRenderToStringHelperName: string, indent: string, ): string { const rendered = `${compatRenderToStringHelperName}(${part.name}, ${emitCompatRuntimePropsObject(part.props, part.children, part.selectedValueCode, part.selectedMultipleCode, part.selectionContextActive)})`; const statements = part.hydrationId === undefined ? [`${sinkName}.append(${rendered});`] : [ `${sinkName}.append(${stringLiteral(``)});`, `${sinkName}.append(${rendered});`, `${sinkName}.append(${stringLiteral(``)});`, ]; return statements.map((statement) => `${indent}${statement}`).join("\n"); } type HtmlPart = | { kind: "static"; value: string; } | { kind: "dynamic"; code: string; escapeHelperName: string; } | { kind: "raw-dynamic"; code: string; } | { kind: "react-node"; code: string; } | { kind: "stream-node"; code: string; escapeHelperName: string; selectedValueCode?: string; selectedMultipleCode?: string; } | { kind: "async-boundary"; valueCode: string; valueName: string; parts: HtmlSyncPart[]; catchName?: string; catchParts?: HtmlSyncPart[]; awaitId?: string; } | { kind: "out-of-order-boundary"; id: string; hydration: boolean; valueCode: string; valueName: string; parts: HtmlSyncPart[]; placeholderParts: HtmlSyncPart[]; placeholderTagCode?: string; catchName?: string; catchParts?: HtmlSyncPart[]; awaitId?: string; } | { kind: "react-suspense-boundary"; parts: HtmlSyncPart[]; } | { kind: "react-suspense-out-of-order-boundary"; boundaryId: string; segmentId: string; valueCode: string; valueName: string; parts: HtmlSyncPart[]; fallbackParts: HtmlSyncPart[]; catchName?: string; catchParts?: HtmlSyncPart[]; nonce?: string; scriptSrc?: string; } | { kind: "component"; name: string; runtime?: "compat"; async?: boolean; hydrationId?: string; selectedValueCode?: string; selectedMultipleCode?: string; selectionContextActive?: boolean; props: ComponentPropIr[]; children: JsxNodeIr[]; escapeHelperName: string; } | { // Issue 085: list direct streaming. The list iterates // `itemsCode`, runs `bodyStatements` and then emits each inner // part per iteration. Sync-only lists still use the string // accumulator fast path; lists that contain async/oob/Suspense // boundaries keep those boundary parts visible to the stream // emitter instead of falling back to a raw `.map().join("")`. kind: "list"; itemsCode: string; bindItem: boolean; itemName: string; itemPattern?: string; indexName?: string; indexPattern?: string; arrayName?: string; arrayPattern?: string; bodyStatements: string[]; parts: HtmlPart[]; }; type HtmlSyncPart = Exclude< HtmlPart, { kind: | "async-boundary" | "out-of-order-boundary" | "react-suspense-boundary" | "react-suspense-out-of-order-boundary"; } >; interface CollectHtmlState { dynamicAttributes: "drop" | "emit"; escapeBatchHelperName?: string; hydration: boolean; awaitHydration: boolean; nextFragmentId: number; reactSuspenseRevealScriptNonce?: string; reactSuspenseRevealScriptSrc?: string; selectedValueCode?: string | undefined; selectedMultipleCode?: string | undefined; selectionContextActive?: boolean; forceInOrder?: boolean; } function collectHtmlParts( node: JsxNodeIr, escapeHelperName: string, asyncBoundaryHelperName: string, outOfOrderBoundaryHelperName: string, reactSuspenseBoundaryHelperName: string, reactSuspenseOutOfOrderBoundaryHelperName: string, state: CollectHtmlState, ): HtmlPart[] { void asyncBoundaryHelperName; void outOfOrderBoundaryHelperName; void reactSuspenseBoundaryHelperName; void reactSuspenseOutOfOrderBoundaryHelperName; if (node.kind === "text") { return [{ kind: "static", value: escapeHtml(node.value) }]; } if (node.kind === "expr") { if (node.renderMode === "html" && isChildrenExpressionCode(node.code)) { return [ { kind: "stream-node", code: node.code, escapeHelperName, selectedValueCode: state.selectedValueCode ?? currentSelectionParameterName, selectedMultipleCode: state.selectedMultipleCode ?? currentSelectionMultipleParameterName, }, ]; } if (node.renderMode === "html") { return [{ kind: "raw-dynamic", code: rawHtmlExpression(node.code) }]; } if (node.renderMode === "react-node") { return [{ kind: "react-node", code: node.code }]; } if (node.renderMode === "server-render-value") { return [ { kind: "stream-node", code: `async (${currentServerRenderValueSinkName}) => { await ${currentRenderServerValueHelperName}(${currentServerRenderValueSinkName}, ${node.code}, ${escapeHelperName}, 0, ${state.selectedValueCode ?? currentSelectionParameterName ?? "undefined"}, ${state.selectedMultipleCode ?? currentSelectionMultipleParameterName ?? "undefined"}); }`, escapeHelperName, }, ]; } if (node.renderMode === "stream-node") { return [ { kind: "stream-node", code: node.code, escapeHelperName, selectedValueCode: state.selectedValueCode ?? currentSelectionParameterName, selectedMultipleCode: state.selectedMultipleCode ?? currentSelectionMultipleParameterName, }, ]; } if (node.renderMode === "compat-child" && currentCompatChildHelperName !== undefined) { return [{ kind: "raw-dynamic", code: `${currentCompatChildHelperName}(${node.code})` }]; } return [{ kind: "dynamic", code: node.code, escapeHelperName }]; } if (node.kind === "conditional") { const collectChildren = (children: JsxNodeIr[]) => children.flatMap((child) => collectHtmlParts( child, escapeHelperName, asyncBoundaryHelperName, outOfOrderBoundaryHelperName, reactSuspenseBoundaryHelperName, reactSuspenseOutOfOrderBoundaryHelperName, state, ), ); const trueParts = collectChildren(node.whenTrue); const falseParts = collectChildren(node.whenFalse); const emitStringExpression = (parts: HtmlPart[]): string | undefined => { if (!parts.every(isHtmlSyncPart)) { return undefined; } const expressions = parts.map((part) => tryEmitPartAsStringExpression(part, currentCompatRenderToStringHelperName), ); return expressions.some((expression) => expression === undefined) ? undefined : expressions.length === 0 ? '""' : (expressions as string[]).join(" + "); }; const whenTrue = emitStringExpression(trueParts); const whenFalse = emitStringExpression(falseParts); if (whenTrue === undefined || whenFalse === undefined) { const condition = node.conditionValueName === undefined ? node.conditionCode : (node.conditionTestCode ?? node.conditionValueName); const conditionStatement = node.conditionValueName === undefined ? "" : ` const ${node.conditionValueName} = (${node.conditionCode});\n`; const trueStatements = emitNestedStreamAppendStatements( trueParts, "$sink", currentCompatRenderToStringHelperName, ); const falseStatements = emitNestedStreamAppendStatements( falseParts, "$sink", currentCompatRenderToStringHelperName, ); return [ { kind: "stream-node", code: `async ($sink) => {\n${conditionStatement} if (${condition}) {\n${trueStatements}\n } else {\n${falseStatements}\n }\n}`, escapeHelperName, }, ]; } return [ { kind: "raw-dynamic", code: node.conditionValueName === undefined ? `((${node.conditionCode}) ? ${whenTrue} : ${whenFalse})` : `(() => { const ${node.conditionValueName} = (${node.conditionCode}); return ${node.conditionTestCode ?? node.conditionValueName} ? ${whenTrue} : ${whenFalse}; })()`, }, ]; } if (node.kind === "list") { // Keep mapped children in the stream emitter so direct `` // boundaries inside list renderers stay visible to out-of-order // lowering. const collectedChildParts: HtmlPart[] = node.children.flatMap((child) => collectHtmlParts( child, escapeHelperName, asyncBoundaryHelperName, outOfOrderBoundaryHelperName, reactSuspenseBoundaryHelperName, reactSuspenseOutOfOrderBoundaryHelperName, state, ), ); return [ { kind: "list", itemsCode: node.itemsCode, bindItem: node.parameterPatterns === undefined || node.parameterPatterns[0] !== undefined, itemName: node.itemName, ...(node.parameterPatterns?.[0] === undefined ? {} : { itemPattern: node.parameterPatterns[0] }), ...(node.parameterPatterns?.[1] === undefined ? {} : { indexPattern: node.parameterPatterns[1] }), ...(node.parameterPatterns?.[2] === undefined ? {} : { arrayPattern: node.parameterPatterns[2] }), ...(node.indexName === undefined ? {} : { indexName: node.indexName }), ...(node.arrayName === undefined ? {} : { arrayName: node.arrayName }), bodyStatements: node.bodyStatements ?? [], parts: collectedChildParts, }, ]; } if (node.kind === "async-boundary") { if (node.placeholderChildren !== undefined && state.forceInOrder !== true) { const id = `mreact-${state.nextFragmentId}`; state.nextFragmentId += 1; return [ { kind: "out-of-order-boundary", id, hydration: state.hydration, valueCode: node.valueCode, valueName: node.valueName, parts: node.children.flatMap((child) => collectHtmlParts( child, escapeHelperName, asyncBoundaryHelperName, outOfOrderBoundaryHelperName, reactSuspenseBoundaryHelperName, reactSuspenseOutOfOrderBoundaryHelperName, state, ), ) as HtmlSyncPart[], placeholderParts: node.placeholderChildren.flatMap((child) => collectHtmlParts( child, escapeHelperName, asyncBoundaryHelperName, outOfOrderBoundaryHelperName, reactSuspenseBoundaryHelperName, reactSuspenseOutOfOrderBoundaryHelperName, state, ), ) as HtmlSyncPart[], ...(node.placeholderTagCode === undefined ? {} : { placeholderTagCode: node.placeholderTagCode }), ...(state.awaitHydration && node.awaitId !== undefined ? { awaitId: node.awaitId } : {}), ...(node.catchName === undefined || node.catchChildren === undefined ? {} : { catchName: node.catchName, catchParts: node.catchChildren.flatMap((child) => collectHtmlParts( child, escapeHelperName, asyncBoundaryHelperName, outOfOrderBoundaryHelperName, reactSuspenseBoundaryHelperName, reactSuspenseOutOfOrderBoundaryHelperName, state, ), ) as HtmlSyncPart[], }), }, ]; } return [ { kind: "async-boundary", valueCode: node.valueCode, valueName: node.valueName, parts: node.children.flatMap((child) => collectHtmlParts( child, escapeHelperName, asyncBoundaryHelperName, outOfOrderBoundaryHelperName, reactSuspenseBoundaryHelperName, reactSuspenseOutOfOrderBoundaryHelperName, state, ), ) as HtmlSyncPart[], ...(state.awaitHydration && node.awaitId !== undefined ? { awaitId: node.awaitId } : {}), ...(node.catchName === undefined || node.catchChildren === undefined ? {} : { catchName: node.catchName, catchParts: node.catchChildren.flatMap((child) => collectHtmlParts( child, escapeHelperName, asyncBoundaryHelperName, outOfOrderBoundaryHelperName, reactSuspenseBoundaryHelperName, reactSuspenseOutOfOrderBoundaryHelperName, state, ), ) as HtmlSyncPart[], }), }, ]; } if (node.kind === "fragment") { return node.children.flatMap((child) => collectHtmlParts( child, escapeHelperName, asyncBoundaryHelperName, outOfOrderBoundaryHelperName, reactSuspenseBoundaryHelperName, reactSuspenseOutOfOrderBoundaryHelperName, state, ), ); } if (node.kind === "component") { if (node.name === "Suspense") { if (state.forceInOrder === true) { return node.children.flatMap((child) => collectHtmlParts( child, escapeHelperName, asyncBoundaryHelperName, outOfOrderBoundaryHelperName, reactSuspenseBoundaryHelperName, reactSuspenseOutOfOrderBoundaryHelperName, state, ), ); } const asyncBoundary = findSuspenseAsyncBoundary(node.children); if (asyncBoundary !== undefined) { const id = state.nextFragmentId; state.nextFragmentId += 1; return [ { kind: "react-suspense-out-of-order-boundary", boundaryId: `B:${id}`, segmentId: `S:${id}`, valueCode: asyncBoundary.valueCode, valueName: asyncBoundary.valueName, parts: replaceSuspenseAsyncBoundary( node.children, asyncBoundary, asyncBoundary.children, ).flatMap((child) => collectHtmlParts( child, escapeHelperName, asyncBoundaryHelperName, outOfOrderBoundaryHelperName, reactSuspenseBoundaryHelperName, reactSuspenseOutOfOrderBoundaryHelperName, state, ), ) as HtmlSyncPart[], fallbackParts: collectSuspenseFallbackParts( node.props, escapeHelperName, asyncBoundaryHelperName, outOfOrderBoundaryHelperName, reactSuspenseBoundaryHelperName, reactSuspenseOutOfOrderBoundaryHelperName, state, ), ...(state.reactSuspenseRevealScriptNonce === undefined ? {} : { nonce: state.reactSuspenseRevealScriptNonce }), ...(state.reactSuspenseRevealScriptSrc === undefined ? {} : { scriptSrc: state.reactSuspenseRevealScriptSrc }), ...(asyncBoundary.catchName === undefined || asyncBoundary.catchChildren === undefined ? {} : { catchName: asyncBoundary.catchName, catchParts: replaceSuspenseAsyncBoundary( node.children, asyncBoundary, asyncBoundary.catchChildren, ).flatMap((child) => collectHtmlParts( child, escapeHelperName, asyncBoundaryHelperName, outOfOrderBoundaryHelperName, reactSuspenseBoundaryHelperName, reactSuspenseOutOfOrderBoundaryHelperName, state, ), ) as HtmlSyncPart[], }), }, ]; } if (containsAsyncComponent(node.children)) { const id = state.nextFragmentId; state.nextFragmentId += 1; return [ { kind: "react-suspense-out-of-order-boundary", boundaryId: `B:${id}`, segmentId: `S:${id}`, valueCode: "undefined", valueName: "_", parts: node.children.flatMap((child) => collectHtmlParts( child, escapeHelperName, asyncBoundaryHelperName, outOfOrderBoundaryHelperName, reactSuspenseBoundaryHelperName, reactSuspenseOutOfOrderBoundaryHelperName, state, ), ) as HtmlSyncPart[], fallbackParts: collectSuspenseFallbackParts( node.props, escapeHelperName, asyncBoundaryHelperName, outOfOrderBoundaryHelperName, reactSuspenseBoundaryHelperName, reactSuspenseOutOfOrderBoundaryHelperName, state, ), ...(state.reactSuspenseRevealScriptNonce === undefined ? {} : { nonce: state.reactSuspenseRevealScriptNonce }), ...(state.reactSuspenseRevealScriptSrc === undefined ? {} : { scriptSrc: state.reactSuspenseRevealScriptSrc }), }, ]; } return [ { kind: "react-suspense-boundary", parts: node.children.flatMap((child) => collectHtmlParts( child, escapeHelperName, asyncBoundaryHelperName, outOfOrderBoundaryHelperName, reactSuspenseBoundaryHelperName, reactSuspenseOutOfOrderBoundaryHelperName, state, ), ) as HtmlSyncPart[], }, ]; } if (isClientBoundaryPlaceholder(node, state.hydration)) { const helperName = currentClientBoundaryHelperName; if (helperName !== undefined) { const hasComponentFallback = shouldRenderClientBoundaryFallback(node); const boundaryProps = emitPropsObject(node.props, [], escapeHelperName); const fallbackHtml = node.clientReference?.compatSsr === true ? `(_childrenHtml, _identifierPrefix, _props) => ${currentCompatRenderToStringHelperName}(${node.name}, _props, { identifierPrefix: _identifierPrefix, stringResult: "text" })` : hasComponentFallback ? `(_childrenHtml) => async (${currentClientBoundaryFallbackSinkName}) => { await ${node.name}(${currentClientBoundaryFallbackSinkName}, ${emitPropsObject(node.props, node.children, escapeHelperName, node.name, "_childrenHtml")}); }` : emitHtmlExpressionFromChildren(node.children, escapeHelperName); const originalChildrenHtml = hasComponentFallback ? (emitStreamRendererFromChildren(node.children, escapeHelperName, true) ?? emitHtmlExpressionFromChildren(node.children, escapeHelperName)) : undefined; const helperCall = `${helperName}(${stringLiteral(node.name)}, ${boundaryProps}, ${fallbackHtml}${originalChildrenHtml === undefined ? "" : `, true, ${originalChildrenHtml}, ${node.children.length > 0}${node.clientReference?.compatSsr === true ? ", true" : ""}`})`; return hasComponentFallback ? [ { kind: "stream-node", code: `async ($sink) => { $sink.append(await ${helperCall}); }`, escapeHelperName, }, ] : [{ kind: "raw-dynamic", code: helperCall }]; } return [{ kind: "static", value: clientBoundaryPlaceholder(node) }]; } return [ { kind: "component", name: node.name, ...(node.runtime === undefined ? {} : { runtime: node.runtime }), ...(node.async === undefined ? {} : { async: node.async }), ...(node.runtime === "compat" && state.hydration ? { hydrationId: `mreact-${state.nextFragmentId++}` } : {}), props: node.props, children: node.children, escapeHelperName, ...(state.selectedValueCode === undefined ? {} : { selectedValueCode: state.selectedValueCode }), ...(state.selectedMultipleCode === undefined ? {} : { selectedMultipleCode: state.selectedMultipleCode }), ...(state.selectionContextActive === true ? { selectionContextActive: true } : {}), }, ]; } const closeTag = ``; if (node.tagName === "textarea") { const attributeScan = scanElementAttributes(node.tagName, node.attributes); return [ { kind: "static", value: "" }, ...collectTextareaValueParts( node, escapeHelperName, asyncBoundaryHelperName, outOfOrderBoundaryHelperName, reactSuspenseBoundaryHelperName, reactSuspenseOutOfOrderBoundaryHelperName, state, attributeScan, ), { kind: "static", value: closeTag }, ]; } const attributeScan = scanElementAttributes(node.tagName, node.attributes); if (hasDynamicSelectSelectionAttribute(node)) { return [ emitBoundSelectPart( node, escapeHelperName, asyncBoundaryHelperName, outOfOrderBoundaryHelperName, reactSuspenseBoundaryHelperName, reactSuspenseOutOfOrderBoundaryHelperName, state, attributeScan, ), ]; } const childSelectedValueCode = node.tagName === "select" ? node.attributes.some((attr) => attr.kind === "spread-attr") ? currentSpreadSelectedValueName : attributeScan.formValueAttributeCode : undefined; const childSelectedMultipleCode = node.tagName === "select" ? node.attributes.some((attr) => attr.kind === "spread-attr") ? `${currentSpreadPropsName}.multiple` : attributeScan.multipleAttributeCode : undefined; const childState = node.tagName !== "select" ? state : { ...state, selectedValueCode: childSelectedValueCode, selectedMultipleCode: childSelectedMultipleCode, selectionContextActive: true, }; const selectedAttributePart = collectOptionSelectedAttributePart( node, state.selectedValueCode, state.selectedMultipleCode, node.attributes.some((attr) => attr.kind === "spread-attr"), ); const capturedOptionPart = emitCapturedOptionPart(node, escapeHelperName, state, attributeScan); if (capturedOptionPart !== undefined) { return [capturedOptionPart]; } if ( state.dynamicAttributes === "emit" && !isVoidHtmlElement(node.tagName) && node.attributes.some((attr) => attr.kind === "spread-attr") ) { const capturedOptionText = state.selectedValueCode === undefined || node.attributes.some( (attr) => attr.kind !== "spread-attr" && attr.name === "dangerouslySetInnerHTML", ) ? undefined : findCapturedOptionText(node, escapeHelperName); const spreadSelectedAttributePart = capturedOptionText === undefined ? selectedAttributePart : collectOptionSelectedAttributePart( node, state.selectedValueCode, state.selectedMultipleCode, true, capturedOptionText.valueCode, ); const fallbackParts = (childSelectedValueCode === undefined ? collectTextSeparatedSimpleChildrenParts( node.children, escapeHelperName, state.escapeBatchHelperName, ) : undefined) ?? node.children.flatMap((child) => collectHtmlParts( child, escapeHelperName, asyncBoundaryHelperName, outOfOrderBoundaryHelperName, reactSuspenseBoundaryHelperName, reactSuspenseOutOfOrderBoundaryHelperName, childState, ), ); return [ emitMergedSpreadElementPart( node.tagName, node.attributes, attributeScan, state.selectedValueCode, spreadSelectedAttributePart, fallbackParts, escapeHelperName, capturedOptionText, ), ]; } const dynamicOptionValueAttribute = findDynamicOptionValueAttribute(node); if (dynamicOptionValueAttribute !== undefined && state.selectedValueCode !== undefined) { return [ emitBoundOptionValuePart( node, escapeHelperName, asyncBoundaryHelperName, outOfOrderBoundaryHelperName, reactSuspenseBoundaryHelperName, reactSuspenseOutOfOrderBoundaryHelperName, state, attributeScan, dynamicOptionValueAttribute, ), ]; } const dangerousInnerHtml = emitDangerouslySetInnerHtmlPart( node.attributes, node.children, escapeHelperName, ); const childrenParts: HtmlPart[] = isVoidHtmlElement(node.tagName) ? [] : dangerousInnerHtml !== undefined ? [dangerousInnerHtml] : ((childSelectedValueCode === undefined ? collectTextSeparatedSimpleChildrenParts( node.children, escapeHelperName, state.escapeBatchHelperName, ) : undefined) ?? node.children.flatMap((child) => collectHtmlParts( child, escapeHelperName, asyncBoundaryHelperName, outOfOrderBoundaryHelperName, reactSuspenseBoundaryHelperName, reactSuspenseOutOfOrderBoundaryHelperName, childState, ), )); return [ { kind: "static", value: `<${node.tagName}` }, ...collectElementAttributeParts( node.tagName, node.attributes, escapeHelperName, state, attributeScan, ), ...(selectedAttributePart === undefined ? [] : [selectedAttributePart]), { kind: "static", value: ">" }, ...childrenParts, ...(isVoidHtmlElement(node.tagName) ? [] : [{ kind: "static" as const, value: closeTag }]), ]; } 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 emitBoundSelectPart( node: Extract, escapeHelperName: string, asyncBoundaryHelperName: string, outOfOrderBoundaryHelperName: string, reactSuspenseBoundaryHelperName: string, reactSuspenseOutOfOrderBoundaryHelperName: string, state: CollectHtmlState, attributeScan: ElementAttributeScan, ): HtmlPart { if (attributeScan.formValueAttributeCode === undefined) { return { kind: "static", value: "" }; } const selectionCode = emitSelectSelectionValueCode( currentOptionSelectedLocalNames.selectValueAttribute, currentOptionSelectedLocalNames.selectDefaultValue, ) ?? "undefined"; const attributeSetup = emitBoundSelectAttributeSetup(node, escapeHelperName, state); const selectedMultipleCode = attributeScan.multipleAttributeCode === undefined ? undefined : currentOptionSelectedLocalNames.selectMultiple; const childState: CollectHtmlState = { ...state, selectedValueCode: currentOptionSelectedLocalNames.selectValue, selectedMultipleCode, selectionContextActive: true, }; const dangerousInnerHtml = emitDangerouslySetInnerHtmlPart( node.attributes, node.children, escapeHelperName, ); const childrenParts: HtmlPart[] = dangerousInnerHtml !== undefined ? [dangerousInnerHtml] : node.children.flatMap((child) => collectHtmlParts( child, escapeHelperName, asyncBoundaryHelperName, outOfOrderBoundaryHelperName, reactSuspenseBoundaryHelperName, reactSuspenseOutOfOrderBoundaryHelperName, childState, ), ); const childExpressions = childrenParts.map((part) => isHtmlSyncPart(part) ? tryEmitPartAsStringExpression(part, currentCompatRenderToStringHelperName) : undefined, ); const innerHtml = childExpressions.length === 0 ? '""' : (childExpressions as string[]).join(" + "); const selectValueName = currentOptionSelectedLocalNames.selectValue; const attributesName = currentOptionSelectedLocalNames.attributes; const opening = `${stringLiteral(""`; const closing = stringLiteral(""); const prefix = `${attributeSetup} const ${selectValueName} = (${selectionCode});`; if (childExpressions.every((expression) => expression !== undefined)) { return { kind: "raw-dynamic", code: `(() => { ${prefix} return ${opening} + (${innerHtml}) + ${closing}; })()`, }; } const nestedStatements = emitNestedStreamAppendStatements( childrenParts, "$sink", currentCompatRenderToStringHelperName, ); return { kind: "stream-node", code: `async ($sink) => { ${prefix} $sink.append(${opening});\n${nestedStatements}\n$sink.append(${closing}); }`, escapeHelperName, }; } function emitBoundSelectAttributeSetup( node: Extract, escapeHelperName: string, state: CollectHtmlState, ): 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, state.escapeBatchHelperName, state.dynamicAttributes, attr.kind === "dynamic-attr" ? `(${multipleName} = (${attr.code}))` : undefined, ); const expressions = parts.map((part) => tryEmitPartAsStringExpression(part, currentCompatRenderToStringHelperName), ); statements.push( ...expressions.map((expression) => `${attributesName} += ${expression ?? '""'};`), ); if ( multipleCode !== undefined && (state.dynamicAttributes === "drop" || attr.kind !== "dynamic-attr") ) { statements.push(`${multipleName} = ${multipleCode};`); } continue; } const parts = collectHtmlAttributeParts( node.tagName, attr, escapeHelperName, state.escapeBatchHelperName, state.dynamicAttributes, ); const expressions = parts.map((part) => tryEmitPartAsStringExpression(part, currentCompatRenderToStringHelperName), ); statements.push( ...expressions.map((expression) => `${attributesName} += ${expression ?? '""'};`), ); } return statements.join(" "); } function emitBoundOptionValuePart( node: Extract, escapeHelperName: string, asyncBoundaryHelperName: string, outOfOrderBoundaryHelperName: string, reactSuspenseBoundaryHelperName: string, reactSuspenseOutOfOrderBoundaryHelperName: string, state: CollectHtmlState, attributeScan: ElementAttributeScan, valueAttribute: Extract, ): HtmlPart { 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, state, attributeScan, valueAttribute, `(${boundValueName} = (${valueAttribute.code}))`, ); const attributeExpressions = attributes.map((part) => tryEmitPartAsStringExpression(part, currentCompatRenderToStringHelperName), ); const attributesCode = attributeExpressions.length === 0 ? '""' : (attributeExpressions as string[]).join(" + "); const boundValueInitialization = state.dynamicAttributes === "drop" ? `${boundValueName} = (${valueAttribute.code});` : ""; const selectedPart = collectOptionSelectedAttributePart( node, state.selectedValueCode, state.selectedMultipleCode, false, capturedOptionText?.valueCode, boundValueName, ) ?? { kind: "raw-dynamic" as const, code: '""' }; const selectedExpression = tryEmitPartAsStringExpression( selectedPart, currentCompatRenderToStringHelperName, ); const optionTextDeclaration = capturedOptionText?.declaration ?? ""; const prefix = `let ${boundValueName}; const ${currentOptionSelectedLocalNames.attributes} = ${attributesCode}; ${boundValueInitialization} ${optionTextDeclaration}`; if (capturedOptionText !== undefined) { return { kind: "raw-dynamic", code: `(() => { ${prefix} return ${stringLiteral("" + ${capturedOptionText.bodyCode} + ${stringLiteral("")}; })()`, }; } const dangerousInnerHtml = emitDangerouslySetInnerHtmlPart( node.attributes, node.children, escapeHelperName, ); const childrenParts: HtmlPart[] = dangerousInnerHtml !== undefined ? [dangerousInnerHtml] : (collectTextSeparatedSimpleChildrenParts( node.children, escapeHelperName, state.escapeBatchHelperName, ) ?? node.children.flatMap((child) => collectHtmlParts( child, escapeHelperName, asyncBoundaryHelperName, outOfOrderBoundaryHelperName, reactSuspenseBoundaryHelperName, reactSuspenseOutOfOrderBoundaryHelperName, state, ), )); const childExpressions = childrenParts.map((part) => isHtmlSyncPart(part) ? tryEmitPartAsStringExpression(part, currentCompatRenderToStringHelperName) : undefined, ); if (childExpressions.every((expression) => expression !== undefined)) { const innerHtml = childExpressions.length === 0 ? '""' : (childExpressions as string[]).join(" + "); return { kind: "raw-dynamic", code: `(() => { ${prefix} return ${stringLiteral("" + (${innerHtml}) + ${stringLiteral("")}; })()`, }; } const nestedStatements = emitNestedStreamAppendStatements( childrenParts, "$sink", currentCompatRenderToStringHelperName, ); return { kind: "stream-node", code: `async ($sink) => { ${prefix} $sink.append(${stringLiteral("");\n${nestedStatements}\n$sink.append(${stringLiteral("")}); }`, escapeHelperName, }; } function emitDangerouslySetInnerHtmlPart( attrs: readonly AttributeIr[], children: JsxNodeIr[], escapeHelperName: string, ): HtmlPart | undefined { if (attrs.some((attr) => attr.kind === "spread-attr")) { const assignments = attrs.flatMap((attr): string[] => { if (attr.kind === "spread-attr") { return [`${currentSpreadAttributesHelperName}$assign(_props, (${attr.code}) ?? {});`]; } return attr.kind === "dynamic-attr" && attr.name === "dangerouslySetInnerHTML" ? [`_props.dangerouslySetInnerHTML = (${attr.code});`] : []; }); if (!children.some(containsAnyAsyncBoundary)) { return { kind: "raw-dynamic", code: `(() => { const _props = {}; ${assignments.join(" ")} if (!Object.prototype.hasOwnProperty.call(_props, "dangerouslySetInnerHTML")) return ${emitHtmlExpressionFromChildren(children, escapeHelperName)}; return ${emitExactDangerouslySetInnerHtmlExpression("_props.dangerouslySetInnerHTML")}; })()`, }; } const fallbackRenderer = emitStreamRendererFromChildren(children, escapeHelperName, true); const fallbackCode = fallbackRenderer === undefined ? "" : ` else { await (${fallbackRenderer})($sink); }`; return { kind: "stream-node", code: `async ($sink) => { const _props = {}; ${assignments.join(" ")} if (Object.prototype.hasOwnProperty.call(_props, "dangerouslySetInnerHTML")) { $sink.append(${emitExactDangerouslySetInnerHtmlExpression("_props.dangerouslySetInnerHTML")}); }${fallbackCode} }`, escapeHelperName, }; } let attr: Extract | undefined; for (let index = attrs.length - 1; index >= 0; index -= 1) { const candidate = attrs[index]; if (candidate?.kind === "dynamic-attr" && candidate.name === "dangerouslySetInnerHTML") { attr = candidate; break; } } if (attr === undefined) { return undefined; } return { kind: "raw-dynamic", code: emitExactDangerouslySetInnerHtmlExpression(attr.code), }; } function emitMergedSpreadElementPart( tagName: string, attrs: readonly AttributeIr[], attributeScan: ElementAttributeScan, selectedValueCode: string | undefined, selectedAttributePart: HtmlSyncPart | undefined, fallbackParts: HtmlPart[], escapeHelperName: string, capturedOptionText?: CapturedOptionText, ): HtmlPart { const propsName = currentSpreadPropsName; const omitSelectedCode = tagName === "option" && selectedValueCode !== undefined ? `(${selectedValueCode}) != null` : "false"; const selectedValueDeclaration = tagName === "select" ? `const ${currentSpreadSelectedValueName} = ${emitSelectSelectionValueCode(`${propsName}.value`, `${propsName}.defaultValue`) ?? "undefined"};` : ""; const assignments = emitMergedSpreadPropsAssignments( tagName, attrs, attributeScan, propsName, true, ); const selectedExpression = selectedAttributePart === undefined ? undefined : tryEmitPartAsStringExpression(selectedAttributePart, currentCompatRenderToStringHelperName); const opening = `${stringLiteral(`<${tagName}`)} + ${currentSpreadAttributesHelperName}(${stringLiteral(tagName)}, ${propsName}, ${omitSelectedCode})${selectedExpression === undefined ? "" : ` + (${selectedExpression})`} + ">"`; const closing = stringLiteral(``); const fallbackExpressions = fallbackParts.map((part) => isHtmlSyncPart(part) ? tryEmitPartAsStringExpression(part, currentCompatRenderToStringHelperName) : undefined, ); const optionTextDeclaration = capturedOptionText === undefined ? "" : capturedOptionText.declaration; if (capturedOptionText !== undefined) { return { kind: "raw-dynamic", code: `(() => { const ${propsName} = {}; ${assignments.join(" ")} ${selectedValueDeclaration}${optionTextDeclaration} return ${opening} + ${capturedOptionText.bodyCode} + ${closing}; })()`, }; } if (fallbackExpressions.every((expression) => expression !== undefined)) { const fallback = fallbackExpressions.length === 0 ? '""' : (fallbackExpressions as string[]).join(" + "); const innerHtml = `Object.prototype.hasOwnProperty.call(${propsName}, "dangerouslySetInnerHTML") ? ${emitExactDangerouslySetInnerHtmlExpression(`${propsName}.dangerouslySetInnerHTML`)} : (${fallback})`; return { kind: "raw-dynamic", code: `(() => { const ${propsName} = {}; ${assignments.join(" ")} ${selectedValueDeclaration} return ${opening} + (${innerHtml}) + ${closing}; })()`, }; } const fallbackStatements = emitNestedStreamAppendStatements( fallbackParts, "$sink", currentCompatRenderToStringHelperName, ); return { kind: "stream-node", code: `async ($sink) => { const ${propsName} = {}; ${assignments.join(" ")} ${selectedValueDeclaration} $sink.append(${opening}); if (Object.prototype.hasOwnProperty.call(${propsName}, "dangerouslySetInnerHTML")) { $sink.append(${emitExactDangerouslySetInnerHtmlExpression(`${propsName}.dangerouslySetInnerHTML`)}); } else {\n${fallbackStatements}\n} $sink.append(${closing}); }`, escapeHelperName, }; } function emitExactDangerouslySetInnerHtmlExpression(code: string): string { return `(() => { const _value = (${code}); if (typeof _value !== "object" || _value === null) return ""; try { const _descriptor = Object.getOwnPropertyDescriptor(_value, "__html"); return _descriptor !== undefined && "value" in _descriptor && typeof _descriptor.value === "string" ? _descriptor.value : ""; } catch { return ""; } })()`; } function isChildrenExpressionCode(code: string): boolean { const trimmed = code.trim(); return ( trimmed === "children" || endsWithChildrenMemberAccess(trimmed) || endsWithChildrenStringIndex(trimmed) ); } function endsWithChildrenMemberAccess(code: string): boolean { const propertyName = "children"; if (!code.endsWith(propertyName)) { return false; } return code[code.length - propertyName.length - 1] === "."; } function endsWithChildrenStringIndex(code: string): boolean { return code.endsWith('["children"]') || code.endsWith("['children']"); } function collectHtmlAttributeParts( tagName: string, attr: AttributeIr, escapeHelperName: string, escapeBatchHelperName: string | undefined, dynamicAttributes: "drop" | "emit", valueCodeOverride?: string, ): HtmlSyncPart[] { if (attr.kind === "dom-ref") { return []; } if (attr.kind === "spread-attr") { return dynamicAttributes === "drop" ? [] : [ { kind: "raw-dynamic", code: `${currentSpreadAttributesHelperName}(${stringLiteral(tagName)}, (${attr.code}))`, }, ]; } if (attr.kind === "event" || attr.name === "key" || attr.name === "dangerouslySetInnerHTML") { return []; } if (attr.kind === "static-attr") { const htmlName = htmlAttributeNameForElement(tagName, attr.name); if (isUrlAttribute(htmlName) && isStaticUrlValueUnsafe(htmlName, attr.value)) { return []; } if (isDangerousHtmlAttribute(htmlName)) { // Issue 077: literal srcdoc strings cannot match the opt-in shape. return []; } return [ { kind: "static", value: ` ${htmlName}="${escapeHtml(attr.value)}"`, }, ]; } if (dynamicAttributes === "drop") { return []; } const valueCode = valueCodeOverride ?? attr.code; if (attr.name === "style") { if (attr.serialization !== "compat" && attr.omitServerRenderValue === true) { return [ { kind: "raw-dynamic", code: emitDynamicStyleAttributeExpression( valueCode, escapeHelperName, escapeBatchHelperName, currentContainsServerRenderValueHelperName, ), }, ]; } return [ { kind: "raw-dynamic", code: emitDynamicAttributeWithServerRenderValueOmission( attr, (code) => attr.serialization === "compat" ? emitCompatDynamicStyleAttributeExpression(code, escapeHelperName) : emitDynamicStyleAttributeExpression( code, escapeHelperName, escapeBatchHelperName, attr.omitServerRenderValue === true ? currentContainsServerRenderValueHelperName : undefined, ), valueCode, ), }, ]; } const dynamicHtmlName = htmlAttributeNameForElement(tagName, attr.name); if (isDangerousHtmlAttribute(dynamicHtmlName)) { return [ { kind: "raw-dynamic", code: emitDynamicAttributeWithServerRenderValueOmission( attr, (code) => `(() => { const _value = (${code}); if (typeof _value !== "object" || _value === null) return ""; try { const _descriptor = Object.getOwnPropertyDescriptor(_value, "__html"); if (_descriptor !== undefined && "value" in _descriptor && typeof _descriptor.value === "string") return ${stringLiteral(` ${dynamicHtmlName}="`)} + ${escapeHelperName}(_descriptor.value) + ${stringLiteral('"')}; return ""; } catch { return ""; } })()`, valueCode, ), }, ]; } return [ { kind: "raw-dynamic", code: emitDynamicAttributeWithServerRenderValueOmission( attr, (code) => attr.serialization === "compat" && !isUrlAttribute(dynamicHtmlName) ? emitCompatDynamicAttributeExpression(dynamicHtmlName, code, escapeHelperName) : emitDynamicAttributeExpression(dynamicHtmlName, code, escapeHelperName), valueCode, ), }, ]; } function emitDynamicAttributeWithServerRenderValueOmission( attr: Extract, emit: (code: string) => string, valueCodeOverride?: string, ): string { const valueCode = valueCodeOverride ?? attr.code; if (attr.omitServerRenderValue !== true) { return emit(valueCode); } if (simpleSideEffectFreeExpression(valueCode)) { return `${currentContainsServerRenderValueHelperName}(${valueCode}) ? "" : (${emit(valueCode)})`; } const valueName = currentServerRenderAttributeValueName; return `(() => { const ${valueName} = (${valueCode}); return ${currentContainsServerRenderValueHelperName}(${valueName}) ? "" : (${emit(valueName)}); })()`; } function collectElementAttributeParts( tagName: string, attrs: readonly AttributeIr[], escapeHelperName: string, state: CollectHtmlState, attributeScan = scanElementAttributes(tagName, attrs), optionValueAttribute?: Extract, optionValueCodeOverride?: string, ): HtmlSyncPart[] { const escapeBatchHelperName = state.escapeBatchHelperName; if (state.dynamicAttributes === "emit" && attrs.some((attr) => attr.kind === "spread-attr")) { return [ { kind: "raw-dynamic", code: emitMergedSpreadAttributeExpression(tagName, attrs, attributeScan), }, ]; } return attrs.flatMap((attr) => { if ( attr.kind !== "spread-attr" && ((tagName === "input" && ((attr.name === "defaultValue" && attributeScan.hasExplicitInputValue) || (attr.name === "defaultChecked" && attributeScan.hasExplicitInputChecked))) || ((tagName === "textarea" || tagName === "select") && (attr.name === "value" || attr.name === "defaultValue")) || isSuppressedOptionSelectedAttribute(tagName, attr.name, state)) ) { return []; } return collectHtmlAttributeParts( tagName, attr, escapeHelperName, escapeBatchHelperName, state.dynamicAttributes, attr === optionValueAttribute ? optionValueCodeOverride : undefined, ); }); } function emitMergedSpreadAttributeExpression( tagName: string, attrs: readonly AttributeIr[], attributeScan: ElementAttributeScan, ): string { const propsName = currentSpreadPropsName; const statements = emitMergedSpreadPropsAssignments( tagName, attrs, attributeScan, propsName, false, ); return `(() => { const ${propsName} = {}; ${statements.join(" ")} return ${currentSpreadAttributesHelperName}(${stringLiteral(tagName)}, ${propsName}); })()`; } function emitMergedSpreadPropsAssignments( tagName: string, attrs: readonly AttributeIr[], attributeScan: ElementAttributeScan, propsName: string, includeDangerouslySetInnerHtml: boolean, ): string[] { return attrs.flatMap((attr): string[] => { if ( attr.kind !== "spread-attr" && ((tagName === "input" && ((attr.name === "defaultValue" && attributeScan.hasExplicitInputValue) || (attr.name === "defaultChecked" && attributeScan.hasExplicitInputChecked))) || (tagName === "textarea" && (attr.name === "value" || attr.name === "defaultValue"))) ) { return []; } if (attr.kind === "spread-attr") { return [`${currentSpreadAttributesHelperName}$assign(${propsName}, (${attr.code}) ?? {});`]; } if (attr.kind === "event" || attr.name === "key") { return []; } if (attr.name === "dangerouslySetInnerHTML" && !includeDangerouslySetInnerHtml) return []; const valueCode = attr.kind === "static-attr" ? stringLiteral(attr.value) : attr.kind === "dynamic-attr" && attr.omitServerRenderValue === true ? `(() => { const _value = (${attr.code}); return ${currentContainsServerRenderValueHelperName}(_value) ? undefined : _value; })()` : `(${attr.code})`; return [`${propsName}[${stringLiteral(attr.name)}] = ${valueCode};`]; }); } interface ElementAttributeScan { hasExplicitInputValue: boolean; hasExplicitInputChecked: boolean; formValueAttributeCode: string | undefined; multipleAttributeCode: string | undefined; } function scanElementAttributes( tagName: string, attrs: readonly AttributeIr[], ): ElementAttributeScan { let hasExplicitInputValue = false; let hasExplicitInputChecked = false; let valueAttributeCode: string | undefined; let defaultValueAttributeCode: string | undefined; let multipleAttributeCode: string | undefined; for (const attr of attrs) { if (attr.kind === "spread-attr") { continue; } if (tagName === "input") { if (attr.name === "value") { hasExplicitInputValue = true; } else if (attr.name === "checked") { hasExplicitInputChecked = true; } } if ((tagName === "textarea" || tagName === "select") && attr.name === "value") { valueAttributeCode = readFormValueAttributeCode(attr); } else if ((tagName === "textarea" || tagName === "select") && attr.name === "defaultValue") { defaultValueAttributeCode = readFormValueAttributeCode(attr); } if (tagName === "select" && attr.name === "multiple") { multipleAttributeCode = readBooleanAttributeCode(attr); } } return { hasExplicitInputValue, hasExplicitInputChecked, formValueAttributeCode: tagName === "select" ? emitSelectSelectionValueCode(valueAttributeCode, defaultValueAttributeCode) : (valueAttributeCode ?? defaultValueAttributeCode), multipleAttributeCode, }; } function readFormValueAttributeCode( attr: Exclude, ): string | undefined { if (attr.kind === "event") { return undefined; } return attr.kind === "static-attr" ? stringLiteral(attr.value) : `(${attr.code})`; } function readBooleanAttributeCode( attr: Exclude, ): string | undefined { if (attr.kind === "event") { return undefined; } return attr.kind === "static-attr" ? attr.value === "" ? "true" : stringLiteral(attr.value) : `(${attr.code})`; } function emitDynamicAttributeExpression( name: string, code: string, escapeHelperName: string, ): string { const booleanishString = isBooleanishStringAttribute(name); if (isUrlAttribute(name)) { return `(() => { const _value = (${code}); if (_value == null || _value === false) return ""; const _checked = ${currentUrlSafeHelperName}(${stringLiteral(name)}, _value === true ? "" : _value); return _checked === undefined ? "" : ${stringLiteral(` ${name}="`)} + ${escapeHelperName}(_checked) + ${stringLiteral('"')}; })()`; } const inlineExpr = simpleSideEffectFreeExpression(code); if (inlineExpr !== undefined) { // Inline 3 evaluations to avoid per-attribute IIFE closure allocation. return booleanishString ? `(${inlineExpr} == null ? "" : ${stringLiteral(` ${name}="`)} + ${escapeHelperName}(${inlineExpr}) + ${stringLiteral('"')})` : `(${inlineExpr} == null || ${inlineExpr} === false ? "" : ${stringLiteral(` ${name}="`)} + ${escapeHelperName}(${inlineExpr} === true ? "" : ${inlineExpr}) + ${stringLiteral('"')})`; } return booleanishString ? `(() => { const _value = (${code}); return _value == null ? "" : ${stringLiteral(` ${name}="`)} + ${escapeHelperName}(_value) + ${stringLiteral('"')}; })()` : `(() => { const _value = (${code}); return _value == null || _value === false ? "" : ${stringLiteral(` ${name}="`)} + ${escapeHelperName}(_value === true ? "" : _value) + ${stringLiteral('"')}; })()`; } function emitCompatDynamicAttributeExpression( name: string, code: string, escapeHelperName: string, ): string { const lowerCased = name.toLowerCase(); const booleanishOrData = lowerCased.startsWith("aria-") || lowerCased.startsWith("data-") || lowerCased === "contenteditable" || lowerCased === "draggable" || lowerCased === "spellcheck"; const booleanBranch = booleanishOrData ? `return ${stringLiteral(` ${name}="`)} + (_value ? "true" : "false") + ${stringLiteral('"')};` : `return _value ? ${stringLiteral(` ${name}=""`)} : "";`; return `(() => { const _value = (${code}); if (_value == null || typeof _value === "function") return ""; if (typeof _value === "boolean") { ${booleanBranch} } if (typeof _value === "object") return ""; return ${stringLiteral(` ${name}="`)} + ${escapeHelperName}(_value) + ${stringLiteral('"')}; })()`; } function emitCompatDynamicStyleAttributeExpression(code: string, escapeHelperName: string): string { const unitlessCheck = '_styleName === "flex" || _styleName === "fontWeight" || _styleName === "lineHeight" || _styleName === "opacity" || _styleName === "order" || _styleName === "zIndex" || _styleName === "zoom"'; return `(() => { const _value = (${code}); if (_value == null || typeof _value !== "object") return ""; let _style = ""; for (const _styleName in _value) { const _styleValue = _value[_styleName]; if (_styleValue == null || typeof _styleValue === "boolean" || _styleValue === "") continue; const _cssName = _styleName.startsWith("--") ? _styleName : _styleName.replace(/[A-Z]/g, (_char) => "-" + _char.toLowerCase()); const _css = typeof _styleValue !== "number" || _styleValue === 0 || (${unitlessCheck}) ? String(_styleValue) : _styleValue + "px"; _style += (_style === "" ? "" : ";") + ${escapeHelperName}(_cssName) + ":" + ${escapeHelperName}(_css); } return _style === "" ? "" : ${stringLiteral(' style="')} + _style + ${stringLiteral('"')}; })()`; } function emitDynamicStyleAttributeExpression( code: string, escapeHelperName: string, escapeBatchHelperName: string | undefined, containsServerRenderValueHelperName?: string, ): string { const staticStyleExpression = emitStaticStyleObjectAttributeExpression( code, escapeHelperName, containsServerRenderValueHelperName, ); if (staticStyleExpression !== undefined) { return staticStyleExpression; } const escapedPair = escapeBatchHelperName === undefined ? `${escapeHelperName}(_cssName) + ":" + ${escapeHelperName}(_styleValue === true ? "" : _styleValue)` : `(() => { const _escaped = ${escapeBatchHelperName}([_cssName, _styleValue === true ? "" : _styleValue]); return _escaped[0] + ":" + _escaped[1]; })()`; const renderValueGuard = containsServerRenderValueHelperName === undefined ? "" : ` if (_entries.some(([, _styleValue]) => ${containsServerRenderValueHelperName}(_styleValue))) return "";`; return `(() => { const _value = (${code}); if (_value == null || _value === false) return ""; if (typeof _value === "string") { const _style = ${escapeHelperName}(_value); return _style === "" ? "" : ${stringLiteral(' style="')} + _style + ${stringLiteral('"')}; } const _entries = Object.entries(_value);${renderValueGuard} const _style = _entries.filter(([, _styleValue]) => _styleValue != null && _styleValue !== false).map(([_styleName, _styleValue]) => { const _cssName = String(_styleName).startsWith("--") ? String(_styleName) : String(_styleName).replace(/[A-Z]/g, (_char) => "-" + _char.toLowerCase()); return ${escapedPair}; }).join(";"); return _style === "" ? "" : ${stringLiteral(' style="')} + _style + ${stringLiteral('"')}; })()`; } function emitStaticStyleObjectAttributeExpression( code: string, escapeHelperName: string, containsServerRenderValueHelperName?: string, ): string | undefined { const entries = parseStaticStyleObjectLiteral(code); if (entries === undefined) { return undefined; } if (entries.length === 0) { return `""`; } const literalEntries = entries.map((entry) => ({ cssName: entry.cssName, literal: parseStyleLiteralValue(entry.valueCode), })); if (literalEntries.every((entry) => entry.literal !== undefined)) { const parts = literalEntries .filter((entry) => entry.literal !== null) .map((entry) => `${entry.cssName}:${escapeHtml(String(entry.literal))}`); if (parts.length === 0) { return `""`; } return stringLiteral(` style="${parts.join(";")}"`); } const statements = entries.map( (entry) => `{ const _v = (${entry.valueCode});${containsServerRenderValueHelperName === undefined ? "" : ` if (${containsServerRenderValueHelperName}(_v)) return "";`} if (_v != null && _v !== false) _style += (_style === "" ? "" : ";") + ${stringLiteral(`${entry.cssName}:`)} + ${escapeHelperName}(_v === true ? "" : _v); }`, ); return `(() => { let _style = ""; ${statements.join(" ")} return _style === "" ? "" : ${stringLiteral(' style="')} + _style + ${stringLiteral('"')}; })()`; } function collectTextareaValueParts( node: Extract, escapeHelperName: string, asyncBoundaryHelperName: string, outOfOrderBoundaryHelperName: string, reactSuspenseBoundaryHelperName: string, reactSuspenseOutOfOrderBoundaryHelperName: string, state: CollectHtmlState, attributeScan = scanElementAttributes(node.tagName, node.attributes), ): HtmlPart[] { const valueCode = attributeScan.formValueAttributeCode; if (valueCode !== undefined) { return [{ kind: "dynamic", code: valueCode, escapeHelperName }]; } return node.children.flatMap((child) => collectHtmlParts( child, escapeHelperName, asyncBoundaryHelperName, outOfOrderBoundaryHelperName, reactSuspenseBoundaryHelperName, reactSuspenseOutOfOrderBoundaryHelperName, state, ), ); } function collectOptionSelectedAttributePart( node: Extract, selectedValueCode: string | undefined, selectedMultipleCode: string | undefined, useMergedSpreadFallback = false, textValueCodeOverride?: string, optionValueCodeOverride?: string, ): HtmlSyncPart | undefined { if (selectedValueCode === undefined || node.tagName !== "option") { return undefined; } const optionValueCode = findOptionValueCode( node, currentOptionSelectedLocalNames.textValue, node.attributes.some((attr) => attr.kind === "spread-attr") ? currentSpreadPropsName : undefined, textValueCodeOverride, optionValueCodeOverride, ); return { kind: "raw-dynamic", code: emitOptionSelectedAttributeCode( selectedValueCode, optionValueCode, useMergedSpreadFallback ? '""' : emitOwnSelectedFallbackCode(node), currentOptionSelectedLocalNames, selectedMultipleCode, ), }; } function emitCapturedOptionPart( node: Extract, escapeHelperName: string, state: CollectHtmlState, attributeScan: ElementAttributeScan, ): HtmlPart | undefined { if ( state.selectedValueCode === undefined || node.tagName !== "option" || node.attributes.some((attr) => attr.kind === "spread-attr") || node.attributes.some( (attr) => attr.kind !== "spread-attr" && attr.name === "dangerouslySetInnerHTML", ) ) { return undefined; } const capturedOptionText = findCapturedOptionText(node, escapeHelperName); if (capturedOptionText === undefined) { return undefined; } const textValueName = capturedOptionText.name; const attributes = collectElementAttributeParts( node.tagName, node.attributes, escapeHelperName, state, attributeScan, ); const attributeExpressions = attributes.map((part) => tryEmitPartAsStringExpression(part, currentCompatRenderToStringHelperName), ); if (attributeExpressions.some((expression) => expression === undefined)) { return undefined; } const attributesCode = attributeExpressions.length === 0 ? '""' : (attributeExpressions as string[]).join(" + "); const optionValueCode = findOptionValueCode( node, textValueName, undefined, capturedOptionText.valueCode, ); const selectedAttribute = emitOptionSelectedAttributeCode( state.selectedValueCode, optionValueCode, emitOwnSelectedFallbackCode(node), currentOptionSelectedLocalNames, state.selectedMultipleCode, ); return { kind: "raw-dynamic", code: `(() => { const ${currentOptionSelectedLocalNames.attributes} = ${attributesCode}; ${capturedOptionText.declaration} return ${stringLiteral("" + ${capturedOptionText.bodyCode} + ${stringLiteral("")}; })()`, }; } type CapturableOptionTextChild = Extract; type CapturedOptionText = { name: string; valueCode: string; bodyCode: string; declaration: string; }; function findCapturableOptionTextChildren( node: Extract, ): CapturableOptionTextChild[] | undefined { let hasDynamicText = false; for (const child of node.children) { if (child.kind === "text") { continue; } if ( child.kind === "expr" && (child.renderMode === undefined || child.renderMode === "dynamic" || child.renderMode === "compiler-keyed-initial-text" || child.renderMode === "compiler-keyed-cell-text" || child.renderMode === "compiler-keyed-text") && !isChildrenExpressionCode(child.code) ) { hasDynamicText = true; continue; } return undefined; } return hasDynamicText ? (node.children as CapturableOptionTextChild[]) : undefined; } function findCapturedOptionText( node: Extract, escapeHelperName: string, ): CapturedOptionText | undefined { const children = findCapturableOptionTextChildren(node); if (children === undefined) { return undefined; } const name = currentOptionSelectedLocalNames.textValue; const partsName = currentOptionSelectedLocalNames.textParts; const bodyName = currentOptionSelectedLocalNames.textBody; const hasTextName = currentOptionSelectedLocalNames.textHasValue; const indexName = currentOptionSelectedLocalNames.index; const parts = children.map((child) => child.kind === "text" ? stringLiteral(child.value) : `String((${child.code}) ?? "")`, ); const partsCode = `(${partsName} ??= [${parts.join(", ")}])`; return { name, valueCode: `(${name} ??= ${partsCode}.join(""))`, bodyCode: `(() => { let ${bodyName} = ""; let ${hasTextName} = false; for (const ${indexName} of ${partsCode}) { if (${indexName} !== "") { if (${hasTextName}) ${bodyName} += ""; ${bodyName} += ${escapeHelperName}(${indexName}); ${hasTextName} = true; } } return ${bodyName}; })()`, declaration: `let ${partsName}; let ${name};`, }; } /** * `