svelte-docinfo

static analysis for TypeScript and Svelte 📜

repo 0.11.0 npm

npm i -D svelte-docinfo

introduction #

svelte-docinfo extracts JSON describing the exports of TypeScript and Svelte modules for open-ended use cases like docs, code search, and dev tools. It uses the TypeScript compiler API and svelte2tsx to resolve types, track exports+imports, and extract semantic details. The npm package has a Vite plugin, CLI, and programmatic API.

svelte-docinfo is largely inspired by sveld, but uses the TypeScript compiler API instead of AST-only inspection, and also analyzes TypeScript modules. See the comparison below.

The library is mostly complete for Svelte 5 and used in production websites, but you may find gaps and flaws -- please open issues for bugs, and discussions for everything else!

Dependencies are minimal and the tool's scope is limited to data, not presentation. These docs were made using the data produced by svelte-docinfo, like the architecture and API reference, with fuz_ui components.

AI disclosure: the code and docs beyond the intro were mostly written by Claude Code with uneven human guidance. The first release took 5 months of intermittent work and ~500 manual commits to extract its initial implementation from fuz_ui, which was more limited, lacking the fancy TS compiler usage, and grew slowly over years without AI assistance.

Install
#

Published as svelte-docinfo to npm:

npm i -D svelte-docinfo

Usage
#

There are several ways to get the JSON, from most opinionated to most flexible.

For SvelteKit and Vite projects, the Vite plugin is the recommended path. It runs the analysis at build time and serves the result as a virtual module that stays current in dev:

import {modules} from 'virtual:svelte-docinfo';

Run the CLI to inspect a project from the command line:

# Analyze the current project and print JSON to stdout npx svelte-docinfo # Analyze a specific directory and write to a file npx svelte-docinfo ./packages/my-lib -o docs/library.json

For standalone use or custom build tools, two functions cover most cases. analyzeFromFiles discovers files for you:

import {analyzeFromFiles} from 'svelte-docinfo'; const {modules, diagnostics} = await analyzeFromFiles({ projectRoot: process.cwd(), });

If your build tool already has file contents in memory, analyze skips discovery. See the build tools guide:

