/* THE program lifecycle: load → preflight — tsconfig adoption, the tsc * gate with its project-world second chance, the import/export form * fences, CommonJS require discipline, and the module evaluation order — * on the native TypeScript compiler (one spawned tsgo server behind * Ts7Host, through the ts7/ adapter) with scriptc's OWN resolver * (resolve.ts) standing in for ts.resolveModuleName / * ts.resolveTypeReferenceDirective, which typescript@7.0.2 no longer * ships. * * HISTORY. This began as a deliberately MECHANICAL PORT of the * typescript@5.9.3 checkPreflight (the adapter's design: swap the ts * import, keep every spelling), shipped alongside it for phases 2-3 with * the order-parity suite holding both lanes to identical diagnostics and * module order over the whole corpus. The phase-4 flip deleted the 5.9.3 * pipeline; its recorded verdicts live on as the canary baselines * (test/ts7/baselines/order-parity/), and the rationale prose the old * program.ts carried was folded in here where behavior demands it. * * Differences from the retired 5.9.3 lane, each deliberate and each * pinned by the canary baselines: * - Module resolution is resolve.ts (answer-parity proven by its suite). * - Diagnostics carry pos/end and flat text + messageChain; toPassthrough * renders through the adapter's flattenDiagnosticMessageText, whose * output is identical to 5.9.3's flattener output. * - Finding 5 (tsgo models CommonJS strictly): TS2309 joins the JS-file * relaxation here — 5.9.3 never fires it on JS CJS files, tsgo does on * the table-then-member pattern. The EXPORT-IDENTITY fences do not live * in tsc codes at all: cjsExportDiscardReason (lowering) names every * genuinely discarded export, so relaxing the checker's stricter * modeling loses nothing. * - isNodeEsmFile: 7's client SourceFile has no impliedNodeFormat, so this * lane spells Node's format decision directly: fixed extensions first, * then an explicit nearest-package `type`, then syntax detection for * ambiguous .js/.ts files. * - Malformed tsconfig.json: tsgo's parseConfigFile swallows JSONC syntax * errors (recovers to empty options) where 5.9.3 reported a passthrough * diagnostic, so this lane validates the config's JSONC syntax itself * (jsoncSyntaxError) and fails preflight like 5.9.3 does — with * JSON.parse's wording rather than tsc's, the one message-text delta on * that path (no snapshot pins it). */ import { builtinModules } from "node:module"; import { dirname, isAbsolute, resolve } from "node:path"; import * as ts from "./ts7/adapter.js"; import type { ScrDiagnostic } from "../diagnostics/diagnostic.js"; import { commonJsModuleSyntaxDiag, invalidPackageConfigDiag, strictNullChecksFloorDiag, tscPassthroughDiag, unsupportedDiag, } from "../diagnostics/diagnostic.js"; import { isNodeModulesPath, nearestInvalidPackageJsonPath, nearestPackageType, nearestPkgJsonPath, packageImportTarget, projectDtsRuntimeSibling, resolveBareModule, resolveProjectModule, resolveTypeDirective, setProjectPathMappings, setProjectRealm, } from "./resolve.js"; import { probeNodeImportRefusal, probeNodeRequireRefusal } from "./npm.js"; import { isNpmStaticPackage, npmStaticActive, npmStaticFsShadow, programBuiltinAliasSource, clearProgramBuiltinAliasCache, npmStaticLexerVisibleNames, npmStaticLiveExportTable, npmStaticPackageOfPath, reportNpmStaticOffender, setNpmStaticDeclarationOverloads, setNpmStaticPackages, setNpmStaticTypeExports, } from "./npm-static.js"; import { isPrunedNpmReexport, planNpmStaticReexports } from "./npm-static-prune.js"; import { npmStaticTypeExportPlan } from "./npm-static-type-exports-host.js"; import { isNpmStaticSubclassArgument } from "./npm-static-subtyping.js"; import { npmStaticDeclarationReexports, npmStaticRuntimeClassTargets, parseNpmStaticDeclarationOverloads, parseNpmStaticDeclarationProperties, } from "./npm-static-declaration-syntax.js"; import type { FrontendServices } from "./services.js"; import type { NpmStaticDeclarationOverloads, NpmStaticDeclarationProperties, NpmStaticOverloadSignature, } from "./npm-static-declaration-syntax.js"; import { configurePackageShims, isPackageShimFile, isPackageShimName, isShimmedImport, } from "./package-shims.js"; import { provenanceEntryFor, provenancePaths } from "./provenance-registry.js"; import { cjsLexedExportsOfFile, cjsVisibleNames } from "./cjs-syntax.js"; import { ambientDtsPath, fallbackDtsPath, isNodeTypesPath, overridesDtsPath, tsgoPath, } from "./dts-paths.js"; import { builtinDefaultImportModule, canonicalBuiltinModule, npmBuiltinAliasOf, SUPPORTED_NODE_MODULES, unsupportedModuleFeatureOf, TEMPORAL_GLOBAL, } from "./builtin-modules.js"; import { clearWorkspacePackages, isRelativeSpecifier, isWorkspacePackageName, npmPackageNameOf, registerWorkspacePackage, workspacePackageOfPath, } from "./workspace-registry.js"; import { ADOPTED_OPTIONS, isJsSourceFileName, JS_ANY_OPERATOR_CODES, JS_RELAXED_TSC_CODES, } from "./tsc-codes.js"; import { trackedFileExists, trackedReadFile, trackedRealpath } from "./input-tracker.js"; import { forkTargetPaths } from "./fork-target.js"; import { workerTargetPaths } from "./worker-target.js"; import { erasedTypeOnlyImport, typeOnlyNamedImport, typeOnlyReachedFiles, } from "./type-only-imports.js"; import { moduleSourceCandidates } from "./module-source-candidates.js"; import { planDeclProjection, type DeclProjection } from "./decl-projection.js"; import { inferredJsDiagnosticSuppressed } from "./inferred-js-diagnostics.js"; import { containsJsx, firstJsx, jsxEmitName, jsxEmitPlan, jsxPragmas, lowerJsx, type LoweredJsx, } from "./jsx-source.js"; const BASE_OPTIONS: ts.Ts7CompilerOptions = { strict: true, }; const FORCED_OPTIONS: ts.Ts7CompilerOptions = { target: ts.ScriptTarget.ESNext as number, module: ts.ModuleKind.ESNext as number, moduleResolution: ts.ModuleResolutionKind.Bundler, // Every file Node executes has its own lexical module wrapper: ESM has a // module environment and CommonJS has the per-file function wrapper. In // particular, an ambiguous file whose ONLY ESM marker is top-level await // must not remain a TypeScript global script (otherwise declarations in // sibling files merge and reads cross module boundaries). Runtime format // classification is intentionally independent below; this option is only // the binder's scope decision. moduleDetection: ts.ModuleDetectionKind.Force, // Explicit resource management, Array.fromAsync and Error.isError are // ESNext library slices in TS 7 even though Node 24 ships them. Include // those focused libs without widening the rest of the standard-library // target past ES2025. lib: [ "lib.es2025.d.ts", "lib.esnext.disposable.d.ts", "lib.esnext.array.d.ts", "lib.esnext.error.d.ts", ], types: [], allowImportingTsExtensions: true, allowJs: true, checkJs: true, resolveJsonModule: true, noEmit: true, }; /** JSONC syntax validation for a tsconfig text: tsgo's parseConfigFile * RECOVERS over syntax errors without reporting them (probed: a hard-broken * file answers empty options, no diagnostic), where 5.9.3's readConfigFile * reported the first parse error — a preflight-visible difference (a broken * config fails the build instead of being adopted). tsconfig's grammar is * JSON plus comments and trailing commas, so stripping exactly those and * handing the rest to JSON.parse decides validity without either * TypeScript's parser. The * MESSAGE is JSON.parse's, not 5.9.3's ("'}' expected.") — the one * remaining wording delta on this path, unpinned by any snapshot. */ function jsoncSyntaxError(text: string): string | null { let out = ""; for (let i = 0; i < text.length; i++) { const ch = text[i]!; if (ch === '"') { const from = i; i++; while (i < text.length && text[i] !== '"') i += text[i] === "\\" ? 2 : 1; out += text.slice(from, i + 1); continue; } if (ch === "/" && text[i + 1] === "/") { while (i < text.length && text[i] !== "\n") i++; out += "\n"; continue; } if (ch === "/" && text[i + 1] === "*") { const close = text.indexOf("*/", i + 2); i = close < 0 ? text.length : close + 1; continue; } out += ch; } // Trailing commas: `,` followed only by whitespace before } or ]. out = out.replace(/,(\s*[}\]])/g, "$1"); if (out.trim() === "") return null; // an empty config file is legal try { JSON.parse(out); return null; } catch (e) { return e instanceof Error ? e.message : String(e); } } /** Preserve TypeScript 5.9's computed synthetic-default permission from the * project's own module settings. Preflight later forces ESNext/Bundler for * scriptc's checker, so those forced options must not decide which source * import forms were legal in the original project. */ function projectAllowsSyntheticDefaultImports(options: Record): boolean { const explicitSyntheticDefaultImports = options["allowSyntheticDefaultImports"]; if (explicitSyntheticDefaultImports !== undefined) return explicitSyntheticDefaultImports === true; const moduleKind = options["module"]; const explicitEsModuleInterop = options["esModuleInterop"]; const esModuleInterop = explicitEsModuleInterop !== undefined ? explicitEsModuleInterop === true : moduleKind === ts.ModuleKind.Node16 || moduleKind === ts.ModuleKind.Node18 || moduleKind === ts.ModuleKind.Node20 || moduleKind === ts.ModuleKind.NodeNext || moduleKind === ts.ModuleKind.Preserve; return ( esModuleInterop || moduleKind === ts.ModuleKind.System || options["moduleResolution"] === ts.ModuleResolutionKind.Bundler ); } /** The project's tsconfig adoption in the 7 world: tsgo's own config parser * (extends chains resolved server-side), the ADOPTED_OPTIONS subset taken, * the rest forced, the strictNullChecks floor enforced. */ function adoptProjectConfig7( host: ts.Ts7Host, entryPath: string, ): { configFile: string | null; options: ts.Ts7CompilerOptions; diags: ScrDiagnostic[]; projectLib?: readonly string[]; forcedStrictNullChecks?: boolean; checkJs?: boolean; } { const configFile = ts.findConfigFile(dirname(entryPath), ts.sys.fileExists) ?? null; if (!configFile) { return { configFile, options: { ...BASE_OPTIONS, ...FORCED_OPTIONS }, diags: [] }; } const diags: ScrDiagnostic[] = []; const syntaxError = jsoncSyntaxError(ts.sys.readFile(configFile) ?? ""); if (syntaxError !== null) { diags.push(tscPassthroughDiag(syntaxError, { file: configFile, start: 0, end: 0 })); return { configFile, options: { ...BASE_OPTIONS, ...FORCED_OPTIONS }, diags }; } const parsed = host.parseConfigFile(configFile); const adopted: Record = {}; for (const key of ADOPTED_OPTIONS) { const value = parsed.options[key]; if (value !== undefined) adopted[key] = value; } if (projectAllowsSyntheticDefaultImports(parsed.options)) { adopted["allowSyntheticDefaultImports"] = true; } // TS 7 accepts `paths`, but no longer accepts `baseUrl` as a compiler // option. Translate the established baseUrl+paths spelling into absolute // targets: the synthesized config lives beside the entry rather than the // real config, so preserving relative targets would change their meaning // without a diagnostic. The absolute map is also the own resolver's // configuration. const rawPaths = parsed.options["paths"]; if (rawPaths !== undefined && typeof rawPaths === "object" && rawPaths !== null) { const configDir = dirname(configFile); const rawBaseUrl = parsed.options["baseUrl"]; const base = typeof rawBaseUrl === "string" ? isAbsolute(rawBaseUrl) ? rawBaseUrl : resolve(configDir, rawBaseUrl) : configDir; const paths: Record = {}; for (const [key, targets] of Object.entries(rawPaths as Record)) { if (!Array.isArray(targets)) continue; paths[key] = targets .filter((target): target is string => typeof target === "string") .map((target) => (isAbsolute(target) ? target : resolve(base, target))); } adopted["paths"] = paths; } const nullChecks = adopted["strictNullChecks"] ?? adopted["strict"] ?? false; const forcedStrictNullChecks = nullChecks !== true; if (forcedStrictNullChecks) { diags.push(strictNullChecksFloorDiag(configFile)); adopted["strictNullChecks"] = true; } // The project's own library selection, kept only to explain errors: the // checker always uses scriptc's fixed library set (FORCED_OPTIONS). const rawLib = parsed.options["lib"]; const projectLib = Array.isArray(rawLib) ? rawLib.filter((entry): entry is string => typeof entry === "string") : undefined; return { configFile, options: { ...BASE_OPTIONS, ...adopted, ...FORCED_OPTIONS }, diags, ...(projectLib === undefined ? {} : { projectLib }), forcedStrictNullChecks, checkJs: parsed.options["checkJs"] === true, }; } /** The target project's @types/node, resolved with the OWN resolver's * type-directive lookup anchored at the ENTRY (typeRoots-free — exactly the * secondary lookup 5.9.3 ran with typeRoots emptied). */ function resolveNodeTypes7(entryPath: string): string | null { const file = resolveTypeDirective("node", entryPath); return file !== null && isNodeTypesPath(file) ? file : null; } export interface TypeWorldDifferences { /** No @types/node: the bundled fallback declarations type Node. */ nodeFallback: boolean; /** The project tsconfig's "lib" entries (scriptc checks with its own). */ projectLib: readonly string[] | null; /** The project disables strictNullChecks; scriptc forces it on. */ forcedStrictNullChecks: boolean; /** The project's tsconfig sets `checkJs` (scriptc's own checker always * reads JavaScript files; this records whether the project's does). */ projectCheckJs: boolean; } export interface LoadResult { program: ts.Program; entry: ts.SourceFile; /** User source files in Node's evaluation order: depth-first postorder * of the import graph from the entry, each module once. The entry is * last. Empty until checkPreflight runs (and empty when preflight fails * structurally). */ moduleOrder: ts.SourceFile[]; /** Inline require() calls whose edge closes a require cycle. Node answers * them with the still-evaluating module's partial exports, which the * static export globals do not model; their lowering refuses them by name * at run time instead of refusing the whole program. Filled by * checkPreflight. */ inlineRequireCycleCalls?: ReadonlySet; /** Every program file's module edges as preflight resolved them, * require() edges included: the lowering orders the modules only a * literal import(), fork() or Worker reaches with them. Filled by * checkPreflight. */ moduleEdges?: ReadonlyMap | undefined; /** Node's startup refusal, when the graph carries an import edge Node * rejects before ANY module evaluates — a RESOLUTION refusal (a bare * specifier nothing installed resolves, an exports/imports map that * refuses the subpath or names a missing file, an invalid "#" * specifier: fetch/resolve phase, ERR_MODULE_NOT_FOUND and friends) or * the module-LINK SyntaxError (a named import of a CommonJS export * Node's lexer cannot detect: instantiate phase). Either way Node * refuses the whole program before evaluation, so the lowering compiles * the program to exactly that startup crash (buildMain); resolution * refusals win over the link SyntaxError because Node's fetch phase * runs first. Null when the graph resolves and links clean. Set by * checkPreflight. */ startupCrash?: StartupCrash | null; configDiags: ScrDiagnostic[]; /** SC6102 performance notes of the declaration-typed boundary * (decl-projection.ts): files whose projection was dropped. */ declBoundaryNotes?: ScrDiagnostic[]; /** The files whose JSX the program lowers (jsx-source.ts), by file name. * The program holds their rewritten text; positions in diagnostics refer * to it. */ jsxSources?: ReadonlyMap; /** With JSX sources: the program over the original text, whose TypeScript * errors are the program's, and its project world when it has errors. */ tscWorld?: { program: ts.Program; /** The program's pre-emit diagnostics, computed once. */ diagnostics: readonly ts.Diagnostic[]; projectWorld: ts.Program | null; /** The declaration-typed boundary rewrote user files: errors fall * back to the declaration world (load.projectWorld()), as without it. */ declWorldFallback?: true; }; /** How scriptc's type world differs from the project's own tsc setup — * used only to label the TypeScript errors those differences cause. */ typeWorld?: TypeWorldDifferences; /** Coverage-only external host modules: exact bare specifier → local * declaration file. These participate in checker resolution but never * become runtime module edges. */ externalTypes: ReadonlyMap; /** The mapped declarations plus their relative declaration-file closure, * keyed by normalized file name and attributed to every owning external * specifier. Multiple exact module names may deliberately share one * declaration surface. */ externalTypeSpecifiersByFile: ReadonlyMap; projectWorld: () => ts.Program; } function declarationReexportTarget(fromFile: string, specifier: string): string | null { const base = resolve(dirname(fromFile), specifier); const candidates: string[] = []; const extension = /\.(mjs|cjs|js)$/.exec(base)?.[1]; if (extension !== undefined) { const stem = base.slice(0, -(extension.length + 1)); if (extension === "mjs") candidates.push(`${stem}.d.mts`, `${stem}.d.ts`); else if (extension === "cjs") candidates.push(`${stem}.d.cts`, `${stem}.d.ts`); else candidates.push(`${stem}.d.ts`, `${stem}.d.mts`, `${stem}.d.cts`); } else { candidates.push(base, `${base}.d.ts`, `${base}.d.mts`, `${base}.d.cts`); } candidates.push( resolve(base, "index.d.ts"), resolve(base, "index.d.mts"), resolve(base, "index.d.cts"), ); return candidates.find((candidate) => trackedFileExists(candidate)) ?? null; } function runtimeReexportTarget(fromFile: string, specifier: string): string | null { const base = resolve(dirname(fromFile), specifier); const candidates = [ base, `${base}.js`, `${base}.mjs`, `${base}.cjs`, resolve(base, "index.js"), resolve(base, "index.mjs"), resolve(base, "index.cjs"), ]; const target = candidates.find( (candidate) => trackedFileExists(candidate) && isJsSourceFileName(candidate), ); return target === undefined ? null : (trackedRealpath(target) ?? target); } /** Visits an opted package's own declaration entry and relative barrel * closure. This is intentionally narrower than TypeScript module * resolution: bare edges name other packages and never inherit trust. */ function visitNpmStaticDeclarationClosure( services: FrontendServices, entryPath: string, onSource: (path: string, source: string) => void, ): void { const seen = new Set(); const packageJson = nearestPkgJsonPath(entryPath); const packageRoot = (packageJson === null ? dirname(entryPath) : dirname(packageJson)) .split("\\") .join("/"); const visit = (path: string): void => { path = resolve(path); const normalized = path.split("\\").join("/"); if (normalized !== packageRoot && !normalized.startsWith(packageRoot + "/")) return; if (seen.has(path)) return; seen.add(path); const source = trackedReadFile(path); if (source === null) return; onSource(path, source); for (const specifier of npmStaticDeclarationReexports(services.parse(path, source, "ts"))) { const target = declarationReexportTarget(path, specifier); if (target !== null) visit(target); } }; visit(entryPath); } function npmStaticDeclarationOverloadsOf( services: FrontendServices, entryPath: string, ): NpmStaticDeclarationOverloads { const classes = new Map>(); visitNpmStaticDeclarationClosure(services, entryPath, (path, source) => { for (const [className, methods] of parseNpmStaticDeclarationOverloads( services.parse(path, source, "ts"), )) { const target = classes.get(className) ?? new Map(); classes.set(className, target); for (const [methodName, signatures] of methods) { if (!target.has(methodName)) target.set(methodName, signatures); } } }); return classes; } function npmStaticDeclarationPropertiesOf( services: FrontendServices, entryPath: string, ): NpmStaticDeclarationProperties { const classes = new Map>(); visitNpmStaticDeclarationClosure(services, entryPath, (path, source) => { for (const [className, properties] of parseNpmStaticDeclarationProperties( services.parse(path, source, "ts"), )) { const target = classes.get(className) ?? new Map(); classes.set(className, target); for (const [propertyName, type] of properties) { if (!target.has(propertyName)) target.set(propertyName, type); } } }); return classes; } /** See LoadResult.startupCrash: Node's exact error message, the IR error * class that carries it (a RUNTIME_ERROR_CLASSES name — %Error for the * resolver's ERR_MODULE_NOT_FOUND family, %TypeError for invalid-specifier * refusals, %SyntaxError for either named-import link check), and the source * position of the refused edge. */ export interface StartupCrash { message: string; className: "%Error" | "%TypeError" | "%SyntaxError"; loc: { file: string; start: number; end: number }; } /** True when a coverage mapping names one exact bare host module. TypeScript * gives `paths` keys containing `*` pattern semantics, so accepting one here * would broaden the mapping beyond the specifier the caller named. */ export function isExactExternalTypeSpecifier(specifier: string): boolean { return ( specifier !== "" && !specifier.startsWith(".") && !specifier.startsWith("/") && !specifier.startsWith("#") && !specifier.startsWith("node:") && !specifier.includes("*") ); } function externalHostModuleDiag7(specifier: string, node: ts.Node): ScrDiagnostic { return unsupportedDiag( "SC1010", locOf7(node), `the '${specifier}' external host module (types supplied by --external-types, but no runtime implementation or scriptc lowering was provided)`, "the declaration mapping is analysis-only: coverage continues through project code, while executing this module requires an embedder integration with explicit runtime semantics", ); } /** Expand each mapped declaration entry through RELATIVE declaration * dependencies. Declaration packages commonly expose a barrel index.d.ts; * the structural types declared in its sibling files are part of the same * checker-only host surface. Bare imports deliberately do not inherit this * trust: they name a separate package/surface and need their own mapping. */ function externalTypeFileClosure7( program: ts.Program, externalTypes: ReadonlyMap, ): ReadonlyMap { const ownersByFile = new Map>(); const addOwner = (file: string, specifier: string): boolean => { let owners = ownersByFile.get(file); if (owners === undefined) { owners = new Set(); ownersByFile.set(file, owners); } if (owners.has(specifier)) return false; owners.add(specifier); return true; }; for (const [specifier, file] of externalTypes) { addOwner(tsgoPath(resolve(file)), specifier); } const queue: { sf: ts.SourceFile; owner: string }[] = []; for (const [owner, entry] of externalTypes) { const file = tsgoPath(resolve(entry)); const sf = program.getSourceFile(file); if (sf?.isDeclarationFile) queue.push({ sf, owner }); } const visited = new Set(); while (queue.length > 0) { const { sf, owner } = queue.shift()!; const file = tsgoPath(resolve(sf.fileName)); const visitKey = `${file}\0${owner}`; if (visited.has(visitKey)) continue; visited.add(visitKey); const admit = (dep: ts.SourceFile | null): void => { if (dep === null || !dep.isDeclarationFile) return; const depFile = tsgoPath(resolve(dep.fileName)); if (!addOwner(depFile, owner)) return; queue.push({ sf: dep, owner }); }; // SourceFile.imports includes import/export declarations, import = // require(), and import("…") type nodes. Only relative edges inherit // the mapped entry's ownership. for (const node of sf.imports) { if (ts.isStringLiteralLike(node) && isRelativeSpecifier(node.text)) { admit(resolveImport7(program, sf, node.text)); } } // Triple-slash path references are the other declaration-local edge. for (const ref of sf.referencedFiles) { const depPath = tsgoPath(resolve(dirname(sf.fileName), ref.fileName)); admit(program.getSourceFile(depPath) ?? null); } } return new Map([...ownersByFile].map(([file, owners]) => [file, [...owners]] as const)); } /** Program roots hidden behind canonical createRequire bindings. tsgo * discovers ordinary import/require edges itself, but a source binding * returned by createRequire is intentionally just a function to the type * checker. Rebuild the native program with each literal project target as * an explicit root, to a fixpoint, so the same TS 7 AST/checker world owns * their exports and transitive graph. */ function createRequireProgramRoots7(program: ts.Program): string[] { const known = new Set(program.getSourceFileNames().map((name) => tsgoPath(resolve(name)))); const roots = new Set(); for (const sf of program.getImplementationSourceFiles()) { if (sf.isDeclarationFile || sf.fileName.endsWith(".json")) continue; const calls: { callee: ts.Identifier; spec: string }[] = []; for (const node of moduleSourceCandidates(program, sf).calls) { if ( node.questionDotToken !== undefined || node.arguments.nodes.length !== 1 || !ts.isStringLiteralLike(node.arguments.nodes[0]!) || !ts.isIdentifier(node.expression) ) { continue; } calls.push({ callee: node.expression, spec: node.arguments.nodes[0]!.text }); } // Root discovery only needs the callee's binding. A first-miss lookup // otherwise fetches symbols and types for every identifier in the file, // including all unreachable bodies in large published packages. program.getTypeChecker().prefetchSymbolNodesExact(calls.map((call) => call.callee)); for (const { callee, spec } of calls) { if (!isCreateRequireBinding7(program, callee)) continue; if (canonicalBuiltinModule(spec) !== null) continue; let target = resolveProjectModule(sf.fileName, spec); if (target === null && !spec.startsWith("#")) { const npm = resolveNpmImport7(sf.fileName, spec, "require"); if ( npm !== null && isNpmStaticPackage(npm.packageName) && isJsSourceFileName(npm.typesFile) ) { target = npm.typesFile; } } // A directory's package.json may point at a native addon. Keep it // out of TypeScript's source roots so the require call can report // the native-addon boundary instead of an unsupported-file error. if (target === null || target.endsWith(".json") || target.endsWith(".node")) continue; const normalized = tsgoPath(resolve(target)); if (!known.has(normalized)) roots.add(normalized); } } return [...roots].sort(); } /** Classify the explicitly selected installed package's own sources. */ export function entryPackageFilePredicate(entryPath: string): (file: string) => boolean { const entryFile = tsgoPath(entryPath); const installedAt = entryFile.lastIndexOf("/node_modules/"); const prefix = installedAt < 0 ? null : entryFile.slice(0, installedAt) + "/node_modules/" + npmPackageNameOf(entryFile) + "/"; return (file) => { const normalized = tsgoPath(file); return ( normalized === entryFile || (prefix !== null && normalized.startsWith(prefix) && !normalized.slice(prefix.length).split("/").includes("node_modules")) ); }; } /** maxNodeModuleJsDepth can omit relative imports within an admitted * package, and opted-in packages deep in the dependency graph. Discover * their runtime sources to a fixpoint so the checker and module resolver * see the same graph regardless of import traversal order or depth. */ function entryPackageProgramRoots7( program: ts.Program, entryPath: string, rootNpmStaticEdges: boolean, ): string[] { const belongsToEntry = entryPackageFilePredicate(entryPath); const roots = new Set(); for (const sf of program.getImplementationSourceFiles()) { if ( sf.isDeclarationFile || (!belongsToEntry(sf.fileName) && npmStaticPackageOfPath(sf.fileName) === null) ) continue; for (const specifier of sf.imports) { if (!ts.isStringLiteralLike(specifier)) continue; const target = resolveProjectModule(sf.fileName, specifier.text); if ( target !== null && (belongsToEntry(target) || npmStaticPackageOfPath(target) !== null) && isJsSourceFileName(target) && program.getSourceFile(target) === undefined ) roots.add(target); // An opted-in package's bare edge counts against the same depth // budget: tsgo charges every node_modules JS hop, so express's // body-parser → qs → side-channel → get-intrinsic chain runs out of // depth before its leaves load. Root the runtime file this edge // lowers to (preflight's own resolution and miss rule) so the leaf // joins like a shallow one; complete programs add nothing. Only // the shadowed lowering world does this (the authored world checks // user code against declarations), and only from package files: a // program file's own edges are never deeper than one hop. // (resolveBareModule directly: resolveNpmImport7's workspace // registration stays with preflight's own walk.) if ( target === null && rootNpmStaticEdges && npmStaticPackageOfPath(sf.fileName) !== null && !isRelativeSpecifier(specifier.text) ) { const spec = specifier.text; if (spec.startsWith("#") || canonicalBuiltinModule(spec) !== null) continue; const npm = resolveBareModule( sf.fileName, spec, undefined, moduleSpecifierKind7(specifier), ); if ( npm !== null && isNpmStaticPackage(npm.packageName) && (isNodeModulesPath(npm.typesFile) || npm.workspaceDir !== undefined) && isJsSourceFileName(npm.typesFile) && npmStaticProgramFile(program, npm.typesFile) === null ) roots.add(tsgoPath(resolve(npm.typesFile))); } } } return [...roots].sort(); } /** TS 7's ESNext temporal declarations: a lib slice scriptc adds only for * programs whose sources name the global (programNamesTemporal7). */ const TEMPORAL_LIB = "lib.esnext.temporal.d.ts"; /** True when a program source (user code or compiled npm JavaScript) * contains the temporal global's name as a whole word. A cheap syntactic * scan of the implementation files root discovery already materialized; a * mention in a comment or string also counts, which only adds the lib * slice. */ function programNamesTemporal7(program: ts.Program): boolean { for (const sf of program.getImplementationSourceFiles()) { const text = sf.text; for ( let at = text.indexOf(TEMPORAL_GLOBAL); at >= 0; at = text.indexOf(TEMPORAL_GLOBAL, at + 8) ) { if (!isIdentifierCharAt(text, at - 1) && !isIdentifierCharAt(text, at + 8)) return true; } } return false; } function isIdentifierCharAt(text: string, index: number): boolean { if (index < 0 || index >= text.length) return false; const c = text.charCodeAt(index); return ( (c >= 48 && c <= 57) || (c >= 65 && c <= 90) || (c >= 97 && c <= 122) || c === 36 || c === 95 || c > 127 ); } /** "require" for a CommonJS edge (`require("x")`, `import x = require("x")`), * "import" for every ESM form — the resolution kind preflight uses. */ function moduleSpecifierKind7(specifier: ts.StringLiteralLike): "import" | "require" { const parent = specifier.parent; if (ts.isExternalModuleReference(parent)) return "require"; return ts.isCallExpression(parent) && ts.isIdentifier(parent.expression) && parent.expression.text === "require" ? "require" : "import"; } /** The program's files with JSX, lowered with the project's jsx settings * and each file's pragmas, or the refusal of a setting that emits no calls. * TypeScript files (.ts, .mts, .cts) cannot hold JSX; without a jsx setting * the checker already rejects any JSX (TS17004). */ function lowerProgramJsx7( program: ts.Program, options: ts.Ts7CompilerOptions, ): { sources: Map; refusal: ScrDiagnostic | null } { const sources = new Map(); const mode = jsxEmitName(options.jsx as string | number | undefined); if (mode === undefined) return { sources, refusal: null }; for (const sf of program.getImplementationSourceFiles()) { if (sf.isDeclarationFile || /\.[mc]?ts$/.test(sf.fileName) || !containsJsx(sf)) continue; const at = firstJsx(sf)!; const loc = { file: sf.fileName, start: ts.tokenStartOfNode(at), end: at.end }; if (mode === "preserve" || mode === "react-native") { return { sources, refusal: { code: "SC1090", message: `JSX with "jsx": "${mode}" is not supported: that setting keeps JSX in the output instead of lowering it to calls`, loc, hint: 'set "jsx" to "react", "react-jsx" or "react-jsxdev" in the project tsconfig', }, }; } const plan = jsxEmitPlan( { jsx: mode, jsxFactory: options.jsxFactory as string | undefined, jsxFragmentFactory: options.jsxFragmentFactory as string | undefined, jsxImportSource: options.jsxImportSource as string | undefined, reactNamespace: options.reactNamespace as string | undefined, }, jsxPragmas(sf.text), ); sources.set(sf.fileName, lowerJsx(sf, plan)); } return { sources, refusal: null }; } function loadProgram7( host: ts.Ts7Host, entryPath: string, externalTypes: ReadonlyMap = new Map(), /** The --npm-static lowering world: root opted-in packages' deep edges. */ rootNpmStaticEdges = false, /** The declaration-typed boundary's projection (decl-projection.ts): * answers rewritten user files for the lowering world, or null. */ projector?: (program: ts.Program) => DeclProjection | null, ): LoadResult & { disposeAll: () => void } { const config = adoptProjectConfig7(host, entryPath); const configuredPaths = config.options["paths"]; setProjectPathMappings( configuredPaths !== undefined && typeof configuredPaths === "object" && configuredPaths !== null ? (configuredPaths as Record) : null, ); const nodeTypes = config.configFile ? resolveNodeTypes7(entryPath) : null; // skipLibCheck is FORCED with @types/node in the program: checking a // third-party lib's internals against OUR lib choice (es2025, no dyn) is // not scriptc's fence and drowns real diagnostics in hundreds of // .d.ts-internal errors. Fence discipline never depended on it: the // lowerer checks provenance and forms at every use site. let options: ts.Ts7CompilerOptions = nodeTypes ? { ...config.options, skipLibCheck: true } : { ...config.options }; // --npm-static: opted-in packages' shipped JS must be TYPE-INCLUDED (not // just resolved) — without maxNodeModuleJsDepth, node_modules JS types as // an implicit-any module (TS7016) and nothing infers. Only flagged // compiles pay this; flagless builds keep the exact historical options. // Eight levels retain package-internal re-export chains such as Effect's; // the former four-level cap left valid bindings outside the checker graph. if (npmStaticActive()) options.maxNodeModuleJsDepth = 8; // --provenance-sources: the registered entries become tsconfig "paths" // so tsgo's OWN resolution of the bare specifiers lands on the same // source files the preflight resolver answers — the checker types the // driver against the package's real TypeScript, not its shipped .d.ts. const provenance = provenancePaths(); if (provenance !== null || externalTypes.size > 0) { const paths: Record = { ...(configuredPaths as Record | undefined), // Keep an index-signature source for native lowering when provenance is absent. // oxlint-disable-next-line unicorn/no-useless-fallback-in-spread ...(provenance ?? {}), }; for (const [specifier, declarationPath] of externalTypes) { paths[specifier] = [declarationPath]; } options = { ...options, paths }; } const coreRoots = [entryPath, ambientDtsPath(), nodeTypes ?? fallbackDtsPath()]; const programRoots = [...coreRoots]; let program = host.createProgram([...programRoots, overridesDtsPath()], options); try { for (let pass = 0; pass < 32; pass++) { const candidates = [ ...entryPackageProgramRoots7(program, entryPath, rootNpmStaticEdges), ...createRequireProgramRoots7(program), ...forkTargetPaths(program, program.getImplementationSourceFiles()), ...workerTargetPaths(program, program.getImplementationSourceFiles()), ]; const extraRoots = candidates.filter( (root, index) => !programRoots.includes(root) && candidates.indexOf(root) === index, ); if (extraRoots.length === 0) break; program.dispose(); programRoots.push(...extraRoots); program = host.createProgram([...programRoots, overridesDtsPath()], options); if (pass === 31) throw new Error("static program-root discovery did not converge"); } // Node 24 ships no temporal global, but TS 7 types it in an ESNext // lib slice. Programs that name it (feature probes such as smol-toml's // typeof check) get that slice so they type-check, and lower to Node's // answers (typeof "undefined", ReferenceError on use). Every other // program keeps the identical lib set: the slice would add globals to // the checker's world for everyone. if (programNamesTemporal7(program)) { program.dispose(); options = { ...options, lib: [...((options.lib as string[] | undefined) ?? []), TEMPORAL_LIB], }; program = host.createProgram([...programRoots, overridesDtsPath()], options); } // JSX: the program above checks the files as written; the program the // lowering compiles holds them with their JSX lowered. const jsx = lowerProgramJsx7(program, options); let tscWorld: LoadResult["tscWorld"]; const declBoundaryNotes: ScrDiagnostic[] = []; if (jsx.refusal !== null) { config.diags.push(jsx.refusal); } else if (jsx.sources.size > 0) { const original = program; // The project world must read the original text too, so it is built // before the rewritten files are served, and only when it is needed. const diagnostics = ts.getPreEmitDiagnostics(original); const errors = diagnostics.some((d) => d.category === ts.DiagnosticCategory.Error); tscWorld = { program: original, diagnostics, projectWorld: errors ? host.createProgram(programRoots, options) : null, }; for (const [file, lowered] of jsx.sources) host.addVirtualFile(file, lowered.text); program = host.createProgram([...programRoots, overridesDtsPath()], options); } else if (projector !== undefined) { // The declaration-typed boundary: user files typed from package // declarations (decl-projection.ts). The tsc gate reads the original // text; the lowering compiles the rewritten files. const projection = projector(program); if (projection !== null) { const original = program; const baseline = new Map(); for (const file of projection.files.keys()) { const sf = original.getSourceFile(file); if (sf !== undefined) baseline.set(file, original.getSemanticDiagnostics(sf).length); } for (const [file, text] of projection.files) host.addVirtualFile(file, text); host.addVirtualFile(projection.declFile, projection.declText); programRoots.push(projection.declFile); program = host.createProgram([...programRoots, overridesDtsPath()], options); // A rewrite the lowering world disagrees with (new errors) is // dropped for that file: the file lowers as it would without it. const dropped: string[] = []; for (const file of projection.files.keys()) { const sf = program.getSourceFile(file); if (sf === undefined) continue; if (program.getSemanticDiagnostics(sf).length > (baseline.get(file) ?? 0)) { dropped.push(file); host.addVirtualFile(file, original.getSourceFile(file)!.text); } } if (dropped.length > 0) { program.dispose(); program = host.createProgram([...programRoots, overridesDtsPath()], options); for (const file of dropped) declBoundaryNotes.push(declBoundaryDroppedDiag(file)); } if (dropped.length === projection.files.size) { program.dispose(); programRoots.pop(); program = original; } else { tscWorld = { program: original, diagnostics: ts.getPreEmitDiagnostics(original), projectWorld: null, declWorldFallback: true, }; } } } const entry = program.getSourceFile(entryPath); if (!entry) throw new Error(`could not load ${entryPath}`); const externalTypeSpecifiersByFile = externalTypeFileClosure7(program, externalTypes); let projectWorld: ts.Program | null = null; return { program, entry, moduleOrder: [], configDiags: config.diags, ...(declBoundaryNotes.length > 0 ? { declBoundaryNotes } : {}), typeWorld: { nodeFallback: nodeTypes === null, projectLib: config.projectLib ?? null, forcedStrictNullChecks: config.forcedStrictNullChecks ?? false, projectCheckJs: config.checkJs ?? false, }, externalTypes, externalTypeSpecifiersByFile, ...(tscWorld === undefined ? {} : { jsxSources: jsx.sources }), ...(tscWorld === undefined ? {} : { tscWorld }), projectWorld: () => (projectWorld ??= host.createProgram(programRoots, options)), disposeAll: () => { try { projectWorld?.dispose(); tscWorld?.projectWorld?.dispose(); tscWorld?.program.dispose(); } finally { program.dispose(); } }, }; } catch (error) { program.dispose(); throw error; } } /** The pipeline surface: ONE tsgo program serves preflight AND the * lowering. The caller runs checkPreflight, hands program/entry/ * moduleOrder straight to lowerToIr, and MUST dispose() when done — the * spawned tsgo server must not outlive the compile. */ export interface ProgramLoadOptions { /** Package names whose shipped JavaScript is compiled as program code. */ npmStatic?: readonly string[]; /** Exact host module specifiers paired with their declaration paths. */ externalTypes?: readonly (readonly [string, string])[]; } export function loadProgram( entryPath: string, services: FrontendServices, opts?: ProgramLoadOptions, ): LoadResult & { services: FrontendServices; dispose: () => void } { entryPath = resolve(entryPath); const externalTypeEntries = opts?.externalTypes ?? []; const externalTypes = new Map(); for (const [specifier, file] of externalTypeEntries) { if (!isExactExternalTypeSpecifier(specifier)) { throw new TypeError( `invalid external type specifier ${JSON.stringify(specifier)} (expected an exact bare package specifier)`, ); } if (!/\.d\.(?:ts|mts|cts)$/.test(file)) { throw new TypeError( `invalid external type declaration ${JSON.stringify(file)} for ${JSON.stringify(specifier)} (expected a .d.ts, .d.mts, or .d.cts file)`, ); } const declarationPath = resolve(file); if (!trackedFileExists(declarationPath)) { throw new TypeError( `external type declaration for ${JSON.stringify(specifier)} does not name a readable file: ${declarationPath}`, ); } externalTypes.set(specifier, declarationPath); } // Packages scriptc implements itself or the project marks never loaded // (package-shims.ts) never compile their published JavaScript, even when // named. configurePackageShims(entryPath); const npmStaticPackages = [...new Set(opts?.npmStatic ?? [])].filter( (name) => !isPackageShimName(name), ); // Resolve authored declaration entries before enabling the JS-only // package resolver. Safe overload metadata binds through the runtime // entry to one exact implementation file; it never contributes values // or executable module edges. const declarationOverloads = new Map(); const declarationProperties = new Map(); const projectionOwners = new Map(); for (const pkg of npmStaticPackages) { const resolved = resolveBareModule(entryPath, pkg, "types-only"); if ( resolved === null || resolved.packageName !== pkg || !/\.d\.(?:ts|mts|cts)$/.test(resolved.typesFile) ) { continue; } const overloads = npmStaticDeclarationOverloadsOf(services, resolved.typesFile); const properties = npmStaticDeclarationPropertiesOf(services, resolved.typesFile); const classNames = new Set([...overloads.keys(), ...properties.keys()]); if (classNames.size === 0) continue; const runtime = resolveBareModule(entryPath, pkg, "js-only"); if (runtime === null || !isJsSourceFileName(runtime.typesFile)) continue; const runtimeSource = trackedReadFile(runtime.typesFile); if (runtimeSource === null) continue; const targets = npmStaticRuntimeClassTargets( services.parse(runtime.typesFile, runtimeSource, "js"), runtimeSource, classNames, ); for (const [className, { specifier, localName }] of targets) { const methods = overloads.get(className); const fields = properties.get(className); if (methods === undefined && fields === undefined) continue; const target = specifier === null ? runtime.typesFile : runtimeReexportTarget(runtime.typesFile, specifier); if (target === null) continue; const targetPackage = npmPackageNameOf(target); const targetNorm = target.split("\\").join("/"); const insideWorkspace = runtime.workspaceDir !== undefined && targetNorm.startsWith(runtime.workspaceDir.split("\\").join("/") + "/"); if (targetPackage !== pkg && !insideWorkspace) continue; const projectionKey = JSON.stringify([targetNorm, localName]); const previousOwner = projectionOwners.get(projectionKey); if (previousOwner !== undefined && previousOwner !== className) { // Two public class declarations naming one runtime class are // ambiguous. Keep inference instead of choosing one alias's ABI. projectionOwners.set(projectionKey, null); const priorMethods = new Map(declarationOverloads.get(targetNorm) ?? []); priorMethods.delete(localName); declarationOverloads.set(targetNorm, priorMethods); const priorFields = new Map(declarationProperties.get(targetNorm) ?? []); priorFields.delete(localName); declarationProperties.set(targetNorm, priorFields); continue; } projectionOwners.set(projectionKey, className); // The safe declaration grammar can name only its own class. Bind // that type to the actual runtime class after a named ESM alias. const renameSelf = (type: string): string => type .split(/([\s[\]<>()|,]+)/) .map((name) => (name === className ? localName : name)) .join(""); if (methods !== undefined) { const byClass = new Map(declarationOverloads.get(targetNorm) ?? []); byClass.set( localName, new Map( [...methods].map(([name, signatures]) => [ name, signatures.map((signature) => ({ parameters: signature.parameters.map((parameter) => ({ name: parameter.name, type: renameSelf(parameter.type), optional: parameter.optional, })), returnType: renameSelf(signature.returnType), })), ]), ), ); declarationOverloads.set(targetNorm, byClass); } if (fields !== undefined) { const byClass = new Map(declarationProperties.get(targetNorm) ?? []); byClass.set( localName, new Map([...fields].map(([name, type]) => [name, renameSelf(type)])), ); declarationProperties.set(targetNorm, byClass); } } } setNpmStaticPackages(npmStaticPackages); setNpmStaticDeclarationOverloads(declarationOverloads, declarationProperties); // Types only the packages' .d.ts declares keep their declarations // (npm-static-type-exports.ts). const typeExports = npmStaticTypeExportPlan(services, entryPath, npmStaticPackages); setNpmStaticTypeExports(typeExports.copies, typeExports.typedefs); // Workspace-package registrations reset per load (same discipline as the // npm-static set), then the opted-in names are probed UP FRONT: a // workspace-linked opt-in resolves to files whose realpaths carry no // node_modules segment, and the tsgo host's fs shadow must already // recognize those directories while the PROGRAM is created (hiding the // package's declaration twins along its internal relative edges). // Non-opted workspace packages register lazily as preflight's own // resolver discovers their import edges. clearWorkspacePackages(); for (const pkg of opts?.npmStatic ?? []) { const probe = resolveBareModule(entryPath, pkg); if (probe?.workspaceDir !== undefined) registerWorkspacePackage(probe.packageName, probe.workspaceDir); } // Declaration-twin hiding is scoped to the entry's own package realm // (see projectDtsRuntimeSibling) — set before the host exists so the // program never sees an out-of-realm twin hidden. setProjectRealm(entryPath); // Two fs shadows compose: --npm-static's per-package hiding, and the // always-on project declaration-TWIN hiding (a .d.ts beside runtime JS // outside node_modules — the classic typed-JS-library entry — must not exist for // the checker, so its resolution lands on the JS Node actually loads; // resolve.ts answers the same sibling for scriptc's own edges). const npmShadow = npmStaticFsShadow(services); clearProgramBuiltinAliasCache(); const fsShadow = { readFile: (path: string) => npmShadow?.readFile(path) ?? programBuiltinAliasSource(services, path), hideFile: (path: string) => (npmShadow?.hideFile(path) ?? false) || projectDtsRuntimeSibling(path) !== null, }; const host = services.createProgramHost({ cwd: dirname(entryPath), fsShadow }); let authoredHost: ts.Ts7Host | null = null; let authoredLoad: ReturnType | null = null; const authored = (): ReturnType => { if (!authoredLoad) { authoredHost = services.createProgramHost({ cwd: dirname(entryPath) }); authoredLoad = loadProgram7(authoredHost, entryPath, externalTypes); } return authoredLoad; }; const declPackages = npmStaticPackages.filter((pkg) => { const resolved = resolveBareModule(entryPath, pkg, "types-only"); return resolved !== null && /\.d\.(?:ts|mts|cts)$/.test(resolved.typesFile); }); const projector = declPackages.length === 0 ? undefined : (program: ts.Program): DeclProjection | null => { const userFiles = declBoundaryUserFiles(program, declPackages); if (userFiles.length === 0) return null; let projection: DeclProjection | null; try { projection = planDeclProjection({ jsProgram: program, declProgram: authored().program, userFiles, outDir: dirname(entryPath), }); } catch (error) { if (process.env["SCRIPTC_DEBUG_DECL_BOUNDARY"] === "1") throw error; return null; } if (process.env["SCRIPTC_DEBUG_DECL_BOUNDARY"] === "1") { process.stderr.write( `[decl-boundary] ${projection === null ? "no projection" : projection.plan.join("\n[decl-boundary] ")}\n`, ); if (projection !== null) process.stderr.write(projection.declText); } return projection; }; try { const load = loadProgram7( host, entryPath, externalTypes, npmStaticPackages.length > 0, projector, ); return { ...load, services, projectWorld: npmStaticPackages.length === 0 ? load.projectWorld : () => // Check authored code against the package's published declarations. // Runtime bodies still lower from JavaScript in the original world; // inference there must not manufacture errors in valid user code. authored().projectWorld(), dispose: () => { try { (authoredLoad as ReturnType | null)?.disposeAll(); } finally { (authoredHost as ts.Ts7Host | null)?.close(); try { load.disposeAll(); } finally { host.close(); } } }, }; } catch (error) { (authoredLoad as ReturnType | null)?.disposeAll(); (authoredHost as ts.Ts7Host | null)?.close(); host.close(); throw error; } } /** User TypeScript files (outside node_modules) that import one of the * packages whose declarations the boundary projects from. */ function declBoundaryUserFiles(program: ts.Program, packages: readonly string[]): string[] { const out: string[] = []; for (const sf of program.getSourceFiles()) { const name = sf.fileName.split("\\").join("/"); if (sf.isDeclarationFile || name.includes("/node_modules/") || !/\.[mc]?tsx?$/.test(name)) continue; if (name.endsWith(".tsx")) continue; // JSX files: not projected yet const text = sf.text; if ( packages.some( (pkg) => text.includes(`"${pkg}"`) || text.includes(`'${pkg}'`) || text.includes(`"${pkg}/`) || text.includes(`'${pkg}/`), ) ) out.push(sf.fileName); } return out; } /** SC6102 — a projected file the lowering world disagreed with. */ function declBoundaryDroppedDiag(file: string): ScrDiagnostic { return { code: "SC6102", message: "values from npm packages stay checked-dynamic in this file: the types projected from the packages' declarations do not type-check against their JavaScript", loc: { file, start: 0, end: 0 }, }; } /** tsc diagnostics (syntax + types), the supported-import fence, and the * module evaluation order: fills load.moduleOrder (the SourceFiles * themselves — the lowering consumes them directly) and returns the * preflight diagnostics. Preserve the caller's complete load shape when * mutating it, including its owned services and disposal callback. */ export function checkPreflight(load: T): ScrDiagnostic[] { const { diags, moduleOrder, startupCrash, inlineRequireCycleCalls, moduleEdges } = preflight7(load); load.moduleOrder = moduleOrder; load.startupCrash = startupCrash; load.inlineRequireCycleCalls = inlineRequireCycleCalls; load.moduleEdges = moduleEdges; return diags; } /* Finding 5: tsgo's strict CommonJS modeling fires checker codes on JS * shapes 5.9.3 accepted, and the real hazards those shapes carry are fenced * by scriptc's OWN analysis, never by these codes — so for JS FILES ONLY * they join the relaxation (TS sources keep the full gate; the order-parity * harness holds both lanes to identical verdicts, so a case where 5.9.3 * itself raises one of these on a JS file would fail there, not slip by): * - TS2309 ("export assignment with other exported elements") on the * table-then-member pattern — cjsExportDiscardReason names every export * Node actually discards. * - TS2323 ("cannot redeclare exported variable") on symbol-keyed expando * assignments in CJS classes (`this[kSym] = v` under `module.exports = * Class`) — 5.9.3 synthesized expando symbols, tsgo trips its * redeclaration check instead (the corpus's countdown fixture). */ // Ordinary JavaScript functions are constructable even when the checker // retains only their call signature after a factory return. Heritage // lowering separately verifies the actual constructor implementation. const TS7_JS_RELAXED_EXTRA: ReadonlySet = new Set([2309, 2323, 2507]); /** Relaxed JavaScript codes that checked JavaScript files keep: values of * another type in a typed slot (2322 and its elaborations 2740/2741/2559, * 2345, 2769), call arity (2554-2556) and incompatible subclass members * (2415). */ const CHECKED_JS_ENFORCED_CODES: ReadonlySet = new Set([ 2322, 2345, 2415, 2554, 2555, 2556, 2559, 2740, 2741, 2769, ]); function suppressedJsStrictness7(d: ts.Diagnostic): boolean { const file = d.fileName; if (!file || !isJsSourceFileName(file)) return false; if (JS_RELAXED_TSC_CODES.has(d.code) || TS7_JS_RELAXED_EXTRA.has(d.code)) return true; // The 8xxx range is TypeScript's JSDoc-specific diagnostic family // (malformed tags, signature/tag mismatches): comment semantics with no // runtime meaning in a JS file. if (d.code >= 8000 && d.code <= 8999) return true; if (JS_ANY_OPERATOR_CODES.has(d.code)) { return ts.flattenDiagnosticMessageText(d, "\n").includes("'any'"); } // The deprecated Buffer(...) call without `new`, which Node's Buffer // function accepts (containers/bytes.ts lowers it). if (d.code === 2348) { return ts.flattenDiagnosticMessageText(d, "\n").includes("'BufferConstructor'"); } return false; } /** The specifier of a `require("...")` call: the callee is the bare * identifier `require` and the single argument a string literal. The one * recognizer preflight edges, global collection, and statement lowering * share — anything require-shaped it does NOT match (computed specifiers, * extra arguments) is not a module edge and fences at its use site. */ function requireSpecOf7(node: ts.Node): string | null { let value = node; while ( ts.isParenthesizedExpression(value) || ts.isAsExpression(value) || ts.isTypeAssertion(value) || ts.isNonNullExpression(value) ) { value = value.expression; } if (!ts.isCallExpression(value)) return null; if (!ts.isIdentifier(value.expression) || value.expression.text !== "require") return null; if (value.arguments.nodes.length !== 1) return null; const arg = value.arguments.nodes[0]!; return ts.isStringLiteral(arg) ? arg.text : null; } /** True for statements that are CommonJS require IMPORTS at a module's top * level: `const x = require("s")` / `const { a, b } = require("s")` * (every declarator a require), and the bare side-effect `require("s");`. * These lower to NOTHING — the bindings are alias plumbing (tsc models * them as import aliases) and the module edge lives in the order walk. */ function isRequireStatement7(stmt: ts.Statement): boolean { if (ts.isExpressionStatement(stmt)) return requireSpecOf7(stmt.expression) !== null; if (!ts.isVariableStatement(stmt)) return false; const decls = stmt.declarationList.declarations.nodes; return ( decls.length > 0 && decls.every((d) => d.initializer !== undefined && requireSpecOf7(d.initializer) !== null) ); } /** A require() occurrence of a top-level statement. `split` marks the * assignment of a split require binding (splitRequireBindings7): `node` * is its require call and `decl` the binding's declaration. */ interface RequireOccurrence7 { spec: string; node: ts.Node; decl: ts.VariableDeclaration | null; split?: true; } /** The require() occurrences of one top-level statement: the declaration * forms, the bare side-effect call, and the assignment of a split require * binding. Empty for everything else. */ function requiresOf7(stmt: ts.Statement, program: ts.Program): RequireOccurrence7[] { if (ts.isExpressionStatement(stmt)) { const split = splitRequireOfStatement7(program, stmt); if (split !== null) return [ { spec: requireSpecOf7(split.call)!, node: split.call, decl: split.decl, split: true }, ]; const spec = requireSpecOf7(stmt.expression); return spec !== null ? [{ spec, node: stmt.expression, decl: null }] : []; } if (!ts.isVariableStatement(stmt)) return []; const out: RequireOccurrence7[] = []; for (const decl of stmt.declarationList.declarations.nodes) { const spec = decl.initializer !== undefined ? requireSpecOf7(decl.initializer) : null; if (spec !== null) out.push({ spec, node: decl, decl }); } return out; } /** CommonJS require bindings split into a declaration and one later * assignment (`var m; m = require("./x")`): a top-level `var` or `let` * declaration without initializer in a CommonJS file, assigned by exactly one * top-level `m = require("")` statement of the require global over * a compiled program module, and written nowhere else. The binding holds the * module's exports from the assignment on, as `const m = require("./x")` * does, so preflight treats the assignment as that require: its module edge, * its cycle admission and its early-read fence. The lowering's recognizer * (splitRequireBindingOf in cjs-inline-require.ts) accepts only these * bindings. Keyed by declaration, with the assignment's require call. */ function splitRequireBindings7( program: ts.Program, sf: ts.SourceFile, ): ReadonlyMap { const memo = program.analysis.splitRequireBindings; const cached = memo.get(sf); if (cached !== undefined) return cached; const out = new Map(); memo.set(sf, out); if (sf.isDeclarationFile || !isCjsJsFile7(sf, program) || !sf.text.includes("require")) return out; const checker = program.getTypeChecker(); const stmts = sf.statements.nodes; const candidates = new Map< ts.Symbol, { decl: ts.VariableDeclaration; call: ts.CallExpression | null; assigns: number } >(); for (const stmt of stmts) { if (!ts.isVariableStatement(stmt)) continue; if ((stmt.declarationList.flags & (ts.NodeFlags.Const | ts.NodeFlags.Using)) !== 0) continue; for (const decl of stmt.declarationList.declarations.nodes) { if (!ts.isIdentifier(decl.name) || decl.initializer !== undefined) continue; const symbol = checker.getSymbolAtLocation(decl.name); if (symbol === undefined || symbol.declarations.length !== 1) continue; candidates.set(symbol, { decl, call: null, assigns: 0 }); } } if (candidates.size === 0) return out; // The one top-level `m = ...` statement, whose right side is the require. for (const stmt of stmts) { if (!ts.isExpressionStatement(stmt) || !ts.isBinaryExpression(stmt.expression)) continue; const assign = stmt.expression; if (assign.operatorToken.kind !== ts.SyntaxKind.EqualsToken || !ts.isIdentifier(assign.left)) continue; const symbol = checker.getSymbolAtLocation(assign.left); const candidate = symbol !== undefined ? candidates.get(symbol) : undefined; if (candidate === undefined) continue; candidate.assigns++; let right = assign.right; while (ts.isParenthesizedExpression(right)) right = right.expression; if (ts.isCallExpression(right) && splitRequireTarget7(program, sf, right)) candidate.call = right; } const names = new Set(); for (const candidate of candidates.values()) { if (candidate.assigns === 1 && candidate.call !== null) names.add((candidate.decl.name as ts.Identifier).text); } if (names.size === 0) return out; // Exactly one write in the whole file: the assignment itself. const writes = new Map(); ts.walkPreorder(sf, (node) => { if (!ts.isIdentifier(node) || !names.has(node.text)) return undefined; const parent = node.parent; const symbol = parent !== undefined && ts.isShorthandPropertyAssignment(parent) && parent.name === node ? (checker.getShorthandAssignmentValueSymbol(parent) ?? checker.getSymbolAtLocation(node)) : checker.getSymbolAtLocation(node); if (symbol === undefined || !candidates.has(symbol) || !isWriteReference7(node)) return undefined; writes.set(symbol, (writes.get(symbol) ?? 0) + 1); return undefined; }); for (const [symbol, candidate] of candidates) { if (candidate.assigns === 1 && candidate.call !== null && writes.get(symbol) === 1) out.set(candidate.decl, candidate.call); } return out; } /** True when `call` is a require of the CommonJS global over a compiled * program module: not a builtin, a JSON document or a declaration file. */ function splitRequireTarget7( program: ts.Program, sf: ts.SourceFile, call: ts.CallExpression, ): boolean { const spec = requireSpecOf7(call); if (spec === null || canonicalBuiltinModule(spec) !== null) return false; if (requireCallBindingKind7(program, call) !== "ambient") return false; const dep = resolveImport7(program, sf, spec, "require") ?? npmStaticDepSf7(program, sf, spec, "require"); return dep !== null && !dep.isDeclarationFile && !dep.fileName.endsWith(".json"); } /** The split require binding a top-level statement assigns, or null. */ function splitRequireOfStatement7( program: ts.Program, stmt: ts.Statement, ): { decl: ts.VariableDeclaration; call: ts.CallExpression } | null { if ( !ts.isExpressionStatement(stmt) || !ts.isBinaryExpression(stmt.expression) || !ts.isSourceFile(stmt.parent) ) return null; let right = stmt.expression.right; while (ts.isParenthesizedExpression(right)) right = right.expression; for (const [decl, call] of splitRequireBindings7(program, stmt.parent)) if (call === right) return { decl, call }; return null; } /** The top-level statement of a split require binding's assignment, from * its require call (`m = require("./x")` or `m = (require("./x"))`). */ function splitRequireStatementOf7(call: ts.CallExpression): ts.Node | undefined { let node: ts.Node = call; while (node.parent !== undefined && ts.isParenthesizedExpression(node.parent)) node = node.parent; return node.parent?.parent; } /** The require call of a split require binding's declaration, or null. */ function splitRequireCallOf7( program: ts.Program, decl: ts.VariableDeclaration, ): ts.CallExpression | null { return splitRequireBindings7(program, ts.getSourceFileOfNode(decl)).get(decl) ?? null; } /** True when `id` is written: an assignment target (compound and * destructuring included), an update operand, or a for-in/of target. */ function isWriteReference7(id: ts.Identifier): boolean { let n: ts.Node = id; for (;;) { const p: ts.Node | undefined = n.parent; if (p === undefined) return false; if (ts.isBinaryExpression(p)) { const k = p.operatorToken.kind; return ( p.left === n && k >= ts.SyntaxKind.FirstAssignment && k <= ts.SyntaxKind.LastAssignment ); } if (ts.isPostfixUnaryExpression(p) || ts.isPrefixUnaryExpression(p)) { return ( (p.operator === ts.SyntaxKind.PlusPlusToken || p.operator === ts.SyntaxKind.MinusMinusToken) && p.operand === n ); } if (ts.isForOfStatement(p) || ts.isForInStatement(p)) return p.initializer === n; if ( ts.isParenthesizedExpression(p) || ts.isArrayLiteralExpression(p) || ts.isSpreadElement(p) || ts.isSpreadAssignment(p) || ts.isShorthandPropertyAssignment(p) || ts.isObjectLiteralExpression(p) || (ts.isPropertyAssignment(p) && p.initializer === n) ) { n = p; continue; } return false; } } /** `true`, or `!0`, its minified spelling. */ function isTrueLiteral(expr: ts.Expression): boolean { return ( expr.kind === ts.SyntaxKind.TrueKeyword || (ts.isPrefixUnaryExpression(expr) && expr.operator === ts.SyntaxKind.ExclamationToken && ts.isNumericLiteral(expr.operand) && expr.operand.text === "0") ); } /** The exact `Object.defineProperty(exports|module.exports, "__esModule", * { value: true })` interop stamp (shared by preflight and the no-op lowering); * `{ value: !0 }` is the same stamp. */ export function isEsModuleStamp(expr: ts.Expression): boolean { if (!ts.isCallExpression(expr) || expr.questionDotToken !== undefined) return false; const callee = expr.expression; if ( !ts.isPropertyAccessExpression(callee) || !ts.isIdentifier(callee.expression) || callee.expression.text !== "Object" || !ts.isIdentifier(callee.name) || callee.name.text !== "defineProperty" ) { return false; } if (expr.arguments.nodes.length !== 3) return false; const [recv, nameArg, desc] = expr.arguments.nodes as unknown as [ ts.Expression, ts.Expression, ts.Expression, ]; const isExports = (ts.isIdentifier(recv) && recv.text === "exports") || (ts.isPropertyAccessExpression(recv) && ts.isIdentifier(recv.expression) && recv.expression.text === "module" && ts.isIdentifier(recv.name) && recv.name.text === "exports"); if (!isExports) return false; if (!ts.isStringLiteral(nameArg) || nameArg.text !== "__esModule") return false; if (!ts.isObjectLiteralExpression(desc) || desc.properties.nodes.length !== 1) return false; const p = desc.properties.nodes[0]!; return ( ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === "value" && isTrueLiteral(p.initializer) ); } /** Plain CommonJS prologue writes only publish values; in particular, * publishing a hoisted function does not invoke its body. Later runnable * statements may call the export or mutate its receiver, and still need * the conservative TDZ scan over the whole prefix. */ function pureCjsExport7(program: ts.Program, stmt: ts.Statement): boolean { const cjs = cjsExportAssignmentOf(stmt); if (cjs === null) return false; const sf = ts.getSourceFileOfNode(stmt); const plainMember = (left: ts.Expression): boolean => { if (!ts.isPropertyAccessExpression(left) || left.questionDotToken) return false; // A replacement export can have setters or non-writable function // properties. Only the wrapper's original object gets this exception. if (sf.statements.nodes.some((s) => cjsExportAssignmentOf(s)?.kind === "table")) return false; if (left.name.text === "__proto__" || left.name.text === "__esModule") return false; let receiver = left.expression; if (isModuleExportsAccess(receiver)) { receiver = receiver.expression; return ts.isIdentifier(receiver) && !sourceBinding7(program, receiver); } return ( ts.isIdentifier(receiver) && receiver.text === "exports" && !sourceBinding7(program, receiver) ); }; if (cjs.kind === "member") { if (!plainMember(cjs.expr.left)) return false; } else { const receiver = (cjs.expr.left as ts.PropertyAccessExpression).expression as ts.Identifier; if (sourceBinding7(program, receiver)) return false; } let value = cjs.expr.right; // tsc emits chains such as exports.A = exports.B = void 0. while (ts.isBinaryExpression(value) && value.operatorToken.kind === ts.SyntaxKind.EqualsToken) { if (!plainMember(value.left)) return false; value = value.right; } if (ts.isVoidExpression(value) && ts.isNumericLiteral(value.expression)) return true; if ( ts.isStringLiteralLike(value) || ts.isNumericLiteral(value) || value.kind === ts.SyntaxKind.TrueKeyword || value.kind === ts.SyntaxKind.FalseKeyword || value.kind === ts.SyntaxKind.NullKeyword ) return true; if (!ts.isIdentifier(value)) return false; if (value.text === "undefined" && !sourceBinding7(program, value)) return true; const checker = program.getTypeChecker(); const symbol = checker.getSymbolAtLocation(value); return ( symbol !== undefined && symbol.declarations.some( (decl) => ts.isFunctionDeclaration(decl) && decl.parent === stmt.parent, ) ); } function sourceBinding7(program: ts.Program, ident: ts.Identifier): boolean { const checker = program.getTypeChecker(); const symbol = checker.getSymbolAtLocation(ident); return ( symbol !== undefined && symbol.declarations.some( (decl) => !ts.getSourceFileOfNode(decl).isDeclarationFile && !ts.isSourceFile(decl), ) ); } function purePrefixDecl7(decl: ts.VariableDeclaration): boolean { const init = decl.initializer; return ( init === undefined || requireSpecOf7(init) !== null || ts.isStringLiteralLike(init) || ts.isNumericLiteral(init) || init.kind === ts.SyntaxKind.TrueKeyword || init.kind === ts.SyntaxKind.FalseKeyword || init.kind === ts.SyntaxKind.NullKeyword ); } /** True for top-level statements that cannot run user code: directives, * empty statements, hoisted declarations, require statements themselves, * literal-initialized variables, and CommonJS function-export prologues. * A require preceded ONLY by these cannot observe its own bindings early; * require cycles are checked separately by the module graph fences. */ function purePrefixStmt7(program: ts.Program, s: ts.Statement): boolean { if (ts.isEmptyStatement(s) || ts.isFunctionDeclaration(s)) return true; if (ts.isExpressionStatement(s) && ts.isStringLiteral(s.expression)) return true; // directive if (isRequireStatement7(s) || splitRequireOfStatement7(program, s) !== null) return true; if (isCjsJsFile7(ts.getSourceFileOfNode(s), program)) { if (ts.isExpressionStatement(s) && isEsModuleStamp(s.expression)) { const call = s.expression as ts.CallExpression; const object = (call.expression as ts.PropertyAccessExpression).expression as ts.Identifier; let receiver = call.arguments.nodes[0]!; if (ts.isPropertyAccessExpression(receiver)) receiver = receiver.expression; if ( !sourceBinding7(program, object) && ts.isIdentifier(receiver) && !sourceBinding7(program, receiver) ) return true; } if (pureCjsExport7(program, s)) return true; } if (ts.isVariableStatement(s)) { return s.declarationList.declarations.nodes.every(purePrefixDecl7); } return false; } /** Can code that runs BEFORE the declaration at index `k` reach one of * its bindings? Static require aliases and symbol field keys bypass * storage reads, so an early use could erase a TDZ error or a var's * initial undefined value. Conservative reachability: earlier * statements' whole subtrees (arrows and function expressions included) * read the binding directly, or reference a hoisted function/class whose * body (transitively through other hoisted declarations) reads it — a * referenced function value may be invoked immediately by whatever takes * it. Asynchronously-scheduled callbacks run after the whole module body, * hence after the require: never an early read. Returns the first * reachable binding's name, or null when position is provably free. * (The CheckerFacade memoizes symbol queries, and 7's client dedupes * symbol identity by server handle, so the Map-keyed-by-Symbol discipline * carries over from the 5.9.3 original unchanged.) */ export function bindingEarlyUse7( program: ts.Program, sf: ts.SourceFile, k: number, decl: ts.VariableDeclaration, precedingDecls: readonly ts.VariableDeclaration[], ): string | null { const checker = program.getTypeChecker(); const bound: ts.Identifier[] = []; if (ts.isIdentifier(decl.name)) bound.push(decl.name); else if (ts.isObjectBindingPattern(decl.name)) { for (const el of decl.name.elements.nodes) if (el.name !== undefined && ts.isIdentifier(el.name)) bound.push(el.name); } const bindings = new Map(); for (const id of bound) { const sym = checker.getSymbolAtLocation(id); if (sym) bindings.set(sym, id.text); } if (bindings.size === 0) return null; const stmts = sf.statements.nodes; const hoisted = new Map(); for (const s of stmts) { if ((ts.isFunctionDeclaration(s) || ts.isClassDeclaration(s)) && s.name) { const sym = checker.getSymbolAtLocation(s.name); if (sym) hoisted.set(sym, s); } } let hit: string | null = null; const scanned = new Set(); const work: ts.Statement[] = []; const scan = (root: ts.Node): void => { // Iterative walk (walkPreorder): a statement's subtree can be // pathologically deep (the binderBinaryExpressionStress chains), and // this scan must survive it so the nesting fence can answer later. ts.walkPreorder(root, (n) => { if (ts.isIdentifier(n)) { const sym = ts.isShorthandPropertyAssignment(n.parent) ? checker.getShorthandAssignmentValueSymbol(n.parent) : checker.getSymbolAtLocation(n); if (sym) { const name = bindings.get(sym); if (name !== undefined) { hit = name; return "stop"; } const target = hoisted.get(sym); if (target && !scanned.has(target)) { scanned.add(target); work.push(target); } } } return undefined; }); }; // These are the exact roots the TDZ analysis scans eagerly. Their files // are phase-managed already, so warm their deferred identifiers as one // symbol-only batch instead of paying one IPC query per occurrence. const roots = [ ...stmts.slice(0, k).filter((stmt) => !ts.isFunctionDeclaration(stmt)), ...precedingDecls, ]; checker.prefetchSymbolRoots(roots); for (const root of roots) { if (hit !== null) break; scan(root); } while (hit === null && work.length > 0) { // A scanned reference can make a hoisted declaration's body reachable // to this analysis. Batch every currently discovered root before // retaining the historical LIFO traversal order. checker.prefetchSymbolRoots(work); scan(work.pop()!); } return hit; } /** The first reference to a top-level require declaration's bindings that * code running before the require completes can reach, or null when no * earlier code can run or reach one. Memoized per program: preflight * decides the fence and the lowering places the initialization checks * from the same answer. */ function requireEarlyReference7( program: ts.Program, decl: ts.VariableDeclaration, ): ts.Identifier | null { const memo = program.analysis.requireEarlyReferences; const cached = memo.get(decl); if (cached !== undefined) return cached; let found: ts.Identifier | null = null; const sf = ts.getSourceFileOfNode(decl); // A split require binding is bound at its assignment statement. const splitCall = splitRequireCallOf7(program, decl); const stmt = splitCall !== null ? splitRequireStatementOf7(splitCall) : decl.parent?.parent; if ( stmt !== undefined && (ts.isVariableStatement(stmt) || splitCall !== null) && ts.isSourceFile(stmt.parent) ) { const stmts = sf.statements.nodes; const k = stmts.indexOf(stmt as ts.Statement); const declarations = ts.isVariableStatement(stmt) ? stmt.declarationList.declarations.nodes : []; // Earlier declarators run too: `const x = read(), dep = require('./dep')`. const precedingDecls = declarations.slice(0, declarations.indexOf(decl)); let firstRunnable = program.analysis.requireFirstRunnable.get(sf); if (firstRunnable === undefined) { firstRunnable = stmts.findIndex((s) => !purePrefixStmt7(program, s)); program.analysis.requireFirstRunnable.set(sf, firstRunnable); } const prefixCanRun = (firstRunnable >= 0 && firstRunnable < k) || precedingDecls.some((d) => !purePrefixDecl7(d)); if (prefixCanRun) found = requireBindingEarlyUse7(program, sf, k, decl, precedingDecls); } memo.set(decl, found); return found; } /** The CommonJS twin of bindingEarlyUse7 for a top-level require binding: * the first reference to one of `decl`'s bindings that code running before * the require completes can reach, or null. It follows Node's evaluation * order instead of the conservative "every earlier statement" scan: * - hoisted functions and classes, and the function and class values a * var stored before the require, run only when reachable code names * them; a class statement runs only its heritage, computed keys, * decorators and static initializers; * - callbacks handed to timers, microtasks and promise reactions run after * the module body, hence after the require; * - publishing a value (`module.exports = f`, `exports.n = f`, * `Object.defineProperty(exports, "n", { get })`, `F.prototype.m = f`) * runs nothing. The published value becomes reachable only when * reachable code reads it back: an `exports.n` read reaches the values * published as `n`, any other read of the export object reaches all of * them, and naming `F` reaches what was stored on `F`. No other module * can read this module's exports before its body completes: require * cycles keep their SC1016 fence and a compiled module graph has no * reflective exports object. * Every other function-like met in reachable code counts as reachable * because it may be handed to a callee. */ export function requireBindingEarlyUse7( program: ts.Program, sf: ts.SourceFile, k: number, decl: ts.VariableDeclaration, precedingDecls: readonly ts.VariableDeclaration[], /** A reference of the binding whose early evaluation is known exact. */ exactEarlyRead?: (reference: ts.Identifier) => boolean, ): ts.Identifier | null { const checker = program.getTypeChecker(); const bindings = new Set(); for (const id of boundNames7(decl.name)) { const sym = checker.getSymbolAtLocation(id); if (sym) bindings.add(sym); } if (bindings.size === 0) return null; // A split require binding's assignment writes the binding; it reads nothing. let splitWrite: ts.Node | null = splitRequireCallOf7(program, decl); while (splitWrite !== null && ts.isParenthesizedExpression(splitWrite.parent)) splitWrite = splitWrite.parent; splitWrite = splitWrite !== null && ts.isBinaryExpression(splitWrite.parent) ? splitWrite.parent.left : null; const stmts = sf.statements.nodes; const cjs = isCjsJsFile7(sf, program); const symbolOf = (id: ts.Identifier): ts.Symbol | undefined => { const parent = id.parent; if (parent !== undefined && ts.isShorthandPropertyAssignment(parent) && parent.name === id) { return checker.getShorthandAssignmentValueSymbol(parent); } return checker.getSymbolAtLocation(id); }; // Stored callables and the scans that skip them. const units = new Map(); const stored = new Set(); const addUnit = (name: ts.Identifier, node: ts.Node): void => { const sym = checker.getSymbolAtLocation(name); if (sym === undefined) return; stored.add(node); const list = units.get(sym); if (list) list.push(node); else units.set(sym, [node]); }; const storedCallables = (e: ts.Expression, out: ts.Node[]): void => { while (ts.isParenthesizedExpression(e)) e = e.expression; if (ts.isArrowFunction(e) || ts.isFunctionExpression(e) || ts.isClassExpression(e)) { out.push(e); } else if (ts.isObjectLiteralExpression(e)) { for (const p of e.properties.nodes) { if ( (ts.isMethodDeclaration(p) || ts.isGetAccessorDeclaration(p) || ts.isSetAccessorDeclaration(p)) && !ts.isComputedPropertyName(p.name) ) { out.push(p); } else if (ts.isPropertyAssignment(p) && !ts.isComputedPropertyName(p.name)) { storedCallables(p.initializer, out); } } } }; stmts.forEach((s, i) => { if ((ts.isFunctionDeclaration(s) || ts.isClassDeclaration(s)) && s.name !== undefined) { addUnit(s.name, s); } else if (i < k && ts.isVariableStatement(s)) { for (const d of s.declarationList.declarations.nodes) { if (!ts.isIdentifier(d.name) || d.initializer === undefined) continue; const out: ts.Node[] = []; storedCallables(d.initializer, out); for (const node of out) addUnit(d.name, node); } } }); // Callbacks that only run as later jobs never run before the require: // the timers, microtasks and promise reactions. (Event listeners can run // synchronously from an emit, so they keep the conservative rule.) const laterJob = (callee: ts.Expression): boolean => { while (ts.isParenthesizedExpression(callee)) callee = callee.expression; const name = ts.isIdentifier(callee) ? callee.text : ts.isPropertyAccessExpression(callee) ? callee.name.text : null; return ( name !== null && (LATER_JOB_BUILTINS7.has(name) || name === "nextTick") && laterJobCallee7(checker, callee) ); }; if (!hasTopLevelAwait7(sf)) { const callbacks = (e: ts.Expression): void => { while (ts.isParenthesizedExpression(e)) e = e.expression; if (ts.isArrowFunction(e) || ts.isFunctionExpression(e)) stored.add(e); }; for (const s of stmts.slice(0, k)) { ts.walkPreorder(s, (node) => { if ((ts.isCallExpression(node) || ts.isNewExpression(node)) && laterJob(node.expression)) { for (const arg of node.arguments?.nodes ?? []) callbacks(arg); } return undefined; }); } } const isWrapperName = (e: ts.Node, name: string): e is ts.Identifier => ts.isIdentifier(e) && e.text === name && !sourceBinding7(program, e); /** `this` at the module's top level: the original export object. */ const isModuleThis = (e: ts.Node): boolean => { if (e.kind !== ts.SyntaxKind.ThisKeyword) return false; for (let p = e.parent; p !== undefined && !ts.isSourceFile(p); p = p.parent) { if (ts.isFunctionLike(p) && !ts.isArrowFunction(p)) return false; if (ts.isClassExpression(p) || ts.isClassDeclaration(p)) return false; } return true; }; const isExportObject = (e: ts.Expression): boolean => isWrapperName(e, "exports") || isModuleThis(e) || (isModuleExportsAccess(e) && isWrapperName(e.expression, "module")); const replacesExports = stmts.some((st) => cjsExportAssignmentOf(st)?.kind === "table"); // Published values: by export name ("*" for a whole replacement), and by // the stored callable whose properties hold them. const exportLinks = new Map(); const ownerLinks = new Map(); const reachedKeys = new Set(); const writtenKeys = new Set(); const reachedUnits = new Set(); const work: ts.Node[] = []; let hit: ts.Identifier | null = null; const reachUnit = (node: ts.Node): void => { if (reachedUnits.has(node)) return; reachedUnits.add(node); work.push(node); }; const reachSymbol = (sym: ts.Symbol): void => { for (const node of units.get(sym) ?? []) reachUnit(node); for (const node of ownerLinks.get(sym) ?? []) reachUnit(node); }; let allReached = false; const reachKey = (key: string | null): void => { if (key === null) { allReached = true; for (const list of exportLinks.values()) for (const node of list) reachUnit(node); return; } reachedKeys.add(key); for (const name of [key, "*"]) for (const node of exportLinks.get(name) ?? []) reachUnit(node); }; const keyReached = (key: string): boolean => allReached || reachedKeys.has(key) || (key === "*" && reachedKeys.size > 0); /** A read of the export object: which published names it can observe. */ const exportRead = (node: ts.Expression): void => { let use: ts.Node = node; while (ts.isParenthesizedExpression(use.parent)) use = use.parent; const parent = use.parent; // A truthiness test reads no member (tsc's `this && this.__helper`). if ( ts.isBinaryExpression(parent) && parent.left === use && parent.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken ) { return; } if (ts.isTypeOfExpression(parent)) return; if (ts.isPropertyAccessExpression(parent) && parent.expression === use) { let target: ts.Node = parent; while (ts.isParenthesizedExpression(target.parent)) target = target.parent; const assignment = target.parent; if ( ts.isBinaryExpression(assignment) && assignment.left === target && assignment.operatorToken.kind === ts.SyntaxKind.EqualsToken ) { // A plain write reads no member. It can still run a setter: one // published for that name (now or later), or a static one of a // replacing class. writtenKeys.add(parent.name.text); for (const node of exportLinks.get(parent.name.text) ?? []) reachUnit(node); for (const node of exportLinks.get("*") ?? []) { if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) reachUnit(node); } return; } reachKey(parent.name.text); return; } if ( ts.isElementAccessExpression(parent) && parent.expression === use && ts.isStringLiteralLike(parent.argumentExpression) ) { reachKey(parent.argumentExpression.text); return; } reachKey(null); }; const scan = (root: ts.Node): void => { ts.walkPreorder(root, (n) => { if (hit !== null) return "stop"; if (n !== root && stored.has(n)) { if (ts.isClassExpression(n) || ts.isClassDeclaration(n)) { for (const part of classSyncParts7(n)) scan(part); } return "skip"; } // A self-require answers this module's partially built exports. if (cjs && ts.isCallExpression(n)) { const spec = requireSpecOf7(n); if (spec !== null && resolveImport7(program, sf, spec, "require") === sf) reachKey(null); } if (cjs && n.kind === ts.SyntaxKind.ThisKeyword && isModuleThis(n)) { exportRead(n as ts.Expression); return undefined; } if (!ts.isIdentifier(n)) return undefined; if (inTypePosition7(n)) return undefined; const named = n.parent; if ( (ts.isPropertyAccessExpression(named) || ts.isPropertyAssignment(named) || ts.isMethodDeclaration(named) || ts.isGetAccessorDeclaration(named) || ts.isSetAccessorDeclaration(named) || ts.isPropertyDeclaration(named)) && named.name === n ) { return undefined; } // A declaration's own name reads nothing. if (named !== undefined && declaredName7(named) === n) return undefined; if (cjs && isWrapperName(n, "exports")) { exportRead(n); return undefined; } if (cjs && isWrapperName(n, "module")) { const parent = n.parent; if (ts.isPropertyAccessExpression(parent) && parent.expression === n) { if (parent.name.text === "exports") exportRead(parent); return "skip"; } reachKey(null); return undefined; } const sym = symbolOf(n); if (sym === undefined) return undefined; if (bindings.has(sym)) { if (n === splitWrite || exactEarlyRead?.(n) === true) return undefined; hit = n; return "stop"; } reachSymbol(sym); return undefined; }); }; /** Links a published value: evaluating it reads identifiers, but the * callables it names run only once they are read back. */ const publish = (value: ts.Expression, link: (node: ts.Node) => void): void => { while (ts.isParenthesizedExpression(value)) value = value.expression; if (ts.isIdentifier(value)) { const sym = symbolOf(value); if (sym !== undefined && bindings.has(sym)) { hit = value; return; } if (sym !== undefined) { // What was stored on the value stays reachable through it. for (const node of [...(units.get(sym) ?? []), ...(ownerLinks.get(sym) ?? [])]) link(node); } return; } if (ts.isFunctionExpression(value) || ts.isArrowFunction(value)) { link(value); return; } if (ts.isClassExpression(value)) { for (const part of classSyncParts7(value)) scan(part); link(value); return; } if (ts.isObjectLiteralExpression(value)) { for (const p of value.properties.nodes) { if (ts.isShorthandPropertyAssignment(p)) { const sym = checker.getShorthandAssignmentValueSymbol(p); if (sym !== undefined && bindings.has(sym)) hit = p.name as ts.Identifier; else if (sym !== undefined) { for (const node of [...(units.get(sym) ?? []), ...(ownerLinks.get(sym) ?? [])]) { link(node); } } } else if (ts.isPropertyAssignment(p) && !ts.isComputedPropertyName(p.name)) { publish(p.initializer, link); } else if ( (ts.isMethodDeclaration(p) || ts.isGetAccessorDeclaration(p) || ts.isSetAccessorDeclaration(p)) && !ts.isComputedPropertyName(p.name) ) { link(p); } else scan(p); } return; } if ( ts.isStringLiteralLike(value) || ts.isNumericLiteral(value) || value.kind === ts.SyntaxKind.TrueKeyword || value.kind === ts.SyntaxKind.FalseKeyword || value.kind === ts.SyntaxKind.NullKeyword || (ts.isVoidExpression(value) && ts.isNumericLiteral(value.expression)) ) { return; } scan(value); // Whatever an arbitrary expression evaluates to is opaque: treat it as // reachable through the target too (scan already reached its parts). }; /** Where a publication stores: an export name, or a stored callable. */ type Target = { kind: "key"; key: string } | { kind: "owner"; owner: ts.Symbol }; const targetOf = (left: ts.Expression): Target | null => { if (isModuleExportsAccess(left) && isWrapperName(left.expression, "module")) { return { kind: "key", key: "*" }; } const names: string[] = []; let e: ts.Expression = left; for (;;) { if (ts.isPropertyAccessExpression(e) && !e.questionDotToken) { if (isExportObject(e)) break; names.unshift(e.name.text); e = e.expression; } else if ( ts.isElementAccessExpression(e) && !e.questionDotToken && ts.isStringLiteralLike(e.argumentExpression) ) { names.unshift(e.argumentExpression.text); e = e.expression; } else break; } if (names.length === 0) return null; if (isExportObject(e)) { // __proto__ and the interop marker are not plain data slots, and a // member of a replaced export may be a read-only function property: // those writes keep the conservative scan. if (names[0] === "__proto__" || names[0] === "__esModule" || replacesExports) return null; return { kind: "key", key: names[0]! }; } if (ts.isIdentifier(e)) { const sym = symbolOf(e); if (sym !== undefined && units.has(sym) && !bindings.has(sym)) return { kind: "owner", owner: sym }; } return null; }; const linkTo = (target: Target, node: ts.Node): void => { if (target.kind === "key") { const replacingClass = target.key === "*" && writtenKeys.size > 0 && (ts.isClassDeclaration(node) || ts.isClassExpression(node)); if (keyReached(target.key) || writtenKeys.has(target.key) || replacingClass) { reachUnit(node); } const list = exportLinks.get(target.key); if (list) list.push(node); else exportLinks.set(target.key, [node]); } else { if ((units.get(target.owner) ?? []).some((u) => reachedUnits.has(u))) reachUnit(node); if ((units.get(target.owner) ?? []).includes(node)) return; const list = ownerLinks.get(target.owner); if (list === undefined) ownerLinks.set(target.owner, [node]); else if (!list.includes(node)) list.push(node); } }; /** A top-level publication statement, handled in place; false for any * other statement. */ const publication = (s: ts.Statement): boolean => { if (!cjs || !ts.isExpressionStatement(s)) return false; const e = s.expression; if (ts.isBinaryExpression(e) && e.operatorToken.kind === ts.SyntaxKind.EqualsToken) { const targets: Target[] = []; let value: ts.Expression = e; while ( ts.isBinaryExpression(value) && value.operatorToken.kind === ts.SyntaxKind.EqualsToken ) { const target = targetOf(value.left); if (target === null) return false; targets.push(target); value = value.right; } publish(value, (node) => { for (const t of targets) linkTo(t, node); }); return true; } // Object.defineProperty(exports, "name", { get() {...}, value: f, ... }) if ( ts.isCallExpression(e) && ts.isPropertyAccessExpression(e.expression) && isWrapperName(e.expression.expression, "Object") && e.expression.name.text === "defineProperty" && e.arguments.nodes.length === 3 ) { const [recv, key, desc] = e.arguments.nodes as unknown as [ ts.Expression, ts.Expression, ts.Expression, ]; if (!isExportObject(recv) || !ts.isStringLiteralLike(key)) return false; if (!ts.isObjectLiteralExpression(desc)) return false; const keyTarget: Target = { kind: "key", key: key.text }; const link = (node: ts.Node): void => linkTo(keyTarget, node); for (const p of desc.properties.nodes) { if ( (ts.isMethodDeclaration(p) || ts.isPropertyAssignment(p)) && ts.isIdentifier(p.name) && (p.name.text === "get" || p.name.text === "set") ) { if (ts.isMethodDeclaration(p)) link(p); else publish(p.initializer, link); } else if ( ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === "value" ) { publish(p.initializer, link); } else scan(p); } return true; } return false; }; const drain = (): void => { while (hit === null && work.length > 0) { const unit = work.pop()!; checker.prefetchSymbolRoots([unit]); scan(unit); } }; const roots = stmts.slice(0, k); checker.prefetchSymbolRoots([...roots, ...precedingDecls]); for (const s of roots) { if (hit !== null) break; if (ts.isFunctionDeclaration(s)) continue; if (ts.isClassDeclaration(s)) { for (const part of classSyncParts7(s)) scan(part); } else if (!publication(s)) { scan(s); } drain(); } for (const d of precedingDecls) { if (hit !== null) break; scan(d); drain(); } return hit; } /** Bare `require("...")` EXPRESSION STATEMENTS anywhere below the top * level of a file — inside top-level ifs/blocks/loops and inside function * bodies (the lazy-require idiom). No bindings, pure side-effect loads. */ function nestedBareRequiresOf7(sf: ts.SourceFile): ts.CallExpression[] { const out: ts.CallExpression[] = []; // Iterative walk (walkPreorder): this sweep sees every JS file whole, // including pathologically deep expression chains that must live to be // fenced by name, not crash a recursive visit. ts.walkPreorder(sf, (n) => { if ( ts.isExpressionStatement(n) && !ts.isSourceFile(n.parent) && ts.isCallExpression(n.expression) && requireSpecOf7(n.expression) !== null ) { out.push(n.expression); } }); return out; } /** INLINE `require("...")` calls in expression position anywhere in a file * (`require("./x").f`, `require("debug")("ns")`, `module.exports = * require("./y")`, `{ v: require("./v") }`): each evaluates its module at * that position, exactly like a bare require statement. Top-level require * statements and bare require statements are collected by their own * scans; a require that initializes a nested declaration keeps its * lowering fence and is not an edge. */ function inlineRequiresOf7(sf: ts.SourceFile): ts.CallExpression[] { const out: ts.CallExpression[] = []; ts.walkPreorder(sf, (n) => { if (!ts.isCallExpression(n) || requireSpecOf7(n) === null) return; let outer: ts.Node = n; while ( outer.parent !== undefined && (ts.isParenthesizedExpression(outer.parent) || ts.isAsExpression(outer.parent) || ts.isTypeAssertion(outer.parent) || ts.isNonNullExpression(outer.parent)) ) { outer = outer.parent; } const parent = outer.parent; if (parent !== undefined && ts.isExpressionStatement(parent)) return; if (parent !== undefined && ts.isVariableDeclaration(parent) && parent.initializer === outer) return; out.push(n); }); return out; } type RequireCallBindingKind7 = "ambient" | "createRequire" | "source"; function stripRequireCasts7(expr: ts.Expression): ts.Expression { let cur = expr; while ( ts.isParenthesizedExpression(cur) || ts.isAsExpression(cur) || ts.isTypeAssertion(cur) || ts.isNonNullExpression(cur) ) { cur = cur.expression; } return cur; } /** The supported-builtin provenance of an imported identifier, narrowed to * the one member this preflight distinction needs. Mirrors the lowering's * builtinImportOf alias handling so a re-export facade remains supported. */ function isCreateRequireImport7(program: ts.Program, ident: ts.Identifier): boolean { const checker = program.getTypeChecker(); const symbol = checker.getSymbolAtLocation(ident); const decl = symbol ? symbol.declarations[0] : undefined; if (decl === undefined || !ts.isImportSpecifier(decl)) return false; const importDecl = decl.parent?.parent?.parent; if (ts.isImportDeclaration(importDecl) && ts.isStringLiteral(importDecl.moduleSpecifier)) { const module = canonicalBuiltinModule(importDecl.moduleSpecifier.text); const member = decl.propertyName?.text ?? decl.name.text; if (module === "module" && member === "createRequire") return true; } if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0) { const target = checker.getAliasedSymbol(symbol); const targetDecl = target.declarations[0]; if (targetDecl !== undefined && ts.getSourceFileOfNode(targetDecl).isDeclarationFile) { for ( let p: ts.Node | undefined = targetDecl.parent; p !== undefined && !ts.isSourceFile(p); p = p.parent ) { if (ts.isModuleDeclaration(p) && ts.isStringLiteral(p.name)) { return ( canonicalBuiltinModule(p.name.text) === "module" && ts.symbolName(target) === "createRequire" ); } } } } return false; } /** True only for createRequire bindings the lowering can erase. Mutable * declaration syntax needs a whole-file proof: no writes or escapes, and * no code before initialization can reach the binding. */ export function isCreateRequireBinding7(program: ts.Program, callee: ts.Identifier): boolean { const checker = program.getTypeChecker(); const symbol = checker.getSymbolAtLocation(callee); const decl = symbol ? symbol.declarations.find((d): d is ts.VariableDeclaration => ts.isVariableDeclaration(d)) : undefined; if ( decl === undefined || decl.initializer === undefined || !ts.isVariableDeclarationList(decl.parent) ) { return false; } return ( isCreateRequireBaseCall7(program, decl.initializer) && ((decl.parent.flags & ts.NodeFlags.Const) !== 0 || stableCreateRequireBindingReason7(program, decl) === null) ); } /** Only literal calls and literal resolution queries can erase the loader * value. In particular, assignments, exported aliases, shorthand properties, * optional calls, and passing the loader to user code cannot pass this test. */ function staticCreateRequireUse7(expr: ts.Expression): boolean { let use: ts.Node = expr; while (ts.isParenthesizedExpression(use.parent)) use = use.parent; if ( ts.isPropertyAccessExpression(use.parent) && use.parent.expression === use && use.parent.name.text === "resolve" && !use.parent.questionDotToken ) { use = use.parent; if ( ts.isPropertyAccessExpression(use.parent) && use.parent.expression === use && use.parent.name.text === "paths" && !use.parent.questionDotToken ) use = use.parent; } const call = use.parent; return ( ts.isCallExpression(call) && call.expression === use && !call.questionDotToken && call.arguments.nodes.length === 1 && ts.isStringLiteralLike(call.arguments.nodes[0]!) ); } function stableCreateRequireBindingReason7( program: ts.Program, decl: ts.VariableDeclaration, ): string | null { const cache = program.analysis.createRequireReasons; const cached = cache.get(decl); if (cached !== undefined) return cached; const check = (): string | null => { const stmt = decl.parent?.parent; if ( !ts.isIdentifier(decl.name) || !ts.isVariableStatement(stmt) || !ts.isSourceFile(stmt.parent) ) { return "its mutable createRequire binding is outside the module's top level"; } const checker = program.getTypeChecker(); const symbol = checker.getSymbolAtLocation(decl.name); if (!symbol || symbol.declarations.length !== 1) return "its createRequire binding is redeclared"; if ((ts.getCombinedModifierFlags(decl) & ts.ModifierFlags.Export) !== 0) return "its createRequire result escapes through an exported binding"; const sf = ts.getSourceFileOfNode(decl); const refs = identifierOccurrences7(sf, decl.name.text); checker.prefetchSymbolNodesExact(refs); for (const ref of refs) { if (ref === decl.name || inTypePosition7(ref)) continue; let target = ts.isShorthandPropertyAssignment(ref.parent) ? checker.getShorthandAssignmentValueSymbol(ref.parent) : checker.getSymbolAtLocation(ref); if (target && (target.flags & ts.SymbolFlags.Alias) !== 0) target = checker.getAliasedSymbol(target); if (target === symbol && !staticCreateRequireUse7(ref)) { return "its createRequire binding is reassigned, escapes, or uses a computed specifier"; } } const preceding = stmt.declarationList.declarations.nodes.slice( 0, stmt.declarationList.declarations.nodes.indexOf(decl), ); if ( bindingEarlyUse7(program, sf, sf.statements.nodes.indexOf(stmt), decl, preceding) !== null ) { return "its createRequire binding can be used before initialization"; } return null; }; const reason = check(); cache.set(decl, reason); return reason; } function isCreateRequireBaseCall7(program: ts.Program, expr: ts.Expression): boolean { const init = stripRequireCasts7(expr); if ( !ts.isCallExpression(init) || init.questionDotToken !== undefined || !ts.isIdentifier(init.expression) || !isCreateRequireImport7(program, init.expression) || init.arguments.nodes.length !== 1 ) { return false; } const base = stripRequireCasts7(init.arguments.nodes[0]!); if (ts.isIdentifier(base) && base.text === "__filename") return !sourceBinding7(program, base); return ( ts.isPropertyAccessExpression(base) && base.questionDotToken === undefined && ts.isMetaProperty(base.expression) && base.expression.keywordToken === ts.SyntaxKind.ImportKeyword && (base.name.text === "url" || base.name.text === "filename") ); } /** Refuse require indirection the lowering cannot erase, while * admitting literal calls through the same stable/inline shapes as program * code. Inspect symbols, not names: nested shadows are unrelated bindings. */ function npmStaticModuleImportReason7( program: ts.Program, stmt: ts.ImportDeclaration, ): string | null { const clause = stmt.importClause; if (!clause) return null; const bindings = clause.namedBindings; if (clause.name || (bindings && !ts.isNamedImports(bindings))) { return "its node:module namespace or default import has no static createRequire usage proof"; } if (!bindings || !ts.isNamedImports(bindings)) return null; const checker = program.getTypeChecker(); const sf = ts.getSourceFileOfNode(stmt); for (const specifier of bindings.elements.nodes) { if ((specifier.propertyName?.text ?? specifier.name.text) !== "createRequire") continue; const imported = checker.getSymbolAtLocation(specifier.name); if (!imported) return "its createRequire import could not be resolved"; checker.prefetchSymbolNodesExact(identifierOccurrences7(sf, specifier.name.text)); let reason: string | null = null; const checkUse = (expr: ts.Expression): void => { if (!staticCreateRequireUse7(expr)) { reason = "its createRequire result escapes or is used with a computed specifier (static requires need literal specifiers)"; } }; ts.walkPreorder(sf, (node) => { if ( reason || !ts.isIdentifier(node) || node.text !== specifier.name.text || node === specifier.name || checker.getSymbolAtLocation(node) !== imported ) return; const call = node.parent; if ( !ts.isCallExpression(call) || call.expression !== node || !isCreateRequireBaseCall7(program, call) ) { reason = "its createRequire import is used outside a supported call based on the current file"; return; } const decl = call.parent; if ( ts.isVariableDeclaration(decl) && decl.initializer === call && ts.isIdentifier(decl.name) ) { if ((ts.getCombinedModifierFlags(decl) & ts.ModifierFlags.Export) !== 0) { reason = "its createRequire result escapes through an exported binding"; return; } if (!isCreateRequireBinding7(program, decl.name)) { reason = stableCreateRequireBindingReason7(program, decl); return; } const sym = checker.getSymbolAtLocation(decl.name); const name = decl.name.text; checker.prefetchSymbolNodesExact(identifierOccurrences7(sf, name)); ts.walkPreorder(sf, (ref) => { if ( ts.isIdentifier(ref) && ref.text === name && ref !== decl.name && checker.getSymbolAtLocation(ref) === sym ) checkUse(ref); }); } else { checkUse(call); } }); if (reason) return reason; } return null; } /** Classifies a require-shaped call as Node's ambient CommonJS global, the * one supported createRequire source binding, or another source binding. * The scanners still collect createRequire calls because they carry static * module edges; other source bindings get a pointed fence before lowering * can mistake them for module syntax. */ function requireCallBindingKind7(program: ts.Program, node: ts.Node): RequireCallBindingKind7 { const raw = ts.isVariableDeclaration(node) ? node.initializer : node; const call = raw && ts.isExpression(raw) ? stripRequireCasts7(raw) : raw; if (!call || !ts.isCallExpression(call) || !ts.isIdentifier(call.expression)) return "ambient"; const checker = program.getTypeChecker(); const symbol = checker.getSymbolAtLocation(call.expression); if (symbol === undefined) return "ambient"; const decls = symbol.declarations; if (decls.every((d) => ts.getSourceFileOfNode(d).isDeclarationFile)) return "ambient"; return isCreateRequireBinding7(program, call.expression) ? "createRequire" : "source"; } function esmRequireFeature7(kind: Exclude): string { return kind === "ambient" ? "require() in an ES module (Node throws ReferenceError — use import)" : "calls through a source binding named 'require' (this spelling is reserved for CommonJS/createRequire module edges — rename the binding)"; } const CJS_WRAPPER_NAMES = new Set(["require", "module", "exports", "__dirname", "__filename"]); function bindingDeclaresCjsWrapper7(name: ts.BindingName): ts.Identifier | null { if (ts.isIdentifier(name)) return CJS_WRAPPER_NAMES.has(name.text) ? name : null; for (const el of name.elements.nodes) { if (el.name === undefined) continue; const hit = bindingDeclaresCjsWrapper7(el.name); if (hit !== null) return hit; } return null; } function insideFunctionLike7(node: ts.Node): boolean { for (let p = node.parent; p !== undefined && !ts.isSourceFile(p); p = p.parent) { if (ts.isFunctionLike(p)) return true; } return false; } /** The first syntax marker Node's ambiguous-file detector recognizes. This * cannot use ts.isExternalModule: moduleDetection=force deliberately gives * EVERY executable file module-local checker scope, including CommonJS. * Runtime classification remains Node's own narrower question: value-level * import/export forms, import.meta, top-level await/for-await, or a top-level * lexical declaration that would redeclare a CommonJS wrapper binding. */ function nodeEsmSyntaxMarker7(sf: ts.SourceFile): ts.Node | null { for (const stmt of sf.statements.nodes) { if (ts.isImportDeclaration(stmt) && !erasedTypeOnlyImport(stmt)) return stmt; if (ts.isExportDeclaration(stmt) && !stmt.isTypeOnly) { return stmt; } if (ts.isExportAssignment(stmt)) return stmt; if ( ts.canHaveModifiers(stmt) && ts.getModifiers(stmt)?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) === true ) { return stmt; } if ( ts.isVariableStatement(stmt) && (stmt.declarationList.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)) !== 0 ) { for (const decl of stmt.declarationList.declarations.nodes) { const hit = bindingDeclaresCjsWrapper7(decl.name); if (hit !== null) return hit; } } if ( ts.isClassDeclaration(stmt) && stmt.name !== undefined && CJS_WRAPPER_NAMES.has(stmt.name.text) ) { return stmt.name; } } let found: ts.Node | null = null; ts.walkPreorder(sf, (node) => { if (ts.isMetaProperty(node) && node.keywordToken === ts.SyntaxKind.ImportKeyword) { found = node; return "stop"; } // Only await syntax needs an ancestor walk. Doing this for every node // makes a deeply nested expression take quadratic time to classify. if ( (ts.isAwaitExpression(node) || (ts.isForOfStatement(node) && node.awaitModifier !== undefined)) && !insideFunctionLike7(node) ) { found = node; return "stop"; } return undefined; }); return found; } /** True when Node would treat this file as an ES MODULE (never defining * require/__dirname there): fixed extensions first, then an explicit * nearest-package type, then Node's syntax markers for ambiguous .js/.ts * files. TypeScript's external-module bit is forced for checker scoping and * therefore intentionally plays no part in this runtime decision. */ function isNodeEsmFile7(sf: ts.SourceFile, program?: ts.Program): boolean { const cached = program?.analysis.nodeEsmFiles.get(sf); if (cached !== undefined) return cached; let result: boolean; if (sf.fileName.endsWith(".cjs") || sf.fileName.endsWith(".cts")) { result = false; } else if (sf.fileName.endsWith(".mjs") || sf.fileName.endsWith(".mts")) { result = true; } else { const packageType = nearestPackageType(sf.fileName); result = packageType === "module" ? true : packageType === "commonjs" ? false : nodeEsmSyntaxMarker7(sf) !== null; } program?.analysis.nodeEsmFiles.set(sf, result); return result; } /** A JavaScript source file Node treats as CommonJS (not an ES module): * ESM namespace/default machinery must not claim it — its export surface * is module.exports, read through the CJS interop paths and their own * fences. */ function isCjsJsFile7(sf: ts.SourceFile, program?: ts.Program): boolean { return isJsSourceFileName(sf.fileName) && !isNodeEsmFile7(sf, program); } /** The exported spelling of isCjsJsFile7 (the lowering's gate for * module.exports surfaces — `ts.isExternalModule` is NOT the CJS test: * tsgo marks CJS files with export assignments as external modules). */ export function isCjsJsFile(sf: ts.SourceFile, program?: ts.Program): boolean { return isCjsJsFile7(sf, program); } /** The self-import TDZ fences: for `import { x } from ""` (default * and namespace forms included), any TOP-LEVEL reference lexically above * the aliased declaration observes Node's temporal dead zone — the direct * spelling of the same read is tsc's own TS2448, which the import * indirection dodges, so preflight restores the bar with SC1030. Function * aliases hoist (initialized at link — readable anywhere); references * inside function-like bodies follow the direct spelling's existing * policy (tsc accepts those too) and are not fenced here. Namespace * bindings fence per MEMBER access (`self.x` above x's declaration); * the bare namespace binding itself is never TDZ. */ function selfImportTdzFences7( program: ts.Program, sf: ts.SourceFile, clause: ts.ImportClause, diags: ScrDiagnostic[], ): void { const checker = program.getTypeChecker(); const insideFunction = (node: ts.Node): boolean => { for (let p = node.parent; p !== undefined && !ts.isSourceFile(p); p = p.parent) { if (ts.isFunctionLike(p)) return true; } return false; }; // The aliased declaration's TDZ horizon: the position after which reads // are initialized. Hoisted functions have none. const tdzEndOf = (aliased: ts.Symbol): number | null => { const d = aliased.valueDeclaration ?? aliased.declarations[0]; if (d === undefined || ts.getSourceFileOfNode(d) !== sf) return null; if (ts.isFunctionDeclaration(d)) return null; return d.end; }; const fence = (ref: ts.Identifier, name: string): void => { diags.push( unsupportedDiag( "SC1030", { file: sf.fileName, start: ts.tokenStartOfNode(ref), end: ref.end }, `the self-import binding '${name}' read above its declaration (the import aliases this module's OWN binding — Node initializes it when the declaration executes, so this read throws ReferenceError; move the read below the declaration)`, ), ); }; const checkNamed = (bindingName: ts.Identifier): void => { const sym = checker.getSymbolAtLocation(bindingName); if (sym === undefined || !(sym.flags & ts.SymbolFlags.Alias)) return; checker.prefetchSymbolNodesExact(identifierOccurrences7(sf, bindingName.text)); const end = tdzEndOf(checker.getAliasedSymbol(sym)); if (end === null) return; const visit = (node: ts.Node): void => { if ( ts.isIdentifier(node) && node !== bindingName && node.text === bindingName.text && ts.tokenStartOfNode(node) < end && !insideFunction(node) && checker.getSymbolAtLocation(node) === sym ) { fence(node, bindingName.text); } ts.forEachChild(node, visit); }; visit(sf); }; if (clause.name !== undefined) checkNamed(clause.name); if (clause.namedBindings !== undefined && ts.isNamedImports(clause.namedBindings)) { for (const el of clause.namedBindings.elements.nodes) { if (!el.isTypeOnly) checkNamed(el.name); } } if (clause.namedBindings !== undefined && ts.isNamespaceImport(clause.namedBindings)) { const nsName = clause.namedBindings.name; const nsSym = checker.getSymbolAtLocation(nsName); if (nsSym === undefined) return; checker.prefetchSymbolNodesExact(identifierOccurrences7(sf, nsName.text)); const visit = (node: ts.Node): void => { if ( ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === nsName.text && ts.isIdentifier(node.name) && !insideFunction(node) && checker.getSymbolAtLocation(node.expression) === nsSym ) { const memberSym = checker.getSymbolAtLocation(node.name); if (memberSym !== undefined) { const end = tdzEndOf(memberSym); if (end !== null && ts.tokenStartOfNode(node) < end) fence(node.expression, `${nsName.text}.${node.name.text}`); } } ts.forEachChild(node, visit); }; visit(sf); } } /** True when every reference to the namespace-import binding `nsName` * (outside its own declaration) is exactly a bare expression statement's * expression — the shape whose lowering is Node's no-op. A self-import's * references all live in the importing file, so one file-local scan * answers completely. */ function nsBindingUsesAreBareStatements7( program: ts.Program, sf: ts.SourceFile, nsName: ts.Identifier, ): boolean { const checker = program.getTypeChecker(); const bindingSym = checker.getSymbolAtLocation(nsName); if (bindingSym === undefined) return false; checker.prefetchSymbolNodesExact(identifierOccurrences7(sf, nsName.text)); let bareOnly = true; const visit = (node: ts.Node): void => { if (!bareOnly) return; if ( ts.isIdentifier(node) && node !== nsName && node.text === nsName.text && checker.getSymbolAtLocation(node) === bindingSym ) { if ( !( node.parent !== undefined && ts.isExpressionStatement(node.parent) && node.parent.expression === node ) ) { bareOnly = false; return; } } ts.forEachChild(node, visit); }; visit(sf); return bareOnly; } /* ── benign-cycle admission ────────────────────────────────────────────── * Node runs legal ESM cycles all over the ecosystem: the revisited module * is a cache hit, and the only hazard is a READ of a binding before its * declaration executed (TDZ ReferenceError for let/const/class, a stale * `undefined` for var). Cycles where such a read is REACHABLE are admitted * by the exact initialization rule (exactCycleRefusal7 below), which * models the TDZ with initialization checks. A cycle needs no checks when * partial initialization is provably unobservable — the DECLARATION-ONLY * INIT WINDOW rule: * * 1. Every module of the cycle cluster (the strongly-connected component * — any member may be mid-initialization while another's top level * runs, and every module that can even NAME a cluster binding during * the window is itself in the cluster) is an ES module whose top * level is INERT: declarations and expressions that cannot call into * user code (nonInertTopLevel7). * 2. Every use of each binding the cycle-closing statement imports sits * in a DEFERRED position — inside a function body, a parameter * default, or an instance-field initializer (inDeferredPosition7). * * Together: cluster bindings are read only from function bodies, and no * top level in the window can call a function, so the first possible read * happens after every member initialized — the guarded %init calls then * reproduce Node's cache-hit order exactly, and Node's TDZ/stale-slot * outcomes are unreachable. */ /** A module edge, carrying what cycle admission needs when it turns out * to close one. `stmt` is the importing/re-exporting statement; absent * for require() edges, which are never admitted (their CJS home fails the * cluster check anyway). */ export interface CycleEdge { dep: ts.SourceFile; stmt?: ts.ImportDeclaration | ts.ExportDeclaration; /** An inline require() in a deferred position (a function body): Node * evaluates the dependency when that code runs, after this module's own * top level, so the edge orders the dependency after the module and * never closes a cycle through the module's initialization. */ lazy?: true; /** The inline require() call behind the edge (inline edges only). */ call?: ts.CallExpression; } /** True when the reference at `node` can only evaluate AFTER the module * graph finished initializing: inside a function-like BODY, inside a * parameter (default initializers evaluate at call time), or inside an * INSTANCE field initializer (runs at construction, and no admitted top * level constructs). Static field initializers, static blocks, heritage * clauses, computed names, and decorators all run at the class statement * itself — not deferred. */ function inDeferredPosition7(node: ts.Node): boolean { let child: ts.Node = node; for ( let p: ts.Node | undefined = node.parent; p !== undefined && !ts.isSourceFile(p); p = p.parent ) { if (p.kind === ts.SyntaxKind.Decorator) return false; if (ts.isClassStaticBlockDeclaration(p)) return false; if (ts.isFunctionLike(p)) { if ((p as { body?: ts.Node }).body === child) return true; if (ts.isParameter(child)) return true; } if ( ts.isPropertyDeclaration(p) && p.initializer === child && (ts.getCombinedModifierFlags(p) & ts.ModifierFlags.Static) === 0 ) { return true; } child = p; } return false; } /** True for identifiers in pure TYPE positions (annotations, `typeof` * queries, interface/alias bodies) — erased at runtime, never a read. A * CLASS `extends` heritage expression is the one TypeNode-shaped position * that IS a runtime read. */ function inTypePosition7(node: ts.Node): boolean { for ( let p: ts.Node | undefined = node.parent; p !== undefined && !ts.isSourceFile(p); p = p.parent ) { if (ts.isTypeNode(p)) { const heritage = p.parent; const isClassExtends = p.kind === ts.SyntaxKind.ExpressionWithTypeArguments && heritage !== undefined && ts.isHeritageClause(heritage) && heritage.token === ts.SyntaxKind.ExtendsKeyword && (ts.isClassDeclaration(heritage.parent) || ts.isClassExpression(heritage.parent)); if (!isClassExtends) return true; } if (ts.isTypeAliasDeclaration(p) || ts.isInterfaceDeclaration(p)) return true; } return false; } /** The first top-level construct of `sf` that could execute USER code * during a cycle's init window, or null when the module's top level is * declaration-only ("inert"). The expression whitelist is deliberately * conservative — anything that could CALL user code (directly, through a * callback-invoking builtin, a getter, an iterator, or an implicit * coercion of a user object) disqualifies the module's cycles. */ function nonInertTopLevel7(program: ts.Program, sf: ts.SourceFile): ts.Node | null { const nonInert = topLevelInertness7(program); for (const stmt of sf.statements.nodes) { const offence = nonInert(stmt); if (offence !== null) return offence; } return null; } /** The per-statement form of nonInertTopLevel7: answers, for one * top-level statement, the first construct that could execute user code * while the statement runs, or null when the statement is inert. */ function topLevelInertness7(program: ts.Program): (stmt: ts.Statement) => ts.Node | null { const checker = program.getTypeChecker(); const PRIM = ts.TypeFlags.StringLike | ts.TypeFlags.NumberLike | ts.TypeFlags.BooleanLike | ts.TypeFlags.BigIntLike | ts.TypeFlags.ESSymbolLike | ts.TypeFlags.EnumLike | ts.TypeFlags.Undefined | ts.TypeFlags.Null | ts.TypeFlags.Void | ts.TypeFlags.Never; /** Coercion-safe: the expression's type is primitive in every arm, so * ToString/ToNumber/ToPrimitive can never reach a user valueOf. */ const primitiveTyped = (e: ts.Expression): boolean => { const t = checker.getTypeAtLocation(e); const parts = t.isUnionType() ? ts.constituentTypes(t) : [t]; return parts.every((p) => (p.flags & PRIM) !== 0); }; /** The identifier's symbol lives entirely in declaration files — a * global builtin (process, WeakSet, Object, …) whose property reads and * calls are runtime-implemented, never user code. */ const dtsRooted = (e: ts.Expression): boolean => { let root: ts.Expression = e; while (ts.isPropertyAccessExpression(root) || ts.isElementAccessExpression(root)) root = root.expression; if (ts.isMetaProperty(root)) return true; // import.meta if (!ts.isIdentifier(root)) return false; let sym = checker.getSymbolAtLocation(root); if (sym === undefined) return false; if (sym.flags & ts.SymbolFlags.Alias) sym = checker.getAliasedSymbol(sym); const decls = sym.declarations; return decls.length > 0 && decls.every((d) => ts.getSourceFileOfNode(d).isDeclarationFile); }; const hasDecorator = (n: ts.Node): boolean => ((n as { modifiers?: ts.NodeArray }).modifiers?.nodes ?? []).some( (m) => m.kind === ts.SyntaxKind.Decorator, ); /** No function-like node anywhere in the subtree — arguments to builtin * calls must not smuggle a callback the builtin could invoke. */ const containsFunctionLike = (n: ts.Node): boolean => { let found = false; ts.walkPreorder(n, (c) => { if (ts.isFunctionLike(c) || ts.isClassExpression(c) || ts.isClassDeclaration(c)) { found = true; return "stop"; } return undefined; }); return found; }; const inertClass = (c: ts.ClassLikeDeclaration): ts.Node | null => { if (hasDecorator(c)) return c; for (const h of c.heritageClauses?.nodes ?? []) { if (h.token !== ts.SyntaxKind.ExtendsKeyword) continue; for (const t of h.types.nodes) if (!inert(t.expression)) return t.expression; } for (const m of c.members.nodes) { if (ts.isClassStaticBlockDeclaration(m)) return m; if (hasDecorator(m)) return m; const memberName = (m as { name?: ts.PropertyName }).name; if (memberName !== undefined && ts.isComputedPropertyName(memberName)) { if (!inert(memberName.expression) || !primitiveTyped(memberName.expression)) return memberName; } if ( ts.isPropertyDeclaration(m) && (ts.getCombinedModifierFlags(m) & ts.ModifierFlags.Static) !== 0 && m.initializer !== undefined && !inert(m.initializer) ) { return m.initializer; } } return null; }; const COERCING_FREE = new Set([ ts.SyntaxKind.EqualsEqualsEqualsToken, ts.SyntaxKind.ExclamationEqualsEqualsToken, ts.SyntaxKind.AmpersandAmpersandToken, ts.SyntaxKind.BarBarToken, ts.SyntaxKind.QuestionQuestionToken, ts.SyntaxKind.CommaToken, ts.SyntaxKind.InstanceOfKeyword, ]); const inert = (e: ts.Expression): boolean => { if ( ts.isStringLiteral(e) || ts.isNumericLiteral(e) || ts.isBigIntLiteral(e) || ts.isRegularExpressionLiteral(e) || ts.isNoSubstitutionTemplateLiteral(e) || e.kind === ts.SyntaxKind.TrueKeyword || e.kind === ts.SyntaxKind.FalseKeyword || e.kind === ts.SyntaxKind.NullKeyword || ts.isIdentifier(e) || ts.isMetaProperty(e) || ts.isArrowFunction(e) || ts.isFunctionExpression(e) ) { return true; } if (ts.isClassExpression(e)) return inertClass(e) === null; if ( ts.isParenthesizedExpression(e) || ts.isAsExpression(e) || ts.isSatisfiesExpression(e) || ts.isNonNullExpression(e) ) { return inert(e.expression); } if (ts.isPropertyAccessExpression(e)) return dtsRooted(e); if (ts.isElementAccessExpression(e)) { return dtsRooted(e) && inert(e.argumentExpression) && primitiveTyped(e.argumentExpression); } if (ts.isTemplateExpression(e)) { return e.templateSpans.nodes.every( (s) => inert(s.expression) && primitiveTyped(s.expression), ); } if (ts.isVoidExpression(e) || ts.isTypeOfExpression(e)) return inert(e.expression); if (ts.isPrefixUnaryExpression(e)) { if (e.operator === ts.SyntaxKind.ExclamationToken) return inert(e.operand); if ( e.operator === ts.SyntaxKind.PlusToken || e.operator === ts.SyntaxKind.MinusToken || e.operator === ts.SyntaxKind.TildeToken ) { return inert(e.operand) && primitiveTyped(e.operand); } return false; } if (ts.isBinaryExpression(e)) { if (!inert(e.left) || !inert(e.right)) return false; if (COERCING_FREE.has(e.operatorToken.kind)) return true; return primitiveTyped(e.left) && primitiveTyped(e.right); } if (ts.isConditionalExpression(e)) return inert(e.condition) && inert(e.whenTrue) && inert(e.whenFalse); if (ts.isArrayLiteralExpression(e)) return e.elements.nodes.every((el) => !ts.isSpreadElement(el) && inert(el)); if (ts.isObjectLiteralExpression(e)) { return e.properties.nodes.every((p) => { if (ts.isShorthandPropertyAssignment(p)) return true; if (!ts.isPropertyAssignment(p) || ts.isComputedPropertyName(p.name)) return false; return inert(p.initializer); }); } if (ts.isCallExpression(e) || ts.isNewExpression(e)) { if (!dtsRooted(e.expression) || !ts.isIdentifier(chainRoot7(e.expression))) return false; const args = e.arguments?.nodes ?? []; // A CALLABLE argument is admissible when it is itself dts-rooted: // a builtin-owned function value (the `promisify(fs.readFile)` // at every cycle member's top level) is runtime-implemented — even // if the builtin callee invokes it, no user code runs and no // cluster binding is observable. Function literals and user // callables keep the refusal. return args.every( (a) => inert(a) && !containsFunctionLike(a) && (checker.getCallSignatures(checker.getTypeAtLocation(a)).length === 0 || dtsRooted(a)), ); } return false; }; return (stmt: ts.Statement): ts.Node | null => { if ( ts.isImportDeclaration(stmt) || ts.isExportDeclaration(stmt) || ts.isImportEqualsDeclaration(stmt) || ts.isFunctionDeclaration(stmt) || ts.isInterfaceDeclaration(stmt) || ts.isTypeAliasDeclaration(stmt) || ts.isEmptyStatement(stmt) || (ts.getCombinedModifierFlags(stmt as unknown as ts.Declaration) & ts.ModifierFlags.Ambient) !== 0 ) { return null; } if (ts.isEnumDeclaration(stmt)) { return ( stmt.members.nodes.find((m) => m.initializer !== undefined && !inert(m.initializer)) ?? null ); } if (ts.isClassDeclaration(stmt)) return inertClass(stmt); if (ts.isVariableStatement(stmt)) { for (const d of stmt.declarationList.declarations.nodes) { if (!ts.isIdentifier(d.name)) return d.name; // patterns can invoke getters/iterators if (d.initializer !== undefined && !inert(d.initializer)) return d.initializer; } return null; } if (ts.isExpressionStatement(stmt)) return inert(stmt.expression) ? null : stmt; if (ts.isExportAssignment(stmt) && !stmt.isExportEquals) { return inert(stmt.expression) ? null : stmt; } return stmt; }; } /** The head of a property/element-access chain. */ function chainRoot7(e: ts.Expression): ts.Expression { let root: ts.Expression = e; while (ts.isPropertyAccessExpression(root) || ts.isElementAccessExpression(root)) root = root.expression; return root; } /** Identifier occurrences a binding-identity preflight scan will query. * Gathered without checker traffic so managed deferred bodies can be * warmed in one exact symbol batch before the semantic walk. */ function identifierOccurrences7(root: ts.Node, text: string): ts.Identifier[] { const out: ts.Identifier[] = []; ts.walkPreorder(root, (node) => { if (ts.isIdentifier(node) && node.text === text) out.push(node); return undefined; }); return out; } /** The first use of a binding this cycle-closing import introduces that * sits OUTSIDE a deferred position (a read there can observe the * partially-initialized exporter), or null when every use defers. * Re-export statements bind nothing locally — always null: the alias is * link-time plumbing, never an evaluation-time read. Export specifiers * of an imported binding are the same alias story, not reads. */ function backEdgeUseOffence7( program: ts.Program, stmt: ts.ImportDeclaration | ts.ExportDeclaration, occurrences: ReadonlyMap, ): { name: string; node: ts.Node } | null { if (!ts.isImportDeclaration(stmt) || stmt.importClause === undefined) return null; const checker = program.getTypeChecker(); const clause = stmt.importClause; const bindingNames: ts.Identifier[] = []; if (clause.name !== undefined) bindingNames.push(clause.name); if (clause.namedBindings !== undefined) { if (ts.isNamespaceImport(clause.namedBindings)) bindingNames.push(clause.namedBindings.name); else for (const el of clause.namedBindings.elements.nodes) if (!el.isTypeOnly) bindingNames.push(el.name); } for (const bindingName of bindingNames) { const sym = checker.getSymbolAtLocation(bindingName); if (sym === undefined) continue; const references = occurrences.get(bindingName.text) ?? []; checker.prefetchSymbolNodesExact(references); for (const node of references) { if ( !(node.parent !== undefined && ts.isExportSpecifier(node.parent)) && checker.getSymbolAtLocation(node) === sym && !inTypePosition7(node) && !inDeferredPosition7(node) ) { return { name: bindingName.text, node }; } } } return null; } export function makeCycleAdmission( program: ts.Program, edgesOf: (sf: ts.SourceFile) => readonly CycleEdge[], ): (importer: ts.SourceFile, e: CycleEdge) => string | null { // Tarjan over the same edges the order walk uses (self-edges skipped — // Node's self-reference rule makes those benign before admission is // ever asked). State persists across lazily-added roots: a later // strongconnect over an unvisited root composes with earlier runs // exactly like the classic all-roots loop. const sccOf = new Map(); const index = new Map(); const low = new Map(); const onStack = new Set(); const tstack: ts.SourceFile[] = []; let next = 0; const strongconnect = (v: ts.SourceFile): void => { index.set(v, next); low.set(v, next); next++; tstack.push(v); onStack.add(v); for (const e of edgesOf(v)) { const w = e.dep; if (w === v) continue; if (!index.has(w)) { strongconnect(w); low.set(v, Math.min(low.get(v)!, low.get(w)!)); } else if (onStack.has(w)) { low.set(v, Math.min(low.get(v)!, index.get(w)!)); } } if (low.get(v) === index.get(v)) { const comp: ts.SourceFile[] = []; for (;;) { const w = tstack.pop()!; onStack.delete(w); comp.push(w); if (w === v) break; } for (const w of comp) sccOf.set(w, comp); } }; const lineOf = (node: ts.Node): string => { const nsf = ts.getSourceFileOfNode(node); return `${nsf.fileName}:${ts.getLineAndCharacterOfPosition(nsf, ts.tokenStartOfNode(node)).line + 1}`; }; // Cluster verdict memo (keyed by the component array identity): true // when every member is an ES module passing the inert-top-level bar. const sccVerdict = new Map(); // The exact-initialization verdict, same keying (exactCycleRefusal7). const exactVerdict = new Map(); // A module can close many cycle edges, each importing many bindings. // Index its identifiers once for this admission pass. ASTs are immutable; // the pass owns the index so it cannot retain a disposed program. const bindingUses = new Map>(); const usesOf = (sf: ts.SourceFile): Map => { const cached = bindingUses.get(sf); if (cached !== undefined) return cached; const uses = new Map(); ts.walkPreorder(sf, (node) => { if (ts.isImportDeclaration(node)) return "skip"; if (ts.isIdentifier(node)) { const references = uses.get(node.text); if (references !== undefined) references.push(node); else uses.set(node.text, [node]); } return undefined; }); bindingUses.set(sf, uses); return uses; }; // The CommonJS cluster verdict, same keying (cjsCycleRefusal7). const cjsVerdict = new Map(); return (importer: ts.SourceFile, e: CycleEdge): string | null => { if (e.stmt === undefined) { if (!index.has(importer)) strongconnect(importer); const comp = sccOf.get(e.dep); if (comp === undefined || comp.length < 2 || !comp.includes(importer)) { return "the cycle closes through a require() edge"; } if (!cjsVerdict.has(comp)) cjsVerdict.set(comp, cjsCycleRefusal7(program, comp, lineOf)); const reason = cjsVerdict.get(comp) ?? null; return reason === null ? null : `the cycle closes through a require() edge: ${reason}`; } // Cheap per-edge admission: nothing readable binds through the edge. if (ts.isImportDeclaration(e.stmt)) { const clause = e.stmt.importClause; if ( clause === undefined || typeOnlyNamedImport(e.stmt) || (clause.name === undefined && (clause.namedBindings === undefined || (ts.isNamespaceImport(clause.namedBindings) && nsBindingUsesAreBareStatements7(program, importer, clause.namedBindings.name)))) ) { return null; } } if (!index.has(importer)) strongconnect(importer); const comp = sccOf.get(e.dep); if (comp === undefined || comp.length < 2 || !comp.includes(importer)) { return "the cycle's module cluster could not be analyzed"; } if (!sccVerdict.has(comp)) { sccVerdict.set( comp, comp.every((m) => !isCjsJsFile7(m, program) && nonInertTopLevel7(program, m) === null), ); } if ( sccVerdict.get(comp) === true && backEdgeUseOffence7(program, e.stmt, usesOf(importer)) === null ) { return null; } // The EXACT INITIALIZATION rule: the cycle runs in Node's evaluation // order and every binding that can be observed before its declaration // runs carries an initialization check (cycleEarlyBindings). if (!exactVerdict.has(comp)) { const reason = exactCycleRefusal7(program, comp, lineOf); exactVerdict.set(comp, reason); if (reason === null) { const members = program.analysis.exactCycleMembers; for (const m of comp) members.add(m); } } return exactVerdict.get(comp) ?? null; }; } /** The reason a strongly-connected cluster of CommonJS modules cannot be * initialized exactly through its require() cycle, or null when it can. * Node answers a require of a module that is still evaluating with that * module's exports object as it stands, while the compiled bindings alias * the exporter's final declarations. The two agree when: * - every member settles its exports before anything can enter the * cycle: all of its export statements come before its first top-level * statement that can run another module's code (a require, or anything * but literals, plain reads, function and class values, and * standard-library globals called with literal arguments), and every * other use of its export object is a plain member read; and * - no member's top-level code can reach its binding of another member, * so no member runs another member's code while the cluster is still * initializing. Each module then initializes exactly as it would * outside the cycle, and its own require bindings keep the acyclic * analysis (requireEarlyReference). * Other cycles keep the SC1016 fence. */ function cjsCycleRefusal7( program: ts.Program, comp: readonly ts.SourceFile[], lineOf: (node: ts.Node) => string, ): string | null { const members = new Set(comp); const builtinRoot = (e: ts.Expression): boolean => { while (ts.isPropertyAccessExpression(e)) e = e.expression; return ts.isIdentifier(e) && !sourceBinding7(program, e); }; const literal = (e: ts.Node): boolean => ts.isStringLiteralLike(e) || ts.isNumericLiteral(e) || ts.isRegularExpressionLiteral(e) || e.kind === ts.SyntaxKind.TrueKeyword || e.kind === ts.SyntaxKind.FalseKeyword || e.kind === ts.SyntaxKind.NullKeyword; /** An expression whose evaluation runs no user code and requires * nothing: literals, plain reads, function and class values, literal * containers of those, and standard-library globals called with * literal arguments. */ const inertExpr = (e: ts.Node): boolean => { while (ts.isParenthesizedExpression(e)) e = e.expression; if (literal(e) || ts.isIdentifier(e)) return true; if (ts.isFunctionExpression(e) || ts.isArrowFunction(e)) return true; if (ts.isClassExpression(e)) return classSyncParts7(e).every(inertPart); if (ts.isObjectLiteralExpression(e)) { return e.properties.nodes.every((p) => ts.isShorthandPropertyAssignment(p) ? true : ts.isPropertyAssignment(p) ? !ts.isComputedPropertyName(p.name) && inertExpr(p.initializer) : (ts.isMethodDeclaration(p) || ts.isGetAccessorDeclaration(p) || ts.isSetAccessorDeclaration(p)) && !ts.isComputedPropertyName(p.name), ); } if (ts.isArrayLiteralExpression(e)) { return e.elements.nodes.every((el) => !ts.isSpreadElement(el) && inertExpr(el)); } if (ts.isPropertyAccessExpression(e)) return builtinRoot(e); if (ts.isVoidExpression(e)) return literal(e.expression); if (ts.isCallExpression(e) || ts.isNewExpression(e)) { return ( requireSpecOf7(e) === null && builtinRoot(e.expression) && (e.arguments?.nodes ?? []).every(literal) ); } return false; }; const inertPart = (part: ts.Node): boolean => { if (ts.isHeritageClause(part)) return part.types.nodes.every((t) => inertExpr(t.expression)); if (ts.isComputedPropertyName(part)) return inertExpr(part.expression); return ts.isExpression(part) && inertExpr(part); }; const exportObject = (e: ts.Expression): boolean => (ts.isIdentifier(e) && e.text === "exports" && !sourceBinding7(program, e)) || (isModuleExportsAccess(e) && ts.isIdentifier(e.expression) && !sourceBinding7(program, e.expression)); /** A top-level statement that only publishes inert values. */ const exportStatement = (stmt: ts.Statement): boolean => { if (!ts.isExpressionStatement(stmt)) return false; if (isEsModuleStamp(stmt.expression)) return true; const cjs = cjsExportAssignmentOf(stmt); if (cjs !== null) { let value: ts.Expression = cjs.expr; while ( ts.isBinaryExpression(value) && value.operatorToken.kind === ts.SyntaxKind.EqualsToken ) { const left = value.left; if ( !isModuleExportsAccess(left) && !(ts.isPropertyAccessExpression(left) && exportObject(left.expression)) ) return false; value = value.right; } return inertExpr(value); } const call = stmt.expression; return ( ts.isCallExpression(call) && ts.isPropertyAccessExpression(call.expression) && ts.isIdentifier(call.expression.expression) && call.expression.expression.text === "Object" && !sourceBinding7(program, call.expression.expression) && call.expression.name.text === "defineProperty" && call.arguments.nodes.length === 3 && exportObject(call.arguments.nodes[0]!) && ts.isStringLiteralLike(call.arguments.nodes[1]!) && inertExpr(call.arguments.nodes[2]!) ); }; for (const m of comp) { if (!isCjsJsFile7(m, program)) { return `${m.fileName} is not a CommonJS module — require cycles are admitted between CommonJS modules only`; } const stmts = m.statements.nodes; // The first statement that can run another module's code. let firstEntry = stmts.length; for (let i = 0; i < stmts.length; i++) { const st = stmts[i]!; const inert = ts.isFunctionDeclaration(st) || ts.isEmptyStatement(st) || (ts.isExpressionStatement(st) && ts.isStringLiteral(st.expression)) || (ts.isClassDeclaration(st) && classSyncParts7(st).every(inertPart)) || (ts.isVariableStatement(st) && st.declarationList.declarations.nodes.every( (d) => ts.isIdentifier(d.name) && (d.initializer === undefined || inertExpr(d.initializer)), )) || exportStatement(st); if (!inert) { firstEntry = i; break; } } // Every other use of the export object must be a plain member read: // anything else can change what a module of the cycle observes. let stray: ts.Node | null = null; const settled = new Set(stmts.slice(0, firstEntry).filter(exportStatement)); // A bundler export table the rewrite certified (npmStaticLiveExportTable) // stands for the live getter object the original installed before its // first require: the tail table settles as if it ran there. const tail = stmts[stmts.length - 1]; if ( tail !== undefined && npmStaticLiveExportTable(m.fileName) && cjsExportAssignmentOf(tail)?.kind === "table" ) settled.add(tail); ts.walkPreorder(m, (n) => { if (stray !== null) return "stop"; if (ts.isStatement(n) && settled.has(n)) return "skip"; let use: ts.Expression | null = null; if (n.kind === ts.SyntaxKind.ThisKeyword) { let moduleThis = true; for (let p = n.parent; p !== undefined && !ts.isSourceFile(p); p = p.parent) { if ( (ts.isFunctionLike(p) && !ts.isArrowFunction(p)) || ts.isClassDeclaration(p) || ts.isClassExpression(p) ) { moduleThis = false; break; } } if (moduleThis) use = n as ts.Expression; } else if ( ts.isIdentifier(n) && (n.text === "exports" || n.text === "module") && !(ts.isPropertyAccessExpression(n.parent) && n.parent.name === n) && !sourceBinding7(program, n) ) { if (n.text === "exports") use = n; else if (!ts.isPropertyAccessExpression(n.parent) || n.parent.expression !== n) stray = n; else if (n.parent.name.text === "exports") use = n.parent; } if (use === null) return undefined; const parent = use.parent; const read = ts.isPropertyAccessExpression(parent) && parent.expression === use && !( ts.isBinaryExpression(parent.parent) && parent.parent.left === parent && parent.parent.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && parent.parent.operatorToken.kind <= ts.SyntaxKind.LastAssignment ) && !(ts.isCallExpression(parent.parent) && parent.parent.expression === parent) && !ts.isDeleteExpression(parent.parent); if (!read) stray = use; return undefined; }); if (stray !== null) { return `${m.fileName} uses its exports at ${lineOf(stray)} outside the export statements that precede its first require, so a module of the cycle can observe them before they settle`; } // Bindings of other members must stay unreachable from top-level code. for (const st of stmts) { for (const req of requiresOf7(st, program)) { if (req.decl === null) continue; const dep = resolveImport7(program, m, req.spec, "require"); if (dep === null || !members.has(dep)) continue; const reached = requireBindingEarlyUse7(program, m, stmts.length, req.decl, [], (id) => functionExportRead7(id, dep), ); if (reached !== null) { return `the top-level code of ${m.fileName} can use its binding '${reached.text}' of a module of the cycle (${lineOf(reached)}) while the cycle initializes`; } } } } return null; } /** True when `use` initializes a top-level const whose every other * reference is a value of an export statement (`module.exports = { … }` * or `exports.n = …`). */ function publishedAlias7(use: ts.Node, decl: ts.Node | undefined): boolean { if ( decl === undefined || !ts.isVariableDeclaration(decl) || decl.initializer !== use || !ts.isIdentifier(decl.name) ) return false; const list = decl.parent; const stmt = list?.parent; const sf = stmt?.parent; if ( list === undefined || (list.flags & ts.NodeFlags.Const) === 0 || stmt === undefined || !ts.isVariableStatement(stmt) || sf === undefined || !ts.isSourceFile(sf) ) return false; const alias = decl.name; const published = new Set(); for (const st of sf.statements.nodes) { const cjs = cjsExportAssignmentOf(st); if (cjs === null) continue; if (cjs.kind === "member") published.add(cjs.value); else if (cjs.obj !== null) { for (const p of cjs.obj.properties.nodes) { if (ts.isPropertyAssignment(p) && !ts.isComputedPropertyName(p.name)) published.add(p.initializer); else if (ts.isShorthandPropertyAssignment(p)) published.add(p.name); } } } let kept = false; ts.walkPreorder(sf, (n) => { if (kept) return "stop"; if (!ts.isIdentifier(n) || n === alias || n.text !== alias.text) return undefined; const p = n.parent; if ( p !== undefined && ((ts.isPropertyAccessExpression(p) && p.name === n) || (ts.isPropertyAssignment(p) && p.name === n)) ) return undefined; if (!published.has(n)) kept = true; return undefined; }); return !kept; } /** True for a plain read `binding.name` (not a call, not a write, not a * value that is stored or passed on) of the cycle member `dep` whose * export `name` is a top-level function declaration nothing reassigns. * Every member settles its exports before entering the cycle, so Node * reads that same function whenever the read runs, and the compiled read * resolves to the declaration itself. */ function functionExportRead7(id: ts.Identifier, dep: ts.SourceFile): boolean { const access = id.parent; if (!ts.isPropertyAccessExpression(access) || access.expression !== id) return false; // The value the read yields: through parentheses and the right operand // of a comma, so `(0, binding.name)(...)` is the call it spells. let use: ts.Node = access; for (;;) { const p = use.parent; if (ts.isParenthesizedExpression(p)) use = p; else if ( ts.isBinaryExpression(p) && p.operatorToken.kind === ts.SyntaxKind.CommaToken && p.right === use ) use = p; else break; } const parent = use.parent; // Only a use that neither runs the function nor keeps it is a read: a // stored alias (`const f = binding.name`, `{ f: binding.name }`) or an // argument can be called while the cycle initializes. A top-level const // that only publishes it (`module.exports = { name: f }`, the static // rewrite's re-export spelling) runs nothing: a module that reads the // export back checks that read itself. const observes = publishedAlias7(use, parent) || ts.isExpressionStatement(parent) || ts.isTypeOfExpression(parent) || ts.isVoidExpression(parent) || (ts.isPrefixUnaryExpression(parent) && parent.operator === ts.SyntaxKind.ExclamationToken) || (ts.isBinaryExpression(parent) && (parent.operatorToken.kind === ts.SyntaxKind.EqualsEqualsEqualsToken || parent.operatorToken.kind === ts.SyntaxKind.ExclamationEqualsEqualsToken || parent.operatorToken.kind === ts.SyntaxKind.EqualsEqualsToken || parent.operatorToken.kind === ts.SyntaxKind.ExclamationEqualsToken)) || ((ts.isIfStatement(parent) || ts.isWhileStatement(parent) || ts.isDoStatement(parent)) && parent.expression === use) || (ts.isConditionalExpression(parent) && parent.condition === use); if (!observes) return false; const name = access.name.text; // The exported value's identifier: a table entry or a member export. const values: ts.Expression[] = []; for (const stmt of dep.statements.nodes) { if (!ts.isExpressionStatement(stmt) || !ts.isBinaryExpression(stmt.expression)) continue; const e = stmt.expression; if (e.operatorToken.kind !== ts.SyntaxKind.EqualsToken) continue; if (isModuleExportsAccess(e.left)) { if (!ts.isObjectLiteralExpression(e.right)) return false; for (const p of e.right.properties.nodes) { if (ts.isSpreadAssignment(p)) return false; if (ts.isShorthandPropertyAssignment(p) && p.name.text === name) values.push(p.name as ts.Identifier); else if ( ts.isPropertyAssignment(p) && (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) && p.name.text === name ) values.push(p.initializer); } } else if ( ts.isPropertyAccessExpression(e.left) && e.left.name.text === name && ((ts.isIdentifier(e.left.expression) && e.left.expression.text === "exports") || isModuleExportsAccess(e.left.expression)) ) { values.push(e.right); } } const value = values.length === 1 ? values[0]! : null; if (value === null || !ts.isIdentifier(value)) return false; const fnName = value.text; if ( !dep.statements.nodes.some( (st) => ts.isFunctionDeclaration(st) && st.name?.text === fnName && st.body !== undefined, ) ) return false; let rebound = false; ts.walkPreorder(dep, (n) => { if (rebound) return "stop"; if ( (ts.isBinaryExpression(n) && ts.isIdentifier(n.left) && n.left.text === fnName && n.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && n.operatorToken.kind <= ts.SyntaxKind.LastAssignment) || (ts.isVariableDeclaration(n) && ts.isIdentifier(n.name) && n.name.text === fnName) || (ts.isFunctionDeclaration(n) && n.name?.text === fnName && !ts.isSourceFile(n.parent)) ) rebound = true; return undefined; }); return !rebound; } /* ── exact cyclic initialization ───────────────────────────────────────── * Cycles that fail the declaration-only rule still evaluate exactly like * Node when their shape is representable: the guarded %init calls already * run every module body in ES evaluation order (depth-first postorder, * a module mid-evaluation answered from the cache), function declarations * are callable from the start, and live bindings are shared globals. What * remains is the temporal dead zone: a let/const binding read before its * declaration ran throws ReferenceError. cycleEarlyBindings finds every * such binding whose read CAN run early; the lowering gives each one an * initialization flag and checks it at every read or write that can run * before the declaration. The shapes below have no such representation * and keep the SC1016 fence. */ /** The modules of `program` whose import cycles were admitted by the * exact initialization rule (not the declaration-only rule). */ export function exactCycleMembers(program: ts.Program): ReadonlySet { return program.analysis.exactCycleMembers; } /** True when the module's top level (outside every function-like body) * contains `await` or `for await`. */ function hasTopLevelAwait7(sf: ts.SourceFile): boolean { let found = false; ts.walkPreorder(sf, (node) => { if (found) return "stop"; if (node !== sf && ts.isFunctionLike(node)) return "skip"; if ( ts.isAwaitExpression(node) || (ts.isForOfStatement(node) && node.awaitModifier !== undefined) ) { found = true; return "stop"; } return undefined; }); return found; } /** The reason a strongly-connected module cluster cannot be initialized * exactly, or null when it can. */ function exactCycleRefusal7( program: ts.Program, comp: readonly ts.SourceFile[], lineOf: (node: ts.Node) => string, ): string | null { const members = new Set(comp); const targetOf = (stmt: ts.ImportDeclaration | ts.ExportDeclaration): ts.SourceFile | null => { const spec = stmt.moduleSpecifier; if (spec === undefined || !ts.isStringLiteral(spec)) return null; const sf = ts.getSourceFileOfNode(stmt); return resolveImport7(program, sf, spec.text) ?? npmStaticDepSf7(program, sf, spec.text); }; for (const m of comp) { if (isCjsJsFile7(m, program) || /\.c[jt]s$/.test(m.fileName)) { return `${m.fileName} is a CommonJS module — cyclic initialization covers ES modules only`; } if (hasTopLevelAwait7(m)) { return `${m.fileName} uses top-level await inside the cycle`; } for (const stmt of m.statements.nodes) { if (ts.isExportAssignment(stmt) && !stmt.isExportEquals) { return `the default export expression at ${lineOf(stmt)} has no initialization state inside the cycle`; } if ( ts.isExportDeclaration(stmt) && !stmt.isTypeOnly && (stmt.exportClause === undefined || ts.isNamespaceExport(stmt.exportClause)) ) { const dep = targetOf(stmt); if (dep !== null && members.has(dep)) { return `the namespace re-export at ${lineOf(stmt)} names a module of the cycle`; } } if ( ts.isImportDeclaration(stmt) && stmt.importClause !== undefined && !stmt.importClause.isTypeOnly && stmt.importClause.namedBindings !== undefined && ts.isNamespaceImport(stmt.importClause.namedBindings) ) { const dep = targetOf(stmt); const nsName = stmt.importClause.namedBindings.name; if ( dep !== null && members.has(dep) && !nsBindingUsesAreBareStatements7(program, m, nsName) ) { return `the namespace import '${nsName.text}' at ${lineOf(stmt)} names a module of the cycle`; } } if (ts.isClassDeclaration(stmt) && stmt.name === undefined) { return `the anonymous default class at ${lineOf(stmt)} has no initialization state inside the cycle`; } const ambient = (ts.getCombinedModifierFlags(stmt as unknown as ts.Declaration) & ts.ModifierFlags.Ambient) !== 0; if (ambient) continue; if ( ts.isEnumDeclaration(stmt) && (ts.getCombinedModifierFlags(stmt) & ts.ModifierFlags.Const) === 0 ) { return `the enum '${stmt.name.text}' at ${lineOf(stmt)} has no initialization state inside the cycle`; } if (ts.isModuleDeclaration(stmt) && !typeOnlyNamespace7(stmt)) { return `the namespace at ${lineOf(stmt)} has no initialization state inside the cycle`; } } } return null; } /** A module-scope binding of an exactly-initialized cycle that some read * or write can reach before its declaration ran. */ export interface CycleEarlyBinding { symbol: ts.Symbol; /** The declaration's name. */ name: ts.Identifier; kind: "lexical" | "var" | "class"; module: ts.SourceFile; /** One reference that can run before the declaration. */ reference: ts.Identifier; /** A reference in a position the lowering may resolve at compile time * from the binding's type (a property key, a `typeof` operand). */ folded?: ts.Identifier; /** Modules whose TOP-LEVEL code runs only after the declaration ran: * modules outside the cycle and cycle members evaluated later. */ settled: ReadonlySet; /** The local name each importing file gives the binding when it differs * from the declared one, by file name: Node's ReferenceError names it. */ localNames?: ReadonlyMap; } /** Every binding of the program's exactly-initialized cycles that can be * observed before its declaration executes. `order` is the compiled module * order; `singleRoot` is false when more than one module graph root runs * (fork or worker entries), so a cycle can be entered at different * members. With one root and a purely ES-module graph the evaluation order * is static and the analysis is precise up to "user code may run": a * binding needs its check when its declaration completes after some * top-level code that can call user functions and a deferred position * (function body, parameter default, instance field) references it, or * when the top level of a cycle member evaluated before it reads it. * Otherwise every deferred reference and every top-level reference from * another member counts. */ export function cycleEarlyBindings( program: ts.Program, entry: ts.SourceFile, order: readonly ts.SourceFile[], singleRoot: boolean, ): CycleEarlyBinding[] { const exact = program.analysis.exactCycleMembers; if (exact.size === 0) return []; // Every lowering pass of one compile asks with the same inputs. const key = `${entry.fileName}\0${order.map((sf) => sf.fileName).join("\0")}\0${singleRoot}`; const memo = program.analysis.cycleEarlyBindings; if (memo !== null && memo.key === key) return memo.bindings; const bindings = computeCycleEarlyBindings7(program, entry, order, singleRoot, exact); program.analysis.cycleEarlyBindings = { key, bindings }; return bindings; } function computeCycleEarlyBindings7( program: ts.Program, entry: ts.SourceFile, order: readonly ts.SourceFile[], singleRoot: boolean, exact: ReadonlySet, ): CycleEarlyBinding[] { const inOrder = new Set(order); const members = [...exact].filter((m) => inOrder.has(m)); if (members.length === 0) return []; const memberSet = new Set(members); const depsOf = (sf: ts.SourceFile): ts.SourceFile[] => orderedImportsOf(program, sf).flatMap(({ dep }) => dep !== null && dep !== sf && inOrder.has(dep) ? [dep] : [], ); // Strongly-connected components among the exactly-initialized members. const comps: ts.SourceFile[][] = []; { const index = new Map(); const low = new Map(); const onStack = new Set(); const stack: ts.SourceFile[] = []; let next = 0; const connect = (v: ts.SourceFile): void => { index.set(v, next); low.set(v, next); next++; stack.push(v); onStack.add(v); for (const w of depsOf(v)) { if (!memberSet.has(w)) continue; if (!index.has(w)) { connect(w); low.set(v, Math.min(low.get(v)!, low.get(w)!)); } else if (onStack.has(w)) { low.set(v, Math.min(low.get(v)!, index.get(w)!)); } } if (low.get(v) === index.get(v)) { const comp: ts.SourceFile[] = []; for (;;) { const w = stack.pop()!; onStack.delete(w); comp.push(w); if (w === v) break; } if (comp.length > 1) comps.push(comp); } }; for (const m of members) if (!index.has(m)) connect(m); } if (comps.length === 0) return []; // The static evaluation order: Node's depth-first postorder from the // entry over ES import edges. Known only for one root and a graph with // no CommonJS module (a require() evaluates its target mid-body). const position = new Map(); if ( singleRoot && !order.some((sf) => isCjsJsFile7(sf, program) || /\.c[jt]s$/.test(sf.fileName)) ) { const seen = new Set(); const visit = (sf: ts.SourceFile): void => { seen.add(sf); for (const dep of depsOf(sf)) if (!seen.has(dep)) visit(dep); position.set(sf, position.size); }; visit(entry); } const checker = program.getTypeChecker(); const nonInert = topLevelInertness7(program); const out: CycleEarlyBinding[] = []; for (const comp of comps) { const known = comp.every((m) => position.has(m)); const evaluation = known ? comp.slice().sort((a, b) => position.get(a)! - position.get(b)!) : comp.slice(); const candidates = new Map(); const names = new Set(); let dirty = !known; evaluation.forEach((m, rank) => { for (const stmt of m.statements.nodes) { const offence = nonInert(stmt); const exposed = dirty || offence !== null; const declared: { name: ts.Identifier; kind: CycleEarlyBinding["kind"] }[] = []; if ( ts.isVariableStatement(stmt) && (ts.getCombinedModifierFlags(stmt as unknown as ts.Declaration) & ts.ModifierFlags.Ambient) === 0 ) { const lexical = (stmt.declarationList.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)) !== 0; for (const d of stmt.declarationList.declarations.nodes) { for (const name of boundNames7(d.name)) { declared.push({ name, kind: lexical ? "lexical" : "var" }); } } } else if (ts.isClassDeclaration(stmt) && stmt.name !== undefined) { declared.push({ name: stmt.name, kind: "class" }); } for (const { name, kind } of declared) { const symbol = checker.getSymbolAtLocation(name); if (symbol === undefined) continue; candidates.set(symbol, { symbol, name, kind, module: m, exposed, rank, start: ts.tokenStartOfNode(stmt), end: stmt.end, reference: null, deferredReference: null, folded: null, localNames: new Map(), }); names.add(name.text); } if (offence !== null) dirty = true; } }); if (candidates.size === 0) continue; const rankOf = new Map(evaluation.map((m, i) => [m, i] as const)); // References: every identifier spelled like a candidate or like a // local name the file imports one under (`import { x as y }`, a // default import), resolved through import aliases to the // declaration's symbol. for (const sf of order) { const local = new Set(); for (const stmt of sf.statements.nodes) { if (!ts.isImportDeclaration(stmt) || stmt.importClause === undefined) continue; const clause = stmt.importClause; const bound: ts.Identifier[] = clause.name !== undefined ? [clause.name] : []; if (clause.namedBindings !== undefined && ts.isNamedImports(clause.namedBindings)) for (const element of clause.namedBindings.elements.nodes) bound.push(element.name); for (const name of bound) { if (names.has(name.text)) continue; const imported = checker.getSymbolAtLocation(name); const candidate = imported === undefined ? undefined : candidates.get(checker.getAliasedSymbol(imported)); if (candidate === undefined) continue; local.add(name.text); candidate.localNames.set(sf.fileName, name.text); } } const refs: ts.Identifier[] = []; ts.walkPreorder(sf, (node) => { if (ts.isImportDeclaration(node)) return "skip"; if (ts.isIdentifier(node) && (names.has(node.text) || local.has(node.text))) refs.push(node); return undefined; }); if (refs.length === 0) continue; checker.prefetchSymbolNodesExact(refs); for (const ref of refs) { const parent = ref.parent; if (parent !== undefined && ts.isExportSpecifier(parent)) continue; if (inTypePosition7(ref)) continue; let symbol = checker.getSymbolAtLocation(ref); if ( parent !== undefined && ts.isShorthandPropertyAssignment(parent) && parent.name === ref ) { symbol = checker.getShorthandAssignmentValueSymbol(parent) ?? symbol; } if (symbol !== undefined && symbol.flags & ts.SymbolFlags.Alias) { symbol = checker.getAliasedSymbol(symbol); } const c = symbol === undefined ? undefined : candidates.get(symbol); if (c === undefined || ref === c.name) continue; if (c.folded === null && foldablePosition7(ref)) c.folded = ref; if (c.reference !== null) continue; // A class body names its own inner binding, initialized throughout. if ( c.kind === "class" && sf === c.module && ts.tokenStartOfNode(ref) >= c.start && ref.end <= c.end ) continue; if (inDeferredPosition7(ref)) { if (c.exposed && c.deferredReference === null) c.deferredReference = ref; continue; } let early: boolean; if (sf === c.module) early = ts.tokenStartOfNode(ref) < c.start; else if (!memberSet.has(sf) || !rankOf.has(sf)) early = false; else early = !known || rankOf.get(sf)! < c.rank; if (early) c.reference = ref; } } // With a static evaluation order, a deferred reference only runs early // when the code that runs before the declaration completes can reach // the function holding it. const reached = known && [...candidates.values()].some((c) => c.reference === null && c.deferredReference) ? cycleReachedDeferredReferences7(program, evaluation, candidates) : null; for (const c of candidates.values()) { if (c.reference === null && c.deferredReference !== null) c.reference = reached === null ? c.deferredReference : (reached.get(c.symbol) ?? null); if (c.reference === null) continue; const settled = new Set(order.filter((sf) => !rankOf.has(sf))); if (known) for (const m of evaluation.slice(c.rank + 1)) settled.add(m); out.push({ symbol: c.symbol, name: c.name, kind: c.kind, module: c.module, reference: c.reference, ...(c.folded !== null ? { folded: c.folded } : {}), settled, ...(c.localNames.size > 0 ? { localNames: c.localNames } : {}), }); } } return out; } /** Collects into `units` the callables an initializer expression stores * (see moduleEarlyBindingsOf7), recording in `immediate` the parts of a * stored node that run where it is evaluated. */ function storedCallables7( checker: ts.TypeChecker, e: ts.Expression, units: ts.Node[], immediate: Map, ): void { while ( ts.isParenthesizedExpression(e) || ts.isAsExpression(e) || ts.isSatisfiesExpression(e) || ts.isNonNullExpression(e) ) e = e.expression; if (ts.isArrowFunction(e) || ts.isFunctionExpression(e)) { units.push(e); } else if (ts.isClassExpression(e)) { units.push(e); immediate.set(e, classSyncParts7(e)); } else if (ts.isObjectLiteralExpression(e)) { for (const p of e.properties.nodes) { if ( ts.isMethodDeclaration(p) || ts.isGetAccessorDeclaration(p) || ts.isSetAccessorDeclaration(p) ) { // A computed key evaluates where the literal does; a plain // member-chain key (`[Equal.symbol]`) runs no user code. if (!ts.isComputedPropertyName(p.name)) units.push(p); else if (memberChainKey7(p.name.expression)) { units.push(p); immediate.set(p, [p.name]); } } else if (ts.isPropertyAssignment(p)) storedCallables7(checker, p.initializer, units, immediate); } } else if (ts.isArrayLiteralExpression(e)) { for (const el of e.elements.nodes) if (!ts.isSpreadElement(el)) storedCallables7(checker, el, units, immediate); } else if (ts.isCallExpression(e)) { // `const f = wrap(callback)` where wrap only returns closures over // the callback: it runs when the stored result is called. e.arguments.nodes.forEach((arg, index) => { let fn: ts.Expression = arg; while (ts.isParenthesizedExpression(fn)) fn = fn.expression; if ( (ts.isArrowFunction(fn) || ts.isFunctionExpression(fn)) && returnsOnlyClosuresOver7(checker, e.expression, index) ) units.push(fn); }); } } /** A binding of an import cycle and what the reference scan found. */ interface CycleCandidate7 { symbol: ts.Symbol; name: ts.Identifier; kind: CycleEarlyBinding["kind"]; module: ts.SourceFile; /** User code can run between the cycle's start and the end of the * declaration (always true when the order is unknown). */ exposed: boolean; rank: number; start: number; end: number; /** A reference read at top level before the declaration ran. */ reference: ts.Identifier | null; /** A deferred reference (function body, parameter default, instance * field) when user code may run before the declaration completes. */ deferredReference: ts.Identifier | null; folded: ts.Identifier | null; localNames: Map; } /** For one import cycle with a static evaluation order: the first deferred * reference to each candidate that code running before the candidate's * declaration completes can reach, or null when the walk cannot be precise * (top-level await, a default-exported function or class declaration). * * The cycle's top-level code runs in evaluation order. A stored callable * (a function or class declaration, or a function, class or object-literal * method stored by a variable initializer) becomes reachable once * reachable code names its binding, through an import alias or a * namespace member too; every other function-like in reachable code counts * as reachable, since it may be handed to a callee. A namespace import of * a cycle member used other than for a member read makes every stored * callable reachable. moduleEarlyBindingsOf7 is the single-module form. */ function cycleReachedDeferredReferences7( program: ts.Program, evaluation: readonly ts.SourceFile[], candidates: ReadonlyMap, ): Map | null { const checker = program.getTypeChecker(); const members = new Set(evaluation); for (const sf of evaluation) { if (hasTopLevelAwait7(sf)) return null; for (const stmt of sf.statements.nodes) { if ( (ts.isFunctionDeclaration(stmt) || ts.isClassDeclaration(stmt)) && (ts.getCombinedModifierFlags(stmt as unknown as ts.Declaration) & ts.ModifierFlags.Default) !== 0 ) return null; } } const stored = new Set(); const immediate = new Map(); const storedNames: ts.Identifier[] = []; const unitsOfName = new Map(); const store = (name: ts.Identifier, units: ts.Node[]): void => { if (units.length === 0) return; for (const u of units) stored.add(u); storedNames.push(name); unitsOfName.set(name, units); }; for (const sf of evaluation) { for (const stmt of sf.statements.nodes) { if ( (ts.getCombinedModifierFlags(stmt as unknown as ts.Declaration) & ts.ModifierFlags.Ambient) !== 0 ) continue; if (ts.isFunctionDeclaration(stmt) && stmt.name !== undefined && stmt.body !== undefined) { store(stmt.name, [stmt]); } else if (ts.isClassDeclaration(stmt) && stmt.name !== undefined) { store(stmt.name, [stmt]); } else if (ts.isVariableStatement(stmt)) { for (const d of stmt.declarationList.declarations.nodes) { if (ts.isIdentifier(d.name) && d.initializer !== undefined) { const units: ts.Node[] = []; storedCallables7(checker, d.initializer, units, immediate); store(d.name, units); } } } } } const unitsBySymbol = new Map(); const allUnits: ts.Node[] = []; for (const name of storedNames) { const symbol = checker.getSymbolAtLocation(name); if (symbol === undefined) continue; const units = unitsOfName.get(name)!; unitsBySymbol.set(symbol, [...(unitsBySymbol.get(symbol) ?? []), ...units]); allUnits.push(...units); } // A named class expression's inner name (`class Impl { static x = // Impl.m(); }`) names the class inside its own body, as the binding it // is stored in names it outside. const innerNames: ts.Identifier[] = []; for (const unit of allUnits) { if (!ts.isClassExpression(unit) || unit.name === undefined) continue; const symbol = checker.getSymbolAtLocation(unit.name); if (symbol === undefined) continue; innerNames.push(unit.name); unitsBySymbol.set(symbol, [...(unitsBySymbol.get(symbol) ?? []), unit]); } // The identifier texts worth resolving: declared names, and the local // names of imports (named, default and namespace) in cycle members. const names = new Set(); for (const c of candidates.values()) names.add(c.name.text); for (const name of storedNames) names.add(name.text); for (const name of innerNames) names.add(name.text); for (const sf of evaluation) { for (const stmt of sf.statements.nodes) { if (!ts.isImportDeclaration(stmt) || stmt.importClause === undefined) continue; const clause = stmt.importClause; if (clause.name !== undefined) names.add(clause.name.text); const bindings = clause.namedBindings; if (bindings === undefined) continue; if (ts.isNamedImports(bindings)) for (const element of bindings.elements.nodes) names.add(element.name.text); else names.add(bindings.name.text); } } const idents: ts.Identifier[] = []; for (const sf of evaluation) { ts.walkPreorder(sf, (node) => { if (ts.isImportDeclaration(node)) return "skip"; if (ts.isIdentifier(node) && names.has(node.text)) idents.push(node); return undefined; }); } checker.prefetchSymbolNodesExact(idents); const symbolOf = (id: ts.Identifier): ts.Symbol | undefined => { const parent = id.parent; let symbol = checker.getSymbolAtLocation(id); if (parent !== undefined && ts.isShorthandPropertyAssignment(parent) && parent.name === id) { symbol = checker.getShorthandAssignmentValueSymbol(parent) ?? symbol; } if (symbol !== undefined && symbol.flags & ts.SymbolFlags.Alias) { symbol = checker.getAliasedSymbol(symbol); } return symbol; }; const isMemberNamespace = (symbol: ts.Symbol): boolean => (symbol.flags & ts.SymbolFlags.ValueModule) !== 0 && symbol.declarations.some((d) => ts.isSourceFile(d) && members.has(d)); const declared = new Set(); const reachedUnits = new Set(); const out = new Map(); const work: ts.Node[] = []; const reach = (unit: ts.Node): void => { if (reachedUnits.has(unit)) return; reachedUnits.add(unit); work.push(unit); }; const visit = (root: ts.Node): void => { ts.walkPreorder(root, (node) => { if (node !== root && stored.has(node)) { for (const part of immediate.get(node) ?? []) visit(part); return "skip"; } if (ts.isImportDeclaration(node)) return "skip"; if (node.kind === ts.SyntaxKind.ThisKeyword) { // `this` in a static initializer or block names the class, so it // reaches the class's methods as naming the class does. const owner = staticThisClass7(node); if (owner !== undefined && stored.has(owner)) reach(owner); return undefined; } if (!ts.isIdentifier(node) || !names.has(node.text)) return undefined; if (inTypePosition7(node)) return undefined; const parent = node.parent; if (parent !== undefined && ts.isExportSpecifier(parent)) return undefined; if (parent !== undefined && declaredName7(parent) === node) return undefined; const symbol = symbolOf(node); if (symbol === undefined) return undefined; if (isMemberNamespace(symbol)) { // `ns.member` resolves through the member's own identifier; any // other use can reach every export. if ( !( parent !== undefined && ts.isPropertyAccessExpression(parent) && parent.expression === node ) ) for (const unit of allUnits) reach(unit); return undefined; } const c = candidates.get(symbol); if ( c !== undefined && node !== c.name && !declared.has(symbol) && !out.has(symbol) && inDeferredPosition7(node) && !( c.kind === "class" && ts.getSourceFileOfNode(node) === c.module && ts.tokenStartOfNode(node) >= c.start && node.end <= c.end ) ) out.set(symbol, node); for (const unit of unitsBySymbol.get(symbol) ?? []) reach(unit); return undefined; }); }; const drain = (): void => { while (work.length > 0) visit(work.pop()!); }; const declare = (name: ts.BindingName): void => { for (const id of boundNames7(name)) { const symbol = checker.getSymbolAtLocation(id); if (symbol !== undefined) declared.add(symbol); } }; for (const sf of evaluation) { for (const stmt of sf.statements.nodes) { if (ts.isClassDeclaration(stmt)) { for (const part of classSyncParts7(stmt)) visit(part); drain(); if (stmt.name !== undefined) declare(stmt.name); } else if (ts.isVariableStatement(stmt)) { for (const d of stmt.declarationList.declarations.nodes) { visit(d); drain(); declare(d.name); } } else if (!ts.isFunctionDeclaration(stmt)) visit(stmt); drain(); } } return out; } /** Top-level let/const and class bindings, in modules outside the * exactly-initialized cycles, that code running before their declaration * completes can read. * Function declarations are callable from the module's start, so a call * made by earlier top-level code (or by the declaration's own initializer) * can observe the temporal dead zone. Reachability is syntactic: a stored * callable (a function or class declaration, or a function, class or * object-literal method stored by a variable initializer) becomes reachable * once reachable code names its binding, and every other function-like in * reachable code counts as reachable because it may be handed to a callee. * A binding is reported only when one of its references in a deferred * position is reachable before the declaration ran, so modules that call * nothing early report nothing. Bindings of exactly-initialized cycles are * covered by cycleEarlyBindings. */ export function moduleEarlyBindings( program: ts.Program, order: readonly ts.SourceFile[], ): CycleEarlyBinding[] { const key = order.map((sf) => sf.fileName).join("\0"); const memo = program.analysis.moduleEarlyBindings; if (memo !== null && memo.key === key) return memo.bindings; const exact = program.analysis.exactCycleMembers; const out: CycleEarlyBinding[] = []; for (const sf of order) { if (sf.isDeclarationFile || exact.has(sf)) continue; out.push(...moduleEarlyBindingsOf7(program, sf, order)); } program.analysis.moduleEarlyBindings = { key, bindings: out }; return out; } function moduleEarlyBindingsOf7( program: ts.Program, sf: ts.SourceFile, order: readonly ts.SourceFile[], ): CycleEarlyBinding[] { const ambient = (stmt: ts.Statement): boolean => (ts.getCombinedModifierFlags(stmt as unknown as ts.Declaration) & ts.ModifierFlags.Ambient) !== 0; // Candidate bindings, by declaring statement. interface Candidate { name: ts.Identifier; index: number; start: number; /** A class declaration: its own body sees the inner binding. */ cls?: ts.ClassDeclaration; } const candidates: Candidate[] = []; const candidateNames = new Set(); // Stored callables, by the name of the binding that stores them. const stored = new Set(); const storedBy = new Map(); // Parts of a stored node that run where it is evaluated: a class's // heritage, computed keys, decorators and static initializers, and a // stored method's computed key. const immediate = new Map(); const store = (name: ts.Identifier, units: ts.Node[]): void => { if (units.length === 0) return; for (const u of units) stored.add(u); const list = storedBy.get(name.text) ?? []; list.push({ name, units }); storedBy.set(name.text, list); }; const checker = program.getTypeChecker(); const storedIn = (e: ts.Expression, units: ts.Node[]): void => storedCallables7(checker, e, units, immediate); sf.statements.nodes.forEach((stmt, index) => { if (ambient(stmt)) return; if (ts.isFunctionDeclaration(stmt) && stmt.name !== undefined && stmt.body !== undefined) { store(stmt.name, [stmt]); } else if (ts.isClassDeclaration(stmt) && stmt.name !== undefined) { store(stmt.name, [stmt]); candidates.push({ name: stmt.name, index, start: ts.tokenStartOfNode(stmt), cls: stmt }); candidateNames.add(stmt.name.text); } else if (ts.isVariableStatement(stmt)) { const lexical = (stmt.declarationList.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)) !== 0; for (const d of stmt.declarationList.declarations.nodes) { if (ts.isIdentifier(d.name) && d.initializer !== undefined) { const units: ts.Node[] = []; storedIn(d.initializer, units); store(d.name, units); } if (!lexical) continue; for (const name of boundNames7(d.name)) { candidates.push({ name, index, start: ts.tokenStartOfNode(stmt) }); candidateNames.add(name.text); } } } }); if (candidates.length === 0) return []; // Resolve every identifier that could name a candidate or a stored // callable in one batch. const idents: ts.Identifier[] = []; ts.walkPreorder(sf, (node) => { if (ts.isImportDeclaration(node)) return "skip"; if (ts.isIdentifier(node) && (candidateNames.has(node.text) || storedBy.has(node.text))) idents.push(node); return undefined; }); checker.prefetchSymbolNodesExact(idents); const symbolOf = (id: ts.Identifier): ts.Symbol | undefined => { const parent = id.parent; let symbol = checker.getSymbolAtLocation(id); if (parent !== undefined && ts.isShorthandPropertyAssignment(parent) && parent.name === id) { symbol = checker.getShorthandAssignmentValueSymbol(parent) ?? symbol; } if (symbol !== undefined && symbol.flags & ts.SymbolFlags.Alias) { symbol = checker.getAliasedSymbol(symbol); } return symbol; }; // A callback handed to a timer, a microtask queue, a promise reaction or // a host (Node) API runs as a later job or event. Without top-level await // the module's body completes before any job runs, so such a callback is // never early. JavaScript builtins (array callbacks, the promise // executor, replacers) and the host APIs that call back synchronously // keep the conservative rule. if (!hasTopLevelAwait7(sf)) { const callbacks = (e: ts.Expression): void => { while (ts.isParenthesizedExpression(e)) e = e.expression; if (ts.isArrowFunction(e) || ts.isFunctionExpression(e)) stored.add(e); else if (ts.isObjectLiteralExpression(e)) { for (const p of e.properties.nodes) { if (ts.isMethodDeclaration(p)) stored.add(p); else if (ts.isPropertyAssignment(p)) callbacks(p.initializer); } } }; ts.walkPreorder(sf, (node) => { if ( (ts.isCallExpression(node) || ts.isNewExpression(node)) && laterJobCallee7(checker, node.expression) ) { for (const arg of node.arguments?.nodes ?? []) callbacks(arg); } return undefined; }); } const unitsBySymbol = new Map(); for (const list of storedBy.values()) { for (const { name, units } of list) { const symbol = checker.getSymbolAtLocation(name); if (symbol === undefined) continue; unitsBySymbol.set(symbol, [...(unitsBySymbol.get(symbol) ?? []), ...units]); } } const candidateSymbols = new Map(); for (const c of candidates) { const symbol = checker.getSymbolAtLocation(c.name); if (symbol !== undefined) candidateSymbols.set(symbol, c); } // Walk reachable code, collecting the candidate references it holds. const reachedUnits = new Set(); const reachedRefs = new Map(); const work: ts.Node[] = []; const visit = (root: ts.Node): void => { ts.walkPreorder(root, (node) => { if (node !== root && stored.has(node)) { for (const part of immediate.get(node) ?? []) visit(part); return "skip"; } if (ts.isImportDeclaration(node)) return "skip"; if (!ts.isIdentifier(node)) return undefined; if (!candidateNames.has(node.text) && !storedBy.has(node.text)) return undefined; if (inTypePosition7(node)) return undefined; const parent = node.parent; if (parent !== undefined && ts.isExportSpecifier(parent)) return undefined; if (parent !== undefined && declaredName7(parent) === node) return undefined; const symbol = symbolOf(node); if (symbol === undefined) return undefined; const c = candidateSymbols.get(symbol); if ( c !== undefined && node !== c.name && !reachedRefs.has(symbol) && !(c.cls !== undefined && ts.tokenStartOfNode(node) >= c.cls.pos && node.end <= c.cls.end) ) { if (inDeferredPosition7(node) || ts.tokenStartOfNode(node) < c.start) reachedRefs.set(symbol, node); } for (const unit of unitsBySymbol.get(symbol) ?? []) { if (reachedUnits.has(unit)) continue; reachedUnits.add(unit); work.push(unit); } return undefined; }); }; const drain = (): void => { while (work.length > 0) visit(work.pop()!); }; const out: CycleEarlyBinding[] = []; const settled = new Set(order.filter((m) => m !== sf)); let next = 0; const report = (declaration: ts.Node): void => { while ( next < candidates.length && ts.tokenStartOfNode(candidates[next]!.name) >= ts.tokenStartOfNode(declaration) && candidates[next]!.name.end <= declaration.end ) { const c = candidates[next++]!; const symbol = checker.getSymbolAtLocation(c.name); const reference = symbol === undefined ? undefined : reachedRefs.get(symbol); if (symbol === undefined || reference === undefined) continue; out.push({ symbol, name: c.name, kind: c.cls !== undefined ? "class" : "lexical", module: sf, reference, settled, }); } }; for (const stmt of sf.statements.nodes) { if (ts.isClassDeclaration(stmt)) { // The binding initializes once the class definition, static // initializers included, has run. for (const part of classSyncParts7(stmt)) visit(part); drain(); report(stmt); } else if (ts.isVariableStatement(stmt)) { // Declarators run in order: a later declarator's initializer cannot // observe an earlier binding before it was initialized. for (const d of stmt.declarationList.declarations.nodes) { visit(d); drain(); report(d); } } else if (!ts.isFunctionDeclaration(stmt)) visit(stmt); drain(); } return out; } /** True when the program function `callee` names reads its parameter * `index` only inside function expressions it returns directly, and never * through `arguments` in its own body: calling it cannot run that argument * before the returned closure is called. */ function returnsOnlyClosuresOver7( checker: ts.TypeChecker, callee: ts.Expression, index: number, ): boolean { while (ts.isParenthesizedExpression(callee)) callee = callee.expression; const name = ts.isIdentifier(callee) ? callee : ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.name) ? callee.name : undefined; if (name === undefined) return false; let symbol = checker.getSymbolAtLocation(name); if (symbol !== undefined && symbol.flags & ts.SymbolFlags.Alias) { symbol = checker.getAliasedSymbol(symbol); } const decls = symbol === undefined ? [] : symbol.declarations; if (decls.length !== 1) return false; const decl = decls[0]!; if (ts.getSourceFileOfNode(decl).isDeclarationFile) return false; let fn: ts.Node | undefined; if (ts.isFunctionDeclaration(decl)) fn = decl; else if (ts.isVariableDeclaration(decl) && decl.initializer !== undefined) { let init: ts.Expression = decl.initializer; while (ts.isParenthesizedExpression(init)) init = init.expression; if (ts.isFunctionExpression(init) || ts.isArrowFunction(init)) fn = init; } if (fn === undefined) return false; const like = fn as ts.FunctionLikeDeclaration; const body = like.body; const param = like.parameters.nodes[index]; if (body === undefined || !ts.isBlock(body) || param === undefined) return false; if (!ts.isIdentifier(param.name) || param.dotDotDotToken !== undefined) return false; if (like.parameters.nodes.slice(0, index).some((p) => p.dotDotDotToken !== undefined)) return false; const paramName = param.name.text; let deferred = true; ts.walkPreorder(body, (node) => { if (!deferred) return "stop"; if (ts.isFunctionLike(node) || ts.isClassExpression(node) || ts.isClassDeclaration(node)) { let child: ts.Node = node; while (ts.isParenthesizedExpression(child.parent)) child = child.parent; const returned = ts.isReturnStatement(child.parent) && child.parent.expression === child; const uses = (part: ts.Node): boolean => { let found = false; ts.walkPreorder(part, (inner) => { if (ts.isIdentifier(inner) && inner.text === paramName) found = true; return found ? "stop" : undefined; }); return found; }; // A returned closure may use the parameter; any other nested // function could escape and run before the result is called. A // returned class still evaluates its heritage, computed keys, // decorators and static initializers when the callee runs. if (returned) { if ( (ts.isClassExpression(node) || ts.isClassDeclaration(node)) && classSyncParts7(node).some(uses) ) deferred = false; return "skip"; } if (uses(node)) deferred = false; return "skip"; } if (ts.isIdentifier(node) && (node.text === paramName || node.text === "arguments")) { if (node !== param.name) deferred = false; } return undefined; }); return deferred; } /** The parts of a class that run where the class is evaluated: heritage * expressions, decorators, computed member keys, static field * initializers and static blocks. Methods and instance fields wait for a * call or a construction. */ /** The class whose static field initializer or static block evaluates `this` * (through arrow functions, which keep the enclosing `this`), or undefined * when another function or a class's instance side binds it. */ function staticThisClass7(node: ts.Node): ts.ClassLikeDeclaration | undefined { let child = node; for (let n = node.parent; n !== undefined; child = n, n = n.parent) { if (ts.isArrowFunction(n)) continue; if (ts.isClassStaticBlockDeclaration(n)) return n.parent as ts.ClassLikeDeclaration; if (ts.isPropertyDeclaration(n)) { if (n.initializer !== child) return undefined; return (ts.getCombinedModifierFlags(n) & ts.ModifierFlags.Static) !== 0 ? (n.parent as ts.ClassLikeDeclaration) : undefined; } if (ts.isComputedPropertyName(n)) { // A computed key evaluates in the scope around the class. const member = n.parent; const cls = member?.parent; if (cls === undefined) return undefined; child = cls; n = cls; continue; } if ( ts.isFunctionLike(n) || ts.isClassDeclaration(n) || ts.isClassExpression(n) || ts.isSourceFile(n) ) return undefined; } return undefined; } function classSyncParts7(c: ts.ClassLikeDeclaration): ts.Node[] { const decorators = (n: ts.Node): ts.Node[] => ((n as { modifiers?: ts.NodeArray }).modifiers?.nodes ?? []).filter( (m) => m.kind === ts.SyntaxKind.Decorator, ); const parts: ts.Node[] = [...decorators(c), ...(c.heritageClauses?.nodes ?? [])]; for (const m of c.members.nodes) { parts.push(...decorators(m)); const memberName = (m as { name?: ts.PropertyName }).name; if (memberName !== undefined && ts.isComputedPropertyName(memberName)) parts.push(memberName); if (ts.isClassStaticBlockDeclaration(m)) parts.push(m); else if ( ts.isPropertyDeclaration(m) && m.initializer !== undefined && (ts.getCombinedModifierFlags(m) & ts.ModifierFlags.Static) !== 0 ) parts.push(m.initializer); } return parts; } /** An identifier or a chain of plain property reads. */ function memberChainKey7(e: ts.Expression): boolean { while (ts.isPropertyAccessExpression(e)) e = e.expression; return ts.isIdentifier(e); } /** The name a declaration node binds, if any. */ function declaredName7(node: ts.Node): ts.Node | undefined { if ( ts.isVariableDeclaration(node) || ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isClassDeclaration(node) || ts.isClassExpression(node) || ts.isBindingElement(node) || ts.isParameter(node) ) return node.name; return undefined; } /** A declared builtin whose callback arguments run only as later jobs or * events: the timers, queueMicrotask, promise reactions, and Node APIs * other than the ones that call back before returning. */ function laterJobCallee7(checker: ts.TypeChecker, callee: ts.Expression): boolean { while (ts.isParenthesizedExpression(callee)) callee = callee.expression; let name: ts.Identifier; if (ts.isIdentifier(callee)) name = callee; else if (ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.name)) { name = callee.name; } else return false; if (SYNC_CALLBACK_HOST_APIS7.has(name.text)) return false; let root: ts.Expression = callee; while (ts.isPropertyAccessExpression(root)) root = root.expression; if (ts.isIdentifier(root) && root.text === "assert") return false; let symbol = checker.getSymbolAtLocation(name); if (symbol !== undefined && symbol.flags & ts.SymbolFlags.Alias) { symbol = checker.getAliasedSymbol(symbol); } if (symbol === undefined) return false; const decls = symbol.declarations; if (decls.length === 0 || !decls.every((d) => ts.getSourceFileOfNode(d).isDeclarationFile)) return false; if (LATER_JOB_BUILTINS7.has(name.text)) return true; // The JavaScript standard library's callbacks mostly run synchronously; // package declarations describe user code. return decls.every((d) => { const file = ts.getSourceFileOfNode(d).fileName; return isNodeTypesPath(file) || file === fallbackDtsPath(); }); } /** Standard-library functions whose callbacks run as later jobs. */ const LATER_JOB_BUILTINS7 = new Set([ "setTimeout", "setInterval", "setImmediate", "queueMicrotask", "then", "catch", "finally", ]); /** Host APIs that can invoke a callback argument before returning. */ const SYNC_CALLBACK_HOST_APIS7 = new Set([ "apply", "bind", "call", "doesNotReject", "doesNotThrow", "emit", "enterWith", "exit", "rejects", "run", "runInAsyncScope", "throws", ]); /** True for a reference whose value the lowering can take from the * binding's checker type instead of its storage: a property key (element * access argument, computed name, `in` operand, through parentheses, * casts and template spans) or a `typeof` operand. */ function foldablePosition7(ref: ts.Identifier): boolean { let child: ts.Node = ref; for (let p: ts.Node | undefined = ref.parent; p !== undefined; p = p.parent) { if ( ts.isParenthesizedExpression(p) || ts.isAsExpression(p) || ts.isSatisfiesExpression(p) || ts.isNonNullExpression(p) || ts.isTemplateSpan(p) || ts.isTemplateExpression(p) ) { child = p; continue; } if (ts.isTypeOfExpression(p)) return true; if (ts.isElementAccessExpression(p)) return p.argumentExpression === child; if (ts.isComputedPropertyName(p)) return true; if (ts.isBinaryExpression(p)) return p.operatorToken.kind === ts.SyntaxKind.InKeyword && p.left === child; return false; } return false; } /** The identifiers a binding name (identifier or destructuring pattern) * declares. */ function boundNames7(name: ts.BindingName): ts.Identifier[] { if (ts.isIdentifier(name)) return [name]; const out: ts.Identifier[] = []; for (const el of name.elements.nodes) { if (ts.isOmittedExpression(el) || el.name === undefined) continue; out.push(...boundNames7(el.name)); } return out; } /** A namespace declaration whose body holds only types (interfaces, type * aliases, nested type-only namespaces) — no runtime object exists. */ function typeOnlyNamespace7(decl: ts.ModuleDeclaration): boolean { let body = decl.body; while (body !== undefined && ts.isModuleDeclaration(body)) body = body.body; if (body === undefined || !ts.isModuleBlock(body)) return true; return body.statements.nodes.every( (s) => ts.isInterfaceDeclaration(s) || ts.isTypeAliasDeclaration(s) || (ts.isModuleDeclaration(s) && typeOnlyNamespace7(s)), ); } /** Resolves an import specifier from `from` to a source file of the * program, or null (unresolvable / outside the program). Exported (as * resolveImport) for the lowering: CommonJS require statements lower to * guarded %init calls of exactly the module preflight resolved here. */ function resolveImport7( program: ts.Program, from: ts.SourceFile, specifier: string, resolutionKind: "import" | "require" = "import", ): ts.SourceFile | null { const resolved = resolveProjectModule(from.fileName, specifier); if (resolved !== null) return program.getSourceFile(resolved) ?? null; const npm = resolveNpmImport7(from.fileName, specifier, resolutionKind); return npm && isNpmStaticPackage(npm.packageName) ? npmStaticProgramDep(program, npm.packageName, npm.typesFile) : null; } function resolveNpmImport7( fromFileName: string, specifier: string, resolutionKind: "import" | "require" = "import", ): { packageName: string; version?: string; typesFile: string } | null { if (isRelativeSpecifier(specifier) || specifier.startsWith("node:")) { return null; } // Node resolves a bare core-module name (`string_decoder`, `events`, // `fs/promises`) to the builtin before it looks in node_modules, so an // installed npm package of the same name never answers it. Prefix-only // builtins (`node:test`) are listed with their prefix, so a bare `test` // still names the package. if (nodeBuiltinNames.has(specifier)) return null; // --provenance-sources: a registered specifier is NOT an npm import — // its attested source compiles as program modules (resolveProjectModule if (provenanceEntryFor(specifier) !== null) return null; // Shimmed and never-loaded packages (package-shims.ts) are not npm // imports either: the shim compiles as a program module, and an unloaded // package's require() is Node's MODULE_NOT_FOUND. if (isShimmedImport(fromFileName, specifier)) return null; const resolved = resolveBareModule(fromFileName, specifier, undefined, resolutionKind); if (!resolved) return null; if (!isNodeModulesPath(resolved.typesFile)) { // A workspace-linked package (the node_modules entry is a symlink into // the project — every monorepo tool's internal-package install): Node // resolves and runs it exactly like any installed package, so it IS an // npm import; the realpath'd answer escaping node_modules must not // refuse the edge. Registered so path-keyed package attribution // (workspace-registry.ts) recognizes the package's real files. Anything else // whose answer left node_modules stays refused. if (resolved.workspaceDir === undefined) return null; registerWorkspacePackage(resolved.packageName, resolved.workspaceDir); } return { packageName: resolved.packageName, ...(resolved.version !== undefined ? { version: resolved.version } : {}), typesFile: resolved.typesFile, }; } function locOf7(node: ts.Node): { file: string; start: number; end: number } { const sf = ts.getSourceFileOfNode(node); return { file: sf.fileName, start: ts.tokenStartOfNode(node), end: node.end }; } /** The program file holding an opted-in package's resolved runtime JS: * the file itself, or its same-stem TypeScript twin. */ function npmStaticProgramFile(program: ts.Program, resolvedFile: string): ts.SourceFile | null { let dep = program.getSourceFile(resolvedFile) ?? null; if (dep === null) { // A workspace-linked package shipping its .ts SOURCE beside the // compiled .js: the checker's resolution preferred the source (it // outranks the sibling .js under bundler resolution), so the program // holds the .ts twin at the same stem — the same module, in its // richer spelling. for (const ext of [".ts", ".tsx", ".mts", ".cts"]) { dep = program.getSourceFile(resolvedFile.replace(/\.(js|mjs|cjs)$/, ext)) ?? null; if (dep !== null) break; } } return dep; } /** The PROGRAM source file an opted-in --npm-static package's resolved * entry maps to, or null with the package marked an offender (resolution * still answering declarations, or the file missing from the type-checked * program — a path-identity mismatch between the two resolvers). */ function npmStaticProgramDep( program: ts.Program, packageName: string, resolvedFile: string, ): ts.SourceFile | null { if (!isJsSourceFileName(resolvedFile)) { reportNpmStaticOffender( packageName, `its import resolves to '${resolvedFile}', not to runtime JavaScript`, ); return null; } const dep = npmStaticProgramFile(program, resolvedFile); if (dep === null) { reportNpmStaticOffender( packageName, `its entry '${resolvedFile}' did not join the type-checked program`, ); return null; } return dep; } /** `const process = require('node:process')` (identifier binding) and the * bare side-effect `require('node:process')`: Node's process module IS * the global process object, so the binding is a stdlib-global alias * (surfaces.ts registers it; reads lower through the process surface) and * the bare load is a no-op. These forms are exempt from the SC1010 * builtin fence; every other shape (destructuring, subpath) keeps it. */ function processModuleAliasRequire7(spec: string, decl: ts.VariableDeclaration | null): boolean { if (spec !== "process" && spec !== "node:process") return false; return decl === null || ts.isIdentifier(decl.name); } /** `import process from 'node:process'`: the ESM spelling of the same * global-object alias as processModuleAliasRequire7. Keep this exemption * deliberately narrow: named and namespace imports need a represented * module namespace, while the default binding is exactly globalThis.process. */ function processModuleAliasImport7(spec: string, stmt: ts.ImportDeclaration): boolean { if (spec !== "process" && spec !== "node:process") return false; const clause = stmt.importClause; return clause !== undefined && clause.name !== undefined && clause.namedBindings === undefined; } /** The whole TS7-lane lifecycle for one entry: spawn (or share) a tsgo * host, build the lowering-world program, run preflight, and release its * snapshot. The caller owns the supplied host and can reuse or close it. */ export function checkPreflightTs7( entryPath: string, host: ts.Ts7Host, ): { diags: ScrDiagnostic[]; moduleOrder: string[] } { const load = loadProgram7(host, resolve(entryPath)); try { const { diags, moduleOrder } = preflight7(load); return { diags, moduleOrder: moduleOrder.map((sf) => sf.fileName) }; } finally { load.disposeAll(); } } function preflight7(load: LoadResult): { diags: ScrDiagnostic[]; moduleOrder: ts.SourceFile[]; startupCrash: StartupCrash | null; inlineRequireCycleCalls: ReadonlySet; moduleEdges?: ReadonlyMap; } { const { program, entry } = load; const diags: ScrDiagnostic[] = [...load.configDiags]; // An explicit entry can itself live in an installed package. Its own // source files are program code, including relative barrels and sibling // modules; their location must not erase the module graph. // Match the full installed directory, not just the package name, and // exclude nested installations so dependencies keep their npm policy. const entryPackageFile = entryPackageFilePredicate(entry.fileName); // Workspace-linked packages register BEFORE the tsc gate. Their files // live at realpaths OUTSIDE node_modules (the monorepo-tool symlink // shape), so nothing path-shaped marks them as npm surface — yet their // shipped JS follows the same foreign-tsconfig rules as node_modules JS. // The lazy // registration inside the import-fence walk below runs AFTER errorsOf, // too late for the gate, so one pass over the program's import edges // registers them up front: every bare specifier any program file loads — // import/export declarations, dynamic import("literal") (tsgo chases // those into the program too; a CLI reaches its workspace sibling exactly // that way), require("literal") — resolved with the own resolver, // workspace answers recorded. { const supportedBuiltins = new Set(SUPPORTED_NODE_MODULES); const probed = new Set(); const probe = (fromFile: string, spec: string): void => { if ( isRelativeSpecifier(spec) || spec.startsWith("#") || spec.startsWith("node:") || supportedBuiltins.has(spec) ) { return; } const key = `${dirname(fromFile)} ${spec}`; if (probed.has(key)) return; probed.add(key); const r = resolveBareModule(fromFile, spec); if (r !== null && r.workspaceDir !== undefined) registerWorkspacePackage(r.packageName, r.workspaceDir); }; for (const sf of program.getImplementationSourceFiles()) { if (sf.isDeclarationFile || sf.fileName.endsWith(".json")) continue; for (const specifier of moduleSourceCandidates(program, sf).specifiers) probe(sf.fileName, specifier); } } const toPassthrough = (d: ts.Diagnostic): ScrDiagnostic => { const message = ts.flattenDiagnosticMessageText(d, "\n"); // File-less (global/options) diagnostics anchor at the entry with a // zero span, exactly like the 5.9.3 lane's `d.start ?? 0` fallback. const file = d.fileName ?? entry.fileName; const start = d.fileName !== undefined ? d.pos : 0; const end = d.fileName !== undefined ? d.end : 0; const diag = tscPassthroughDiag(message, { file, start, end }); const hint = typeWorldHint(d, message, load); if (hint !== null) { diag.typeWorld = true; diag.hint = hint; } return diag; }; /* TS7 checker change (not finding 5, same discipline): tsgo types a * NAMESPACE import as the spec's non-callable module namespace object * even when the module is declared with the callable `export =` shape — * 5.9.3 bound the callable value directly, so `import * as a from * "node:assert"; a(x)` typechecked there and draws TS2349 here. Those * callable module objects are scriptc's OWN declared surface, and calls * through the namespace binding are fenced per site by the lowering (the * assert-fences fixture pins the wording), so tsgo's extra TS2349 on * exactly those callees — an identifier that is a namespace-import * binding of a supported builtin module — is suppressed to the 5.9.3 * verdict. Nothing else rides this: any other non-callable call keeps * tsc's voice in both lanes. */ const builtinNamespaceNames = new Map>(); const namespaceCalleeSuppressed = (p: ts.Program, d: ts.Diagnostic): boolean => { if (d.code !== 2349 || d.fileName === undefined) return false; let names = builtinNamespaceNames.get(d.fileName); if (!names) { names = new Set(); builtinNamespaceNames.set(d.fileName, names); const sf = p.getSourceFile(d.fileName); for (const stmt of sf?.statements?.nodes ?? []) { if (!ts.isImportDeclaration(stmt) || !ts.isStringLiteral(stmt.moduleSpecifier)) continue; if (canonicalBuiltinModule(stmt.moduleSpecifier.text) === null) continue; const bindings = stmt.importClause?.namedBindings; if (bindings && ts.isNamespaceImport(bindings)) names.add(bindings.name.text); } } if (names.size === 0) return false; const sf = p.getSourceFile(d.fileName); return sf !== undefined && names.has(sf.text.slice(d.pos, d.end)); }; const npmStaticFileSuppressed = (d: ts.Diagnostic): boolean => d.fileName !== undefined && npmStaticPackageOfPath(d.fileName) !== null; const unselectedPackageJsFile = (file: string): boolean => isJsSourceFileName(file) && !entryPackageFile(file) && npmStaticPackageOfPath(file) === null && (isNodeModulesPath(file) || workspacePackageOfPath(file) !== null); const nodeModulesJsSuppressed = (d: ts.Diagnostic): boolean => d.fileName !== undefined && unselectedPackageJsFile(d.fileName); /* JSDoc TYPE positions in JS files are documentation Node never reads: * a name-resolution failure THERE (2304/2552 — the pattern's utilities.js * spells a mapped type over a @template name it never declared) types * as the error-any and the per-site fences apply, same stance as the * strictness families above. A 2300 duplicate-identifier PAIR formed by * a @typedef and a same-named class/function is the same story — the * typedef half sits inside the comment, and the code half is suppressed * exactly when its partner (same file, same name) was comment-side, so * a REAL duplicate declaration (both halves in code — Node's own * SyntaxError) keeps tsc's voice. */ const insideBlockComment = (text: string, pos: number): boolean => { const open = text.lastIndexOf("/*", pos); if (open < 0) return false; const close = text.indexOf("*/", open + 2); return close === -1 || close >= pos; }; const jsdocTypeSuppressed = ( p: ts.Program, d: ts.Diagnostic, commentDup: Set, ): boolean => { if (d.fileName === undefined || !isJsSourceFileName(d.fileName)) return false; if (d.code !== 2304 && d.code !== 2552 && d.code !== 2300 && d.code !== 1003) return false; if (d.pos === undefined) return false; const sf = p.getSourceFile(d.fileName); if (!sf) return false; if (insideBlockComment(sf.text, d.pos)) return true; return d.code === 2300 && commentDup.has(`${d.fileName}:${sf.text.slice(d.pos, d.end)}`); }; /* `typeof window` / `typeof self` in a JavaScript file: the operand of * typeof may name a global nothing declares (Node answers "undefined" * and never throws), the feature-detection idiom every isomorphic * package uses, and the guarded uses of that name (`typeof window < "u" * ? window : …`) are code Node never runs. A name the file feature-tests * this way is therefore not a type error in JavaScript; a reached use * still meets the lowering's located refusal. TypeScript files keep the * error and its hint. */ const typeofTestedNameSuppressed = (p: ts.Program, d: ts.Diagnostic): boolean => { if (d.code !== 2304 && d.code !== 2552 && d.code !== 2584) return false; if (d.fileName === undefined || !isJsSourceFileName(d.fileName)) return false; if (d.pos === undefined || d.end === undefined) return false; const sf = p.getSourceFile(d.fileName); if (!sf) return false; const name = sf.text.slice(d.pos, d.end); if (!/^[A-Za-z_$][\w$]*$/.test(name)) return false; return new RegExp(`\\btypeof\\s*\\(?\\s*${name.replace(/\$/g, "\\$")}(?![\\w$])`).test(sf.text); }; const workspaceImplicitAnySuppressed = (p: ts.Program, d: ts.Diagnostic): boolean => { if (d.code !== 7016 || d.fileName === undefined || d.pos === undefined || d.end === undefined) return false; const sf = p.getSourceFile(d.fileName); if (!sf) return false; const spec = sf.text.slice(d.pos, d.end).replace(/^['"]|['"]$/g, ""); const prefix = spec.startsWith("@") ? spec.split("/").slice(0, 2).join("/") : spec.split("/")[0]!; return isWorkspacePackageName(prefix); }; // A JavaScript file that TypeScript checks keeps the type errors that // protect its JSDoc types: a value of another type reaching a typed slot, // a call with the wrong arity, and a subclass member that does not // match its base. const projectCheckJs = load.typeWorld?.projectCheckJs ?? false; const enforcedInCheckedJs = (p: ts.Program, d: ts.Diagnostic): boolean => { if (!CHECKED_JS_ENFORCED_CODES.has(d.code) || d.fileName === undefined) return false; const sf = p.getSourceFile(d.fileName); return sf !== undefined && isCheckedJsSourceFile(sf, projectCheckJs); }; const errorsOf = (p: ts.Program): ts.Diagnostic[] => keptErrors(p, ts.getPreEmitDiagnostics(p)); const keptErrors = (p: ts.Program, all: readonly ts.Diagnostic[]): ts.Diagnostic[] => { // First pass: every comment-side 2300's (file, name) — the partners // the second pass forgives. const commentDup = new Set(); for (const d of all) { if (d.code !== 2300 || d.fileName === undefined || d.pos === undefined || d.end === undefined) continue; if (!isJsSourceFileName(d.fileName)) continue; const sf = p.getSourceFile(d.fileName); if (sf && insideBlockComment(sf.text, d.pos)) commentDup.add(`${d.fileName}:${sf.text.slice(d.pos, d.end)}`); } return all.filter( (d) => d.category === ts.DiagnosticCategory.Error && (!suppressedJsStrictness7(d) || enforcedInCheckedJs(p, d)) && !npmStaticFileSuppressed(d) && !nodeModulesJsSuppressed(d) && (d.fileName === undefined || !isPackageShimFile(d.fileName)) && !namespaceCalleeSuppressed(p, d) && !inferredJsDiagnosticSuppressed(p, d) && !isNpmStaticSubclassArgument(p, d) && !workspaceImplicitAnySuppressed(p, d) && !typeofTestedNameSuppressed(p, d) && !jsdocTypeSuppressed(p, d, commentDup), ); }; // The tsc gate, with a second chance: the LOWERING world's checker // includes the divergence overrides (JSON.parse(): unknown, the Promise // executor shape, ...), which are TIGHTER than the standard lib — a // project that typechecks clean under its own tsc can still error here // (any-typed JSON.parse results, most commonly). Those errors are OURS, // not the project's, so they must not fail preflight: when the lowering // world has errors, the PROJECT world (same program without the override // declarations) is built and consulted. Clean there → preflight passes // and the override-affected sites meet the lowerer's honest fences // instead (the SC1100 checked-cast family). Dirty there → the project- // world errors are the ones reported: they are reproducible with the // project's own tsc, which is what "fix type errors first" asks for. // With JSX sources, the program over the original text answers the tsc // gate; its positions move into the rewritten text the program holds. // Errors only the rewritten text has come from the calls the JSX lowers // to (a factory whose signature does not accept them). const tscWorld = load.tscWorld; const tscErrors = tscWorld === undefined ? errorsOf(program) : keptErrors(tscWorld.program, tscWorld.diagnostics); if (tscErrors.length > 0) { const world = tscWorld === undefined || tscWorld.declWorldFallback === true ? load.projectWorld() : (tscWorld.projectWorld ?? tscWorld.program); for (const d of errorsOf(world)) { const diag = toPassthrough(d); const lowered = tscWorld === undefined ? undefined : load.jsxSources?.get(diag.loc.file); if (lowered !== undefined) { diag.loc = { ...diag.loc, start: lowered.rewrittenPosition(diag.loc.start, false), end: lowered.rewrittenPosition(diag.loc.end, true), }; } diags.push(diag); } } else if (load.jsxSources !== undefined) { for (const file of load.jsxSources.keys()) { const sf = program.getSourceFile(file); if (sf === undefined) continue; const own = [...program.getSyntacticDiagnostics(sf), ...program.getSemanticDiagnostics(sf)]; for (const d of keptErrors(program, own)) { const diag = toPassthrough(d); diag.message = `${diag.message} (in the call this JSX lowers to)`; diags.push(diag); } } } // Preflight and lowering share this CheckerFacade. Claim executable // source files after workspace discovery and the tsc gate but before // structural preflight checker queries, so a miss cannot trigger the old // whole-file sweep (and eagerly fetch every dead body's answers). The // structure wave includes declaration headers and top-level code; // reachable bodies are added by lowering's worklists. const ambient = ambientDtsPath(); const programFiles = program .getImplementationSourceFiles() .filter( (sf) => sf.fileName !== ambient && !sf.isDeclarationFile && !sf.fileName.endsWith(".json") && (!isNodeModulesPath(sf.fileName) || entryPackageFile(sf.fileName) || npmStaticPackageOfPath(sf.fileName) !== null) && !unselectedPackageJsFile(sf.fileName), ); // Files reached only through `import type` / `export type … from` are // checker input, never program modules: Node does not load them, so // their imports and syntax must not fence the program. const resolveRuntimeEdge = ( sf: ts.SourceFile, spec: string, resolutionKind?: "import" | "require", ): ts.SourceFile | null => resolveImport7(program, sf, spec, resolutionKind) ?? npmStaticDepSf7(program, sf, spec, resolutionKind); const typeOnlyFiles = typeOnlyReachedFiles( program, entry, programFiles, () => [ ...forkTargetPaths(program, program.getImplementationSourceFiles()), ...workerTargetPaths(program, program.getImplementationSourceFiles()), ], resolveRuntimeEdge, ); const runtimeFiles = typeOnlyFiles.size === 0 ? programFiles : programFiles.filter((sf) => !typeOnlyFiles.has(sf)); const userFiles = npmStaticActive() ? planNpmStaticReexports( program, entry, runtimeFiles, [ ...createRequireProgramRoots7(program), ...forkTargetPaths(program, program.getImplementationSourceFiles()), ...workerTargetPaths(program, program.getImplementationSourceFiles()), ], resolveRuntimeEdge, ) : runtimeFiles; program.getTypeChecker().prefetchSourceFileStructures(userFiles); // Unselected node_modules JS is not native program source even when // maxNodeModuleJsDepth pulled it into the checker (nodeModulesJsSuppressed). // Node stops at the nearest package.json even when it is malformed, and // an explicit CommonJS scope (or .cjs/.cts extension) disables ambiguous- // file syntax detection entirely. TypeScript's bundler checker models // neither loader rule, so restore them before any module-edge analysis: // one malformed-scope diagnostic per package file, and one pointed syntax // diagnostic per CommonJS source file. const invalidPackageConfigs = new Set(); for (const sf of userFiles) { const invalidConfig = nearestInvalidPackageJsonPath(sf.fileName); if (invalidConfig !== null) { if (!invalidPackageConfigs.has(invalidConfig)) { invalidPackageConfigs.add(invalidConfig); diags.push( invalidPackageConfigDiag(invalidConfig, { file: sf.fileName, start: 0, end: 0 }), ); } continue; } if (!isNodeEsmFile7(sf, program)) { const marker = nodeEsmSyntaxMarker7(sf); if (marker !== null) diags.push(commonJsModuleSyntaxDiag(locOf7(marker))); } } const ambientModules = new Set(SUPPORTED_NODE_MODULES); // Ambient `declare module "name"` declarations anywhere in the program — // exact names and `*` patterns — so the import fence can say "type // surface only, no runtime module" instead of the generic package story. // Computed lazily: the scan only runs when a bare-import fence is about // to fire. let ambientModuleDecls: { names: Set; patterns: string[] } | null = null; const ambientDeclared = (spec: string): boolean => { if (ambientModuleDecls === null) { ambientModuleDecls = { names: new Set(), patterns: [] }; for (const f of program.getSourceFiles()) { for (const s of f.statements.nodes) { if (!ts.isModuleDeclaration(s) || !ts.isStringLiteral(s.name)) continue; const name = s.name.text; if (name.includes("*")) ambientModuleDecls.patterns.push(name); else ambientModuleDecls.names.add(name); } } } if (ambientModuleDecls.names.has(spec)) return true; return ambientModuleDecls.patterns.some((p) => { const star = p.indexOf("*"); return ( spec.length >= p.length - 1 && spec.startsWith(p.slice(0, star)) && spec.endsWith(p.slice(star + 1)) ); }); }; // Module edges. An edge that closes a cycle is judged by the shared // admission engine (makeCycleAdmission): admissible when nothing can // OBSERVE the partially-initialized module through it — the importing // statement binds nothing (side-effect import) or binds only NAMESPACE // objects (initialized at link, never TDZ) whose every use is a bare // expression statement (the no-op lowering; no member is ever read), // or the cycle passes the declaration-only init window analysis (the // benign-cycle admission block above). Node evaluates such cycles // benignly (the revisited module is a cache hit, and no binding read // can hit TDZ or a stale slot), so the guarded %init calls reproduce // its order exactly. Cycles that fail both are judged by the exact // initialization rule: admitted when every early read is modeled by an // initialization check, fenced with SC1016 otherwise. const edges = new Map(); // Import edges Node's RESOLUTION refuses before any module evaluates — // recorded in source order per file (interleaved with the resolved deps // via `pos`) so the Node-order walk below can answer which refusal Node // reports first. Each carries a fallback fence: a refusal in a module // the entry never reaches contributes no startup crash (Node never // resolves it), so it keeps today's compile-time diagnostic instead. interface RefusalCandidate { crash: StartupCrash; fallback: ScrDiagnostic; pos: number; } const refusals = new Map(); const depPositions = new Map(); for (const sf of userFiles) { const deps: CycleEdge[] = []; edges.set(sf, deps); refusals.set(sf, []); depPositions.set(sf, []); for (const stmt of sf.statements.nodes) { if (ts.isImportEqualsDeclaration(stmt)) { // The ENTITY form (`import a = A`, `import x = N.y`) is namespace // alias plumbing — the lowerer resolves references through it and // fences what it cannot honor (lower-namespaces.ts). Only the // require form is a module edge, and its RUNTIME form stays fenced // (under the forced ESNext module world tsc itself rejects it, so // only nested/namespace-internal sites can reach the lowering's // fence); `import type x = require(...)` is pure type surface and // lowers to nothing. if (ts.isExternalModuleReference(stmt.moduleReference) && !stmt.isTypeOnly) { const spec = stmt.moduleReference.expression; if ( spec !== undefined && ts.isStringLiteralLike(spec) && load.externalTypes.has(spec.text) ) { diags.push(externalHostModuleDiag7(spec.text, stmt)); } else { diags.push( unsupportedDiag("SC1013", locOf7(stmt), "import = require(...) assignments"), ); } } continue; } if (ts.isExportAssignment(stmt)) { if (stmt.isExportEquals) { diags.push(unsupportedDiag("SC1012", locOf7(stmt), "export = assignments")); } continue; } if (ts.isExportDeclaration(stmt)) { if (stmt.isTypeOnly || isPrunedNpmReexport(program, stmt)) continue; if (!stmt.moduleSpecifier) continue; const fromSpec = ts.isStringLiteral(stmt.moduleSpecifier) ? stmt.moduleSpecifier.text : ""; if (load.externalTypes.has(fromSpec)) { diags.push(externalHostModuleDiag7(fromSpec, stmt)); continue; } const projectReexport = resolveImport7(program, sf, fromSpec); if (!isRelativeSpecifier(fromSpec) && projectReexport === null) { // NAMED re-exports from a SUPPORTED builtin pass (`export { ok } // from "node:assert"` — a universal re-export facade facade): the // statement binds nothing locally and evaluates nothing (builtins // have no %init), and importers' references chase the alias to // the builtin's own declarations exactly as if they imported the // builtin directly — per-member support stays the lowering's // per-site business. Star and namespace re-exports and package // specifiers keep the fence. if ( canonicalBuiltinModule(fromSpec) !== null && stmt.exportClause !== undefined && ts.isNamedExports(stmt.exportClause) ) { continue; } // NAMED re-exports from an INSTALLED npm package (`export // { isUrl } from "url-or-path"` — the pattern's universal facade, // `export { visitorKeys as default } from "@glimmer/syntax"`): // import-plus-export plumbing. The lowering owns the package // value diagnostic when no compiled source can supply the binding. if ( stmt.exportClause !== undefined && ts.isNamedExports(stmt.exportClause) && !fromSpec.startsWith("#") && resolveNpmImport7(sf.fileName, fromSpec) !== null ) { continue; } diags.push( unsupportedDiag("SC1014", locOf7(stmt), "re-exports from packages or builtin modules"), ); continue; } const reDep = projectReexport; // `export * as ns from "./m"` re-exports the module NAMESPACE // object under a name: importers' `x.ns.member` reads resolve // statically through the same alias machinery as `import * as ns` // (moduleNsSymbolOf follows the chain to the module symbol), so a // RESOLVED user module lowers; everything else keeps the fence — // there is no namespace object to materialize. if (stmt.exportClause && ts.isNamespaceExport(stmt.exportClause)) { if (reDep === null || reDep.fileName.endsWith(".json") || isCjsJsFile7(reDep, program)) { diags.push( unsupportedDiag( "SC1013", locOf7(stmt), "namespace re-exports (export * as ns) of this module form", ), ); continue; } } if (reDep && !reDep.fileName.endsWith(".json")) { deps.push({ dep: reDep, stmt }); depPositions.get(sf)!.push({ dep: reDep, pos: ts.tokenStartOfNode(stmt) }); } continue; } if (!ts.isImportDeclaration(stmt)) continue; if (erasedTypeOnlyImport(stmt)) continue; // Import-like nodes whose specifier the parse could not shape into a // string literal — the jsdoc `/** @import x = require(...) */` // materialization and `import defer type * as ns` both arrive here // with no specifier text. Neither is a runtime module edge (both are // type-only forms), and where the form is malformed TS the SC0001 // passthrough above already names it — never a crash here. const specNode: ts.Expression | undefined = stmt.moduleSpecifier; if (specNode === undefined || !ts.isStringLiteral(specNode)) continue; const spec = specNode.text; if (load.externalTypes.has(spec)) { diags.push(externalHostModuleDiag7(spec, stmt)); continue; } const isRelative = isRelativeSpecifier(spec); const isBare = !isRelative && !ambientModules.has(spec); // Npm packages can use the same static createRequire forms as program // code. Preserve fallback for bundler banners that pass the require // value around or feed it computed specifiers during initialization. if (canonicalBuiltinModule(spec) === "module") { const pkg = npmStaticPackageOfPath(sf.fileName); if (pkg !== null) { const reason = npmStaticModuleImportReason7(program, stmt); if (reason !== null) reportNpmStaticOffender(pkg, reason); } } // An import edge Node's own resolution refuses BEFORE any module // evaluates: recorded as a startup-crash candidate (the Node-order // walk below picks the first one Node would report; the program // compiles to exactly that crash — buildMain) with a compile-fence // fallback for candidates the entry never reaches. The bindings of a // refused edge never link, so every clause fence below is skipped — // their uses meet the lowering's own fences if the code demands // values from them. const refuse = ( message: string, className: StartupCrash["className"], fallbackFeature: string, ): void => { refusals.get(sf)!.push({ crash: { message, className, loc: locOf7(stmt) }, fallback: unsupportedDiag("SC1010", locOf7(stmt), fallbackFeature), pos: ts.tokenStartOfNode(stmt), }); }; // Project modules win before npm fallback: this is the source graph // tsgo type-checked for tsconfig paths, package imports/self-references, // and provenance entries. Ordinary packages still answer null here. const projDep = isBare ? resolveImport7(program, sf, spec) : null; // "#" specifiers can never name an npm package — they are the // imports-field family, resolved by the project arm above. const npm = isBare && projDep === null && !spec.startsWith("#") ? resolveNpmImport7(sf.fileName, spec) : null; if (npm && isNodeTypesPath(npm.typesFile)) { diags.push(unsupportedDiag("SC1010", locOf7(stmt), unsupportedModuleFeatureOf(spec))); continue; } // --npm-static: an opted-in package's import is a PROGRAM-MODULE // edge — its resolution (types stripped) answered the shipped JS, // which allowJs + maxNodeModuleJsDepth pulled into the program, so // the entry joins the module order like any user module and the // clause fences below apply to it as to program JS. A miss (the // resolution still answers declarations, or the file never joined // the program) marks the package an OFFENDER: the frontend's // selection loop rebuilds without it and reports the package refusal. let npmStaticDep: ts.SourceFile | null = null; if (npm !== null) { if (isNpmStaticPackage(npm.packageName)) { npmStaticDep = npmStaticProgramDep(program, npm.packageName, npm.typesFile); if (npmStaticDep === null) continue; } else { // Types resolved, but Node's RUNTIME resolution can still refuse // the edge (a types-only package whose exports target ships no JS, // most commonly): that program is Node's startup crash, not an const refusal = probeNodeImportRefusal(sf.fileName, spec); if (refusal !== null) { refuse( refusal.message, "%Error", `the '${spec}' package (its runtime resolution fails: ${refusal.message})`, ); } continue; } } // PROJECT imports: tsconfig paths and package.json-mediated specifiers // that resolve to the program's own sources. A source answer is an // ordinary user-module edge, exactly like a relative import (the // checker resolved the bindings the same way); a refused resolution is // Node's startup crash with Node's exact message; what keeps a compile // fence is unsupported builtin or types-only surface. if (isBare && npmStaticDep === null) { if (projDep !== null && projDep.isDeclarationFile) { diags.push( unsupportedDiag( "SC1010", locOf7(stmt), `the '${spec}' import (it resolves only to type declarations — there is no runtime implementation to compile)`, ), ); continue; } if (projDep === null) { // node:process's default export is the global process object. It // contributes no module-order edge; the imported binding lowers // through stdlibGlobalNameOf exactly like the bare global. if (processModuleAliasImport7(spec, stmt)) continue; const nodeBuiltin = spec.startsWith("node:") || nodeBuiltinNames.has(spec); if (nodeBuiltin) { diags.push(unsupportedDiag("SC1010", locOf7(stmt), unsupportedModuleFeatureOf(spec))); continue; } if (spec === "#") { refuse( `Invalid module "#" is not a valid internal imports specifier name imported from ${sf.fileName}`, "%TypeError", `the '#' import (Node rejects the bare '#' specifier as invalid)`, ); continue; } if (spec.startsWith("#")) { const target = packageImportTarget(sf.fileName, spec); if (target !== null && !target.startsWith("./") && !target.startsWith("../")) { diags.push( unsupportedDiag( "SC1010", locOf7(stmt), `package.json imports entries that map to other packages (the '${spec}' import maps to '${target}')`, ), ); continue; } const pkgJson = nearestPkgJsonPath(sf.fileName); refuse( `Package import specifier "${spec}" is not defined${pkgJson !== null ? ` in package ${pkgJson}` : ""} imported from ${sf.fileName}`, "%TypeError", `the '${spec}' import (no package.json imports-field entry maps it)`, ); continue; } const refusal = probeNodeImportRefusal(sf.fileName, spec); if (refusal !== null) { const ambientNote = ambientDeclared(spec) ? `the '${spec}' module (it exists only as an ambient 'declare module' type surface — there is no runtime module to import)` : `the '${spec}' package (nothing installed resolves it)`; refuse(refusal.message, "%Error", ambientNote); continue; } // Runtime-resolvable (or a shape the probe stays conservative // about) with no compilable types answer: scriptc's fence. diags.push( unsupportedDiag( "SC1010", locOf7(stmt), ambientDeclared(spec) ? `the '${spec}' module (it exists only as an ambient 'declare module' type surface — there is no runtime module to import)` : `the '${spec}' package (nothing installed resolves it)`, ), ); continue; } } const clause = stmt.importClause; const dep = isRelative ? resolveImport7(program, sf, spec) : (npmStaticDep ?? projDep); const isJson = dep !== null && dep.fileName.endsWith(".json"); // SELF-imports: the bindings alias this module's OWN declarations, // so a TOP-LEVEL read lexically above the aliased declaration is // Node's TDZ ReferenceError at runtime (exactly what tsc's TS2448 // refuses for the DIRECT spelling — imports are exempt from that // check, so the fence restores the same bar). Hoisted function // aliases initialize at link and read fine anywhere; reads inside // function bodies keep the direct spelling's existing policy. if (dep === sf && clause !== undefined) { selfImportTdzFences7(program, sf, clause, diags); } if (clause) { // Default imports of SUPPORTED builtins pass where the spelling is // legal: Node's default export of a CJS builtin IS the module // object, so the binding exposes exactly the namespace-import // surface and the lowering keys the same tables // (builtinNamespaceModuleOf's default-import twin). JS sources // always (Node never asks for interop flags); TS sources when the // project's explicit or implied synthetic-default permission accepts // the spelling. A TS project without that permission keeps the // SC1012 fence. The callable module objects (assert, events, test) // stay allowed everywhere. The process default is the global process // alias, including when combined with named builtin imports. const syntheticDefaultsOn = (program.getCompilerOptions() as { allowSyntheticDefaultImports?: boolean }) .allowSyntheticDefaultImports === true; const defaultOk = spec === "process" || spec === "node:process" || builtinDefaultImportModule(spec) !== null || ((isJsSourceFileName(sf.fileName) || syntheticDefaultsOn) && canonicalBuiltinModule(spec) !== null); if (clause.name && !isJson && dep === null && !defaultOk) { diags.push(unsupportedDiag("SC1012", locOf7(clause.name))); } if (clause.namedBindings && ts.isNamedImports(clause.namedBindings) && isJson) { diags.push( unsupportedDiag( "SC1012", locOf7(clause.namedBindings), "named imports from JSON modules", ), ); } if (clause.namedBindings && ts.isNamespaceImport(clause.namedBindings)) { // `import * as ns from "./m"` of a RESOLVED user module lowers: // ns.member accesses resolve statically through the checker to // the exporter's own registrations (lower-namespaces.ts's // module-namespace paths); the ns OBJECT as a first-class value // fences per site. What keeps a fence here: JSON modules (their // namespace object has no static story — default imports bake // the data), CommonJS JS modules (Node builds their namespace // from its lexer's static analysis, a different surface), // and unresolved specifiers. Supported builtins pass through as // before (their namespace members are the declared surface). if (isJson) { diags.push( unsupportedDiag( "SC1013", locOf7(clause.namedBindings), "namespace imports of JSON modules", ), ); } else if (dep !== null && isCjsJsFile7(dep, program)) { // A CJS namespace binding whose every use is a bare expression // statement (`cjs;` — the corpus's "the import linked" // assertion) carries no surface question: the statement lowers // to Node's no-op, the module still evaluates through its // ordinary init edge, and no member is ever read through the // lexer facade (self-imports of a CJS module included — the // partially-built exports are never observed). Any OTHER use // keeps the fence. if (!nsBindingUsesAreBareStatements7(program, sf, clause.namedBindings.name)) { diags.push( unsupportedDiag( "SC1013", locOf7(clause.namedBindings), "namespace imports of CommonJS modules", ), ); } } else if (dep === null && !(!isRelative && ambientModules.has(spec))) { diags.push(unsupportedDiag("SC1013", locOf7(clause.namedBindings))); } } } if (dep && !isJson) { deps.push({ dep, stmt }); depPositions.get(sf)!.push({ dep, pos: ts.tokenStartOfNode(stmt) }); } } { const jsRequireFile = isJsSourceFileName(sf.fileName); const stmts = sf.statements.nodes; // A binding that code running before the require can reach. Node // initializes it AT the require: a const/let read earlier throws // ReferenceError, which the lowering reproduces with an // initialization check at every reference that can run early // (require-initialization.ts). A var reads undefined there, which // module aliases cannot represent; createRequire bindings have no // checked form either. const earlyRequireFence = ( req: RequireOccurrence7, bindingKind: RequireCallBindingKind7, loc: { file: string; start: number; end: number }, ): boolean => { const early = req.decl !== null ? requireEarlyReference7(program, req.decl) : null; if ( early === null || (bindingKind !== "createRequire" && req.split === undefined && ((req.decl!.parent?.flags ?? 0) & (ts.NodeFlags.Const | ts.NodeFlags.Let)) !== 0) ) { return false; } diags.push( unsupportedDiag( "SC1013", loc, bindingKind === "createRequire" ? `createRequire() binding '${early.text}' below code that can reach it (Node throws ReferenceError on any earlier access, which is not modeled — move the require above the code that can run first)` : req.split !== undefined ? `require() binding '${early.text}' assigned below code that can reach it (Node reads undefined before the assignment runs, which is not modeled — move the assignment above the code that can run first)` : `var require() binding '${early.text}' below code that can reach it (Node reads undefined before the require runs, which is not modeled — declare it with const or let, or move the require above the code that can run first)`, ), ); return true; }; for (let k = 0; k < stmts.length; k++) { const stmt = stmts[k]!; for (const req of requiresOf7(stmt, program)) { const loc = { file: sf.fileName, start: ts.tokenStartOfNode(req.node), end: req.node.end, }; const bindingKind = requireCallBindingKind7(program, req.node); // TypeScript source files join this scan only through the // canonical createRequire bridge. Their ordinary require-shaped // calls retain the existing checker/lowering diagnostics; JS // files keep the full CommonJS scan. if (!jsRequireFile && bindingKind !== "createRequire") continue; if (isNodeEsmFile7(sf, program)) { if (bindingKind !== "createRequire") { diags.push(unsupportedDiag("SC1013", loc, esmRequireFeature7(bindingKind))); continue; } } if (load.externalTypes.has(req.spec)) { continue; } // A JSON document: the require answers its cached parsed object // (json-require.ts), an ordinary value the binding stores, so // every destructuring form lowers as usual. No module edge. const jsonDep = resolveImport7(program, sf, req.spec, "require"); if (jsonDep !== null && jsonDep.fileName.endsWith(".json")) { if (bindingKind === "createRequire") continue; if (ts.isVariableStatement(stmt) && requiresOf7(stmt, program).length > 1) { const others = requiresOf7(stmt, program).some((r) => { const d = resolveImport7(program, sf, r.spec, "require"); return d === null || !d.fileName.endsWith(".json"); }); if (others) { diags.push( unsupportedDiag( "SC1012", loc, "require() of a JSON module declared together with other require() bindings", ), ); continue; } } earlyRequireFence(req, bindingKind, loc); continue; } if (req.decl && ts.isArrayBindingPattern(req.decl.name)) { diags.push(unsupportedDiag("SC1012", loc, "array-destructuring require() bindings")); continue; } if (req.decl && ts.isObjectBindingPattern(req.decl.name)) { // A nested pattern of a builtin binds members of the sub-module // its key names (`const { types: { isPromise } } = // require("util")` — util.types IS util/types). const builtin = canonicalBuiltinModule(req.spec); const plainElements = ( pattern: ts.ObjectBindingPattern, module: string | null, ): boolean => pattern.elements.nodes.every((e) => { if (e.initializer !== undefined || e.dotDotDotToken !== undefined) return false; if (e.name !== undefined && ts.isIdentifier(e.name)) return true; // A nested pattern under a named export of a program module // destructures that export's VALUE at the require // (`const { codes: { ERR_X } } = require("./errors")`): the // inner bindings are ordinary destructured storage. if ( module === null && builtin === null && bindingKind !== "createRequire" && pattern === req.decl!.name && e.name !== undefined && ts.isObjectBindingPattern(e.name) && e.propertyName !== undefined && ts.isIdentifier(e.propertyName) ) return true; if (module === null || e.name === undefined || !ts.isObjectBindingPattern(e.name)) return false; const key = e.propertyName; if (key === undefined || !ts.isIdentifier(key)) return false; const sub = canonicalBuiltinModule(`${module}/${key.text}`); return sub !== null && plainElements(e.name, sub); }); if (!plainElements(req.decl.name, builtin)) { diags.push( unsupportedDiag( "SC1012", loc, builtin !== null ? "require() destructuring with defaults, rest, or nested patterns other than builtin sub-modules" : "require() destructuring with defaults, rest, or nested patterns of program modules (JSON documents and builtin sub-modules destructure fully)", ), ); continue; } } // An npm package served as a builtin (graceful-fs as fs) is never // a program-module edge, even when --npm-static names it. if (npmBuiltinAliasOf(req.spec) !== null) continue; const isRelative = isRelativeSpecifier(req.spec); let dep = resolveImport7(program, sf, req.spec, "require"); if (dep === null && !isRelative) { // --npm-static: a require() of an OPTED-IN package is a // program-module edge exactly like the import-declaration // form above (bundle dists require their workspace siblings — // the same resolution, the same offender discipline on a // miss). const npmReq = !req.spec.startsWith("#") ? resolveNpmImport7(sf.fileName, req.spec, "require") : null; if (npmReq !== null && isNpmStaticPackage(npmReq.packageName)) { dep = npmStaticProgramDep(program, npmReq.packageName, npmReq.typesFile); if (dep === null) continue; // offender recorded — the fallback loop reloads } else { // createRequire's non-program targets belong to its own // lowering, where missing packages become catchable // MODULE_NOT_FOUND throws. Only resolved program targets are // module-order edges in this preflight walk. if (bindingKind === "createRequire") continue; if ( canonicalBuiltinModule(req.spec) === null && !processModuleAliasRequire7(req.spec, req.decl) ) { // A binding-less require of a package NOTHING resolves: // Node throws MODULE_NOT_FOUND at the require site — the // lowering compiles exactly that catchable throw (the // optional-dependency try/require pattern), so no fence. // Binding forms keep the fence: their downstream reads // would need the module that never loads. if (req.decl !== null || probeNodeRequireRefusal(sf.fileName, req.spec) === null) { diags.push(unsupportedDiag("SC1010", loc, unsupportedModuleFeatureOf(req.spec))); } } continue; } } if (earlyRequireFence(req, bindingKind, loc)) continue; if (dep) deps.push({ dep }); } } const nestedBareRequires = nestedBareRequiresOf7(sf); program .getTypeChecker() .prefetchSymbolNodesExact( nestedBareRequires.flatMap((call) => ts.isIdentifier(call.expression) ? [call.expression] : [], ), ); for (const call of nestedBareRequires) { const spec = requireSpecOf7(call)!; const loc = { file: sf.fileName, start: ts.tokenStartOfNode(call), end: call.end }; const bindingKind = requireCallBindingKind7(program, call); if (!jsRequireFile && bindingKind !== "createRequire") continue; if (isNodeEsmFile7(sf, program)) { if (bindingKind !== "createRequire") { diags.push(unsupportedDiag("SC1013", loc, esmRequireFeature7(bindingKind))); continue; } } if (load.externalTypes.has(spec)) { continue; } if (npmBuiltinAliasOf(spec) !== null) continue; const projectDep = resolveImport7(program, sf, spec, "require"); if (projectDep !== null) { // A bare require of a JSON document only parses it: nothing to // evaluate (json-require.ts creates the cached object). if (projectDep.fileName.endsWith(".json")) continue; deps.push({ dep: projectDep }); continue; } if (!isRelativeSpecifier(spec)) { // --npm-static: opted-in packages ride the program-module edge // (the statement-level require branch above). const npmReq = !spec.startsWith("#") ? resolveNpmImport7(sf.fileName, spec, "require") : null; if (npmReq !== null && isNpmStaticPackage(npmReq.packageName)) { const nDep = npmStaticProgramDep(program, npmReq.packageName, npmReq.typesFile); if (nDep !== null) deps.push({ dep: nDep }); continue; } if (bindingKind === "createRequire") continue; if (canonicalBuiltinModule(spec) === null && !processModuleAliasRequire7(spec, null)) { // Binding-less by construction — same require-site throw // channel as the statement-level form above. if (probeNodeRequireRefusal(sf.fileName, spec) === null) { diags.push(unsupportedDiag("SC1010", loc, unsupportedModuleFeatureOf(spec))); } } continue; } } // Inline requires of program modules are module edges like the bare // statements above (cjs-inline-require.ts lowers each to the module's // %init at its position). Only the CommonJS global in a CommonJS file // counts, and targets outside the compiled graph add nothing here: // their use sites keep their existing lowering fences. if (jsRequireFile && !isNodeEsmFile7(sf, program)) { // A split require binding's assignment is a statement-level require // edge (requiresOf7 above), parenthesized or not. const splitCalls = new Set(splitRequireBindings7(program, sf).values()); for (const call of inlineRequiresOf7(sf)) { if (requireCallBindingKind7(program, call) !== "ambient") continue; if (splitCalls.has(call)) continue; const spec = requireSpecOf7(call)!; if (load.externalTypes.has(spec) || canonicalBuiltinModule(spec) !== null) continue; const lazy = inDeferredPosition7(call); const edgeTo = (dep: ts.SourceFile): CycleEdge => lazy ? { dep, lazy: true, call } : { dep, call }; const projectDep = resolveImport7(program, sf, spec, "require"); if (projectDep !== null) { if (!projectDep.fileName.endsWith(".json")) deps.push(edgeTo(projectDep)); continue; } if (isRelativeSpecifier(spec) || spec.startsWith("#")) continue; const npmReq = resolveNpmImport7(sf.fileName, spec, "require"); if (npmReq !== null && isNpmStaticPackage(npmReq.packageName)) { const nDep = npmStaticProgramDep(program, npmReq.packageName, npmReq.typesFile); if (nDep !== null) deps.push(edgeTo(nDep)); } } } } } // Node's evaluation order: depth-first postorder from the entry, each // module once, back-edges answered from the cache (a module already // evaluating is skipped) — which the guarded %init calls reproduce // exactly. Cycles are admitted where Node's partially-initialized // semantics are UNOBSERVABLE or exactly modeled (JS's partial // initialization otherwise misbehaves without a diagnostic, so SC1016 // stays for the rest): // - a DIRECT SELF-IMPORT (the package self-name reference resolving to // the importing module itself — Node's self-reference rule): the // imported bindings alias the module's OWN top-level bindings with // identical timing, evaluation happens once — always benign; // - a back edge the shared admission engine (makeCycleAdmission) // clears: the cheap per-edge rule (nothing binds, or only namespace // objects bound to bare-statement uses — no read can hit TDZ or a // stale slot) or the DECLARATION-ONLY INIT WINDOW analysis (the // benign-cycle admission block: every module of the cycle's // strongly-connected component has an inert top level and the // closing edge's bindings are used only in deferred positions, so no // read can execute before every member initialized); // - the EXACT INITIALIZATION rule: an ES-module cluster whose early // reads are all let/const bindings with module storage — the // lowering checks each one (cycleEarlyBindings) and throws Node's // ReferenceError on a read before the declaration ran. const cycleAdmissionReason = makeCycleAdmission(program, (sf) => { const all = edges.get(sf) ?? []; return all.some((e) => e.call !== undefined) ? all.filter((e) => e.call === undefined) : all; }); const order: ts.SourceFile[] = []; const inlineRequireCycleCalls = new Set(); const state = new Map(); const stack: string[] = []; const visit = (sf: ts.SourceFile): void => { state.set(sf, "visiting"); stack.push(sf.fileName); for (const e of edges.get(sf) ?? []) { if (e.dep === sf) continue; // self-import: a Node cache hit, not a cycle if (e.lazy === true) continue; // visited after this module completes const s = state.get(e.dep); if (s === "done") continue; if (s === "visiting") { if (e.call !== undefined) { inlineRequireCycleCalls.add(e.call); continue; } // A back-edge: Node answers it from the cache (the module is // already evaluating) — admitted by the cheap per-edge rule, the // declaration-only init window rule, or the exact initialization // rule. const reason = cycleAdmissionReason(sf, e); if (reason !== null) { const cycleStart = stack.indexOf(e.dep.fileName); const cycle = [...stack.slice(cycleStart), e.dep.fileName].join(" → "); diags.push( unsupportedDiag( "SC1016", { file: e.dep.fileName, start: 0, end: 0 }, `circular imports (${cycle}; ${reason})`, ), ); } continue; } visit(e.dep); } stack.pop(); state.set(sf, "done"); order.push(sf); // Lazy inline requires run after this module's top level: their // modules join the order afterwards (cache hits when already loaded). for (const e of edges.get(sf) ?? []) { if (e.lazy === true && state.get(e.dep) === undefined) visit(e.dep); } }; visit(entry); // The startup crash Node's RESOLUTION phase reports: modules resolve // their request specifiers in source order, children linking depth-first // as each resolves (ModuleJob.syncLink) — a preorder walk over the // resolved edges, statement-interleaved, so the first refusal found here // is the one Node reports. Refusal candidates in modules the entry never // reaches keep their compile-fence fallback instead (Node never resolves // them). A found resolution crash wins over the CJS link check below: // Node's fetch/resolve phase runs before instantiate. let resolveCrash: StartupCrash | null = null; { const seen = new Set(); const walk = (sf: ts.SourceFile): StartupCrash | null => { if (seen.has(sf)) return null; seen.add(sf); const steps: { pos: number; dep?: ts.SourceFile; crash?: StartupCrash }[] = [ ...(depPositions.get(sf) ?? []), ...(refusals.get(sf) ?? []).map((r) => ({ pos: r.pos, crash: r.crash })), ].sort((a, b) => a.pos - b.pos); for (const step of steps) { if (step.crash !== undefined) return step.crash; if (step.dep !== undefined && step.dep !== sf) { const hit = walk(step.dep); if (hit !== null) return hit; } } return null; }; resolveCrash = walk(entry); // The crash channel engages only for programs that OTHERWISE pass // preflight: when any other diagnostic already fails the build, every // refusal keeps its compile-fence fallback instead — the report names // each import's story, so edges whose crash will never compile still // get a diagnostic. (No reachable refusal → same: fallbacks only.) if (resolveCrash === null || diags.length > 0) { resolveCrash = null; for (const cands of refusals.values()) { for (const c of cands) diags.push(c.fallback); } } } const linkCrash = resolveCrash !== null ? null : earlierLinkCrash( order, cjsNamedImportLinkCheck(program, entry, order, diags), esmNamedImportLinkCheck(program, entry, order, diags), ); return { diags, moduleOrder: order, startupCrash: resolveCrash ?? linkCrash, inlineRequireCycleCalls, moduleEdges: edges, }; } /* ── the CJS named-import link check ───────────────────────────────────── * Node builds a CommonJS module's ESM facade from its LEXER (cjs-lexer.ts * mirrors it), never from execution — so a named import the checker binds * happily can still fail Node's instantiate phase with a SyntaxError, * thrown before ANY module in the graph evaluates. The checker's word is * NOT Node's word exactly here, and shipping the checker's answer would * diverge from Node without a diagnostic (`module.exports = { a: 7 }` served `a` where * Node refuses the program). This walk asks Node's question at compile * time, in Node's exact ORDER, and answers with Node's exact message. */ interface BadCjsImport { /** The export name requested from the CJS module. */ exportName: string; /** The import specifier as written. */ spec: string; /** The failing name token (`a` in `import { a }` / `export { a } from`, * the PROPERTY name in the aliased forms). */ nameNode: ts.Node; sf: ts.SourceFile; } /** Walks the entry's ESM instantiate graph exactly as V8 does — children * in request order, each module's own imports checked AFTER its children * (postorder), regular imports sorted by LOCAL name (code units), then * `export ... from` re-exports in source order — and answers the first * named import of a lexer-INVISIBLE CommonJS export as Node's link-time * SyntaxError (the caller compiles the program to that startup crash). * ESM modules OUTSIDE the instantiate graph (reached only through * require() of an ES module from CommonJS) instantiate mid-evaluation in * Node, where the SyntaxError interleaves with output already produced — * not modeled: those get a pointed fence instead. */ function cjsNamedImportLinkCheck( program: ts.Program, entry: ts.SourceFile, moduleOrder: ts.SourceFile[], diags: ScrDiagnostic[], ): StartupCrash | null { const lexMemo = new Map>(); // Project specifiers resolve as ever; aliases and package.json-mediated // edges (#alias/self-name) join relative imports so // a named import THROUGH one of a CommonJS module keeps Node's lexer // check. Builtin/npm targets contribute nothing here — except opted-in // --npm-static packages, whose CJS entries face Node's lexer exactly // like program CJS files (their JS IS the program now). const resolveEdge = (from: ts.SourceFile, spec: string): ts.SourceFile | null => { const project = resolveImport7(program, from, spec); if (project !== null) return project; const npmStatic = npmStaticDepSf7(program, from, spec); return npmStatic; }; // Reexport targets union in only when they resolve to CommonJS program // files (Node's cjsPreparseModuleExports rule). const resolveCjsDep = (from: ts.SourceFile, spec: string): ts.SourceFile | null => { const dep = resolveImport7(program, from, spec, "require") ?? npmStaticDepSf7(program, from, spec, "require"); return dep !== null && isCjsJsFile7(dep, program) ? dep : null; }; // A bundler rewrite spells some lexer-visible names as accessors. const lexWithAccessorNames = (sf: ts.SourceFile): ReturnType => { const lexed = cjsLexedExportsOfFile(sf); for (const name of npmStaticLexerVisibleNames(sf.fileName)) lexed.exports.add(name); return lexed; }; const visible = (dep: ts.SourceFile, name: string): boolean => // `default` is the module.exports binding itself — always provided. // The program already owns native source ASTs; reuse them without // starting another syntax session or reparsing their source text. name === "default" || cjsVisibleNames(dep, lexWithAccessorNames, resolveCjsDep, lexMemo).has(name); /** The statement's resolved LOCAL CommonJS dependency, when it is an * import/re-export from one. */ const cjsDepOf = (sf: ts.SourceFile, spec: string): ts.SourceFile | null => { const dep = resolveEdge(sf, spec); return dep !== null && isCjsJsFile7(dep, program) ? dep : null; }; /** The first lexer-invisible CJS name request of ONE module, in V8's * check order: all regular named imports sorted by local binding name, * then `export { x } from` elements in source order. */ const firstInvisibleOf = (sf: ts.SourceFile): BadCjsImport | null => { const entries: { local: string; exportName: string; nameNode: ts.Node; spec: string; dep: ts.SourceFile; }[] = []; for (const stmt of sf.statements.nodes) { if ( !ts.isImportDeclaration(stmt) || stmt.moduleSpecifier === undefined || !ts.isStringLiteral(stmt.moduleSpecifier) ) continue; const clause = stmt.importClause; if (!clause || clause.phaseModifier === ts.SyntaxKind.TypeKeyword) continue; if (!clause.namedBindings || !ts.isNamedImports(clause.namedBindings)) continue; const spec = stmt.moduleSpecifier.text; const dep = cjsDepOf(sf, spec); if (dep === null) continue; for (const e of clause.namedBindings.elements.nodes) { if (e.isTypeOnly) continue; const nameNode = e.propertyName ?? e.name; entries.push({ local: e.name.text, exportName: nameNode.text, nameNode, spec, dep }); } } entries.sort((a, b) => (a.local < b.local ? -1 : a.local > b.local ? 1 : 0)); for (const e of entries) { if (!visible(e.dep, e.exportName)) return { exportName: e.exportName, spec: e.spec, nameNode: e.nameNode, sf }; } for (const stmt of sf.statements.nodes) { if (!ts.isExportDeclaration(stmt) || stmt.isTypeOnly) continue; if (stmt.moduleSpecifier === undefined || !ts.isStringLiteral(stmt.moduleSpecifier)) continue; if (!stmt.exportClause || !ts.isNamedExports(stmt.exportClause)) continue; const spec = stmt.moduleSpecifier.text; const dep = cjsDepOf(sf, spec); if (dep === null) continue; for (const e of stmt.exportClause.elements.nodes) { if (e.isTypeOnly) continue; const nameNode = e.propertyName ?? e.name; if (!visible(dep, nameNode.text)) return { exportName: nameNode.text, spec, nameNode, sf }; } } return null; }; // The instantiate graph: from an ESM entry, every import/re-export // request in source order, recursing into ESM dependencies only (CJS // and JSON modules are leaves — their own requires never join V8's // linking). A CommonJS ENTRY has no instantiate phase at all. const visited = new Set(); const dfs = (sf: ts.SourceFile): BadCjsImport | null => { if (visited.has(sf)) return null; visited.add(sf); for (const stmt of sf.statements.nodes) { if (!ts.isImportDeclaration(stmt) && !(ts.isExportDeclaration(stmt) && !stmt.isTypeOnly)) continue; if (ts.isExportDeclaration(stmt) && isPrunedNpmReexport(program, stmt)) continue; if ( ts.isImportDeclaration(stmt) && stmt.importClause?.phaseModifier === ts.SyntaxKind.TypeKeyword ) continue; const specNode = stmt.moduleSpecifier; if (specNode === undefined || !ts.isStringLiteral(specNode)) continue; const spec = specNode.text; const dep = resolveEdge(sf, spec); if (dep === null || dep.fileName.endsWith(".json") || !isNodeEsmFile7(dep, program)) continue; const bad = dfs(dep); if (bad !== null) return bad; } return firstInvisibleOf(sf); }; const bad = isNodeEsmFile7(entry, program) ? dfs(entry) : null; // ESM modules only require() reaches: same invisibility, but Node's // SyntaxError fires mid-evaluation at the require — a pointed fence // instead of a mismodeled crash position. for (const sf of moduleOrder) { if (!isNodeEsmFile7(sf, program) || visited.has(sf)) continue; const b = firstInvisibleOf(sf); if (b !== null) { diags.push( unsupportedDiag( "SC1013", locOf7(b.nameNode), `a named import of a CommonJS export Node's lexer cannot detect, reached through require() of an ES module (Node throws its SyntaxError mid-evaluation at that require; import the CommonJS module's default and read members off it instead)`, ), ); } } if (bad === null) return null; // Node's message: V8's generic missing-export SyntaxError, rewritten to // the CommonJS hint form only when the failing SPECIFIER STRING is also // one of the ENTRY module's own requests and the entry's resolution of // it is CommonJS (module_job.js consults the ROOT job's moduleRequests/ // commonJsDeps — a deep module's failure keeps the generic wording). let flavored = false; for (const stmt of entry.statements.nodes) { if (!ts.isImportDeclaration(stmt) && !(ts.isExportDeclaration(stmt) && !stmt.isTypeOnly)) continue; if ( ts.isImportDeclaration(stmt) && stmt.importClause?.phaseModifier === ts.SyntaxKind.TypeKeyword ) continue; const specNode = stmt.moduleSpecifier; if (specNode === undefined || !ts.isStringLiteral(specNode) || specNode.text !== bad.spec) continue; if (cjsDepOf(entry, bad.spec) !== null) { flavored = true; break; } } const loc = locOf7(bad.nameNode); if (!flavored) { return { message: `The requested module '${bad.spec}' does not provide an export named '${bad.exportName}'`, className: "%SyntaxError", loc, }; } // The destructuring hint: Node regexes the failing name's SOURCE LINE // for its brace group (greedy, first `{` to last `}` on the line) and // swaps ` as ` for `: ` — reproduced exactly, quirks included // (multi-line import statements simply lose the hint). const text = bad.sf.text; const pos = ts.tokenStartOfNode(bad.nameNode); const lineStart = text.lastIndexOf("\n", pos - 1) + 1; const lineEnd = text.indexOf("\n", pos); let line = text.slice(lineStart, lineEnd < 0 ? text.length : lineEnd); if (line.endsWith("\r")) line = line.slice(0, -1); const braces = /{.*}/.exec(line); const destructuring = braces === null ? null : braces[0].replace(/\s+as\s+/g, ": "); return { message: `Named export '${bad.exportName}' not found. The requested module '${bad.spec}' is a CommonJS module, which may not support all module.exports as named exports.\n` + "CommonJS modules can always be imported via the default export, for example using:\n\n" + `import pkg from '${bad.spec}';\n` + (destructuring !== null ? `const ${destructuring} = pkg;\n` : ""), className: "%SyntaxError", loc, }; } /* ── the native ESM named-import link check ───────────────────────────── * TypeScript accepts `import { Shape } from "./types.ts"` when Shape is an * interface or type alias: the checker is answering a TYPE question. Node's * ESM linker asks a VALUE question instead. Because Node 24's strip-only * execution leaves that import request in the module graph, the request * fails before any module evaluates unless the source uses `import type`. * * Keep this separate from cjsNamedImportLinkCheck. CommonJS exports are * determined by Node's source lexer and have their own interop message and * ordering rules; native ESM exports come from the resolved TypeScript * module symbol and use Node's generic missing-export SyntaxError. */ interface BadEsmImport { exportName: string; spec: string; nameNode: ts.Node; sf: ts.SourceFile; } interface EsmNamedImportLinkAnalysis { crash: StartupCrash | null; visited: Set; firstMissingOf: (sf: ts.SourceFile) => BadEsmImport | null; } /** Node's strip-only loader exposes an empty `default` export for a default * interface only when the `.ts` file is in an ambiguous (typeless) package * scope. Explicitly ESM files (`.mts` and `.mjs`) and `.ts` files in a * `"type": "module"` package do not get that placeholder. */ function hasNodeTsDefaultInterfacePlaceholder7(sf: ts.SourceFile): boolean { return sf.fileName.endsWith(".ts") && nearestPackageType(sf.fileName) === null; } /** Finds the first native-ESM link failure in the static module graph rooted * at `entry`. This is used both for startup linking of the entry graph and * for dynamic imports: a dynamically loaded graph rejects its import promise * at this same link point instead of running any module body. */ function analyzeEsmNamedImportLinks( program: ts.Program, entry: ts.SourceFile, ): EsmNamedImportLinkAnalysis { const checker = program.getTypeChecker(); const resolveEdge = (from: ts.SourceFile, spec: string): ts.SourceFile | null => { const project = resolveImport7(program, from, spec); if (project !== null) return project; return npmStaticDepSf7(program, from, spec); }; const runtimeExport = ( dep: ts.SourceFile, name: string, seenModules: ReadonlySet = new Set(), ): ts.Symbol | undefined => { if (seenModules.has(dep)) return undefined; const nextSeen = new Set(seenModules); nextSeen.add(dep); const module = checker.getSymbolAtLocation(dep); const exported = module?.getExports().get(ts.escapedSymbolName(name)); if (exported !== undefined) { let resolved = exported; const seenSymbols = new Set(); while ((resolved.flags & ts.SymbolFlags.Alias) !== 0 && !seenSymbols.has(resolved)) { seenSymbols.add(resolved); resolved = checker.getAliasedSymbol(resolved); } if (resolved.flags & ts.SymbolFlags.Value) return resolved; // Node's strip-only TypeScript loader materializes a direct `export // default interface X {}` as an empty default export in an ambiguous // `.ts` file. The checker correctly classifies the declaration as // type-only, but the native ESM linker sees the runtime placeholder. if ( name === "default" && hasNodeTsDefaultInterfacePlaceholder7(dep) && resolved.declarations.some( (declaration) => ts.isInterfaceDeclaration(declaration) && ts .getModifiers(declaration) ?.some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword) === true && ts .getModifiers(declaration) ?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) === true, ) ) { return resolved; } return undefined; } // TypeScript 7's client-side module symbol exposes the module's own // export table but not names contributed by `export *`. Follow those // source edges explicitly, applying the ESM ambiguity rule: the same // resolved symbol may arrive through multiple stars, while distinct // symbols make the name unavailable. `default` never star-reexports. if (name === "default") return undefined; let starExport: ts.Symbol | undefined; for (const stmt of dep.statements.nodes) { if (!ts.isExportDeclaration(stmt) || stmt.isTypeOnly || stmt.exportClause !== undefined) continue; if (stmt.moduleSpecifier === undefined || !ts.isStringLiteral(stmt.moduleSpecifier)) continue; const target = resolveEdge(dep, stmt.moduleSpecifier.text); if (target === null || target.fileName.endsWith(".json") || !isNodeEsmFile7(target, program)) continue; const candidate = runtimeExport(target, name, nextSeen); if (candidate === undefined) continue; if (starExport !== undefined && starExport !== candidate) return undefined; starExport = candidate; } return starExport; }; const firstMissingOf = (sf: ts.SourceFile): BadEsmImport | null => { const imports: { local: string; exportName: string; spec: string; nameNode: ts.Node; dep: ts.SourceFile; }[] = []; for (const stmt of sf.statements.nodes) { if (!ts.isImportDeclaration(stmt) || !ts.isStringLiteral(stmt.moduleSpecifier)) continue; const clause = stmt.importClause; if (clause === undefined || clause.phaseModifier === ts.SyntaxKind.TypeKeyword) continue; const spec = stmt.moduleSpecifier.text; const dep = resolveEdge(sf, spec); if (dep === null || dep.fileName.endsWith(".json") || !isNodeEsmFile7(dep, program)) continue; if (clause.name !== undefined) { imports.push({ local: clause.name.text, exportName: "default", spec, nameNode: clause.name, dep, }); } if (clause.namedBindings === undefined || !ts.isNamedImports(clause.namedBindings)) continue; for (const element of clause.namedBindings.elements.nodes) { if (element.isTypeOnly) continue; const nameNode = element.propertyName ?? element.name; imports.push({ local: element.name.text, exportName: nameNode.text, spec, nameNode, dep, }); } } // Node checks regular named requests in local-binding order. Keeping the // same order as the CommonJS linker matters when one statement asks for // more than one erased TypeScript export. imports.sort((a, b) => (a.local < b.local ? -1 : a.local > b.local ? 1 : 0)); for (const request of imports) { if (runtimeExport(request.dep, request.exportName) === undefined) { return { exportName: request.exportName, spec: request.spec, nameNode: request.nameNode, sf, }; } } for (const stmt of sf.statements.nodes) { if (!ts.isExportDeclaration(stmt) || stmt.isTypeOnly) continue; const moduleSpecifier = stmt.moduleSpecifier; const exportClause = stmt.exportClause; if (moduleSpecifier === undefined || exportClause === undefined) continue; if (!ts.isStringLiteral(moduleSpecifier) || !ts.isNamedExports(exportClause)) continue; const spec = moduleSpecifier.text; const dep = resolveEdge(sf, spec); if (dep === null || dep.fileName.endsWith(".json") || !isNodeEsmFile7(dep, program)) continue; for (const element of exportClause.elements.nodes) { if (element.isTypeOnly) continue; const nameNode = element.propertyName ?? element.name; if (runtimeExport(dep, nameNode.text) === undefined) { return { exportName: nameNode.text, spec, nameNode, sf }; } } } return null; }; // The ESM instantiate graph is the same depth-first graph used by the CJS // linker: dependencies are linked before the importing module, and a // CommonJS/JSON target is a leaf for this native-export check. const visited = new Set(); const dfs = (sf: ts.SourceFile): BadEsmImport | null => { if (visited.has(sf)) return null; visited.add(sf); for (const stmt of sf.statements.nodes) { if (!ts.isImportDeclaration(stmt) && !(ts.isExportDeclaration(stmt) && !stmt.isTypeOnly)) continue; if (ts.isExportDeclaration(stmt) && isPrunedNpmReexport(program, stmt)) continue; if ( ts.isImportDeclaration(stmt) && stmt.importClause?.phaseModifier === ts.SyntaxKind.TypeKeyword ) continue; const moduleSpecifier = stmt.moduleSpecifier; if (moduleSpecifier === undefined || !ts.isStringLiteral(moduleSpecifier)) continue; const dep = resolveEdge(sf, moduleSpecifier.text); if (dep === null || dep.fileName.endsWith(".json") || !isNodeEsmFile7(dep, program)) continue; const bad = dfs(dep); if (bad !== null) return bad; } return firstMissingOf(sf); }; // Native ESM linking itself starts only from an ESM entry. The caller // supplies dynamic-only roots here too; each such root gets its own // promise rejection rather than a startup crash. const bad = isNodeEsmFile7(entry, program) ? dfs(entry) : null; if (bad !== null) { return { crash: { message: `The requested module '${bad.spec}' does not provide an export named '${bad.exportName}'`, className: "%SyntaxError", loc: locOf7(bad.nameNode), }, visited, firstMissingOf, }; } return { crash: null, visited, firstMissingOf }; } export function esmNamedImportLinkCrash( program: ts.Program, entry: ts.SourceFile, ): StartupCrash | null { return analyzeEsmNamedImportLinks(program, entry).crash; } function esmNamedImportLinkCheck( program: ts.Program, entry: ts.SourceFile, moduleOrder: ts.SourceFile[], diags: ScrDiagnostic[], ): StartupCrash | null { const analysis = analyzeEsmNamedImportLinks(program, entry); if (analysis.crash !== null) return analysis.crash; // A CommonJS entry can synchronously require an ESM graph. Node links that // graph at the require site, after the CommonJS module has already begun // evaluating, so this cannot use startupCrash (which would move the error // before earlier output). Keep the compile-time fence used by the CJS link // checker for the analogous mid-evaluation failure instead. for (const sf of moduleOrder) { if (!isNodeEsmFile7(sf, program) || analysis.visited.has(sf)) continue; const childFailure = analysis.firstMissingOf(sf); if (childFailure !== null) { diags.push( unsupportedDiag( "SC1013", locOf7(childFailure.nameNode), "a named import of an unavailable export in an ES module reached through require() (Node throws its SyntaxError mid-evaluation at that require; make the import match the module's runtime exports)", ), ); } } return null; } /** Both named-export link checks run independently because their export * questions are different (Node's CJS lexer versus TypeScript's value * symbols). Their first failures still share one Node instantiate order: the * module postorder established by preflight, then regular named imports by * local binding name, then source-order re-exports. Pick the earlier result * so a native ESM failure in a child cannot be hidden by a CJS failure in a * later parent. */ function earlierLinkCrash( moduleOrder: ts.SourceFile[], first: StartupCrash | null, second: StartupCrash | null, ): StartupCrash | null { if (first === null) return second; if (second === null) return first; const moduleIndex = new Map(moduleOrder.map((sf, index) => [sf.fileName, index])); const firstIndex = moduleIndex.get(first.loc.file) ?? Number.MAX_SAFE_INTEGER; const secondIndex = moduleIndex.get(second.loc.file) ?? Number.MAX_SAFE_INTEGER; if (firstIndex !== secondIndex) return firstIndex < secondIndex ? first : second; if (first.loc.start !== second.loc.start) { const sf = moduleOrder[firstIndex]; if (sf === undefined) return first.loc.start < second.loc.start ? first : second; const rank = (crash: StartupCrash): [number, number] => { const entries: { local: string; start: number; kind: 0 | 1 }[] = []; for (const stmt of sf.statements.nodes) { if (ts.isImportDeclaration(stmt) && ts.isStringLiteral(stmt.moduleSpecifier)) { const clause = stmt.importClause; if (clause?.phaseModifier === ts.SyntaxKind.TypeKeyword) continue; if (clause?.namedBindings && ts.isNamedImports(clause.namedBindings)) { for (const element of clause.namedBindings.elements.nodes) { if (!element.isTypeOnly) { entries.push({ local: element.name.text, start: ts.tokenStartOfNode(element.propertyName ?? element.name), kind: 0, }); } } } } } entries.sort((a, b) => (a.local < b.local ? -1 : a.local > b.local ? 1 : 0)); const direct = entries.findIndex((entry) => entry.start === crash.loc.start); if (direct >= 0) return [0, direct]; let reexport = 0; for (const stmt of sf.statements.nodes) { const exportClause = ts.isExportDeclaration(stmt) ? stmt.exportClause : undefined; if ( !ts.isExportDeclaration(stmt) || stmt.isTypeOnly || exportClause === undefined || !ts.isNamedExports(exportClause) ) continue; for (const element of exportClause.elements.nodes) { if ( !element.isTypeOnly && ts.tokenStartOfNode(element.propertyName ?? element.name) === crash.loc.start ) { return [1, reexport]; } if (!element.isTypeOnly) reexport++; } } return [2, crash.loc.start]; }; const firstRank = rank(first); const secondRank = rank(second); if (firstRank[0] !== secondRank[0]) return firstRank[0] < secondRank[0] ? first : second; return firstRank[1] <= secondRank[1] ? first : second; } return first; } /* node's builtin-module name list, for the SC1010 wording decision ("the * 'fs' module" vs the generic package message) — shared verbatim with the * retired 5.9.3 lane's builtinModules use. */ const nodeBuiltinNames: ReadonlySet = new Set(builtinModules); /* ── the lowering-facing surface ───────────────────────────────────────── * A family of world-typed helpers the LOWERING imports (require * recognizers, the CJS export-identity analysis, ordered imports, * resolution, locations), exported under the names the 5.9.3 program.ts * established (the phase-2 port kept every spelling); the world-neutral * helpers re-export from their focused modules. */ export { ambientDtsPath, fallbackDtsPath, isNodeTypesPath, overridesDtsPath } from "./dts-paths.js"; export { builtinDefaultImportModule, canonicalBuiltinModule, npmBuiltinAliasOf, SUPPORTED_BUILTIN_MODULES, } from "./builtin-modules.js"; export { npmPackageNameOf, workspacePackageOfPath } from "./workspace-registry.js"; export { requireSpecOf7 as requireSpecOf, isRequireStatement7 as isRequireStatement, requiresOf7 as requiresOf, splitRequireCallOf7 as splitRequireCallOf, isNodeEsmFile7 as isNodeEsmFile, resolveImport7 as resolveImport, resolveNpmImport7 as resolveNpmImport, locOf7 as locOf, requireEarlyReference7 as requireEarlyReference, inDeferredPosition7 as inDeferredPosition, }; /** True for JavaScript source files (.js/.mjs/.cjs) — the files whose * types come entirely from inference (checkJs + JSDoc) and whose classes * declare fields by constructor assignment. */ export function isJsSourceFile(sf: ts.SourceFile): boolean { return isJsSourceFileName(sf.fileName); } /** True for a JavaScript file of the program that TypeScript type-checks: * one with a `// @ts-check` comment before its first statement, or any * file of a project whose tsconfig sets `checkJs` unless the file has a * `// @ts-nocheck` comment. Files under node_modules are never checked. */ export function isCheckedJsSourceFile(sf: ts.SourceFile, projectCheckJs: boolean): boolean { if (!isJsSourceFile(sf) || isNodeModulesPath(sf.fileName)) return false; let checked = projectCheckJs; const head = sf.text.slice( 0, (sf.statements.nodes.length === 0 ? undefined : ts.tokenStartOfNode(sf.statements.nodes[0]!)) ?? sf.text.length, ); for (const line of head.split("\n")) { const directive = /^\s*\/\/\s*@ts-(no)?check\b/.exec(line); if (directive) checked = directive[1] === undefined; } return checked; } export function orderedImportsOf( program: ts.Program, sf: ts.SourceFile, ): { stmt: ts.Statement; dep: ts.SourceFile | null }[] { const out: { stmt: ts.Statement; dep: ts.SourceFile | null }[] = []; for (const stmt of sf.statements.nodes) { if (ts.isExportDeclaration(stmt) && isPrunedNpmReexport(program, stmt)) continue; if (ts.isExportDeclaration(stmt) && (stmt.isTypeOnly || !stmt.moduleSpecifier)) continue; if (!ts.isImportDeclaration(stmt) && !ts.isExportDeclaration(stmt)) continue; if (ts.isImportDeclaration(stmt) && erasedTypeOnlyImport(stmt)) continue; if (!stmt.moduleSpecifier || !ts.isStringLiteral(stmt.moduleSpecifier)) continue; const spec = stmt.moduleSpecifier.text; // Every project edge (relative, tsconfig alias, #alias, or self-name) // resolves to the same dep so the importer's %init header calls theirs. // Builtin and npm bare specifiers stay null — EXCEPT opted-in --npm-static // packages, whose entries are program modules the header must init. const dep = resolveImport7(program, sf, spec) ?? npmStaticDepSf7(program, sf, spec); const isJson = dep !== null && dep.fileName.endsWith(".json"); out.push({ stmt, dep: isJson ? null : dep }); } return out; } /** The files a compiled program can load: the module order closed over * every literal require() and import() call it contains (deferred and * nested ones included) and, for opted-in package JavaScript, over every * import and export declaration as well. A package file outside this set * is reachable only through types, so whatever it spells cannot affect * the compiled program. Null when a reachable file loads a computed * specifier: then any file may load. */ export function npmStaticLoadableFiles( program: ts.Program, moduleOrder: readonly ts.SourceFile[], ): Set | null { const seen = new Set(); const work: ts.SourceFile[] = []; const add = (sf: ts.SourceFile | null | undefined): void => { if (sf === null || sf === undefined || seen.has(sf) || sf.isDeclarationFile) return; seen.add(sf); work.push(sf); }; for (const sf of moduleOrder) add(sf); let computed = false; while (work.length > 0 && !computed) { const sf = work.pop()!; const packageJs = npmStaticPackageOfPath(sf.fileName) !== null && isJsSourceFileName(sf.fileName); const follow = (spec: string, kind: "import" | "require"): void => { if (canonicalBuiltinModule(spec) !== null) return; add(resolveImport7(program, sf, spec, kind) ?? npmStaticDepSf7(program, sf, spec, kind)); }; ts.walkPreorder(sf, (n) => { if (computed) return "stop"; if (ts.isCallExpression(n)) { const isImport = n.expression.kind === ts.SyntaxKind.ImportKeyword; const isRequire = ts.isIdentifier(n.expression) && n.expression.text === "require"; if (!isImport && !isRequire) return undefined; const arg = n.arguments.nodes[0]; if (arg !== undefined && ts.isStringLiteralLike(arg)) follow(arg.text, isImport ? "import" : "require"); else if (packageJs || isImport) computed = true; } else if ( packageJs && (ts.isImportDeclaration(n) || ts.isExportDeclaration(n)) && n.moduleSpecifier !== undefined && ts.isStringLiteral(n.moduleSpecifier) ) { follow(n.moduleSpecifier.text, "import"); } return undefined; }); } return computed ? null : new Set([...seen].map((sf) => sf.fileName.split("\\").join("/"))); } /** The program source file a bare specifier reaches when it names an * opted-in --npm-static package (the shipped-JS entry preflight admitted * as a module edge), else null. No offender reporting here — preflight * already classified the import; this is the lookup the module-order and * lowering paths share. */ export function npmStaticDepSf7( program: ts.Program, sf: ts.SourceFile, spec: string, resolutionKind: "import" | "require" = "import", ): ts.SourceFile | null { if (!npmStaticActive() || isRelativeSpecifier(spec)) return null; if (spec.startsWith("node:") || spec.startsWith("#")) return null; const npm = resolveNpmImport7(sf.fileName, spec, resolutionKind); if (npm === null || !isNpmStaticPackage(npm.packageName)) return null; if (!isJsSourceFileName(npm.typesFile)) return null; return program.getSourceFile(npm.typesFile) ?? null; } /** True when `expr` is the `module.exports` property access. */ export function isModuleExportsAccess(expr: ts.Expression): expr is ts.PropertyAccessExpression { return ( ts.isPropertyAccessExpression(expr) && !expr.questionDotToken && ts.isIdentifier(expr.expression) && expr.expression.text === "module" && expr.name.text === "exports" ); } /** Classifies a statement as a CommonJS EXPORT assignment: * - table: `module.exports = { ... }` — the whole export table at once * (obj is null when the RHS is not an object literal — the * recognized-but-unsupported form, fenced by the caller); * - member: `exports.f = ` / `module.exports.f = `. * Null for everything else. Callers gate on top-level position in a JS * module file — these forms anywhere else keep their generic fences. */ export function cjsExportAssignmentOf(stmt: ts.Statement): | { kind: "table"; obj: ts.ObjectLiteralExpression | null; expr: ts.BinaryExpression; /** `exports = module.exports = value`: the `exports` binding names * the new value too, so later `exports.f =` members attach to it. */ rebindsExports?: true; } | { kind: "member"; name: ts.MemberName; value: ts.Expression; expr: ts.BinaryExpression } | null { if (!ts.isExpressionStatement(stmt)) return null; const e = stmt.expression; if (!ts.isBinaryExpression(e) || e.operatorToken.kind !== ts.SyntaxKind.EqualsToken) return null; if (isModuleExportsAccess(e.left)) { return { kind: "table", obj: ts.isObjectLiteralExpression(e.right) ? e.right : null, expr: e }; } if (ts.isIdentifier(e.left) && e.left.text === "exports") { let inner: ts.Expression = e.right; while (ts.isParenthesizedExpression(inner)) inner = inner.expression; if ( ts.isBinaryExpression(inner) && inner.operatorToken.kind === ts.SyntaxKind.EqualsToken && isModuleExportsAccess(inner.left) && // `exports = module.exports = {}` is the fresh-table reset, which the // export walk handles on its own. !ts.isObjectLiteralExpression(inner.right) ) { return { kind: "table", obj: null, expr: inner, rebindsExports: true }; } } if (ts.isPropertyAccessExpression(e.left) && !e.left.questionDotToken) { const recv = e.left.expression; const isExports = ts.isIdentifier(recv) && recv.text === "exports"; if (isExports || isModuleExportsAccess(recv)) { return { kind: "member", name: e.left.name, value: e.right, expr: e }; } } return null; } /** The tsc VOID-INIT PREAMBLE (`exports.a = exports.b = void 0;` with the * real `exports.a = a;` further down; every tsc-emitted dist file opens its * exports this way): when every name in the chain has a later real * assignment, the export takes its value there and the preamble lowers to * nothing. Returns those later assignments by name, or null when this is * not such a preamble (a preamble with no later real assignment keeps the * ordinary path: the export IS undefined then). */ export function cjsVoidInitPreambleReals( cjs: Extract, { kind: "member" }>, ): Map, { kind: "member" }>> | null { if (!ts.isIdentifier(cjs.name)) return null; const isExportsMember = (e: ts.Expression): e is ts.PropertyAccessExpression => ts.isPropertyAccessExpression(e) && ts.isIdentifier(e.name) && ((ts.isIdentifier(e.expression) && e.expression.text === "exports") || (ts.isPropertyAccessExpression(e.expression) && ts.isIdentifier(e.expression.expression) && e.expression.expression.text === "module" && ts.isIdentifier(e.expression.name) && e.expression.name.text === "exports")); const isVoid = (e: ts.Expression): boolean => { while (ts.isParenthesizedExpression(e)) e = e.expression; return ( e.kind === ts.SyntaxKind.VoidExpression || (ts.isIdentifier(e) && e.text === "undefined") ); }; // chains: `exports.a = exports.b = void 0;` collects every name const names: string[] = [cjs.name.text]; let tail: ts.Expression = cjs.value; while ( ts.isBinaryExpression(tail) && tail.operatorToken.kind === ts.SyntaxKind.EqualsToken && isExportsMember(tail.left) ) { names.push((tail.left.name as ts.Identifier).text); tail = tail.right; } if (!isVoid(tail)) return null; const sf = ts.getSourceFileOfNode(cjs.expr); const myPos = ts.tokenStartOfNode(cjs.expr); const reals = new Map< string, Extract, { kind: "member" }> >(); for (const name of names) { const real = sf.statements.nodes .map((s) => cjsExportAssignmentOf(s)) .find( (other) => other?.kind === "member" && ts.isIdentifier(other.name) && other.name.text === name && ts.tokenStartOfNode(other.expr) > myPos && !isVoid(other.value), ); if (real?.kind !== "member") return null; reals.set(name, real); } return reals; } /** The members and shared value of a CHAINED member export * `exports.a = exports.b = module.exports.c = value` (outermost name * first), or null when the assignment is not a chain of at least two * identifier-named export members or its value is undefined. */ export function cjsExportChainOf( e: ts.BinaryExpression, ): { names: ts.Identifier[]; value: ts.Expression } | null { const names: ts.Identifier[] = []; let cur: ts.Expression = e; for (;;) { while (ts.isParenthesizedExpression(cur)) cur = cur.expression; if (!ts.isBinaryExpression(cur) || cur.operatorToken.kind !== ts.SyntaxKind.EqualsToken) break; const left = cur.left; if ( !ts.isPropertyAccessExpression(left) || left.questionDotToken || !ts.isIdentifier(left.name) ) break; const recv = left.expression; if (!((ts.isIdentifier(recv) && recv.text === "exports") || isModuleExportsAccess(recv))) break; names.push(left.name); cur = cur.right; } // tsc's `exports.a = exports.b = void 0;` preamble is not a value chain // (the export-statement lowering owns it). const isVoid = cur.kind === ts.SyntaxKind.VoidExpression || (ts.isIdentifier(cur) && cur.text === "undefined"); return names.length >= 2 && !isVoid ? { names, value: cur } : null; } /** Why Node would DISCARD this CommonJS export statement, or null when it * survives (Node's export-object identity rules — checker-free, pure * AST): `module.exports = {...}` REPLACES the export object, so members * attached before it (either spelling), any earlier table, and * `exports.f =` after it (exports still references the replaced object) * are never seen by importers. Lowering fences these instead of shipping * the checker's everything-is-live answer — the checker/runtime split * would otherwise diverge from Node without a diagnostic. */ export function cjsExportDiscardReason(stmt: ts.Statement): string | null { const sf = stmt.parent; if (!ts.isSourceFile(sf)) return null; const mine = cjsExportAssignmentOf(stmt); if (!mine) return null; const stmts = sf.statements.nodes; let lastTable = -1; for (let i = 0; i < stmts.length; i++) { if (cjsExportAssignmentOf(stmts[i]!)?.kind === "table") lastTable = i; } if (lastTable < 0) return null; const myIdx = stmts.indexOf(stmt as never); if (mine.kind === "table") { return myIdx < lastTable ? "multiple 'module.exports =' assignments (Node keeps only the last one — this table is discarded)" : null; } if (myIdx < lastTable) { return "export members attached before a 'module.exports =' assignment (Node replaces the export object — importers never see this member; attach it to module.exports AFTER the table)"; } const left = mine.expr.left as ts.PropertyAccessExpression; const last = cjsExportAssignmentOf(stmts[lastTable]!); if (last?.kind === "table" && last.rebindsExports === true) return null; return ts.isIdentifier(left.expression) && left.expression.text === "exports" ? "'exports.f =' after a 'module.exports =' assignment ('exports' still references the replaced object — Node discards this member; write module.exports.f instead)" : null; } /** The object literal a `module.exports = ` statement's RHS * RESOLVES to (syntactically): the literal itself; `new Proxy(target, * handler)` — Node programs export identity-forwarding proxies over a * table, and the TARGET is the table (traps cannot run in a compiled * program: every member access is statically resolved against the * target's own type); or an identifier naming a top-level const whose * initializer is the literal. Null for everything else. */ export function cjsExportTargetLiteral( e: ts.Expression, sf: ts.SourceFile, ): ts.ObjectLiteralExpression | null { while (ts.isParenthesizedExpression(e)) e = e.expression; if (ts.isObjectLiteralExpression(e)) return e; if ( ts.isNewExpression(e) && ts.isIdentifier(e.expression) && e.expression.text === "Proxy" && (e.arguments?.nodes.length ?? 0) >= 1 ) { return cjsExportTargetLiteral(e.arguments!.nodes[0]!, sf); } if (ts.isIdentifier(e)) { for (const stmt of sf.statements.nodes) { if (!ts.isVariableStatement(stmt)) continue; if ((stmt.declarationList.flags & ts.NodeFlags.Const) === 0) continue; for (const d of stmt.declarationList.declarations.nodes) { if ( d.name !== undefined && ts.isIdentifier(d.name) && d.name.text === e.text && d.initializer ) { let init: ts.Expression = d.initializer; while (ts.isParenthesizedExpression(init)) init = init.expression; return ts.isObjectLiteralExpression(init) ? init : null; } } } } return null; } /** The class expression a JS module's WHOLE export is (`module.exports = * class …{}`): the kept (last) top-level `module.exports =` statement's * paren-unwrapped RHS when it is a class expression, together with the * statement it sits in (in-file `module.exports` reads gate on source * order against it). Null when the kept export is anything else. */ export function cjsClassExprWholeExportOf( sf: ts.SourceFile, ): { classExpr: ts.ClassExpression; stmt: ts.Statement } | null { let last: { classExpr: ts.ClassExpression; stmt: ts.Statement } | null = null; for (const stmt of sf.statements.nodes) { const cjs = cjsExportAssignmentOf(stmt); if (!cjs || cjs.kind !== "table") continue; let rhs: ts.Expression = cjs.expr.right; while (ts.isParenthesizedExpression(rhs)) rhs = rhs.expression; // Any LATER table replaces the export object — a non-class one takes // the class answer back off the table. last = ts.isClassExpression(rhs) ? { classExpr: rhs, stmt } : null; } return last; } /** True when `obj` is the effective export-table literal of a JS module: * the object literal of a top-level `module.exports = { ... }` statement * directly, OR the RESOLVED TARGET of the module's kept `module.exports =` * statement (the identifier/Proxy forms — see cjsExportTargetLiteral). * The gate for the CJS export-property alias re-resolution * (resolveValueSymbol) and the accessor-read lift. */ export function isCjsExportTableLiteral(obj: ts.ObjectLiteralExpression): boolean { const assign = obj.parent; if ( ts.isBinaryExpression(assign) && assign.right === obj && isModuleExportsAccess(assign.left) && ts.isExpressionStatement(assign.parent) && ts.isSourceFile(assign.parent.parent) ) { return isJsSourceFile(assign.parent.parent); } const decl = obj.parent; if (!ts.isVariableDeclaration(decl) || decl.initializer !== obj) return false; const sf = ts.getSourceFileOfNode(obj); if (!isJsSourceFile(sf)) return false; for (const stmt of sf.statements.nodes) { const cjs = cjsExportAssignmentOf(stmt); if (cjs?.kind === "table" && cjsExportDiscardReason(stmt) === null) { return cjsExportTargetLiteral(cjs.expr.right, sf) === obj; } } return false; } /** Names other runtimes and the DOM declare that scriptc's type world * (the ES2025 library plus Node types) does not. */ const FOREIGN_RUNTIME_GLOBALS = new Set(["Deno", "Bun"]); const DOM_GLOBALS = new Set([ "window", "document", "navigator", "location", "localStorage", "sessionStorage", "HTMLElement", "Element", "Document", "Window", "BodyInit", "HeadersInit", "RequestInfo", "XMLHttpRequest", "requestAnimationFrame", ]); /** Root identifiers whose members the bundled Node fallback declares. */ const NODE_GLOBAL_ROOTS = new Set(["process", "Buffer", "console", "globalThis"]); /** A hint when a TypeScript error comes from scriptc's own type world rather * than from the project: a library member newer than ES2025, a global of * another runtime or of the DOM, a Node member the bundled fallback types * leave out, or the strictNullChecks scriptc enforces. Null otherwise. */ function typeWorldHint(d: ts.Diagnostic, message: string, load: LoadResult): string | null { const world = load.typeWorld; if (world === undefined) return null; const quoted = /'([^']+)'/.exec(message)?.[1]; // TS2304/TS2552: Cannot find name; TS2580/TS2591/TS2592: missing type // packages; TS2583: needs a newer library. if (d.code === 2304 || d.code === 2552 || d.code === 2583) { if (quoted !== undefined && FOREIGN_RUNTIME_GLOBALS.has(quoted)) { return `'${quoted}' is another runtime's global; scriptc's type world declares the ES2025 library and Node only — feature-test it through globalThis instead, e.g. '(globalThis as { ${quoted}?: unknown }).${quoted}' (the native program runs as Node would, where it is undefined)`; } if (quoted !== undefined && DOM_GLOBALS.has(quoted)) { return `'${quoted}' comes from the DOM library, which scriptc's type world does not include (it checks with the ES2025 library and Node types regardless of the project's "lib") — declare the shape you need locally, or move browser-only code out of the program`; } if (d.code === 2583) { return `scriptc checks with the ES2025 library regardless of the project's "lib" setting, and '${quoted ?? "this name"}' comes from a newer library that does not compile yet`; } } // TS2339/TS2550/TS2551: property does not exist (2550: on this lib). if (d.code === 2339 || d.code === 2550 || d.code === 2551) { const typeText = /on type '([^']+)'/.exec(message)?.[1] ?? ""; const newerLib = world.projectLib !== null && world.projectLib.some((lib) => /esnext|es202[6-9]|es20[3-9]\d/i.test(lib)); if (d.code === 2550 || (newerLib && /^[A-Z][A-Za-z0-9]*(?:Constructor)?$/.test(typeText))) { return `scriptc checks with the ES2025 library regardless of the project's "lib" setting, and '${quoted ?? "this member"}' comes from a newer library that does not compile yet — use an ES2025 equivalent here`; } if (world.nodeFallback && d.fileName !== undefined) { const root = receiverRoot(load.program.getSourceFile(d.fileName)?.text ?? "", d.pos); if ( root !== null && (NODE_GLOBAL_ROOTS.has(root) || importsNodeBuiltin(load, d.fileName, root)) ) { return `the project has no @types/node, so scriptc types Node with its bundled declarations, which list only the members that compile natively — '${quoted ?? "this member"}' does not compile natively yet (https://scriptc.dev/limitations); installing @types/node turns this into a located refusal with a hint`; } } } if (world.forcedStrictNullChecks && /\b(?:null|undefined)\b/.test(message)) { return `the project disables strictNullChecks, which scriptc always enables — this error appears only under strict null checks; narrow the value (or enable "strictNullChecks" in the project to see the same errors from its own tsc)`; } return null; } /** The root identifier of the property access whose member name starts at * `pos` (`process.version` → `process`), or null. */ function receiverRoot(text: string, pos: number): string | null { let i = pos - 1; while (i >= 0 && /\s/.test(text[i]!)) i--; if (text[i] !== ".") return null; i--; if (text[i] === "?") i--; let root: string | null = null; for (;;) { while (i >= 0 && /\s/.test(text[i]!)) i--; let j = i; while (j >= 0 && /[\w$]/.test(text[j]!)) j--; if (j === i) return root; root = text.slice(j + 1, i + 1); i = j; while (i >= 0 && /\s/.test(text[i]!)) i--; if (text[i] !== ".") return root; i--; if (text[i] === "?") i--; } } /** Whether `name` is bound in `fileName` by an import of a Node builtin. */ function importsNodeBuiltin(load: LoadResult, fileName: string, name: string): boolean { const sf = load.program.getSourceFile(fileName); for (const stmt of sf?.statements?.nodes ?? []) { if (!ts.isImportDeclaration(stmt) || !ts.isStringLiteral(stmt.moduleSpecifier)) continue; if (canonicalBuiltinModule(stmt.moduleSpecifier.text) === null) continue; const clause = stmt.importClause; if (clause?.name?.text === name) return true; const bindings = clause?.namedBindings; if (bindings && ts.isNamespaceImport(bindings) && bindings.name.text === name) return true; if ( bindings && ts.isNamedImports(bindings) && bindings.elements.nodes.some((e) => e.name.text === name) ) return true; } return false; }