output format #

svelte-docinfo outputs JSON describing your project's exported API. The data format is a hierarchy: modules contain declarations, and some declarations contain members or props. You may want to skip the explanation and jump down to the examples.

Top-level structure
#

Programmatic entry points (analyze, analyzeFromFiles) return both modules and accumulated diagnostics:

{ modules: ModuleJson[], diagnostics: Diagnostic[] }

All surfaces emit this shape. The CLI's stdout JSON and the Vite plugin's virtual module both expose modules and diagnostics (matching AnalyzeResultJson). The CLI runs output through compactReplacer so empty arrays strip on the wire (an empty-project run emits {}); parse JSON consumers through AnalyzeResultJson to restore Zod defaults.

ModuleJson
#

A ModuleJson describes a single source file and its exports:

  • path: file path relative to the source root (e.g., "math.ts")
  • declarations: exported items from this module
  • moduleComment: file-level JSDoc comment, if present
  • dependencies: paths of modules this file imports
  • dependents: paths of modules that import this file
  • starExports: export * from './module' patterns
  • reExports: same-name re-export edges ({name, module, typeOnly, sourceLine}) in this module's source — the forward view of alsoExportedFrom (see Re-exports below)
  • externalReExports: direct re-exports from packages ({name, specifier, originalName?, typeOnly, sourceLine})
  • externalStarExports: export * from 'pkg' specifiers as written

Array fields (declarations, dependencies, etc.) are omitted from JSON when empty and default to [] at runtime after parsing.

DeclarationJson
#

Each declaration is a DeclarationJson, a discriminated union on the kind field with nine variants:

  • "function": adds parameters, returnType, returnTypeInfo, returnDescription, overloads
  • "variable": adds optional defaultValue (from @default), plus reactivity when the initializer is a Svelte rune ($state, $state.raw, $derived, $derived.by)
  • "class": adds members, extends, implements, externalTypes
  • "interface": adds members, extends, externalTypes, mergedValue
  • "type": adds members, externalTypes, mergedValue
  • "enum": adds members (enum values)
  • "component": adds props, externalTypes, acceptsChildren, lang (Svelte components)
  • "snippet": adds parameters (exported Svelte template snippets)
  • "namespace": adds module (the source module path projected under this binding); synthesized for export * as ns from './x'

A merged value+type export — one name declared in both spaces, like the schema pattern export const Foo = z.strictObject({...}) + export type Foo = z.infer<typeof Foo> — produces one declaration: the type meaning wins the slot and documents like its un-merged equivalent (structure, members, typeInfo), and mergedValue: true marks that the name is also importable as a runtime value — generateImport reads it to emit import instead of import type. JSDoc falls back to the value declaration's comment when the type declaration has none, and @nodocs on either declaration excludes the pair.

