static analysis for TypeScript and Svelte 📜
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-docinfoUsage #
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@nodocsto exclude from docs,@internalkept as aninternalMessagemarker, and@mutatesto flag side effects - structured types:
typeInfotrees beside the flat type strings, with union members, type arguments, tuple elements, and alias names recovered where TypeScript drops them (z.inferand 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, markedmergedValuesince 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
@typeon$props()) - Svelte 5 reactivity runes: detects
$state,$state.raw,$derived, and$derived.byon variables and class fields via thereactivityfield, in any analyzed file - re-export tracking:
alsoExportedFromarrays with the forward view onModuleJson.reExports,aliasOffor renames, default-slot entries named"default",export * frompatterns, 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 #
| Flag | Description |
|---|---|
[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-dependencies | disable dependency resolution (dependencies/dependents stay
empty) |
--pretty | pretty-print JSON output (default: compact) |
-q, --quiet | suppress 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, --version | show 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.
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()], });Add TypeScript support in your
app.d.ts:/// <reference types="svelte-docinfo/virtual-svelte-docinfo.js" />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 exportsThe 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:
- configResolved: validates options, so bad configs (like
discovery: 'exports'withinclude, or a path outside the project root) fail at startup - 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. - buildStart: creates a session, discovers and ingests the source files, runs
query, and caches the result - resolveId / load: serves the cached result as
virtual:svelte-docinfo, exportingmodules,diagnostics, and a default{modules, diagnostics} - 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:
| Method | Purpose |
|---|---|
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 fromsetFile/setFilesand kept with the file until it is re-ingested or deleted. - Query-time (the rest): recomputed and returned by every
querycall.
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:
| Use | When |
|---|---|
| analyzeFromFiles | You own a project root and want everything in one call: file discovery, dependency resolution, analysis. CLI-style. |
| analyze | You already hold file contents in memory (a Rollup/esbuild plugin with files in the bundle graph). Single-pass, one-shot. |
| createAnalysisSession | You 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
dependenciesbut absent fromcontentare accepted. - Imports in
contentbut missing fromdependenciesare 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, inlinetypespecifiers, and type-only re-exports likeexport 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 modulemoduleComment: file-level JSDoc comment, if presentdependencies: paths of modules this file importsdependents: paths of modules that import this filestarExports:export * from './module'targetsreExports: same-name re-export edges ({name, module, typeOnly, sourceLine}), the forward view ofalsoExportedFrom(see Re-exports)externalReExports: direct re-exports from packages ({name, specifier, originalName?, typeOnly, sourceLine})externalStarExports:export * from 'pkg'specifiers as writtenpartial:truewhen the module is a placeholder for a.sveltefile that svelte2tsx failed to transform (seetransform_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": addsparameters,returnType,returnTypeInfo,returnDescription,overloads"variable": adds optionaldefaultValue(from@default) andreactivity(see Reactivity)"class": addsmembers,extends,implements,externalTypes"interface": addsmembers,extends,externalTypes,mergedValue"type": addsmembers,externalTypes,mergedValue"enum": addsmembers(enum values)"component": addsprops,externalTypes,acceptsChildren,lang(Svelte components)"snippet": addsparameters(exported Svelte template snippets)"namespace": addsmodule, the path of the module projected byexport * 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 carryname === "default"(see Re-exports)docComment: JSDoc comment texttypeSignature: 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 viareturnTypeInfo). See structured types belowsourceLine: line number in the source filemodifiers: e.g.,"readonly","static","getter"genericParams: type parameters with constraints and defaultsexamples,deprecatedMessage,seeAlso,throws,since: from standard JSDoc tagsinternalMessage: from@internal, a marker rather than an exclusion (empty string for a bare tag)mutates: from@mutates, aRecord<string, string>of target to description (empty for a target with no description). See tagsalsoExportedFrom: modules that re-export this declarationaliasOf: the canonical{module, name}of a renamed re-exportpartial:truewhen 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.tsfor 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
alsoExportedFromlists the modules that re-export it: one declaration, several import paths. The re-exporting module lists the same edges onModuleJson.reExports, withmodulenaming 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);sourceLineis the local export specifier's line. - Star exports:
export * from './x'lands onModuleJson.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. ItsaliasOf.modulenames a module absent from output, and noreExportsedge is emitted. - External re-exports: statements that directly name a package
(
export {x} from 'pkg',export * as ns from 'pkg',export * from 'pkg') land onexternalReExports/externalStarExportswith 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. Addsparameters,returnType,returnTypeInfo,returnDescription,overloads, and optionaldefaultValue(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. Addsparameters,overloads"variable": properties, accessors, and index signatures. Adds optionaldefaultValue(from@default),typeInfo, andreactivityfor 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 typetypeInfo: structured TypeJson tree, when it adds structureoptional: whether the prop is optionaldescription: from JSDoc on the propdefaultValue: default value as source text, if presentbindable: set when the prop is declared with$bindable(), so<Foo bind:value />worksparameters: 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,mutatesdeclares 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 inrest, not the name)type: resolved TypeScript type as a stringtypeInfo: structured TypeJson tree, when it adds structureoptional: whether the parameter has a?tokenrest: whether the parameter uses rest syntax (...args)description: from@paramdefaultValue: default value expression from the source, if presentpropertyDescriptions: sub-path to description, from dotted@param obj.proptags 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 stringparameters: this overload's parameters, with its own@paramdescriptionsreturnType: return type for this overload (functions only)returnTypeInfo: structured TypeJson tree for the return type, when it adds structuregenericParams: type parameters for this overloaddocComment: per-overload JSDoc text, if presentreturnDescription: from@returnson 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:extendsconstraint, if presentdefaultType: 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 #
| Tag | Where it lands |
|---|---|
@param | ParameterJson.description on the matching
parameter. Dotted keys (obj.prop) land in propertyDescriptions. Keys matching no parameter emit unknown_param |
@returns | returnDescription on functions, function members, and per-overload OverloadJson. @return (JSDoc synonym) parses
identically |
@throws | throws 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 |
@example | examples array on the parent declaration |
@deprecated | deprecatedMessage on the parent declaration. An empty body still marks
the symbol deprecated |
@internal | internalMessage on the parent declaration, member, or component prop. A
marker, not an exclusion (see @internal) |
@see | seeAlso array, each entry in its source form (URLs, {@link}, module names) |
@since | since string on the parent declaration |
@default | defaultValue 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 |
@mutates | mutates record of target to description (see @mutates) |
@nodocs | Excludes 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 |
@module | Makes 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:
@paramand@returns. These flow to the matching overload'sparameters[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 namedparsecan coexist with a publicparsein another module withoutduplicate_declaration. - Re-exports: on a re-export statement, it drops the
alsoExportedFromlink, any synthesized alias, and the module'sreExports,starExports, orexternalReExports/externalStarExportsentry. The canonical declaration is unaffected. - Merged value+type pairs: a
const Foo+type Foopair 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@nodocsin a@modulecommentunknown_param:@paramkey with no matching parameterduplicate_comment: more than one@modulesource in a Svelte file, or an HTML@componentcomment 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 anextendstarget that doesn't exist, like SvelteKit's generated tsconfig beforesvelte-kit synchas run - Svelte older than 5
- invalid source options: empty
sourcePaths, a path or absolute pattern outsideprojectRoot, orsourcePathsnot undersourceRoot - conflicting options:
discovery: 'exports'withinclude, orresolveImportwithresolveDependencies: false discovery: 'exports'whenexportsis 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 withprojectRootto get the absolute path. It names a file, not a module:ModuleJson.pathis relative tosourceRoot, so don't look modules up byfile.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.sveltesource; one that can't be mapped is omittedmessage: human-readable description, with project-root paths made relative likefile- 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.
| kind | When it fires |
|---|---|
type_extraction_failed | Trigger: type resolution threw on a declaration or member. Consequence: it's included with partial: true; typeSignature and typeInfo may be absent. |
signature_analysis_failed | Trigger: function or method signature analysis threw. Consequence: declaration included with partial: true;
parameters and overloads may be empty. |
class_member_failed | Trigger: one member of a class couldn't be analyzed. Consequence: member included with partial: true;
siblings still extract normally. |
svelte_prop_failed | Trigger: 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_props | Trigger: 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_skipped | Trigger: 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_unreadable | Trigger: 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_failed | Trigger: 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_comment | Trigger: 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_tag | Trigger: 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_param | Trigger: a @param key matches no parameter (a typo, or a
stale doc after a rename). Consequence: the description is dropped. |
alias_lost | Trigger: 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_declaration | Trigger: 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_failed | Trigger: svelte2tsx threw on a .svelte file. Consequence: the file's ModuleJson is a
placeholder (partial: true, empty declarations). Ingest-time. |
source_map_failed | Trigger: 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_failed | Trigger: 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:
- hasErrors, hasWarnings: boolean checks by severity
- errorsOf, warningsOf: filter by severity
- byKind: filter by kind, narrowed to the matching variant
- formatDiagnostic: format as
'./file.ts:10:5: error: message'(the'./'prefix is fixed)
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.