/** * Utility functions for working with declaration and member types. * * Display formatting, code generation, serialization, type narrowing, * and type reference discovery for `DeclarationJson` and `MemberJson`. * * @see `types.ts` for `DeclarationJson`, `MemberJson` Zod schemas * * @module */ import type {DeclarationJson, MemberJson, DeclarationKind, MemberKind} from './types.js'; // ── Serialization ─────────────────────────────────────────────────────────── /** * JSON replacer that strips Zod default values for compact serialization. * * Strips empty arrays and `false` booleans — both are Zod `.default()` values * restored on `.parse()`, so the round-trip is lossless for svelte-docinfo types. * Assumes all boolean fields in the schema default to `false` — a `true`-defaulted * boolean would need its `false` values preserved, breaking the round-trip. * * **Root-value caveat**: `JSON.stringify([], compactReplacer)` returns the JS * `undefined` (not the string `'[]'`), and `JSON.stringify(false, compactReplacer)` * returns the JS `undefined` too. Object-rooted callers (`AnalyzeResultJson` * envelope, CLI output) don't hit this — empty inner arrays strip and * `AnalyzeResultJson.parse` restores them on the consumer side. Array-rooted * callers (Vite plugin, anyone splicing the JSON into a source template) * must handle the empty case themselves before calling this; see * `vite.ts:updateOutputFromQuery` for the pattern. * * Two guard tests in `declaration-helpers.test.ts` lock this in: * - `every z.boolean().default in types.ts uses false` — source-regex check * that fails on a new `z.boolean().default(true)`. * - `parse → stringify(compactReplacer) → parse is a faithful round-trip * across every variant` — exercises every variant and member through a * full round-trip, catching regressions where a `.default(false)` or * `.default([])` is removed (or a new field is added that the replacer * drops but Zod doesn't restore). * * @example * ```ts * const result = await analyze({sourceFiles, sourceOptions}); * const json = JSON.stringify(result, compactReplacer); * // On the consumer side, restore Zod defaults: * const restored = AnalyzeResultJson.parse(JSON.parse(json)); * ``` */ export const compactReplacer = (_key: string, value: unknown): unknown => (Array.isArray(value) && value.length === 0) || value === false ? undefined : value; // ── Display Helpers ───────────────────────────────────────────────────────── /** * Format declaration or member name with generic parameters for display. * * Default-slot entries return the literal `'default'` (the symbol's actual * name in JS). Renderers that want a richer label (PascalCased module path, * an explicit "default export" header) should branch on `name === 'default'` * themselves before calling this. * * @see `generateImport` for the divergent default-slot fallback used in * import-statement generation (PascalCased module path, since an import * needs a JS identifier binding, not a label). * * @param declaration - the `DeclarationJson` or `MemberJson` to format * @returns name with generic parameters appended (e.g., `Map<K, V>`) * * @example * ```ts * getDisplayName({name: 'Map', kind: 'type', genericParams: [{name: 'K'}, {name: 'V'}]}) * // => 'Map<K, V>' * ``` */ export const getDisplayName = (declaration: DeclarationJson | MemberJson): string => { if (!declaration.genericParams.length) return declaration.name; const params = declaration.genericParams.map((p) => { let param = p.name; if (p.constraint) param += ` extends ${p.constraint}`; if (p.defaultType) param += ` = ${p.defaultType}`; return param; }); return `${declaration.name}<${params.join(', ')}>`; }; /** * Generate TypeScript import statement for a declaration. * * Produces `import type` for type/interface declarations, `import` for values. * * **Default export handling**: when `declaration.name === 'default'`, emits * `import X from '...'` with the binding derived by PascalCasing the module * path. (`'default'` is the symbol's actual name in JS — `import X from 'mod'` * is sugar for `import {default as X} from 'mod'`.) * * @see `getDisplayName` for the divergent default-slot fallback used as a * display label (the literal `'default'`, since a label has no use for a * synthesized JS binding). * * @param declaration - the `DeclarationJson` to generate an import for * @param modulePath - module path relative to source root (e.g., `foo.ts`) * @param libraryName - package name for the import specifier (e.g., `@pkg/lib`) * @returns formatted import statement string * * @example * ```ts * generateImport({name: 'Foo', kind: 'type'}, 'foo.ts', '@pkg/lib') * // => "import type {Foo} from '@pkg/lib/foo.js';" * * generateImport({name: 'default', kind: 'function'}, 'foo-bar.ts', '@pkg/lib') * // => "import FooBar from '@pkg/lib/foo-bar.js';" * ``` */ export const generateImport = ( declaration: DeclarationJson, modulePath: string, libraryName: string, ): string => { const jsPath = modulePath.replace(/\.ts$/, '.js'); const specifier = `${libraryName}/${jsPath}`; // Default-slot entries — derive the import binding from the module path. if (declaration.name === 'default') { return `import ${pascalCaseFromModulePath(modulePath)} from '${specifier}';`; } // Components are default exports in Svelte if (declaration.kind === 'component') { return `import ${declaration.name} from '${specifier}';`; } // Namespace re-export: `export * as ns from './x'` in the source becomes // `import * as ns from '<package>/<re-exporter>.js'` for consumers. if (declaration.kind === 'namespace') { return `import * as ${declaration.name} from '${specifier}';`; } const importKeyword = declaration.kind === 'type' || declaration.kind === 'interface' ? 'import type' : 'import'; return `${importKeyword} {${declaration.name}} from '${specifier}';`; }; const pascalCaseFromModulePath = (modulePath: string): string => { const moduleName = modulePath.replace(/\.(js|ts|svelte)$/, ''); return moduleName .split(/[-_/]/) .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) .join(''); }; // ── Type Reference Helpers ───────────────────────────────────────────────── /** * Escape special regex characters in a string. */ const escapeRegex = (s: string): string => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); /** Matches TypeScript identifier characters (letters, digits, `_`, `$`). */ const ID_CHAR = '[a-zA-Z0-9_$]'; /** * Build a regex that matches `name` only at identifier boundaries. * * Uses lookaround for `[a-zA-Z0-9_$]` instead of `\b` so that * `$`-prefixed identifiers like `$state` are matched correctly. */ const buildIdentifierPattern = (name: string): RegExp => new RegExp('(?<!' + ID_CHAR + ')' + escapeRegex(name) + '(?!' + ID_CHAR + ')'); /** * Pre-compile identifier-boundary patterns for a set of declaration names. * * When scanning many type strings against the same declaration set, * call this once and pass the result to `findTypeReferences` * to avoid recompiling regexes on every call. * * @param declarationNames - set of known in-project declaration names * @returns array of `[name, pattern]` pairs for use with `findTypeReferences` * * @example * ```ts * const names = new Set(modules.flatMap(m => m.declarations.map(d => d.name))); * const patterns = buildTypeReferencePatterns(names); * for (const decl of declarations) { * const refs = findTypeReferences(decl.typeSignature, patterns); * } * ``` */ export const buildTypeReferencePatterns = ( declarationNames: ReadonlySet<string>, ): Array<[string, RegExp]> => { const patterns: Array<[string, RegExp]> = []; for (const name of declarationNames) { if (name) patterns.push([name, buildIdentifierPattern(name)]); } return patterns; }; /** * Find in-project declaration names referenced in a type string. * * Uses identifier-boundary matching to find which known declaration names appear * in an opaque type string (e.g., `typeSignature`, `returnType`, parameter `type`). * Enables consumers to render clickable type links without needing access to * the TypeScript type checker. * * Handles identifiers starting with `$` (e.g., `$state`) which `\b` does not * recognize as word boundaries. * * Accepts either a `ReadonlySet<string>` (convenience) or pre-compiled patterns * from `buildTypeReferencePatterns` (performance). Use pre-compiled patterns * when scanning many type strings against the same declaration set. * * **Known limitations**: Identifier-boundary matching can produce false positives * when a declaration name appears as a property key in an object literal type * (e.g., `{ Foo: string }` when `Foo` is a declaration). This is rare in practice. * * @param typeString - opaque type string from analysis output * @param declarationNames - set of names or pre-compiled patterns from `buildTypeReferencePatterns` * @returns array of declaration names found in the type string * * @example * ```ts * const names = new Set(modules.flatMap(m => m.declarations.map(d => d.name))); * findTypeReferences('Map<string, ModuleJson[]>', names) * // => ['ModuleJson'] * ``` */ export const findTypeReferences = ( typeString: string, declarationNames: ReadonlySet<string> | Array<[string, RegExp]>, ): Array<string> => { if (!typeString) return []; const patterns: Array<[string, RegExp]> = Array.isArray(declarationNames) ? declarationNames : buildTypeReferencePatterns(declarationNames); const refs: Array<string> = []; for (const [name, pattern] of patterns) { if (pattern.test(typeString)) { refs.push(name); } } return refs; }; // ── Narrowed Declaration Types ────────────────────────────────────────────── /** * Narrow a declaration by kind for type-safe field access. * * Works with both `DeclarationJson` (top-level) and `MemberJson` (nested). * Accepts `DeclarationKind | MemberKind` so `isKind(member, 'constructor')` compiles. * * @example * ```ts * if (isKind(declaration, 'function')) { * declaration.parameters; // FunctionDeclarationJson — has parameters * declaration.returnType; // has returnType * } * if (isKind(member, 'constructor')) { * member.parameters; // ConstructorMemberJson — has parameters * } * ``` */ export const isKind = <K extends DeclarationKind | MemberKind>( declaration: DeclarationJson | MemberJson, kind: K, ): declaration is Extract<DeclarationJson | MemberJson, {kind: K}> => declaration.kind === kind;
{ "path": "declaration-helpers.ts", "declarations": [ { "name": "compactReplacer", "kind": "function", "docComment": "JSON replacer that strips Zod default values for compact serialization.\n\nStrips empty arrays and `false` booleans — both are Zod `.default()` values\nrestored on `.parse()`, so the round-trip is lossless for svelte-docinfo types.\nAssumes all boolean fields in the schema default to `false` — a `true`-defaulted\nboolean would need its `false` values preserved, breaking the round-trip.\n\n**Root-value caveat**: `JSON.stringify([], compactReplacer)` returns the JS\n`undefined` (not the string `'[]'`), and `JSON.stringify(false, compactReplacer)`\nreturns the JS `undefined` too. Object-rooted callers (`AnalyzeResultJson`\nenvelope, CLI output) don't hit this — empty inner arrays strip and\n`AnalyzeResultJson.parse` restores them on the consumer side. Array-rooted\ncallers (Vite plugin, anyone splicing the JSON into a source template)\nmust handle the empty case themselves before calling this; see\n`vite.ts:updateOutputFromQuery` for the pattern.\n\nTwo guard tests in `declaration-helpers.test.ts` lock this in:\n- `every z.boolean().default in types.ts uses false` — source-regex check\n that fails on a new `z.boolean().default(true)`.\n- `parse → stringify(compactReplacer) → parse is a faithful round-trip\n across every variant` — exercises every variant and member through a\n full round-trip, catching regressions where a `.default(false)` or\n `.default([])` is removed (or a new field is added that the replacer\n drops but Zod doesn't restore).", "typeSignature": "(_key: string, value: unknown): unknown", "sourceLine": 50, "examples": [ "```ts\nconst result = await analyze({sourceFiles, sourceOptions});\nconst json = JSON.stringify(result, compactReplacer);\n// On the consumer side, restore Zod defaults:\nconst restored = AnalyzeResultJson.parse(JSON.parse(json));\n```" ], "alsoExportedFrom": [ "index.ts" ], "parameters": [ { "name": "_key", "type": "string" }, { "name": "value", "type": "unknown" } ], "returnType": "unknown" }, { "name": "getDisplayName", "kind": "function", "docComment": "Format declaration or member name with generic parameters for display.\n\nDefault-slot entries return the literal `'default'` (the symbol's actual\nname in JS). Renderers that want a richer label (PascalCased module path,\nan explicit \"default export\" header) should branch on `name === 'default'`\nthemselves before calling this.", "typeSignature": "(declaration: { kind: \"function\"; name: string; optional: boolean; parameters: { name: string; type: string; optional: boolean; rest: boolean; description?: string | undefined; defaultValue?: string | undefined; propertyDescriptions?: Record<...> | undefined; }[]; ... 14 more ...; sourceLine?: number | undefined; } | ... 10 more ... | { ...; }): string", "sourceLine": 76, "examples": [ "```ts\ngetDisplayName({name: 'Map', kind: 'type', genericParams: [{name: 'K'}, {name: 'V'}]})\n// => 'Map<K, V>'\n```" ], "seeAlso": [ "`generateImport` for the divergent default-slot fallback used in import-statement generation (PascalCased module path, since an import needs a JS identifier binding, not a label)." ], "alsoExportedFrom": [ "index.ts" ], "parameters": [ { "name": "declaration", "type": "{ kind: \"function\"; name: string; optional: boolean; parameters: { name: string; type: string; optional: boolean; rest: boolean; description?: string | undefined; defaultValue?: string | undefined; propertyDescriptions?: Record<...> | undefined; }[]; ... 14 more ...; sourceLine?: number | undefined; } | ... 10 more ...", "description": "the `DeclarationJson` or `MemberJson` to format" } ], "returnType": "string", "returnDescription": "name with generic parameters appended (e.g., `Map<K, V>`)" }, { "name": "generateImport", "kind": "function", "docComment": "Generate TypeScript import statement for a declaration.\n\nProduces `import type` for type/interface declarations, `import` for values.\n\n**Default export handling**: when `declaration.name === 'default'`, emits\n`import X from '...'` with the binding derived by PascalCasing the module\npath. (`'default'` is the symbol's actual name in JS — `import X from 'mod'`\nis sugar for `import {default as X} from 'mod'`.)", "typeSignature": "(declaration: { 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 ... | { ...; }, modulePath: string, libraryName: string): string", "sourceLine": 115, "examples": [ "```ts\ngenerateImport({name: 'Foo', kind: 'type'}, 'foo.ts', '@pkg/lib')\n// => \"import type {Foo} from '@pkg/lib/foo.js';\"\n\ngenerateImport({name: 'default', kind: 'function'}, 'foo-bar.ts', '@pkg/lib')\n// => \"import FooBar from '@pkg/lib/foo-bar.js';\"\n```" ], "seeAlso": [ "`getDisplayName` for the divergent default-slot fallback used as a display label (the literal `'default'`, since a label has no use for a synthesized JS binding)." ], "alsoExportedFrom": [ "index.ts" ], "parameters": [ { "name": "declaration", "type": "{ 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 `DeclarationJson` to generate an import for" }, { "name": "modulePath", "type": "string", "description": "module path relative to source root (e.g., `foo.ts`)" }, { "name": "libraryName", "type": "string", "description": "package name for the import specifier (e.g., `@pkg/lib`)" } ], "returnType": "string", "returnDescription": "formatted import statement string" }, { "name": "buildTypeReferencePatterns", "kind": "function", "docComment": "Pre-compile identifier-boundary patterns for a set of declaration names.\n\nWhen scanning many type strings against the same declaration set,\ncall this once and pass the result to `findTypeReferences`\nto avoid recompiling regexes on every call.", "typeSignature": "(declarationNames: ReadonlySet<string>): [string, RegExp][]", "sourceLine": 190, "examples": [ "```ts\nconst names = new Set(modules.flatMap(m => m.declarations.map(d => d.name)));\nconst patterns = buildTypeReferencePatterns(names);\nfor (const decl of declarations) {\n const refs = findTypeReferences(decl.typeSignature, patterns);\n}\n```" ], "alsoExportedFrom": [ "index.ts" ], "parameters": [ { "name": "declarationNames", "type": "ReadonlySet<string>", "description": "set of known in-project declaration names" } ], "returnType": "[string, RegExp][]", "returnDescription": "array of `[name, pattern]` pairs for use with `findTypeReferences`" }, { "name": "findTypeReferences", "kind": "function", "docComment": "Find in-project declaration names referenced in a type string.\n\nUses identifier-boundary matching to find which known declaration names appear\nin an opaque type string (e.g., `typeSignature`, `returnType`, parameter `type`).\nEnables consumers to render clickable type links without needing access to\nthe TypeScript type checker.\n\nHandles identifiers starting with `$` (e.g., `$state`) which `\\b` does not\nrecognize as word boundaries.\n\nAccepts either a `ReadonlySet<string>` (convenience) or pre-compiled patterns\nfrom `buildTypeReferencePatterns` (performance). Use pre-compiled patterns\nwhen scanning many type strings against the same declaration set.\n\n**Known limitations**: Identifier-boundary matching can produce false positives\nwhen a declaration name appears as a property key in an object literal type\n(e.g., `{ Foo: string }` when `Foo` is a declaration). This is rare in practice.", "typeSignature": "(typeString: string, declarationNames: ReadonlySet<string> | [string, RegExp][]): string[]", "sourceLine": 230, "examples": [ "```ts\nconst names = new Set(modules.flatMap(m => m.declarations.map(d => d.name)));\nfindTypeReferences('Map<string, ModuleJson[]>', names)\n// => ['ModuleJson']\n```" ], "alsoExportedFrom": [ "index.ts" ], "parameters": [ { "name": "typeString", "type": "string", "description": "opaque type string from analysis output" }, { "name": "declarationNames", "type": "ReadonlySet<string> | [string, RegExp][]", "description": "set of names or pre-compiled patterns from `buildTypeReferencePatterns`" } ], "returnType": "string[]", "returnDescription": "array of declaration names found in the type string" }, { "name": "isKind", "kind": "function", "docComment": "Narrow a declaration by kind for type-safe field access.\n\nWorks with both `DeclarationJson` (top-level) and `MemberJson` (nested).\nAccepts `DeclarationKind | MemberKind` so `isKind(member, 'constructor')` compiles.", "typeSignature": "<K extends DeclarationKind | MemberKind>(declaration: { kind: \"function\"; name: string; optional: boolean; parameters: { name: string; type: string; optional: boolean; rest: boolean; description?: string | undefined; defaultValue?: string | undefined; propertyDescriptions?: Record<...> | undefined; }[]; ... 14 more ...; sourceLine?: number | undefined; } | ... 10 more ... | { ...; }, kind: K): declaration is Extract<...> | ... 10 more ... | Extract<...>", "sourceLine": 266, "genericParams": [ { "name": "K", "constraint": "DeclarationKind | MemberKind" } ], "examples": [ "```ts\nif (isKind(declaration, 'function')) {\n declaration.parameters; // FunctionDeclarationJson — has parameters\n declaration.returnType; // has returnType\n}\nif (isKind(member, 'constructor')) {\n member.parameters; // ConstructorMemberJson — has parameters\n}\n```" ], "alsoExportedFrom": [ "index.ts" ], "parameters": [ { "name": "declaration", "type": "{ kind: \"function\"; name: string; optional: boolean; parameters: { name: string; type: string; optional: boolean; rest: boolean; description?: string | undefined; defaultValue?: string | undefined; propertyDescriptions?: Record<...> | undefined; }[]; ... 14 more ...; sourceLine?: number | undefined; } | ... 10 more ..." }, { "name": "kind", "type": "K" } ], "returnType": "boolean" } ], "moduleComment": "Utility functions for working with declaration and member types.\n\nDisplay formatting, code generation, serialization, type narrowing,\nand type reference discovery for `DeclarationJson` and `MemberJson`.\n\n@see `types.ts` for `DeclarationJson`, `MemberJson` Zod schemas", "dependencies": [ "types.ts" ], "dependents": [ "cli.ts", "index.ts", "vite.ts" ] }