@ttsc/lint 0.20.1 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1182,6 +1182,33 @@ func init() { rule.RegisterProject(noCycles{}) }
1182
1182
 
1183
1183
  Each project rule runs once per loaded Program, before file rules. `ctx.Identity` includes the invocation cwd, logical and physical config paths and roots, an optional explicit project root, the plugin-config origin, and a lifecycle id. `Report` marks the rule failed and emits one project finding; `Fail` marks it failed without a finding. Later file rules can call `ctx.ProjectResult(name)` and distinguish `absent`, `off`, `not_evaluated`, `passed`, and `failed`.
1184
1184
 
1185
+ When a project rule reads local files outside the TypeScript Program, implement `rule.ProjectInputRule` so watch and editor hosts can observe exactly those inputs:
1186
+
1187
+ ```go
1188
+ func (evidenceRule) ProjectInputs(ctx *rule.ProjectInputContext) []rule.ProjectInput {
1189
+ var options struct {
1190
+ Markdown []string `json:"markdown"`
1191
+ OpenAPI string `json:"openapi"`
1192
+ }
1193
+ if err := ctx.DecodeOptions(&options); err != nil {
1194
+ panic(err)
1195
+ }
1196
+ inputs := []rule.ProjectInput{{
1197
+ Kind: rule.ProjectInputFile,
1198
+ Pattern: options.OpenAPI,
1199
+ }}
1200
+ for _, pattern := range options.Markdown {
1201
+ inputs = append(inputs, rule.ProjectInput{
1202
+ Kind: rule.ProjectInputGlob,
1203
+ Pattern: pattern,
1204
+ })
1205
+ }
1206
+ return inputs
1207
+ }
1208
+ ```
1209
+
1210
+ Relative patterns are anchored to `ctx.Identity.PhysicalProjectRoot`. Exact files remain dependencies while missing, and globs remain populations while they match nothing, so later create, rename, and repair events are observable. The host normalizes symlink aliases and shares duplicate declarations before publishing one snapshot. Declare configured topology rather than only files a successful `Check` happened to read; `ProjectInputs` runs after options and project identity are resolved but before a TypeScript Program is loaded. HTTP(S) URLs are not filesystem inputs and require a contributor-owned polling or conditional-revalidation policy.
1211
+
1185
1212
  Use `ctx.SetState(value)` when a later file rule needs the exact project binding selected during that check. The host returns the same value without interpreting or serializing it:
1186
1213
 
