session.ts

Persistent analysis session — δ-shaped API over a ts.LanguageService.

Maps cleanly onto LSP and Vite/HMR consumers:

  • setFile / setFiles — additive ingest; transform-if-Svelte, lex specifiers, resolve imports (parallel), push content/virtual to the LS. Returns ingest-time diagnostics + a changed flag. Cache-hit no-op when content matches AND the mode-specific cache key matches (resolver identity for lex+resolve, dependency-snapshot equality for pre-resolved).
  • deleteFile — drop owned entry, evict from LS.
  • has / list — owned-set introspection (covers what consumers used to get from their own mirror caches).
  • query — sync analysis pass against the current owned set; returns analysis-pass diagnostics only (ingest diagnostics surface via the setFile/setFiles returns).
  • dispose — release LS resources.

The session owns a single Map<id, OwnedEntry> covering content, svelte virtuals, unfiltered deps, the mode-specific cache key (resolver identity or pre-resolved snapshot), and ingest-time diagnostics. svelte2tsx runs at most once per content change. Resolver work parallelizes across the batch in phase 2 of the three-phase setFiles pipeline; fully pre-resolved batches skip phase 2 (and the default-resolver construction) entirely. A batch that adds paths then retries the import specifiers earlier batches couldn't resolve, since the file that settles one is the dep rather than the importer (see AnalysisSession → Deferred resolutions).

@see analyze-core.ts for the two-phase analysis orchestrator @see dep-resolver.ts for the ImportResolver token contract

view source

Declarations
#

7 declarations

AnalysisSession
#

session.ts view source

also exported from index.ts

AnalysisSession import type {AnalysisSession} from 'svelte-docinfo/session.js';

Persistent analysis handle.

Concurrency: not safe across overlapping calls. Serialize externally (each caller awaits the previous setFile/setFiles before starting the next). The LS underneath is sync, but the resolver phase awaits I/O for async resolvers (Vite/Rollup), so the session does cross await boundaries.

Cache-hit semantics: per-entry, all-or-nothing. The implementation must not split the guarantee across separate caches (e.g. transform-cache hit + lex re-run). The match criterion is mode-discriminated:

  • lex+resolve mode: existing.content === incoming.content AND existing.resolverIdentity === incoming.resolverIdentity.
  • pre-resolved mode: existing.content === incoming.content AND arraysShallowEqual(existing.preResolvedDepsSnapshot, incoming.dependencies).

Mode flips (an entry previously ingested as lex+resolve now arrives with dependencies, or vice versa) always cache-miss.

Deferred resolutions: a cache hit reuses the entry's dependency edges, but an import specifier that resolved to nothing isn't a settled answer — its target may be ingested later, and the file that changes then is the *dep*, not the importer, so the importer would stay a cache hit with a missing edge. A setFiles that adds paths therefore retries the unresolved specifiers of already-owned entries and updates their edges in place, retiring any resolver_failed that has since resolved. This is the one way an entry changes without being re-ingested; it consumes no resolver work for files whose imports all resolved, and none at all for pre-resolved callers, whose edges are theirs to declare.

Promise resolution: setFile / setFiles resolve only after the serial LS push (phase 3) completes for every file in the batch. Awaiting the returned promise is sufficient — no separate flush step.

Owned ⊇ emitted: ingest is additive and ungated — any file can be pushed, and owned entries are served to the checker from memory before the disk fallback, so non-source files (unsaved buffers, virtual-only helpers, configs) can shape type resolution in the modules that import them. query() gates the *module set* through isSource: only owned files under sourceOptions.sourcePaths and not matching exclude emit a ModuleJson. The gate emits no diagnostics — query() logs the gated count as info, and list() reports the full owned set for introspection. By default the session completes the owned set itself: contextClosure ingests the in-root non-source dependency closure (e.g. internal/ modules public files import) so those files are version-tracked rather than pinned at their first disk read.

setFile

Ingest one file's content into the session. Idempotent on cache hit.

