import type { JsxNodeIr } from "./ir.js"; import { readArray, readObject, readSource, unwrapOxcParentheses } from "./oxc-node-utils.js"; interface ReactiveAliasReplacement { end: number; name: string; start: number; text: string; } interface ReactiveAliasExpressionState { reactive: boolean; safe: boolean; } export const OXC_COMPUTED_REACTIVE_ALIAS_PLACEHOLDER = "__mreactComputedReactiveAlias"; export const OXC_UNTRACK_REACTIVE_ALIAS_PLACEHOLDER = "__mreactUntrackReactiveAlias"; export function collectOxcBodyJsxBindingNames( statements: readonly unknown[], jsxReturnFunctionNames: ReadonlySet = new Set(), ): Set { const names = new Set(); for (const statement of statements) { const object = readObject(statement); if (object.type === "ForOfStatement" || object.type === "ForStatement") { collectOxcPushJsxBindingNames(readArray(readObject(object.body).body), names); continue; } if (object.type !== "VariableDeclaration") { continue; } for (const declarationValue of readArray(object.declarations)) { const declaration = readObject(declarationValue); const id = readObject(declaration.id); const initializer = unwrapOxcParentheses(readObject(declaration.init)); if (typeof id.name !== "string") continue; if (!isJsxLikeInitializer(initializer, jsxReturnFunctionNames)) continue; names.add(id.name); } } return names; } export function collectOxcReactiveReadAliases( code: string, statements: readonly unknown[], reactiveDerivedFunctions: ReadonlySet = new Set(), memoizeComputedKeys = false, ): Map { const aliases = new Map(); for (const statement of statements) { const object = readObject(statement); if (object.type !== "VariableDeclaration" || object.kind !== "const") { continue; } for (const declarationValue of readArray(object.declarations)) { const declaration = readObject(declarationValue); const id = readObject(declaration.id); const initializer = unwrapOxcParentheses(readObject(declaration.init)); if ( !isOxcReactiveAliasExpression(initializer, aliases) && !isOxcReactiveDerivedAliasExpression(initializer, reactiveDerivedFunctions) ) { continue; } const initializerCode = rewriteOxcReactiveAliasExpressionCode(code, initializer, aliases) ?? readSource(code, initializer); if (typeof id.name === "string") { aliases.set(id.name, initializerCode); continue; } for (const [name, expressionCode] of collectOxcPatternReactiveAliases( code, id, initializerCode, memoizeComputedKeys, )) { aliases.set(name, expressionCode); } } } return aliases; } export function collectOxcReactiveJsxBindingNames( statements: readonly unknown[], aliases: ReadonlyMap, ): Set { const names = new Set(); for (const statementValue of statements) { const statement = readObject(statementValue); if (statement.type !== "VariableDeclaration") continue; for (const declarationValue of readArray(statement.declarations)) { const declaration = readObject(declarationValue); const id = readObject(declaration.id); const initializer = unwrapOxcParentheses(readObject(declaration.init)); if ( typeof id.name === "string" && containsOxcJsxSyntax(initializer) && containsOxcReactiveDependency(initializer, aliases) ) { names.add(id.name); } } } let changed = true; while (changed) { changed = false; for (const statementValue of statements) { const statement = readObject(statementValue); if (statement.type !== "VariableDeclaration" || statement.kind !== "const") continue; for (const declarationValue of readArray(statement.declarations)) { const declaration = readObject(declarationValue); const id = readObject(declaration.id); const initializer = unwrapOxcParentheses(readObject(declaration.init)); if ( typeof id.name === "string" && initializer.type === "Identifier" && typeof initializer.name === "string" && names.has(initializer.name) && !names.has(id.name) ) { names.add(id.name); changed = true; } } } } return names; } export function formatOxcUntrackedReactiveAliasDeclaration( code: string, statementValue: unknown, aliases: ReadonlyMap, ownedAliases: ReadonlyMap = aliases, loweredDeclarators?: ReadonlyMap, ): string | undefined { const statement = readObject(statementValue); if (statement.type !== "VariableDeclaration" || statement.kind !== "const") { return undefined; } const statementStart = readNumber(statement.start); const statementEnd = readNumber(statement.end); if (statementStart === undefined || statementEnd === undefined) { return undefined; } const replacements: ReactiveAliasReplacement[] = []; const computedKeys = new Map>(); const patternDeclarations: string[] = []; for (const declarationValue of readArray(statement.declarations)) { const declaration = readObject(declarationValue); const id = readObject(declaration.id); const initializer = unwrapOxcParentheses(readObject(declaration.init)); const start = readNumber(initializer.start); const end = readNumber(initializer.end); const loweredDeclarator = loweredDeclarators?.get(declarationValue); if (loweredDeclarator !== undefined) { const declarationStart = readNumber(declaration.start); const declarationEnd = readNumber(declaration.end); if (declarationStart !== undefined && declarationEnd !== undefined) { replacements.push({ start: declarationStart, end: declarationEnd, name: String(id.name), text: loweredDeclarator, }); } continue; } const hasAlias = (typeof id.name === "string" && aliases.has(id.name)) || (id.type !== "Identifier" && hasOxcReactiveAliasBinding(id, aliases)); if (!hasAlias || start === undefined || end === undefined) { continue; } const owned = typeof id.name === "string" ? ownedAliases.has(id.name) : collectOxcReactiveAliasBindingNames(id, aliases).every((name) => ownedAliases.has(name)); if (containsArrayPattern(id)) { const names = collectOxcReactiveAliasBindingNames(id, aliases); const cacheName = patternBindingName(readNumber(id.start) ?? start); const keys = new Map>(); collectOxcComputedKeyNodes(id, keys); let patternCode = readSource(code, id); for (const [keyStart, key] of [...keys].sort(([a], [b]) => b - a)) { const keyEnd = readNumber(key.end); if (keyEnd === undefined) continue; const keyName = computedKeyBindingName(keyStart); computedKeys.set(keyStart, key); const offset = readNumber(id.start) ?? start; patternCode = patternCode.slice(0, keyStart - offset) + `(${cacheName}Ready ? ${keyName} : (${keyName} = ${OXC_UNTRACK_REACTIVE_ALIAS_PLACEHOLDER}(() => (${readSource(code, key)}))))` + patternCode.slice(keyEnd - offset); } // The readiness flag only exists so a computed key inside the pattern is // evaluated once and reused on every recomputation. Without a computed // key nothing reads it, so emitting it would be dead code. const tracksReadiness = keys.size > 0; if (tracksReadiness) { patternDeclarations.push(`let ${cacheName}Ready = false;`); } const initializerCode = rewriteOxcReactiveAliasExpressionCode(code, initializer, aliases) ?? readSource(code, initializer); patternDeclarations.push( `const ${cacheName} = ${OXC_COMPUTED_REACTIVE_ALIAS_PLACEHOLDER}(() => { const ${patternCode} = (${initializerCode}); ${tracksReadiness ? `${cacheName}Ready = true; ` : ""}return { ${names.join(", ")} }; });`, ); replacements.push({ start: readNumber(id.start) ?? start, end: readNumber(readObject(declaration.init).end) ?? end, name: cacheName, text: `{ ${names.join(", ")} } = ${owned ? `${OXC_UNTRACK_REACTIVE_ALIAS_PLACEHOLDER}(() => ${cacheName}.get())` : `${cacheName}.get()`}`, }); continue; } collectOxcComputedKeyNodes(id, computedKeys); if (!owned) continue; replacements.push({ start, end, name: typeof id.name === "string" ? id.name : "pattern", text: `${OXC_UNTRACK_REACTIVE_ALIAS_PLACEHOLDER}(() => (${readSource(code, initializer)}))`, }); } for (const [start, key] of computedKeys) { const end = readNumber(key.end); if (end === undefined) continue; const name = computedKeyBindingName(start); if (replacements.some((replacement) => replacement.start <= start && replacement.end >= end)) continue; replacements.push({ start, end, name, text: `(${name} = (${readSource(code, key)}))`, }); } if (replacements.length === 0) { return undefined; } let source = readSource(code, statement); for (const replacement of replacements.sort((left, right) => right.start - left.start)) { const start = replacement.start - statementStart; const end = replacement.end - statementStart; if (start < 0 || end > source.length || start > end) { return undefined; } source = `${source.slice(0, start)}${replacement.text}${source.slice(end)}`; } const computedKeyDeclarations = [...computedKeys.keys()] .map((start) => `let ${computedKeyBindingName(start)};`) .join("\n"); return [computedKeyDeclarations, ...patternDeclarations, source].filter(Boolean).join("\n"); } export function collectOxcCompilerOwnedReactiveAliases( statements: readonly unknown[], rootStatement: unknown, aliases: ReadonlyMap, compilerOwnedRenderValueBindings: ReadonlySet = new Set(), ): Map { const dependencies = new Map>(); const unownedReferences = new Set(); const compilerOwnedRenderValueReferences = new Set(); for (const statementValue of statements) { const statement = readObject(statementValue); if (statementValue === rootStatement) continue; if (statement.type !== "VariableDeclaration") { for (const name of collectOxcReactiveAliasReferenceNames(statement, aliases)) { unownedReferences.add(name); } continue; } for (const declarationValue of readArray(statement.declarations)) { const declaration = readObject(declarationValue); const id = readObject(declaration.id); const initializer = unwrapOxcParentheses(readObject(declaration.init)); const references = collectOxcReactiveAliasReferenceNames(initializer, aliases); if (typeof id.name === "string" && aliases.has(id.name)) { dependencies.set(id.name, references); continue; } const patternAliases = collectOxcReactiveAliasBindingNames(id, aliases); if (patternAliases.length > 0) { for (const name of patternAliases) { dependencies.set(name, references); } continue; } const referencesTargetCompilerOwnedRenderValue = typeof id.name === "string" && compilerOwnedRenderValueBindings.has(id.name); for (const name of references) { if (referencesTargetCompilerOwnedRenderValue) { compilerOwnedRenderValueReferences.add(name); } else { unownedReferences.add(name); } } } } const reachable = collectOxcReactiveAliasDependencyClosure( new Set([ ...collectOxcReactiveAliasReferenceNames(readObject(rootStatement), aliases), ...compilerOwnedRenderValueReferences, ]), dependencies, ); const disqualified = collectOxcReactiveAliasDependencyClosure(unownedReferences, dependencies); return new Map([...aliases].filter(([name]) => reachable.has(name) && !disqualified.has(name))); } function collectOxcReactiveAliasReferenceNames( node: Record, aliases: ReadonlyMap, ): Set { const replacements: ReactiveAliasReplacement[] = []; collectOxcReactiveAliasReplacements(node, undefined, undefined, aliases, new Set(), replacements); return new Set(replacements.map((replacement) => replacement.name)); } function collectOxcPatternReactiveAliases( code: string, pattern: Record, initializerCode: string, memoizeComputedKeys: boolean, ): Map { const aliases = new Map(); if (memoizeComputedKeys && containsArrayPattern(pattern)) { const cacheName = patternBindingName(readNumber(pattern.start) ?? 0); const names = new Set(); collectOxcBindingNames(pattern, names); for (const name of names) aliases.set(name, `${cacheName}.get().${name}`); } else { collectOxcPatternReactiveAliasesInto( pattern, initializerCode, aliases, code, memoizeComputedKeys, ); } return aliases; } function patternBindingName(start: number): string { return `__mreactPattern_${start}`; } function containsArrayPattern(pattern: Record): boolean { if (pattern.type === "ArrayPattern") return true; if (pattern.type === "AssignmentPattern") return containsArrayPattern(readObject(pattern.left)); if (pattern.type === "ObjectPattern") return readArray(pattern.properties).some((value) => { const property = readObject(value); return containsArrayPattern( readObject(property.type === "RestElement" ? property.argument : property.value), ); }); return false; } function collectOxcPatternReactiveAliasesInto( pattern: Record, sourceCode: string, aliases: Map, code: string, memoizeComputedKeys: boolean, ): void { if (pattern.type === "Identifier" && typeof pattern.name === "string") { aliases.set(pattern.name, sourceCode); return; } if (pattern.type === "AssignmentPattern") { const left = readObject(pattern.left); const right = readSource(code, pattern.right); if (left.type === "Identifier" && typeof left.name === "string") { aliases.set( left.name, `((__mreactAliasValue) => __mreactAliasValue === undefined ? (${right}) : __mreactAliasValue)(${sourceCode})`, ); } return; } if (pattern.type === "ObjectPattern") { const properties = readArray(pattern.properties); const excludedProperties: string[] = []; for (const propertyValue of properties) { const property = readObject(propertyValue); if (property.type === "RestElement") { const argument = readObject(property.argument); if (argument.type === "Identifier" && typeof argument.name === "string") { const excludedPattern = excludedProperties.join(", "); aliases.set( argument.name, `((__mreactRestSource) => { const { ${excludedPattern}, ...__mreactRestValue } = __mreactRestSource; return __mreactRestValue; })(${sourceCode})`, ); } continue; } if (property.type !== "Property" && property.type !== "ObjectProperty") { continue; } const access = readPropertyAccess(property, code, memoizeComputedKeys); const excludedProperty = readExcludedProperty(property, code, memoizeComputedKeys); if (access === undefined || excludedProperty === undefined) continue; excludedProperties.push( `${excludedProperty}: __mreactRestExcluded${excludedProperties.length}`, ); collectOxcPatternReactiveAliasesInto( readObject(property.value), `(${sourceCode})${access}`, aliases, code, memoizeComputedKeys, ); } return; } if (pattern.type === "ArrayPattern") { for (const [index, elementValue] of readArray(pattern.elements).entries()) { const element = readObject(elementValue); if (element.type === "RestElement") { const argument = readObject(element.argument); if (argument.type === "Identifier" && typeof argument.name === "string") { aliases.set(argument.name, `Array.from(${sourceCode}).slice(${index})`); } continue; } if (Object.keys(element).length === 0) continue; collectOxcPatternReactiveAliasesInto( element, `Array.from(${sourceCode})[${index}]`, aliases, code, memoizeComputedKeys, ); } } } function readPropertyKeyCode( property: Record, code: string, memoizeComputedKeys: boolean, ): string | undefined { const key = readObject(property.key); if (property.computed === true) { const keyStart = readNumber(key.start); if (memoizeComputedKeys && keyStart !== undefined) { return computedKeyBindingName(keyStart); } return readSource(code, key); } if (key.type === "Identifier" && typeof key.name === "string") { return key.name; } if (key.type === "Literal" && (typeof key.value === "string" || typeof key.value === "number")) { return JSON.stringify(key.value); } return undefined; } function readPropertyAccess( property: Record, code: string, memoizeComputedKeys: boolean, ): string | undefined { const key = readPropertyKeyCode(property, code, memoizeComputedKeys); if (key === undefined) return undefined; return property.computed === true || !/^[A-Za-z_$][\w$]*$/.test(key) ? `[${key}]` : `.${key}`; } function readExcludedProperty( property: Record, code: string, memoizeComputedKeys: boolean, ): string | undefined { const key = readPropertyKeyCode(property, code, memoizeComputedKeys); if (key === undefined) return undefined; return property.computed === true ? `[${key}]` : key; } function computedKeyBindingName(start: number): string { return `__mreactComputedKey_${start}`; } function collectOxcComputedKeyNodes( pattern: Record, keys: Map>, ): void { if (pattern.type === "AssignmentPattern") { collectOxcComputedKeyNodes(readObject(pattern.left), keys); return; } if (pattern.type === "RestElement") { collectOxcComputedKeyNodes(readObject(pattern.argument), keys); return; } if (pattern.type === "ObjectPattern") { for (const propertyValue of readArray(pattern.properties)) { const property = readObject(propertyValue); if (property.type === "RestElement") { collectOxcComputedKeyNodes(readObject(property.argument), keys); continue; } if (property.type !== "Property" && property.type !== "ObjectProperty") continue; if (property.computed === true) { const key = readObject(property.key); const start = readNumber(key.start); if (start !== undefined) keys.set(start, key); } collectOxcComputedKeyNodes(readObject(property.value), keys); } return; } if (pattern.type === "ArrayPattern") { for (const elementValue of readArray(pattern.elements)) { collectOxcComputedKeyNodes(readObject(elementValue), keys); } } } function collectOxcReactiveAliasBindingNames( pattern: Record, aliases: ReadonlyMap, ): string[] { const names: string[] = []; if (pattern.type === "Identifier" && typeof pattern.name === "string") { if (aliases.has(pattern.name)) names.push(pattern.name); return names; } if (pattern.type === "AssignmentPattern") { return collectOxcReactiveAliasBindingNames(readObject(pattern.left), aliases); } if (pattern.type === "RestElement") { return collectOxcReactiveAliasBindingNames(readObject(pattern.argument), aliases); } if (pattern.type === "ObjectPattern") { for (const propertyValue of readArray(pattern.properties)) { const property = readObject(propertyValue); names.push( ...collectOxcReactiveAliasBindingNames( readObject(property.type === "RestElement" ? property.argument : property.value), aliases, ), ); } return names; } if (pattern.type === "ArrayPattern") { for (const elementValue of readArray(pattern.elements)) { const element = readObject(elementValue); if (Object.keys(element).length > 0) { names.push(...collectOxcReactiveAliasBindingNames(element, aliases)); } } } return names; } function hasOxcReactiveAliasBinding( pattern: Record, aliases: ReadonlyMap, ): boolean { return collectOxcReactiveAliasBindingNames(pattern, aliases).length > 0; } function collectOxcReactiveAliasDependencyClosure( initial: ReadonlySet, dependencies: ReadonlyMap>, ): Set { const closure = new Set(initial); const pending = [...initial]; while (pending.length > 0) { const name = pending.pop(); if (name === undefined) continue; for (const dependency of dependencies.get(name) ?? []) { if (closure.has(dependency)) continue; closure.add(dependency); pending.push(dependency); } } return closure; } export function collectOxcReactiveDerivedFunctionNames( statements: readonly unknown[], ): Set { const names = new Set(); for (const statementValue of statements) { const statement = readObject(statementValue); if (statement.type === "FunctionDeclaration") { const id = readObject(statement.id); if (typeof id.name === "string" && isOxcReactiveDerivedFunction(statement)) { names.add(id.name); } continue; } if (statement.type !== "VariableDeclaration" || statement.kind !== "const") { continue; } for (const declarationValue of readArray(statement.declarations)) { const declaration = readObject(declarationValue); const id = readObject(declaration.id); const initializer = unwrapOxcParentheses(readObject(declaration.init)); if ( typeof id.name === "string" && (initializer.type === "FunctionExpression" || initializer.type === "ArrowFunctionExpression") && isOxcReactiveDerivedFunction(initializer) ) { names.add(id.name); } } } return names; } export function rewriteOxcReactiveAliasExpressionCode( code: string, expression: Record, aliases: ReadonlyMap | undefined, ): string | undefined { if (aliases === undefined || aliases.size === 0) { return undefined; } const expressionStart = readNumber(expression.start); const expressionEnd = readNumber(expression.end); if (expressionStart === undefined || expressionEnd === undefined) { return undefined; } const replacements: ReactiveAliasReplacement[] = []; collectOxcReactiveAliasReplacements( expression, undefined, undefined, aliases, new Set(), replacements, ); if (replacements.length === 0) { return undefined; } let source = readSource(code, expression); for (const replacement of replacements.sort((left, right) => right.start - left.start)) { const start = replacement.start - expressionStart; const end = replacement.end - expressionStart; if (start < 0 || end > source.length || start > end) { return undefined; } source = `${source.slice(0, start)}${replacement.text}${source.slice(end)}`; } return source; } function collectOxcReactiveAliasReplacements( node: unknown, parent: Record | undefined, parentKey: string | undefined, aliases: ReadonlyMap, shadowed: ReadonlySet, replacements: ReactiveAliasReplacement[], ): void { const object = readObject(node); if (typeof object.type !== "string") { return; } if (object.type.startsWith("TS")) { return; } if ( object.type === "Identifier" && typeof object.name === "string" && !shadowed.has(object.name) && isOxcReactiveAliasReference(object, parent, parentKey) ) { const replacement = aliases.get(object.name); const start = readNumber(object.start); const end = readNumber(object.end); if (replacement !== undefined && start !== undefined && end !== undefined) { replacements.push({ end, name: object.name, start, text: isOxcShorthandPropertyValue(object, parent) ? `${object.name}: (${replacement})` : `(${replacement})`, }); } return; } if (isOxcFunctionNode(object)) { const functionShadowed = new Set(shadowed); collectOxcBindingNames(object.id, functionShadowed); for (const parameter of readArray(object.params)) { collectOxcBindingNames(parameter, functionShadowed); } collectOxcReactiveAliasReplacements( object.body, object, "body", aliases, addOxcBlockBindingNames(object.body, functionShadowed), replacements, ); return; } const childShadowed = object.type === "BlockStatement" || object.type === "Program" ? addOxcBlockBindingNames(object, new Set(shadowed)) : shadowed; for (const [key, value] of Object.entries(object)) { if (key === "type" || key === "start" || key === "end" || key === "loc") { continue; } if (key === "id" && isOxcDeclarationWithId(object)) { continue; } if (key === "params" && isOxcFunctionNode(object)) { continue; } if (key === "key" && isOxcNonComputedKey(object)) { continue; } if (Array.isArray(value)) { for (const item of value) { collectOxcReactiveAliasReplacements( item, object, key, aliases, childShadowed, replacements, ); } continue; } if (typeof value === "object" && value !== null) { collectOxcReactiveAliasReplacements(value, object, key, aliases, childShadowed, replacements); } } } function isOxcReactiveAliasReference( node: Record, parent: Record | undefined, parentKey: string | undefined, ): boolean { if (parent === undefined) { return true; } if (parent.type === "MemberExpression" && parentKey === "property" && parent.computed !== true) { return false; } if (parentKey === "id" && isOxcDeclarationWithId(parent)) { return false; } if (parentKey === "params" && isOxcFunctionNode(parent)) { return false; } if (parentKey === "key" && isOxcNonComputedKey(parent)) { return isOxcShorthandPropertyValue(node, parent); } if ( (parent.type === "BreakStatement" || parent.type === "ContinueStatement" || parent.type === "LabeledStatement") && parentKey === "label" ) { return false; } return true; } function isOxcShorthandPropertyValue( node: Record, parent: Record | undefined, ): boolean { const key = readObject(parent?.key); return ( parent !== undefined && (parent.type === "Property" || parent.type === "ObjectProperty") && parent.shorthand === true && parent.value === node && readNumber(key.start) === readNumber(node.start) && readNumber(key.end) === readNumber(node.end) ); } function isOxcNonComputedKey(node: Record): boolean { return ( (node.type === "Property" || node.type === "ObjectProperty" || node.type === "PropertyDefinition" || node.type === "MethodDefinition") && node.computed !== true ); } function isOxcDeclarationWithId(node: Record): boolean { return ( node.type === "VariableDeclarator" || node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ClassDeclaration" || node.type === "ClassExpression" ); } function isOxcFunctionNode(node: Record): boolean { return ( node.type === "ArrowFunctionExpression" || node.type === "FunctionDeclaration" || node.type === "FunctionExpression" ); } function addOxcBlockBindingNames(node: unknown, shadowed: Set): ReadonlySet { const object = readObject(node); const body = readArray(object.body); if (body.length === 0) { return shadowed; } for (const statement of body) { collectOxcStatementBindingNames(statement, shadowed); } return shadowed; } function collectOxcStatementBindingNames(node: unknown, names: Set): void { const object = readObject(node); if (object.type === "VariableDeclaration") { for (const declaration of readArray(object.declarations)) { collectOxcBindingNames(readObject(declaration).id, names); } return; } if (object.type === "FunctionDeclaration" || object.type === "ClassDeclaration") { collectOxcBindingNames(object.id, names); } } function collectOxcBindingNames(node: unknown, names: Set): void { const object = readObject(node); if (object.type === "Identifier" && typeof object.name === "string") { names.add(object.name); return; } if (object.type === "RestElement") { collectOxcBindingNames(object.argument, names); return; } if (object.type === "AssignmentPattern") { collectOxcBindingNames(object.left, names); return; } if (object.type === "ArrayPattern") { for (const element of readArray(object.elements)) { collectOxcBindingNames(element, names); } return; } if (object.type === "ObjectPattern") { for (const property of readArray(object.properties)) { collectOxcBindingNames(readObject(property).value ?? readObject(property).argument, names); } } } function collectOxcFunctionLocalBindings( functionNode: Record, names: Set, ): void { collectOxcBindingNames(functionNode.id, names); for (const parameter of readArray(functionNode.params)) { collectOxcBindingNames(parameter, names); } collectOxcLocalBindingsFromNode(readObject(functionNode.body), names); } function collectOxcLocalBindingsFromNode(node: unknown, names: Set): void { const object = readObject(node); if (typeof object.type !== "string" || object.type.startsWith("TS")) { return; } if (object.type === "VariableDeclaration") { for (const declaration of readArray(object.declarations)) { collectOxcBindingNames(readObject(declaration).id, names); } } else if (object.type === "FunctionDeclaration" || object.type === "ClassDeclaration") { collectOxcBindingNames(object.id, names); return; } else if (object.type === "ForOfStatement" || object.type === "ForInStatement") { const left = readObject(object.left); const declarations = readArray(left.declarations); collectOxcBindingNames( declarations.length > 0 ? readObject(declarations[0]).id : object.left, names, ); } else if (object.type === "ForStatement") { collectOxcLocalBindingsFromNode(object.init, names); } else if (isOxcFunctionNode(object)) { return; } for (const [key, value] of Object.entries(object)) { if (key === "type" || key === "start" || key === "end" || key === "loc") { continue; } if (Array.isArray(value)) { for (const item of value) { collectOxcLocalBindingsFromNode(item, names); } continue; } if (typeof value === "object" && value !== null) { collectOxcLocalBindingsFromNode(value, names); } } } function readNumber(value: unknown): number | undefined { return typeof value === "number" ? value : undefined; } export function markOxcRenderValueExpressions( nodes: readonly JsxNodeIr[], names: Set, renderMode: "dynamic" | "html" | "render-value" | "server-render-value" = "dynamic", ): void { if (names.size === 0) { return; } for (const node of nodes) { if (node.kind === "expr" && names.has(node.code)) { node.renderMode = renderMode; continue; } if (node.kind === "conditional") { markOxcRenderValueExpressions(node.whenTrue, names, renderMode); markOxcRenderValueExpressions(node.whenFalse, names, renderMode); continue; } if (node.kind === "list") { markOxcRenderValueExpressions(node.children, names, renderMode); continue; } if (node.kind === "fragment" || node.kind === "element" || node.kind === "component") { markOxcRenderValueExpressions(node.children, names, renderMode); } } } export function isOxcRenderValueExpression(expression: Record): boolean { if (isOxcRendererCallExpression(expression)) { return true; } if (expression.type !== "MemberExpression") { return false; } const object = readObject(expression.object); const property = readObject(expression.property); return ( object.type === "Identifier" && object.name === "props" && typeof property.name === "string" && ["children", "fallback", "header", "sidebar", "element"].includes(property.name) ); } function isOxcRendererCallExpression(expression: Record): boolean { if (expression.type !== "CallExpression") { return false; } const callee = readObject(expression.callee); if ( callee.type === "Identifier" && typeof callee.name === "string" && /^render[A-Z0-9_$]/.test(callee.name) ) { return true; } if (callee.type !== "MemberExpression" || callee.computed === true) { return false; } const object = readObject(callee.object); const property = readObject(callee.property); return ( object.type === "Identifier" && object.name === "props" && typeof property.name === "string" && /^render[A-Z0-9_$]/.test(property.name) ); } export function containsOxcJsxSyntax(node: Record): boolean { if (node.type === "JSXElement" || node.type === "JSXFragment") { return true; } return Object.values(node).some((value) => Array.isArray(value) ? value.some((item) => containsOxcJsxSyntax(readObject(item))) : typeof value === "object" && value !== null && containsOxcJsxSyntax(readObject(value)), ); } function isJsxLikeInitializer( node: Record, jsxReturnFunctionNames: ReadonlySet, ): boolean { if (node.type === "JSXElement" || node.type === "JSXFragment") return true; if (node.type === "CallExpression") { const callee = unwrapOxcParentheses(readObject(node.callee)); return ( callee.type === "Identifier" && typeof callee.name === "string" && jsxReturnFunctionNames.has(callee.name) ); } if (node.type === "ConditionalExpression") { return ( isJsxLikeInitializer(readObject(node.consequent), jsxReturnFunctionNames) || isJsxLikeInitializer(readObject(node.alternate), jsxReturnFunctionNames) ); } if (node.type === "LogicalExpression") { return ( isJsxLikeInitializer(readObject(node.left), jsxReturnFunctionNames) || isJsxLikeInitializer(readObject(node.right), jsxReturnFunctionNames) ); } if (node.type === "ArrayExpression") { return readArray(node.elements).some((element) => { const object = readObject(element); return Object.keys(object).length > 0 && isJsxLikeInitializer(object, jsxReturnFunctionNames); }); } if (node.type === "ObjectExpression") { return readArray(node.properties).some((property) => { const object = readObject(property); const value = object.type === "SpreadElement" ? readObject(object.argument) : readObject(object.value); return Object.keys(value).length > 0 && isJsxLikeInitializer(value, jsxReturnFunctionNames); }); } return containsOxcJsxSyntax(node); } function isOxcReactiveReadExpression(expression: Record): boolean { if (expression.type !== "CallExpression") { return false; } const callee = readObject(expression.callee); if (callee.type !== "MemberExpression" || callee.computed === true || callee.optional === true) { return false; } const property = readObject(callee.property); return property.type === "Identifier" && property.name === "get"; } function isOxcReactiveAliasExpression( expression: Record, aliases: ReadonlyMap = new Map(), ): boolean { const state = analyzeOxcReactiveAliasExpression(expression, aliases); return state.safe && state.reactive; } function containsOxcReactiveDependency( node: Record, aliases: ReadonlyMap, ): boolean { if (isOxcFunctionNode(node)) { return false; } if (isOxcReactiveAliasExpression(node, aliases)) { return true; } for (const [key, value] of Object.entries(node)) { if (key === "type" || key === "start" || key === "end" || key === "loc") { continue; } if (Array.isArray(value)) { if ( value.some( (item) => typeof item === "object" && item !== null && containsOxcReactiveDependency(readObject(item), aliases), ) ) { return true; } continue; } if ( typeof value === "object" && value !== null && containsOxcReactiveDependency(readObject(value), aliases) ) { return true; } } return false; } function isOxcReactiveDerivedAliasExpression( expression: Record, reactiveDerivedFunctions: ReadonlySet, ): boolean { if (reactiveDerivedFunctions.size === 0) { return false; } const unwrappedExpression = unwrapOxcParentheses(expression); if (unwrappedExpression.type !== "CallExpression") { return false; } if (readArray(unwrappedExpression.arguments).length !== 0) { return false; } const callee = readObject(unwrappedExpression.callee); return ( callee.type === "Identifier" && typeof callee.name === "string" && reactiveDerivedFunctions.has(callee.name) ); } function isOxcReactiveDerivedFunction(functionNode: Record): boolean { if (readArray(functionNode.params).length !== 0) { return false; } const localBindings = new Set(); collectOxcFunctionLocalBindings(functionNode, localBindings); const usage = analyzeOxcReactiveDerivedFunctionUsage( readObject(functionNode.body), localBindings, ); return usage.reactive && usage.safe; } function analyzeOxcReactiveDerivedFunctionUsage( node: unknown, localBindings: ReadonlySet, ): ReactiveAliasExpressionState { const object = readObject(node); if (typeof object.type !== "string" || object.type.startsWith("TS")) { return { reactive: false, safe: true }; } if (isOxcFunctionNode(object)) { return { reactive: false, safe: true }; } if (object.type === "CallExpression") { const callee = readObject(object.callee); const member = callee.type === "MemberExpression" ? callee : undefined; const property = readObject(member?.property); const owner = readObject(member?.object); const propertyName = member?.computed === true ? undefined : typeof property.name === "string" ? property.name : undefined; const ownerName = owner.type === "Identifier" ? owner.name : undefined; if (propertyName === "get" && typeof ownerName === "string" && !localBindings.has(ownerName)) { return mergeReactiveAliasStates([ { reactive: true, safe: true }, ...readArray(object.arguments).map((argument) => analyzeOxcReactiveDerivedFunctionUsage(argument, localBindings), ), ]); } if ( propertyName !== undefined && typeof ownerName === "string" && !localBindings.has(ownerName) && isLikelyMutatingMethodName(propertyName) ) { return { reactive: false, safe: false }; } } const states: ReactiveAliasExpressionState[] = []; for (const [key, value] of Object.entries(object)) { if (key === "type" || key === "start" || key === "end" || key === "loc") { continue; } if (Array.isArray(value)) { states.push( ...value.map((item) => analyzeOxcReactiveDerivedFunctionUsage(item, localBindings)), ); continue; } if (typeof value === "object" && value !== null) { states.push(analyzeOxcReactiveDerivedFunctionUsage(value, localBindings)); } } return mergeReactiveAliasStates(states); } function mergeReactiveAliasStates( states: readonly ReactiveAliasExpressionState[], ): ReactiveAliasExpressionState { return states.reduce( (merged, state) => ({ reactive: merged.reactive || state.reactive, safe: merged.safe && state.safe, }), { reactive: false, safe: true }, ); } function isLikelyMutatingMethodName(name: string): boolean { return ( name === "set" || name === "delete" || name === "clear" || name === "push" || name === "pop" || name === "shift" || name === "unshift" || name === "splice" || name === "sort" || name === "reverse" ); } function analyzeOxcReactiveAliasExpression( expression: Record, aliases: ReadonlyMap = new Map(), ): ReactiveAliasExpressionState { const unwrappedExpression = unwrapOxcParentheses(expression); if (unwrappedExpression.type === "Identifier") { return { reactive: typeof unwrappedExpression.name === "string" && aliases.has(unwrappedExpression.name), safe: true, }; } if (unwrappedExpression.type === "Literal" || unwrappedExpression.type === "ThisExpression") { return { reactive: false, safe: true }; } if (isOxcReactiveReadExpression(unwrappedExpression)) { return { reactive: true, safe: true }; } if (unwrappedExpression.type === "ChainExpression") { return analyzeOxcReactiveAliasExpression(readObject(unwrappedExpression.expression), aliases); } if ( unwrappedExpression.type === "TSAsExpression" || unwrappedExpression.type === "TSSatisfiesExpression" || unwrappedExpression.type === "TSNonNullExpression" || unwrappedExpression.type === "TSInstantiationExpression" || unwrappedExpression.type === "TypeCastExpression" ) { return analyzeOxcReactiveAliasExpression(readObject(unwrappedExpression.expression), aliases); } if (unwrappedExpression.type === "MemberExpression") { const objectState = analyzeOxcReactiveAliasExpression( readObject(unwrappedExpression.object), aliases, ); const propertyState = unwrappedExpression.computed === true ? analyzeOxcReactiveAliasExpression(readObject(unwrappedExpression.property), aliases) : { reactive: false, safe: true }; return { reactive: objectState.reactive || propertyState.reactive, safe: objectState.safe && propertyState.safe, }; } if (unwrappedExpression.type === "UnaryExpression") { return analyzeOxcReactiveAliasExpression(readObject(unwrappedExpression.argument), aliases); } if ( unwrappedExpression.type === "BinaryExpression" || unwrappedExpression.type === "LogicalExpression" ) { const leftState = analyzeOxcReactiveAliasExpression( readObject(unwrappedExpression.left), aliases, ); const rightState = analyzeOxcReactiveAliasExpression( readObject(unwrappedExpression.right), aliases, ); return { reactive: leftState.reactive || rightState.reactive, safe: leftState.safe && rightState.safe, }; } if (unwrappedExpression.type === "ConditionalExpression") { const testState = analyzeOxcReactiveAliasExpression( readObject(unwrappedExpression.test), aliases, ); const consequentState = analyzeOxcReactiveAliasExpression( readObject(unwrappedExpression.consequent), aliases, ); const alternateState = analyzeOxcReactiveAliasExpression( readObject(unwrappedExpression.alternate), aliases, ); return { reactive: testState.reactive || consequentState.reactive || alternateState.reactive, safe: testState.safe && consequentState.safe && alternateState.safe, }; } return { reactive: false, safe: false }; } function collectOxcPushJsxBindingNames(statements: readonly unknown[], names: Set): void { for (const statement of statements) { const object = readObject(statement); if (object.type === "ForOfStatement" || object.type === "ForStatement") { collectOxcPushJsxBindingNames(readArray(readObject(object.body).body), names); continue; } const expression = readObject(object.expression); if (object.type !== "ExpressionStatement" || expression.type !== "CallExpression") { continue; } const callee = readObject(expression.callee); const argument = unwrapOxcParentheses(readObject(readArray(expression.arguments)[0])); if ( callee.type !== "MemberExpression" || readObject(callee.property).name !== "push" || !containsOxcJsxSyntax(argument) ) { continue; } const target = readObject(callee.object); if (typeof target.name === "string") { names.add(target.name); } } }