Shared fields on all variants:

  • name, kind: identity. Default exports carry name === "default" (see Re-exports below)
  • docComment: JSDoc comment text
  • typeSignature: full type as a string. Where TypeScript embeds a module reference — typeof import("…"), from an import() expression or a typeof over a namespace import — the path is normalized so output never carries an absolute path: a module in this output is named by its ModuleJson.path, so the string doubles as a lookup key (modules.find((m) => m.path === s); a miss means it isn't a module here). A package is named by its path below node_modules/, and everything else is relative to the project root — root-relative for an in-project file that emits no module, ../sibling/x.ts for one outside it. The same applies to returnType, member type signatures, and the text of a typeInfo node — in the tree a module object is a terminal {kind: "other"} node carrying the same typeof import("…") text, never a reference (its symbol name is the quoted specifier, not a type name).
  • typeInfo: structured TypeJson tree beside the flat string — on variable and type-alias declarations (plus type-alias, interface, and class members, component props, parameters, and return types via returnTypeInfo). Absent when the flat string is the whole story; present when the tree carries structure the string can't: union/intersection members (alias name kept; enum members as {value, text} pairs with the qualified name as text), reference name + typeArgs, array element, tuple elements (label, ?/... markers, recursive type; arrays and tuples mark readonly). Object literals and function types stay terminal text — callability is the load-bearing renderer signal — with one narrow exception: a named generic instantiation classifies as a reference even when callable, so Snippet<[a: string]> is a reference whose tuple typeArg carries real elements (an instantiation over the empty tuple — Snippet<[]> — says nothing the string doesn't, so it stays absent). The headline case: a union alias's typeSignature prints as just its own name, and typeInfo carries the enumerable members — which is also why type aliases relax the absence rule: their flat string is always just the alias name, so the tree is emitted whatever its shape, except for object and function roots that members already covers. An alias TypeScript dropped (an indexed-access or conditional right-hand side — z.infer<typeof S>, valibot's InferOutput — loses its alias symbol, so the checker expands the structure everywhere) is recovered as {kind: "reference", name} instead of expanding, through two channels consulted only at nameless positions. The written annotation where one exists: each bare written type reference resolves by checker type identity. And the alias registry — the analyzed set's exported lost aliases, keyed by type identity — which also covers unannotated positions: inferred returns and variables, nested tree positions, null-bearing optionals. A registry-recovered reference additionally carries module — the declaring module's ModuleJson.path, always one the output emits, so a (module, name) lookup can't dangle; written-channel recoveries and checker-named references never carry it. Recovery applies at the root too, where the flat string carries the anonymous expansion, not the name. Names the checker has are never overridden; import renames recover the importable name; two aliases over one lost type resolve to a single winner everywhere; and a loss the registry can't recover surfaces as an alias_lost warning (see diagnostics)
  • sourceLine: line number in the source file
  • modifiers: e.g., "readonly", "static", "getter"
  • genericParams: type parameters with constraints and defaults
  • examples, deprecatedMessage, seeAlso, throws, since: from standard JSDoc tags
  • internalMessage: from @internal — a marker, not an exclusion (presence means tagged, empty string for a bare tag, trailing prose kept). Declarations and members only, not component props
  • mutates: from the non-standard @mutates tag, stored as Record<string, string> mapping target keys to descriptions. Keys are typically parameter names but compound paths (this.foo) and external state references are accepted as-is
  • alsoExportedFrom: modules that re-export this declaration
  • aliasOf: original name if this is a renamed re-export
  • partial: true when extraction failed partway through the declaration, indicating incomplete data

Declarations tagged with @nodocs are excluded from the output entirely and are also excluded from duplicate name checking.

Re-exports
#

Re-exports are encoded with two shapes, chosen by content:

  • Same-name: the canonical declaration carries an alsoExportedFrom array listing the modules that re-export it. One declaration, multiple import paths. The same edges publish from the re-exporting side as ModuleJson.reExports ({name, module, typeOnly, sourceLine}, with module the canonical module, multi-hop resolved), so barrels are self-describing without inverting every alsoExportedFrom array.
  • Renamed: a synthesized declaration appears in the re-exporting module with aliasOf: {module, name} pointing at the canonical. Inherits typeSignature, docComment, parameters, reactivity, and defaultValue from the canonical; sourceLine is the local export specifier's line.
  • Star exports: export * from './x' patterns are tracked separately on ModuleJson.starExports and don't synthesize per-declaration entries.
  • External re-exports: statements directly referencing a package (export {x} from 'pkg', export * as ns from 'pkg', export * from 'pkg') land on externalReExports / externalStarExports — flat statement facts with the specifier as written, no canonical to resolve.

When a re-export statement carries its own JSDoc or @nodocs, an alias is also synthesized in the re-exporting module so the local content has somewhere to live, even when the name is unchanged. The trigger is "presence of local content," not "presence of rename." Local doc-comment fields apply first and stick; canonical fields only fill gaps. @nodocs on a re-export suppresses both the link and the synthesis.

