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:

MethodPurpose
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 from setFile / setFiles and kept with the file until it is re-ingested or deleted.
  • Query-time (the rest): recomputed and returned by every query call.

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.