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 svelte2tsx and the TypeScript compiler API to resolve types, track exports, and extract semantic details. The npm package has a Vite plugin, CLI, and programmatic API.
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, mainly the API reference, with fuz_ui components.
svelte-docinfo is largely inspired by sveld, but instead of AST-only inspection it uses the TypeScript compiler API for richer information, and also analyzes TypeScript modules. See the comparison below.
This is an early release. There are gaps to fill and design choices still open, but Svelte 5 coverage seems good and it's used in production websites. Please open issues for bugs, and discussions for everything else!
AI disclosure: the code and docs beyond the intro were written by Claude Code with uneven human guidance.
Install #
Published as svelte-docinfo to npm:
npm install svelte-docinfo -DUsage #
The tool's main function is outputting JSON, and there are several integration paths, in rough order 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 with HMR:
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 handles file discovery automatically:
import {analyzeFromFiles} from 'svelte-docinfo';
const {modules, diagnostics} = await analyzeFromFiles({
projectRoot: process.cwd(),
}); If your build tool already has file contents in memory, use analyze directly to skip file discovery. See the build tools guide for the full integration surface:
import {analyze, createSourceOptions} from 'svelte-docinfo';
const {modules} = await analyze({
sourceFiles: [{id: '/path/to/file.ts', content: '...'}],
sourceOptions: createSourceOptions('/project'),
}); For long-lived consumers (Vite plugin, LSP-style tools) that re-analyze the same source set
repeatedly, createAnalysisSession returns a persistent handle backed
by a TypeScript LanguageService. Parsed ASTs and svelte2tsx output are reused
across calls. The one-shot analyze and analyzeFromFiles are thin
wrappers over single-use sessions. See the session guide for the full incremental
API.
See the API reference for all exported functions and types.
Not supported #
A few constructs are silently skipped: standalone namespace Foo {} declarations (namespace re-exports are supported), decorators, and per-parameter
doc fields beyond @param descriptions. ParameterJson deliberately
doesn't carry @example/@deprecated/@since/@see/@throws. Per the TSDoc spec, those tags are scoped to the function symbol and
live on the parent declaration.
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 and@mutatesto flag side effects - Svelte 5 components: analyzes components via svelte2tsx, extracting prop types, defaults, bindability, snippet parameters, children detection, and exported template snippets
- Svelte 5 reactivity runes: detects
$state,$state.raw,$derived, and$derived.byon variables and class fields and exposes them via thereactivityfield. Detection is syntactic, so the same patterns can be captured in any analyzed file - re-export tracking:
alsoExportedFromarrays,aliasOffor renames, default-slot entries named"default", andexport * frompatterns - dependency graphs: tracks imports between modules and computes dependents
- function overloads: captures all public overload signatures with per-overload JSDoc
- build-tool agnostic: works with any source: file system, build pipeline, or in-memory
- diagnostic collection: accumulates warnings and errors without halting, so you can report problems in batch
Compared to sveld #
svelte-docinfo is largely inspired by sveld, 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 does not currently support Svelte 4 features like legacy slots, dispatched events, or the context API. Svelte 5 replaces most of these with snippets and callback props.
cli #
Run in any directory with TypeScript or Svelte source files. Prints JSON describing your 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 Discovery defaults to package.json exports, falling back to glob. -i forces explicit patterns; --discovery glob skips exports; --discovery exports is strict (fails when exports is missing).
--source-dir sets the source directory (default src/lib,
repeatable for monorepos) and seeds the implicit include glob. --source-root controls module-path stripping in the output (defaults to the
single --source-dir or their longest common prefix).
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; info, warnings, and errors go to stderr, so the terminal interleaves
them, but > and | capture clean JSON. -q/--quiet silences info; warnings and errors still print.
Options #
| Flag | Description |
|---|---|
[project-root] | project root directory (default: cwd) |
-i, --include <pattern> | include pattern (repeatable, replaces exports discovery) |
-e, --exclude <pattern> | exclude glob, applied at discovery and analysis (repeatable; fully replaces defaults,
so it does not merge with **/*.test.ts, **/*.spec.ts) |
-o, --output <file> | output file (default: stdout; pass - for explicit stdout, so -o "$OUT" works when $OUT=-) |
--discovery <mode> | auto | exports | glob (default: auto: exports first, glob fallback). exports is strict and fails
when package.json exports is missing. |
--dist-dir <dir> | dist directory for exports discovery (default: dist) |
--source-dir <dir> | source directory relative to project root (default: src/lib). Repeatable for monorepos; also seeds the implicit include glob. |
--source-root <dir> | source root for module-path stripping (default: single source-dir or longest common prefix) |
--on-duplicates <mode> | dispatch on duplicate declaration names: throw | warn (default: emit duplicate_declaration diagnostic, no dispatch) |
--only <pattern> | glob filter applied to module paths in output (repeatable). Full project is still analyzed (re-exports/dependents stay correct); diagnostics aren't filtered |
--no-resolve-dependencies | disable dependency resolution |
--pretty | pretty-print JSON output (default: compact) |
-q, --quiet | suppress info messages on stderr (warnings and errors still print) |
-V, --version | show version number |
Exit codes: 0 success, 1 analysis errors, 2 CLI errors.
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 mode it watches source
files and sends HMR updates as you edit.
Setup #
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 exportsBoth exports match the programmatic AnalyzeResultJson shape. See diagnostics for what flows through
diagnostics.
If TypeScript reports Cannot find module 'virtual:svelte-docinfo', ensure the /// <reference> line is in your app.d.ts.
Options #
All options are optional; the minimal call uses defaults (package.json exports discovery, glob fallback):
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).
include: ['src/**/*.ts', 'src/**/*.svelte'],
// Exclude globs. When provided, fully replaces the default
// ['**/*.test.ts', '**/*.spec.ts'] — re-include those patterns
// explicitly if you want them filtered.
exclude: ['**/*.test.ts', '**/*.spec.ts'],
// Discovery strategy: 'auto' | 'exports' | 'glob'. Default: 'auto'.
// 'auto' → exports first, glob fallback
// 'exports' → strict; throws if package.json exports is missing
// '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,
// Dispatch on duplicate declaration names across modules.
// 'throw' | 'warn' | (duplicates, log) => void.
// Default: undefined — the duplicate_declaration diagnostic still emits,
// but no extra dispatch fires. Set to 'throw' to fail fast on duplicates.
onDuplicates: undefined,
// Partial overrides for default source options (SvelteKit src/lib layout).
// Merged into createSourceOptions(projectRoot, sourceOptions).
sourceOptions: {sourcePaths: ['src/lib']},
// HMR debounce in ms. Default: 100.
hmrDebounceMs: 100,
}) The plugin runs the same pipeline as analyzeFromFiles internally: discover
via discoverSourceFiles, resolve dependencies, then analyze. sourceOptions is merged with defaults via createSourceOptions before discovery; hmrDebounceMs only affects the dev-mode watcher.
CLI vs Vite plugin #
The CLI calls analyzeFromFiles once, so use it for CI pipelines and one-off generation. The plugin owns a persistent createAnalysisSession, so HMR re-analyses reuse parsed TypeScript ASTs and svelte2tsx output across cycles. Use it when the analysis feeds the SvelteKit/Vite bundle. See the session guide if you're driving a session directly (custom bundler, LSP, etc.).
How it works #
The plugin hooks into four Vite lifecycle stages:
- configResolved: throws synchronously when
discovery: 'exports'is combined withinclude, so contradictory configs fail at startup rather than at first analysis - buildStart: discovers the source set, reads file contents, and runs createAnalysisSession's
analyze; caches the serialized JSON result - resolveId / load: serves the cached result as
virtual:svelte-docinfo, a JavaScript module exportingmodules,diagnostics, and a default{modules, diagnostics} - configureServer: watches source directories for changes, debounces re-analysis, and sends HMR updates only when the output actually changes. The session diffs incoming files by content equality, so unchanged files skip re-parsing entirely.
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. Layers fall out of the import
graph itself, not asserted in prose but observed in data. 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 nothing in the library depends on transitively: 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, then the vite plugin and cli entries.
Across 30 modules and 69 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
reads library.json (itself generated by library.gen.ts via analyze) 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. About 300 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) so parsed ASTs, svelte2tsx output, and the
dependency graph are reused across cycles. The one-shot analyze and analyzeFromFiles are thin wrappers over single-use sessions.
Construction #
import {createAnalysisSession, createSourceOptions} from 'svelte-docinfo';
const session = createAnalysisSession({
sourceOptions: createSourceOptions(process.cwd()),
// Optional: session-default ImportResolver. Lazily constructed
// (TS + tsconfig) on first use when omitted, unless every batch
// arrives fully pre-resolved.
// resolveImport: myResolver,
// Optional: logger for session-level messages.
// log: console,
}); AnalysisSessionOptions requires a fully-constructed ModuleSourceOptions. Use createSourceOptions to merge with DEFAULT_SOURCE_OPTIONS. The session re-runs normalizeSourceOptions (idempotent) but does not apply any further defaults.
tsconfig and compilerOptions flow to both the underlying LanguageService and the lazy default ImportResolver.
User-supplied compilerOptions merge over the parsed tsconfig per key, but the tsconfig
file is still required.
Lifecycle #
The session exposes a small incremental surface — add, update, remove — for owning the source set:
| Method | Purpose |
|---|---|
setFile(file, opts?) | Ingest one file's content. Idempotent on cache hit. Returns SetFileResult |
setFiles(files, opts?) | Ingest a batch. Additive, never removes. Returns SetFilesResult with both aggregate and per-file views |
deleteFile(id) | Drop a file from the session and evict it from the LS |
has(id) | Whether the given absolute path is currently owned |
list() | Snapshot of owned file IDs (insertion order) |
query(opts?) | Run a two-phase analysis pass against the current owned set. Returns AnalyzeResultJson |
allIngestDiagnostics() | Cumulative ingest-time diagnostics across every owned entry |
dispose() | Release LS resources and clear the owned set. Session must not be used after |
Concurrency. The session is not safe across overlapping calls, so serialize
externally. The LS underneath is sync, but the resolver phase awaits I/O for async resolvers
(Vite/Rollup), so setFile / setFiles cross await boundaries. query is sync and reads the current owned set; await any pending ingest first.
Cache-hit semantics #
Each owned entry is all-or-nothing per cache decision: either every cached artifact for that file is reused, or every artifact is recomputed. Whether a re-ingest cache-hits is mode-discriminated:
- lex+resolve (default): cache key is
(content, resolverIdentity). Match requires byte-for-byte content equality AND identity equality on the ImportResolver. - pre-resolved (caller supplies
SourceFileInfo.dependencies): cache key is(content, dependencies-element-wise-equal). A fresh array with identical contents cache-hits (so[...filer.deps.keys()]-per-call works); any length, element, or order difference cache-misses.
A mode flip (an entry previously ingested as lex+resolve now arrives with dependencies, or vice versa) always cache-misses and rewrites the entry.
On a cache hit, SetFileResult.changed is false and the cached ingest diagnostics
are returned with no work run. On a miss, the entry is rewritten. The LS push happens on miss for
TS/JS files and for Svelte files with a successful svelte2tsx virtual; CSS/JSON and transform-failed
Svelte rewrite the entry without touching the LS.
ImportResolver and identity #
ImportResolver is a token pair: {resolve, identity}. identity is a stable opaque token (string or
symbol) that keys the resolve cache alongside content.
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 (not optional). The naive alternative, keying on the function reference, would silently destroy cache reuse when callers wrap the resolver in a fresh closure per call (a common Vite/Rollup pattern). Opaque tokens lift the responsibility to the caller, where it can be done correctly: reuse the same string/symbol across calls if the resolution semantics haven't changed.
When neither AnalysisSessionOptions.resolveImport nor a per-call SetFileOptions.resolveImport is supplied, the session
lazily constructs a TS + tsconfig default with a fresh symbol identity on first use. That
laziness matters: if every file in every batch arrives fully pre-resolved (SourceFileInfo.dependencies populated), the default is never built and loadTsconfig is never called, saving a multi-second ts.createProgram on cold start.
Trust mode for pre-resolved dependencies #
When a caller supplies SourceFileInfo.dependencies, the session accepts the array
unconditionally and skips its own resolve pass. A buggy caller-side resolver skews ModuleJson.dependencies / dependents with no warning. The
lex+resolve fallback is always grounded in syntactic imports, so switch to it if you don't
control the dependency source. See build-tool integration for the full trust contract and type-only-edge
policy.
Diagnostics: ingest-time vs query-time #
Diagnostic kinds split into two categories with different lifecycles:
- Ingest-time (
transform_failed,source_map_failed,import_parse_failed,resolver_failed): surfaced viasetFile/setFilesreturns and durable on the owned entry. Survive subsequentquerycalls until the entry is replaced or deleted. - Query-time (the rest): recomputed on every
querycall. Returned inAnalyzeResultJson.diagnostics.
query() returns analysis-pass diagnostics only; it does NOT include ingest
diagnostics. Concat with prior setFile / setFiles returns for the
full picture, or call allIngestDiagnostics() for the cumulative ingest view across
every owned entry:
const queryResult = session.query();
const fullDiagnostics = [
...session.allIngestDiagnostics(),
...queryResult.diagnostics,
]; allIngestDiagnostics is the publish path for long-lived consumers. It lets the Vite
plugin republish the cumulative ingest picture on every HMR cycle without tracking per-batch returns,
and lets an LSP push a complete diagnostic set to the client on demand. Cheap: walks the owned map.
Discovery-time diagnostics (module_unreadable from discoverFromExports) are a third category. The session doesn't run discovery, so
direct consumers own those: track them in a side-channel field for HMR survival as the Vite
plugin does, or run discovery once and merge into the first query.
Worked example: incremental loop #
Sketch of an LSP-style edit/save/query loop. Each setFile updates a single file; query reanalyzes the whole owned set but reuses 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 / file-watcher consumers, setFiles on the batch of changed paths in one
call is preferable to looping setFile: it shares the ensureLexerReady warmup and runs the resolve phase in parallel with a bounded worker
pool.
When to use one-shot APIs instead #
If you only call analysis once (CLI, CI pipeline, one-off doc generation), use analyze or analyzeFromFiles directly. They create a single-use session, run it, and dispose. There's no caching benefit to keeping a session alive across one call. See session.ts for the full type definitions and diagnostics for the diagnostic kinds.
build tools #
svelte-docinfo is build-tool agnostic. Files come in through SourceFileInfo objects (absolute path plus content, optionally with pre-resolved dependencies), and the analyzer never touches disk on its own. This page covers the integration surface for embedding analysis inside a bundler, watcher, or LSP-style tool that doesn't fit the bundled Vite plugin.
SourceFileInfo #
Every analysis entry point, including analyze, createAnalysisSession's setFile / setFiles, and even discoverSourceFiles's output,
operates on SourceFileInfo:
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 paths are accepted at the public-API boundary and posixified at ingest (Windows backslash paths become forward-slash internally). After ingest, every comparison and output field uses POSIX form.content: required, since analysis functions don't read from disk. Files may come from any source: filesystem, build pipeline, in-memory editor buffer.dependencies: opt-in. When supplied, the session skips its lex+resolve pass for this entry and treats the array as authoritative. See "Pre-resolved dependencies" below.
Reverse edges (dependents) are computed inside the analyzer from forward edges,
never caller-supplied.
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. They share the same two-phase analysis loop, same diagnostic model, same output shape.
discoverSourceFiles (standalone) #
discoverSourceFiles runs file discovery without analysis. Useful when your tool wants to know the source set up front (for watcher-glob registration, count displays, pre-flight checks) but defers analysis to a separate pass.
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) Discovery strategies match the CLI and Vite plugin: 'auto' tries package.json
exports first and falls back to glob; 'exports' is strict (throws if exports is
missing); 'glob' skips exports entirely. Providing include under 'auto' collapses the chain to glob immediately, since
honoring it from exports discovery would silently drop the user's filter on packages with an exports field.
module_unreadable is the discovery-time diagnostic; the session doesn't run
discovery, so direct session consumers own it. analyzeFromFiles merges discovery diagnostics
into the final return before handing back to the caller.
Pre-resolved dependencies (fast path) #
Build tools that already maintain a dependency graph (Gro's filer, Rollup's bundle graph,
webpack's module graph) can hand it over by populating SourceFileInfo.dependencies with absolute paths. The session then skips its lex+resolve
pass for that entry entirely.
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); The cache key for the entry shifts to (content, dependencies-element-wise-equal) instead of (content, resolverIdentity). A fresh array per call cache-hits cleanly as long as
the contents match, with no upstream memoization needed. The session snapshots the array at
ingest, so mid-flight mutation of the caller's array won't produce false hits.
Trust contract #
The pre-resolved path is trust mode: the session does not cross-check declared dependencies
against the file's content. Two consequences a buggy resolver upstream will hit
silently:
- Edges declared in
dependenciesbut absent fromcontentare accepted as-is. - Edges present in
contentbut missing fromdependenciesare silently omitted.
There's no diagnostic for either case, because legitimate cross-batch sequences (declare-then-set, declare-then-delete) would produce noise. The lex+resolve fallback path has no such hole; its edges are always grounded in syntactic imports. If you don't fully trust the dependency source you're handing in, use lex+resolve and pay the parse cost.
Type-only edges #
Whether import type {X} from './x' shows up as a dependency is the caller's decision
under the pre-resolved path. Two common stances:
- Keep type-only edges: what default lex+resolve does (
es-module-lexerdoesn't distinguish them). The output'sModuleJson.dependenciesmatches the syntactic import set. - Drop type-only edges: what Gro's filer (
parse_importswithignore_types=true) does. Type imports aren't runtime deps; the output reflects runtime graph only.
Both are valid; the pre-resolved path defers the policy to the caller. Switching from
lex+resolve to pre-resolved-via-Gro will visibly remove type-only edges from ModuleJson.dependencies / dependents for affected modules.
ImportResolver (lex+resolve path) #
When you don't hand over pre-resolved dependencies, the session lexes specifiers from content and passes them through an ImportResolver. Build-tool integrations typically want to wire
their own resolver in so module resolution matches what the rest of the build does:
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 the stable cache token. See the session guide for why
it's required and how to choose one. When omitted entirely, the session lazily constructs a TS +
tsconfig default on first use, but only if at least one file in any batch lacks dependencies. Fully pre-resolved batches never trigger the default and skip the loadTsconfig call entirely.
Source options and the source root #
createSourceOptions builds the ModuleSourceOptions the session needs. Customize for non-default project layouts:
import {createSourceOptions} from 'svelte-docinfo';
// Default: single sourcePaths=['src/lib'], standard test/spec 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.
});
// Custom exclude (replaces defaults entirely — no merge).
const customOpts = createSourceOptions(process.cwd(), {
exclude: ['**/*.test.ts', '**/*.spec.ts', '**/fixtures/**'],
}); sourceRoot controls module-path stripping in ModuleJson.path: pass '.' for project-relative paths. exclude is the single source of truth, applied at both
discovery and analysis time, so a file dropped here never shows up in the output regardless of how
it was discovered.
Paths: POSIX-form contract #
Every path stored, compared, or used as a Map/Set key inside the analyzer is POSIX form
(forward slashes). Native paths are accepted at the public-API boundary and posixified at
ingest, so Windows callers can hand in C:\\repo\\src\\lib\\foo.ts without thinking about it, but the resulting ModuleJson.path, Diagnostic.file, and session list() output report POSIX form.
Out of scope: drive-letter case normalization (C:\\ vs c:\\) and
Windows extended-length \\\\?\\ prefixes. See paths.ts for the chokepoint implementation.
Concurrency caps #
Two bounded-concurrency caps protect against runaway parallelism in the integration paths:
MAX_FILE_CONCURRENCYcaps parallelreadFilecalls (used byfiles.globFiles,exports.discoverFromExports).MAX_RESOLVE_CONCURRENCYcaps parallel resolver calls (used by the session's phase-2 lex+resolve pass).
Same numerical value today, named separately for future independent tuning. Both run through a
fail-fast, order-preserving worker pool (map_concurrent in concurrency.ts).
output format #
svelte-docinfo outputs JSON describing your project's exported API. The data format is a hierarchy: modules contain declarations, and some declarations contain members or props.
Top-level structure #
Programmatic entry points (analyze, analyzeFromFiles) return both modules and accumulated diagnostics:
{
modules: ModuleJson[],
diagnostics: Diagnostic[]
} All surfaces emit this shape. The CLI's stdout JSON and the Vite plugin's virtual module
both expose modules and diagnostics (matching AnalyzeResultJson). The CLI runs output through compactReplacer so empty arrays strip on the wire (an empty-project
run emits {}); parse JSON consumers through AnalyzeResultJson to restore Zod defaults.
ModuleJson #
A ModuleJson describes a single source file and its exports:
path: file path relative to the source root (e.g.,"math.ts")declarations: exported items from this modulemoduleComment: file-level JSDoc comment, if presentdependencies: paths of modules this file importsdependents: paths of modules that import this filestarExports:export * from './module'patterns
Array fields (declarations, dependencies, etc.) are omitted from
JSON when empty and default to [] at runtime after parsing.
DeclarationJson #
Each declaration is a DeclarationJson, a discriminated union on
the kind field with nine variants:
"function": addsparameters,returnType,returnDescription,overloads"variable": adds optionaldefaultValue(from@default), plusreactivitywhen the initializer is a Svelte rune ($state,$state.raw,$derived,$derived.by)"class": addsmembers,extends,implements"interface": addsmembers,extends"type": addsmembers,intersects"enum": addsmembers(enum values)"component": addsprops,intersects,acceptsChildren,lang(Svelte components)"snippet": addsparameters(exported Svelte template snippets)"namespace": addsmodule(the source module path projected under this binding); synthesized forexport * as ns from './x'
Shared fields on all variants:
name,kind: identity. Default exports carryname === "default"(see Re-exports below)docComment: JSDoc comment texttypeSignature: full type as a stringsourceLine: 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 tagsmutates: from the non-standard@mutatestag, stored asRecord<string, string>mapping target keys to descriptions. Keys are typically parameter names but compound paths (this.foo) and external state references are accepted as-isalsoExportedFrom: modules that re-export this declarationaliasOf: original name if this is a renamed re-exportpartial:truewhen extraction failed partway through the declaration, indicating incomplete data
Declarations tagged with @nodocs are excluded from the output entirely and are also
excluded from duplicate name checking.
Re-exports #
Re-exports are encoded with two shapes, chosen by content:
- Same-name: the canonical declaration carries an
alsoExportedFromarray listing the modules that re-export it. One declaration, multiple import paths. - Renamed: a synthesized declaration appears in the re-exporting module
with
aliasOf: {module, name}pointing at the canonical. InheritstypeSignature,docComment,parameters,reactivity, anddefaultValuefrom the canonical;sourceLineis undefined. - Star exports:
export * from './x'patterns are tracked separately onModuleJson.starExportsand don't synthesize per-declaration entries.
When a re-export statement carries its own JSDoc or @nodocs, an alias is also synthesized in the re-exporting module so the local content has somewhere to
live, even when the name is unchanged. The trigger is "presence of local content," not
"presence of rename." Local doc-comment fields apply first and stick; canonical fields only
fill gaps. @nodocs on a re-export suppresses both the link and the synthesis.
Default-slot entries carry name === "default" (see the
shared-fields note above for why). Renames out of the default slot (export {default as Foo} from './x') carry name: "Foo" and aliasOf: {module, name: "default"}. Duplicate-name checks skip "default" since the default slot is module-scoped per the JS spec.
Namespace re-exports (export * as ns from './x') synthesize a NamespaceDeclarationJson with module pointing at the source the
namespace projects. Consumers render ns.a / ns.b by reading the
source module's declarations; namespaces don't inline members.
MemberJson #
Classes, interfaces, types, and enums can contain MemberJson entries
in their members arrays. MemberJson is a discriminated union on kind with three variants:
"function": methods and call signatures. Addsparameters,returnType,returnDescription,overloads"constructor": class constructors and construct signatures. Addsparameters,overloads"variable": properties, accessors, and index signatures. Adds optionaldefaultValue(from@default), plusreactivityfor class fields initialized with a Svelte rune
Member kind is restricted to these three variants. Nesting is exactly one level deep:
members never contain their own members.
Member name is the user-chosen identifier in most cases, but three synthesized
sentinels appear when no source identifier exists: "constructor" (class
constructor), "(construct)" (construct signature on an interface or type
alias), and "(call)" (call signature on an interface or type alias).
ComponentPropJson #
Component declarations have a props array of ComponentPropJson entries:
name,type: prop name and TypeScript typeoptional: whether the prop is optionaldescription: from JSDoc on the propdefaultValue: default value as a string, if presentbindable: set when the prop is declared with the$bindable()rune, so<Foo bind:value />is supported. Modeled here (not via the variable-levelreactivityfield) because$props/$bindableare component-prop concernsparameters: structured parameters for snippet-typed props (e.g.,Snippet<[text: string]>), absent for non-snippet propsexamples,deprecatedMessage,seeAlso,throws,since: symbol-scope JSDoc tags parsed from the prop's own doc comment (same shape as the declaration shared fields)
Asymmetry with ParameterJson. Props carry the
symbol-scope tag fields above; function parameters deliberately don't. A prop is a named
slot with its own documentation surface. A parameter is positional, and its @example/@deprecated/@since/@see/@throws belong on the enclosing function symbol per the TSDoc spec. Per-parameter content lives on ParameterJson.description from @param only.
ParameterJson #
Functions, snippets, constructors, and snippet-typed component props use ParameterJson entries in their parameters arrays:
name: parameter name (e.g.,"options","...args")type: resolved TypeScript type as a stringoptional: whether the parameter has a?tokenrest: whether the parameter uses rest syntax (...args)description: from@paramJSDocdefaultValue: default value expression from the source, if present
OverloadJson #
Functions and constructors with multiple signatures use OverloadJson entries in their overloads arrays. Each overload
captures only signature-scope content, the fields that can vary meaningfully per signature:
typeSignature: the full overload signature as a stringparameters: parameter list for this overload, with per-overload@paramdescriptionsreturnType: return type for this overload (functions only)genericParams: type parameters for this overloaddocComment: per-overload JSDoc text, if presentreturnDescription: from@returnson this overload
Symbol-scope JSDoc tags (@example, @deprecated, @since, @see, @throws, @mutates)
describe the function as a whole and live on the parent declaration only, not duplicated per
overload. The primary overload's JSDoc feeds the parent's symbol-level extraction; placing
one of those tags on a non-primary overload signature emits a misplaced_tag warning and the tag is dropped (no synthetic content, no silent loss). Typo'd or stale @param keys produce unknown_param warnings the same way.
Reactivity #
The reactivity field appears on VariableDeclarationJson and VariableMemberJson when the initializer is a value-producing Svelte
rune call: $state, $state.raw, $derived, or $derived.by. Detection is purely syntactic and runs on every analyzed file
regardless of extension, capturing the same patterns in a plain .ts file as in .svelte.ts or a component's <script>.
It covers variables (top-level and class fields). Function parameters and destructured
bindings are not annotated even when the value flows from a rune. $props and $bindable are component-prop concerns and surface on ComponentPropJson's bindable field instead.
GenericParamJson #
Declarations and members with type parameters use GenericParamJson entries in their genericParams arrays:
name: type parameter name (e.g.,"T")constraint:extendsconstraint, if presentdefaultType: default type, if present
Working with type strings #
Type signatures are opaque strings produced by the TypeScript compiler. To discover which in-project declaration names appear in a type string (e.g., for rendering clickable links), use findTypeReferences:
import {findTypeReferences} from 'svelte-docinfo';
const names = new Set(modules.flatMap(m => m.declarations.map(d => d.name)));
findTypeReferences('Map<string, ModuleJson[]>', names);
// => ['ModuleJson'] When scanning many type strings against the same set of names, pre-compile the patterns with buildTypeReferencePatterns to avoid recompiling regexes on every call.
Compact JSON and absent-as-false #
By default, output uses compact JSON via compactReplacer: empty
arrays, false booleans, and undefined fields are stripped, so optional, acceptsChildren, partial, rest, bindable, and similar fields vanish from the wire form when their value is the
default. After parsing with the Zod schemas from types.ts (or AnalyzeResultJson for the full {modules, diagnostics} envelope), all defaults are restored, and the round-trip
is lossless.
Raw-JSON consumers (e.g., jq, hand-rolled pipelines that skip .parse()) must treat absent as false; a literal decl.optional === false check silently fails because the key is gone. Use the
schemas, or truthy/falsy checks (if (decl.optional) …) on raw JSON.
Examples #
A TypeScript function:
{
"modules": [
{
"path": "math.ts",
"declarations": [
{
"name": "clamp",
"kind": "function",
"docComment": "Clamp a number to a range.",
"typeSignature": "(value: number, min: number, max: number): number",
"parameters": [
{"name": "value", "type": "number"},
{"name": "min", "type": "number"},
{"name": "max", "type": "number"}
],
"returnType": "number",
"sourceLine": 2
}
]
}
]
} A Svelte component with a snippet prop, children, and an exported snippet:
{
"modules": [
{
"path": "Card.svelte",
"declarations": [
{
"name": "Card",
"kind": "component",
"docComment": "A card with a customizable header.",
"acceptsChildren": true,
"props": [
{"name": "title", "type": "string"},
{
"name": "header",
"type": "Snippet<[title: string]>",
"optional": true,
"description": "Custom header rendering.",
"parameters": [
{"name": "title", "type": "string"}
]
}
],
"sourceLine": 1
},
{
"name": "card_footer",
"kind": "snippet",
"docComment": "Default footer snippet.",
"typeSignature": "Snippet<[text: string]>",
"parameters": [
{"name": "text", "type": "string"}
],
"sourceLine": 12
}
]
}
]
} A rune module exporting reactive state (e.g., a .svelte.ts file):
{
"modules": [
{
"path": "counter.svelte.ts",
"declarations": [
{
"name": "count",
"kind": "variable",
"typeSignature": "number",
"reactivity": "$state",
"sourceLine": 1
},
{
"name": "doubled",
"kind": "variable",
"typeSignature": "number",
"reactivity": "$derived",
"sourceLine": 2
}
]
}
]
} A function defined in math.ts and re-exported under a new name from the barrel index.ts. The canonical entry carries alsoExportedFrom if any module re-exports it under the same name. Renames
synthesize a separate declaration with aliasOf:
{
"modules": [
{
"path": "math.ts",
"declarations": [
{
"name": "clamp",
"kind": "function",
"typeSignature": "(value: number, min: number, max: number): number",
"sourceLine": 2
}
]
},
{
"path": "index.ts",
"declarations": [
{
"name": "clampNumber",
"kind": "function",
"typeSignature": "(value: number, min: number, max: number): number",
"aliasOf": {"module": "math.ts", "name": "clamp"}
}
],
"starExports": ["other.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 and surfaces them as structured fields on the output. This page lists every tag that is parsed, where its value lands, and the rules that decide which symbol receives it.
Supported tags #
| Tag | Where it lands |
|---|---|
@param | ParameterJson.description on the matching
parameter. Unmatched keys emit unknown_param |
@returns | returnDescription on functions, function members, and per-overload OverloadJson. @return is not accepted |
@throws | throws array on the parent declaration. {Type} hints are extracted
as the leading error type |
@example | examples array on the parent declaration |
@deprecated | deprecatedMessage on the parent declaration. Empty body still marks the symbol
deprecated |
@see | seeAlso array. Plain URLs, {@link} syntax, and module names
all preserved in their source form |
@since | since string on the parent declaration |
@default | defaultValue on variable declarations and variable members; falls back
for ComponentPropJson.defaultValue when no destructuring
default is present |
@mutates | mutates record. Non-standard; same format as @param: @mutates key - description |
@nodocs | Excludes the declaration from output entirely; also excludes it from flat-namespace duplicate checking |
@module | Promotes the comment to ModuleJson.moduleComment instead of attaching to a declaration |
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,@since,@see,@throws,@mutates,@default,@nodocs. These describe the symbol as a whole and live on the parent declaration only.
Placing a symbol-scope tag on a non-primary overload emits misplaced_tag and the tag is dropped, with no synthetic content and no silent
loss. Move it to the primary signature (typically the first overload, or the implementation
signature's JSDoc which feeds the symbol-level extraction). See diagnostics for the diagnostic details.
/**
* @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 actual parameter names. The leading - separator is stripped (TypeScript's parser keeps it as syntax, not content). Destructured
parameters match the outer binding name, not inner properties.
When a key doesn't match any parameter (a typo, a stale doc after a rename, or a description
of a destructured property), the description is dropped and an unknown_param diagnostic fires with the orphaned key. Fix the JSDoc rather than relying
on the silent fallback.
@mutates #
Non-standard tag for documenting mutations to parameters or external state. Same key - description format as @param, but keys are not validated against the parameter list. Anything goes:
- a parameter name:
@mutates options - sets defaults in place - a compound path:
@mutates this.cache - inserts the result - an external state reference:
@mutates global_registry - registers the handler
The output is a Record<string, string> mapping each key to its description.
Consumers decide how to render or group by key shape.
@nodocs #
@nodocs on a declaration removes it from the analysis output entirely. Two follow-on
effects:
- Flat-namespace duplicate checking skips it: a hidden helper named
parsecan coexist with a publicparsein another module without triggeringduplicate_declaration. - Re-export synthesis is suppressed:
@nodocson a re-export statement drops both thealsoExportedFromlink and any synthesized alias declaration. The canonical entry stays untouched.
@module #
A comment tagged @module attaches to the file rather than to the next
declaration. The text lands on ModuleJson.moduleComment and is suppressed from
any declaration docComment that might otherwise capture it.
/**
* Date math utilities.
*
* @module
*/
export function add_days(d: Date, n: number): Date { /* ... */ } Svelte files have a second module-comment source: an HTML comment directly above <script>. When both an HTML and a JSDoc @module comment
supply a value for the same target, duplicate_comment fires with commentType: "module_comment". The same diagnostic covers declaration-level
collisions (commentType: "doc_comment").
Re-exports inherit comments selectively #
A re-export that carries its own JSDoc synthesizes an alias in the re-exporting module so the local content has somewhere to live, even when the name is unchanged. See Re-exports for the full encoding rules and merge order.
Tag-related diagnostics #
Three diagnostic kinds surface tag-handling problems:
misplaced_tag: symbol-scope tag on a non-primary overload signatureunknown_param:@paramkey with no matching parameterduplicate_comment: two sources supplied a comment for the same target (HTML + JSDoc@module, or any other collision)
All three are warnings; analysis still completes and the declaration is included. See tsdoc.ts for the parser internals and diagnostics for the full diagnostic schema.
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 Diagnostic array, analysis continues. Covers type resolution failures,
member or prop extraction failures, and JSDoc tag misuse. The return value is still valid but
may carry partial: true on affected declarations.
Thrown (fatal): a small set of setup-level conditions throws from public
entry points: missing tsconfig.json, Svelte <5 detected, or discovery: 'exports' mode with no resolvable exports. Wrap the top-level analyze / analyzeFromFiles call if you want to handle these.
svelte2tsx transformation failures are not thrown; they flow as transform_failed diagnostics.
Shape #
diagnostics is a plain Array<Diagnostic>, no wrapper, no methods.
Round-trips through JSON.stringify / z.array(Diagnostic).parse, so
it serializes alongside modules in CLI output and rehydrates cleanly.
{
modules: ModuleJson[],
diagnostics: Diagnostic[]
} Each Diagnostic carries:
kind: discriminant, one per failure mode (see table below)severity:"error"or"warning"file: POSIX-form, project-relative (no leading./). Rejoin withprojectRootfor absolute paths.line,column: 1-based, optional. Absent when there's no precise AST node (e.g., a module-level skip)message: human-readable description- additional fields specific to the variant:
symbolName,className,tagName, etc.
Diagnostic kinds #
Severity is stable per kind: every kind below is warning severity except transform_failed and module_unreadable, which are always error.
| kind | When it fires |
|---|---|
type_extraction_failed | Trigger: type resolution threw on a symbol. Consequence: declaration included with partial: true and
empty typeSignature. |
signature_analysis_failed | Trigger: function or method signature analysis threw, usually
circular generics or unresolved call signatures. 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 through
the checker. Consequence: per-prop type resolution failures fall back
to "any" for that prop with siblings unaffected; when the whole $props<T>() annotation type is itself unresolvable, the component's props array drops to empty. |
module_skipped | Trigger: whole module skipped during the analysis pass. reason narrows to "not_in_program", "no_analyzer", or "requires_program". Consequence: module absent from modules[]. |
module_unreadable | Trigger: file named in package.json exports exists but readFile failed (permission denied, FS error). Consequence: file dropped from the discovered set. Discovery-time. |
import_parse_failed | Trigger: import parsing failed during dependency resolution. Consequence: the dependency edge is dropped; the module itself still analyzes. Ingest-time. |
duplicate_comment | Trigger: two sources supplied a comment for the same target (HTML @component + script JSDoc, or multiple @module comments). commentType narrows to "module_comment" or "doc_comment". Consequence: the higher-priority source
wins: JSDoc for doc comments; instance <script> > <script module> > HTML comment for module comments. |
misplaced_tag | Trigger: symbol-scope tag (@example, @deprecated, @since, @see, @throws, @mutates, @default, @nodocs) found on a non-primary overload signature. Consequence: the tag is dropped; move it to the primary signature to keep
it. |
unknown_param | Trigger: @param key didn't match any actual parameter
(typo or stale doc after a rename). Consequence: the description is dropped. |
duplicate_declaration | Trigger: a declaration name appears in more than one module, so the
flat-namespace assumption collides. declarationName and modules name the conflict. Consequence: always emitted; onDuplicates only controls whether to additionally throw, log, or invoke a
callback. |
transform_failed | Trigger: svelte2tsx threw on a .svelte file. Consequence: the file's ModuleJson is synthesized as a placeholder (partial: true, empty declarations). Ingest-time. |
source_map_failed | Trigger: source map parsing failed for a Svelte virtual file. Consequence: analysis continues using virtual positions, so
downstream line/column may point into the svelte2tsx output
rather than the original .svelte source. Ingest-time. |
resolver_failed | Trigger: import resolver threw on a specifier (vs. legitimately
returning null for externals). specifier names the failing
import. Consequence: the dependency edge is dropped. Ingest-time. |
Each variant is a strict Zod object with its own extra fields. Use byKind to narrow to a specific variant for typed access:
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 specific declaration or member has incomplete data,
typically from type_extraction_failed, signature_analysis_failed, class_member_failed, or svelte_prop_failed. Branch on partial directly; no need to cross-reference diagnostics by file and line.
Helpers #
The diagnostics array is a plain Array<Diagnostic>: construct with [] and mutate with Array.push. Read helpers:
- 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 always emits the structured diagnostics field alongside modules in JSON output, and also prints warnings and errors to stderr:
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 both modules and diagnostics, so SvelteKit apps can render a
doc-warnings page without re-running analysis:
import {modules, diagnostics} from 'virtual:svelte-docinfo';
import {hasErrors} from 'svelte-docinfo';
if (hasErrors(diagnostics)) {
// surface in the UI or fail the build
}Absence rule #
Optional scalar fields (line, column) drop on serialize per the
same compact-output rules as the rest of the schema. See output format for
the full rule. The Vite plugin's virtual module exposes modules and diagnostics as separate ES module exports, so they're always present even when
empty. See diagnostics.ts for the Zod schemas
and helper signatures.
api #
Browse the full api docs.