svelte-docinfo 0.5.4 → 0.6.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.
- package/README.md +69 -50
- package/dist/analyze-core.d.ts +179 -24
- package/dist/analyze-core.d.ts.map +1 -1
- package/dist/analyze-core.js +462 -42
- package/dist/analyze.d.ts +31 -6
- package/dist/analyze.d.ts.map +1 -1
- package/dist/analyze.js +29 -9
- package/dist/cli.d.ts +17 -7
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +12 -9
- package/dist/declaration-build.d.ts +14 -4
- package/dist/declaration-build.d.ts.map +1 -1
- package/dist/declaration-helpers.d.ts +65 -42
- package/dist/declaration-helpers.d.ts.map +1 -1
- package/dist/declaration-helpers.js +167 -72
- package/dist/dep-resolver.d.ts +38 -13
- package/dist/dep-resolver.d.ts.map +1 -1
- package/dist/dep-resolver.js +40 -22
- package/dist/diagnostics.d.ts +365 -14
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js +112 -28
- package/dist/discovery.d.ts +7 -2
- package/dist/discovery.d.ts.map +1 -1
- package/dist/discovery.js +11 -5
- package/dist/exports.d.ts +39 -2
- package/dist/exports.d.ts.map +1 -1
- package/dist/exports.js +251 -46
- package/dist/files.d.ts +14 -2
- package/dist/files.d.ts.map +1 -1
- package/dist/files.js +49 -8
- package/dist/index.d.ts +9 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -4
- package/dist/logo.js +3 -3
- package/dist/paths.d.ts +22 -1
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +22 -1
- package/dist/postprocess.d.ts +31 -10
- package/dist/postprocess.d.ts.map +1 -1
- package/dist/postprocess.js +124 -76
- package/dist/session.d.ts +101 -11
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +457 -127
- package/dist/source-config.d.ts +210 -33
- package/dist/source-config.d.ts.map +1 -1
- package/dist/source-config.js +313 -26
- package/dist/source.d.ts +35 -0
- package/dist/source.d.ts.map +1 -1
- package/dist/source.js +37 -0
- package/dist/svelte.d.ts +81 -23
- package/dist/svelte.d.ts.map +1 -1
- package/dist/svelte.js +686 -359
- package/dist/tsdoc.d.ts +47 -12
- package/dist/tsdoc.d.ts.map +1 -1
- package/dist/tsdoc.js +70 -17
- package/dist/types.d.ts +967 -45
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +259 -48
- package/dist/typescript-alias-registry.d.ts +76 -0
- package/dist/typescript-alias-registry.d.ts.map +1 -0
- package/dist/typescript-alias-registry.js +206 -0
- package/dist/typescript-exports.d.ts +20 -15
- package/dist/typescript-exports.d.ts.map +1 -1
- package/dist/typescript-exports.js +264 -137
- package/dist/typescript-extract-class.d.ts +3 -4
- package/dist/typescript-extract-class.d.ts.map +1 -1
- package/dist/typescript-extract-class.js +71 -52
- package/dist/typescript-extract-function.d.ts +6 -8
- package/dist/typescript-extract-function.d.ts.map +1 -1
- package/dist/typescript-extract-function.js +20 -15
- package/dist/typescript-extract-shared.d.ts +309 -29
- package/dist/typescript-extract-shared.d.ts.map +1 -1
- package/dist/typescript-extract-shared.js +957 -127
- package/dist/typescript-extract-type-json.d.ts +286 -0
- package/dist/typescript-extract-type-json.d.ts.map +1 -0
- package/dist/typescript-extract-type-json.js +854 -0
- package/dist/typescript-extract-type-properties.d.ts +2 -3
- package/dist/typescript-extract-type-properties.d.ts.map +1 -1
- package/dist/typescript-extract-type-properties.js +91 -84
- package/dist/typescript-extract-type.d.ts +6 -8
- package/dist/typescript-extract-type.d.ts.map +1 -1
- package/dist/typescript-extract-type.js +138 -40
- package/dist/typescript-program.d.ts +106 -23
- package/dist/typescript-program.d.ts.map +1 -1
- package/dist/typescript-program.js +182 -54
- package/dist/vite.d.ts +23 -8
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +34 -14
- package/package.json +17 -15
- package/virtual-svelte-docinfo.d.ts +1 -1
package/dist/analyze-core.js
CHANGED
|
@@ -17,24 +17,35 @@
|
|
|
17
17
|
* - `AnalyzeResultJson` / `OnDuplicates` / `OnDuplicatesCallback` — shared types
|
|
18
18
|
* surfaced through the main barrel directly from this module.
|
|
19
19
|
* - `throwOnDuplicates` — convenience callback paired with `OnDuplicates`.
|
|
20
|
+
* - `finalizeDiagnostics` — the diagnostic boundary passes (virtual position
|
|
21
|
+
* remap, then path normalization) in their required order; the one call for
|
|
22
|
+
* callers assembling modules themselves.
|
|
20
23
|
* - `normalizeDiagnosticPaths` — boundary helper for build-tool integrations
|
|
21
24
|
* that bypass the session and collect their own diagnostics.
|
|
25
|
+
* - `normalizeModulePathsInTypes` — output pass rewriting the absolute module
|
|
26
|
+
* paths the checker embeds in printed type text. Runs inside `analyzeCore`;
|
|
27
|
+
* exposed for the same reason as `normalizeDiagnosticPaths`, so a caller
|
|
28
|
+
* assembling modules itself through `analyzeModule` can meet the same
|
|
29
|
+
* output contract.
|
|
22
30
|
*
|
|
23
31
|
* @internal — module split is implementation detail; consumers go through
|
|
24
32
|
* `analyze.ts` / `session.ts` for the stable surface.
|
|
25
33
|
*
|
|
26
34
|
* @module
|
|
27
35
|
*/
|
|
36
|
+
import { relative } from 'node:path';
|
|
28
37
|
import ts from 'typescript';
|
|
29
38
|
import { z } from 'zod';
|
|
30
39
|
import { ModuleJson } from './types.js';
|
|
31
40
|
import { Diagnostic } from './diagnostics.js';
|
|
32
41
|
import { analyzeTypescriptModule } from './typescript-exports.js';
|
|
33
|
-
import { analyzeSvelteModule } from './svelte.js';
|
|
34
|
-
import { stripVirtualSuffix, getComponentName } from './source.js';
|
|
42
|
+
import { analyzeSvelteModule, remapVirtualDiagnosticPositions } from './svelte.js';
|
|
43
|
+
import { scrubVirtualSuffixes, stripVirtualSuffix, getComponentName } from './source.js';
|
|
35
44
|
import { extractPath, extractDependencies } from './source-config.js';
|
|
36
|
-
import { toPosixPath } from './paths.js';
|
|
37
|
-
import {
|
|
45
|
+
import { toPosixPath, isAbsolutePosixPath } from './paths.js';
|
|
46
|
+
import { buildAliasRegistry } from './typescript-alias-registry.js';
|
|
47
|
+
import { createExtractContext } from './typescript-extract-shared.js';
|
|
48
|
+
import { sortModules, findDuplicates, mergeReExports, resolveComponentAliases, compareStrings } from './postprocess.js';
|
|
38
49
|
/**
|
|
39
50
|
* Convenience `OnDuplicatesCallback` that throws on any duplicate.
|
|
40
51
|
*
|
|
@@ -63,18 +74,30 @@ const formatDuplicates = (duplicates) => {
|
|
|
63
74
|
'(2) add /** @nodocs */ to exclude from documentation.\n' +
|
|
64
75
|
details);
|
|
65
76
|
};
|
|
66
|
-
|
|
77
|
+
/**
|
|
78
|
+
* Emit one `duplicate_declaration` per colliding name.
|
|
79
|
+
*
|
|
80
|
+
* The record carries both path bases on purpose: `file` names a file
|
|
81
|
+
* (project-root-relative, like every diagnostic) while `modules` and the
|
|
82
|
+
* message name modules (`ModuleJson.path`, relative to `sourceRoot`).
|
|
83
|
+
*
|
|
84
|
+
* @param sourceIdsByModulePath - `ModuleJson.path` → absolute source id, since
|
|
85
|
+
* `DuplicateDeclaration` carries only the module path. Covers every module
|
|
86
|
+
* `duplicates` can name by construction — `findDuplicates` reports paths of
|
|
87
|
+
* the modules handed to it — so the lookup's fallback never fires.
|
|
88
|
+
*/
|
|
89
|
+
const emitDuplicateDiagnostics = (diagnostics, duplicates, sourceIdsByModulePath) => {
|
|
67
90
|
for (const [name, occurrences] of duplicates) {
|
|
68
91
|
const first = occurrences[0];
|
|
69
92
|
const modules = occurrences.map((o) => o.module);
|
|
70
93
|
diagnostics.push({
|
|
71
94
|
kind: 'duplicate_declaration',
|
|
72
|
-
file: first.module,
|
|
95
|
+
file: sourceIdsByModulePath.get(first.module) ?? first.module,
|
|
73
96
|
line: first.declaration.sourceLine,
|
|
74
97
|
message: `Duplicate declaration "${name}" defined in: ${modules.join(', ')}`,
|
|
75
98
|
severity: 'warning',
|
|
76
99
|
declarationName: name,
|
|
77
|
-
modules
|
|
100
|
+
modules
|
|
78
101
|
});
|
|
79
102
|
}
|
|
80
103
|
};
|
|
@@ -119,7 +142,7 @@ const dispatchOnDuplicates = (mode, duplicates, log) => {
|
|
|
119
142
|
*/
|
|
120
143
|
export const AnalyzeResultJson = z.strictObject({
|
|
121
144
|
modules: z.array(ModuleJson).default([]),
|
|
122
|
-
diagnostics: z.array(Diagnostic).default([])
|
|
145
|
+
diagnostics: z.array(Diagnostic).default([])
|
|
123
146
|
});
|
|
124
147
|
const toModuleJson = (raw) => {
|
|
125
148
|
const filtered = raw.declarations.filter((d) => !d.nodocs).map((d) => d.declaration);
|
|
@@ -151,7 +174,7 @@ const toModuleJson = (raw) => {
|
|
|
151
174
|
reExports,
|
|
152
175
|
externalReExports,
|
|
153
176
|
externalStarExports: raw.externalStarExports,
|
|
154
|
-
...(raw.moduleComment ? { moduleComment: raw.moduleComment } : {})
|
|
177
|
+
...(raw.moduleComment ? { moduleComment: raw.moduleComment } : {})
|
|
155
178
|
});
|
|
156
179
|
};
|
|
157
180
|
// Module dispatch
|
|
@@ -175,19 +198,26 @@ const toModuleJson = (raw) => {
|
|
|
175
198
|
* `alsoExportedFrom` on returned declarations is always empty from this path —
|
|
176
199
|
* cross-module re-export resolution requires all modules (`mergeReExports`
|
|
177
200
|
* consumes the returned module's `reExports` in phase 2).
|
|
201
|
+
*
|
|
202
|
+
* `aliasRegistry` is optional at this boundary: direct callers assembling
|
|
203
|
+
* modules themselves get written-name recovery only, while `analyzeCore`
|
|
204
|
+
* passes the registry its pre-pass built (see `buildAliasRegistry`).
|
|
178
205
|
*/
|
|
179
|
-
export const analyzeModule = (sourceFile, program, options, diagnostics, log) => {
|
|
206
|
+
export const analyzeModule = (sourceFile, program, options, diagnostics, log, aliasRegistry) => {
|
|
180
207
|
const checker = program.getTypeChecker();
|
|
181
208
|
const modulePath = extractPath(sourceFile.id, options);
|
|
209
|
+
// diagnostics carry the absolute id (see the `Diagnostic.file` schema doc);
|
|
210
|
+
// the returned `ModuleJson` keeps `modulePath`, a different base
|
|
211
|
+
const diagnosticFile = toPosixPath(sourceFile.id);
|
|
182
212
|
const analyzerType = options.getAnalyzerType(sourceFile.id);
|
|
183
213
|
let raw;
|
|
184
214
|
if (analyzerType === 'svelte') {
|
|
185
215
|
diagnostics.push({
|
|
186
216
|
kind: 'module_skipped',
|
|
187
|
-
file:
|
|
217
|
+
file: diagnosticFile,
|
|
188
218
|
message: 'Svelte files require program integration. Use createAnalysisSession or analyze()/analyzeFromFiles() instead.',
|
|
189
219
|
severity: 'warning',
|
|
190
|
-
reason: 'requires_program'
|
|
220
|
+
reason: 'requires_program'
|
|
191
221
|
});
|
|
192
222
|
log?.warn(`Svelte file skipped in analyzeModule: ${sourceFile.id}`);
|
|
193
223
|
return undefined;
|
|
@@ -197,15 +227,22 @@ export const analyzeModule = (sourceFile, program, options, diagnostics, log) =>
|
|
|
197
227
|
if (!tsSourceFile) {
|
|
198
228
|
diagnostics.push({
|
|
199
229
|
kind: 'module_skipped',
|
|
200
|
-
file:
|
|
201
|
-
|
|
230
|
+
file: diagnosticFile,
|
|
231
|
+
// interpolating the same id as `file` keeps the two from naming the
|
|
232
|
+
// file differently — the message gets the same project-root scrub.
|
|
233
|
+
// The log line below keeps the raw id: logs are for developers,
|
|
234
|
+
// diagnostics ship.
|
|
235
|
+
message: `Could not get source file from program: ${diagnosticFile}`,
|
|
202
236
|
severity: 'warning',
|
|
203
|
-
reason: 'not_in_program'
|
|
237
|
+
reason: 'not_in_program'
|
|
204
238
|
});
|
|
205
239
|
log?.warn(`Could not get source file from program: ${sourceFile.id}`);
|
|
206
240
|
return undefined;
|
|
207
241
|
}
|
|
208
|
-
|
|
242
|
+
// the pass-scoped extraction context — constructed here because the
|
|
243
|
+
// dispatcher is where the program is in hand (see `analyzeExports`)
|
|
244
|
+
const ctx = createExtractContext(program, checker, options, diagnostics, aliasRegistry);
|
|
245
|
+
raw = analyzeTypescriptModule(sourceFile, tsSourceFile, modulePath, ctx, options);
|
|
209
246
|
}
|
|
210
247
|
else if (analyzerType === 'css' || analyzerType === 'json') {
|
|
211
248
|
const { dependencies, dependents } = extractDependencies(sourceFile, options);
|
|
@@ -217,16 +254,16 @@ export const analyzeModule = (sourceFile, program, options, diagnostics, log) =>
|
|
|
217
254
|
starExports: [],
|
|
218
255
|
reExports: [],
|
|
219
256
|
externalReExports: [],
|
|
220
|
-
externalStarExports: []
|
|
257
|
+
externalStarExports: []
|
|
221
258
|
};
|
|
222
259
|
}
|
|
223
260
|
else {
|
|
224
261
|
diagnostics.push({
|
|
225
262
|
kind: 'module_skipped',
|
|
226
|
-
file:
|
|
227
|
-
message: `No analyzer for file type: ${
|
|
263
|
+
file: diagnosticFile,
|
|
264
|
+
message: `No analyzer for file type: ${diagnosticFile}`,
|
|
228
265
|
severity: 'warning',
|
|
229
|
-
reason: 'no_analyzer'
|
|
266
|
+
reason: 'no_analyzer'
|
|
230
267
|
});
|
|
231
268
|
log?.warn(`No analyzer for file: ${sourceFile.id}`);
|
|
232
269
|
return undefined;
|
|
@@ -250,11 +287,82 @@ export const analyzeModule = (sourceFile, program, options, diagnostics, log) =>
|
|
|
250
287
|
* `extractDependencies`); `analyzeCore` does not compute them. `session.query`
|
|
251
288
|
* runs `computeDependents` on the owned set before invoking this.
|
|
252
289
|
*/
|
|
290
|
+
/**
|
|
291
|
+
* Analyze the gated Svelte canonicals referenced by an emitted component
|
|
292
|
+
* alias, as `resolveComponentAliases` fill context only (see
|
|
293
|
+
* `AnalyzeCoreInputs.contextSvelteFiles`). Unreferenced files are skipped
|
|
294
|
+
* entirely; analysis diagnostics are dropped — the canonical's `partial`
|
|
295
|
+
* flag propagates through the fill, which is the emitted signal.
|
|
296
|
+
*/
|
|
297
|
+
const analyzeContextComponents = (contextSvelteFiles, emittedModules, inputs, aliasRegistry) => {
|
|
298
|
+
const { sourceOptions, program, svelteVirtualFiles } = inputs;
|
|
299
|
+
const referenced = new Set();
|
|
300
|
+
for (const mod of emittedModules) {
|
|
301
|
+
for (const decl of mod.declarations) {
|
|
302
|
+
if (decl.kind === 'component' && decl.aliasOf)
|
|
303
|
+
referenced.add(decl.aliasOf.module);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const contextModules = [];
|
|
307
|
+
if (referenced.size === 0)
|
|
308
|
+
return contextModules;
|
|
309
|
+
for (const sourceFile of contextSvelteFiles) {
|
|
310
|
+
const modulePath = extractPath(sourceFile.id, sourceOptions);
|
|
311
|
+
if (!referenced.has(modulePath))
|
|
312
|
+
continue;
|
|
313
|
+
const virtualFile = svelteVirtualFiles.get(sourceFile.id);
|
|
314
|
+
if (!virtualFile)
|
|
315
|
+
continue;
|
|
316
|
+
const droppedDiagnostics = [];
|
|
317
|
+
const raw = analyzeSvelteModule(sourceFile, modulePath, program.getTypeChecker(), sourceOptions, droppedDiagnostics, program, virtualFile, aliasRegistry);
|
|
318
|
+
if (raw)
|
|
319
|
+
contextModules.push(toModuleJson(raw));
|
|
320
|
+
}
|
|
321
|
+
return contextModules;
|
|
322
|
+
};
|
|
253
323
|
export const analyzeCore = (inputs) => {
|
|
254
|
-
const { sourceFiles, sourceOptions, program, svelteVirtualFiles, transformFailedIds, onDuplicates, log
|
|
324
|
+
const { sourceFiles, sourceOptions, program, svelteVirtualFiles, transformFailedIds, contextSvelteFiles, onDuplicates, log } = inputs;
|
|
255
325
|
const checker = program.getTypeChecker();
|
|
256
326
|
const diagnostics = [];
|
|
327
|
+
// Registry pre-pass: register the emitted set's exported lost aliases
|
|
328
|
+
// before any extraction, so every `resolveTypeInfo` call this cycle can
|
|
329
|
+
// consult a complete registry. Svelte modules are reached through their
|
|
330
|
+
// svelte2tsx virtuals (`program.getSourceFile(<.svelte id>)` is undefined);
|
|
331
|
+
// CSS/JSON have no exports and analyzer-less files no program file. Gated
|
|
332
|
+
// context files are deliberately absent — their aliases must not register
|
|
333
|
+
// (emitted references always have a doc page to link).
|
|
334
|
+
const registrySources = [];
|
|
335
|
+
for (const sourceFile of sourceFiles) {
|
|
336
|
+
if (transformFailedIds?.has(sourceFile.id))
|
|
337
|
+
continue;
|
|
338
|
+
const virtualFile = svelteVirtualFiles.get(sourceFile.id);
|
|
339
|
+
const programFile = virtualFile
|
|
340
|
+
? program.getSourceFile(virtualFile.virtualPath)
|
|
341
|
+
: sourceOptions.getAnalyzerType(sourceFile.id) === 'typescript'
|
|
342
|
+
? program.getSourceFile(sourceFile.id)
|
|
343
|
+
: undefined;
|
|
344
|
+
if (!programFile)
|
|
345
|
+
continue;
|
|
346
|
+
registrySources.push({
|
|
347
|
+
sourceFile: programFile,
|
|
348
|
+
modulePath: extractPath(sourceFile.id, sourceOptions)
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
const aliasRegistry = buildAliasRegistry(registrySources, checker);
|
|
257
352
|
const modules = [];
|
|
353
|
+
// `ModuleJson.path` → the absolute id it was extracted from, so a
|
|
354
|
+
// module-path-keyed pass can still report a project-root-relative
|
|
355
|
+
// `Diagnostic.file` (`emitDuplicateDiagnostics` is the consumer). The two
|
|
356
|
+
// bases differ, and the reverse direction isn't reconstructible:
|
|
357
|
+
// `extractPath`'s no-prefix fallback returns the absolute path, so
|
|
358
|
+
// prepending `sourceRoot` to a module path can concatenate into nonsense.
|
|
359
|
+
// Recording the id the loop already holds keeps them exact.
|
|
360
|
+
const sourceIdsByModulePath = new Map();
|
|
361
|
+
// the one way to add a module, so a new branch can't record only half of it
|
|
362
|
+
const pushModule = (mod, sourceId) => {
|
|
363
|
+
sourceIdsByModulePath.set(mod.path, toPosixPath(sourceId));
|
|
364
|
+
modules.push(mod);
|
|
365
|
+
};
|
|
258
366
|
// Phase 1: analyze every module (forward re-export edges land on `ModuleJson.reExports`)
|
|
259
367
|
for (const sourceFile of sourceFiles) {
|
|
260
368
|
// Failed-transform Svelte file: synthesize placeholder ModuleJson so
|
|
@@ -265,14 +373,14 @@ export const analyzeCore = (inputs) => {
|
|
|
265
373
|
const modulePath = extractPath(sourceFile.id, sourceOptions);
|
|
266
374
|
const componentName = getComponentName(modulePath);
|
|
267
375
|
const { dependencies, dependents } = extractDependencies(sourceFile, sourceOptions);
|
|
268
|
-
|
|
376
|
+
pushModule(ModuleJson.parse({
|
|
269
377
|
path: modulePath,
|
|
270
378
|
declarations: [],
|
|
271
379
|
dependencies,
|
|
272
380
|
dependents,
|
|
273
381
|
starExports: [],
|
|
274
|
-
partial: true
|
|
275
|
-
}));
|
|
382
|
+
partial: true
|
|
383
|
+
}), sourceFile.id);
|
|
276
384
|
log?.warn(`Svelte component ${componentName} marked partial (transform failed at ingest)`);
|
|
277
385
|
continue;
|
|
278
386
|
}
|
|
@@ -280,7 +388,7 @@ export const analyzeCore = (inputs) => {
|
|
|
280
388
|
const virtualFile = svelteVirtualFiles.get(sourceFile.id);
|
|
281
389
|
if (virtualFile) {
|
|
282
390
|
const modulePath = extractPath(sourceFile.id, sourceOptions);
|
|
283
|
-
const raw = analyzeSvelteModule(sourceFile, modulePath, checker, sourceOptions, diagnostics, program, virtualFile);
|
|
391
|
+
const raw = analyzeSvelteModule(sourceFile, modulePath, checker, sourceOptions, diagnostics, program, virtualFile, aliasRegistry);
|
|
284
392
|
if (raw) {
|
|
285
393
|
mod = toModuleJson(raw);
|
|
286
394
|
}
|
|
@@ -289,50 +397,100 @@ export const analyzeCore = (inputs) => {
|
|
|
289
397
|
}
|
|
290
398
|
}
|
|
291
399
|
else {
|
|
292
|
-
mod = analyzeModule(sourceFile, program, sourceOptions, diagnostics, log);
|
|
400
|
+
mod = analyzeModule(sourceFile, program, sourceOptions, diagnostics, log, aliasRegistry);
|
|
293
401
|
}
|
|
294
402
|
if (!mod)
|
|
295
403
|
continue;
|
|
296
|
-
|
|
404
|
+
pushModule(mod, sourceFile.id);
|
|
297
405
|
}
|
|
406
|
+
// Phase 1.5: gated Svelte canonicals as fill context — kept out of
|
|
407
|
+
// `mergeReExports` (their edges would back-link modules absent from
|
|
408
|
+
// output) and out of the emitted set.
|
|
409
|
+
const contextModules = contextSvelteFiles?.length
|
|
410
|
+
? analyzeContextComponents(contextSvelteFiles, modules, inputs, aliasRegistry)
|
|
411
|
+
: [];
|
|
298
412
|
// Phase 2a: build alsoExportedFrom arrays from the modules' forward edges
|
|
299
|
-
mergeReExports(modules);
|
|
413
|
+
const mergedModules = mergeReExports(modules);
|
|
300
414
|
// Phase 2b: fill component-only fields on renamed component aliases —
|
|
301
415
|
// canonical components are only fully populated after phase 1 finishes.
|
|
302
|
-
|
|
303
|
-
const
|
|
416
|
+
// Gated canonicals extend the lookup without joining the output.
|
|
417
|
+
const resolvedModules = resolveComponentAliases(mergedModules, contextModules);
|
|
418
|
+
const sortedModules = sortModules(resolvedModules);
|
|
304
419
|
// Always run duplicate detection so a `duplicate_declaration` diagnostic
|
|
305
420
|
// reaches consumers regardless of `onDuplicates`. The `onDuplicates`
|
|
306
421
|
// callback/shortcut still fires for callers that want fail-fast or custom
|
|
307
422
|
// handling — diagnostics is the data, `onDuplicates` is the action.
|
|
308
423
|
const duplicates = findDuplicates(sortedModules);
|
|
309
|
-
emitDuplicateDiagnostics(diagnostics, duplicates);
|
|
424
|
+
emitDuplicateDiagnostics(diagnostics, duplicates, sourceIdsByModulePath);
|
|
310
425
|
if (onDuplicates) {
|
|
311
426
|
dispatchOnDuplicates(onDuplicates, duplicates, log);
|
|
312
427
|
}
|
|
313
|
-
|
|
428
|
+
normalizeModulePathsInTypes(sortedModules, sourceOptions, program);
|
|
429
|
+
finalizeDiagnostics(diagnostics, {
|
|
430
|
+
projectRoot: sourceOptions.projectRoot,
|
|
431
|
+
virtualFiles: svelteVirtualFiles.values()
|
|
432
|
+
});
|
|
314
433
|
return {
|
|
315
434
|
modules: sortedModules,
|
|
316
|
-
diagnostics
|
|
435
|
+
diagnostics
|
|
317
436
|
};
|
|
318
437
|
};
|
|
319
438
|
/**
|
|
320
|
-
*
|
|
439
|
+
* Run the diagnostic boundary passes in their required order: virtual
|
|
440
|
+
* position remap first (`remapVirtualDiagnosticPositions`), then path
|
|
441
|
+
* normalization (`normalizeDiagnosticPaths` — it strips the virtual suffix
|
|
442
|
+
* the remap matches `file` against, so the reverse order silently keeps
|
|
443
|
+
* virtual positions). The one call for callers assembling modules themselves
|
|
444
|
+
* through `analyzeModule` / `analyzeSvelteModule`; using it makes the
|
|
445
|
+
* ordering unrepresentable instead of a contract to remember. Omit
|
|
446
|
+
* `virtualFiles` when no Svelte virtuals are in play.
|
|
447
|
+
*
|
|
448
|
+
* @mutates diagnostics — rewrites positions, `file`, and `message`
|
|
449
|
+
*/
|
|
450
|
+
export const finalizeDiagnostics = (diagnostics, options) => {
|
|
451
|
+
if (options.virtualFiles) {
|
|
452
|
+
remapVirtualDiagnosticPositions(diagnostics, options.virtualFiles);
|
|
453
|
+
}
|
|
454
|
+
normalizeDiagnosticPaths(diagnostics, options.projectRoot);
|
|
455
|
+
};
|
|
456
|
+
/**
|
|
457
|
+
* Normalize the paths a `Diagnostic` carries to project-root-relative form,
|
|
458
|
+
* in place.
|
|
321
459
|
*
|
|
322
460
|
* Producers inside the analysis pipeline can write absolute paths or virtual
|
|
323
461
|
* paths (svelte2tsx output like `Foo.svelte.__svelte2tsx__.ts`). This pass
|
|
324
462
|
* collapses both to the public contract: a path relative to `projectRoot`
|
|
325
|
-
* with no leading slash and no `./` prefix.
|
|
463
|
+
* with no leading slash and no `./` prefix. A file outside the root gets the
|
|
464
|
+
* `../` form, matching module paths in printed type text — and making the
|
|
465
|
+
* documented "rejoin with `projectRoot` to get an absolute path" actually
|
|
466
|
+
* hold for it, which dropping the leading slash did not.
|
|
467
|
+
*
|
|
468
|
+
* `message` gets the same treatment as `file`, by textual substitution. A
|
|
469
|
+
* message is free-form, and not every path in one comes from a field this
|
|
470
|
+
* pass can see — `import_parse_failed` wraps an es-module-lexer error that
|
|
471
|
+
* embeds the file name itself. Scrubbing here means the contract holds for
|
|
472
|
+
* the whole record rather than one field, so a producer can't reintroduce an
|
|
473
|
+
* absolute path through prose.
|
|
326
474
|
*
|
|
327
475
|
* Exposed for build-tool integrations that bypass the session and collect
|
|
328
476
|
* their own discovery/dep diagnostics — they need the same normalization to
|
|
329
|
-
* match the public contract.
|
|
477
|
+
* match the public contract. **Hand it absolute paths.** An already-relative
|
|
478
|
+
* `file` is left alone (relativizing it would resolve against `cwd`), so this
|
|
479
|
+
* pass can only correct the absolute form — a relative path on a base other
|
|
480
|
+
* than the project root passes through and ships as-is.
|
|
330
481
|
*
|
|
331
|
-
* @mutates diagnostics — rewrites each diagnostic's `file`
|
|
482
|
+
* @mutates diagnostics — rewrites each diagnostic's `file` and `message`
|
|
483
|
+
* @see `finalizeDiagnostics` — when Svelte virtuals are in play, the position
|
|
484
|
+
* remap must run before this pass (it strips the suffix the remap matches on)
|
|
332
485
|
*/
|
|
333
486
|
export const normalizeDiagnosticPaths = (diagnostics, projectRoot) => {
|
|
334
|
-
const
|
|
487
|
+
const root = projectRoot.endsWith('/') ? projectRoot.slice(0, -1) : projectRoot;
|
|
488
|
+
const prefix = root + '/';
|
|
335
489
|
for (const d of diagnostics) {
|
|
490
|
+
if (d.message.includes(prefix)) {
|
|
491
|
+
d.message = d.message.split(prefix).join('');
|
|
492
|
+
}
|
|
493
|
+
d.message = scrubVirtualSuffixes(d.message);
|
|
336
494
|
// Posixify so producers that emitted native-separator paths (e.g., a
|
|
337
495
|
// custom discovery layer building `path.relative` results on Windows)
|
|
338
496
|
// match the POSIX prefix derived from the normalized projectRoot.
|
|
@@ -340,11 +498,273 @@ export const normalizeDiagnosticPaths = (diagnostics, projectRoot) => {
|
|
|
340
498
|
if (file.startsWith(prefix)) {
|
|
341
499
|
file = file.slice(prefix.length);
|
|
342
500
|
}
|
|
343
|
-
else if (file
|
|
344
|
-
//
|
|
345
|
-
//
|
|
346
|
-
|
|
501
|
+
else if (isAbsolutePosixPath(file)) {
|
|
502
|
+
// Outside the root. Relativize rather than dropping the leading slash,
|
|
503
|
+
// which produced a string that reads as root-relative but resolves
|
|
504
|
+
// somewhere else entirely. A path already relative is left alone —
|
|
505
|
+
// `relative` would resolve it against `cwd`.
|
|
506
|
+
file = toPosixPath(relative(root, file));
|
|
347
507
|
}
|
|
348
508
|
d.file = file;
|
|
349
509
|
}
|
|
350
510
|
};
|
|
511
|
+
/**
|
|
512
|
+
* Extensions TypeScript elides when it prints a module specifier inside a type
|
|
513
|
+
* (`typeof import("…/dep")` for `…/dep.ts`), in module-resolution preference
|
|
514
|
+
* order.
|
|
515
|
+
*
|
|
516
|
+
* Order encodes preference, not match order: `elidedExtensionOf` takes the
|
|
517
|
+
* *longest* match, so `.d.ts` beats `.ts` on `b.d.ts` regardless of position.
|
|
518
|
+
* That frees the order to break the tie when two program files elide to the
|
|
519
|
+
* same path — which is the only thing standing between this pass and a
|
|
520
|
+
* dependence on `program.getSourceFiles()` iteration order, in a pass whose
|
|
521
|
+
* whole purpose is output that doesn't vary between machines.
|
|
522
|
+
*/
|
|
523
|
+
const ELIDED_MODULE_EXTENSIONS = [
|
|
524
|
+
'.ts',
|
|
525
|
+
'.tsx',
|
|
526
|
+
'.mts',
|
|
527
|
+
'.cts',
|
|
528
|
+
'.d.ts',
|
|
529
|
+
'.d.mts',
|
|
530
|
+
'.d.cts',
|
|
531
|
+
'.js',
|
|
532
|
+
'.jsx',
|
|
533
|
+
'.mjs',
|
|
534
|
+
'.cjs'
|
|
535
|
+
];
|
|
536
|
+
/** The longest elided extension `path` ends with, or `undefined`. */
|
|
537
|
+
const elidedExtensionOf = (path) => {
|
|
538
|
+
let longest;
|
|
539
|
+
for (const ext of ELIDED_MODULE_EXTENSIONS) {
|
|
540
|
+
if (path.endsWith(ext) && (longest === undefined || ext.length > longest.length)) {
|
|
541
|
+
longest = ext;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return longest;
|
|
545
|
+
};
|
|
546
|
+
const stripElidedExtension = (path) => {
|
|
547
|
+
const ext = elidedExtensionOf(path);
|
|
548
|
+
return ext === undefined ? path : path.slice(0, -ext.length);
|
|
549
|
+
};
|
|
550
|
+
/** Lower wins. Ranks two files that elide to the same path by resolution preference. */
|
|
551
|
+
const elidedExtensionRank = (path) => {
|
|
552
|
+
const ext = elidedExtensionOf(path);
|
|
553
|
+
return ext === undefined
|
|
554
|
+
? ELIDED_MODULE_EXTENSIONS.length
|
|
555
|
+
: ELIDED_MODULE_EXTENSIONS.indexOf(ext);
|
|
556
|
+
};
|
|
557
|
+
/**
|
|
558
|
+
* Matches a double-quoted absolute path inside printed type text — the
|
|
559
|
+
* checker's `import("…")` form. Deliberately not anchored to the `import(`
|
|
560
|
+
* wrapper: the pass fails open, and the resolution check downstream is the
|
|
561
|
+
* real guard.
|
|
562
|
+
*
|
|
563
|
+
* The absolute-path alternation mirrors `isAbsolutePosixPath` — same rule, but
|
|
564
|
+
* this one has to match *inside* a larger string rather than test a whole one.
|
|
565
|
+
*/
|
|
566
|
+
const QUOTED_ABSOLUTE_PATH_RE = /"((?:\/|[A-Za-z]:\/)[^"\n]*)"/g;
|
|
567
|
+
/**
|
|
568
|
+
* Keys holding author-written text or already-relative paths, never printed
|
|
569
|
+
* type text. Skipped by the walk so a doc comment, a raw `defaultValue`, or a
|
|
570
|
+
* written heritage field can't be rewritten — `extends` and `implements` are
|
|
571
|
+
* `getText()` of the written node, and `externalTypes` is written text with
|
|
572
|
+
* identifier-scoped splices (rename resolution, type-parameter substitution),
|
|
573
|
+
* never a checker print.
|
|
574
|
+
*
|
|
575
|
+
* A denylist rather than an allowlist of type-bearing keys, deliberately: the
|
|
576
|
+
* failure this pass exists to prevent is a *missed* type field, so it should
|
|
577
|
+
* fail open. A new author-text field added later gets walked, but the rewrite
|
|
578
|
+
* still only fires on a quoted absolute path resolving to a loaded file — the
|
|
579
|
+
* resolution check, not this list, is the real guard.
|
|
580
|
+
*/
|
|
581
|
+
const NON_TYPE_TEXT_KEYS = new Set([
|
|
582
|
+
'dependencies',
|
|
583
|
+
'dependents',
|
|
584
|
+
'deprecatedMessage',
|
|
585
|
+
'description',
|
|
586
|
+
'defaultValue',
|
|
587
|
+
'docComment',
|
|
588
|
+
'examples',
|
|
589
|
+
'extends',
|
|
590
|
+
'externalStarExports',
|
|
591
|
+
'externalTypes',
|
|
592
|
+
'implements',
|
|
593
|
+
'internalMessage',
|
|
594
|
+
'module',
|
|
595
|
+
'moduleComment',
|
|
596
|
+
'mutates',
|
|
597
|
+
'path',
|
|
598
|
+
'propertyDescriptions',
|
|
599
|
+
'returnDescription',
|
|
600
|
+
'seeAlso',
|
|
601
|
+
'since',
|
|
602
|
+
'specifier',
|
|
603
|
+
'starExports',
|
|
604
|
+
'throws'
|
|
605
|
+
]);
|
|
606
|
+
/**
|
|
607
|
+
* Build the printed-path → public-path mapper for one analysis run.
|
|
608
|
+
*
|
|
609
|
+
* Tiers, in order: a module in this output emits its `ModuleJson.path`; a
|
|
610
|
+
* package emits the tail after the last `node_modules/`; everything else is
|
|
611
|
+
* relative to `projectRoot`, which covers both an in-project file that emits
|
|
612
|
+
* no module and an out-of-project one (`../sibling/src/x.ts`).
|
|
613
|
+
*
|
|
614
|
+
* The relative form is not *guaranteed* stable across machines — a target with
|
|
615
|
+
* no near common ancestor gets a `../` run whose length tracks how deep
|
|
616
|
+
* `projectRoot` happens to sit. It's still strictly better than the absolute
|
|
617
|
+
* path it replaces: stable whenever the layout is (the sibling-repo and
|
|
618
|
+
* monorepo cases this actually arises from), and it never carries a home
|
|
619
|
+
* directory.
|
|
620
|
+
*/
|
|
621
|
+
const createModulePathNormalizer = (modules, options, program) => {
|
|
622
|
+
const modulePaths = new Set(modules.map((m) => m.path));
|
|
623
|
+
const programFiles = new Set();
|
|
624
|
+
// Recovers the extension the checker elided. Two program files can elide to
|
|
625
|
+
// the same path (`a.ts` beside `a.d.ts`), and picking the wrong one costs
|
|
626
|
+
// more than it looks: `byModule` is keyed on the resolved path, so a wrong
|
|
627
|
+
// pick *misses* tier 1 and silently degrades to a root-relative path. Ranked
|
|
628
|
+
// by resolution preference rather than first-wins, so the choice doesn't
|
|
629
|
+
// ride on `getSourceFiles()` order.
|
|
630
|
+
const byElidedExtension = new Map();
|
|
631
|
+
// Resolved absolute → `ModuleJson.path`, built from the program files that
|
|
632
|
+
// actually became modules. Keyed on the *resolved* path — the same form the
|
|
633
|
+
// lookup computes — so no extension guess enters tier 1. Keying on the
|
|
634
|
+
// elided form instead would collide `Foo.svelte` with a sibling
|
|
635
|
+
// `Foo.svelte.ts` (the Svelte 5 rune-module idiom), and let a non-module
|
|
636
|
+
// `.d.ts` sibling claim a module's key.
|
|
637
|
+
const byModule = new Map();
|
|
638
|
+
for (const sourceFile of program.getSourceFiles()) {
|
|
639
|
+
const fileName = toPosixPath(sourceFile.fileName);
|
|
640
|
+
programFiles.add(fileName);
|
|
641
|
+
const elided = stripElidedExtension(fileName);
|
|
642
|
+
const incumbent = byElidedExtension.get(elided);
|
|
643
|
+
if (incumbent === undefined || elidedExtensionRank(fileName) < elidedExtensionRank(incumbent)) {
|
|
644
|
+
byElidedExtension.set(elided, fileName);
|
|
645
|
+
}
|
|
646
|
+
const real = toPosixPath(stripVirtualSuffix(fileName));
|
|
647
|
+
const modulePath = extractPath(real, options);
|
|
648
|
+
if (modulePaths.has(modulePath))
|
|
649
|
+
byModule.set(real, modulePath);
|
|
650
|
+
}
|
|
651
|
+
const cache = new Map();
|
|
652
|
+
return (printed) => {
|
|
653
|
+
const cached = cache.get(printed);
|
|
654
|
+
if (cached !== undefined)
|
|
655
|
+
return cached;
|
|
656
|
+
const posix = toPosixPath(printed);
|
|
657
|
+
// Only rewrite a path naming a file this program actually loaded. A
|
|
658
|
+
// string-literal type that happens to look like one (`type P = "/usr/bin"`)
|
|
659
|
+
// resolves to nothing and is left alone.
|
|
660
|
+
const resolved = programFiles.has(posix) ? posix : byElidedExtension.get(posix);
|
|
661
|
+
if (resolved === undefined) {
|
|
662
|
+
cache.set(printed, printed);
|
|
663
|
+
return printed;
|
|
664
|
+
}
|
|
665
|
+
// Strip after resolving: the checker elides the virtual's `.ts`, so the
|
|
666
|
+
// printed form (`Foo.svelte.__svelte2tsx__`) doesn't carry the full suffix.
|
|
667
|
+
const real = toPosixPath(stripVirtualSuffix(resolved));
|
|
668
|
+
const modulePath = byModule.get(real);
|
|
669
|
+
// Last, not first: pnpm nests the real package under its store entry
|
|
670
|
+
// (`node_modules/.pnpm/pkg@1/node_modules/pkg/…`), and a hoisted monorepo
|
|
671
|
+
// dep can sit above `projectRoot` — both want the innermost segment. This
|
|
672
|
+
// tier running before the `projectRoot` one is what covers out-of-root
|
|
673
|
+
// dependencies, leaving only out-of-root *source* to fall through.
|
|
674
|
+
const packageIndex = real.lastIndexOf('/node_modules/');
|
|
675
|
+
let normalized;
|
|
676
|
+
if (modulePath !== undefined) {
|
|
677
|
+
normalized = modulePath;
|
|
678
|
+
}
|
|
679
|
+
else if (packageIndex !== -1) {
|
|
680
|
+
normalized = real.slice(packageIndex + '/node_modules/'.length);
|
|
681
|
+
}
|
|
682
|
+
else {
|
|
683
|
+
// One `relative` covers both remaining tiers: in-project files come
|
|
684
|
+
// back root-relative, out-of-project ones as `../…`. The `../` prefix
|
|
685
|
+
// is a usable marker — a `ModuleJson.path` can never start with it
|
|
686
|
+
// (out-of-root source paths throw in `normalizeSourceOptions`) and
|
|
687
|
+
// neither can a package specifier, so it reads unambiguously as
|
|
688
|
+
// "outside this project."
|
|
689
|
+
normalized = toPosixPath(relative(options.projectRoot, real));
|
|
690
|
+
}
|
|
691
|
+
cache.set(printed, normalized);
|
|
692
|
+
return normalized;
|
|
693
|
+
};
|
|
694
|
+
};
|
|
695
|
+
/**
|
|
696
|
+
* Idempotent: a rewritten string no longer matches, because every tier but the
|
|
697
|
+
* out-of-project one emits a relative path and the pattern requires a leading
|
|
698
|
+
* `/` (or drive letter). Load-bearing — phase 2 shares structure between
|
|
699
|
+
* modules (`mergeReExports`, `resolveComponentAliases` copy only what changes),
|
|
700
|
+
* so one array can be reached through more than one declaration.
|
|
701
|
+
*/
|
|
702
|
+
const replaceQuotedPaths = (text, rewrite) => {
|
|
703
|
+
if (!text.includes('"'))
|
|
704
|
+
return text;
|
|
705
|
+
return text.replace(QUOTED_ABSOLUTE_PATH_RE, (match, path) => {
|
|
706
|
+
const normalized = rewrite(path);
|
|
707
|
+
return normalized === path ? match : `"${normalized}"`;
|
|
708
|
+
});
|
|
709
|
+
};
|
|
710
|
+
const rewriteTypeText = (value, rewrite) => {
|
|
711
|
+
if (Array.isArray(value)) {
|
|
712
|
+
for (let i = 0; i < value.length; i++) {
|
|
713
|
+
const entry = value[i];
|
|
714
|
+
if (typeof entry === 'string') {
|
|
715
|
+
value[i] = replaceQuotedPaths(entry, rewrite);
|
|
716
|
+
}
|
|
717
|
+
else {
|
|
718
|
+
rewriteTypeText(entry, rewrite);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
if (value === null || typeof value !== 'object')
|
|
724
|
+
return;
|
|
725
|
+
const record = value;
|
|
726
|
+
// A `TypeJson` literal node's `text`/`value` are the author's literal type,
|
|
727
|
+
// not a printed module reference.
|
|
728
|
+
if (record.kind === 'literal')
|
|
729
|
+
return;
|
|
730
|
+
for (const key of Object.keys(record)) {
|
|
731
|
+
if (NON_TYPE_TEXT_KEYS.has(key))
|
|
732
|
+
continue;
|
|
733
|
+
const entry = record[key];
|
|
734
|
+
if (typeof entry === 'string') {
|
|
735
|
+
record[key] = replaceQuotedPaths(entry, rewrite);
|
|
736
|
+
}
|
|
737
|
+
else {
|
|
738
|
+
rewriteTypeText(entry, rewrite);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
};
|
|
742
|
+
/**
|
|
743
|
+
* Normalize the absolute module paths TypeScript embeds in printed type text,
|
|
744
|
+
* in place.
|
|
745
|
+
*
|
|
746
|
+
* The checker prints a module object as `typeof import("<absolute path>")`,
|
|
747
|
+
* which reaches output through every checker-printed field — `typeSignature`
|
|
748
|
+
* on declarations and members, `returnType`, and the `text`/`name` of a
|
|
749
|
+
* `TypeJson` node. Left alone it makes output machine-dependent (two checkouts
|
|
750
|
+
* of the same source produce different bytes), publishes local filesystem
|
|
751
|
+
* paths on any site that renders `typeSignature`, and exposes the svelte2tsx
|
|
752
|
+
* virtual suffix that `stripVirtualSuffix` exists to hide.
|
|
753
|
+
*
|
|
754
|
+
* A path that resolves to a module in this output is rewritten to that
|
|
755
|
+
* module's `ModuleJson.path`, so the string doubles as a lookup key: a
|
|
756
|
+
* consumer linkifies with `modules.find((m) => m.path === s)` and reads a miss
|
|
757
|
+
* as "not a module here." See `createModulePathNormalizer` for the remaining
|
|
758
|
+
* tiers.
|
|
759
|
+
*
|
|
760
|
+
* Runs as a whole-output pass rather than at the ~20 `typeToString` /
|
|
761
|
+
* `signatureToString` call sites, so a new printing site can't miss it.
|
|
762
|
+
*
|
|
763
|
+
* @mutates modules — rewrites printed type text on declarations and members
|
|
764
|
+
*/
|
|
765
|
+
export const normalizeModulePathsInTypes = (modules, options, program) => {
|
|
766
|
+
const rewrite = createModulePathNormalizer(modules, options, program);
|
|
767
|
+
for (const mod of modules) {
|
|
768
|
+
rewriteTypeText(mod, rewrite);
|
|
769
|
+
}
|
|
770
|
+
};
|