To compute a module's complete export surface from these encodings, use resolveExportSurface(modules, path) — it combines declarations, re-export edges, externals, and transitively-resolved star exports with ES semantics (explicit exports shadow star-projected names, names ambiguous between stars are excluded, default never projects), and reports unresolved or external star targets whose names it can't know.

Default-slot entries carry name === "default" (see the shared-fields note above for why). Renames out of the default slot (export {default as Foo} from './x') carry name: "Foo" and aliasOf: {module, name: "default"}. Duplicate-name checks skip "default" since the default slot is module-scoped per the JS spec.

Namespace re-exports (export * as ns from './x') synthesize a NamespaceDeclarationJson with module pointing at the source the namespace projects. Consumers render ns.a / ns.b by reading the source module's declarations; namespaces don't inline members.

MemberJson
#

Classes, interfaces, types, and enums can contain MemberJson entries in their members arrays. MemberJson is a discriminated union on kind with three variants:

  • "function": methods and call signatures. Adds parameters, returnType, returnTypeInfo, returnDescription, overloads, and optional defaultValue (from @default — the documented behavior when a callable option is omitted; top-level function declarations never carry one). External-origin call signatures are never enumerated: a property typed by an external function (run?: typeof spawn) documents as "variable" with the flat type text instead of pulling the package's overload set and docs into output; a mixed callable keeps its local signatures
  • "constructor": class constructors and construct signatures. Adds parameters, overloads
  • "variable": properties, accessors, and index signatures. Adds optional defaultValue (from @default), plus reactivity for class fields initialized with a Svelte rune, plus typeInfo beside the checker-rendered typeSignature — member types are checker-backed everywhere, interface properties, class properties, and setter-only accessors included

Member kind is restricted to these three variants. Nesting is exactly one level deep: members never contain their own members.

Member name is the user-chosen identifier in most cases, but synthesized names appear when no source identifier exists: "constructor" (class constructor), "(construct)" (construct signature on an interface or type alias), "(call)" (call signature on an interface or type alias), and bracketed index-signature names like "[key: string]" (the parameter name and key type as written for interfaces; a synthesized key for type aliases).

ComponentPropJson
#

Component declarations have a props array of ComponentPropJson entries, in source order:

  • name, type: prop name and TypeScript type
  • typeInfo: structured TypeJson tree when the type carries structure the flat string can't (see the declaration shared fields above)
  • optional: whether the prop is optional
  • description: from JSDoc on the prop
  • defaultValue: default value as a string, if present
  • bindable: set when the prop is declared with the $bindable() rune, so <Foo bind:value /> is supported. Modeled here (not via the variable-level reactivity field) because $props/$bindable are component-prop concerns
  • parameters: structured parameters for snippet-typed props (e.g., Snippet<[text: string]>), absent for non-snippet props
  • examples, deprecatedMessage, seeAlso, throws, since: symbol-scope JSDoc tags parsed from the prop's own doc comment (same shape as the declaration shared fields)

Asymmetry with ParameterJson. Props carry the symbol-scope tag fields above; function parameters deliberately don't. A prop is a named slot with its own documentation surface. A parameter is positional, and its @example/@deprecated/@since/@see/@throws belong on the enclosing function symbol per the TSDoc spec. Per-parameter content lives on ParameterJson.description (and propertyDescriptions for object-property docs) from @param only.

ParameterJson
#

