/**
* Import resolver primitives for the analysis session.
*
* Exposes the `ImportResolver` token contract (`{resolve, identity}`) plus
* building blocks the session uses inside its three-phase setFiles pipeline:
*
* - `ensureLexerReady` — one-time wasm init for `es-module-lexer`. Phase 1 is
* sync per-file; the session awaits this once before the loop starts.
* - `lexImports` — sync specifier extraction from already-prepared content
* (raw TS/JS, or svelte2tsx virtual content for `.svelte` files).
* - `createDefaultResolver` — the TS + tsconfig fallback used when neither
* `AnalysisSessionOptions.resolveImport` nor a per-call override is provided.
* Carries a fresh symbol identity per session (cache-key-safe).
* - `normalizeResolveImport` — coerces the public `ResolveImport` union (bare
* function or token-paired resolver) to an `ImportResolver` at each boundary.
* - `wrapResolveImport` — the fn→token primitive behind `normalizeResolveImport`:
* wraps a bare function in an `ImportResolver`, synthesizing a throwaway
* identity unless a stable one is passed.
*
* @internal — subpath-importable for power users who want to drive resolution
* outside the session, but not part of the stable barrel surface.
*
* @see `session.ts` for the three-phase ingestion pipeline that consumes these
* @see `loadTsconfig` in `typescript-program.ts` for the TS module-resolution
* compiler-options shortcut used by the default resolver
*
* @module
*/
import {isBuiltin} from 'node:module';
import ts from 'typescript';
import {init as esModuleLexerInit, parse as esModuleLexerParse} from 'es-module-lexer';
/**
* Bare import-resolver function — the convenience form.
*
* Resolve an import specifier to an absolute file path, or `null` if the
* specifier is unresolvable (external package, missing file, etc.). May return
* synchronously or asynchronously; sync returns are awaited harmlessly in the
* session's parallel resolve phase.
*
* @param specifier - import specifier from source code
* @param fromFile - absolute path of the importing file
*/
export type ResolveImportFn = (
specifier: string,
fromFile: string,
) => string | null | Promise<string | null>;
/**
* Token-paired import resolver.
*
* `identity` is a stable opaque token that keys the session's resolve cache
* alongside content. Cache hits require both (a) byte-for-byte identical
* source content and (b) identity equality. Naive function-reference keys
* would silently destroy cache reuse when callers wrap the resolver in fresh
* closures (a very common pattern in Vite/Rollup plugins) — opaque tokens
* lift the responsibility to the caller, where it can be done correctly.
*/
export interface ImportResolver {
/** Resolve an import specifier to an absolute file path, or `null`. */
resolve: ResolveImportFn;
/**
* Stable opaque token identifying this resolver's cache scope.
*
* Two `ImportResolver`s with `identity` `===` to each other are treated
* as cache-equivalent. `string` for human-readable identities (e.g.,
* `'vite-plugin-container'`); `symbol` for generated sentinels.
*/
identity: string | symbol;
}
/**
* Public resolver shape accepted across the API — a bare `ResolveImportFn` or
* the token-paired `ImportResolver`.
*
* Every entry point that accepts `resolveImport` (`analyze`, `analyzeFromFiles`,
* `createAnalysisSession`, and the session's `setFile`/`setFiles` per-call
* override) takes this union, so the same value copy-pastes between them. Pass:
*
* - a **bare function** for the common case — a fresh cache identity is
* synthesized at the boundary. Correct for one-shot use (`analyze`,
* `analyzeFromFiles`) and for a session default (wrapped once at
* construction, so identity is stable for the session's lifetime).
* - an **`ImportResolver`** with a stable `identity` when you need the session
* to reuse its resolve cache across calls where the same logical resolver is
* rebuilt as a fresh closure (Vite/Rollup plugins). A bare function handed to
* a *per-call* `setFile`/`setFiles` override is treated as a distinct
* resolver each call (fresh identity → touched files re-resolve), which is
* the expected behavior for a deliberate one-off override.
*/
export type ResolveImport = ResolveImportFn | ImportResolver;
/**
* Normalize the public `ResolveImport` union to an `ImportResolver`.
*
* Wraps a bare function via `wrapResolveImport` (fresh synthesized identity);
* passes a token-paired resolver through unchanged. `undefined` in, `undefined`
* out — callers fall back to the session default. Call once per logical
* resolver scope: at session construction for the default, per call for an
* override.
*/
export const normalizeResolveImport = (
value: ResolveImport | undefined,
): ImportResolver | undefined =>
value === undefined ? undefined : typeof value === 'function' ? wrapResolveImport(value) : value;
/**
* Ensure `es-module-lexer`'s wasm runtime is initialized.
*
* Idempotent and cheap after the first call. The session awaits this once at
* the top of `setFiles` so phase 1's per-file lex is purely synchronous.
*/
export const ensureLexerReady = async (): Promise<void> => {
await esModuleLexerInit;
};
/**
* Lex import specifiers from prepared content.
*
* Sync — caller must have awaited `ensureLexerReady` at least once before
* invoking. For `.svelte` files, pass the svelte2tsx-transformed virtual
* content, not the raw `.svelte` source (which isn't lex-able as JS/TS).
*
* Dynamic imports (`import(specifier)` with non-literal arg) are omitted.
*
* @throws Error if lexing fails (malformed source). Callers should catch and
* emit `import_parse_failed`.
*
* @param content - prepared file content (TS/JS, or svelte2tsx virtual)
* @param fileId - absolute file path (used for error messages)
* @returns specifiers in declaration order
*/
export const lexImports = (content: string, fileId: string): Array<string> => {
const [imports] = esModuleLexerParse(content, fileId);
const specifiers: Array<string> = [];
for (const imp of imports) {
if (imp.n) specifiers.push(imp.n);
}
return specifiers;
};
/**
* Whether a lexed specifier names a Node builtin (`fs`, `node:fs/promises`, …).
*
* Builtins can never be a project source file, so resolving them is pointless —
* and routing them through a host resolver (Vite/Rollup) makes that host emit
* "externalized for browser compatibility" warnings for browser-targeted
* configs. Callers skip resolution for these and treat them as unresolved
* (`null`), which the downstream `isSource` filter would do anyway.
*/
export const isNodeBuiltin = (specifier: string): boolean => isBuiltin(specifier);
/**
* Create the default `ImportResolver` (TypeScript + tsconfig).
*
* Uses `ts.resolveModuleName` against `ts.sys` directly — no `ts.Program` is
* built. Identity is a fresh symbol per call, so each session that constructs
* its own default gets a unique cache scope. Multiple sessions sharing one
* resolver instance share the cache scope (correct, since resolver state is
* shared too).
*
* Falls back to appending `.svelte` when the bare specifier doesn't resolve —
* lets `.svelte` imports work without polluting the consumer's tsconfig.
*
* @param compilerOptions - parsed tsconfig (from `loadTsconfig`)
* @param projectRoot - absolute project root for the module-resolution cache
*/
export const createDefaultResolver = (
compilerOptions: ts.CompilerOptions,
projectRoot: string,
): ImportResolver => {
const cache = ts.createModuleResolutionCache(
projectRoot,
ts.sys.useCaseSensitiveFileNames ? (f) => f : (f) => f.toLowerCase(),
compilerOptions,
);
const resolve = (specifier: string, fromFile: string): string | null => {
const result = ts.resolveModuleName(specifier, fromFile, compilerOptions, ts.sys, cache);
if (result.resolvedModule) return result.resolvedModule.resolvedFileName;
// Fallback: try with .svelte extension. ts.resolveModuleName doesn't
// know about .svelte files unless explicitly configured.
if (!specifier.endsWith('.svelte')) {
const svelte = ts.resolveModuleName(
specifier + '.svelte',
fromFile,
compilerOptions,
ts.sys,
cache,
);
if (svelte.resolvedModule) return svelte.resolvedModule.resolvedFileName;
}
return null;
};
return {resolve, identity: Symbol('ts-default')};
};
/**
* Wrap a bare `resolveImport` function into an `ImportResolver` token.
*
* `identity` is optional; when omitted, a fresh `Symbol('wrapped')` is
* synthesized per call. The synthesized default suits single-use scopes —
* `normalizeResolveImport` relies on it to wrap a bare function for a one-shot
* `analyze` call or for a session default (wrapped once at construction, so the
* identity stays stable for the session's lifetime).
*
* For a long-lived session that re-wraps the *same* logical resolver across
* calls (Vite plugin, LSP), a throwaway identity cache-misses every time — the
* fresh `Symbol('wrapped')` never compares equal. Pass a stable `identity`
* here, or — simpler — construct an `ImportResolver` (`{resolve, identity}`)
* directly and pass it through the `ResolveImport` union.
*/
export const wrapResolveImport = (
resolveImport: ResolveImportFn,
identity: string | symbol = Symbol('wrapped'),
): ImportResolver => ({
resolve: resolveImport,
identity,
});
/**
* Shared no-op `ImportResolver` for the "dependency resolution disabled" case.
*
* Stable string identity (`'no-deps'`) instead of a per-call symbol so that
* repeated ingests within a long-lived session cache-hit on identity. Each
* session still owns its own cache (entries live on the per-session `owned`
* map; sessions don't share state), but within one session every reference
* to `noDepsResolver` is `===` to every other — so a Vite re-save of
* byte-identical content with `resolveDependencies: false` exercises the
* cache-hit branch. The resolver always returns `null`, so any two calls
* with identical content under this identity produce identical resolution
* results — `string` identity correctly captures that. Used by both the
* one-shot `analyzeFromFiles` path and the long-lived Vite plugin
* (`resolveDependencies: false`).
*/
export const noDepsResolver: ImportResolver = {
resolve: () => null,
identity: 'no-deps',
};
{
"path": "dep-resolver.ts",
"declarations": [
{
"name": "ResolveImportFn",
"kind": "type",
"docComment": "Bare import-resolver function — the convenience form.\n\nResolve an import specifier to an absolute file path, or `null` if the\nspecifier is unresolvable (external package, missing file, etc.). May return\nsynchronously or asynchronously; sync returns are awaited harmlessly in the\nsession's parallel resolve phase.",
"typeSignature": "ResolveImportFn",
"sourceLine": 45,
"alsoExportedFrom": [
"index.ts"
],
"members": [
{
"name": "(call)",
"kind": "function",
"typeSignature": "(specifier: string, fromFile: string): string | Promise<string | null> | null",
"parameters": [
{
"name": "specifier",
"type": "string"
},
{
"name": "fromFile",
"type": "string"
}
],
"returnType": "string | Promise<string | null> | null"
}
]
},
{
"name": "ImportResolver",
"kind": "interface",
"docComment": "Token-paired import resolver.\n\n`identity` is a stable opaque token that keys the session's resolve cache\nalongside content. Cache hits require both (a) byte-for-byte identical\nsource content and (b) identity equality. Naive function-reference keys\nwould silently destroy cache reuse when callers wrap the resolver in fresh\nclosures (a very common pattern in Vite/Rollup plugins) — opaque tokens\nlift the responsibility to the caller, where it can be done correctly.",
"typeSignature": "ImportResolver",
"sourceLine": 60,
"alsoExportedFrom": [
"index.ts"
],
"members": [
{
"name": "resolve",
"kind": "variable",
"docComment": "Resolve an import specifier to an absolute file path, or `null`.",
"typeSignature": "ResolveImportFn"
},
{
"name": "identity",
"kind": "variable",
"docComment": "Stable opaque token identifying this resolver's cache scope.\n\nTwo `ImportResolver`s with `identity` `===` to each other are treated\nas cache-equivalent. `string` for human-readable identities (e.g.,\n`'vite-plugin-container'`); `symbol` for generated sentinels.",
"typeSignature": "string | symbol"
}
]
},
{
"name": "ResolveImport",
"kind": "type",
"docComment": "Public resolver shape accepted across the API — a bare `ResolveImportFn` or\nthe token-paired `ImportResolver`.\n\nEvery entry point that accepts `resolveImport` (`analyze`, `analyzeFromFiles`,\n`createAnalysisSession`, and the session's `setFile`/`setFiles` per-call\noverride) takes this union, so the same value copy-pastes between them. Pass:\n\n- a **bare function** for the common case — a fresh cache identity is\n synthesized at the boundary. Correct for one-shot use (`analyze`,\n `analyzeFromFiles`) and for a session default (wrapped once at\n construction, so identity is stable for the session's lifetime).\n- an **`ImportResolver`** with a stable `identity` when you need the session\n to reuse its resolve cache across calls where the same logical resolver is\n rebuilt as a fresh closure (Vite/Rollup plugins). A bare function handed to\n a *per-call* `setFile`/`setFiles` override is treated as a distinct\n resolver each call (fresh identity → touched files re-resolve), which is\n the expected behavior for a deliberate one-off override.",
"typeSignature": "ResolveImport",
"sourceLine": 92,
"alsoExportedFrom": [
"index.ts"
]
},
{
"name": "normalizeResolveImport",
"kind": "function",
"docComment": "Normalize the public `ResolveImport` union to an `ImportResolver`.\n\nWraps a bare function via `wrapResolveImport` (fresh synthesized identity);\npasses a token-paired resolver through unchanged. `undefined` in, `undefined`\nout — callers fall back to the session default. Call once per logical\nresolver scope: at session construction for the default, per call for an\noverride.",
"typeSignature": "(value: ResolveImport | undefined): ImportResolver | undefined",
"sourceLine": 103,
"alsoExportedFrom": [
"index.ts"
],
"parameters": [
{
"name": "value",
"type": "ResolveImport | undefined"
}
],
"returnType": "ImportResolver | undefined"
},
{
"name": "ensureLexerReady",
"kind": "function",
"docComment": "Ensure `es-module-lexer`'s wasm runtime is initialized.\n\nIdempotent and cheap after the first call. The session awaits this once at\nthe top of `setFiles` so phase 1's per-file lex is purely synchronous.",
"typeSignature": "(): Promise<void>",
"sourceLine": 114,
"returnType": "Promise<void>"
},
{
"name": "lexImports",
"kind": "function",
"docComment": "Lex import specifiers from prepared content.\n\nSync — caller must have awaited `ensureLexerReady` at least once before\ninvoking. For `.svelte` files, pass the svelte2tsx-transformed virtual\ncontent, not the raw `.svelte` source (which isn't lex-able as JS/TS).\n\nDynamic imports (`import(specifier)` with non-literal arg) are omitted.",
"typeSignature": "(content: string, fileId: string): string[]",
"sourceLine": 134,
"throws": [
{
"type": "Error",
"description": "if lexing fails (malformed source). Callers should catch and"
}
],
"parameters": [
{
"name": "content",
"type": "string",
"description": "prepared file content (TS/JS, or svelte2tsx virtual)"
},
{
"name": "fileId",
"type": "string",
"description": "absolute file path (used for error messages)"
}
],
"returnType": "string[]",
"returnDescription": "specifiers in declaration order"
},
{
"name": "isNodeBuiltin",
"kind": "function",
"docComment": "Whether a lexed specifier names a Node builtin (`fs`, `node:fs/promises`, …).\n\nBuiltins can never be a project source file, so resolving them is pointless —\nand routing them through a host resolver (Vite/Rollup) makes that host emit\n\"externalized for browser compatibility\" warnings for browser-targeted\nconfigs. Callers skip resolution for these and treat them as unresolved\n(`null`), which the downstream `isSource` filter would do anyway.",
"typeSignature": "(specifier: string): boolean",
"sourceLine": 152,
"parameters": [
{
"name": "specifier",
"type": "string"
}
],
"returnType": "boolean"
},
{
"name": "createDefaultResolver",
"kind": "function",
"docComment": "Create the default `ImportResolver` (TypeScript + tsconfig).\n\nUses `ts.resolveModuleName` against `ts.sys` directly — no `ts.Program` is\nbuilt. Identity is a fresh symbol per call, so each session that constructs\nits own default gets a unique cache scope. Multiple sessions sharing one\nresolver instance share the cache scope (correct, since resolver state is\nshared too).\n\nFalls back to appending `.svelte` when the bare specifier doesn't resolve —\nlets `.svelte` imports work without polluting the consumer's tsconfig.",
"typeSignature": "(compilerOptions: CompilerOptions, projectRoot: string): ImportResolver",
"sourceLine": 169,
"parameters": [
{
"name": "compilerOptions",
"type": "CompilerOptions",
"description": "parsed tsconfig (from `loadTsconfig`)"
},
{
"name": "projectRoot",
"type": "string",
"description": "absolute project root for the module-resolution cache"
}
],
"returnType": "ImportResolver"
},
{
"name": "wrapResolveImport",
"kind": "function",
"docComment": "Wrap a bare `resolveImport` function into an `ImportResolver` token.\n\n`identity` is optional; when omitted, a fresh `Symbol('wrapped')` is\nsynthesized per call. The synthesized default suits single-use scopes —\n`normalizeResolveImport` relies on it to wrap a bare function for a one-shot\n`analyze` call or for a session default (wrapped once at construction, so the\nidentity stays stable for the session's lifetime).\n\nFor a long-lived session that re-wraps the *same* logical resolver across\ncalls (Vite plugin, LSP), a throwaway identity cache-misses every time — the\nfresh `Symbol('wrapped')` never compares equal. Pass a stable `identity`\nhere, or — simpler — construct an `ImportResolver` (`{resolve, identity}`)\ndirectly and pass it through the `ResolveImport` union.",
"typeSignature": "(resolveImport: ResolveImportFn, identity?: string | symbol): ImportResolver",
"sourceLine": 213,
"parameters": [
{
"name": "resolveImport",
"type": "ResolveImportFn"
},
{
"name": "identity",
"type": "string | symbol",
"defaultValue": "Symbol('wrapped')"
}
],
"returnType": "ImportResolver"
},
{
"name": "noDepsResolver",
"kind": "variable",
"docComment": "Shared no-op `ImportResolver` for the \"dependency resolution disabled\" case.\n\nStable string identity (`'no-deps'`) instead of a per-call symbol so that\nrepeated ingests within a long-lived session cache-hit on identity. Each\nsession still owns its own cache (entries live on the per-session `owned`\nmap; sessions don't share state), but within one session every reference\nto `noDepsResolver` is `===` to every other — so a Vite re-save of\nbyte-identical content with `resolveDependencies: false` exercises the\ncache-hit branch. The resolver always returns `null`, so any two calls\nwith identical content under this identity produce identical resolution\nresults — `string` identity correctly captures that. Used by both the\none-shot `analyzeFromFiles` path and the long-lived Vite plugin\n(`resolveDependencies: false`).",
"typeSignature": "ImportResolver",
"sourceLine": 236
}
],
"moduleComment": "Import resolver primitives for the analysis session.\n\nExposes the `ImportResolver` token contract (`{resolve, identity}`) plus\nbuilding blocks the session uses inside its three-phase setFiles pipeline:\n\n- `ensureLexerReady` — one-time wasm init for `es-module-lexer`. Phase 1 is\n sync per-file; the session awaits this once before the loop starts.\n- `lexImports` — sync specifier extraction from already-prepared content\n (raw TS/JS, or svelte2tsx virtual content for `.svelte` files).\n- `createDefaultResolver` — the TS + tsconfig fallback used when neither\n `AnalysisSessionOptions.resolveImport` nor a per-call override is provided.\n Carries a fresh symbol identity per session (cache-key-safe).\n- `normalizeResolveImport` — coerces the public `ResolveImport` union (bare\n function or token-paired resolver) to an `ImportResolver` at each boundary.\n- `wrapResolveImport` — the fn→token primitive behind `normalizeResolveImport`:\n wraps a bare function in an `ImportResolver`, synthesizing a throwaway\n identity unless a stable one is passed.\n\n@internal — subpath-importable for power users who want to drive resolution\noutside the session, but not part of the stable barrel surface.\n\n@see `session.ts` for the three-phase ingestion pipeline that consumes these\n@see `loadTsconfig` in `typescript-program.ts` for the TS module-resolution\n compiler-options shortcut used by the default resolver",
"dependents": [
"analyze.ts",
"index.ts",
"session.ts",
"vite.ts"
]
}