1187
1214
  ```go
package/lib/index.d.ts CHANGED
@@ -12,6 +12,9 @@ type TtscPluginDescriptor = {
12
12
  diagnosticsTiming?: boolean;
13
13
  lsp?: boolean;
14
14
  projectContextArgs?: boolean;
15
+ projectDiagnostics?: boolean;
16
+ projectInputs?: boolean;
17
+ residentCheck?: boolean;
15
18
  threadingArgs?: boolean;
16
19
  };
17
20
  contributors?: TtscPluginContributor[];
@@ -59,3 +62,11 @@ type TtscPluginFactoryContext<TConfig> = {
59
62
  * descriptor's `contributors` field.
60
63
  */
61
64
  export default function createTtscPlugin(context: TtscPluginFactoryContext<ITtscLintPluginConfig>): TtscPluginDescriptor;
65
+ /**
66
+ * The descriptor extractor's emitted source.
67
+ *
68
+ * Exported so a regression can inspect the same bytes the loader executes. The
69
+ * template consumes its own escapes, so reading this file's text instead would
70
+ * check characters no consumer ever sees.
71
+ */
72
+ 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 requireFromConfig = createRequire(configUrl);\nconst CONFIG_KEYS = new Set<string>([\n \"files\",\n \"ignores\",\n \"extends\",\n \"plugins\",\n \"rules\",\n \"format\",\n]);\nconst dependencies = new Map<string, {\n digest: string;\n kind: \"directory\" | \"file\" | \"optional-file\";\n path: string;\n owners: Set<string>;\n}>();\nconst graphNodes = new Map<string, string>();\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];\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 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 specifier === configUrl ||\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 (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\ntry {\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 pluginMaps = collectPluginObjects(current);\n const entries: Array<{ namespace: string; source: string }> = [];\n for (const map of pluginMaps) {\n for (const [namespace, value] of Object.entries(map)) {\n const source = extractPluginSource(value);\n if (source === undefined || source.length === 0) {\n throw new Error(\n `contributor ${JSON.stringify(namespace)} must resolve to an object with a non-empty \"source\" string`,\n );\n }\n entries.push({ namespace, source });\n }\n }\n fs.writeFileSync(outputPath, JSON.stringify({\n dependencies: finalizeDependencies(),\n entries,\n }), \"utf8\");\n} catch (error) {\n process.stderr.write(error instanceof Error && error.stack ? error.stack : String(error));\n process.exit(1);\n} finally {\n hooks.deregister();\n}\n\nfunction isObject(value: unknown): value is Record<string, unknown> {\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<string>();\n for (const owner of owners) mergedOwners.add(owner);\n dependencies.set(key, {\n digest: previous !== undefined && previous.digest !== digest ? \"\" : digest,\n kind,\n owners: mergedOwners,\n path: location,\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(\"<unreadable>\");\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(\"<unreadable>\");\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 if (fs.statSync(location).isFile()) {\n return createHash(\"sha256\")\n .update(Buffer.concat([Buffer.from(\"file\\0\"), fs.readFileSync(location)]))\n .digest(\"hex\");\n }\n } catch {\n // Missing, unreadable, and non-file 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 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 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 }\n } catch {\n // Missing search levels do not participate in the current resolution.\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 try {\n if (!fs.statSync(packageRoot).isDirectory()) return false;\n } catch {\n return false;\n }\n const subpath = specifier\n .slice(packageName.length)\n .replace(/^[/\\\\]+/, \"\");\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 (!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<string, unknown>;\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 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<string>,\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<string>,\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 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<string, unknown>;\n if (typeof metadata.main === \"string\") {\n const main = path.resolve(candidate, metadata.main);\n recordPackagePathCandidate(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<string> = 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<typeof fs.lstatSync>;\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 relative = path.relative(\n packageRoot,\n fs.realpathSync(childLocation),\n );\n return (\n relative === \"\" ||\n (relative !== \"..\" &&\n !relative.startsWith(\"..\" + path.sep) &&\n !path.isAbsolute(relative))\n );\n } catch {\n return false;\n }\n}\n\nfunction sameResolutionPath(left: string, right: string): boolean {\n return path.relative(left, right) === \"\";\n}\n\nfunction samePhysicalPath(left: string, right: string): boolean {\n try {\n return sameResolutionPath(realPath(left), realPath(right));\n } catch {\n // Fall back to the spellings themselves, folding case the way the platform\n // does. On the entry gate a false negative is catastrophic \u2014 the config\n // stops being recognized and its whole graph collapses \u2014 while a false\n // positive only over-includes, so the degradation has to lean toward \"same\n // file\". A drive-letter or component case difference is the ordinary\n // Windows situation; a per-directory case-sensitive tree is the rare one.\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 kind: \"directory\" | \"file\" | \"optional-file\";\n path: string;\n scope: \"cache\" | \"watch\";\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 graphWatchReachability(): Set<string> {\n const adjacency = new Map<string, typeof graphEdges>();\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<string>();\n const watched = new Set<string>();\n while (queue.length !== 0) {\n const state = queue.shift()!;\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<string, unknown>, key: string): boolean {\n return Object.prototype.hasOwnProperty.call(value, key);\n}\n\nasync function resolveConfig(value: unknown, allowNamedConfig: boolean): Promise<unknown> {\n let current = value;\n for (let i = 0; i < 8; i++) {\n if (typeof current === \"function\") {\n current = await (current as () => unknown | Promise<unknown>)();\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<string, unknown>): boolean {\n return Object.prototype.toString.call(value) === \"[object Module]\";\n}\n\nfunction hasConfigKey(value: Record<string, unknown>): 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<string, unknown>,\n override: Record<string, unknown>,\n): Record<string, unknown> {\n const out: Record<string, unknown> = {};\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\nfunction collectPluginObjects(value: unknown): Array<Record<string, unknown>> {\n const out: Array<Record<string, unknown>> = [];\n visit(value);\n return out;\n\n function visit(node: unknown): void {\n if (Array.isArray(node)) {\n for (const item of node) visit(item);\n return;\n }\n if (!isObject(node)) return;\n if (hasOwn(node, \"plugins\") && isObject(node.plugins)) {\n out.push(node.plugins as Record<string, unknown>);\n }\n }\n}\n\nfunction extractPluginSource(value: unknown): string | undefined {\n if (typeof value === \"string\") {\n value = requireFromConfig(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<string, unknown> = value;\n // 8 hops to match the outer-process unwrapDefault helper; previously\n // 4, which silently misrouted deeply re-exported plugins while\n // unwrapDefault would have unwrapped them.\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";