Functions, snippets, constructors, and snippet-typed component props use ParameterJson entries in their parameters arrays:

  • name: parameter name (e.g., "options", "args" — a rest parameter's dots live in rest, never in the name)
  • type: resolved TypeScript type as a string
  • typeInfo: structured TypeJson tree when the type carries structure the flat string can't (see the declaration shared fields above)
  • optional: whether the parameter has a ? token
  • rest: whether the parameter uses rest syntax (...args)
  • description: from @param JSDoc
  • defaultValue: default value expression from the source, if present
  • propertyDescriptions: for named object parameters, a record of sub-path → description from dotted @param obj.prop tags, if present (destructured params, named __0 by TypeScript, are not covered)

OverloadJson
#

Functions and constructors with multiple signatures use OverloadJson entries in their overloads arrays. Each overload captures only signature-scope content, the fields that can vary meaningfully per signature:

  • typeSignature: the full overload signature as a string
  • parameters: parameter list for this overload, with per-overload @param descriptions
  • returnType: return type for this overload (functions only)
  • returnTypeInfo: structured TypeJson tree for this overload's return type, when it carries structure the string can't
  • genericParams: type parameters for this overload
  • docComment: per-overload JSDoc text, if present
  • returnDescription: from @returns on this overload

Symbol-scope JSDoc tags (@example, @deprecated, @since, @see, @throws, @mutates) describe the function as a whole and live on the parent declaration only, not duplicated per overload. The primary overload's JSDoc feeds the parent's symbol-level extraction; placing one of those tags on a non-primary overload signature emits a misplaced_tag warning and the tag is dropped (no synthetic content, no silent loss). Typo'd or stale @param keys produce unknown_param warnings the same way.

Reactivity
#

The reactivity field appears on VariableDeclarationJson and VariableMemberJson when the initializer is a value-producing Svelte rune call: $state, $state.raw, $derived, or $derived.by. Detection is purely syntactic and runs on every analyzed file regardless of extension, capturing the same patterns in a plain .ts file as in .svelte.ts or a component's <script>.

It covers variables (top-level and class fields). Function parameters and destructured bindings are not annotated even when the value flows from a rune. $props and $bindable are component-prop concerns and surface on ComponentPropJson's bindable field instead.

GenericParamJson
#

Declarations and members with type parameters use GenericParamJson entries in their genericParams arrays:

  • name: type parameter name (e.g., "T")
  • constraint: extends constraint, if present
  • defaultType: default type, if present

Rendering structured types
#

Flat type signatures are opaque strings produced by the TypeScript compiler. Where a typeInfo/returnTypeInfo tree exists beside one, flatten it with typeJsonToTokens to render per-node — linking name tokens to in-project declarations, syntax-highlighting code tokens, printing text punctuation as-is:

import {typeJsonToTokens} from 'svelte-docinfo'; typeJsonToTokens(declaration.typeInfo); // e.g. [{kind: 'name', name: 'Map'}, {kind: 'text', text: '<'}, // {kind: 'code', text: 'string'}, {kind: 'text', text: ', '}, // {kind: 'name', name: 'Tome'}, {kind: 'text', text: '>'}]

Spacing, separators, parenthesization, and tuple labels are decided by the tokenizer in lockstep with the TypeJson projection rules; what a token looks like — link, colored span, plain text — stays the renderer's decision.

Compact JSON and absent-as-false
#

By default, output uses compact JSON via compactReplacer: empty arrays, false booleans, and undefined fields are stripped, so optional, acceptsChildren, partial, rest, bindable, and similar fields vanish from the wire form when their value is the default. After parsing with the Zod schemas from types.ts (or AnalyzeResultJson for the full {modules, diagnostics} envelope), all defaults are restored, and the round-trip is lossless.

Raw-JSON consumers (e.g., jq, hand-rolled pipelines that skip .parse()) must treat absent as false; a literal decl.optional === false check silently fails because the key is gone. Use the schemas, or truthy/falsy checks (if (decl.optional) …) on raw JSON.

Examples
#

The JSON output for these examples is the compact wire form described above, so empty arrays, false booleans, and absent optional fields are stripped.

A TypeScript function in math.ts:

/** Clamp a number to a range. */ export const clamp = (value: number, min: number, max: number): number => Math.min(Math.max(value, min), max); { "modules": [ { "path": "math.ts", "declarations": [ { "name": "clamp", "kind": "function", "docComment": "Clamp a number to a range.", "typeSignature": "(value: number, min: number, max: number): number", "sourceLine": 2, "parameters": [ {"name": "value", "type": "number"}, {"name": "min", "type": "number"}, {"name": "max", "type": "number"} ], "returnType": "number" } ] } ] }

A Svelte component Card.svelte with a snippet prop, children, and an exported snippet:

<!-- @component A card with a customizable header. --> <script lang="ts" module> /** Default footer snippet. */ export {card_footer}; </script> <script lang="ts"> import type {Snippet} from 'svelte'; const { title, header, children, }: { title: string; /** Custom header rendering. */ header?: Snippet<[title: string]>; children?: Snippet; } = $props(); </script> <div class="card"> {#if header}{@render header(title)}{:else}<h2>{title}</h2>{/if} {@render children?.()} </div> {#snippet card_footer(text: string)} <small>{text}</small> {/snippet} { "modules": [ { "path": "Card.svelte", "declarations": [ { "name": "Card", "kind": "component", "docComment": "A card with a customizable header.", "sourceLine": 7, "props": [ {"name": "title", "type": "string"}, { "name": "header", "type": "Snippet<[title: string]>", "optional": true, "description": "Custom header rendering.", "typeInfo": { "kind": "reference", "name": "Snippet", "typeArgs": [ { "kind": "tuple", "elements": [ {"name": "title", "type": {"kind": "intrinsic", "text": "string"}} ] } ] }, "parameters": [ {"name": "title", "type": "string"} ] }, {"name": "children", "type": "Snippet<[]>", "optional": true} ], "acceptsChildren": true }, { "name": "card_footer", "kind": "snippet", "docComment": "Default footer snippet.", "typeSignature": "Snippet<[text: string]>", "sourceLine": 27, "parameters": [ {"name": "text", "type": "string"} ] } ] } ] }

A rune module counter.svelte.ts exporting reactive state:

export let count = $state(0); export const doubled = $derived(count * 2); { "modules": [ { "path": "counter.svelte.ts", "declarations": [ { "name": "count", "kind": "variable", "typeSignature": "number", "sourceLine": 1, "reactivity": "$state" }, { "name": "doubled", "kind": "variable", "typeSignature": "number", "sourceLine": 2, "reactivity": "$derived" } ] } ] }

A barrel index.ts that re-exports clamp from the math.ts above under a new name and star-exports other.ts. The rename synthesizes a declaration with aliasOf that inherits the canonical's signature and docs, the star export lands on starExports, and the dependency graph fields connect the modules. (A same-name re-export would instead add the barrel to the canonical's alsoExportedFrom.)

// index.ts export {clamp as clampNumber} from './math.js'; export * from './other.js'; // other.ts export const TAU = Math.PI * 2; { "modules": [ { "path": "index.ts", "declarations": [ { "name": "clampNumber", "kind": "function", "docComment": "Clamp a number to a range.", "typeSignature": "(value: number, min: number, max: number): number", "sourceLine": 1, "aliasOf": {"module": "math.ts", "name": "clamp"}, "parameters": [ {"name": "value", "type": "number"}, {"name": "min", "type": "number"}, {"name": "max", "type": "number"} ], "returnType": "number" } ], "dependencies": ["math.ts", "other.ts"], "starExports": ["other.ts"] }, { "path": "math.ts", "declarations": [ { "name": "clamp", "kind": "function", "docComment": "Clamp a number to a range.", "typeSignature": "(value: number, min: number, max: number): number", "sourceLine": 2, "parameters": [ {"name": "value", "type": "number"}, {"name": "min", "type": "number"}, {"name": "max", "type": "number"} ], "returnType": "number" } ], "dependents": ["index.ts"] }, { "path": "other.ts", "declarations": [ {"name": "TAU", "kind": "variable", "typeSignature": "number", "sourceLine": 1} ], "dependents": ["index.ts"] } ] }

See the types module for the full Zod schemas, and the API reference for all exported types.