import type { ModuleIr } from "./ir.js"; import { analyzeCompilerModuleContextWithOxc, analyzeWithOxc } from "./oxc.js"; import { createCompilerModuleContextWithOxc, type CompilerModuleContext, } from "./compiler-module-context.js"; export { transformCompilerModuleContext } from "./transform.js"; export { stripTypeScriptWithOxc } from "./oxc-transform.js"; export type { CompilerModuleContext } from "./compiler-module-context.js"; import type { AnalyzeModuleOptions, CompileTarget, Diagnostic } from "./types.js"; /** Configures source code, filename, target, and analysis options for IR generation. */ export interface AnalyzeToIrInput { code: string; filename: string; target: CompileTarget; options?: AnalyzeModuleOptions; } /** Contains the module IR and diagnostics produced by compiler analysis. */ export interface AnalyzeToIrOutput { ir: ModuleIr; diagnostics: Diagnostic[]; usedTypescriptFallback?: boolean; } /** Describes a static import declaration and the local names it introduces. */ export interface StaticImportReference { localNames: string[]; sideEffect: boolean; source: string; } /** Describes one specifier inside a static import declaration. */ export interface StaticImportSpecifierReference { importedName: string; kind: "default" | "named" | "namespace"; localName: string; } /** Describes a static import declaration with its individual specifiers. */ export interface ClientRouteStaticImportReference extends StaticImportReference { specifiers: StaticImportSpecifierReference[]; } /** Describes a static export declaration and the names it exports. */ export interface StaticExportReference { exportedNames: string[]; exportAll: boolean; specifiers: StaticExportSpecifierReference[]; source: string; } /** Describes one specifier inside a static export declaration. */ export interface StaticExportSpecifierReference { exportedName: string; localName: string; } /** Describes render and client-runtime reachability for one top-level export. */ export interface TopLevelExportRenderInfo { calledComponentRoots: string[]; clientRuntime: boolean; localName?: string | undefined; name: string; renderedComponentRoots: string[]; } /** Summarizes route-module imports, exports, directives, references, and reachable render roots. */ export interface ClientRouteModuleAnalysis { clientRuntime: boolean; defaultExportIdentifier: string | undefined; hasUseClientDirective: boolean; hasUseServerDirective: boolean; componentCallRoots: string[]; identifierReferences: string[]; jsxComponentRoots: string[]; reachableExportRenderedComponentNames: Record; reachableExportRenderedComponentRoots: Record; reachableRenderedComponentNames: string[]; reachableRenderedComponentRoots: string[]; staticExports: StaticExportReference[]; staticImports: ClientRouteStaticImportReference[]; topLevelExportRenderInfo: TopLevelExportRenderInfo[]; } /** Describes a named form action reference and its source span. */ export interface FormActionReference { end: number; name: string; start: number; } /** Describes a form action expression reference and both wrapper and expression source spans. */ export interface FormActionExpressionReference { end: number; expression: string; expressionEnd: number; expressionStart: number; start: number; } interface ComponentAliasState { aliases: Map; stringConstants: Map; } /** Analyzes source code into compiler IR with diagnostics. */ export function analyzeToIr(input: AnalyzeToIrInput): AnalyzeToIrOutput { return analyzeWithOxc(input); } /** Analyzes a pre-parsed compiler module context into compiler IR with diagnostics. */ export function analyzeCompilerModuleContextToIr( context: CompilerModuleContext, input: Omit, ): AnalyzeToIrOutput { return analyzeCompilerModuleContextWithOxc(context, input); } /** Creates a compiler module context from source code and an optional filename. */ export function createCompilerModuleContext(input: { code: string; filename?: string | undefined; }): CompilerModuleContext { return createCompilerModuleContextWithOxc(input); } /** Checks whether a module declares any of the given names as top-level exports. */ export function hasTopLevelExportDeclaration(input: { code: string; filename?: string | undefined; names: readonly string[]; }): boolean { const names = new Set(input.names); const parsed = parseModule(input.code, input.filename); return programBody(parsed.program).some((statement) => exportedNames(statement).some((name) => names.has(name)), ); } /** Reads a top-level exported boolean literal by name, ignoring comments and non-literal values. */ export function readTopLevelBooleanExport(input: { code: string; filename?: string | undefined; name: string; }): boolean | undefined { return readTopLevelBooleanExportFromContext( createCompilerModuleContext({ code: input.code, filename: input.filename }), input.name, ); } /** Reads a top-level exported boolean literal from a cached compiler module context. */ export function readTopLevelBooleanExportFromContext( context: CompilerModuleContext, name: string, ): boolean | undefined { const parsed = parseModuleContext(context); for (const statement of programBody(parsed.program)) { if (statement.type !== "ExportNamedDeclaration") { continue; } const declaration = readOptionalObject(statement.declaration); if (declaration?.type !== "VariableDeclaration") { continue; } const declarators = Array.isArray(declaration.declarations) ? declaration.declarations.map(readObject) : []; for (const declarator of declarators) { const id = readOptionalObject(declarator.id); if (typeof id?.name !== "string" || id.name !== name) { continue; } const value = booleanExpressionValue(readOptionalObject(declarator.init)); if (value !== undefined) { return value; } } } return undefined; } function booleanExpressionValue(node: Record | undefined): boolean | undefined { if ( (node?.type === "BooleanLiteral" || node?.type === "Literal") && typeof node.value === "boolean" ) { return node.value; } if ( node?.type === "TSAsExpression" || node?.type === "TSSatisfiesExpression" || node?.type === "ParenthesizedExpression" ) { return booleanExpressionValue(readOptionalObject(node.expression)); } return undefined; } /** Removes top-level export declarations for selected names while preserving their values where possible. */ export function stripTopLevelExportDeclarations(input: { code: string; filename?: string | undefined; names: readonly string[]; }): string { const names = new Set(input.names); const parsed = parseModule(input.code, input.filename); const replacements = programBody(parsed.program) .map((statement) => exportDeclarationReplacement(input.code, statement, names)) .filter((replacement): replacement is Replacement => replacement !== undefined) .sort((left, right) => right.start - left.start); let code = input.code; for (const replacement of replacements) { code = `${code.slice(0, replacement.start)}${replacement.text}${code.slice(replacement.end)}`; } return code; } /** Converts selected top-level export declarations into non-exported declarations. */ export function demoteTopLevelExportDeclarations(input: { code: string; filename?: string | undefined; names: readonly string[]; }): string { const names = new Set(input.names); const parsed = parseModule(input.code, input.filename); const replacements = programBody(parsed.program) .map((statement) => exportDeclarationDemotion(input.code, statement, names)) .filter((replacement): replacement is Replacement => replacement !== undefined) .sort((left, right) => right.start - left.start); let code = input.code; for (const replacement of replacements) { code = `${code.slice(0, replacement.start)}${replacement.text}${code.slice(replacement.end)}`; } return code; } /** Removes unused static value imports while preserving side-effect and type-only imports. */ export function stripUnusedStaticValueImports(input: { code: string; filename?: string | undefined; }): string { const parsed = parseModule(input.code, input.filename); const referencedNames = new Set(collectIdentifierReferenceNames(input)); const replacements = programBody(parsed.program) .map((statement) => unusedStaticValueImportReplacement(input.code, statement, referencedNames)) .filter((replacement): replacement is Replacement => replacement !== undefined) .sort((left, right) => right.start - left.start); let code = input.code; for (const replacement of replacements) { code = `${code.slice(0, replacement.start)}${replacement.text}${code.slice(replacement.end)}`; } return code; } /** Collects module specifier strings from static import and export declarations. */ export function collectStaticModuleSpecifiers(input: { code: string; filename?: string | undefined; }): string[] { const parsed = parseModule(input.code, input.filename); return programBody(parsed.program).flatMap(staticModuleSpecifier); } /** Collects static import declarations and their local binding names. */ export function collectStaticImportReferences(input: { code: string; filename?: string | undefined; }): StaticImportReference[] { const parsed = parseModule(input.code, input.filename); return programBody(parsed.program).flatMap(staticImportReference); } /** Collects static export declarations and their exported names. */ export function collectStaticExportReferences(input: { code: string; filename?: string | undefined; }): StaticExportReference[] { const parsed = parseModule(input.code, input.filename); return programBody(parsed.program).flatMap(staticExportReference); } /** Collects root component names referenced by JSX elements in a module. */ export function collectJsxComponentRootNames(input: { code: string; filename?: string | undefined; }): string[] { const parsed = parseModule(input.code, input.filename); const names = new Set(); const aliasState = createComponentAliasState(); collectJsxComponentRootNamesFromNode(parsed.program, names); collectSimpleComponentAliasesFromNode(parsed.program, aliasState); expandJsxComponentAliasRoots(names, aliasState.aliases); return Array.from(names).sort(); } /** Collects identifier reference names from a module, excluding declarations. */ export function collectIdentifierReferenceNames(input: { code: string; filename?: string | undefined; }): string[] { const parsed = parseModule(input.code, input.filename); const names = new Set(); collectIdentifierReferenceNamesFromNode(parsed.program, names); return Array.from(names).sort(); } /** Checks whether browser globals may execute during server rendering without a guard. */ export function hasUnguardedBrowserGlobalReference(input: { code: string; filename?: string | undefined; }): boolean { const parsed = parseModule(input.code, input.filename); return hasUnguardedBrowserGlobalReferenceInNode(parsed.program, false); } /** Collects unique named form action references from a module. */ export function collectFormActionReferenceNames(input: { code: string; filename?: string | undefined; }): string[] { return Array.from( new Set(collectFormActionReferences(input).map((reference) => reference.name)), ).sort(); } /** Collects named form action references and their source spans from a module. */ export function collectFormActionReferences(input: { code: string; filename?: string | undefined; }): FormActionReference[] { const parsed = parseModule(input.code, input.filename); const references: FormActionReference[] = []; collectFormActionReferencesFromNode(parsed.program, references); return references.sort((left, right) => left.start === right.start ? left.name.localeCompare(right.name) : left.start - right.start, ); } /** Collects form action expression references and their source spans from a module. */ export function collectFormActionExpressionReferences(input: { code: string; filename?: string | undefined; }): FormActionExpressionReference[] { const parsed = parseModule(input.code, input.filename); const references: FormActionExpressionReference[] = []; collectFormActionExpressionReferencesFromNode(input.code, parsed.program, references); return references.sort((left, right) => left.start === right.start ? left.expression.localeCompare(right.expression) : left.start - right.start, ); } /** Collects value export names declared at the top level of a module. */ export function collectTopLevelValueExportNames(input: { code: string; filename?: string | undefined; }): string[] { const parsed = parseModule(input.code, input.filename); const names = new Set(); for (const statement of programBody(parsed.program)) { for (const name of exportedNames(statement)) { names.add(name); } } return Array.from(names).sort(); } /** Collects render reachability information for top-level exports in a module. */ export function collectTopLevelExportRenderInfo(input: { code: string; filename?: string | undefined; }): TopLevelExportRenderInfo[] { const parsed = parseModule(input.code, input.filename); return collectTopLevelExportRenderInfoFromProgram(parsed.program); } /** Analyzes a route module for directives, static imports, exports, references, and render reachability. */ export function collectClientRouteModuleAnalysis(input: { code: string; filename?: string | undefined; }): ClientRouteModuleAnalysis { const parsed = parseModule(input.code, input.filename); return collectClientRouteModuleAnalysisFromContext(parsed); } /** Analyzes a cached compiler module context for route-module metadata. */ export function collectClientRouteModuleAnalysisFromContext( context: CompilerModuleContext, ): ClientRouteModuleAnalysis { const parsed = parseModuleContext(context); const body = programBody(parsed.program); const identifierReferences = new Set(); const reachableRenderedComponents = collectReachableExportRenderedComponentsFromProgram( parsed.program, ); collectIdentifierReferenceNamesFromNode(parsed.program, identifierReferences); return { clientRuntime: hasClientRuntimeSyntaxNode(parsed.program), defaultExportIdentifier: reachableRenderedComponents.defaultExportIdentifier, componentCallRoots: collectComponentCallRootNamesFromSubtree(parsed.program), hasUseClientDirective: hasModuleDirectiveInProgram(parsed.program, "use client"), hasUseServerDirective: hasModuleDirectiveInProgram(parsed.program, "use server"), identifierReferences: Array.from(identifierReferences).sort(), jsxComponentRoots: collectJsxComponentRootNamesFromSubtree(parsed.program), reachableExportRenderedComponentNames: reachableRenderedComponents.perExportNames, reachableExportRenderedComponentRoots: reachableRenderedComponents.perExportRoots, reachableRenderedComponentNames: reachableRenderedComponents.names, reachableRenderedComponentRoots: reachableRenderedComponents.roots, staticExports: body.flatMap(staticExportReference), staticImports: body.flatMap(staticImportReference), topLevelExportRenderInfo: collectTopLevelExportRenderInfoFromProgram(parsed.program), }; } interface ModuleExportMap { aliasState: ComponentAliasState; declarations: Map; // Local name when the default export is a bare identifier // (`export default Page`), including when that identifier is an import // binding. Such an identifier is the route's rendered component even though // the module body contains no JSX for it. defaultExportIdentifier: string | undefined; directExports: Map; exported: Map; } // Builds the module's export surface: top-level declarations by name, the // exported-name -> local-name map, and direct export declaration nodes. A // default export that references an identifier (`export default Page`) is // resolved through `declarations` so it behaves like `export { Page as default // }` rather than stopping at the bare identifier node. function collectModuleExportMap(program: unknown): ModuleExportMap { const declarations = new Map(); const exported = new Map(); const directExports = new Map(); const aliasState = createComponentAliasState(); let defaultExportIdentifier: string | undefined; collectSimpleComponentAliasesFromNode(program, aliasState); for (const statement of programBody(program)) { collectTopLevelDeclarationReferences(statement, declarations); if (statement.type === "ExportDefaultDeclaration") { const declaration = readOptionalObject(statement.declaration); if (declaration?.type === "Identifier" && typeof declaration.name === "string") { exported.set("default", declaration.name); defaultExportIdentifier = declaration.name; } else { directExports.set("default", declaration); exported.set("default", "default"); } continue; } if (statement.type !== "ExportNamedDeclaration") { continue; } const declaration = readOptionalObject(statement.declaration); if (declaration !== undefined) { for (const name of exportedNames(statement)) { exported.set(name, name); directExports.set(name, declarationForExportedName(declaration, name) ?? declaration); } continue; } const specifiers = Array.isArray(statement.specifiers) ? statement.specifiers.map(readObject) : []; for (const specifier of specifiers) { const exportedName = exportedNameForSpecifier(specifier); const localName = localNameForExportSpecifier(specifier); if (exportedName !== undefined && localName !== undefined) { exported.set(exportedName, localName); } } } return { aliasState, declarations, defaultExportIdentifier, directExports, exported }; } function collectTopLevelExportRenderInfoFromProgram(program: unknown): TopLevelExportRenderInfo[] { const { aliasState, declarations, directExports, exported } = collectModuleExportMap(program); return [...exported.entries()] .map(([name, localName]) => { const node = directExports.get(name) ?? declarations.get(localName); const calledComponentRoots = node === undefined ? [] : collectComponentCallRootNamesFromSubtree(node, aliasState.aliases); const renderedComponentRoots = node === undefined ? [] : collectJsxComponentRootNamesFromSubtree(node, aliasState.aliases); const clientRuntime = node === undefined ? false : hasReachableExportClientRuntime({ aliases: aliasState.aliases, declarations, localName, node, }); return node === undefined ? undefined : { calledComponentRoots, clientRuntime, localName, name, renderedComponentRoots, }; }) .filter((item) => item !== undefined) .sort((left, right) => left.name.localeCompare(right.name)); } interface ReachableExportRenderedComponents { defaultExportIdentifier: string | undefined; names: string[]; roots: string[]; perExportNames: Record; perExportRoots: Record; } // Collects the JSX components that are actually reachable from the module's // exports, walking transitively through same-file declarations (including // non-exported helper components). Unlike the file-wide `jsxComponentRoots`, // this excludes components rendered only inside dead/unreachable code, so it // reflects what the route's server-rendered tree truly contains. Per-export // breakdowns let callers attribute a rendered component to the specific export // whose subtree renders it (e.g. to keep barrel re-exports precise). function collectReachableExportRenderedComponentsFromProgram( program: unknown, ): ReachableExportRenderedComponents { const { aliasState, declarations, defaultExportIdentifier, directExports, exported } = collectModuleExportMap(program); const names = new Set(); const roots = new Set(); const perExportNames: Record = {}; const perExportRoots: Record = {}; const visit = ( node: unknown, exportRoots: Set, exportNames: Set, seen: Set, ): void => { const renderedRoots = collectJsxComponentRootNamesFromSubtree(node, aliasState.aliases); for (const root of renderedRoots) { exportRoots.add(root); roots.add(root); } const renderedNames = collectJsxComponentNamesFromSubtree(node, aliasState.aliases); for (const name of renderedNames) { exportNames.add(name); names.add(name); } const calledRoots = collectComponentCallRootNamesFromSubtree(node, aliasState.aliases); for (const root of [...renderedRoots, ...calledRoots]) { const resolved = aliasState.aliases.get(root) ?? root; if (seen.has(resolved)) { continue; } seen.add(resolved); const declaration = declarations.get(resolved); if (declaration !== undefined) { visit(declaration, exportRoots, exportNames, seen); } } }; for (const [name, localName] of exported.entries()) { const node = directExports.get(name) ?? declarations.get(localName); if (node === undefined) { continue; } const exportRoots = new Set(); const exportNames = new Set(); visit(node, exportRoots, exportNames, new Set([localName])); perExportRoots[name] = Array.from(exportRoots).sort(); perExportNames[name] = Array.from(exportNames).sort(); } return { defaultExportIdentifier, names: Array.from(names).sort(), roots: Array.from(roots).sort(), perExportNames, perExportRoots, }; } function hasReachableLocalClientRuntime(options: { aliases: ReadonlyMap; declarations: ReadonlyMap; roots: readonly string[]; seen: Set; }): boolean { for (const root of options.roots) { const resolved = options.aliases.get(root) ?? root; if (options.seen.has(resolved)) { continue; } const declaration = options.declarations.get(resolved); if (declaration === undefined) { continue; } if (hasClientRuntimeSyntaxNode(declaration)) { return true; } options.seen.add(resolved); const nestedCalledRoots = collectComponentCallRootNamesFromSubtree( declaration, options.aliases, ); const nestedRenderedRoots = collectJsxComponentRootNamesFromSubtree( declaration, options.aliases, ); if ( hasReachableLocalClientRuntime({ aliases: options.aliases, declarations: options.declarations, roots: [...nestedCalledRoots, ...nestedRenderedRoots], seen: options.seen, }) ) { return true; } } return false; } function hasReachableExportClientRuntime(options: { aliases: ReadonlyMap; declarations: ReadonlyMap; localName: string; node: unknown; }): boolean { return hasReachableExportClientRuntimeNode({ aliases: options.aliases, declarations: options.declarations, node: options.node, seen: new Set([options.localName]), }); } function hasReachableExportClientRuntimeNode(options: { aliases: ReadonlyMap; declarations: ReadonlyMap; node: unknown; seen: Set; }): boolean { if (hasClientRuntimeSyntaxNode(options.node)) { return true; } const references = collectRuntimeReferenceRootNamesFromSubtree(options.node, options.aliases); for (const reference of references) { const declaration = options.declarations.get(reference); if ( declaration !== undefined && !isFunctionLikeClientRuntimeDeclaration(declaration) && hasClientRuntimeSyntaxNode(declaration) ) { return true; } } const calledRoots = collectRuntimeCallRootNamesFromSubtree(options.node, options.aliases); const renderedRoots = collectJsxComponentRootNamesFromSubtree(options.node, options.aliases); if ( hasReachableLocalClientRuntime({ aliases: options.aliases, declarations: options.declarations, roots: [...calledRoots, ...renderedRoots], seen: options.seen, }) ) { return true; } return false; } function collectRuntimeReferenceRootNamesFromSubtree( node: unknown, aliases: ReadonlyMap, ): string[] { const names = new Set(); collectRuntimeReferenceRootNamesFromNode( node, names, createComponentAliasState(aliases), new Set(), undefined, undefined, ); return Array.from(names).sort(); } function collectRuntimeCallRootNamesFromSubtree( node: unknown, aliases: ReadonlyMap, ): string[] { const names = new Set(); collectRuntimeCallRootNamesFromNode(node, names, createComponentAliasState(aliases), new Set()); return Array.from(names).sort(); } function collectRuntimeReferenceRootNamesFromNode( node: unknown, names: Set, state: ComponentAliasState, shadowed: ReadonlySet, parent: Record | undefined, parentKey: string | undefined, ): void { if (Array.isArray(node)) { for (const child of node) { collectRuntimeReferenceRootNamesFromNode(child, names, state, shadowed, parent, parentKey); } return; } const object = readOptionalObject(node); if (object === undefined || shouldSkipRuntimeReferenceNode(object)) { return; } if (object.type === "Identifier" && typeof object.name === "string") { if (!shadowed.has(object.name) && isRuntimeReferenceIdentifier(parent, parentKey)) { names.add(state.aliases.get(object.name) ?? object.name); } return; } if (isFunctionLikeClientRuntimeDeclaration(object)) { const nextShadowed = new Set(shadowed); collectBindingNamesInto(object.id, nextShadowed); for (const parameter of readArray(object.params)) { collectBindingNamesInto(parameter, nextShadowed); } collectRuntimeReferenceRootNamesFromNode( object.body, names, state, addBlockBindingNames(object.body, nextShadowed), object, "body", ); return; } const childShadowed = object.type === "Program" || object.type === "BlockStatement" ? addBlockBindingNames(object, new Set(shadowed)) : shadowed; for (const [key, value] of Object.entries(object)) { if (shouldSkipRuntimeReferenceProperty(object, key)) { continue; } collectRuntimeReferenceRootNamesFromNode(value, names, state, childShadowed, object, key); } } function collectRuntimeCallRootNamesFromNode( node: unknown, names: Set, state: ComponentAliasState, shadowed: ReadonlySet, ): void { if (Array.isArray(node)) { for (const child of node) { collectRuntimeCallRootNamesFromNode(child, names, state, shadowed); } return; } const object = readOptionalObject(node); if (object === undefined || shouldSkipRuntimeReferenceNode(object)) { return; } if (isFunctionLikeClientRuntimeDeclaration(object)) { const nextShadowed = new Set(shadowed); collectBindingNamesInto(object.id, nextShadowed); for (const parameter of readArray(object.params)) { collectBindingNamesInto(parameter, nextShadowed); } collectRuntimeCallRootNamesFromNode( object.body, names, state, addBlockBindingNames(object.body, nextShadowed), ); return; } if (object.type === "CallExpression") { const root = expressionRootName(readOptionalObject(object.callee), state); if (root !== undefined && !shadowed.has(root)) { names.add(root); } } const childShadowed = object.type === "Program" || object.type === "BlockStatement" ? addBlockBindingNames(object, new Set(shadowed)) : shadowed; for (const [key, value] of Object.entries(object)) { if (shouldSkipRuntimeReferenceProperty(object, key)) { continue; } collectRuntimeCallRootNamesFromNode(value, names, state, childShadowed); } } function shouldSkipRuntimeReferenceNode(node: Record): boolean { return ( (typeof node.type === "string" && node.type.startsWith("TS")) || node.type === "ImportDeclaration" ); } function shouldSkipRuntimeReferenceProperty(node: Record, key: string): boolean { if (key === "type" || key === "start" || key === "end" || key === "loc") { return true; } if (key === "id" && isDeclarationWithId(node)) { return true; } if (key === "params" && isFunctionLikeClientRuntimeDeclaration(node)) { return true; } if (key === "key" && isNonComputedPropertyKey(node)) { return true; } return false; } function isRuntimeReferenceIdentifier( 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" && isDeclarationWithId(parent)) { return false; } if (parentKey === "params" && isFunctionLikeClientRuntimeDeclaration(parent)) { return false; } if (parentKey === "key" && isNonComputedPropertyKey(parent)) { return false; } return !( (parent.type === "BreakStatement" || parent.type === "ContinueStatement" || parent.type === "LabeledStatement") && parentKey === "label" ); } function addBlockBindingNames(node: unknown, names: Set): ReadonlySet { const body = readArray(readObject(node).body); for (const statement of body) { const object = readObject(statement); if (object.type === "VariableDeclaration") { for (const declaration of readArray(object.declarations)) { collectBindingNamesInto(readObject(declaration).id, names); } continue; } if (object.type === "FunctionDeclaration" || object.type === "ClassDeclaration") { collectBindingNamesInto(object.id, names); } } return names; } function collectBindingNamesInto(node: unknown, names: Set): void { for (const name of bindingNames(node)) { names.add(name); } } function isFunctionLikeClientRuntimeDeclaration(node: unknown): boolean { const object = readObject(node); return ( object.type === "FunctionDeclaration" || object.type === "FunctionExpression" || object.type === "ArrowFunctionExpression" ); } function isDeclarationWithId(node: Record): boolean { return ( node.type === "VariableDeclarator" || node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ClassDeclaration" || node.type === "ClassExpression" ); } function isNonComputedPropertyKey(node: Record): boolean { return ( (node.type === "Property" || node.type === "ObjectProperty" || node.type === "PropertyDefinition" || node.type === "MethodDefinition") && node.computed !== true ); } /** Checks whether a module begins with a specific directive string. */ export function hasModuleDirective(input: { code: string; directive: string; filename?: string | undefined; }): boolean { const parsed = parseModule(input.code, input.filename); return hasModuleDirectiveInProgram(parsed.program, input.directive); } function hasModuleDirectiveInProgram(program: unknown, expectedDirective: string): boolean { for (const statement of programBody(program)) { if (statement.type !== "ExpressionStatement") { return false; } const directive = statement.directive; if (typeof directive !== "string") { return false; } if (directive === expectedDirective) { return true; } } return false; } /** Checks whether a module contains syntax that requires client runtime execution. */ export function hasClientRuntimeSyntax(input: { code: string; filename?: string | undefined; }): boolean { const parsed = parseModule(input.code, input.filename); return hasClientRuntimeSyntaxNode(parsed.program); } interface Replacement { end: number; start: number; text: string; } const parseModuleCache = new Map(); const parseModuleCacheLimit = 128; function parseModule(code: string, filename: string | undefined) { const cacheKey = `${filename ?? ""}\0${code}`; const cached = parseModuleCache.get(cacheKey); if (cached !== undefined) { return parseModuleContext(cached); } const parsed = createCompilerModuleContext({ code, filename }); rememberParsedModule(cacheKey, parsed); return parseModuleContext(parsed); } function rememberParsedModule(cacheKey: string, context: CompilerModuleContext): void { if (parseModuleCache.size >= parseModuleCacheLimit) { const first = parseModuleCache.keys().next().value; if (first !== undefined) { parseModuleCache.delete(first); } } parseModuleCache.set(cacheKey, context); } function parseModuleContext(context: CompilerModuleContext): CompilerModuleContext { if (context.parseErrors.length > 0) { throw new Error( `${context.filename}: ${context.parseErrors.map(parseErrorMessage).join("\n")}`, ); } return context; } function parseErrorMessage(error: unknown): string { const object = readObject(error); const message = typeof object.message === "string" ? object.message : "Parse error"; const codeframe = typeof object.codeframe === "string" ? object.codeframe.trimEnd() : undefined; return codeframe === undefined ? message : `${message}\n${codeframe}`; } function programBody(program: unknown): Record[] { const body = readObject(program).body; return Array.isArray(body) ? body.map(readObject) : []; } function exportedNames(statement: Record): string[] { if (statement.type === "ExportDefaultDeclaration") { return ["default"]; } if (statement.type !== "ExportNamedDeclaration") { return []; } const declaration = readOptionalObject(statement.declaration); if (declaration !== undefined) { if (declaration.type === "FunctionDeclaration") { const name = readOptionalObject(declaration.id)?.name; return typeof name === "string" ? [name] : []; } if (declaration.type === "VariableDeclaration") { const declarations = Array.isArray(declaration.declarations) ? declaration.declarations : []; return declarations.flatMap((item) => bindingNames(readObject(item).id)); } return []; } const specifiers = Array.isArray(statement.specifiers) ? statement.specifiers : []; return specifiers.flatMap((specifier) => { const name = exportedNameForSpecifier(readObject(specifier)); return typeof name === "string" ? [name] : []; }); } function collectTopLevelDeclarationReferences( statement: Record, declarations: Map, ): void { const declaration = statement.type === "ExportNamedDeclaration" ? readOptionalObject(statement.declaration) : statement; if (declaration?.type === "FunctionDeclaration") { const name = readOptionalObject(declaration.id)?.name; if (typeof name === "string") { declarations.set(name, declaration); } return; } if (declaration?.type !== "VariableDeclaration") { return; } const declarators = Array.isArray(declaration.declarations) ? declaration.declarations.map(readObject) : []; for (const declarator of declarators) { const id = readOptionalObject(declarator.id); const name = typeof id?.name === "string" ? id.name : undefined; if (name !== undefined) { declarations.set(name, readOptionalObject(declarator.init) ?? declarator); } } } function declarationForExportedName( declaration: Record, name: string, ): unknown | undefined { if (declaration.type === "VariableDeclaration") { const declarators = Array.isArray(declaration.declarations) ? declaration.declarations.map(readObject) : []; for (const declarator of declarators) { if (bindingNames(declarator.id).includes(name)) { return readOptionalObject(declarator.init) ?? declarator; } } } return declaration; } function localNameForExportSpecifier(specifier: Record): string | undefined { const local = readOptionalObject(specifier.local); const name = local?.name ?? local?.value; return typeof name === "string" ? name : undefined; } function exportDeclarationDemotion( code: string, statement: Record, names: ReadonlySet, ): Replacement | undefined { if (statement.type !== "ExportNamedDeclaration") { return undefined; } const declaration = readOptionalObject(statement.declaration); const exported = exportedNames(statement); if (exported.length === 0 || !exported.every((name) => names.has(name))) { return partialSpecifierExportReplacement(code, statement, names); } const range = statementRange(code, statement); if (range === undefined) { return undefined; } if (declaration?.type === "FunctionDeclaration" || declaration?.type === "VariableDeclaration") { return { ...range, text: `${nodeText(code, declaration)}\n` }; } return { ...range, text: "" }; } function exportDeclarationReplacement( code: string, statement: Record, names: ReadonlySet, ): Replacement | undefined { if (statement.type === "ExportDefaultDeclaration") { const range = statementRange(code, statement); return names.has("default") && range !== undefined ? { ...range, text: "" } : undefined; } if (statement.type !== "ExportNamedDeclaration") { return undefined; } const partial = partialExportDeclarationReplacement(code, statement, names); if (partial !== undefined) { return partial; } const exported = exportedNames(statement); if (exported.length === 0 || !exported.every((name) => names.has(name))) { return undefined; } const range = statementRange(code, statement); return range === undefined ? undefined : { ...range, text: "" }; } function partialExportDeclarationReplacement( code: string, statement: Record, names: ReadonlySet, ): Replacement | undefined { const declaration = readOptionalObject(statement.declaration); if (declaration?.type === "VariableDeclaration") { return partialVariableExportReplacement(code, statement, declaration, names); } if (declaration !== undefined) { return undefined; } return partialSpecifierExportReplacement(code, statement, names); } function partialVariableExportReplacement( code: string, statement: Record, declaration: Record, names: ReadonlySet, ): Replacement | undefined { const declarations = Array.isArray(declaration.declarations) ? declaration.declarations.map(readObject) : []; if (declarations.length <= 1) { return undefined; } const kept = declarations.filter((declarator) => { const declaredNames = bindingNames(declarator.id); return declaredNames.every((name) => !names.has(name)); }); if (kept.length === declarations.length) { return undefined; } if (kept.length === 0) { const range = statementRange(code, statement); return range === undefined ? undefined : { ...range, text: "" }; } const range = statementRange(code, statement); const kind = typeof declaration.kind === "string" ? declaration.kind : "const"; return range === undefined ? undefined : { ...range, text: `export ${kind} ${kept.map((item) => nodeText(code, item)).join(", ")};\n`, }; } function partialSpecifierExportReplacement( code: string, statement: Record, names: ReadonlySet, ): Replacement | undefined { const specifiers = Array.isArray(statement.specifiers) ? statement.specifiers.map(readObject) : []; if (specifiers.length <= 1) { return undefined; } const kept = specifiers.filter((specifier) => { const name = exportedNameForSpecifier(specifier); return typeof name !== "string" || !names.has(name); }); if (kept.length === specifiers.length) { return undefined; } const range = statementRange(code, statement); if (range === undefined) { return undefined; } if (kept.length === 0) { return { ...range, text: "" }; } const source = readOptionalObject(statement.source); const sourceText = source === undefined ? "" : ` from ${nodeText(code, source)}`; const exportKind = statement.exportKind === "type" ? "export type" : "export"; return { ...range, text: `${exportKind} { ${kept.map((item) => nodeText(code, item)).join(", ")} }${sourceText};\n`, }; } function unusedStaticValueImportReplacement( code: string, statement: Record, referencedNames: ReadonlySet, ): Replacement | undefined { if (statement.type !== "ImportDeclaration" || statement.importKind === "type") { return undefined; } const specifiers = Array.isArray(statement.specifiers) ? statement.specifiers.map(readObject) : []; if (specifiers.length === 0) { return undefined; } const localNames = specifiers .filter((specifier) => specifier.importKind !== "type") .flatMap((specifier) => { const local = readOptionalObject(specifier.local); return typeof local?.name === "string" ? [local.name] : []; }); if (localNames.length === 0 || localNames.some((name) => referencedNames.has(name))) { return undefined; } const range = statementRange(code, statement); return range === undefined ? undefined : { ...range, text: "" }; } function staticModuleSpecifier(statement: Record): string[] { if (statement.type === "ImportDeclaration") { if (statement.importKind === "type") { return []; } return sourceValue(statement); } if (statement.type === "ExportAllDeclaration" || statement.type === "ExportNamedDeclaration") { if (statement.exportKind === "type") { return []; } return sourceValue(statement); } return []; } function staticImportReference( statement: Record, ): ClientRouteStaticImportReference[] { if (statement.type !== "ImportDeclaration" || statement.importKind === "type") { return []; } const source = sourceValue(statement)[0]; if (source === undefined) { return []; } const specifiers = Array.isArray(statement.specifiers) ? statement.specifiers.map(readObject) : []; const importSpecifiers = specifiers.flatMap(staticImportSpecifierReference); const localNames = specifiers .filter((specifier) => specifier.importKind !== "type") .flatMap((specifier) => { const local = readOptionalObject(specifier.local); return typeof local?.name === "string" ? [local.name] : []; }); return [ { localNames, sideEffect: localNames.length === 0, source, specifiers: importSpecifiers, }, ]; } function staticImportSpecifierReference( specifier: Record, ): StaticImportSpecifierReference[] { if (specifier.importKind === "type") { return []; } const localName = readOptionalObject(specifier.local)?.name; if (typeof localName !== "string") { return []; } if (specifier.type === "ImportDefaultSpecifier") { return [{ importedName: "default", kind: "default", localName }]; } if (specifier.type === "ImportNamespaceSpecifier") { return [{ importedName: "*", kind: "namespace", localName }]; } const imported = readOptionalObject(specifier.imported); const importedName = imported?.name ?? imported?.value; return typeof importedName === "string" ? [{ importedName, kind: "named", localName }] : []; } function staticExportReference(statement: Record): StaticExportReference[] { if (statement.type === "ExportAllDeclaration") { if (statement.exportKind === "type") { return []; } const source = sourceValue(statement)[0]; return source === undefined ? [] : [{ exportedNames: [], exportAll: true, specifiers: [], source }]; } if (statement.type !== "ExportNamedDeclaration" || statement.exportKind === "type") { return []; } const source = sourceValue(statement)[0]; if (source === undefined) { return []; } return [ { exportedNames: exportedNames(statement), exportAll: false, specifiers: staticExportSpecifierReferences(statement), source, }, ]; } function staticExportSpecifierReferences( statement: Record, ): StaticExportSpecifierReference[] { const specifiers = Array.isArray(statement.specifiers) ? statement.specifiers.map(readObject) : []; return specifiers.flatMap((specifier) => { const exportedName = exportedNameForSpecifier(specifier); const localName = localNameForExportSpecifier(specifier); return exportedName === undefined || localName === undefined ? [] : [{ exportedName, localName }]; }); } function sourceValue(statement: Record): string[] { const source = readOptionalObject(statement.source); const value = source?.value; return typeof value === "string" ? [value] : []; } function collectJsxComponentRootNamesFromNode(node: unknown, names: Set): void { if (Array.isArray(node)) { for (const child of node) { collectJsxComponentRootNamesFromNode(child, names); } return; } const object = readOptionalObject(node); if (object === undefined) { return; } if (object.type === "JSXElement") { const opening = readOptionalObject(object.openingElement); const nameNode = readOptionalObject(opening?.name); const name = jsxNameRoot(nameNode); if (name !== undefined && (nameNode?.type === "JSXMemberExpression" || /^[A-Z]/.test(name))) { names.add(name); } } for (const [key, value] of Object.entries(object)) { if (key === "type" || key === "start" || key === "end" || key === "loc") { continue; } collectJsxComponentRootNamesFromNode(value, names); } } function collectJsxComponentNamesFromNode(node: unknown, names: Set): void { if (Array.isArray(node)) { for (const child of node) { collectJsxComponentNamesFromNode(child, names); } return; } const object = readOptionalObject(node); if (object === undefined) { return; } if (object.type === "JSXElement") { const opening = readOptionalObject(object.openingElement); const nameNode = readOptionalObject(opening?.name); const root = jsxNameRoot(nameNode); const name = jsxTagName(nameNode); if ( name !== "" && root !== undefined && (nameNode?.type === "JSXMemberExpression" || /^[A-Z]/.test(root)) ) { names.add(name); } } for (const [key, value] of Object.entries(object)) { if (key === "type" || key === "start" || key === "end" || key === "loc") { continue; } collectJsxComponentNamesFromNode(value, names); } } function collectJsxComponentNamesFromSubtree( node: unknown, outerAliases?: ReadonlyMap | undefined, ): string[] { const names = new Set(); const aliasState = createComponentAliasState(outerAliases); collectJsxComponentNamesFromNode(node, names); collectSimpleComponentAliasesFromNode(node, aliasState); expandJsxComponentAliasRoots(names, aliasState.aliases); return Array.from(names).sort(); } function collectJsxComponentRootNamesFromSubtree( node: unknown, outerAliases?: ReadonlyMap | undefined, ): string[] { const names = new Set(); const aliasState = createComponentAliasState(outerAliases); collectJsxComponentRootNamesFromNode(node, names); collectSimpleComponentAliasesFromNode(node, aliasState); expandJsxComponentAliasRoots(names, aliasState.aliases); return Array.from(names).sort(); } function collectComponentCallRootNamesFromNode( node: unknown, names: Set, state: ComponentAliasState, ): void { if (Array.isArray(node)) { for (const child of node) { collectComponentCallRootNamesFromNode(child, names, state); } return; } const object = readOptionalObject(node); if (object === undefined) { return; } if (typeof object.type === "string" && object.type.startsWith("TS")) { return; } if (object.type === "CallExpression") { const root = expressionRootName(readOptionalObject(object.callee), state); if (root !== undefined && /^[A-Z]/.test(root)) { names.add(root); } } for (const [key, value] of Object.entries(object)) { if (key === "type" || key === "start" || key === "end" || key === "loc") { continue; } collectComponentCallRootNamesFromNode(value, names, state); } } function collectComponentCallRootNamesFromSubtree( node: unknown, outerAliases?: ReadonlyMap | undefined, ): string[] { const names = new Set(); const aliasState = createComponentAliasState(outerAliases); collectSimpleComponentAliasesFromNode(node, aliasState); collectComponentCallRootNamesFromNode(node, names, aliasState); expandJsxComponentAliasRoots(names, aliasState.aliases); return Array.from(names).sort(); } function jsxNameRoot(node: Record | undefined): string | undefined { if (node === undefined) { return undefined; } if (typeof node.name === "string") { return node.name; } if (node.type === "JSXMemberExpression") { return jsxNameRoot(readOptionalObject(node.object)); } return undefined; } function collectSimpleComponentAliasesFromNode(node: unknown, state: ComponentAliasState): void { if (Array.isArray(node)) { for (const child of node) { collectSimpleComponentAliasesFromNode(child, state); } return; } const object = readOptionalObject(node); if (object === undefined) { return; } if (typeof object.type === "string" && object.type.startsWith("TS")) { return; } if (object.type === "VariableDeclaration") { const constant = object.kind === "const"; const declarations = Array.isArray(object.declarations) ? object.declarations : []; for (const declaration of declarations) { collectVariableDeclaratorComponentAliases(readOptionalObject(declaration), state, constant); } return; } if (object.type === "VariableDeclarator") { collectVariableDeclaratorComponentAliases(object, state, false); } if (object.type === "AssignmentExpression") { collectAssignmentComponentAlias(object, state); } if (object.type === "CallExpression") { collectObjectAssignComponentAliases(object, state); } for (const [key, value] of Object.entries(object)) { if (key === "type" || key === "start" || key === "end" || key === "loc") { continue; } collectSimpleComponentAliasesFromNode(value, state); } } function createComponentAliasState( aliases?: ReadonlyMap | undefined, ): ComponentAliasState { return { aliases: new Map(aliases), stringConstants: new Map(), }; } function collectVariableDeclaratorComponentAliases( object: Record | undefined, state: ComponentAliasState, constant: boolean, ): void { if (object?.type !== "VariableDeclarator") { return; } const id = readOptionalObject(object.id); const init = readOptionalObject(object.init); const aliasName = typeof id?.name === "string" ? id.name : undefined; if (aliasName === undefined) { return; } if (constant) { const stringValue = stringExpressionValue(init, state); if (stringValue !== undefined) { state.stringConstants.set(aliasName, stringValue); } } collectObjectLiteralComponentAliases(aliasName, init, state); const rootName = expressionRootName(init, state); if (rootName !== undefined) { state.aliases.set(aliasName, rootName); } } function collectObjectLiteralComponentAliases( objectName: string | undefined, init: Record | undefined, state: ComponentAliasState, ): void { if (objectName === undefined || init?.type !== "ObjectExpression") { return; } const properties = Array.isArray(init.properties) ? init.properties : []; for (const propertyValue of properties) { const property = readOptionalObject(propertyValue); if (property?.type !== "Property") { continue; } const keyName = propertyName( readOptionalObject(property.key), property.computed === true, state, ); const valueName = expressionRootName(readOptionalObject(property.value), state); if (keyName !== undefined && valueName !== undefined) { state.aliases.set(`${objectName}.${keyName}`, valueName); } } } function collectAssignmentComponentAlias( node: Record, state: ComponentAliasState, ): void { if (node.operator !== "=") { return; } const left = readOptionalObject(node.left); const right = readOptionalObject(node.right); if (left?.type !== "MemberExpression") { return; } const objectRoot = expressionRootName(readOptionalObject(left.object), state); const memberName = propertyName(readOptionalObject(left.property), left.computed === true, state); const valueName = expressionRootName(right, state); if (objectRoot !== undefined && memberName !== undefined && valueName !== undefined) { state.aliases.set(`${objectRoot}.${memberName}`, valueName); } } function collectObjectAssignComponentAliases( node: Record, state: ComponentAliasState, ): void { if (!isObjectAssignCall(node)) { return; } const args = Array.isArray(node.arguments) ? node.arguments.map(readOptionalObject) : []; const target = expressionRootName(args[0], state); if (target === undefined) { return; } for (const source of args.slice(1)) { collectObjectLiteralComponentAliases(target, source, state); } } function isObjectAssignCall(node: Record): boolean { const callee = readOptionalObject(node.callee); if (callee?.type !== "MemberExpression" || callee.computed === true) { return false; } const object = readOptionalObject(callee.object); const property = readOptionalObject(callee.property); return ( object?.type === "Identifier" && object.name === "Object" && property?.type === "Identifier" && property.name === "assign" ); } function expandJsxComponentAliasRoots( names: Set, aliases: ReadonlyMap, ): void { let changed = true; while (changed) { changed = false; for (const [alias, root] of aliases) { if (names.has(alias) && !names.has(root)) { names.add(root); changed = true; } } } } function expressionRootName( node: Record | undefined, state?: ComponentAliasState | undefined, ): string | undefined { if (node === undefined) { return undefined; } if (node.type === "Identifier" && typeof node.name === "string") { return node.name; } if (node.type === "MemberExpression") { const objectRoot = expressionRootName(readOptionalObject(node.object), state); const aliasedObjectRoot = objectRoot === undefined ? undefined : (state?.aliases.get(objectRoot) ?? objectRoot); const memberName = propertyName( readOptionalObject(node.property), node.computed === true, state, ); const memberAlias = objectRoot !== undefined && memberName !== undefined ? (state?.aliases.get(`${objectRoot}.${memberName}`) ?? (aliasedObjectRoot === undefined ? undefined : state?.aliases.get(`${aliasedObjectRoot}.${memberName}`))) : undefined; return ( memberAlias ?? (node.computed === true && memberName === undefined ? uniqueObjectMemberAlias(aliasedObjectRoot, state) : aliasedObjectRoot) ); } if (node.type === "ConditionalExpression") { return uniqueDefinedString([ expressionRootName(readOptionalObject(node.consequent), state), expressionRootName(readOptionalObject(node.alternate), state), ]); } if ( node.type === "ChainExpression" || node.type === "TSAsExpression" || node.type === "TSSatisfiesExpression" || node.type === "TSNonNullExpression" || node.type === "ParenthesizedExpression" ) { return expressionRootName(readOptionalObject(node.expression), state); } return undefined; } function propertyName( node: Record | undefined, computed: boolean, state?: ComponentAliasState | undefined, ): string | undefined { if (!computed && node?.type === "Identifier" && typeof node.name === "string") { return node.name; } if (computed && node?.type === "Identifier" && typeof node.name === "string") { return state?.stringConstants.get(node.name); } return stringExpressionValue(node, state); } function stringExpressionValue( node: Record | undefined, state?: ComponentAliasState | undefined, ): string | undefined { if ( (node?.type === "StringLiteral" || node?.type === "Literal") && typeof node.value === "string" ) { return node.value; } if (node?.type === "ConditionalExpression") { return uniqueDefinedString([ stringExpressionValue(readOptionalObject(node.consequent), state), stringExpressionValue(readOptionalObject(node.alternate), state), ]); } if (node?.type === "Identifier" && typeof node.name === "string") { return state?.stringConstants.get(node.name); } if ( node?.type === "ChainExpression" || node?.type === "TSAsExpression" || node?.type === "TSSatisfiesExpression" || node?.type === "TSNonNullExpression" || node?.type === "ParenthesizedExpression" ) { return stringExpressionValue(readOptionalObject(node.expression), state); } return undefined; } function uniqueObjectMemberAlias( objectName: string | undefined, state?: ComponentAliasState | undefined, ): string | undefined { if (objectName === undefined || state === undefined) { return undefined; } const prefix = `${objectName}.`; return uniqueDefinedString( Array.from(state.aliases.entries()) .filter(([key]) => key.startsWith(prefix)) .map(([, value]) => value), ); } function uniqueDefinedString(values: readonly (string | undefined)[]): string | undefined { const unique = new Set(values.filter((value): value is string => value !== undefined)); return unique.size === 1 ? Array.from(unique)[0] : undefined; } function collectIdentifierReferenceNamesFromNode(node: unknown, names: Set): void { if (Array.isArray(node)) { for (const child of node) { collectIdentifierReferenceNamesFromNode(child, names); } return; } const object = readOptionalObject(node); if (object === undefined) { return; } if (typeof object.type === "string" && object.type.startsWith("TS")) { return; } if (object.type === "ImportDeclaration") { return; } if ( (object.type === "Identifier" || object.type === "JSXIdentifier") && typeof object.name === "string" ) { names.add(object.name); } for (const [key, value] of Object.entries(object)) { if (key === "type" || key === "start" || key === "end" || key === "loc") { continue; } collectIdentifierReferenceNamesFromNode(value, names); } } const browserGlobalNames = new Set(["window", "document", "localStorage"]); function hasUnguardedBrowserGlobalReferenceInNode(node: unknown, browserOnly: boolean): boolean { if (Array.isArray(node)) { return node.some((child) => hasUnguardedBrowserGlobalReferenceInNode(child, browserOnly)); } const object = readOptionalObject(node); if (object === undefined) { return false; } if (typeof object.type === "string" && object.type.startsWith("TS")) { return false; } if (object.type === "ImportDeclaration") { return false; } if (object.type === "Identifier") { return !browserOnly && typeof object.name === "string" && browserGlobalNames.has(object.name); } // `typeof window` itself never throws on the server; a bare identifier // operand is the guard syntax rather than a real use. if ( object.type === "UnaryExpression" && object.operator === "typeof" && readOptionalObject(object.argument)?.type === "Identifier" ) { return false; } // `settings.window` reads a property name, not the global. if (object.type === "MemberExpression" && object.computed !== true) { return hasUnguardedBrowserGlobalReferenceInNode(object.object, browserOnly); } // `{ window: value }` declares a key, not a global reference; shorthand // (`{ window }`) keeps its value identifier and is still walked. if (object.type === "Property" && object.computed !== true) { return hasUnguardedBrowserGlobalReferenceInNode(object.value, browserOnly); } if (object.type === "IfStatement" || object.type === "ConditionalExpression") { const consequentBrowserOnly = browserOnly || guardImpliesBrowser(object.test, true); const alternateBrowserOnly = browserOnly || guardImpliesBrowser(object.test, false); return ( hasUnguardedBrowserGlobalReferenceInNode(object.test, browserOnly) || hasUnguardedBrowserGlobalReferenceInNode(object.consequent, consequentBrowserOnly) || hasUnguardedBrowserGlobalReferenceInNode(object.alternate, alternateBrowserOnly) ); } if (object.type === "LogicalExpression") { const rightBrowserOnly = browserOnly || (object.operator === "&&" && guardImpliesBrowser(object.left, true)) || (object.operator === "||" && guardImpliesBrowser(object.left, false)); return ( hasUnguardedBrowserGlobalReferenceInNode(object.left, browserOnly) || hasUnguardedBrowserGlobalReferenceInNode(object.right, rightBrowserOnly) ); } // Statement lists track guarded early exits: past an // `if (typeof window === "undefined") return;` statement the rest of the // block only executes in the browser. if (object.type === "Program" || object.type === "BlockStatement") { let scopedBrowserOnly = browserOnly; for (const statement of readArray(object.body)) { if (hasUnguardedBrowserGlobalReferenceInNode(statement, scopedBrowserOnly)) { return true; } if (!scopedBrowserOnly && isBrowserOnlyEarlyExitGuard(statement)) { scopedBrowserOnly = true; } } return false; } for (const [key, value] of Object.entries(object)) { if (key === "type" || key === "start" || key === "end" || key === "loc") { continue; } if (hasUnguardedBrowserGlobalReferenceInNode(value, browserOnly)) { return true; } } return false; } // Does `test === whenTruthy` imply the code runs in a browser? Recognizes // `typeof !== "undefined"` comparisons in either operand // order, `!` negation, and the logical compositions whose overall value pins // every operand: a truthy `&&` pins both sides truthy, a falsy `||` pins both // sides falsy. function guardImpliesBrowser(test: unknown, whenTruthy: boolean): boolean { const object = readOptionalObject(test); if (object === undefined) { return false; } if ( object.type === "ParenthesizedExpression" || object.type === "TSAsExpression" || object.type === "TSSatisfiesExpression" || object.type === "TSNonNullExpression" ) { return guardImpliesBrowser(object.expression, whenTruthy); } if (object.type === "UnaryExpression" && object.operator === "!") { return guardImpliesBrowser(object.argument, !whenTruthy); } if (object.type === "LogicalExpression") { if (object.operator === "&&" && whenTruthy) { return guardImpliesBrowser(object.left, true) || guardImpliesBrowser(object.right, true); } if (object.operator === "||" && !whenTruthy) { return guardImpliesBrowser(object.left, false) || guardImpliesBrowser(object.right, false); } return false; } if (object.type !== "BinaryExpression" || typeof object.operator !== "string") { return false; } const comparesTypeofToUndefined = (isTypeofBrowserGlobal(object.left) && isUndefinedStringLiteral(object.right)) || (isTypeofBrowserGlobal(object.right) && isUndefinedStringLiteral(object.left)); if (!comparesTypeofToUndefined) { return false; } if (object.operator === "!==" || object.operator === "!=") { return whenTruthy; } if (object.operator === "===" || object.operator === "==") { return !whenTruthy; } return false; } function isTypeofBrowserGlobal(node: unknown): boolean { const object = readOptionalObject(node); if (object?.type !== "UnaryExpression" || object.operator !== "typeof") { return false; } const argument = readOptionalObject(object.argument); return ( argument?.type === "Identifier" && typeof argument.name === "string" && browserGlobalNames.has(argument.name) ); } function isUndefinedStringLiteral(node: unknown): boolean { const object = readOptionalObject(node); return ( (object?.type === "Literal" || object?.type === "StringLiteral") && object.value === "undefined" ); } // A statement after which the enclosing block only executes in the browser: // an alternate-free `if` whose test pins the server case and whose consequent // always exits (`return`, `throw`, `break`, `continue`, or a block ending in // one of those). function isBrowserOnlyEarlyExitGuard(statement: unknown): boolean { const object = readOptionalObject(statement); if (object?.type !== "IfStatement" || readOptionalObject(object.alternate) !== undefined) { return false; } return guardImpliesBrowser(object.test, false) && consequentAlwaysExits(object.consequent); } function consequentAlwaysExits(node: unknown): boolean { const object = readOptionalObject(node); if (object === undefined) { return false; } if ( object.type === "ReturnStatement" || object.type === "ThrowStatement" || object.type === "BreakStatement" || object.type === "ContinueStatement" ) { return true; } if (object.type === "BlockStatement") { const body = readArray(object.body); return body.length > 0 && consequentAlwaysExits(body[body.length - 1]); } return false; } function collectFormActionReferencesFromNode( node: unknown, references: FormActionReference[], ): void { if (Array.isArray(node)) { for (const child of node) { collectFormActionReferencesFromNode(child, references); } return; } const object = readOptionalObject(node); if (object === undefined) { return; } if (typeof object.type === "string" && object.type.startsWith("TS")) { return; } if (object.type === "ImportDeclaration") { return; } if ( object.type === "JSXOpeningElement" && jsxTagName(readOptionalObject(object.name)) === "form" ) { const attributes = Array.isArray(object.attributes) ? object.attributes : []; for (const attribute of attributes) { const attr = readObject(attribute); if (attr.type !== "JSXAttribute" || readObject(attr.name).name !== "action") { continue; } const value = readObject(attr.value); const expression = readObject(value.expression); const start = typeof object.start === "number" ? object.start : undefined; const end = typeof object.end === "number" ? object.end : undefined; if ( value.type === "JSXExpressionContainer" && typeof expression.name === "string" && start !== undefined && end !== undefined ) { references.push({ end, name: expression.name, start }); } } } for (const [key, value] of Object.entries(object)) { if (key === "type" || key === "start" || key === "end" || key === "loc") { continue; } collectFormActionReferencesFromNode(value, references); } } function collectFormActionExpressionReferencesFromNode( code: string, node: unknown, references: FormActionExpressionReference[], ): void { if (Array.isArray(node)) { for (const child of node) { collectFormActionExpressionReferencesFromNode(code, child, references); } return; } const object = readOptionalObject(node); if (object === undefined) { return; } if (typeof object.type === "string" && object.type.startsWith("TS")) { return; } if (object.type === "ImportDeclaration") { return; } if ( object.type === "JSXOpeningElement" && jsxTagName(readOptionalObject(object.name)) === "form" ) { const attributes = Array.isArray(object.attributes) ? object.attributes : []; for (const attribute of attributes) { const attr = readObject(attribute); if (attr.type !== "JSXAttribute" || readObject(attr.name).name !== "action") { continue; } const value = readObject(attr.value); const expression = readObject(value.expression); const start = typeof object.start === "number" ? object.start : undefined; const end = typeof object.end === "number" ? object.end : undefined; const expressionStart = typeof expression.start === "number" ? expression.start : undefined; const expressionEnd = typeof expression.end === "number" ? expression.end : undefined; if ( value.type === "JSXExpressionContainer" && start !== undefined && end !== undefined && expressionStart !== undefined && expressionEnd !== undefined ) { references.push({ end, expression: code.slice(expressionStart, expressionEnd).trim(), expressionEnd, expressionStart, start, }); } } } for (const [key, value] of Object.entries(object)) { if (key === "type" || key === "start" || key === "end" || key === "loc") { continue; } collectFormActionExpressionReferencesFromNode(code, value, references); } } function jsxTagName(node: Record | undefined): string { if (node === undefined) { return ""; } if (typeof node.name === "string") { return node.name; } if (node.type === "JSXIdentifier" && typeof node.name === "string") { return node.name; } if (node.type === "JSXMemberExpression") { const objectName = jsxTagName(readOptionalObject(node.object)); const propertyName = jsxTagName(readOptionalObject(node.property)); return `${objectName}.${propertyName}`; } return ""; } function hasClientRuntimeSyntaxNode(node: unknown): boolean { if (Array.isArray(node)) { return node.some(hasClientRuntimeSyntaxNode); } const object = readOptionalObject(node); if (object === undefined) { return false; } if (typeof object.type === "string" && object.type.startsWith("TS")) { return false; } if (object.type === "ImportDeclaration") { return false; } if (object.type === "ExportAllDeclaration") { return false; } if (object.type === "ExportNamedDeclaration" || object.type === "ExportDefaultDeclaration") { return hasClientRuntimeSyntaxNode(object.declaration); } if (object.type === "JSXAttribute") { const name = readOptionalObject(object.name)?.name; return typeof name === "string" && (name === "domRef" || /^on[A-Z]/.test(name)); } if (object.type === "CallExpression") { const callee = readOptionalObject(object.callee); if (callee?.type === "Identifier" && callee.name === "cell") { return true; } } if (object.type === "Identifier" && isClientRuntimeGlobal(object.name)) { return true; } if (object.type === "MemberExpression") { return ( hasClientRuntimeSyntaxNode(object.object) || (object.computed === true && hasClientRuntimeSyntaxNode(object.property)) ); } for (const [key, value] of Object.entries(object)) { if (key === "type" || key === "start" || key === "end" || key === "loc") { continue; } if (hasClientRuntimeSyntaxNode(value)) { return true; } } return false; } function isClientRuntimeGlobal(name: unknown): boolean { return name === "window" || name === "document" || name === "localStorage"; } function exportedNameForSpecifier(specifier: Record): string | undefined { const exported = readOptionalObject(specifier.exported); const local = readOptionalObject(specifier.local); const name = exported?.name ?? exported?.value ?? local?.name ?? local?.value; return typeof name === "string" ? name : undefined; } function bindingNames(node: unknown): string[] { const object = readObject(node); if (typeof object.name === "string") { return [object.name]; } if (Array.isArray(object.properties)) { return object.properties.flatMap((property) => bindingNames(readObject(property).value)); } if (Array.isArray(object.elements)) { return object.elements.flatMap((element) => (element === null ? [] : bindingNames(element))); } if (object.type === "AssignmentPattern") { return bindingNames(object.left); } if (object.type === "RestElement") { return bindingNames(object.argument); } return []; } function statementRange( code: string, statement: Record, ): { end: number; start: number } | undefined { const start = typeof statement.start === "number" ? statement.start : undefined; const end = typeof statement.end === "number" ? statement.end : undefined; if (start === undefined || end === undefined) { return undefined; } let removalStart = start; while (removalStart > 0 && code[removalStart - 1] !== "\n") { if (!/\s/.test(code[removalStart - 1] ?? "")) { break; } removalStart -= 1; } let removalEnd = end; while (removalEnd < code.length && /[ \t]/.test(code[removalEnd] ?? "")) { removalEnd += 1; } if (code[removalEnd] === "\r") { removalEnd += 1; } if (code[removalEnd] === "\n") { removalEnd += 1; } return { start: removalStart, end: removalEnd }; } function nodeText(code: string, node: Record): string { const start = typeof node.start === "number" ? node.start : undefined; const end = typeof node.end === "number" ? node.end : undefined; return start === undefined || end === undefined ? "" : code.slice(start, end); } function readObject(value: unknown): Record { return typeof value === "object" && value !== null ? (value as Record) : {}; } function readArray(value: unknown): unknown[] { return Array.isArray(value) ? value : []; } function readOptionalObject(value: unknown): Record | undefined { return typeof value === "object" && value !== null ? (value as Record) : undefined; } export type { AsyncBoundaryIr, AttributeIr, CompiledSingleNodeListIr, CompilerKeyedEventProgramIr, CompilerSelectedClassIr, ComponentIr, ComponentPropIr, ComponentRefIr, ConditionalIr, DomRefAttributeIr, DynamicAttributeIr, EventAttributeIr, ExprIr, JsxElementIr, JsxFragmentIr, JsxNodeIr, ListIr, ModuleIr, SpreadAttributeIr, StaticAttributeIr, TextIr, } from "./ir.js";