build tools #

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

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

SourceFileInfo
#

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

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

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

Three entry points by ownership
#

Pick by what your tool already owns:

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

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

discoverSourceFiles (standalone)
#

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

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

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

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

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

Pre-resolved dependencies (fast path)
#

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

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

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

Trust contract
#

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

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

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

Type-only edges
#

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

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

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

ImportResolver (lex+resolve path)
#

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

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

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

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

Context files
#

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

Source options and the source root
#

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

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

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

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

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

Paths: POSIX-form contract
#

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

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

Concurrency caps
#

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