svelte-docinfo 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +349 -0
  3. package/dist/analyze-core.d.ts +1386 -0
  4. package/dist/analyze-core.d.ts.map +1 -0
  5. package/dist/analyze-core.js +324 -0
  6. package/dist/analyze.d.ts +137 -0
  7. package/dist/analyze.d.ts.map +1 -0
  8. package/dist/analyze.js +148 -0
  9. package/dist/cli.d.ts +102 -0
  10. package/dist/cli.d.ts.map +1 -0
  11. package/dist/cli.js +206 -0
  12. package/dist/concurrency.d.ts +38 -0
  13. package/dist/concurrency.d.ts.map +1 -0
  14. package/dist/concurrency.js +57 -0
  15. package/dist/declaration-build.d.ts +164 -0
  16. package/dist/declaration-build.d.ts.map +1 -0
  17. package/dist/declaration-build.js +15 -0
  18. package/dist/declaration-helpers.d.ts +169 -0
  19. package/dist/declaration-helpers.d.ts.map +1 -0
  20. package/dist/declaration-helpers.js +239 -0
  21. package/dist/dep-resolver.d.ts +166 -0
  22. package/dist/dep-resolver.d.ts.map +1 -0
  23. package/dist/dep-resolver.js +144 -0
  24. package/dist/diagnostics.d.ts +650 -0
  25. package/dist/diagnostics.d.ts.map +1 -0
  26. package/dist/diagnostics.js +377 -0
  27. package/dist/discovery.d.ts +115 -0
  28. package/dist/discovery.d.ts.map +1 -0
  29. package/dist/discovery.js +126 -0
  30. package/dist/exports.d.ts +105 -0
  31. package/dist/exports.d.ts.map +1 -0
  32. package/dist/exports.js +274 -0
  33. package/dist/files.d.ts +77 -0
  34. package/dist/files.d.ts.map +1 -0
  35. package/dist/files.js +95 -0
  36. package/dist/index.d.ts +32 -0
  37. package/dist/index.d.ts.map +1 -0
  38. package/dist/index.js +30 -0
  39. package/dist/log.d.ts +32 -0
  40. package/dist/log.d.ts.map +1 -0
  41. package/dist/log.js +11 -0
  42. package/dist/logo.d.ts +8 -0
  43. package/dist/logo.d.ts.map +1 -0
  44. package/dist/logo.js +9 -0
  45. package/dist/main.d.ts +11 -0
  46. package/dist/main.d.ts.map +1 -0
  47. package/dist/main.js +18 -0
  48. package/dist/paths.d.ts +35 -0
  49. package/dist/paths.d.ts.map +1 -0
  50. package/dist/paths.js +34 -0
  51. package/dist/postprocess.d.ts +145 -0
  52. package/dist/postprocess.d.ts.map +1 -0
  53. package/dist/postprocess.js +305 -0
  54. package/dist/session.d.ts +248 -0
  55. package/dist/session.d.ts.map +1 -0
  56. package/dist/session.js +496 -0
  57. package/dist/source-config.d.ts +330 -0
  58. package/dist/source-config.d.ts.map +1 -0
  59. package/dist/source-config.js +356 -0
  60. package/dist/source.d.ts +126 -0
  61. package/dist/source.d.ts.map +1 -0
  62. package/dist/source.js +68 -0
  63. package/dist/svelte.d.ts +179 -0
  64. package/dist/svelte.d.ts.map +1 -0
  65. package/dist/svelte.js +898 -0
  66. package/dist/tsdoc.d.ts +145 -0
  67. package/dist/tsdoc.d.ts.map +1 -0
  68. package/dist/tsdoc.js +262 -0
  69. package/dist/types.d.ts +3381 -0
  70. package/dist/types.d.ts.map +1 -0
  71. package/dist/types.js +588 -0
  72. package/dist/typescript-exports.d.ts +91 -0
  73. package/dist/typescript-exports.d.ts.map +1 -0
  74. package/dist/typescript-exports.js +656 -0
  75. package/dist/typescript-extract-class.d.ts +29 -0
  76. package/dist/typescript-extract-class.d.ts.map +1 -0
  77. package/dist/typescript-extract-class.js +251 -0
  78. package/dist/typescript-extract-function.d.ts +47 -0
  79. package/dist/typescript-extract-function.d.ts.map +1 -0
  80. package/dist/typescript-extract-function.js +108 -0
  81. package/dist/typescript-extract-shared.d.ts +166 -0
  82. package/dist/typescript-extract-shared.d.ts.map +1 -0
  83. package/dist/typescript-extract-shared.js +528 -0
  84. package/dist/typescript-extract-type-properties.d.ts +34 -0
  85. package/dist/typescript-extract-type-properties.d.ts.map +1 -0
  86. package/dist/typescript-extract-type-properties.js +241 -0
  87. package/dist/typescript-extract-type.d.ts +48 -0
  88. package/dist/typescript-extract-type.d.ts.map +1 -0
  89. package/dist/typescript-extract-type.js +238 -0
  90. package/dist/typescript-program.d.ts +211 -0
  91. package/dist/typescript-program.d.ts.map +1 -0
  92. package/dist/typescript-program.js +290 -0
  93. package/dist/vite.d.ts +116 -0
  94. package/dist/vite.d.ts.map +1 -0
  95. package/dist/vite.js +418 -0
  96. package/package.json +131 -0
  97. package/virtual-svelte-docinfo.d.ts +49 -0