type (file: SourceFileInfo, opts?: SetFileOptions | undefined): Promise<SetFileResult>

file

opts?

optional
returns Promise<SetFileResult>

{changed, diagnostics}changed: false indicates a cache-hit no-op where the cached ingest diagnostics are returned.

setFiles

Ingest a batch of files. Additive — never removes; use deleteFile for removal. Cache hits are folded into the result with changed: false.

type (files: readonly SourceFileInfo[], opts?: SetFileOptions | undefined): Promise<SetFilesResult>

files

type readonly SourceFileInfo[]

opts?

optional
returns Promise<SetFilesResult>

deleteFile

Drop a file from the session and evict from the LS.

type (id: string): Promise<void>

id

type string
returns Promise<void>

has

Whether the given file ID is currently owned by the session.

type (id: string): boolean

id

type string
returns boolean

list

Snapshot of currently-owned file IDs (sort order is insertion order).

type (): readonly string[]

returns readonly string[]

query

Run a two-phase analysis pass against the current owned set, gated by isSource — owned files outside sourcePaths (or matching exclude) provide checker context but emit no module (see "Owned ⊇ emitted" above).

type (opts?: QueryOptions | undefined): { modules: { path: string; declarations: ({ kind: "function"; parameters: { name: string; type: string; optional: boolean; rest: boolean; typeInfo?: TypeJson | undefined; description?: string | undefined; defaultValue?: string | undefined; propertyDescriptions?: Record<...> | undefined; }[]; ... 19 more ...; sourceLine?: number | undefined; } | ... 7 more ... | { ...; })[]; ... 7 more ...; moduleComment?: string | undefined; }[]; diagnostics: ({ ...; } | ... 14 more ... | { ...; })[]; }

opts?

