/** * Post-processing for analyzed library metadata. * * These functions transform analysis results after module-level analysis is complete: * * 1. **Validation** — `findDuplicates` checks flat namespace constraints * 2. **Transformation** — `mergeReExports` resolves re-export relationships, * `computeDependents` builds bidirectional dependency graphs * 3. **Output** — `sortModules` prepares deterministic output * * @see `analyze.ts` for the main analysis entry point * * @module */ import type {ComponentDeclarationJson, DeclarationJson, ModuleJson} from './types.js'; import type {SourceFileInfo} from './source.js'; import {toPosixPath} from './paths.js'; /** * Posixify every entry in `arr`. Returns the same array reference when no * entry needed normalization, so identity-equality short-circuits in * `computeDependents`'s no-rewrite branch. */ const posixifyArray = ( arr: ReadonlyArray<string> | undefined, ): ReadonlyArray<string> | undefined => { if (!arr) return arr; let changed: Array<string> | null = null; for (let i = 0; i < arr.length; i++) { const p = toPosixPath(arr[i]!); if (p !== arr[i]) { if (!changed) changed = arr.slice(); changed[i] = p; } } return changed ?? arr; }; /** * A duplicate declaration with its full metadata and module path. */ export interface DuplicateDeclaration { /** The full declaration metadata. */ declaration: DeclarationJson; /** Module path where this declaration is defined. */ module: string; } /** * Build the `(module, name)` → declaration lookup used for `aliasOf`-chain * walking. Key format: `` `${modulePath}\n${name}` `` — `\n` can't appear in * either part. */ const buildDeclarationIndex = (modules: Array<ModuleJson>): Map<string, DeclarationJson> => { const byIdentity: Map<string, DeclarationJson> = new Map(); for (const mod of modules) { for (const declaration of mod.declarations) { byIdentity.set(`${mod.path}\n${declaration.name}`, declaration); } } return byIdentity; }; /** * Resolve a declaration to its canonical identity. A same-name re-export of * an intermediate rename produces an alias pointing at another alias, so a * single hop isn't enough — follow the chain through declarations present in * the index. The terminal is the declaration *object* when it's in the set * (object identity keeps two same-`(module, name)` declarations distinct in * pathological inputs), else the dangling `(module, name)` key (two aliases * of the same absent canonical still resolve to one identity). The visited * set guards malformed cyclic input. */ const resolveCanonicalIdentity = ( byIdentity: Map<string, DeclarationJson>, modulePath: string, declaration: DeclarationJson, ): DeclarationJson | string => { let current = declaration; const visited = new Set([`${modulePath}\n${declaration.name}`]); while (current.aliasOf) { const nextKey = `${current.aliasOf.module}\n${current.aliasOf.name}`; if (visited.has(nextKey)) return nextKey; visited.add(nextKey); const next = byIdentity.get(nextKey); if (!next) return nextKey; current = next; } return current; }; /** * Find duplicate declaration names across modules. * * A duplicate is two *different things* sharing a name in the flat namespace. * Occurrences are compared by canonical identity — `aliasOf` chains are * resolved first, so an alias and its canonical (or two aliases of the same * canonical) are one thing, not a collision. Documenting a same-name re-export * (which synthesizes an alias) or re-exporting a component under its own name * (`export {default as Foo} from './Foo.svelte'`) therefore doesn't flag. * When a name does flag, all occurrences are reported, aliases included. * * Callers can decide how to handle duplicates (throw, warn, ignore). * * @returns `Map` of declaration names to their `DuplicateDeclaration` occurrences (only includes duplicates) * * @example * ```ts * const duplicates = findDuplicates(modules); * if (duplicates.size > 0) { * for (const [name, occurrences] of duplicates) { * console.error(`"${name}" found in:`); * for (const {declaration, module} of occurrences) { * console.error(` - ${module}:${declaration.sourceLine} (${declaration.kind})`); * } * } * throw new Error(`Found ${duplicates.size} duplicate declaration names`); * } * ``` */ export const findDuplicates = ( modules: Array<ModuleJson>, ): Map<string, Array<DuplicateDeclaration>> => { const byIdentity = buildDeclarationIndex(modules); const resolveCanonical = (modulePath: string, declaration: DeclarationJson) => resolveCanonicalIdentity(byIdentity, modulePath, declaration); // Collect declaration names with their canonical identities. The default // slot is module-scoped per the JS spec — every module can have its own // `'default'` and they don't collide — so skip `name === 'default'` // entries from this flat-namespace check. const allOccurrences: Map< string, Array<DuplicateDeclaration & {canonical: DeclarationJson | string}> > = new Map(); for (const mod of modules) { for (const declaration of mod.declarations) { if (declaration.name === 'default') continue; if (!allOccurrences.has(declaration.name)) { allOccurrences.set(declaration.name, []); } allOccurrences.get(declaration.name)!.push({ declaration, module: mod.path, canonical: resolveCanonical(mod.path, declaration), }); } } // A name is duplicated only when its occurrences span >1 canonical identity const duplicates: Map<string, Array<DuplicateDeclaration>> = new Map(); for (const [name, occurrences] of allOccurrences) { const identities = new Set(occurrences.map((o) => o.canonical)); if (identities.size > 1) { duplicates.set( name, occurrences.map(({declaration, module}) => ({declaration, module})), ); } } return duplicates; }; /** * Case-insensitive string comparator for deterministic output ordering. * * Case-folded comparison first (so `Analyze` and `analyze` sort together * instead of all uppercase before all lowercase), then a code-unit tiebreak — * so equal-ignoring-case strings still compare unequal and the result is an * exact total order. Unlike `localeCompare` (host-locale/ICU-dependent, so * byte-identical input can serialize in different orders on different * machines), both passes use Unicode default mappings only and are * environment-independent. All output ordering goes through this comparator — * never bare `localeCompare` or default `Array.prototype.sort`. */ export const compareStrings = (a: string, b: string): number => { const af = a.toLowerCase(); const bf = b.toLowerCase(); if (af < bf) return -1; if (af > bf) return 1; return a < b ? -1 : a > b ? 1 : 0; }; /** * Sort modules alphabetically by path for deterministic output and cleaner diffs. * * Case-insensitive order (`compareStrings`) so the output is environment-independent. * * @param modules - the modules to sort * @returns a new sorted array (does not mutate input) */ export const sortModules = (modules: Array<ModuleJson>): Array<ModuleJson> => { return modules.slice().sort((a, b) => compareStrings(a.path, b.path)); }; /** * Build `alsoExportedFrom` arrays from the modules' forward re-export edges. * * Each module carries its same-name re-export edges as `ModuleJson.reExports` * (collected in phase 1); this phase-2 pass inverts them onto the canonical * declarations so both directions of the same fact are queryable. Edges whose * canonical module or declaration is absent from `modules` are skipped — the * forward entry remains without a back-link (see `ReExportJson` for the * presence caveats). * * Component-only fields on renamed component aliases (`props`, * `acceptsChildren`, etc.) are populated separately by `resolveComponentAliases` * — call it after this function. They split because they touch disjoint fields * and have different inputs. * * @param modules - the modules array with all modules (will be mutated). * Must be parsed `ModuleJson`s — wire JSON strips empty arrays, so run * raw JSON through `AnalyzeResultJson.parse` first or `reExports` may be * `undefined` * @mutates modules - unions re-exporters into `declaration.alsoExportedFrom` * (deduped + sorted), so a second call with the same inputs is a no-op * * @example * ```ts * // helpers.ts exports: foo, bar * // index.ts does: export {foo, bar} from './helpers.js' * // (so index.ts's ModuleJson carries reExports: * // [{name: 'foo', module: 'helpers.ts'}, {name: 'bar', module: 'helpers.ts'}]) * // * // After processing: * // - helpers.ts foo declaration gets: alsoExportedFrom: ['index.ts'] * // - helpers.ts bar declaration gets: alsoExportedFrom: ['index.ts'] * ``` */ export const mergeReExports = (modules: Array<ModuleJson>): void => { // Group edges by `(canonical module, name)`. The default slot keys as // `'default'` like any other name — each module owns its own default slot, // so the per-module map prevents cross-module collisions naturally. const reExportMap: Map<string, Map<string, Array<string>>> = new Map(); for (const mod of modules) { for (const {name, module: originalModule} of mod.reExports) { if (!reExportMap.has(originalModule)) { reExportMap.set(originalModule, new Map()); } const moduleMap = reExportMap.get(originalModule)!; if (!moduleMap.has(name)) { moduleMap.set(name, []); } moduleMap.get(name)!.push(mod.path); } } // Merge into original declarations for (const mod of modules) { const moduleReExports = reExportMap.get(mod.path); if (!moduleReExports) continue; for (const declaration of mod.declarations) { const reExporters = moduleReExports.get(declaration.name); if (reExporters?.length) { // Union with existing entries, dedupe, sort — keeps the function idempotent // when re-run on already-merged modules const merged = new Set(declaration.alsoExportedFrom); for (const reExporter of reExporters) merged.add(reExporter); declaration.alsoExportedFrom = Array.from(merged).sort(compareStrings); } } } }; /** * Copy props/acceptsChildren/lang/etc. from canonical component declarations * onto synthesized component-aliased declarations. * * Renamed Svelte component re-exports (`export {default as Foo} from './X.svelte'`) * are emitted as `kind: 'component'` placeholders by `analyzeExports`, with `aliasOf` * pointing at the canonical. The canonical's component-specific fields are only * available after `analyzeSvelteModule` synthesizes the canonical declaration, so * the copy happens here in phase 2 once all modules are analyzed. * * Call this *after* `mergeReExports` — both walk the same modules array but * read/write disjoint fields, so order between them only matters for clarity. * * @mutates modules - fills component-only fields on aliased component declarations */ export const resolveComponentAliases = (modules: Array<ModuleJson>): void => { // Build {modulePath → {componentName → ComponentDeclarationJson}} for canonical lookups const canonicalByModule = new Map<string, Map<string, ComponentDeclarationJson>>(); for (const mod of modules) { for (const decl of mod.declarations) { if (decl.kind !== 'component' || decl.aliasOf) continue; let perModule = canonicalByModule.get(mod.path); if (!perModule) { perModule = new Map(); canonicalByModule.set(mod.path, perModule); } perModule.set(decl.name, decl); } } for (const mod of modules) { for (const decl of mod.declarations) { if (decl.kind !== 'component' || !decl.aliasOf) continue; const canonical = canonicalByModule.get(decl.aliasOf.module)?.get(decl.aliasOf.name); if (!canonical) continue; decl.props = canonical.props; decl.acceptsChildren = canonical.acceptsChildren; decl.intersects = canonical.intersects; decl.genericParams = canonical.genericParams; if (canonical.lang !== undefined) decl.lang = canonical.lang; if (canonical.docComment !== undefined && decl.docComment === undefined) { decl.docComment = canonical.docComment; } if (canonical.typeSignature !== undefined && decl.typeSignature === undefined) { decl.typeSignature = canonical.typeSignature; } if (canonical.examples.length > 0 && decl.examples.length === 0) { decl.examples = canonical.examples; } if (canonical.seeAlso.length > 0 && decl.seeAlso.length === 0) { decl.seeAlso = canonical.seeAlso; } if (canonical.throws.length > 0 && decl.throws.length === 0) { decl.throws = canonical.throws; } if (canonical.deprecatedMessage !== undefined && decl.deprecatedMessage === undefined) { decl.deprecatedMessage = canonical.deprecatedMessage; } if (canonical.since !== undefined && decl.since === undefined) { decl.since = canonical.since; } if (canonical.mutates !== undefined && decl.mutates === undefined) { decl.mutates = canonical.mutates; } if (canonical.partial) decl.partial = true; } } }; /** * Compute bidirectional dependencies from source files. * * This function ensures that if file A has file B in its `dependencies`, * then file B will have file A in its `dependents`. This provides consistent * output regardless of whether callers provide one-directional or bidirectional * dependency information. * * Returns new `SourceFileInfo` objects when computed dependents exist or when * paths needed posixification; otherwise the original input objects flow * through `===`-equal (fast path for session callers, who already pass POSIX * paths and may have no inferable dependents for a given file). * * @param files - source files with optional dependency information * @returns new array with bidirectional dependencies computed * * @example * ```ts * // Input: Calculator.svelte has dependencies: [math.ts] * // Output: Calculator.svelte has dependencies: [math.ts] * // math.ts has dependents: [Calculator.svelte] * const filesWithBidirectional = computeDependents(files); * ``` */ export const computeDependents = ( files: ReadonlyArray<SourceFileInfo>, ): Array<SourceFileInfo & {dependents?: ReadonlyArray<string>}> => { // Posixify ids and dependency lists at the boundary so a power user // supplying a hand-built `SourceFileInfo[]` with mixed-shape paths still // gets correct lookup behavior. Identity-preserving when no normalization // is needed (session callers, who already pass POSIX): both arrays and the // outer `SourceFileInfo` flow through `===`-equal. const posixFiles = files.map((file) => { const posixId = toPosixPath(file.id); const posixDeps = posixifyArray(file.dependencies); if (posixId === file.id && posixDeps === file.dependencies) { return file; } return { ...file, id: posixId, dependencies: posixDeps, }; }); // Build a map of file id -> dependents (computed from dependencies) const computedDependents: Map<string, Set<string>> = new Map(); // Initialize all files in the map for (const file of posixFiles) { computedDependents.set(file.id, new Set()); } // Compute dependents from dependencies for (const file of posixFiles) { if (!file.dependencies) continue; for (const depId of file.dependencies) { // Only add if the dependency is in our file set if (computedDependents.has(depId)) { computedDependents.get(depId)!.add(file.id); } } } // Attach computed dependents to each file. Dependents are derived from // forward edges in the owned set, not from caller input — the public // `SourceFileInfo` carries no `dependents` field. return posixFiles.map((file) => { const computed = computedDependents.get(file.id); if (!computed || computed.size === 0) { // No computed dependents, return as-is return file; } // Sort for deterministic output const dependents = Array.from(computed).sort(compareStrings); return { ...file, dependents, }; }); }; /** * One name on a module's resolved export surface. */ export interface ExportSurfaceEntry { /** * Exported name, in the docinfo model's terms — Svelte components appear * under their filename-derived name (the model's convention for default * exports of `.svelte` files), not `'default'`. */ name: string; /** * How the name reaches this module's surface: an own declaration * (including synthesized aliases), a same-name re-export edge, a direct * external re-export, or projection through `export * from './x'`. */ via: 'declaration' | 'reExport' | 'external' | 'star'; /** Canonical module path, when known (`undefined` for external entries). */ module?: string; /** The canonical declaration, when present in the analyzed set. */ declaration?: DeclarationJson; /** Package specifier for external entries (as written in the statement). */ specifier?: string; /** Name inside the external package when renamed. */ originalName?: string; /** Type-only re-export — the name is erased at runtime. */ typeOnly?: boolean; /** For star-projected entries: the `starExports` target the name arrived through. */ starFrom?: string; } /** * A module's resolved export surface — see `resolveExportSurface`. */ export interface ExportSurface { /** Surface entries, sorted by `name` (case-insensitive order, `compareStrings`). */ entries: Array<ExportSurfaceEntry>; /** * Star targets (own or transitive) absent from the analyzed set — their * projected names are unknown, so the surface is incomplete. */ unresolvedStarExports: Array<string>; /** * External star specifiers reachable from this module (own or transitive) * — their projected names are unknowable without analyzing the package. */ externalStarExports: Array<string>; } interface InternalSurfaceEntry { entry: ExportSurfaceEntry; /** Canonical identity for ES ambiguity comparison across stars. */ identity: DeclarationJson | string; } interface InternalSurface { entries: Map<string, InternalSurfaceEntry>; unresolvedStars: Set<string>; externalStars: Set<string>; } /** * Resolve a module's full export surface from the analyzed model, applying * ES module semantics to star exports. * * Combines the module's own `declarations` (including synthesized aliases), * `reExports` edges, `externalReExports`, and transitively-resolved * `starExports` into one deduped, name-sorted list. The ES rules applied to * star projection: * * - explicit exports (declarations, edges, externals) shadow star-projected * names * - a name projected by two stars that resolve to *different* canonicals is * ambiguous and excluded (same canonical through a diamond is included * once) * - `default` is never star-projected — including canonical Svelte component * declarations, which represent their file's default export. (Caveat: a * star-projected re-export edge whose canonical is a component is treated * as a default-slot re-export and skipped; a `<script module>` const * sharing the component's exact name would be skipped with it.) * * A Position-3 alias and its `reExports` edge are the same fact — the * declaration entry wins, inheriting the edge's `typeOnly`. * * Cyclic star graphs terminate by contributing nothing along the back-edge * (an approximation of ES fixpoint resolution — fine in practice). Surfaces * resolved inside a cycle are path-relative and not memoized, so sibling * star paths resolve independently rather than inheriting them. Star * targets missing from `modules` are reported in `unresolvedStarExports` * rather than guessed at; `externalStarExports` aggregates external star * specifiers reachable from the module, whose names are unknowable. * * @param modules - the analyzed modules (parsed `ModuleJson`s — run wire * JSON through `AnalyzeResultJson.parse` first) * @param path - the module to resolve, as a `ModuleJson.path` value * @returns the resolved surface, or `null` when `path` isn't in `modules` */ export const resolveExportSurface = ( modules: Array<ModuleJson>, path: string, ): ExportSurface | null => { const byPath = new Map(modules.map((m) => [m.path, m])); if (!byPath.has(path)) return null; const byIdentity = buildDeclarationIndex(modules); const cache = new Map<string, InternalSurface>(); const visiting = new Set<string>(); const emptySurface = (): InternalSurface => ({ entries: new Map(), unresolvedStars: new Set(), externalStars: new Set(), }); // Only surfaces resolved without hitting a cycle back-edge are memoized. // A surface computed while one of its star ancestors is mid-resolution is // relative to that path — it can carry names a full resolution would // exclude as ambiguous (the back-edge contributed nothing to weigh them // against). Caching it would leak the path-relative answer to sibling // star paths; tainted surfaces are instead recomputed per consumer. // Recomputation is bounded by `visiting` and only occurs inside cyclic // clusters, which are pathological to begin with. const resolveModule = (modulePath: string): {surface: InternalSurface; tainted: boolean} => { const cached = cache.get(modulePath); if (cached) return {surface: cached, tainted: false}; // Cycle break: a star back-edge contributes nothing if (visiting.has(modulePath)) return {surface: emptySurface(), tainted: true}; visiting.add(modulePath); const mod = byPath.get(modulePath)!; const surface = emptySurface(); let tainted = false; const {entries} = surface; // 1. Own declarations — including synthesized aliases and `default` const edgesByName = new Map(mod.reExports.map((e) => [e.name, e])); for (const declaration of mod.declarations) { // A Position-3 alias duplicates its edge; carry the edge's typeOnly const matchingEdge = declaration.aliasOf && edgesByName.get(declaration.name)?.module === declaration.aliasOf.module ? edgesByName.get(declaration.name) : undefined; entries.set(declaration.name, { entry: { name: declaration.name, via: 'declaration', module: modulePath, declaration, ...(matchingEdge?.typeOnly ? {typeOnly: true} : {}), }, identity: resolveCanonicalIdentity(byIdentity, modulePath, declaration), }); } // 2. Same-name re-export edges not already covered by a declaration. // Edges can collide on `name` (Svelte default re-keying — see // `ReExportJson`); the surface is keyed by name, so the first edge in // sort order wins and the collider is dropped for (const edge of mod.reExports) { if (entries.has(edge.name)) continue; const canonical = byIdentity.get(`${edge.module}\n${edge.name}`); entries.set(edge.name, { entry: { name: edge.name, via: 'reExport', module: edge.module, ...(canonical ? {declaration: canonical} : {}), ...(edge.typeOnly ? {typeOnly: true} : {}), }, identity: canonical ? resolveCanonicalIdentity(byIdentity, edge.module, canonical) : `${edge.module}\n${edge.name}`, }); } // 3. Direct external re-exports for (const external of mod.externalReExports) { if (entries.has(external.name)) continue; entries.set(external.name, { entry: { name: external.name, via: 'external', specifier: external.specifier, ...(external.originalName !== undefined ? {originalName: external.originalName} : {}), ...(external.typeOnly ? {typeOnly: true} : {}), }, identity: `ext\n${external.specifier}\n${external.originalName ?? external.name}`, }); } // 4. External stars — names unknowable, surface incompleteness recorded for (const specifier of mod.externalStarExports) { surface.externalStars.add(specifier); } // 5. Star projection, with ES shadowing and ambiguity rules const excluded = new Set<string>(); for (const target of mod.starExports) { if (!byPath.has(target)) { surface.unresolvedStars.add(target); continue; } const resolved = resolveModule(target); if (resolved.tainted) tainted = true; const sub = resolved.surface; for (const star of sub.unresolvedStars) surface.unresolvedStars.add(star); for (const star of sub.externalStars) surface.externalStars.add(star); for (const {entry, identity} of sub.entries.values()) { // `default` never projects — nor do canonical Svelte components, // which represent their file's default export (whether reached // as a declaration or through a re-export edge) if (entry.name === 'default') continue; if (entry.declaration?.kind === 'component' && !entry.declaration.aliasOf) continue; if (excluded.has(entry.name)) continue; const existing = entries.get(entry.name); if (existing) { // Explicit exports shadow; identical canonicals (diamond) merge if (existing.entry.via !== 'star' || existing.identity === identity) continue; // Ambiguous between two stars — excluded per ES semantics entries.delete(entry.name); excluded.add(entry.name); continue; } entries.set(entry.name, { entry: {...entry, via: 'star', starFrom: target}, identity, }); } } visiting.delete(modulePath); if (!tainted) cache.set(modulePath, surface); return {surface, tainted}; }; const resolved = resolveModule(path).surface; return { entries: Array.from(resolved.entries.values()) .map(({entry}) => entry) .sort((a, b) => compareStrings(a.name, b.name)), unresolvedStarExports: Array.from(resolved.unresolvedStars).sort(compareStrings), externalStarExports: Array.from(resolved.externalStars).sort(compareStrings), }; };
{ "path": "postprocess.ts", "declarations": [ { "name": "DuplicateDeclaration", "kind": "interface", "docComment": "A duplicate declaration with its full metadata and module path.", "typeSignature": "DuplicateDeclaration", "sourceLine": 43, "alsoExportedFrom": [ "index.ts" ], "members": [ { "name": "declaration", "kind": "variable", "docComment": "The full declaration metadata.", "typeSignature": "DeclarationJson" }, { "name": "module", "kind": "variable", "docComment": "Module path where this declaration is defined.", "typeSignature": "string" } ] }, { "name": "findDuplicates", "kind": "function", "docComment": "Find duplicate declaration names across modules.\n\nA duplicate is two *different things* sharing a name in the flat namespace.\nOccurrences are compared by canonical identity — `aliasOf` chains are\nresolved first, so an alias and its canonical (or two aliases of the same\ncanonical) are one thing, not a collision. Documenting a same-name re-export\n(which synthesizes an alias) or re-exporting a component under its own name\n(`export {default as Foo} from './Foo.svelte'`) therefore doesn't flag.\nWhen a name does flag, all occurrences are reported, aliases included.\n\nCallers can decide how to handle duplicates (throw, warn, ignore).", "typeSignature": "(modules: { path: string; declarations: ({ kind: \"function\"; parameters: { name: string; type: string; optional: boolean; rest: boolean; description?: string | undefined; defaultValue?: string | undefined; propertyDescriptions?: Record<...> | undefined; }[]; ... 17 more ...; sourceLine?: number | undefined; } | ... 7 more ... | { ...; })[]; ... 7 more ...; moduleComment?: string | undefined; }[]): Map<...>", "sourceLine": 122, "examples": [ "```ts\nconst duplicates = findDuplicates(modules);\nif (duplicates.size > 0) {\n for (const [name, occurrences] of duplicates) {\n console.error(`\"${name}\" found in:`);\n for (const {declaration, module} of occurrences) {\n console.error(` - ${module}:${declaration.sourceLine} (${declaration.kind})`);\n }\n }\n throw new Error(`Found ${duplicates.size} duplicate declaration names`);\n}\n```" ], "alsoExportedFrom": [ "index.ts" ], "parameters": [ { "name": "modules", "type": "{ path: string; declarations: ({ kind: \"function\"; parameters: { name: string; type: string; optional: boolean; rest: boolean; description?: string | undefined; defaultValue?: string | undefined; propertyDescriptions?: Record<...> | undefined; }[]; ... 17 more ...; sourceLine?: number | undefined; } | ... 7 more ......" } ], "returnType": "Map<string, DuplicateDeclaration[]>", "returnDescription": "`Map` of declaration names to their `DuplicateDeclaration` occurrences (only includes duplicates)" }, { "name": "compareStrings", "kind": "function", "docComment": "Case-insensitive string comparator for deterministic output ordering.\n\nCase-folded comparison first (so `Analyze` and `analyze` sort together\ninstead of all uppercase before all lowercase), then a code-unit tiebreak —\nso equal-ignoring-case strings still compare unequal and the result is an\nexact total order. Unlike `localeCompare` (host-locale/ICU-dependent, so\nbyte-identical input can serialize in different orders on different\nmachines), both passes use Unicode default mappings only and are\nenvironment-independent. All output ordering goes through this comparator —\nnever bare `localeCompare` or default `Array.prototype.sort`.", "typeSignature": "(a: string, b: string): number", "sourceLine": 178, "parameters": [ { "name": "a", "type": "string" }, { "name": "b", "type": "string" } ], "returnType": "number" }, { "name": "sortModules", "kind": "function", "docComment": "Sort modules alphabetically by path for deterministic output and cleaner diffs.\n\nCase-insensitive order (`compareStrings`) so the output is environment-independent.", "typeSignature": "(modules: { path: string; declarations: ({ kind: \"function\"; parameters: { name: string; type: string; optional: boolean; rest: boolean; description?: string | undefined; defaultValue?: string | undefined; propertyDescriptions?: Record<...> | undefined; }[]; ... 17 more ...; sourceLine?: number | undefined; } | ... 7 more ... | { ...; })[]; ... 7 more ...; moduleComment?: string | undefined; }[]): { ...; }[]", "sourceLine": 194, "parameters": [ { "name": "modules", "type": "{ path: string; declarations: ({ kind: \"function\"; parameters: { name: string; type: string; optional: boolean; rest: boolean; description?: string | undefined; defaultValue?: string | undefined; propertyDescriptions?: Record<...> | undefined; }[]; ... 17 more ...; sourceLine?: number | undefined; } | ... 7 more ......", "description": "the modules to sort" } ], "returnType": "{ path: string; declarations: ({ kind: \"function\"; parameters: { name: string; type: string; optional: boolean; rest: boolean; description?: string | undefined; defaultValue?: string | undefined; propertyDescriptions?: Record<...> | undefined; }[]; ... 17 more ...; sourceLine?: number | undefined; } | ... 7 more ......", "returnDescription": "a new sorted array (does not mutate input)" }, { "name": "mergeReExports", "kind": "function", "docComment": "Build `alsoExportedFrom` arrays from the modules' forward re-export edges.\n\nEach module carries its same-name re-export edges as `ModuleJson.reExports`\n(collected in phase 1); this phase-2 pass inverts them onto the canonical\ndeclarations so both directions of the same fact are queryable. Edges whose\ncanonical module or declaration is absent from `modules` are skipped — the\nforward entry remains without a back-link (see `ReExportJson` for the\npresence caveats).\n\nComponent-only fields on renamed component aliases (`props`,\n`acceptsChildren`, etc.) are populated separately by `resolveComponentAliases`\n— call it after this function. They split because they touch disjoint fields\nand have different inputs.", "typeSignature": "(modules: { path: string; declarations: ({ kind: \"function\"; parameters: { name: string; type: string; optional: boolean; rest: boolean; description?: string | undefined; defaultValue?: string | undefined; propertyDescriptions?: Record<...> | undefined; }[]; ... 17 more ...; sourceLine?: number | undefined; } | ... 7 more ... | { ...; })[]; ... 7 more ...; moduleComment?: string | undefined; }[]): void", "sourceLine": 232, "examples": [ "```ts\n// helpers.ts exports: foo, bar\n// index.ts does: export {foo, bar} from './helpers.js'\n// (so index.ts's ModuleJson carries reExports:\n// [{name: 'foo', module: 'helpers.ts'}, {name: 'bar', module: 'helpers.ts'}])\n//\n// After processing:\n// - helpers.ts foo declaration gets: alsoExportedFrom: ['index.ts']\n// - helpers.ts bar declaration gets: alsoExportedFrom: ['index.ts']\n```" ], "mutates": { "modules": "unions re-exporters into `declaration.alsoExportedFrom`" }, "alsoExportedFrom": [ "index.ts" ], "parameters": [ { "name": "modules", "type": "{ path: string; declarations: ({ kind: \"function\"; parameters: { name: string; type: string; optional: boolean; rest: boolean; description?: string | undefined; defaultValue?: string | undefined; propertyDescriptions?: Record<...> | undefined; }[]; ... 17 more ...; sourceLine?: number | undefined; } | ... 7 more ......", "description": "the modules array with all modules (will be mutated).\nMust be parsed `ModuleJson`s — wire JSON strips empty arrays, so run\nraw JSON through `AnalyzeResultJson.parse` first or `reExports` may be\n`undefined`" } ], "returnType": "void" }, { "name": "resolveComponentAliases", "kind": "function", "docComment": "Copy props/acceptsChildren/lang/etc. from canonical component declarations\nonto synthesized component-aliased declarations.\n\nRenamed Svelte component re-exports (`export {default as Foo} from './X.svelte'`)\nare emitted as `kind: 'component'` placeholders by `analyzeExports`, with `aliasOf`\npointing at the canonical. The canonical's component-specific fields are only\navailable after `analyzeSvelteModule` synthesizes the canonical declaration, so\nthe copy happens here in phase 2 once all modules are analyzed.\n\nCall this *after* `mergeReExports` — both walk the same modules array but\nread/write disjoint fields, so order between them only matters for clarity.", "typeSignature": "(modules: { path: string; declarations: ({ kind: \"function\"; parameters: { name: string; type: string; optional: boolean; rest: boolean; description?: string | undefined; defaultValue?: string | undefined; propertyDescriptions?: Record<...> | undefined; }[]; ... 17 more ...; sourceLine?: number | undefined; } | ... 7 more ... | { ...; })[]; ... 7 more ...; moduleComment?: string | undefined; }[]): void", "sourceLine": 284, "mutates": { "modules": "fills component-only fields on aliased component declarations" }, "alsoExportedFrom": [ "index.ts" ], "parameters": [ { "name": "modules", "type": "{ path: string; declarations: ({ kind: \"function\"; parameters: { name: string; type: string; optional: boolean; rest: boolean; description?: string | undefined; defaultValue?: string | undefined; propertyDescriptions?: Record<...> | undefined; }[]; ... 17 more ...; sourceLine?: number | undefined; } | ... 7 more ......" } ], "returnType": "void" }, { "name": "computeDependents", "kind": "function", "docComment": "Compute bidirectional dependencies from source files.\n\nThis function ensures that if file A has file B in its `dependencies`,\nthen file B will have file A in its `dependents`. This provides consistent\noutput regardless of whether callers provide one-directional or bidirectional\ndependency information.\n\nReturns new `SourceFileInfo` objects when computed dependents exist or when\npaths needed posixification; otherwise the original input objects flow\nthrough `===`-equal (fast path for session callers, who already pass POSIX\npaths and may have no inferable dependents for a given file).", "typeSignature": "(files: readonly SourceFileInfo[]): (SourceFileInfo & { dependents?: readonly string[] | undefined; })[]", "sourceLine": 362, "examples": [ "```ts\n// Input: Calculator.svelte has dependencies: [math.ts]\n// Output: Calculator.svelte has dependencies: [math.ts]\n// math.ts has dependents: [Calculator.svelte]\nconst filesWithBidirectional = computeDependents(files);\n```" ], "parameters": [ { "name": "files", "type": "readonly SourceFileInfo[]", "description": "source files with optional dependency information" } ], "returnType": "(SourceFileInfo & { dependents?: readonly string[] | undefined; })[]", "returnDescription": "new array with bidirectional dependencies computed" }, { "name": "ExportSurfaceEntry", "kind": "interface", "docComment": "One name on a module's resolved export surface.", "typeSignature": "ExportSurfaceEntry", "sourceLine": 425, "alsoExportedFrom": [ "index.ts" ], "members": [ { "name": "name", "kind": "variable", "docComment": "Exported name, in the docinfo model's terms — Svelte components appear\nunder their filename-derived name (the model's convention for default\nexports of `.svelte` files), not `'default'`.", "typeSignature": "string" }, { "name": "via", "kind": "variable", "docComment": "How the name reaches this module's surface: an own declaration\n(including synthesized aliases), a same-name re-export edge, a direct\nexternal re-export, or projection through `export * from './x'`.", "typeSignature": "'declaration' | 'reExport' | 'external' | 'star'" }, { "name": "module", "kind": "variable", "docComment": "Canonical module path, when known (`undefined` for external entries).", "typeSignature": "string", "optional": true }, { "name": "declaration", "kind": "variable", "docComment": "The canonical declaration, when present in the analyzed set.", "typeSignature": "DeclarationJson", "optional": true }, { "name": "specifier", "kind": "variable", "docComment": "Package specifier for external entries (as written in the statement).", "typeSignature": "string", "optional": true }, { "name": "originalName", "kind": "variable", "docComment": "Name inside the external package when renamed.", "typeSignature": "string", "optional": true }, { "name": "typeOnly", "kind": "variable", "docComment": "Type-only re-export — the name is erased at runtime.", "typeSignature": "boolean", "optional": true }, { "name": "starFrom", "kind": "variable", "docComment": "For star-projected entries: the `starExports` target the name arrived through.", "typeSignature": "string", "optional": true } ] }, { "name": "ExportSurface", "kind": "interface", "docComment": "A module's resolved export surface — see `resolveExportSurface`.", "typeSignature": "ExportSurface", "sourceLine": 455, "alsoExportedFrom": [ "index.ts" ], "members": [ { "name": "entries", "kind": "variable", "docComment": "Surface entries, sorted by `name` (case-insensitive order, `compareStrings`).", "typeSignature": "Array<ExportSurfaceEntry>" }, { "name": "unresolvedStarExports", "kind": "variable", "docComment": "Star targets (own or transitive) absent from the analyzed set — their\nprojected names are unknown, so the surface is incomplete.", "typeSignature": "Array<string>" }, { "name": "externalStarExports", "kind": "variable", "docComment": "External star specifiers reachable from this module (own or transitive)\n— their projected names are unknowable without analyzing the package.", "typeSignature": "Array<string>" } ] }, { "name": "resolveExportSurface", "kind": "function", "docComment": "Resolve a module's full export surface from the analyzed model, applying\nES module semantics to star exports.\n\nCombines the module's own `declarations` (including synthesized aliases),\n`reExports` edges, `externalReExports`, and transitively-resolved\n`starExports` into one deduped, name-sorted list. The ES rules applied to\nstar projection:\n\n- explicit exports (declarations, edges, externals) shadow star-projected\n names\n- a name projected by two stars that resolve to *different* canonicals is\n ambiguous and excluded (same canonical through a diamond is included\n once)\n- `default` is never star-projected — including canonical Svelte component\n declarations, which represent their file's default export. (Caveat: a\n star-projected re-export edge whose canonical is a component is treated\n as a default-slot re-export and skipped; a `<script module>` const\n sharing the component's exact name would be skipped with it.)\n\nA Position-3 alias and its `reExports` edge are the same fact — the\ndeclaration entry wins, inheriting the edge's `typeOnly`.\n\nCyclic star graphs terminate by contributing nothing along the back-edge\n(an approximation of ES fixpoint resolution — fine in practice). Surfaces\nresolved inside a cycle are path-relative and not memoized, so sibling\nstar paths resolve independently rather than inheriting them. Star\ntargets missing from `modules` are reported in `unresolvedStarExports`\nrather than guessed at; `externalStarExports` aggregates external star\nspecifiers reachable from the module, whose names are unknowable.", "typeSignature": "(modules: { path: string; declarations: ({ kind: \"function\"; parameters: { name: string; type: string; optional: boolean; rest: boolean; description?: string | undefined; defaultValue?: string | undefined; propertyDescriptions?: Record<...> | undefined; }[]; ... 17 more ...; sourceLine?: number | undefined; } | ... 7 more ... | { ...; })[]; ... 7 more ...; moduleComment?: string | undefined; }[], path: string): ExportSurface | null", "sourceLine": 518, "alsoExportedFrom": [ "index.ts" ], "parameters": [ { "name": "modules", "type": "{ path: string; declarations: ({ kind: \"function\"; parameters: { name: string; type: string; optional: boolean; rest: boolean; description?: string | undefined; defaultValue?: string | undefined; propertyDescriptions?: Record<...> | undefined; }[]; ... 17 more ...; sourceLine?: number | undefined; } | ... 7 more ......", "description": "the analyzed modules (parsed `ModuleJson`s — run wire\nJSON through `AnalyzeResultJson.parse` first)" }, { "name": "path", "type": "string", "description": "the module to resolve, as a `ModuleJson.path` value" } ], "returnType": "ExportSurface | null", "returnDescription": "the resolved surface, or `null` when `path` isn't in `modules`" } ], "moduleComment": "Post-processing for analyzed library metadata.\n\nThese functions transform analysis results after module-level analysis is complete:\n\n1. **Validation** — `findDuplicates` checks flat namespace constraints\n2. **Transformation** — `mergeReExports` resolves re-export relationships,\n `computeDependents` builds bidirectional dependency graphs\n3. **Output** — `sortModules` prepares deterministic output\n\n@see `analyze.ts` for the main analysis entry point", "dependencies": [ "paths.ts", "source.ts", "types.ts" ], "dependents": [ "analyze-core.ts", "index.ts", "session.ts", "source-config.ts" ] }