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.