@@ -0,0 +1,496 @@
1
+ /**
2
+ * Persistent analysis session — δ-shaped API over a `ts.LanguageService`.
3
+ *
4
+ * Maps cleanly onto LSP and Vite/HMR consumers:
5
+ *
6
+ * - `setFile` / `setFiles` — additive ingest; transform-if-Svelte, lex
7
+ * specifiers, resolve imports (parallel), push content/virtual to the LS.
8
+ * Returns ingest-time diagnostics + a `changed` flag. Cache-hit no-op
9
+ * when content matches AND the mode-specific cache key matches (resolver
10
+ * identity for lex+resolve, dependency-snapshot equality for pre-resolved).
11
+ * - `deleteFile` — drop owned entry, evict from LS.
12
+ * - `has` / `list` — owned-set introspection (covers what consumers used to
13
+ * get from their own mirror caches).
14
+ * - `query` — sync analysis pass against the current owned set; returns
15
+ * analysis-pass diagnostics only (ingest diagnostics surface via the
16
+ * `setFile`/`setFiles` returns).
17
+ * - `dispose` — release LS resources.
18
+ *
19
+ * The session owns a single `Map<id, OwnedEntry>` covering content, svelte
20
+ * virtuals, unfiltered deps, the mode-specific cache key (resolver identity
21
+ * or pre-resolved snapshot), and ingest-time diagnostics. svelte2tsx runs
22
+ * at most once per content change. Resolver work parallelizes across the
23
+ * batch in phase 2 of the three-phase setFiles pipeline; fully pre-resolved
24
+ * batches skip phase 2 (and the default-resolver construction) entirely.
25
+ *
26
+ * @see `analyze-core.ts` for the two-phase analysis orchestrator
27
+ * @see `dep-resolver.ts` for the `ImportResolver` token contract
28
+ *
29
+ * @module
30
+ */
31
+ import { createAnalysisLanguageService, loadTsconfig, } from './typescript-program.js';
32
+ import { transformSvelteSource } from './svelte.js';
33
+ import { createDefaultResolver, ensureLexerReady, lexImports, normalizeResolveImport, } from './dep-resolver.js';
34
+ import { isSource, normalizeSourceOptions } from './source-config.js';
35
+ import { toPosixPath } from './paths.js';
36
+ import { MAX_RESOLVE_CONCURRENCY, map_concurrent } from './concurrency.js';
37
+ import { analyzeCore, normalizeDiagnosticPaths, AnalyzeResultJson, } from './analyze-core.js';
38
+ import { computeDependents } from './postprocess.js';
39
+ /**
40
+ * Element-wise equality on two readonly string arrays. Used for the
41
+ * pre-resolved-deps cache key: a fresh array with identical contents
42
+ * cache-hits (matches Gro filer's `[...Map.keys()]`-per-call pattern), while
43
+ * any length, element, or order difference cache-misses. Order-sensitive
44
+ * because `unfilteredDeps` ordering reflects the caller's declared graph
45
+ * shape — reordering without a content change is a caller-driven intent
46
+ * signal worth honoring.
47
+ */
48
+ const arraysShallowEqual = (a, b) => {
49
+ if (a === b)
50
+ return true;
51
+ if (a.length !== b.length)
52
+ return false;
53
+ for (let i = 0; i < a.length; i++) {
54
+ if (a[i] !== b[i])
55
+ return false;
56
+ }
57
+ return true;
58
+ };
59
+ /**
60
+ * Create a persistent analysis session.
61
+ *
62
+ * @example Vite plugin integration
63
+ * ```ts
64
+ * const session = createAnalysisSession({sourceOptions, resolveImport, log});
65
+ * await session.setFiles(initialFiles);
66
+ * const result = session.query();
67
+ * // on watcher events:
68
+ * await session.setFile({id, content});
69
+ * await session.deleteFile(removedId);
70
+ * const next = session.query();
71
+ * // on shutdown:
72
+ * session.dispose();
73
+ * ```
74
+ *
75
+ * @example One-shot via the public wrapper
76
+ * ```ts
77
+ * // Equivalent to `analyze(...)` — the wrapper goes through a session internally.
78
+ * const session = createAnalysisSession({sourceOptions});
79
+ * try {
80
+ * await session.setFiles(sourceFiles);
81
+ * return session.query({onDuplicates: 'throw'});
82
+ * } finally {
83
+ * session.dispose();
84
+ * }
85
+ * ```
86
+ */
87
+ export const createAnalysisSession = (options) => {
88
+ const sourceOptions = normalizeSourceOptions(options.sourceOptions);
89
+ const ls = createAnalysisLanguageService({
90
+ projectRoot: sourceOptions.projectRoot,
91
+ tsconfig: options.tsconfig,
92
+ compilerOptions: options.compilerOptions,
93
+ documentRegistry: options.documentRegistry,
94
+ }, options.log);
95
+ const owned = new Map();
96
+ // TODO: the owned-entry cache is in-memory only, so cold one-shot runs
97
+ // (`analyzeFromFiles` from the CLI, `vite build`) re-transform and re-analyze
98
+ // every file from scratch — the expensive case given svelte2tsx + the TS
99
+ // checker. A schema-versioned disk cache (cf. fuz_css `css_cache.ts`: mirror
100
+ // the source tree, key each file's extracted JSON by content hash, stamp an
101
+ // integer cache-version bumped on output-shape changes, self-heal on
102
+ // hash/version/parse mismatch, atomic temp-file+rename writes, skip on CI)
103
+ // would make those cold paths incremental across process restarts. The
104
+ // existing per-entry content-equality check is the in-memory analog; the
105
+ // cache key would need the same `(content, mode-specific key)` discipline
106
+ // the session already uses, plus the version stamp.
107
+ // Lazy default resolver — only constructed when needed: a `setFiles` batch
108
+ // has at least one file lacking `dependencies`, the call doesn't supply a
109
+ // per-call override, and no session-default resolver was configured. Fully
110
+ // pre-resolved batches skip the construction entirely (see `needsResolver`
111
+ // gating below). The TS module-resolution cache is kept on the resolver so
112
+ // consecutive resolves share state.
113
+ let lazyDefault;
114
+ const getDefaultResolver = () => {
115
+ if (lazyDefault)
116
+ return lazyDefault;
117
+ const { compilerOptions } = loadTsconfig({
118
+ projectRoot: sourceOptions.projectRoot,
119
+ tsconfig: options.tsconfig,
120
+ compilerOptions: options.compilerOptions,
121
+ }, options.log);
122
+ lazyDefault = createDefaultResolver(compilerOptions, sourceOptions.projectRoot);
123
+ return lazyDefault;
124
+ };
125
+ // Normalize the session default once at construction. A bare function gets a
126
+ // single synthesized identity here, stable for the session's lifetime — so
127
+ // repeated ingests of byte-identical content cache-hit on identity.
128
+ const sessionDefaultResolver = normalizeResolveImport(options.resolveImport);
129
+ const pickResolver = (override) => {
130
+ if (override)
131
+ return override;
132
+ if (sessionDefaultResolver)
133
+ return sessionDefaultResolver;
134
+ return getDefaultResolver();
135
+ };
136
+ // ── Phase 1: sync per-file transform/lex ─────────────────────────────────
137
+ // `resolver` is `null` when the entire batch is pre-resolved — phase 1
138
+ // needs no resolver identity in that case, and skipping `pickResolver`
139
+ // avoids constructing the lazy default for fully pre-resolved consumers.
140
+ const phase1 = (file, resolver) => {
141
+ const existing = owned.get(file.id);
142
+ // Pre-resolved deps mode: caller asserted `file.dependencies` is the
143
+ // authoritative absolute-path list. Skip lex+resolve; Svelte still gets
144
+ // transformed because the LS needs the virtual.
145
+ const preResolvedDeps = file.dependencies;
146
+ const usePreResolved = preResolvedDeps !== undefined;
147
+ // Cache hit shape depends on mode:
148
+ // pre-resolved → match content + element-wise deps equality (a fresh
149
+ // array with the same contents still hits — Gro's
150
+ // `[...Map.keys()]` per-call pattern cache-reuses
151
+ // cleanly across persistent-session calls)
152
+ // lex+resolve → match content + same resolver identity AND prior
153
+ // entry was lex+resolve mode (mode flip invalidates)
154
+ // All-or-nothing per the spec (no half-runs).
155
+ const cacheHit = existing !== undefined &&
156
+ existing.content === file.content &&
157
+ (usePreResolved
158
+ ? existing.mode === 'pre-resolved' &&
159
+ arraysShallowEqual(existing.preResolvedDepsSnapshot, preResolvedDeps)
160
+ : existing.mode === 'lex+resolve' &&
161
+ resolver !== null &&
162
+ existing.resolverIdentity === resolver.identity);
163
+ if (cacheHit) {
164
+ return {
165
+ file,
166
+ virtual: existing.virtual,
167
+ transformFailed: existing.transformFailed === true,
168
+ ingestDiagnostics: existing.ingestDiagnostics,
169
+ specifiers: [],
170
+ // Phase 3 short-circuits on `cacheHit: true` before reading this
171
+ // field — leave it undefined to document that it's unused on
172
+ // the cache-hit path (the entry in `owned` already carries the
173
+ // authoritative resolver identity / deps snapshot).
174
+ resolverIdentity: undefined,
175
+ cacheHit: true,
176
+ previousVirtualPath: undefined,
177
+ preResolvedDeps: usePreResolved ? preResolvedDeps : undefined,
178
+ };
179
+ }
180
+ // Capture the prior virtual path before we replace the entry. If the new
181
+ // transform fails, phase 3 evicts the stale virtual from the LS.
182
+ const previousVirtualPath = existing?.virtual?.virtualPath;
183
+ const ingestDiagnostics = [];
184
+ const analyzer = sourceOptions.getAnalyzerType(file.id);
185
+ // Transform if Svelte. transformSvelteSource returns ingest diagnostics
186
+ // directly (transform_failed on throw, source_map_failed on map error).
187
+ // Runs regardless of mode — the LS host needs the virtual for checker
188
+ // state, independent of how we obtain dependency edges.
189
+ let virtual;
190
+ let transformFailed = false;
191
+ if (analyzer === 'svelte') {
192
+ const tres = transformSvelteSource(file);
193
+ for (const d of tres.diagnostics)
194
+ ingestDiagnostics.push(d);
195
+ if (tres.virtual) {
196
+ virtual = tres.virtual;
197
+ }
198
+ else {
199
+ transformFailed = true;
200
+ }
201
+ }
202
+ // Lex import specifiers — skipped when caller supplied pre-resolved deps.
203
+ // Sync — caller awaited ensureLexerReady. CSS/JSON files have nothing to
204
+ // lex; transform_failed Svelte has no virtual content to lex.
205
+ let specifiers = [];
206
+ if (!usePreResolved &&
207
+ !transformFailed &&
208
+ (analyzer === 'typescript' || analyzer === 'svelte')) {
209
+ try {
210
+ const contentToLex = virtual ? virtual.content : file.content;
211
+ specifiers = lexImports(contentToLex, file.id);
212
+ }
213
+ catch (err) {
214
+ ingestDiagnostics.push({
215
+ kind: 'import_parse_failed',
216
+ file: file.id,
217
+ message: `Failed to parse imports: ${err instanceof Error ? err.message : String(err)}`,
218
+ severity: 'warning',
219
+ });
220
+ }
221
+ }
222
+ return {
223
+ file,
224
+ virtual,
225
+ transformFailed,
226
+ ingestDiagnostics,
227
+ specifiers,
228
+ resolverIdentity: resolver?.identity,
229
+ cacheHit: false,
230
+ previousVirtualPath,
231
+ preResolvedDeps: usePreResolved ? preResolvedDeps : undefined,
232
+ };
233
+ };
234
+ // ── Phase 3: serial per-file LS push + entry write ───────────────────────
235
+ const phase3 = (pending, resolved) => {
236
+ if (pending.cacheHit) {
237
+ // Cached SetFileResult: same diagnostics array reference, changed: false.
238
+ // `[...]` clone keeps the caller from mutating the entry's stored array.
239
+ // Cached entry's diagnostics were normalized at the time the entry was stored.
240
+ return { changed: false, diagnostics: [...pending.ingestDiagnostics] };
241
+ }
242
+ // Normalize ingest diagnostics to project-root-relative paths before
243
+ // the entry is stored. After this point, `entry.ingestDiagnostics` is
244
+ // normalized at rest — `allIngestDiagnostics()` can read it directly
245
+ // without depending on a later in-place mutation via the aggregate.
246
+ normalizeDiagnosticPaths(pending.ingestDiagnostics, sourceOptions.projectRoot);
247
+ // Build `unfilteredDeps` from one of two sources:
248
+ // pre-resolved → caller's `file.dependencies`, posixified + filtered
249
+ // lex+resolve → resolver task outputs, already posixified, filtered
250
+ const unfilteredDeps = [];
251
+ if (pending.preResolvedDeps !== undefined) {
252
+ for (const raw of pending.preResolvedDeps) {
253
+ const posix = toPosixPath(raw);
254
+ if (!isSource(posix, sourceOptions))
255
+ continue;
256
+ unfilteredDeps.push(posix);
257
+ }
258
+ }
259
+ else {
260
+ for (const r of resolved) {
261
+ if (r === null)
262
+ continue;
263
+ if (!isSource(r, sourceOptions))
264
+ continue;
265
+ unfilteredDeps.push(r);
266
+ }
267
+ }
268
+ // Build a mode-specific entry. The discriminator (`mode`) tags which
269
+ // cache-key field to read on the next ingest: `resolverIdentity` for
270
+ // lex+resolve, `preResolvedDepsSnapshot` for pre-resolved. Snapshot
271
+ // the caller's array on the pre-resolved branch — owning a copy means
272
+ // subsequent caller-side mutation doesn't produce false cache hits.
273
+ const entry = pending.preResolvedDeps !== undefined
274
+ ? {
275
+ mode: 'pre-resolved',
276
+ content: pending.file.content,
277
+ virtual: pending.virtual,
278
+ unfilteredDeps,
279
+ preResolvedDepsSnapshot: pending.preResolvedDeps.slice(),
280
+ ingestDiagnostics: pending.ingestDiagnostics,
281
+ }
282
+ : {
283
+ mode: 'lex+resolve',
284
+ content: pending.file.content,
285
+ virtual: pending.virtual,
286
+ unfilteredDeps,
287
+ // Non-null assert: lex+resolve branch implies the batch picked a
288
+ // resolver, so phase 1 stored its identity on the pending entry.
289
+ resolverIdentity: pending.resolverIdentity,
290
+ ingestDiagnostics: pending.ingestDiagnostics,
291
+ };
292
+ if (pending.transformFailed)
293
+ entry.transformFailed = true;
294
+ owned.set(pending.file.id, entry);
295
+ // LS push — virtual path for successful Svelte transforms, real path
296
+ // for TS/JS. CSS/JSON aren't TypeScript-resolvable; skip the push.
297
+ // Transform-failed Svelte: no virtual; skip.
298
+ const analyzer = sourceOptions.getAnalyzerType(pending.file.id);
299
+ if (pending.virtual) {
300
+ ls.setFile(pending.virtual.virtualPath, pending.virtual.content);
301
+ }
302
+ else if (analyzer === 'typescript') {
303
+ ls.setFile(pending.file.id, pending.file.content);
304
+ }
305
+ else if (pending.previousVirtualPath) {
306
+ // Transform regressed (had virtual → now transform_failed). Evict the
307
+ // stale virtual from the LS so other files importing this `.svelte`
308
+ // don't see the prior svelte2tsx output via the checker.
309
+ ls.deleteFile(pending.previousVirtualPath);
310
+ }
311
+ return { changed: true, diagnostics: [...pending.ingestDiagnostics] };
312
+ };
313
+ // ── setFiles: orchestrate three phases ───────────────────────────────────
314
+ const setFiles = async (files, opts) => {
315
+ await ensureLexerReady();
316
+ // Resolver gating: pick one only if any file in the batch lacks
317
+ // `dependencies` (i.e., needs lex+resolve). For fully pre-resolved
318
+ // batches the resolver is never consulted, so we skip `pickResolver`
319
+ // entirely — that avoids constructing the lazy default
320
+ // (`loadTsconfig` + `createDefaultResolver`) for consumers like the
321
+ // Gro filer that always hand over pre-resolved deps.
322
+ const needsResolver = files.some((f) => f.dependencies === undefined);
323
+ // Normalize the per-call override (bare fn → fresh identity each call).
324
+ const resolver = needsResolver
325
+ ? pickResolver(normalizeResolveImport(opts?.resolveImport))
326
+ : null;
327
+ // Posixify ids at ingest — the internal contract is forward-slash
328
+ // everywhere. Skip the clone when the input is already POSIX (common
329
+ // path on Linux/macOS).
330
+ const normalizedFiles = files.map((f) => {
331
+ const posixId = toPosixPath(f.id);
332
+ return posixId === f.id ? f : { ...f, id: posixId };
333
+ });
334
+ // Phase 1: sync per-file transform + lex.
335
+ const pendings = [];
336
+ for (const file of normalizedFiles) {
337
+ pendings.push(phase1(file, resolver));
338
+ }
339
+ const tasks = [];
340
+ for (let pi = 0; pi < pendings.length; pi++) {
341
+ const p = pendings[pi];
342
+ if (p.cacheHit)
343
+ continue;
344
+ for (let si = 0; si < p.specifiers.length; si++) {
345
+ tasks.push({ pendingIdx: pi, specIdx: si, specifier: p.specifiers[si] });
346
+ }
347
+ }
348
+ // Resolver invariant: `tasks` non-empty implies `resolver !== null`.
349
+ // A task is only enqueued for a non-cache-hit pending with at least
350
+ // one specifier, which by phase 1's logic implies the file went
351
+ // through the lex+resolve branch, which only runs when at least one
352
+ // file in the batch lacks `dependencies` — i.e., `needsResolver` was
353
+ // `true` and a resolver was picked. Convert this unreachable-by-
354
+ // invariant into unreachable-by-throw so the `resolver!` below has
355
+ // an explicit runtime defense rather than relying on the chain.
356
+ if (tasks.length > 0 && resolver === null) {
357
+ throw new Error('svelte-docinfo: phase-2 invariant violated — tasks pending without a resolver');
358
+ }
359
+ const taskResults = await map_concurrent(tasks, MAX_RESOLVE_CONCURRENCY, async (t) => {
360
+ const pending = pendings[t.pendingIdx];
361
+ try {
362
+ const resolved = await resolver.resolve(t.specifier, pending.file.id);
363
+ // Posixify resolver output — custom resolvers (Vite/Rollup,
364
+ // user-supplied) may emit native paths on Windows. The TS
365
+ // default resolver already returns POSIX, so this is a no-op
366
+ // there. Storing POSIX keeps unfilteredDeps consistent with
367
+ // owned-set keys in `query()`'s ownedIds filter.
368
+ return {
369
+ ...t,
370
+ resolved: resolved === null ? null : toPosixPath(resolved),
371
+ error: undefined,
372
+ };
373
+ }
374
+ catch (err) {
375
+ return {
376
+ ...t,
377
+ resolved: null,
378
+ error: err instanceof Error ? err.message : String(err),
379
+ };
380
+ }
381
+ });
382
+ // Group resolved results by pending index for phase 3. Resolver errors
383
+ // land on the importing file's ingest diagnostics so per-file grouping
384
+ // (LSP publish, etc.) keeps them attached to the right source.
385
+ // Dedup on (pendingIdx, specifier) — duplicate imports of the same path
386
+ // throw N times, but the diagnostic carries no per-import-site info, so
387
+ // emitting once per specifier keeps the output non-redundant.
388
+ const resolvedByPending = pendings.map((p) => p.cacheHit ? [] : new Array(p.specifiers.length).fill(null));
389
+ const seenFailures = new Map();
390
+ for (const r of taskResults) {
391
+ resolvedByPending[r.pendingIdx][r.specIdx] = r.resolved;
392
+ if (r.error === undefined)
393
+ continue;
394
+ let seen = seenFailures.get(r.pendingIdx);
395
+ if (!seen) {
396
+ seen = new Set();
397
+ seenFailures.set(r.pendingIdx, seen);
398
+ }
399
+ if (seen.has(r.specifier))
400
+ continue;
401
+ seen.add(r.specifier);
402
+ const pending = pendings[r.pendingIdx];
403
+ pending.ingestDiagnostics.push({
404
+ kind: 'resolver_failed',
405
+ file: pending.file.id,
406
+ message: `Import resolver threw for "${r.specifier}": ${r.error}`,
407
+ severity: 'warning',
408
+ specifier: r.specifier,
409
+ });
410
+ }
411
+ // Phase 3: serial per-file LS push + entry write. Single LS mutator
412
+ // across the batch — no interleaved updates from concurrent tasks.
413
+ const perFile = new Map();
414
+ const changedIds = new Set();
415
+ const aggregateDiagnostics = [];
416
+ for (let i = 0; i < pendings.length; i++) {
417
+ const pending = pendings[i];
418
+ const result = phase3(pending, resolvedByPending[i]);
419
+ perFile.set(pending.file.id, result);
420
+ if (result.changed)
421
+ changedIds.add(pending.file.id);
422
+ for (const d of result.diagnostics)
423
+ aggregateDiagnostics.push(d);
424
+ }
425
+ return { changedIds, diagnostics: aggregateDiagnostics, perFile };
426
+ };
427
+ const setFile = async (file, opts) => {
428
+ const batch = await setFiles([file], opts);
429
+ return batch.perFile.get(toPosixPath(file.id));
430
+ };
431
+ // `deleteFile` returns `Promise<void>` for symmetry with `setFile`/`setFiles`
432
+ // (the spec calls this "async-by-convention"). The body is purely sync —
433
+ // `ls.deleteFile` and `Map.delete` are sync — so we wrap the return rather
434
+ // than declaring `async` (which would trip eslint's require-await).
435
+ const deleteFile = (id) => {
436
+ const posixId = toPosixPath(id);
437
+ const entry = owned.get(posixId);
438
+ if (!entry)
439
+ return Promise.resolve();
440
+ if (entry.virtual) {
441
+ ls.deleteFile(entry.virtual.virtualPath);
442
+ }
443
+ else {
444
+ ls.deleteFile(posixId);
445
+ }
446
+ owned.delete(posixId);
447
+ return Promise.resolve();
448
+ };
449
+ const has = (id) => owned.has(toPosixPath(id));
450
+ const list = () => [...owned.keys()];
451
+ const allIngestDiagnostics = () => {
452
+ const out = [];
453
+ for (const entry of owned.values()) {
454
+ for (const d of entry.ingestDiagnostics)
455
+ out.push(d);
456
+ }
457
+ return out;
458
+ };
459
+ const query = (opts) => {
460
+ // Build query inputs from owned entries. Filter unfilteredDeps to the
461
+ // current owned set per cache strategy A.
462
+ const ownedIds = new Set(owned.keys());
463
+ const sourceFiles = [];
464
+ const svelteVirtualFiles = new Map();
465
+ const transformFailedIds = new Set();
466
+ for (const [id, entry] of owned) {
467
+ const filteredDeps = entry.unfilteredDeps.filter((d) => ownedIds.has(d));
468
+ sourceFiles.push({ id, content: entry.content, dependencies: filteredDeps });
469
+ if (entry.virtual)
470
+ svelteVirtualFiles.set(id, entry.virtual);
471
+ if (entry.transformFailed)
472
+ transformFailedIds.add(id);
473
+ }
474
+ // Compute bidirectional dependents from the filtered forward edges.
475
+ const filesWithDeps = computeDependents(sourceFiles);
476
+ // `getProgram()` returns the same `ts.Program` reference as the prior
477
+ // call when no version bumped, or a fresh program reusing unchanged
478
+ // ASTs via the document registry.
479
+ const program = ls.getProgram();
480
+ const result = analyzeCore({
481
+ sourceFiles: filesWithDeps,
482
+ sourceOptions,
483
+ program,
484
+ svelteVirtualFiles,
485
+ transformFailedIds,
486
+ onDuplicates: opts?.onDuplicates,
487
+ log: opts?.log ?? options.log,
488
+ });
489
+ return result;
490
+ };
491
+ const dispose = () => {
492
+ ls.dispose();
493
+ owned.clear();
494
+ };
495
+ return { setFile, setFiles, deleteFile, has, list, query, allIngestDiagnostics, dispose };
496
+ };