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:

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

The one-shot APIs are thin wrappers over single-use sessions. 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 dependencies but absent from content are accepted as-is.
  • Edges present in content but missing from dependencies are 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-lexer doesn't distinguish them). The output's ModuleJson.dependencies matches the syntactic import set.
  • Drop type-only edges: what Gro's filer (parse_imports with ignore_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_CONCURRENCY caps parallel readFile calls (used by files.globFiles, exports.discoverFromExports).
  • MAX_RESOLVE_CONCURRENCY caps 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).