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:

MethodPurpose
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 via setFile / setFiles returns and durable on the owned entry. Survive subsequent query calls until the entry is replaced or deleted.
  • Query-time (the rest): recomputed on every query call. Returned in AnalyzeResultJson.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.