import {analyze, createSourceOptions} from 'svelte-docinfo'; const {modules} = await analyze({ // files outside sourcePaths (default src/lib) feed the checker but emit no module sourceFiles: [{id: '/project/src/lib/file.ts', content: '...'}], sourceOptions: createSourceOptions('/project'), });

For long-lived consumers that re-analyze the same source set (Vite plugin, LSP-style tools), createAnalysisSession returns a persistent handle that reuses parsed ASTs and svelte2tsx output across calls. See the session guide.

See the API reference for all exported functions and types.

Not supported
#

Standalone namespace Foo {} declarations document as a bare variable with no members (namespace re-exports are supported). Decorators aren't modeled.

Svelte 4 features like slots are not supported. Legacy export let props aren't extracted, but a legacy_props warning names them. Svelte context usage isn't captured either.

Key features
#

  • full type resolution: infers complex types without manual annotations, including generics, imported types, and inferred return types, with source locations for every declaration
  • TSDoc/JSDoc parsing: extracts standard tags (@param, @returns, @example, @deprecated, etc.) plus @nodocs to exclude from docs, @internal kept as an internalMessage marker, and @mutates to flag side effects
  • structured types: typeInfo trees beside the flat type strings, with union members, type arguments, tuple elements, and alias names recovered where TypeScript drops them (z.infer and friends); see the output format
  • merged value+type symbols: a schema/type pair (const Foo = z.strictObject({...}) + type Foo = z.infer<typeof Foo>) documents the type with full structure, marked mergedValue since the name is also a runtime value
  • Svelte 5 components: analyzes components via svelte2tsx, extracting prop types, defaults, bindability, snippet parameters, children detection, and exported template snippets, including JS components (props from the JSDoc @type on $props())
  • Svelte 5 reactivity runes: detects $state, $state.raw, $derived, and $derived.by on variables and class fields via the reactivity field, in any analyzed file
  • re-export tracking: alsoExportedFrom arrays with the forward view on ModuleJson.reExports, aliasOf for renames, default-slot entries named "default", export * from patterns, direct external re-exports, and resolveExportSurface to combine them all with ES star semantics
  • dependency graphs: tracks imports between modules and computes dependents
  • function overloads: captures all public overload signatures with per-overload JSDoc
  • build-tool agnostic: files can come from disk, a build pipeline, or memory
  • diagnostic collection: accumulates warnings and errors without halting, so you can report problems in batch

Compared to sveld
#

sveld is a Svelte component documentation generator that walks the AST and infers types from JSDoc annotations and literal values. svelte-docinfo instead uses the TypeScript compiler API (via svelte2tsx) as its source of truth, so it resolves imported types, generics, and complex inferred types without requiring @type annotations. It also analyzes TypeScript modules, not just .svelte files.

svelte-docinfo additionally tracks re-exports across modules, computes dependency graphs, and records source locations. It doesn't support Svelte 4 features like slots and dispatched events (Svelte 5 replaces them with snippets and callback props) or the context API.

Example output
#

See the output format reference for example JSON output.

cli #

Run in a project with TypeScript or Svelte source and a tsconfig.json at or above its root. The CLI prints JSON describing the project's exports to stdout.

Basic usage
#

npx svelte-docinfo # analyze the current directory npx svelte-docinfo ./packages/my-lib # analyze a specific directory npx svelte-docinfo -o output.json # write to a file instead npx svelte-docinfo --pretty # pretty-print the JSON output

Files are discovered from package.json exports, falling back to glob. -i supplies explicit patterns, --discovery glob skips exports, and --discovery exports fails when exports is missing or resolves to no files.

--source-dir sets the source directory (default src/lib, repeatable for monorepos). --source-root sets the base of output module paths (default: the single source dir, or the longest common prefix of several).

Compact JSON pairs well with jq:

npx svelte-docinfo | jq '.modules | length' # count modules npx svelte-docinfo | jq -r '.modules[].declarations[].name' # list all exported names

JSON goes to stdout and messages go to stderr, so > and | capture clean JSON. -q silences info messages; warnings and errors still print.

Options
#

FlagDescription
[project-root]project root directory (default: cwd)
-i, --include <pattern>include pattern (repeatable). Replaces exports discovery and widens the source scope (see below); incompatible with --discovery exports
-e, --exclude <pattern>exclude glob, applied at discovery and analysis (repeatable). Replaces the defaults **/*.test.ts, **/*.spec.ts, **/internal/** entirely
-o, --output <file>output file (default: stdout; - also means stdout, so -o "$OUT" works when $OUT=-)
--discovery <mode>auto | exports | glob (default: auto: exports first, glob fallback). exports fails when package.json exports is missing or resolves to no files
--dist-dir <dir>dist directory for exports discovery (default: dist)
--source-dir <dir>source directory, relative to the project root or absolute inside it (default: src/lib). Repeatable; also sets the default include glob
--source-root <dir>base of output module paths (default: single source dir or longest common prefix; . for project-relative)
--only <pattern>glob filter on output module paths (repeatable). The full project is still analyzed, so re-exports and dependents stay correct; diagnostics aren't filtered
--no-resolve-dependenciesdisable dependency resolution (dependencies/dependents stay empty)
--prettypretty-print JSON output (default: compact)
-q, --quietsuppress info messages on stderr (warnings and errors still print)
--on-duplicates <mode>opt-in check that names are unique across modules, for packages with a flat export namespace: diagnostic | warn | throw (default: off)
-V, --versionshow version number

--include patterns widen the source scope: each pattern's static base joins the source dirs, and module paths become relative to the widened root. A pattern with no base (**/*.ts, a root file) makes the whole project root source and logs an info line.

Regardless of --exclude, node_modules and dot-directories below a source dir are never source. The default excludes cover tests and the src/lib/internal/ convention (**/internal/**). Exports discovery also skips subpaths blocked by null exports entries ("./internal/*": null), using Node's best-match rules.

Absolute paths and patterns inside the project root are accepted; ones outside it are an error.

Exit codes: 0 success, 1 analysis errors, 2 CLI errors or a thrown analysis error (a missing or unloadable tsconfig.json, strict exports discovery failing).

For SvelteKit and Vite projects where the analysis feeds into your app bundle, see the Vite plugin.

vite plugin #

The Vite plugin is the recommended path for SvelteKit and Vite projects. It runs analysis at build time and serves the result as 'virtual:svelte-docinfo'. In dev it watches your source files, plus non-source files they import like internal/ modules, and reloads the page when an edit changes the result.

Setup
#

Requires Vite 6 or later.

  1. Add the plugin to vite.config.ts:

    import {defineConfig} from 'vite'; import {sveltekit} from '@sveltejs/kit/vite'; import svelteDocinfo from 'svelte-docinfo/vite.js'; export default defineConfig({ plugins: [sveltekit(), svelteDocinfo()], });
  2. Add TypeScript support in your app.d.ts:

    /// <reference types="svelte-docinfo/virtual-svelte-docinfo.js" />
  3. Import the virtual module anywhere in your app:

    import {modules, diagnostics} from 'virtual:svelte-docinfo'; // or use the default export: import data from 'virtual:svelte-docinfo'; // data.modules and data.diagnostics are the same as the named exports

    The shape is AnalyzeResultJson, the same as the programmatic API. See diagnostics for diagnostics.

If TypeScript reports Cannot find module 'virtual:svelte-docinfo', check the /// <reference> line in app.d.ts.

Options
#

All options are optional; the minimal call discovers files from package.json exports, falling back to glob:

svelteDocinfo()

Every option, with its default:

import svelteDocinfo from 'svelte-docinfo/vite.js'; svelteDocinfo({ // Project root directory. Default: Vite's resolved config.root. projectRoot: process.cwd(), // Glob patterns for file discovery. Forces glob mode under discovery: 'auto'. // Default: undefined (use exports discovery). // Each pattern's static base joins sourceOptions.sourcePaths; a pattern // with no base makes the whole project root source and logs an info line. include: ['src/**/*.ts', 'src/**/*.svelte'], // Exclude globs. An array replaces the default // ['**/*.test.ts', '**/*.spec.ts', '**/internal/**']; a callback extends // it. Replaces sourceOptions.exclude when both are set. node_modules and // dot-directories are always excluded. exclude: (defaults) => [...defaults, '**/*.gen.ts'], // Discovery strategy: 'auto' | 'exports' | 'glob'. Default: 'auto'. // 'auto' → exports first, glob fallback // 'exports' → strict; throws if package.json exports is missing or resolves to no files // 'glob' → skip exports, use glob patterns discovery: 'auto', // Dist directory for exports discovery. Default: 'dist'. distDir: 'dist', // Resolve module dependency graph. Default: true. resolveDependencies: true, // Partial overrides for default source options (SvelteKit src/lib layout). // Merged into createSourceOptions(projectRoot, sourceOptions). sourceOptions: {sourcePaths: ['src/lib']}, // dev re-analysis debounce in ms. Default: 100. hmrDebounceMs: 100, // Opt-in check that names are unique across modules, for packages with a // flat export namespace. Default: undefined (off). // 'diagnostic' → emit duplicate_declaration diagnostics // 'warn' → also log them // 'throw' → also fail the build onDuplicates: undefined, })

The plugin runs the same pipeline as analyzeFromFiles: discover, resolve dependencies, analyze. In dev, imports resolve through Vite's resolveId, so Vite aliases apply; in builds, the TypeScript default resolver is used (it honors tsconfig paths). hmrDebounceMs only affects the dev watcher.

Paths and patterns resolve against projectRoot. Absolute paths and patterns inside the root are accepted; anything outside it throws at config time.

CLI vs Vite plugin
#

The CLI runs analyzeFromFiles once; use it for CI and one-off generation. The plugin keeps a persistent createAnalysisSession, so dev re-analysis reuses parsed ASTs and svelte2tsx output; use it when the analysis feeds your app bundle. To drive a session yourself (custom bundler, LSP), see the session guide.

How it works
#

The plugin hooks into these Vite lifecycle stages:

  1. configResolved: validates options, so bad configs (like discovery: 'exports' with include, or a path outside the project root) fail at startup
  2. configureServer: in dev, watches source files and the non-source files they import (e.g. internal/ modules), debounces re-analysis, and reloads the page only when the output changes. Unchanged files aren't re-parsed.
  3. buildStart: creates a session, discovers and ingests the source files, runs query, and caches the result
  4. resolveId / load: serves the cached result as virtual:svelte-docinfo, exporting modules, diagnostics, and a default {modules, diagnostics}
  5. buildEnd: disposes the session, in dev when the server closes or restarts and in a build once the bundle's modules have loaded

Any change to the docs data reloads the page, even one that only shifts a declaration's source line, instead of letting Svelte HMR update components in place. Importers build state from the data (like a layout building a library from modules), so re-running the module alone would leave them stale.

architecture #

svelte-docinfo analyzes its own source and draws the result below. Each node is one module under src/lib/; each arrow is an internal import. Hover any module to isolate its immediate neighbors; click to jump to its API page.

How to read it
#

  • Arrows point from importer to imported: X depends on Y. Reads with gravity, so dependencies fall downward toward the foundation.
  • Layers are assigned by longest-path-from-sink. A module's layer is one plus the deepest layer it transitively depends on. Sinks (no internal imports) land at the bottom.
  • Hover a module to isolate its immediate neighbors. Edges split by direction: what the module depends on and what depends on it.

What the layers say
#

The bottom rows are the primitives that import nothing else in the library: path normalization, types, concurrency caps, the diagnostics schema. The middle rows are the per-kind TypeScript extractors and the file-system helpers. The top rows are the orchestrators: the persistent session on top of core analysis, then the one-shot wrappers and the vite plugin (both built directly on the session), then the cli.

Across 33 modules and 151 internal imports, the graph is naturally acyclic, so no back-edges had to be reversed to lay it out.

How this was drawn
#

The layout is precomputed at build time by dependency_graph.gen.json.ts, which calls analyzeFromFiles on this project and runs a Sugiyama-style layered layout: longest-path-from-sink for layer assignment, dummy nodes on long edges, median-heuristic crossing reduction. The result is a small JSON sibling the Svelte component renders to SVG. No layout libraries — a few hundred lines, end to end.

session #

createAnalysisSession returns a persistent analysis handle backed by a TypeScript LanguageService. Use it when the same source set is re-analyzed repeatedly (Vite plugin, LSP-style tools): parsed ASTs, svelte2tsx output, and the dependency graph are reused across cycles. The one-shot analyze and analyzeFromFiles wrap single-use sessions.

Construction
#

import {createAnalysisSession, createSourceOptions} from 'svelte-docinfo'; const session = createAnalysisSession({ sourceOptions: createSourceOptions(process.cwd()), // Optional: session-default ImportResolver (or a bare resolve function). // Defaults to TypeScript module resolution with your tsconfig. // resolveImport: myResolver, // Optional (default true): also ingest the in-project non-source files // your sources import, so edits to them stay live. // contextClosure: true, // Optional: logger for session-level messages. // log: console, });

AnalysisSessionOptions requires complete ModuleSourceOptions; build them with createSourceOptions, which merges your overrides with DEFAULT_SOURCE_OPTIONS. The session applies no further defaults.

The tsconfig (the tsconfig option, or the project's) is parsed once, at construction. compilerOptions merge over it per key, and the type checker and default resolver both see the merged result. The tsconfig file is still required, its whole extends chain must load, and later edits to it aren't picked up; create a new session.

Lifecycle
#

The session owns a source set that you add to, update, and remove from:

MethodPurpose
setFile(file, opts?)Ingest one file's content; a no-op on cache hit. Returns SetFileResult
setFiles(files, opts?)Ingest a batch. Additive; never removes. Returns SetFilesResult with aggregate and per-file views
deleteFile(id)Remove a file from the session
has(id)Whether the given absolute path is currently owned
list()Snapshot of owned file IDs (insertion order), context files included
query(opts?)Analyze the owned set. Files outside sourcePaths or matching exclude inform type resolution but emit no module. Returns AnalyzeResultJson
allIngestDiagnostics()Cumulative ingest-time diagnostics across every owned entry
getProgram()The underlying ts.Program, for your own checker work. It goes stale after any setFile, setFiles, or deleteFile that changes the session, so call it again afterward
dispose()Release resources and clear the owned set. Don't use the session afterward

Concurrency. The session isn't safe for overlapping calls, so serialize them. setFile / setFiles are async (resolvers may await I/O); query is sync and reads the current owned set, so await pending ingests first.

Cache-hit semantics
#

Re-ingesting a file either reuses everything cached for it or recomputes everything. What counts as a hit depends on the mode:

  • lex+resolve (default): identical content and the same ImportResolver identity.
  • pre-resolved (caller supplies SourceFileInfo.dependencies): identical content and an element-wise equal array. A fresh array with the same elements hits, so [...filer.deps.keys()] per call works; any length, element, or order difference misses.

Switching a file between modes always misses.

Unresolved imports are the exception: a file's edges can change without re-ingesting it. When a setFile / setFiles call adds files, the session retries specifiers that previously resolved to null in lex+resolve files (with the same resolver), updates their edges, and clears stale resolver_failed diagnostics. It also calls the resolver's optional invalidate() before the next batch whenever files were added or removed since it last ran, so cached failed lookups don't stick. Pre-resolved files are untouched.

On a hit, SetFileResult.changed is false and the cached ingest diagnostics are returned without doing any work.

ImportResolver and identity
#

ImportResolver is {resolve, identity, invalidate?}. identity is a stable opaque token (string or symbol) that keys the cache alongside content; invalidate() clears any cached failed lookups when files are added or removed.

import type {ImportResolver} from 'svelte-docinfo'; const myResolver: ImportResolver = { identity: 'vite-plugin-container', resolve: (specifier, fromFile) => { // return absolute path, or null for externals return null; }, };

identity is required because function references make poor cache keys: callers often wrap a resolver in a fresh closure per call (a common Vite/Rollup pattern), which would defeat caching. Reuse the same token as long as resolution behaves the same, and change it when it doesn't.

resolveImport also accepts a bare resolve function. The session-level one is wrapped once at construction, so its identity is stable; a bare function passed per call gets a fresh identity every call, and so never cache-hits.

When neither AnalysisSessionOptions.resolveImport nor a per-call SetFileOptions.resolveImport is supplied, the session builds a default from TypeScript module resolution and the merged compiler options on first use. It's never built if every file arrives with SourceFileInfo.dependencies and the context closure is off or finds nothing to ingest.

Trust mode for pre-resolved dependencies
#

Supplied SourceFileInfo.dependencies are accepted as-is, so a buggy upstream resolver skews ModuleJson.dependencies / dependents without warning. See build-tool integration for the trust contract and type-only edges.

Diagnostics: ingest-time vs query-time
#

Diagnostic kinds have two lifecycles:

  • Ingest-time (transform_failed, source_map_failed, import_parse_failed, resolver_failed): returned from setFile / setFiles and kept with the file until it is re-ingested or deleted.
  • Query-time (the rest): recomputed and returned by every query call.

query() does not include ingest diagnostics, and setFile / setFiles return only those for the files you passed (not context-closure files). allIngestDiagnostics() returns the complete set for every owned file:

const queryResult = session.query(); const fullDiagnostics = [ ...session.allIngestDiagnostics(), ...queryResult.diagnostics, ];

It's cheap, so long-lived consumers can republish the full picture every cycle without tracking per-batch returns. The Vite plugin does this on every dev re-analysis.

Discovery-time diagnostics (module_unreadable) are a third category. Sessions don't run discovery, so if you do, keep those diagnostics yourself and merge them into what you publish (see discoverSourceFiles).

Worked example: incremental loop
#

An LSP-style edit/query loop. Each setFile updates one file; query reanalyzes, reusing parsed ASTs and svelte2tsx output for unchanged files.

import {createAnalysisSession, createSourceOptions, hasErrors} from 'svelte-docinfo'; const session = createAnalysisSession({ sourceOptions: createSourceOptions(process.cwd()), }); // Initial population. await session.setFiles([ {id: '/abs/src/lib/a.ts', content: '...'}, {id: '/abs/src/lib/b.ts', content: '...'}, ]); // Edit: one file changed. const {changed, diagnostics: ingest} = await session.setFile({ id: '/abs/src/lib/a.ts', content: '... new content ...', }); if (changed) { const {modules, diagnostics: pass} = session.query(); const all = [...session.allIngestDiagnostics(), ...pass]; if (hasErrors(all)) { // surface to the editor } } // Tear down. session.dispose();

For HMR and file watchers, pass all changed files to one setFiles call rather than looping setFile: a batch resolves imports in parallel.

When to use one-shot APIs instead
#

If you analyze once (CLI, CI, one-off doc generation), use analyze or analyzeFromFiles; a session only pays off across repeated calls. See session.ts for the full type definitions and diagnostics for the diagnostic kinds.

build tools #

svelte-docinfo is build-tool agnostic: you hand it files as SourceFileInfo objects, from disk, a build pipeline, or editor buffers. This page covers embedding analysis in a bundler, watcher, or LSP-style tool that the Vite plugin doesn't fit.

The analyzer reads the disk only for the tsconfig, for type-checker context you don't supply, and, except in analyze(), for the in-project non-source files your sources import (context files).

SourceFileInfo
#

Every entry point takes SourceFileInfo, including analyze and the session's setFile / setFiles; discoverSourceFiles returns it:

interface SourceFileInfo { id: string; // absolute path (native ok at boundary) content: string; // file contents dependencies?: string[]; // optional: pre-resolved deps (opt-in) }
  • id: absolute path. Native Windows paths are accepted (see Paths).
  • content: required; inputs are never read from disk.
  • dependencies: optional absolute paths. When supplied, the session skips its own import parsing for this file (see Pre-resolved dependencies).

Reverse edges (dependents) are always computed by the analyzer.

Three entry points by ownership
#

Pick by what your tool already owns:

UseWhen
analyzeFromFilesYou own a project root and want everything in one call: file discovery, dependency resolution, analysis. CLI-style.
analyzeYou already hold file contents in memory (a Rollup/esbuild plugin with files in the bundle graph). Single-pass, one-shot.
createAnalysisSessionYou re-analyze the same source set across many cycles (Vite, watch mode, LSP). See the session guide.

The one-shot APIs are thin wrappers over single-use sessions, with the same diagnostics and output shape.

discoverSourceFiles (standalone)
#

discoverSourceFiles runs file discovery without analysis, for tools that need the source set up front (watcher registration, counts, pre-flight checks) and analyze separately.

import {discoverSourceFiles, createSourceOptions} from 'svelte-docinfo'; const {files, diagnostics} = await discoverSourceFiles({ sourceOptions: createSourceOptions(process.cwd()), // discovery: 'auto' | 'exports' | 'glob' (default 'auto') // include: ['src/**/*.ts'] (forces glob under 'auto') // distDir: 'dist' (for exports discovery) }); // files: Array<SourceFileInfo> — content already loaded from disk // diagnostics: module_unreadable for any file the exports map names // but readFile failed on (permission denied, FS error)

The strategies match the CLI and Vite plugin: 'auto' tries package.json exports and falls back to glob, 'exports' is strict (throws if exports is missing or resolves to no files), and 'glob' skips exports. Passing include under 'auto' goes straight to glob, so the filter is never silently ignored.

Unlike analyzeFromFiles and the Vite plugin, standalone discovery doesn't widen sourcePaths to cover include, so files outside src/lib would be dropped at analysis. To keep them, build the options with createSourceOptionsWithInclude (from svelte-docinfo/source-config.js), passing the same include.

Session consumers own the discovery-time module_unreadable diagnostics, since sessions don't run discovery (analyzeFromFiles merges them for you). Their message embeds the filesystem error's absolute path, so pass them through normalizeDiagnosticPaths (from svelte-docinfo/analyze-core.js) before publishing.

Pre-resolved dependencies (fast path)
#

Build tools with their own dependency graph (Gro's filer, Rollup's bundle graph, webpack's module graph) can hand it over in SourceFileInfo.dependencies as absolute paths. The session then skips import parsing and resolution for that file.

import type {SourceFileInfo} from 'svelte-docinfo'; // inside your build-tool integration const files: Array<SourceFileInfo> = [...filer.modules].map(([id, mod]) => ({ id, content: mod.content, dependencies: [...mod.dependencies.keys()], // absolute paths })); await session.setFiles(files);

A file re-ingested with the same content and an element-wise equal dependencies array is a cache hit, so building a fresh array per call is fine. See Cache-hit semantics.

Trust contract
#

Pre-resolved dependencies are trusted: the session doesn't check them against content. A buggy upstream resolver goes unnoticed:

  • Edges in dependencies but absent from content are accepted.
  • Imports in content but missing from dependencies are omitted.

Neither case emits a diagnostic, because legitimate sequences across batches (declaring a dependency on a file that is set later, or later deleted) would be flagged too. The default lex+resolve path always follows the actual imports, so use it if you don't fully trust your dependency source.

Type-only edges
#

With pre-resolved dependencies, you decide whether import type {X} from './x' counts as a dependency. Two common stances:

  • Keep type-only edges: what the default lex+resolve path does, for import type, inline type specifiers, and type-only re-exports like export type {X} from.
  • Drop type-only edges: what Gro's filer does, so the output reflects the runtime graph only.

Switching from lex+resolve to Gro's graph therefore removes type-only edges from ModuleJson.dependencies and dependents.

ImportResolver (lex+resolve path)
#

Without pre-resolved dependencies, the session parses import specifiers from content and resolves them through an ImportResolver. Wire in your own so resolution matches the rest of your build:

import {createAnalysisSession, createSourceOptions} from 'svelte-docinfo'; import type {ImportResolver} from 'svelte-docinfo'; const resolver: ImportResolver = { identity: 'my-bundler@v1', resolve: (specifier, fromFile) => bundler.resolve(specifier, fromFile), }; const session = createAnalysisSession({ sourceOptions: createSourceOptions(process.cwd()), resolveImport: resolver, });

identity is a stable cache token; see the session guide. Without a resolver, the session uses a default built on TypeScript module resolution and your tsconfig.

Implement the optional invalidate() if your resolver caches failed lookups: the session calls it before a batch whenever files were added to or removed from the session, since a specifier that resolved to nothing may resolve now. The default resolver also sees files that exist only in the session (even in directories absent from disk); a custom resolver must handle in-memory files itself to get dependency edges for them.

Context files
#

By default a session also ingests the in-project, non-source files its sources import (transitively, skipping node_modules and dot-directories), so edits to them, like internal/ modules, stay live instead of being read from disk once. These context files emit no modules and add no edges. Their imports always go through lex+resolve, so when the closure finds files, even a fully pre-resolved session builds the default resolver (unless you supply resolveImport). analyze() turns the closure off; analyzeFromFiles keeps it on.

Source options and the source root
#

createSourceOptions builds the ModuleSourceOptions the session needs. Customize for non-default layouts:

import {createSourceOptions} from 'svelte-docinfo'; // Default: single sourcePaths=['src/lib'], test/spec + internal/ exclude. const opts = createSourceOptions(process.cwd()); // Monorepo: multiple source directories, optional explicit sourceRoot. const monorepoOpts = createSourceOptions(process.cwd(), { sourcePaths: ['packages/a/src', 'packages/b/src'], // sourceRoot derived as longest common prefix when omitted. }); // Extend the default exclude (callback form — the array form replaces). const customOpts = createSourceOptions(process.cwd(), { exclude: (defaults) => [...defaults, '**/fixtures/**'], });

sourceRoot is the base of ModuleJson.path; pass '.' for project-relative paths.

exclude applies at both discovery and analysis, so an excluded file never appears in output however it was found. The defaults cover tests and the src/lib/internal/ convention (**/internal/**: internal modules that public modules import but that aren't documented). An array replaces the defaults; the callback form extends them. Independent of exclude, node_modules and dot-directories below a source path are never source. That rule is relative to the source path, so an explicit dot-directory source path (sourcePaths: ['.hidden/src']) still works.

sourcePaths and sourceRoot are relative to the project root. Absolute entries inside the root are accepted and stored relative; entries outside it throw when the options are created, since output paths can't represent them. Absolute exclude globs (and include patterns at the discovery entry points) follow the same rule.

Paths: POSIX-form contract
#

Native paths are accepted at the API boundary and converted to POSIX form (forward slashes) on ingest, so Windows callers can pass C:\repo\src\lib\foo.ts. ModuleJson.path, Diagnostic.file, and the session's list() always report POSIX form.

Drive-letter case (C:\ vs c:\) and extended-length \\?\ prefixes are not normalized. See paths.ts.

Concurrency caps
#

File reads and resolver calls each run with bounded parallelism (MAX_FILE_CONCURRENCY and MAX_RESOLVE_CONCURRENCY in concurrency.ts), so a custom resolver never sees an unbounded burst of concurrent calls.

output format #

svelte-docinfo outputs JSON describing your project's exported API. Modules contain declarations, and some declarations contain members or props. To see it in practice, jump to the examples.

Top-level structure
#

Every surface returns the same envelope, AnalyzeResultJson: the programmatic entry points (analyze, analyzeFromFiles), the CLI's JSON, and the Vite plugin's virtual module.

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

diagnostics is covered in diagnostics. The CLI emits compact JSON, so an empty project prints {}; parse it through AnalyzeResultJson to restore the 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' targets
  • reExports: same-name re-export edges ({name, module, typeOnly, sourceLine}), the forward view of alsoExportedFrom (see Re-exports)
  • externalReExports: direct re-exports from packages ({name, specifier, originalName?, typeOnly, sourceLine})
  • externalStarExports: export * from 'pkg' specifiers as written
  • partial: true when the module is a placeholder for a .svelte file that svelte2tsx failed to transform (see transform_failed)

Array fields are omitted from JSON when empty and default to [] after parsing.

DeclarationJson
#

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

  • "function": adds parameters, returnType, returnTypeInfo, returnDescription, overloads
  • "variable": adds optional defaultValue (from @default) and reactivity (see Reactivity)
  • "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 path of the module projected by export * as ns from './x'

A name declared as both a value and a type, like the schema pattern export const Foo = z.strictObject({...}) + export type Foo = z.infer<typeof Foo>, produces one declaration. The type meaning documents like its un-merged equivalent (structure, members, typeInfo), and mergedValue: true marks that the name is also a runtime value, so generateImport emits import instead of import type. JSDoc falls back to the value declaration's comment when the type 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)
  • docComment: JSDoc comment text
  • typeSignature: the full type as a string, rendered by the TypeScript checker (see module paths below)
  • typeInfo: a structured TypeJson tree beside the flat string, on variables and type aliases (plus members, component props, parameters, and return types via returnTypeInfo). See structured types below
  • 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 rather than an exclusion (empty string for a bare tag)
  • mutates: from @mutates, a Record<string, string> of target to description (empty for a target with no description). See tags
  • alsoExportedFrom: modules that re-export this declaration
  • aliasOf: the canonical {module, name} of a renamed re-export
  • partial: true when extraction failed partway, so the data is incomplete

Declarations tagged @nodocs are excluded from the output and from the opt-in duplicate-name check.

Module paths in type text
#

TypeScript prints a module object as typeof import("…") with an absolute path, from an import() expression or a typeof over a namespace import. svelte-docinfo rewrites the path so output never carries one:

  • 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), where a miss means it isn't a module here
  • a package is named by its path below node_modules/
  • anything else is relative to the project root, with ../sibling/x.ts for a file outside it

This applies to typeSignature, returnType, member signatures, and typeInfo text. In a tree, a module object is a terminal {kind: "other"} node carrying that text, never a reference.

Structured types (typeInfo)
#

typeInfo is absent when the flat string says everything: intrinsics, bare references, object and function types. It is present when the tree adds structure: union and intersection members (alias names kept; enum members as {value, text} pairs with the qualified name as text), reference name and typeArgs, array element, and tuple elements (label, ?/... markers, element type). Arrays and tuples mark readonly. The ten node kinds are intrinsic, literal, reference, array, tuple, union, intersection, function, object, and other.

Object literal and function types stay terminal text. Anything callable is a function node, except a named generic instantiation, which is a reference: Snippet<[a: string]> is a reference whose tuple type argument carries real elements. An instantiation over the empty tuple (Snippet<[]>) adds nothing, so it stays absent.

Type aliases relax the absence rule. A union alias's typeSignature prints as just its own name, so the tree is emitted whatever its shape to carry the members, except for object and function roots, which members already covers.

Union members follow the order the author wrote them: the alias declaration's for an aliased union, else the annotation at that position, with null and undefined last. The flat strings print in TypeScript's internal order, which follows type-creation history, so the two can list the same members in different orders; read the tree for author intent, such as control options. A union with no written form (inferred, keyof, mapped or conditional results, generic alias instantiations) takes the internal order in both.

Lost aliases. TypeScript drops the name of an alias whose right-hand side is an indexed access or conditional (z.infer<typeof S>, valibot's InferOutput) and expands its structure everywhere it's used. svelte-docinfo recovers the name as {kind: "reference", name} wherever the checker has none, from two sources: the type annotation written at that position, and a registry of the analyzed set's exported lost aliases. The registry also covers unannotated positions: inferred returns and variables, nested positions, and optional positions typed T | null.

A registry-recovered reference also carries module, the declaring module's ModuleJson.path. It always names a module in the output, so a (module, name) lookup can't dangle; other references never carry it. Recovery applies at the root too, where the flat string still holds the expansion. Names the checker has are never overridden, import renames recover the importable name, and two aliases over one lost type resolve to a single winner everywhere. A loss that can't be recovered emits an alias_lost warning (see diagnostics).

Re-exports
#

Re-exports are encoded by shape:

  • Same-name: the canonical declaration's alsoExportedFrom lists the modules that re-export it: one declaration, several import paths. The re-exporting module lists the same edges on ModuleJson.reExports, with module naming the canonical module (resolved through chains), so barrels are self-describing.
  • Renamed: a declaration is synthesized in the re-exporting module with aliasOf: {module, name} pointing at the canonical. It inherits the canonical's analyzed shape (typeSignature, docComment, parameters, and so on); sourceLine is the local export specifier's line.
  • Star exports: export * from './x' lands on ModuleJson.starExports, with no per-declaration entries.
  • Gated canonicals: re-exporting a symbol from a module excluded from output (e.g. internal/) synthesizes a full alias even when the name is unchanged, since the canonical module emits nothing to link. Its aliasOf.module names a module absent from output, and no reExports edge is emitted.
  • External re-exports: statements that directly name a package (export {x} from 'pkg', export * as ns from 'pkg', export * from 'pkg') land on externalReExports / externalStarExports with the specifier as written.

A re-export statement with its own JSDoc also synthesizes an alias in the re-exporting module, even when the name is unchanged, so the local content has somewhere to live. Local doc fields win; the canonical's fields fill gaps. @nodocs on a re-export suppresses both the link and the synthesis.

To get a module's complete export surface, call resolveExportSurface(modules, path). It combines declarations, re-export edges, externals, and star exports with ES semantics (explicit exports shadow star-projected names, names ambiguous between stars are excluded, default never projects), and reports star targets whose names it can't know.

Default-slot entries carry name === "default", the symbol's JS-spec name. 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 each module has its own default slot.

Namespace re-exports (export * as ns from './x') produce a "namespace" declaration whose module names the projected source. Namespaces don't inline members; render ns.a by reading that module's declarations.

MemberJson
#

Classes, interfaces, types, and enums list MemberJson entries in members, a discriminated union on kind with three variants:

  • "function": methods and call signatures. Adds parameters, returnType, returnTypeInfo, returnDescription, overloads, and optional defaultValue (from @default, documenting the behavior when a callable option is omitted). A property typed by an external function (run?: typeof spawn) is a "variable" with the flat type text rather than the package's overloads and docs; 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), typeInfo, and reactivity for class fields initialized with a Svelte rune

Nesting is exactly one level deep: members never contain members.

Member name is the source identifier, or a synthesized name where none exists: "constructor" (class constructor), "(construct)" and "(call)" (construct and call signatures on an interface or type alias), and index-signature names like "[key: string]" (the parameter name as written for interfaces; a synthesized key for type aliases).

ComponentPropJson
#

Component declarations have a props array of ComponentPropJson entries, in source order, followed by any props an external type declares that the $props() destructuring names, in destructuring order:

  • name, type: prop name and TypeScript type
  • typeInfo: structured TypeJson tree, when it adds structure
  • optional: whether the prop is optional
  • description: from JSDoc on the prop
  • defaultValue: default value as source text, if present
  • bindable: set when the prop is declared with $bindable(), so <Foo bind:value /> works
  • parameters: structured parameters for snippet-typed props (e.g., Snippet<[text: string]>)
  • examples, deprecatedMessage, seeAlso, throws, since, internalMessage, mutates: from the prop's own doc comment. On a prop, mutates declares what the component writes into the object or array it's handed

ParameterJson deliberately lacks those tag fields: a prop is a named slot with its own documentation, while a parameter's @example, @deprecated, and similar tags belong on the enclosing function per the TSDoc spec. Parameters get their docs from @param only.

ParameterJson
#

Functions, snippets, constructors, and snippet-typed props list ParameterJson entries in parameters:

  • name: parameter name (e.g., "options"; a rest parameter's dots live in rest, not the name)
  • type: resolved TypeScript type as a string
  • typeInfo: structured TypeJson tree, when it adds structure
  • optional: whether the parameter has a ? token
  • rest: whether the parameter uses rest syntax (...args)
  • description: from @param
  • defaultValue: default value expression from the source, if present
  • propertyDescriptions: sub-path to description, from dotted @param obj.prop tags on a named object parameter (destructured parameters aren't covered)

OverloadJson
#

Functions and constructors with multiple signatures list OverloadJson entries in overloads. Each carries only the fields that vary per signature:

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

Symbol-scope tags (@example, @deprecated, and the rest) live on the parent declaration only, read from the primary overload. On a non-primary overload they emit misplaced_tag and are dropped. See Symbol-scope vs signature-scope.

Reactivity
#

reactivity appears on VariableDeclarationJson and VariableMemberJson when the initializer is a value-producing Svelte rune call: $state, $state.raw, $derived, or $derived.by. Detection is syntactic and runs on every file, so a plain .ts file is treated the same as .svelte.ts or a component script.

Only variables and class fields are annotated, not parameters or destructured bindings. $props and $bindable surface on ComponentPropJson instead.

GenericParamJson
#

Declarations and members with type parameters list GenericParamJson entries in genericParams:

  • 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. Where a typeInfo or returnTypeInfo tree exists, flatten it with typeJsonToTokens to render per node: link name tokens to declarations, syntax-highlight code tokens, and print 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: '>'}]

The tokenizer decides spacing, separators, parentheses, and tuple labels; the renderer decides what each token looks like. typeJsonToText concatenates the same tokens into a plain string.

Compact JSON and absent-as-false
#

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 at their default. The value key is exempt, so a {kind: "literal", value: false} type node survives. Parsing with the Zod schemas from types.ts (or AnalyzeResultJson for the full envelope) restores every default, so the round-trip is lossless.

Raw-JSON consumers (jq, pipelines that skip .parse()) must treat absent as false: decl.optional === false fails because the key is gone. Use the schemas, or truthiness checks like if (decl.optional).

Examples
#

These show the compact wire form, so empty and default 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]>", "typeInfo": { "kind": "reference", "name": "Snippet", "typeArgs": [ { "kind": "tuple", "elements": [ {"name": "title", "type": {"kind": "intrinsic", "text": "string"}} ] } ] }, "optional": true, "description": "Custom header rendering.", "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 math.ts 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 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.

tags #

svelte-docinfo extracts TSDoc/JSDoc tags from source comments into structured fields on the output. This page lists every parsed tag, where its value lands, and which symbol receives it.

The set is the common tags of JSDoc and the TSDoc spec, plus the custom @nodocs and @mutates. Where the standards spell a tag differently, both spellings work (@default / @defaultValue, @return / @returns).

Inline tags like {@link} are kept in the extracted text for the consumer to render. TypeScript reprints {@link A|b} as {@link A |b}; @see keeps its source text exactly. Please open an issue if a tag you need is missing or off-spec.

Supported tags
#

TagWhere it lands
@paramParameterJson.description on the matching parameter. Dotted keys (obj.prop) land in propertyDescriptions. Keys matching no parameter emit unknown_param
@returnsreturnDescription on functions, function members, and per-overload OverloadJson. @return (JSDoc synonym) parses identically
@throwsthrows array on the parent declaration. A braced type (@throws {TypeError} - description, unions included) lands in type; the description may be empty or multiline. Without braces the first word is the type (@throws TypeError if …), so @throws if … records if as the type
@exampleexamples array on the parent declaration
@deprecateddeprecatedMessage on the parent declaration. An empty body still marks the symbol deprecated
@internalinternalMessage on the parent declaration, member, or component prop. A marker, not an exclusion (see @internal)
@seeseeAlso array, each entry in its source form (URLs, {@link}, module names)
@sincesince string on the parent declaration
@defaultdefaultValue on variable declarations and on members (for a function member, the behavior when the callback is omitted). Also fills ComponentPropJson.defaultValue when the prop has no destructuring default. Never on top-level functions or overloads. @defaultValue and @defaultvalue parse identically
@mutatesmutates record of target to description (see @mutates)
@nodocsExcludes the declaration from output and from the opt-in duplicate-name check (see @nodocs). Applies to declarations and export statements only: in a @module comment it warns and does nothing, and on a member or component prop it silently does nothing
@moduleMakes the comment ModuleJson.moduleComment instead of a declaration's doc

Symbol-scope vs signature-scope
#

Two tags vary per overload signature; the rest describe the symbol as a whole.

  • Signature-scope: @param and @returns. These flow to the matching overload's parameters[i].description / returnDescription. Each overload can carry its own.
  • Symbol-scope: @example, @deprecated, @internal, @since, @see, @throws, @mutates, @default, @nodocs. These describe the symbol as a whole and live on the parent declaration only.

A symbol-scope tag on a non-primary overload emits misplaced_tag and is dropped. Put these tags on the first overload signature: its JSDoc documents the symbol, while JSDoc on a later overload documents only that overload.

/** * @example double(2) // 4 * @deprecated use `scale` instead */ export function double(n: number): number; export function double(n: bigint): bigint; export function double(n: number | bigint): number | bigint { return (n as any) * 2; }

@param matching and unknown_param
#

@param keys are matched against parameter names, and the leading - separator is stripped from the description. A dotted key (obj.prop) documents a property of a named object parameter.

Dotted keys land in ParameterJson's propertyDescriptions, keyed by sub-path (obj.prop → prop, obj.a.b → a.b). The property path isn't checked against the parameter's type. Destructured parameters (fn({a, b}: T), which TypeScript names __0) can't be matched by name, so they aren't covered.

A key whose name (or dotted root) matches no parameter drops its description and emits unknown_param with the key, usually a typo or a stale doc after a rename.

@mutates
#

A non-standard tag for documenting mutations to parameters or external state: @mutates target - description. The target is everything before the first - (space-hyphen-space), so unlike @param it can be a hyphenated name or a phrase. Targets are not validated against the parameters, and backticks are stripped, so `options` and options are the same key:

  • a parameter name: @mutates options - sets defaults in place
  • a compound path: @mutates this.cache - inserts the result
  • a multi-word external reference: @mutates `app_settings` row - bumps the revision
  • a bare target with no description: @mutates `this`

The output is a Record<string, string> of target to description, empty for the bare form. Without a separator, the first line is the target and any following lines are the description.

On a component prop, @mutates declares that the component writes into the object or array it's handed, the same contract a function declares for a parameter: @mutates items - appends the selected entry. Reassigning a prop isn't a mutation in this sense; bindable covers that. The two combine: Svelte warns when a component mutates state the parent didn't bind, so an in-place write into a parent's $state usually sits on a $bindable prop.

@internal
#

@internal marks a symbol as not stable public API, per TSDoc. It lands as internalMessage on declarations and members of every kind, and on component props: an empty string for a bare tag, or the trailing prose (@internal used during development), kept separate from docComment.

It's a marker, not an exclusion: the declaration stays fully documented, so consumers can badge or filter it. To remove a declaration from output, use @nodocs; to omit whole modules, use exclude patterns.

@nodocs
#

@nodocs on a declaration removes it from the output entirely. It doesn't apply to members or component props, which stay in the output with no warning. It also affects:

  • Duplicate checking (opt-in, via onDuplicates): a hidden helper named parse can coexist with a public parse in another module without duplicate_declaration.
  • Re-exports: on a re-export statement, it drops the alsoExportedFrom link, any synthesized alias, and the module's reExports, starExports, or externalReExports / externalStarExports entry. The canonical declaration is unaffected.
  • Merged value+type pairs: a const Foo + type Foo pair is one symbol, so the tag on either excludes it.

@module
#

A comment tagged @module documents the file, not the next declaration. The text lands on ModuleJson.moduleComment and never in a declaration's docComment.

/** * Date math utilities. * * @module */ export function add_days(d: Date, n: number): Date { /* ... */ }

Svelte files have three module-comment sources, in priority order: a JSDoc @module in the instance <script>, one in <script module>, and the first HTML <!-- --> comment with @module at a line start. The highest-priority source wins, and more than one emits duplicate_comment (commentType: "module_comment"). Likewise, an HTML @component comment plus in-script component JSDoc emits it with commentType: "doc_comment", and the JSDoc wins.

Re-exports inherit comments selectively
#

A re-export with its own JSDoc synthesizes an alias in the re-exporting module, even when the name is unchanged. Its local doc fields win and the canonical's fill gaps. See Re-exports.

Tag-related diagnostics
#

Three diagnostic kinds report tag problems, all warnings:

  • misplaced_tag: a symbol-scope tag on a non-primary overload, or @nodocs in a @module comment
  • unknown_param: @param key with no matching parameter
  • duplicate_comment: more than one @module source in a Svelte file, or an HTML @component comment plus in-script component JSDoc

See diagnostics for details and tsdoc.ts for the parser.

diagnostics #

Analysis accumulates errors and warnings without halting. A failing declaration is marked partial: true and the rest of the module still analyzes. Detail lands in an array of Diagnostic entries, alongside modules in the result.

Two-tier error model
#

Accumulated (non-fatal): appended to the diagnostics array while analysis continues. Covers type resolution failures, member or prop extraction failures, and JSDoc tag misuse. The result is still valid, and declarations or members with incomplete data are marked partial: true.

Thrown (fatal): setup-level problems throw from the public entry points:

  • a missing or unloadable tsconfig.json: one that fails to parse, or an extends target that doesn't exist, like SvelteKit's generated tsconfig before svelte-kit sync has run
  • Svelte older than 5
  • invalid source options: empty sourcePaths, a path or absolute pattern outside projectRoot, or sourcePaths not under sourceRoot
  • conflicting options: discovery: 'exports' with include, or resolveImport with resolveDependencies: false
  • discovery: 'exports' when exports is missing or resolves to no files
  • a name collision under onDuplicates: 'throw'

Wrap analyze / analyzeFromFiles, or createAnalysisSession and query, to handle these. svelte2tsx failures don't throw; they surface as transform_failed.

The table below marks each kind's lifecycle. Ingest-time kinds surface when files are ingested and persist with the file until it is re-ingested or deleted, discovery-time comes from file discovery, and unmarked kinds are recomputed on every analysis pass. See the session guide for how they combine.

Shape
#

diagnostics is a plain Array<Diagnostic> beside modules in AnalyzeResultJson. It round-trips through JSON.stringify and z.array(Diagnostic).parse. Each entry carries:

  • kind: discriminant, one per failure mode (see table below)
  • severity: "error" or "warning"
  • file: POSIX-form and relative to the project root, with no leading ./; a file outside the project takes the ../ form. Join with projectRoot to get the absolute path. It names a file, not a module: ModuleJson.path is relative to sourceRoot, so don't look modules up by file.
  • line, column: 1-based, optional. Absent when there's no precise position (e.g., a module-level skip). Positions in Svelte files point at the original .svelte source; one that can't be mapped is omitted
  • message: human-readable description, with project-root paths made relative like file
  • fields specific to the kind: symbolName, className, tagName, etc.

Diagnostic kinds
#

Severity is fixed per kind: every kind is a warning except transform_failed and module_unreadable, which are always error.

kindWhen it fires
type_extraction_failedTrigger: type resolution threw on a declaration or member. Consequence: it's included with partial: true; typeSignature and typeInfo may be absent.
signature_analysis_failedTrigger: function or method signature analysis threw. Consequence: declaration included with partial: true; parameters and overloads may be empty.
class_member_failedTrigger: one member of a class couldn't be analyzed. Consequence: member included with partial: true; siblings still extract normally.
svelte_prop_failedTrigger: a Svelte component prop type couldn't be resolved. Consequence: that prop's type falls back to "any"; if the whole props type on $props() is unresolvable, props is empty. When the acceptsChildren check itself fails (propName: "children", no position), the type-based detection reports false, though template use of children can still set it.
legacy_propsTrigger: a component declares props with legacy export let syntax, which is still legal in Svelte 5 but isn't extracted. propNames lists them. Consequence: the component has empty props; migrate to $props().
module_skippedTrigger: a whole module was skipped during analysis. reason is "not_in_program" (the file wasn't in the TypeScript program), "no_analyzer" (unsupported extension), or "requires_program" (a Svelte file passed to analyzeModule directly). Consequence: module absent from modules[].
module_unreadableTrigger: a file named by package.json exports couldn't be read (permission denied, FS error). Consequence: the file is dropped from the discovered set. Discovery-time.
import_parse_failedTrigger: import scanning threw during dependency resolution (not expected: the parse tolerates malformed source). Consequence: the module's dependency edges are dropped; the module still analyzes. Ingest-time.
duplicate_commentTrigger: two sources supplied a comment for the same target: an HTML @component comment plus script JSDoc (commentType: "doc_comment"), or several @module comments ("module_comment"). Consequence: the higher-priority source wins: script JSDoc for doc comments; instance <script>, then <script module>, then HTML for module comments.
misplaced_tagTrigger: a symbol-scope tag (@example, @deprecated, @internal, @since, @see, @throws, @mutates, @default, @nodocs) on a non-primary overload signature, or @nodocs in a @module comment (then functionName is absent). Consequence: the tag is dropped. Move it to the primary signature; to omit a module, use exclude patterns.
unknown_paramTrigger: a @param key matches no parameter (a typo, or a stale doc after a rename). Consequence: the description is dropped.
alias_lostTrigger: TypeScript dropped the name of an exported type alias (aliasName) whose right-hand side is an indexed access or conditional, like z.infer<typeof S>, and svelte-docinfo can't recover it. Literal-only unions (z.enum) and brand intersections don't warn, since they read fine expanded. Consequence: unannotated positions document the alias's structure instead of its name. Where applicable, a nominal symbol (interface Foo extends z.infer<typeof S> {}) restores it.
duplicate_declarationTrigger: a declaration name appears in more than one module, and the opt-in onDuplicates check is on (for packages with a flat export namespace). declarationName and modules name the conflict; modules holds ModuleJson.path values, unlike file. Consequence: none beyond the diagnostic, unless onDuplicates is 'warn' (logs) or 'throw'.
transform_failedTrigger: svelte2tsx threw on a .svelte file. Consequence: the file's ModuleJson is a placeholder (partial: true, empty declarations). Ingest-time.
source_map_failedTrigger: the source map svelte2tsx produced for a .svelte file couldn't be parsed. Consequence: analysis continues without position mapping: other diagnostics for the file omit line/column, and declaration sourceLines point into the generated TypeScript. Ingest-time.
resolver_failedTrigger: the import resolver threw on specifier (as opposed to returning null for an external). Consequence: the dependency edge is deferred and retried when a later setFile / setFiles adds files or the importer changes; the diagnostic clears once it resolves. Ingest-time.

Use byKind to narrow to one variant with typed fields:

import {byKind} from 'svelte-docinfo'; for (const d of byKind(diagnostics, 'misplaced_tag')) { // d.tagName, d.functionName, d.file, d.line are typed console.warn(`${d.functionName}: move @${d.tagName} to the primary overload`); }

severity vs partial
#

severity says how loud to be about a problem. partial: true says a declaration or member has incomplete data, from type_extraction_failed, signature_analysis_failed, or class_member_failed, and a ModuleJson is partial when it's a transform_failed placeholder. svelte_prop_failed sets no flag; the prop's type falls back to any. Check partial directly instead of matching diagnostics by file and line.

Helpers
#

Read helpers for the diagnostics array:

Consuming diagnostics
#

The CLI includes diagnostics in its JSON output, omitted when empty (parse through AnalyzeResultJson to restore []). Progress messages go to stderr; structured diagnostics appear only in the JSON:

npx svelte-docinfo | jq '.diagnostics | group_by(.kind) | map({kind: .[0].kind, count: length})'

Programmatically:

import {analyzeFromFiles, errorsOf, formatDiagnostic, byKind} from 'svelte-docinfo'; const {modules, diagnostics} = await analyzeFromFiles({projectRoot: process.cwd()}); // File paths in diagnostics are already project-relative. for (const d of errorsOf(diagnostics)) { console.error(formatDiagnostic(d)); } // Specific check: any @param typos? const stale = byKind(diagnostics, 'unknown_param'); if (stale.length) { console.warn(`${stale.length} stale @param tag(s); fix or remove`); }

The Vite plugin's virtual module exports diagnostics too, so an app can render doc warnings without re-running analysis:

import {modules, diagnostics} from 'virtual:svelte-docinfo'; // the diagnostics subpath keeps the TypeScript compiler out of the client bundle import {hasErrors} from 'svelte-docinfo/diagnostics.js'; if (hasErrors(diagnostics)) { // surface in the UI or fail the build }

Absence rule
#

Absent optional fields (line, column) are omitted from JSON, per the compact output rules. The Vite plugin's modules and diagnostics exports are always present, even when empty. See diagnostics.ts for the Zod schemas and helpers.

api #

Browse the full api docs.