optional
returns { modules: { path: string; declarations: ({ kind: "function"; parameters: { name: string; type: string; optional: boolean; rest: boolean; typeInfo?: TypeJson | undefined; description?: string | undefined; defaultValue?: string | undefined; propertyDescriptions?: Record<...> | undefined; }[]; ... 19 more ...; sourceL...

analyzed modules and analysis-pass diagnostics. Ingest diagnostics from prior setFile/setFiles calls are NOT included here — concat with those returns for the full picture.

throws

  • Error - if `onDuplicates: 'throw'` and duplicates exist

allIngestDiagnostics

Concatenated ingest-time diagnostics across every owned entry — the cumulative view of every setFile/setFiles return, kept current as entries are added/replaced/deleted.

Lets long-lived consumers (Vite plugin, LSP) publish the full ingest picture without tracking per-batch returns themselves. Cheap: walks the owned map.

type (): ({ symbolName: string; file: string; message: string; severity: "error" | "warning"; kind: "type_extraction_failed"; line?: number | undefined; column?: number | undefined; } | { functionName: string; ... 5 more ...; column?: number | undefined; } | ... 13 more ... | { ...; })[]

returns ({ symbolName: string; file: string; message: string; severity: "error" | "warning"; kind: "type_extraction_failed"; line?: number | undefined; column?: number | undefined; } | { functionName: string; ... 5 more ...; column?: number | undefined; } | ... 13 more ... | { ...; })[]

getProgram

The LS-backed ts.Program, for incremental consumers doing their own checker work over analyzed declarations (e.g., a docgen provider converting ts.Types into its own structured model).

Freshness caveat: returns whatever the most recent ingest produced — the same reference as the prior call when no file version bumped, else a fresh program reusing unchanged ASTs via the document registry. A retained reference goes stale after any setFile / setFiles / deleteFile; re-call after mutating. Subject to the session's concurrency contract above; invalid after dispose().

type (): Program

returns Program

dispose

Release LS resources and clear the owned set. The session must not be used after disposal.

type (): void

returns void

AnalysisSessionOptions
#

session.ts view source

also exported from index.ts

AnalysisSessionOptions import type {AnalysisSessionOptions} from 'svelte-docinfo/session.js';

Options for createAnalysisSession.

documentRegistry flows through to the underlying LanguageService only. tsconfig and compilerOptions drive the LS's construction-time loadTsconfig parse — the session's only tsconfig parse. The lazy default ImportResolver reuses the LS's merged options via getCompilerOptions(), so module resolution and the checker see the same merge semantics — user-supplied compilerOptions override parsed tsconfig keys, but never bypass the tsconfig.json file requirement. The parse is a construction-time snapshot: a tsconfig.json edit mid-session is not picked up — create a new session.

projectRoot and virtualFiles from the LS options shape are excluded — the session derives projectRoot from sourceOptions and manages svelte2tsx virtuals internally per file.

inheritance

extends: Omit< AnalysisLanguageServiceOptions, 'projectRoot' | 'virtualFiles' >

sourceOptions

Module source options for path extraction and source filtering.

Must be a fully-constructed ModuleSourceOptions — the session re-runs normalizeSourceOptions (idempotent) but does not apply any defaults. Pass through createSourceOptions(projectRoot, overrides?) to merge with DEFAULT_SOURCE_OPTIONS. (The SourceOptionsOverrides ergonomic shape — exclude-callback form included — exists only on AnalyzeFromFilesOptions.sourceOptions and the Vite plugin, where the defaults merge happens inside createSourceOptions.)

type ModuleSourceOptions

resolveImport?

Session-default custom import resolver used when no per-call override is supplied — a bare ResolveImportFn or a token-paired ImportResolver (see ResolveImport). A bare function is normalized once at construction, so its synthesized identity is stable for the session's lifetime (cache reuse works). When omitted, the session lazily constructs the TS+tsconfig default on first use.

type ResolveImport

contextClosure?

Own the in-root non-source dependency closure as context files (default true).

After each ingest batch, the session reads from disk any file the batch's imports resolved to that is under projectRoot, fails isSource (outside sourcePaths or matching exclude — e.g. the src/lib/internal/ convention), has no node_modules/dot-directory segments, and has an analyzer type — transitively, until the closure converges. Context files are owned but never emit modules (query() gates them), so this changes no output; what it changes is *freshness*: an owned file's edit version-bumps the LS, whereas a disk-resolved file is read once and pinned for the session's lifetime. Watch-style consumers (the Vite plugin) gate their watchers on isSource(file) || session.has(file), so context-file edits trigger re-analysis and public output tracks internal types live.

Context batches always ingest in lex+resolve mode (their edges exist only to walk the closure — context files emit nothing), so a fully pre-resolved consumer whose files import in-root non-source paths constructs the default resolver after all. Context ingest diagnostics surface via allIngestDiagnostics(), not the batch return, and setFiles results stay keyed by the caller's input IDs. Unreadable candidates are skipped silently (the LS disk fallback covers them).

Context files are never evicted: once owned they stay owned (and, under the Vite plugin, watched) for the session's lifetime, even when no importer remains.

Set false only when the caller supplies every file the checker needs (the internal analyze() wrapper does): TS/JS context is covered by the LS disk fallback either way, but a gated .svelte dependency resolves only through the closure's ingest (svelte2tsx runs there — the disk fallback serves raw Svelte the checker can't parse), so analyzeFromFiles() keeps the closure on despite being one-shot.

type boolean

log?

Optional logger for session-level messages.

type AnalysisLog

createAnalysisSession
#

session.ts view source

also exported from index.ts

(options: AnalysisSessionOptions): AnalysisSession import {createAnalysisSession} from 'svelte-docinfo/session.js';

Create a persistent analysis session.

options

returns

AnalysisSession

examples

Vite plugin integration

const session = createAnalysisSession({sourceOptions, resolveImport, log}); await session.setFiles(initialFiles); const result = session.query(); // on watcher events: await session.setFile({id, content}); await session.deleteFile(removedId); const next = session.query(); // on shutdown: session.dispose();

One-shot via the public wrapper

// Equivalent to `analyze(...)` — the wrapper goes through a session internally. const session = createAnalysisSession({sourceOptions}); try { await session.setFiles(sourceFiles); return session.query({onDuplicates: 'throw'}); } finally { session.dispose(); }

QueryOptions
#

session.ts view source

also exported from index.ts

QueryOptions import type {QueryOptions} from 'svelte-docinfo/session.js';

Per-call input to query.

onDuplicates?

Behavior when duplicate declaration names are found across modules.

type OnDuplicates

log?

Per-call logger override (defaults to the session-level logger).

type AnalysisLog

SetFileOptions
#

session.ts view source

also exported from index.ts

SetFileOptions import type {SetFileOptions} from 'svelte-docinfo/session.js';

Options for a per-file or per-batch resolver override.

Identity is required (not optional) — silently coalescing missing identities to a function reference would destroy cache reuse when the same logical resolver is wrapped in fresh closures across calls.

resolveImport?

Per-call override of the session-default resolver — a bare ResolveImportFn or a token-paired ImportResolver (see ResolveImport).

A bare function is normalized with a fresh identity on each call, so the files touched by this call re-resolve rather than cache-hitting — the expected behavior for a deliberate one-off override. To reuse the resolve cache across calls, pass an ImportResolver with a stable identity.

type ResolveImport

SetFileResult
#

session.ts view source

also exported from index.ts

SetFileResult import type {SetFileResult} from 'svelte-docinfo/session.js';

Result of setFile (single-file ingest).

changed is true when content or the mode-specific cache key (resolver identity for lex+resolve; dependency snapshot for pre-resolved) differed from the cached entry — the owned entry was rewritten. An LS push accompanies the entry write only when the file is TS/JS or has a successful Svelte virtual; CSS/JSON and transform-failed Svelte rewrite the entry without touching the LS. false indicates a cache-hit no-op: the cached ingestDiagnostics are returned but no work ran.

changed

Whether content or the mode-specific cache key differed from the cached entry.

type boolean

diagnostics

Ingest-time diagnostics for this file (durable on the entry).

type ({ symbolName: string; file: string; message: string; severity: "error" | "warning"; kind: "type_extraction_failed"; line?: number | undefined; column?: number | undefined; } | { functionName: string; ... 5 more ...; column?: number | undefined; } | ... 13 more ... | { ...; })[]

SetFilesResult
#

session.ts view source

also exported from index.ts

SetFilesResult import type {SetFilesResult} from 'svelte-docinfo/session.js';

Result of setFiles (batch ingest).

Carries both aggregate views (changedIds, pre-flattened diagnostics) and a structured perFile map. HMR-style consumers want changedIds.size > 0 as the hot check; LSP-style consumers want per-file diagnostic association via perFile. Both are populated in the same single-pass walk over the batch — no extra cost.

changedIds

IDs whose content or mode-specific cache key differed from the cached entry — the subset of input file IDs that actually triggered work. Empty when every file was a cache-hit no-op.

type ReadonlySet<string>

diagnostics

Pre-flattened union of every file's ingestDiagnostics. Consumers can group by Diagnostic.file for per-file publish — already project-root-relative, normalized at rest before the entry was stored.

The array is fresh but its elements are the *stored* diagnostic objects, so mutating one (re-running normalizeDiagnosticPaths against a different root, say) corrupts the session's own state. Copy before mutating.

type ({ symbolName: string; file: string; message: string; severity: "error" | "warning"; kind: "type_extraction_failed"; line?: number | undefined; column?: number | undefined; } | { functionName: string; ... 5 more ...; column?: number | undefined; } | ... 13 more ... | { ...; })[]

perFile

Per-file SetFileResult keyed by input file ID. Use this when the grouping Diagnostic.file would do isn't enough — e.g., LSP wanting to publish empty-diagnostic-list updates for files that ingested cleanly.

type ReadonlyMap<string, SetFileResult>

Depends on
#

Imported by
#