import type { ITtscLintPluginConfig } from "./structures"; /** A resolved contributor: Go sub-package name + absolute source directory. */ type TtscPluginContributor = { name: string; source: string; }; /** Descriptor shape returned to ttsc's plugin builder by the factory. */ type TtscPluginDescriptor = { capabilities?: { checkObservations?: true; compilerArgsCwd?: true; diagnosticsTiming?: boolean; graphNodes?: boolean; lsp?: boolean; projectContextArgs?: boolean; projectDiagnostics?: boolean; projectInputs?: boolean; residentCheck?: boolean; threadingArgs?: boolean; }; contributors?: TtscPluginContributor[]; hostInputHashes?: Record; hostInputRealpaths?: Record; hostInputs?: string[]; name: string; reportsTypeScriptDiagnostics?: boolean; source: string; stage?: "check" | "transform"; }; /** * Context object injected by ttsc into every plugin factory call. The generic * `TConfig` is the tsconfig plugin entry shape. */ type TtscPluginFactoryContext = { binary: string; cwd: string; /** This descriptor's own directory — the ESM-safe replacement for `__dirname`. */ dirname: string; /** This descriptor's own path — the ESM-safe replacement for `__filename`. */ filename: string; plugin: TConfig; /** * Caller-declared anchor for plugin config-file discovery, present when the * embedder compiles through a generated tsconfig outside the project (see * `ITtscPluginFactoryContext.pluginConfigDir`). */ pluginConfigDir?: string; projectRoot: string; tsconfig: string; }; /** * Plugin descriptor factory consumed by ttsc package discovery. * * Contributor lint plugins come from one place: the project's lint config file * (`lint.config.{ts,cts,mts,js,cjs,mjs,json}` or `ttsc-lint.config.*`). The * tsconfig plugin entry carries no rule or plugin surface — it optionally names * the config file via `configFile`, otherwise the file is discovered by walking * upward from the tsconfig directory (or from the caller-declared * `pluginConfigDir` when the resolved tsconfig is a generated wrapper in a temp * directory). * * The factory locates the config file and evaluates contributor-bearing configs * in an isolated ttsx process. JSON without plugins or an extends chain needs * no evaluator. The extractor reads plugin maps from the resolved config and * its bases and forwards contributor Go sources to the descriptor's * `contributors` field. Its dependency envelope owns config observations; the * child's bootstrap does not inherit the parent descriptor's observation * channel. * * @evidence contracts/common.md#principled-implementation Entry validation and config-origin discovery resolve registered contributor sources and observed inputs before constructing the check-stage descriptor with supported capabilities. * @evidence contracts/common.md#clear-and-simple-design The exported factory composes validation, discovery and descriptor construction; helpers own module loading and dependency fingerprints separately. * @evidence contracts/common.md#prohibited-implementation-shortcuts Contributors use the supported Go-source extension boundary and config-file schema; no foreign method replacement or fixture-specific descriptor is installed. * @evidence contracts/common.md#meaningful-documentation Native paragraphs explain config origins, evaluator formats and contributor forwarding; separated prose and tags follow documentation guidance. */ export default function createTtscPlugin(context: TtscPluginFactoryContext): TtscPluginDescriptor; /** * The descriptor extractor's emitted source. * * Exported so a regression can inspect the same bytes the loader executes. The * template consumes its own escapes, so reading this file's text instead would * check characters no consumer ever sees. */ export declare const TTSX_EXTRACTOR_SCRIPT = "// @ts-ignore -- internal loader must not require user-installed Node typings.\nimport * as fs from \"node:fs\";\n// @ts-ignore -- internal loader must not require user-installed Node typings.\nimport { Buffer } from \"node:buffer\";\n// @ts-ignore -- internal loader must not require user-installed Node typings.\nimport { createHash } from \"node:crypto\";\n// @ts-ignore -- internal loader must not require user-installed Node typings.\nimport { createRequire, registerHooks } from \"node:module\";\n// @ts-ignore -- internal loader must not require user-installed Node typings.\nimport * as path from \"node:path\";\n// @ts-ignore -- internal loader must not require user-installed Node typings.\nimport { fileURLToPath, pathToFileURL } from \"node:url\";\n\nconst configUrl = %CONFIG_IMPORT%;\nconst outputPath = %CONFIG_OUTPUT%;\nconst resolutionRoot = path.resolve(%CONFIG_ROOT%);\nconst CONFIG_KEYS = new Set([\n \"files\",\n \"ignores\",\n \"extends\",\n \"plugins\",\n \"rules\",\n \"format\",\n]);\nconst dependencies = new Map;\n realpath: string | null;\n}>();\nconst graphNodes = new Map();\nconst graphEdges: Array<{\n child: string;\n packageBoundary: boolean;\n parent: string;\n}> = [];\nconst configLocation = fileURLToPath(configUrl);\n// Every spelling of this config the module system might key an edge under.\n//\n// Which one it uses is not knowable from here, and guessing has failed in both\n// directions. A path handed in by another producer can be escaped by a rule\n// Node does not share. Node respells a resolved file module through its real\n// path unless \"--preserve-symlinks\" is set, so a config reached through a\n// symlinked directory is keyed by its target. And a Windows 8.3 short name is\n// not a symlink: fs.realpathSync expands it, the module resolver does not, so\n// asking the volume there produces a spelling no edge carries.\n//\n// A seed that names a URL no edge was keyed under sits on a node with no\n// outgoing edges, the walk ends immediately, and every dependency recorded\n// after the first import is demoted from watch to cache. That failure is\n// silent: the build still succeeds and simply stops reacting. Seeding every\n// spelling costs one extra queue entry and cannot be wrong.\nconst configUrlSpellings = [\n ...new Set([\n configUrl,\n pathToFileURL(configLocation).href,\n pathToFileURL(realConfigLocation()).href,\n ]),\n];\nconst configEntryUrls = new Set(configUrlSpellings);\nconst moduleProbeExtensions = [\n \".ts\",\n \".tsx\",\n \".mts\",\n \".cts\",\n \".js\",\n \".mjs\",\n \".cjs\",\n \".json\",\n \".node\",\n] as const;\nconst jsToTsProbeExtensions = new Map([\n [\".js\", [\".ts\", \".tsx\"]],\n [\".jsx\", [\".tsx\"]],\n [\".mjs\", [\".mts\"]],\n [\".cjs\", [\".cts\"]],\n]);\nfor (const spelling of configUrlSpellings) {\n graphNodes.set(spelling, configLocation);\n}\nrecordDependency(\n \"file\",\n configLocation,\n createHash(\"sha256\").update(fs.readFileSync(configLocation)).digest(\"hex\"),\n configUrlSpellings,\n);\nrecordPackageManifests(configLocation, configUrlSpellings);\n\ndeclare const process: {\n cwd(): string;\n platform: string;\n stdout: { write(value: string): void };\n stderr: { write(value: string): void };\n exit(code?: number): never;\n};\n\nconst hooks = registerHooks({\n resolve(specifier, context, nextResolve) {\n const requestedParent =\n context.parentURL && new URL(context.parentURL).href;\n if (requestedParent !== undefined && graphNodes.has(requestedParent)) {\n recordLocalResolutionCandidates(specifier, requestedParent);\n }\n const resolved = nextResolve(specifier, context);\n if (typeof resolved.url !== \"string\" || !resolved.url.startsWith(\"file:\")) {\n return resolved;\n }\n const url = new URL(resolved.url).href;\n const parent = context.parentURL && new URL(context.parentURL).href;\n const location = fileURLToPath(url);\n // The entry is recognized by what was asked for, not only by what came\n // back. A module URL is assigned by whoever loaded it: a compiling loader\n // can serve the config from its emitted output, and a platform can hand\n // back a different spelling of the same file. Either way the URL bears no\n // resemblance to the one this process was given, so the config's own\n // imports would be rejected here \u2014 their parent is a URL no node was\n // recorded under \u2014 and the graph would collapse to the records made before\n // the first import. The request itself is unambiguous, so it decides.\n const entry =\n configEntryUrls.has(specifier) ||\n url === new URL(configUrl).href ||\n samePhysicalPath(location, configLocation);\n if (!entry && (parent === undefined || !graphNodes.has(parent))) {\n return resolved;\n }\n graphNodes.set(url, location);\n if (entry && configEntryUrls.has(specifier) && specifier !== url) {\n graphEdges.push({ child: url, packageBoundary: false, parent: specifier });\n }\n if (parent !== undefined) {\n graphEdges.push({\n child: url,\n packageBoundary:\n pathHasNodeModules(location) && !isLocalModuleSpecifier(specifier),\n parent,\n });\n recordResolutionTopology(\n specifier,\n parent,\n url,\n location,\n context.conditions,\n );\n }\n try {\n recordDependency(\n \"file\",\n location,\n createHash(\"sha256\").update(fs.readFileSync(location)).digest(\"hex\"),\n [url],\n );\n } catch {\n // The evaluator remains authoritative for the load error. An unreadable\n // dependency simply makes this result non-cacheable in the parent.\n recordDependency(\"file\", location, \"\", [url]);\n }\n return resolved;\n },\n});\n\n// Wrapped and settled explicitly rather than written as a top-level await.\n// The loader tsconfig's \"module\" now follows the config's own package, and\n// TS1378 rejects top-level await under a CommonJS module option however this\n// .mts file emits. The trailing catch is what a top-level await gave for\n// free: without it a throw from the finally would leave the promise\n// unsettled instead of failing the load.\n(async () => {\n try {\n const importedConfig = configLocation.toLowerCase().endsWith(\".json\")\n ? JSON.parse(fs.readFileSync(configLocation, \"utf8\").replace(/^\uFEFF/, \"\"))\n : await import(configUrl);\n const current = await resolveConfig(importedConfig, true);\n const entries = await collectPluginEntries(current, configLocation, %CONFIG_CHAIN%);\n fs.writeFileSync(outputPath, JSON.stringify({\n dependencies: finalizeDependencies(),\n entries,\n }), \"utf8\");\n } catch (error) {\n reportLoaderFailure(error);\n } finally {\n hooks.deregister();\n }\n})().catch((error) => {\n // Reached only when the finally above throws: the catch already ends the\n // process, so this is the deregistration's own failure, not the config's.\n reportLoaderFailure(error);\n});\n\n// reportLoaderFailure ends this loader on an error it can name, on both of the\n// channels the parent uses.\n//\n// The stack is for a reader and streams to stderr as it is written. The reason\n// is a fact about the user's config that a caller has to act on, so it travels\n// as data through the result file the parent already reads. Only a well-formed\n// envelope is honoured there, so a partially written or unrelated file leaves\n// the process status to speak for itself.\nfunction reportLoaderFailure(error: unknown): never {\n // The trailing newline ends the stack as a line of its own. Without it the\n // parent's own message, written to this same stream, starts mid-line.\n process.stderr.write((error instanceof Error && error.stack ? error.stack : String(error)) + \"\\n\");\n try {\n fs.writeFileSync(\n outputPath,\n JSON.stringify({ __ttscLoaderError: error instanceof Error ? error.message : String(error) }),\n \"utf8\",\n );\n } catch {\n // A reason that cannot be written leaves the exit status as the report.\n }\n return process.exit(1);\n}\n\nfunction isObject(value: unknown): value is Record {\n return value !== null && typeof value === \"object\";\n}\n\nfunction recordDependency(\n kind: \"directory\" | \"file\" | \"optional-file\",\n location: string,\n digest: string,\n owners: readonly string[],\n): void {\n const key = kind + \"\\0\" + location;\n const previous = dependencies.get(key);\n const mergedOwners = previous?.owners ?? new Set();\n for (const owner of owners) mergedOwners.add(owner);\n const realpath = dependencyRealpath(location);\n const identityStable =\n previous?.identityStable !== false &&\n (previous === undefined || previous.realpath === realpath);\n dependencies.set(key, {\n digest:\n !identityStable ||\n (previous !== undefined && previous.digest !== digest)\n ? \"\"\n : digest,\n identityStable,\n kind,\n owners: mergedOwners,\n path: location,\n realpath,\n });\n}\n\nfunction dependencyRealpath(location: string): string | null {\n try {\n return realPath(location);\n } catch {\n return null;\n }\n}\n\nfunction isLocalModuleSpecifier(specifier: string): boolean {\n return specifier.startsWith(\".\") ||\n specifier.startsWith(\"/\") ||\n specifier.startsWith(\"file:\") ||\n /^[A-Za-z]:[\\\\/]/.test(specifier);\n}\n\nfunction pathHasNodeModules(location: string): boolean {\n return location.replaceAll(\"\\\\\", \"/\").split(\"/\").includes(\"node_modules\");\n}\n\nfunction recordResolutionTopology(\n specifier: string,\n parentUrl: string,\n childUrl: string,\n childLocation: string,\n conditions: readonly string[],\n): void {\n const owners = [parentUrl, childUrl];\n const parentLocation = graphNodes.get(parentUrl);\n if (parentLocation !== undefined && isLocalModuleSpecifier(specifier)) {\n recordDirectoryDependency(path.dirname(parentLocation), owners);\n }\n recordDirectoryDependency(path.dirname(childLocation), owners);\n recordPackageManifests(childLocation, owners);\n if (parentLocation !== undefined && !isLocalModuleSpecifier(specifier)) {\n recordNodeModulesSearchDirectories(\n parentLocation,\n specifier,\n childLocation,\n owners,\n conditions,\n );\n }\n}\n\nfunction recordDirectoryDependency(\n location: string,\n owners: readonly string[],\n): void {\n try {\n recordDependency(\"directory\", location, directoryDigest(location), owners);\n } catch {\n recordDependency(\"directory\", location, \"\", owners);\n }\n}\n\nfunction directoryDigest(location: string): string {\n const entries: Buffer[] = [];\n if (process.platform === \"win32\") {\n for (const entry of fs.readdirSync(location, { withFileTypes: true })) {\n let target = Buffer.alloc(0);\n if (entry.isSymbolicLink()) {\n try {\n target = Buffer.from(\n fs.readlinkSync(path.join(location, entry.name)),\n \"utf8\",\n );\n } catch {\n target = Buffer.from(\"\");\n }\n }\n entries.push(directoryDigestRecord(Buffer.from(entry.name), entry, target));\n }\n } else {\n for (const entry of fs.readdirSync(location, {\n encoding: \"buffer\",\n withFileTypes: true,\n })) {\n let target = Buffer.alloc(0);\n if (entry.isSymbolicLink()) {\n try {\n target = fs.readlinkSync(\n Buffer.concat([\n Buffer.from(location),\n Buffer.from(path.sep),\n entry.name,\n ]),\n { encoding: \"buffer\" },\n );\n } catch {\n target = Buffer.from(\"\");\n }\n }\n entries.push(directoryDigestRecord(entry.name, entry, target));\n }\n }\n entries.sort(Buffer.compare);\n const serialized = Buffer.concat(\n entries.flatMap((entry, index) =>\n index === 0 ? [entry] : [Buffer.from([0]), entry],\n ),\n );\n return createHash(\"sha256\").update(serialized).digest(\"hex\");\n}\n\nfunction directoryDigestRecord(\n name: Buffer,\n entry: {\n isDirectory(): boolean;\n isFile(): boolean;\n isSymbolicLink(): boolean;\n },\n target: Buffer,\n): Buffer {\n const kind = entry.isDirectory()\n ? \"directory\"\n : entry.isFile()\n ? \"file\"\n : entry.isSymbolicLink()\n ? \"symlink\"\n : \"other\";\n return Buffer.concat([name, Buffer.from(\"\\0\" + kind + \"\\0\"), target]);\n}\n\nfunction optionalFileDigest(location: string): string {\n try {\n const entry = fs.statSync(location);\n if (entry.isFile()) {\n return createHash(\"sha256\")\n .update(Buffer.concat([Buffer.from(\"file\\0\"), fs.readFileSync(location)]))\n .digest(\"hex\");\n }\n if (entry.isDirectory()) {\n return createHash(\"sha256\")\n .update(\"ttsc:host-input:directory\\0\")\n .digest(\"hex\");\n }\n } catch {\n // Missing and unreadable candidates share the absent state.\n }\n return createHash(\"sha256\").update(\"missing\\0\").digest(\"hex\");\n}\n\nfunction recordOptionalFileDependency(\n location: string,\n owners: readonly string[],\n): boolean {\n try {\n if (fs.statSync(location).isFile()) {\n recordDependency(\n \"file\",\n location,\n createHash(\"sha256\").update(fs.readFileSync(location)).digest(\"hex\"),\n owners,\n );\n return true;\n }\n } catch {\n // The exact missing path remains a dependency of the resolution result.\n }\n recordDependency(\"optional-file\", location, optionalFileDigest(location), owners);\n return false;\n}\n\nfunction moduleResolutionCandidates(base: string): string[] {\n const extension = path.extname(base).toLowerCase();\n const substitutions = jsToTsProbeExtensions.get(extension) ?? [];\n const stem = base.slice(0, base.length - extension.length);\n return [\n base,\n ...substitutions.map((candidate) => stem + candidate),\n ...moduleProbeExtensions.map((candidate) => base + candidate),\n path.join(base, \"package.json\"),\n ...moduleProbeExtensions.map((candidate) =>\n path.join(base, \"index\" + candidate),\n ),\n ];\n}\n\n/** Record exact local probes before the runtime resolver chooses one. */\nfunction recordLocalResolutionCandidates(\n specifier: string,\n parentUrl: string,\n): void {\n if (!isLocalModuleSpecifier(specifier)) return;\n let bases: string[];\n try {\n if (specifier.startsWith(\"file:\")) {\n bases = [fileURLToPath(specifier)];\n } else {\n const directory = path.dirname(fileURLToPath(parentUrl));\n const raw = path.resolve(directory, specifier);\n const suffixStart = specifier.search(/[?#]/);\n const pathname =\n suffixStart === -1 ? specifier : specifier.slice(0, suffixStart);\n bases = pathname === \"\"\n ? [raw]\n : [...new Set([raw, path.resolve(directory, pathname)])];\n }\n } catch {\n return;\n }\n const owners = [parentUrl];\n for (const base of bases) {\n try {\n if (fs.statSync(base).isFile()) {\n recordOptionalFileDependency(base, owners);\n continue;\n }\n } catch {\n // A missing exact spelling falls through to source/extension/directory\n // probes, all of which can redirect a later evaluation.\n }\n for (const candidate of moduleResolutionCandidates(base)) {\n recordOptionalFileDependency(candidate, owners);\n }\n }\n}\n\n/** CommonJS LOAD_AS_FILE / LOAD_AS_DIRECTORY candidates for one legacy path. */\nfunction recordLegacyPackagePathCandidates(\n candidate: string,\n owners: readonly string[],\n): void {\n for (const file of [\n candidate,\n candidate + \".js\",\n candidate + \".json\",\n candidate + \".node\",\n path.join(candidate, \"package.json\"),\n path.join(candidate, \"index.js\"),\n path.join(candidate, \"index.json\"),\n path.join(candidate, \"index.node\"),\n ]) {\n recordOptionalFileDependency(file, owners);\n }\n}\n\nfunction recordPackageManifests(\n location: string,\n owners: readonly string[],\n): void {\n let current = path.dirname(location);\n while (true) {\n const manifest = path.join(current, \"package.json\");\n if (recordOptionalFileDependency(manifest, owners)) return;\n const parent = path.dirname(current);\n if (parent === current || path.basename(current) === \"node_modules\") return;\n current = parent;\n }\n}\n\nfunction recordNodeModulesSearchDirectories(\n parentLocation: string,\n specifier: string,\n childLocation: string,\n owners: readonly string[],\n conditions: readonly string[],\n): void {\n const packageName = modulePackageName(specifier);\n const scope =\n specifier.startsWith(\"@\") && specifier.includes(\"/\")\n ? specifier.slice(0, specifier.indexOf(\"/\"))\n : undefined;\n let current = path.dirname(parentLocation);\n while (true) {\n // A newly created nearer node_modules directory can shadow the package\n // selected by this evaluation, so missing search levels are dependencies.\n recordDirectoryDependency(current, owners);\n const modules = path.join(current, \"node_modules\");\n try {\n if (fs.statSync(modules).isDirectory()) {\n recordDirectoryDependency(modules, owners);\n if (scope !== undefined) {\n const scoped = path.join(modules, scope);\n try {\n if (fs.statSync(scoped).isDirectory()) {\n recordDirectoryDependency(scoped, owners);\n }\n } catch {\n // The directory digest of node_modules records a missing scope.\n }\n }\n }\n } catch {\n // Missing search levels do not participate in the current resolution.\n }\n if (packageName !== undefined) {\n const selected = recordPackageCandidateTopology(\n modules,\n packageName,\n specifier,\n childLocation,\n owners,\n conditions,\n );\n if (\n selected ||\n resolvedPackageContains(modules, packageName, childLocation)\n ) {\n return;\n }\n }\n if (\n packageName === undefined &&\n samePhysicalPath(current, resolutionRoot)\n ) {\n return;\n }\n const parent = path.dirname(current);\n if (parent === current) return;\n current = parent;\n }\n}\n\nfunction recordPackageCandidateTopology(\n modules: string,\n packageName: string,\n specifier: string,\n childLocation: string,\n owners: readonly string[],\n conditions: readonly string[],\n): boolean {\n const packageRoot = path.join(modules, packageName);\n const subpath = specifier\n .slice(packageName.length)\n .replace(/^[/\\\\]+/, \"\");\n try {\n if (!fs.statSync(packageRoot).isDirectory()) {\n recordOptionalFileDependency(\n path.join(packageRoot, \"package.json\"),\n owners,\n );\n recordLegacyPackagePathCandidates(\n subpath === \"\" ? packageRoot : path.join(packageRoot, subpath),\n owners,\n );\n return false;\n }\n } catch {\n recordOptionalFileDependency(\n path.join(packageRoot, \"package.json\"),\n owners,\n );\n recordLegacyPackagePathCandidates(\n subpath === \"\" ? packageRoot : path.join(packageRoot, subpath),\n owners,\n );\n return false;\n }\n const rootTopology = recordPackageRootTopology(\n packageRoot,\n owners,\n subpath === \"\",\n subpath === \"\" ? \".\" : \"./\" + subpath.replaceAll(\"\\\\\", \"/\"),\n childLocation,\n conditions,\n );\n if (subpath !== \"\" && !rootTopology.hasExports) {\n return (\n recordPackageSubpathTopology(\n packageRoot,\n subpath,\n childLocation,\n owners,\n ) || rootTopology.selected\n );\n }\n return rootTopology.selected;\n}\n\nfunction recordPackageRootTopology(\n packageRoot: string,\n owners: readonly string[],\n useMain: boolean,\n packageSubpath: string,\n childLocation: string,\n conditions: readonly string[],\n): { hasExports: boolean; selected: boolean } {\n const normalizedRoot = path.resolve(packageRoot);\n const manifest = path.join(normalizedRoot, \"package.json\");\n const legacySelected = (): boolean =>\n useMain &&\n packagePathCandidateMatchesChild(normalizedRoot, childLocation, true);\n if (useMain) recordLegacyPackagePathCandidates(normalizedRoot, owners);\n if (!recordOptionalFileDependency(manifest, owners)) {\n const selected = legacySelected();\n if (!selected) {\n recordPackageIndexCandidates(normalizedRoot, useMain, owners);\n }\n return { hasExports: false, selected };\n }\n try {\n const value = JSON.parse(fs.readFileSync(manifest, \"utf8\"));\n if (value !== null && typeof value === \"object\") {\n const metadata = value as Record;\n const hasExports =\n metadata.exports !== undefined && metadata.exports !== null;\n if (hasExports) {\n const target = selectPackageExportsTarget(\n metadata.exports,\n packageSubpath,\n new Set(conditions),\n );\n const candidate =\n typeof target === \"string\"\n ? packageExportsTarget(normalizedRoot, target)\n : undefined;\n const selected =\n candidate !== undefined &&\n packagePathCandidateMatchesChild(\n candidate,\n childLocation,\n false,\n );\n if (selected) {\n recordPackagePathCandidate(candidate, owners);\n } else if (candidate !== undefined) {\n // A nearer package the search skipped starts winning the moment its\n // own active target appears, and neither the parent node_modules\n // listing nor the manifest changes when only that file is created.\n recordOptionalFileDependency(candidate, owners);\n }\n return { hasExports: true, selected };\n }\n let selected = legacySelected();\n if (useMain && typeof metadata.main === \"string\") {\n // CommonJS main is a legacy path, not an exports target. Node resolves\n // it literally and permits absolute paths and paths outside the package.\n const main = path.resolve(normalizedRoot, metadata.main);\n recordPackagePathCandidate(main, owners);\n recordLegacyPackagePathCandidates(main, owners);\n selected =\n packagePathCandidateMatchesChild(main, childLocation, true) ||\n selected;\n }\n if (!selected) {\n recordPackageIndexCandidates(normalizedRoot, useMain, owners);\n }\n return {\n hasExports: false,\n selected,\n };\n }\n } catch {\n // Node owns malformed-manifest diagnostics; the manifest digest is enough\n // to invalidate this evaluation when its contents change.\n }\n const selected = legacySelected();\n if (!selected) {\n recordPackageIndexCandidates(normalizedRoot, useMain, owners);\n }\n return { hasExports: false, selected };\n}\n\n// recordPackageIndexCandidates pins the LOAD_INDEX fallbacks of a package root\n// this resolution walked past without selecting. An empty package directory, or\n// one whose manifest declares no usable entry, becomes resolvable as soon as one\n// of these files exists, and that creation changes neither the parent directory\n// listing nor the manifest digest already recorded for the candidate.\nfunction recordPackageIndexCandidates(\n packageRoot: string,\n useMain: boolean,\n owners: readonly string[],\n): void {\n if (!useMain) return;\n for (const name of [\"index.js\", \"index.json\", \"index.node\"]) {\n recordOptionalFileDependency(path.join(packageRoot, name), owners);\n }\n}\n\nfunction selectPackageExportsTarget(\n exportsValue: unknown,\n packageSubpath: string,\n conditions: ReadonlySet,\n): string | null | undefined {\n let mappings: unknown = exportsValue;\n if (\n typeof mappings === \"string\" ||\n Array.isArray(mappings) ||\n (isObject(mappings) &&\n Object.keys(mappings).every((key) => !key.startsWith(\".\")))\n ) {\n if (packageSubpath !== \".\") return undefined;\n return selectPackageTarget(mappings, \"\", false, conditions);\n }\n if (!isObject(mappings)) return undefined;\n if (\n Object.prototype.hasOwnProperty.call(mappings, packageSubpath) &&\n !packageSubpath.includes(\"*\") &&\n !packageSubpath.endsWith(\"/\")\n ) {\n return selectPackageTarget(\n mappings[packageSubpath],\n \"\",\n false,\n conditions,\n );\n }\n let bestMatch = \"\";\n let bestSubpath = \"\";\n for (const key of Object.keys(mappings)) {\n const wildcard = key.indexOf(\"*\");\n if (\n wildcard === -1 ||\n key.lastIndexOf(\"*\") !== wildcard ||\n !packageSubpath.startsWith(key.slice(0, wildcard))\n ) {\n continue;\n }\n const trailer = key.slice(wildcard + 1);\n if (\n packageSubpath.length < key.length ||\n !packageSubpath.endsWith(trailer) ||\n packagePatternKeyCompare(bestMatch, key) !== 1\n ) {\n continue;\n }\n bestMatch = key;\n bestSubpath = packageSubpath.slice(\n wildcard,\n packageSubpath.length - trailer.length,\n );\n }\n return bestMatch === \"\"\n ? undefined\n : selectPackageTarget(\n mappings[bestMatch],\n bestSubpath,\n true,\n conditions,\n );\n}\n\nfunction selectPackageTarget(\n target: unknown,\n subpath: string,\n pattern: boolean,\n conditions: ReadonlySet,\n): string | null | undefined {\n if (typeof target === \"string\") {\n const selected = pattern ? target.replaceAll(\"*\", subpath) : target;\n return validPackageExportsTarget(selected) ? selected : undefined;\n }\n if (Array.isArray(target)) {\n for (const item of target) {\n const selected = selectPackageTarget(\n item,\n subpath,\n pattern,\n conditions,\n );\n if (selected !== undefined && selected !== null) return selected;\n }\n return null;\n }\n if (isObject(target)) {\n for (const [condition, value] of Object.entries(target)) {\n if (condition !== \"default\" && !conditions.has(condition)) continue;\n const selected = selectPackageTarget(\n value,\n subpath,\n pattern,\n conditions,\n );\n if (selected !== undefined) return selected;\n }\n return undefined;\n }\n return target === null ? null : undefined;\n}\n\nfunction packagePatternKeyCompare(left: string, right: string): number {\n const leftWildcard = left.indexOf(\"*\");\n const rightWildcard = right.indexOf(\"*\");\n const leftBase =\n leftWildcard === -1 ? left.length : leftWildcard + 1;\n const rightBase =\n rightWildcard === -1 ? right.length : rightWildcard + 1;\n if (leftBase > rightBase) return -1;\n if (rightBase > leftBase) return 1;\n if (leftWildcard === -1) return 1;\n if (rightWildcard === -1) return -1;\n if (left.length > right.length) return -1;\n if (right.length > left.length) return 1;\n return 0;\n}\n\nfunction packageExportsTarget(\n packageRoot: string,\n target: string,\n): string | undefined {\n if (!validPackageExportsTarget(target)) return undefined;\n try {\n // Node resolves an exports target as a URL against the package manifest,\n // so percent escapes, query strings, and fragments all take part in the\n // path it finally loads. Joining the raw target by hand diverges from that\n // whenever the target is anything but a plain relative path, and a target\n // Node resolves while this model rejects loses the selected file's\n // fingerprint, leaving a retargeted symlink cached as fresh.\n const packageUrl = pathToFileURL(path.join(packageRoot, \"package.json\"));\n const resolved = new URL(target, packageUrl);\n const packagePath = new URL(\".\", packageUrl).pathname;\n if (!resolved.pathname.startsWith(packagePath)) return undefined;\n return fileURLToPath(resolved);\n } catch {\n return undefined;\n }\n}\n\nfunction validPackageExportsTarget(target: string): boolean {\n if (!target.startsWith(\"./\") || /%2f|%5c/i.test(target)) return false;\n const components = target\n .slice(2)\n .replaceAll(\"\\\\\", \"/\")\n .split(\"/\");\n if (\n components.some(\n (component) => {\n try {\n const decoded = decodeURIComponent(component);\n return (\n decoded === \".\" ||\n decoded === \"..\" ||\n decoded.includes(\"/\") ||\n decoded.includes(\"\\\\\") ||\n decoded.toLowerCase() === \"node_modules\"\n );\n } catch {\n return true;\n }\n },\n )\n ) {\n return false;\n }\n return true;\n}\n\nfunction packagePathCandidateMatchesChild(\n candidate: string,\n childLocation: string,\n legacy: boolean,\n): boolean {\n let child: string;\n try {\n child = fs.realpathSync.native(childLocation);\n } catch {\n child = path.resolve(childLocation);\n }\n const candidates = legacy\n ? [\n candidate,\n candidate + \".js\",\n candidate + \".json\",\n candidate + \".node\",\n path.join(candidate, \"index.js\"),\n path.join(candidate, \"index.json\"),\n path.join(candidate, \"index.node\"),\n ]\n : [candidate];\n return candidates.some((location) => {\n try {\n return sameResolutionPath(fs.realpathSync.native(location), child);\n } catch {\n return false;\n }\n });\n}\n\nfunction recordPackageSubpathTopology(\n packageRoot: string,\n subpath: string,\n childLocation: string,\n owners: readonly string[],\n): boolean {\n const candidate = boundedPackageTarget(packageRoot, subpath);\n if (candidate === undefined) return false;\n recordPackagePathCandidate(candidate, owners);\n recordLegacyPackagePathCandidates(candidate, owners);\n let selected = packagePathCandidateMatchesChild(\n candidate,\n childLocation,\n true,\n );\n try {\n if (!fs.statSync(candidate).isDirectory()) return selected;\n } catch {\n return selected;\n }\n const manifest = path.join(candidate, \"package.json\");\n if (!recordOptionalFileDependency(manifest, owners)) return selected;\n try {\n const value = JSON.parse(fs.readFileSync(manifest, \"utf8\"));\n if (value !== null && typeof value === \"object\") {\n const metadata = value as Record;\n if (typeof metadata.main === \"string\") {\n const main = path.resolve(candidate, metadata.main);\n recordPackagePathCandidate(main, owners);\n recordLegacyPackagePathCandidates(main, owners);\n selected =\n packagePathCandidateMatchesChild(main, childLocation, true) ||\n selected;\n }\n }\n } catch {\n // Node owns malformed nested-package diagnostics.\n }\n return selected;\n}\n\nfunction boundedPackageTarget(\n packageRoot: string,\n target: string,\n): string | undefined {\n const candidate = path.resolve(packageRoot, target);\n const relative = path.relative(packageRoot, candidate);\n if (\n relative === \"..\" ||\n relative.startsWith(\"..\" + path.sep) ||\n path.isAbsolute(relative)\n ) {\n return undefined;\n }\n return candidate;\n}\n\nfunction recordPackagePathCandidate(\n candidate: string,\n owners: readonly string[],\n visited: Set = new Set(),\n depth = 0,\n): void {\n const normalized = path.resolve(candidate);\n // The depth bound owns termination. A platform-wide case fold would merge\n // paths that differ only by case, which a per-directory case-sensitive\n // Windows tree keeps distinct, and would truncate a valid symlink chain.\n if (depth >= 64 || visited.has(normalized)) return;\n visited.add(normalized);\n const parsed = path.parse(normalized);\n const components = normalized\n .slice(parsed.root.length)\n .split(path.sep)\n .filter(Boolean);\n let current = parsed.root;\n for (let index = 0; index < components.length; index++) {\n const component = components[index];\n const next = path.join(current, component);\n let entry: ReturnType;\n try {\n entry = fs.lstatSync(next);\n } catch {\n recordDirectoryDependency(current, owners);\n return;\n }\n if (entry.isSymbolicLink()) {\n // The containing directory digest carries the raw link target.\n recordDirectoryDependency(current, owners);\n try {\n const target = fs.readlinkSync(next);\n const remainder = components.slice(index + 1);\n recordPackagePathCandidate(\n path.join(\n path.resolve(current, target),\n ...remainder,\n ),\n owners,\n visited,\n depth + 1,\n );\n } catch {\n // The lexical link record already carries the unreadable state.\n }\n }\n let isDirectory = entry.isDirectory();\n if (entry.isSymbolicLink()) {\n try {\n isDirectory = fs.statSync(next).isDirectory();\n } catch {\n return;\n }\n }\n if (index === components.length - 1) {\n recordDirectoryDependency(isDirectory ? next : current, owners);\n return;\n }\n if (!isDirectory) {\n recordDirectoryDependency(current, owners);\n return;\n }\n current = next;\n }\n recordDirectoryDependency(current, owners);\n}\n\nfunction modulePackageName(specifier: string): string | undefined {\n if (specifier.startsWith(\"@\")) {\n const components = specifier.split(\"/\");\n return components.length >= 2\n ? components[0] + \"/\" + components[1]\n : undefined;\n }\n const [name] = specifier.split(\"/\");\n return name && !name.startsWith(\"#\") ? name : undefined;\n}\n\nfunction resolvedPackageContains(\n modules: string,\n packageName: string,\n childLocation: string,\n): boolean {\n try {\n const packageRoot = fs.realpathSync(path.join(modules, packageName));\n const child = fs.realpathSync(childLocation);\n const relative = path.relative(packageRoot, child);\n if (relative === \"..\" || relative.startsWith(\"..\" + path.sep) || path.isAbsolute(relative)) {\n return false;\n }\n let ancestor = child;\n const depth = relative === \"\" ? 0 : relative.split(path.sep).length;\n for (let i = 0; i < depth; i++) ancestor = path.dirname(ancestor);\n return sameResolutionPath(packageRoot, ancestor);\n } catch {\n return false;\n }\n}\n\nfunction sameResolutionPath(left: string, right: string): boolean {\n const leftPath = path.resolve(left);\n const rightPath = path.resolve(right);\n if (leftPath === rightPath) return true;\n if (path.relative(leftPath, rightPath) !== \"\") return false;\n // A native lexical case fold does not establish filesystem identity.\n // Verify case aliases without merging differently located hard links.\n try {\n const leftInfo = fs.statSync(leftPath, { bigint: true });\n const rightInfo = fs.statSync(rightPath, { bigint: true });\n return leftInfo.ino !== 0n && leftInfo.dev === rightInfo.dev && leftInfo.ino === rightInfo.ino;\n } catch {\n return false;\n }\n}\n\nfunction samePhysicalPath(left: string, right: string): boolean {\n try {\n return sameResolutionPath(realPath(left), realPath(right));\n } catch {\n // Exact lexical spelling is still usable when a file disappears. A case\n // alias remains unproven until the filesystem identifies both paths.\n return sameResolutionPath(left, right);\n }\n}\n\n/**\n * The config's real path, or its declared one when the volume will not say.\n *\n * A config can disappear between the host reading it and this loader starting,\n * and a throw here would replace a precise report from the import below with a\n * crash in bookkeeping. Seeding lexically instead only risks the demotion this\n * value exists to prevent, on a file that is already gone.\n */\nfunction realConfigLocation(): string {\n try {\n return realPath(configLocation);\n } catch {\n return configLocation;\n }\n}\n\nfunction realPath(location: string): string {\n return fs.realpathSync.native\n ? fs.realpathSync.native(location)\n : fs.realpathSync(location);\n}\n\nfunction finalizeDependencies(): Array<{\n digest: string;\n identityStable: boolean;\n kind: \"directory\" | \"file\" | \"optional-file\";\n path: string;\n realpath: string | null;\n scope: \"cache\" | \"watch\";\n}> {\n // The evaluator may have observed a dependency, run arbitrary config code,\n // and then serialize after that path changed again. Re-read every recorded\n // dependency under its original ownership set. Differences between recorded\n // observations invalidate reuse; identical digest and realpath observations\n // do not establish that no transient replacement occurred between them.\n for (const dependency of [...dependencies.values()]) {\n recordDependency(\n dependency.kind,\n dependency.path,\n currentDependencyDigest(dependency.kind, dependency.path),\n [...dependency.owners],\n );\n }\n const watched = graphWatchReachability();\n return [...dependencies.values()].map(({ owners, ...dependency }) => ({\n ...dependency,\n scope: [...owners].some((owner) => watched.has(owner))\n ? \"watch\"\n : \"cache\",\n }));\n}\n\nfunction currentDependencyDigest(\n kind: \"directory\" | \"file\" | \"optional-file\",\n location: string,\n): string {\n try {\n if (kind === \"directory\") return directoryDigest(location);\n if (kind === \"optional-file\") return optionalFileDigest(location);\n return createHash(\"sha256\")\n .update(fs.readFileSync(location))\n .digest(\"hex\");\n } catch {\n return \"\";\n }\n}\n\nfunction graphWatchReachability(): Set {\n const adjacency = new Map();\n for (const edge of graphEdges) {\n const outgoing = adjacency.get(edge.parent) ?? [];\n outgoing.push(edge);\n adjacency.set(edge.parent, outgoing);\n }\n const queue: Array<{ url: string; watched: boolean }> =\n configUrlSpellings.map((url) => ({ url, watched: true }));\n const visited = new Set();\n const watched = new Set();\n for (let head = 0; head < queue.length; head++) {\n const state = queue[head]!;\n const key = state.url + \"\\0\" + (state.watched ? \"1\" : \"0\");\n if (visited.has(key)) continue;\n visited.add(key);\n if (state.watched) watched.add(state.url);\n for (const edge of adjacency.get(state.url) ?? []) {\n const childLocation = graphNodes.get(edge.child);\n const childWatched = edge.packageBoundary\n ? false\n : childLocation !== undefined && !pathHasNodeModules(childLocation)\n ? true\n : state.watched;\n queue.push({ url: edge.child, watched: childWatched });\n }\n }\n return watched;\n}\n\nfunction hasOwn(value: Record, key: string): boolean {\n return Object.prototype.hasOwnProperty.call(value, key);\n}\n\nasync function resolveConfig(value: unknown, allowNamedConfig: boolean): Promise {\n let current = value;\n for (let i = 0; i < 8; i++) {\n if (typeof current === \"function\") {\n current = await (current as () => unknown | Promise)();\n allowNamedConfig = false;\n continue;\n }\n if (isObject(current) && !Array.isArray(current)) {\n if (hasOwn(current, \"default\")) {\n const defaultValue = current.default;\n if (isModuleNamespace(current) || !hasConfigKey(current)) {\n current = defaultValue;\n allowNamedConfig = false;\n continue;\n }\n const normalizedDefault = await resolveConfig(defaultValue, false);\n if (isObject(normalizedDefault) && !Array.isArray(normalizedDefault)) {\n current = mergeConfigObjects(normalizedDefault, current);\n allowNamedConfig = false;\n continue;\n }\n }\n if (allowNamedConfig && hasOwn(current, \"config\")) {\n current = current.config;\n allowNamedConfig = false;\n continue;\n }\n }\n break;\n }\n return current;\n}\n\nfunction isModuleNamespace(value: Record): boolean {\n return Object.prototype.toString.call(value) === \"[object Module]\";\n}\n\nfunction hasConfigKey(value: Record): boolean {\n for (const key of CONFIG_KEYS) {\n if (hasOwn(value, key)) {\n return true;\n }\n }\n return false;\n}\n\nfunction mergeConfigObjects(\n base: Record,\n override: Record,\n): Record {\n const out: Record = {};\n for (const key of CONFIG_KEYS) {\n if (hasOwn(base, key)) {\n out[key] = base[key];\n }\n }\n for (const key of CONFIG_KEYS) {\n if (hasOwn(override, key)) {\n out[key] = override[key];\n }\n }\n return out;\n}\n\n// Follow the same containing-file-relative, base-first extends chain as the\n// native config reader. Each explicit base is a watch input even when it lives\n// in node_modules; imports made by that base retain ordinary package boundaries.\nasync function collectPluginEntries(\n value: unknown,\n location: string,\n chain: readonly string[],\n): Promise> {\n const out: Array<{ namespace: string; source: string } | { configFile: string; chain: readonly string[] }> = [];\n if (Array.isArray(value)) {\n for (const item of value) out.push(...await collectPluginEntries(item, location, chain));\n return out;\n }\n if (!isObject(value)) return out;\n if (value.extends !== undefined && value.extends !== null) {\n if (typeof value.extends !== \"string\" || value.extends.trim() === \"\") {\n throw new Error(\"extends must be a non-empty string path to another config file\");\n }\n const next = path.resolve(path.dirname(location), value.extends);\n if (chain.includes(next)) {\n throw new Error(\"extends cycle detected: \" + [...chain, next].join(\" -> \"));\n }\n if (chain.length >= 32) {\n throw new Error(\"extends chain exceeds the depth limit of 32: \" + chain.join(\" -> \"));\n }\n const nextUrl = pathToFileURL(next).href;\n const parentUrl = pathToFileURL(location).href;\n configEntryUrls.add(nextUrl);\n graphNodes.set(nextUrl, next);\n graphEdges.push({ child: nextUrl, packageBoundary: false, parent: parentUrl });\n recordDependency(\"file\", next, createHash(\"sha256\").update(fs.readFileSync(next)).digest(\"hex\"), [nextUrl]);\n recordPackageManifests(next, [nextUrl]);\n // The owning parent evaluates typed bases under their own synthetic project.\n // A dynamic import here would give a typed base the consumer's tsconfig\n // when it falls outside this evaluator's entry manifest.\n if ([\".ts\", \".cts\", \".mts\"].includes(path.extname(next).toLowerCase())) {\n out.push({ configFile: next, chain: [...chain, next] });\n } else {\n const imported = next.toLowerCase().endsWith(\".json\")\n ? JSON.parse(fs.readFileSync(next, \"utf8\").replace(/^\uFEFF/, \"\"))\n : await import(nextUrl);\n const base = await resolveConfig(imported, true);\n out.push(...await collectPluginEntries(base, next, [...chain, next]));\n }\n }\n if (hasOwn(value, \"plugins\") && isObject(value.plugins)) {\n for (const [namespace, plugin] of Object.entries(value.plugins)) {\n const source = extractPluginSource(plugin, location);\n if (source === undefined || source.length === 0) {\n throw new Error(\"contributor \" + JSON.stringify(namespace) + \" must resolve to an object with a non-empty source string\");\n }\n out.push({ namespace, source });\n }\n }\n return out;\n}\n\nfunction extractPluginSource(value: unknown, location: string): string | undefined {\n if (typeof value === \"string\") {\n value = createRequire(pathToFileURL(location))(value);\n }\n if (!isObject(value)) return undefined;\n // ESM-from-CJS interop wraps CJS modules' `exports.default` so the\n // plugin object can land under a `.default` indirection. Walk a few\n // hops so contributors authored as `export default plugin` and\n // contributors authored as plain `module.exports = plugin` both\n // resolve identically.\n let current: Record = value;\n // Bound default-wrapper traversal so a cyclic descriptor cannot loop.\n for (let i = 0; i < 8; i++) {\n if (typeof current.source === \"string\") break;\n const next = current.default;\n if (!isObject(next)) break;\n current = next;\n }\n const source = current.source;\n return typeof source === \"string\" ? source : undefined;\n}\n"; /** * Creates the typed config evaluator below a stable same-volume cache directory * when a descriptor load has already opened one. The identity directory exists * before descriptor imports are observed, so creating and removing an evaluator * below it leaves the project directory's missing-path witness unchanged. A * moved, aliased, unwritable, or differently mounted identity directory uses * the established loader placement instead. TypeScript-Go needs the generated * loader and the lexical config path on one volume (#305), so a TEMP junction * cannot override the volume of the canonical loader path. Without a suitable * cache, an existing user temp directory outside the config tree is preferred * to writing beside the config. A TEMP path in or above the declared project or * config directory yields to an existing identity store so observed ancestor * metadata remains stable. An independent config evaluation without descriptor * input capture may retain a strict external ancestor as its historical system * temp. No safe same-volume directory is an error. * * @evidence contracts/common.md#principled-implementation Native realpath and the returned canonical child must share the config's lexical volume; a rejected empty child is removed by its physical path. A system temp overlapping a physical project or config ancestor yields to an authoritative identity store, and descriptor-load children retain conservative ancestor handling. Descriptor identities need physical child and config-ancestor checks, and alternate user temp candidates must already exist outside the config tree. * @evidence contracts/common.md#clear-and-simple-design This function owns placement, while the existing canonical creator and nearest-cache selector retain their respective creation and discovery responsibilities. * @evidence contracts/common.md#prohibited-implementation-shortcuts Selection uses the descriptor's actual cache root and native filesystem identity, and every accepted fallback satisfies the compiler's lexical-volume requirement without returning a mutable junction spelling. * @evidence contracts/common.md#meaningful-documentation The comment explains the preexisting cache preference, TEMP junction and ancestor handling, and explicit failure when no usable same-volume directory exists. */ export declare function createConfigLoaderTempDirectory(configPath: string, systemTemp?: string, recordRoot?: string | undefined, projectRoot?: string): string; export {};