svelte-docinfo 0.4.1 → 0.5.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.
@@ -39,7 +39,7 @@ import { extractClassInfo } from './typescript-extract-class.js';
39
39
  */
40
40
  export const analyzeTypescriptModule = (sourceFileInfo, tsSourceFile, modulePath, checker, options, diagnostics) => {
41
41
  // Use the mid-level helper for core analysis
42
- const { moduleComment, declarations, reExports, starExports } = analyzeExports(tsSourceFile, checker, options, diagnostics);
42
+ const { moduleComment, declarations, reExports, starExports, externalReExports, externalStarExports, } = analyzeExports(tsSourceFile, checker, options, diagnostics);
43
43
  // Extract dependencies and dependents if provided
44
44
  const { dependencies, dependents } = extractDependencies(sourceFileInfo, options);
45
45
  return {
@@ -50,6 +50,8 @@ export const analyzeTypescriptModule = (sourceFileInfo, tsSourceFile, modulePath
50
50
  dependents,
51
51
  starExports,
52
52
  reExports,
53
+ externalReExports,
54
+ externalStarExports,
53
55
  };
54
56
  };
55
57
  /**
@@ -93,29 +95,19 @@ const classifyNamespaceReExport = (exportSymbol, checker, currentFileName, optio
93
95
  if ((deeplyAliased.flags & ts.SymbolFlags.ValueModule) === 0)
94
96
  return null;
95
97
  // Source module = where the deeply-resolved module symbol lives.
96
- const sourceModuleDecl = deeplyAliased.valueDeclaration ?? deeplyAliased.declarations?.[0];
97
- if (!sourceModuleDecl)
98
+ const sourceModuleFile = getPrimaryDeclarationFile(deeplyAliased);
99
+ if (!sourceModuleFile || !isSource(sourceModuleFile, options))
98
100
  return null;
99
- const sourceModuleFile = stripVirtualSuffix(sourceModuleDecl.getSourceFile().fileName);
100
- if (!isSource(sourceModuleFile, options))
101
- return null;
102
- // Origination + star-projection: export's first declaration is itself a
103
- // NamespaceExport. TypeScript shares the same NamespaceExport node across
104
- // star-projecting modules, so the defining file may not equal currentFile.
101
+ // Origination: export's first declaration is itself a NamespaceExport in
102
+ // this file. The caller's locality skip filters star-projected bindings
103
+ // before classification, but merged symbols could put a foreign
104
+ // declaration first — bail rather than misclassify.
105
105
  const exportDecl = exportSymbol.declarations?.[0];
106
106
  if (exportDecl && ts.isNamespaceExport(exportDecl)) {
107
107
  const definingFile = stripVirtualSuffix(exportDecl.getSourceFile().fileName);
108
- if (definingFile === currentFileName) {
109
- return { kind: 'origination', sourceModule: extractPath(sourceModuleFile, options) };
110
- }
111
- if (isSource(definingFile, options)) {
112
- return {
113
- kind: 'same-name',
114
- canonicalModule: extractPath(definingFile, options),
115
- sourceModule: extractPath(sourceModuleFile, options),
116
- };
117
- }
118
- return null;
108
+ if (definingFile !== currentFileName)
109
+ return null;
110
+ return { kind: 'origination', sourceModule: extractPath(sourceModuleFile, options) };
119
111
  }
120
112
  // Re-export specifier (`export {ns ...} from`). Use immediate-alias name
121
113
  // comparison for rename detection — this matches the existing non-namespace
@@ -169,6 +161,108 @@ const classifyNamespaceReExport = (exportSymbol, checker, currentFileName, optio
169
161
  sourceModule: extractPath(sourceModuleFile, options),
170
162
  };
171
163
  };
164
+ /**
165
+ * Whether any of the symbol's declarations lives in `fileName`
166
+ * (virtual-suffix-normalized).
167
+ *
168
+ * Merged symbols (module augmentation, declaration merging) can have
169
+ * declarations in several files — a symbol counts as declared in the file
170
+ * when at least one declaration is, so checking a single declaration node
171
+ * would drop locally-declared exports depending on bind order. Symbols
172
+ * without declarations are treated as declared in the file (permissive).
173
+ */
174
+ const isDeclaredInFile = (symbol, fileName) => {
175
+ const decls = symbol.declarations;
176
+ if (!decls?.length)
177
+ return true;
178
+ return decls.some((d) => stripVirtualSuffix(d.getSourceFile().fileName) === fileName);
179
+ };
180
+ /**
181
+ * The source file of a symbol's primary declaration (`valueDeclaration`,
182
+ * else the first declaration), or `undefined` for declaration-less symbols.
183
+ *
184
+ * Resolves which file "owns" a symbol for canonical-module attribution.
185
+ * Distinct from `isDeclaredInFile`, which asks whether *any* declaration
186
+ * lives in a given file — the right question for ownership tests on
187
+ * potentially-merged symbols.
188
+ */
189
+ const getPrimaryDeclarationSourceFile = (symbol) => (symbol.valueDeclaration ?? symbol.declarations?.[0])?.getSourceFile();
190
+ /**
191
+ * The virtual-suffix-normalized file name of a symbol's primary declaration,
192
+ * or `undefined` for declaration-less symbols. String form of
193
+ * `getPrimaryDeclarationSourceFile` for callers that only compare paths.
194
+ */
195
+ const getPrimaryDeclarationFile = (symbol) => {
196
+ const source = getPrimaryDeclarationSourceFile(symbol);
197
+ return source && stripVirtualSuffix(source.fileName);
198
+ };
199
+ /**
200
+ * The local export statement and binding node for an alias export symbol —
201
+ * `{node, statement}` where `node` is the `ExportSpecifier` (or, for
202
+ * `export * as ns`, the `NamespaceExport`) and `statement` its
203
+ * `ExportDeclaration`.
204
+ *
205
+ * Returns `undefined` when the statement isn't in `sourceFile`: merged
206
+ * symbols can put a foreign declaration first, and parsing JSDoc or
207
+ * positions there would attribute another module's content here.
208
+ */
209
+ const getLocalExportStatement = (exportSymbol, sourceFile) => {
210
+ const node = exportSymbol.declarations?.[0];
211
+ if (!node)
212
+ return undefined;
213
+ if (ts.isExportSpecifier(node)) {
214
+ const statement = node.parent.parent;
215
+ if (statement.getSourceFile() !== sourceFile)
216
+ return undefined;
217
+ return { node, statement };
218
+ }
219
+ if (ts.isNamespaceExport(node)) {
220
+ const statement = node.parent;
221
+ if (statement.getSourceFile() !== sourceFile)
222
+ return undefined;
223
+ return { node, statement };
224
+ }
225
+ return undefined;
226
+ };
227
+ /**
228
+ * Whether a local export statement/specifier pair is type-only — either
229
+ * statement-level (`export type {…} from`) or specifier-level
230
+ * (`export {type A} from`). Type-only names are erased at runtime.
231
+ */
232
+ const isTypeOnlyLocalExport = (local) => local.statement.isTypeOnly || (ts.isExportSpecifier(local.node) && local.node.isTypeOnly);
233
+ /**
234
+ * Synthesize a cross-file alias declaration for a renamed or documented
235
+ * same-name re-export.
236
+ *
237
+ * Svelte canonicals get a `kind: 'component'` placeholder — running
238
+ * `analyzeDeclaration` on svelte2tsx's `__SvelteComponent_` type alias would
239
+ * leak internal names; phase-2 `resolveComponentAliases` copies
240
+ * props/acceptsChildren/lang/etc. from the canonical (fill-gaps-only, so
241
+ * local JSDoc applied by the caller sticks). Everything else is analyzed in
242
+ * its own source file so the alias inherits `typeSignature`, `reactivity`,
243
+ * `docComment`, `parameters`, etc.
244
+ *
245
+ * `aliasOf.name` is the canonical's own symbol name — `'default'` for
246
+ * default-slot canonicals (renames into and out of the slot flow through
247
+ * uniformly), the filename-derived component name for Svelte. `sourceLine`
248
+ * is the local export specifier's line, not the canonical's location.
249
+ */
250
+ const synthesizeCrossFileAlias = (publicName, aliasedSymbol, originalSource, originalModule, specifierLine, checker, diagnostics, isExternalFile) => {
251
+ if (originalSource.fileName.endsWith(SVELTE_VIRTUAL_SUFFIX)) {
252
+ return {
253
+ name: publicName,
254
+ kind: 'component',
255
+ aliasOf: { module: originalModule, name: getComponentName(originalModule) },
256
+ sourceLine: specifierLine,
257
+ };
258
+ }
259
+ const { declaration: analyzed } = analyzeDeclaration(aliasedSymbol, originalSource, checker, diagnostics, isExternalFile);
260
+ const canonicalName = analyzed.name;
261
+ analyzed.name = publicName;
262
+ analyzed.aliasOf = { module: originalModule, name: canonicalName };
263
+ analyzed.sourceLine = specifierLine;
264
+ return analyzed;
265
+ };
172
266
  /**
173
267
  * Analyze all exports from a TypeScript source file.
174
268
  *
@@ -178,6 +272,8 @@ const classifyNamespaceReExport = (exportSymbol, checker, currentFileName, optio
178
272
  * - Same-name re-exports: tracked in `reExports` for `alsoExportedFrom` building
179
273
  * - Renamed re-exports: included as new declarations with `aliasOf` metadata
180
274
  * - Star exports (`export * from`): tracked in `starExports` for namespace-level info
275
+ * - Direct external re-exports: tracked in `externalReExports`/`externalStarExports`
276
+ * (specifier as written; import-then-export and source-chained forms stay silent)
181
277
  *
182
278
  * This is a mid-level function (above the individual `extract*` helpers, below `analyze`)
183
279
  * suitable for building documentation, API explorers, or analysis tools.
@@ -187,19 +283,23 @@ const classifyNamespaceReExport = (exportSymbol, checker, currentFileName, optio
187
283
  * @param checker - the TypeScript type checker
188
284
  * @param options - module source options for path extraction in re-exports
189
285
  * @param diagnostics - diagnostics collector for non-fatal issues
190
- * @returns module comment, declarations, re-exports, and star exports
286
+ * @returns module comment, declarations, re-exports (source + external), and star exports (source + external)
191
287
  */
192
288
  export const analyzeExports = (sourceFile, checker, options, diagnostics) => {
193
289
  const declarations = [];
194
290
  const reExports = [];
291
+ const externalReExports = [];
195
292
  const isExternalFile = createIsExternalFile(options);
196
293
  // Extract module-level comment
197
294
  const moduleComment = extractModuleComment(sourceFile);
198
- // Extract star exports (export * from './module')
199
- const starExports = extractStarExports(sourceFile, checker, options);
295
+ // Extract star exports (export * from './module' / 'pkg')
296
+ const { starExports, externalStarExports } = extractStarExports(sourceFile, checker, options);
200
297
  // Normalize virtual paths once (e.g., Foo.svelte.__svelte2tsx__.ts → Foo.svelte)
201
298
  // so re-export tracking matches real module paths
202
299
  const currentFileName = stripVirtualSuffix(sourceFile.fileName);
300
+ // 1-based line of a node in this file. Virtual coordinates for Svelte
301
+ // `<script module>` sources — remapped in `analyzeSvelteModule`.
302
+ const lineOf = (node) => sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
203
303
  // Get all exported symbols
204
304
  const symbol = checker.getSymbolAtLocation(sourceFile);
205
305
  if (symbol) {
@@ -208,6 +308,19 @@ export const analyzeExports = (sourceFile, checker, options, diagnostics) => {
208
308
  // Check if this is an alias (potential re-export) using the Alias flag
209
309
  const isAlias = (exportSymbol.flags & ts.SymbolFlags.Alias) !== 0;
210
310
  if (isAlias) {
311
+ // Star-projected alias bindings: `export * from './b'` projects
312
+ // b.ts's own re-export bindings into this module's export table,
313
+ // sharing the foreign declaration node (an `ExportSpecifier` or,
314
+ // for `export * as ns`, a `NamespaceExport`). Same encoding rule
315
+ // as star-projected value symbols below — `starExports` is the
316
+ // sole encoding; processing the binding here would publish
317
+ // re-export edges for statements this module's source doesn't
318
+ // contain and read the foreign statement's JSDoc as if local
319
+ // (synthesizing duplicate declarations with mis-attributed docs).
320
+ // Runs before namespace classification so star-projected
321
+ // namespace bindings are silenced uniformly.
322
+ if (!isDeclaredInFile(exportSymbol, currentFileName))
323
+ continue;
211
324
  // Namespace re-exports (`export * as ns from './x'` and re-exports
212
325
  // of such bindings) need special handling: their deeply-resolved
213
326
  // canonical is a module symbol, and `analyzeDeclaration` would fall
@@ -219,35 +332,21 @@ export const analyzeExports = (sourceFile, checker, options, diagnostics) => {
219
332
  // not `NamespaceExport`).
220
333
  const nsClass = classifyNamespaceReExport(exportSymbol, checker, currentFileName, options);
221
334
  if (nsClass) {
222
- // Locate the local export statement, if it lives in the current
223
- // source file. For origination/renamed/named-same-name, the
224
- // statement IS local. For star-projection, `exportSymbol.declarations[0]`
225
- // is the shared `NamespaceExport` node from the *defining* file —
226
- // its parent is in the defining file too, so parsing JSDoc there
227
- // would attribute another module's docs to the local re-export.
228
- // Identity-check on the source file rules star-projection out.
229
- const exportDeclNode = exportSymbol.declarations?.[0];
230
- const candidateStatement = exportDeclNode && ts.isNamespaceExport(exportDeclNode)
231
- ? exportDeclNode.parent
232
- : exportDeclNode?.parent.parent;
233
- const localExportStatement = candidateStatement &&
234
- ts.isExportDeclaration(candidateStatement) &&
235
- candidateStatement.getSourceFile() === sourceFile
236
- ? candidateStatement
237
- : undefined;
238
- const localTsdoc = localExportStatement
239
- ? parseComment(localExportStatement, sourceFile)
240
- : undefined;
335
+ // The locality skip above filters star-projected bindings before
336
+ // classification, so origination/renamed/same-name statements are
337
+ // local; `getLocalExportStatement`'s identity check stays as
338
+ // defense against merged symbols whose first declaration could be
339
+ // a foreign node.
340
+ const local = getLocalExportStatement(exportSymbol, sourceFile);
341
+ const localTsdoc = local ? parseComment(local.statement, sourceFile) : undefined;
342
+ const nsSpecifierLine = local ? lineOf(local.node) : undefined;
241
343
  if (nsClass.kind === 'origination') {
242
344
  const decl = {
243
345
  name: exportSymbol.name,
244
346
  kind: 'namespace',
245
347
  module: nsClass.sourceModule,
348
+ sourceLine: nsSpecifierLine,
246
349
  };
247
- if (localExportStatement) {
248
- const start = localExportStatement.getStart(sourceFile);
249
- decl.sourceLine = sourceFile.getLineAndCharacterOfPosition(start).line + 1;
250
- }
251
350
  if (localTsdoc) {
252
351
  applyToDeclaration(decl, localTsdoc);
253
352
  }
@@ -262,8 +361,10 @@ export const analyzeExports = (sourceFile, checker, options, diagnostics) => {
262
361
  module: nsClass.namespaceDefiningFile,
263
362
  name: nsClass.canonicalName,
264
363
  },
364
+ // Synthesized alias — the local export specifier's line,
365
+ // not the canonical's location
366
+ sourceLine: nsSpecifierLine,
265
367
  };
266
- decl.sourceLine = undefined;
267
368
  if (localTsdoc) {
268
369
  applyToDeclaration(decl, localTsdoc);
269
370
  }
@@ -286,15 +387,17 @@ export const analyzeExports = (sourceFile, checker, options, diagnostics) => {
286
387
  module: nsClass.canonicalModule,
287
388
  name: exportSymbol.name,
288
389
  },
390
+ sourceLine: nsSpecifierLine,
289
391
  };
290
- decl.sourceLine = undefined;
291
392
  applyToDeclaration(decl, localTsdoc);
292
393
  declarations.push({ declaration: decl, nodocs: !!localTsdoc.nodocs });
293
394
  }
294
395
  if (!localTsdoc?.nodocs) {
295
396
  reExports.push({
296
397
  name: exportSymbol.name,
297
- originalModule: nsClass.canonicalModule,
398
+ module: nsClass.canonicalModule,
399
+ ...(local && isTypeOnlyLocalExport(local) ? { typeOnly: true } : {}),
400
+ ...(nsSpecifierLine !== undefined ? { sourceLine: nsSpecifierLine } : {}),
298
401
  });
299
402
  }
300
403
  }
@@ -302,12 +405,18 @@ export const analyzeExports = (sourceFile, checker, options, diagnostics) => {
302
405
  }
303
406
  // This might be a re-export - use getAliasedSymbol to find the original
304
407
  const aliasedSymbol = checker.getAliasedSymbol(exportSymbol);
305
- const aliasedDecl = aliasedSymbol.valueDeclaration || aliasedSymbol.declarations?.[0];
306
- if (aliasedDecl) {
307
- const originalSource = aliasedDecl.getSourceFile();
408
+ const originalSource = getPrimaryDeclarationSourceFile(aliasedSymbol);
409
+ if (originalSource) {
308
410
  const originalFileName = stripVirtualSuffix(originalSource.fileName);
309
411
  // Check if this is a CROSS-FILE re-export (original in different file)
310
412
  if (originalFileName !== currentFileName) {
413
+ // The local export statement, shared by the source and external
414
+ // arms. JSDoc on `/** Doc */ export {...} from './x'` lives on
415
+ // the ExportDeclaration, not on the canonical's declaration in
416
+ // the foreign file.
417
+ const local = getLocalExportStatement(exportSymbol, sourceFile);
418
+ const specifierTypeOnly = local ? isTypeOnlyLocalExport(local) : false;
419
+ const specifierLine = local ? lineOf(local.node) : undefined;
311
420
  // Only track if the original is from a source module (not node_modules)
312
421
  if (isSource(originalFileName, options)) {
313
422
  const originalModule = extractPath(originalFileName, options);
@@ -318,58 +427,11 @@ export const analyzeExports = (sourceFile, checker, options, diagnostics) => {
318
427
  const immediateAlias = checker.getImmediateAliasedSymbol(exportSymbol);
319
428
  const immediateName = immediateAlias?.name ?? aliasedSymbol.name;
320
429
  const isRenamed = exportSymbol.name !== immediateName;
321
- // Look up JSDoc on the local re-export statement once for both
322
- // renamed and same-name branches. JSDoc on `/** Doc */ export {...}
323
- // from './x'` lives on the ExportDeclaration, not on the canonical's
324
- // declaration in the foreign file.
325
- const exportDecl = exportSymbol.declarations?.[0];
326
- const exportStatement = exportDecl?.parent.parent;
327
- const localTsdoc = exportStatement && ts.isExportDeclaration(exportStatement)
328
- ? parseComment(exportStatement, sourceFile)
329
- : undefined;
430
+ const localTsdoc = local ? parseComment(local.statement, sourceFile) : undefined;
330
431
  if (isRenamed) {
331
- let decl;
332
- if (originalSource.fileName.endsWith(SVELTE_VIRTUAL_SUFFIX)) {
333
- // Svelte component re-export (export {default as Foo} from './X.svelte').
334
- // `analyzeDeclaration` on the svelte2tsx-generated `__SvelteComponent_`
335
- // type would produce a `kind: 'type'` declaration with the internal name
336
- // leaking. Instead synthesize a `kind: 'component'` placeholder; phase 2
337
- // fixup in `mergeReExports` copies props/acceptsChildren/lang/etc. from
338
- // the canonical component declaration in `analyzeSvelteModule`'s output
339
- // (fill-gaps-only — local JSDoc applied below sticks).
340
- const componentName = getComponentName(originalModule);
341
- decl = {
342
- name: exportSymbol.name,
343
- kind: 'component',
344
- aliasOf: {
345
- module: originalModule,
346
- name: componentName,
347
- },
348
- };
349
- }
350
- else {
351
- // Renamed re-export (export {foo as bar}) — analyze the canonical
352
- // declaration in its own source file so the alias inherits
353
- // typeSignature, reactivity, docComment, parameters, etc.
354
- const { declaration: analyzed } = analyzeDeclaration(aliasedSymbol, originalSource, checker, diagnostics, isExternalFile);
355
- // `analyzed.name` is the canonical's symbol name — `'default'`
356
- // when the canonical was reached via `export default ...`. Both
357
- // branches (named canonical, default canonical) flow through
358
- // uniformly: `aliasOf.name` is whatever the canonical's name is.
359
- // Renames *into* the default slot (`export {foo as default}`)
360
- // likewise just take `exportSymbol.name === 'default'`.
361
- const canonicalName = analyzed.name;
362
- analyzed.name = exportSymbol.name;
363
- analyzed.aliasOf = {
364
- module: originalModule,
365
- name: canonicalName,
366
- };
367
- // The alias declaration is synthesized — it doesn't live at any
368
- // line in the re-exporting file, so clear sourceLine (which would
369
- // otherwise refer to the canonical's location).
370
- analyzed.sourceLine = undefined;
371
- decl = analyzed;
372
- }
432
+ // Renamed re-export (`export {foo as bar}`, `export {default as
433
+ // Foo} from './X.svelte'`) — synthesize the alias declaration.
434
+ const decl = synthesizeCrossFileAlias(exportSymbol.name, aliasedSymbol, originalSource, originalModule, specifierLine, checker, diagnostics, isExternalFile);
373
435
  // Local JSDoc on the export statement overrides the canonical's
374
436
  // (mirrors within-file branch semantics). `applyToDeclaration` only
375
437
  // overwrites fields the local tsdoc actually populates, so canonical
@@ -387,8 +449,7 @@ export const analyzeExports = (sourceFile, checker, options, diagnostics) => {
387
449
  // canonical (whose declaration uses the pre-rename name and
388
450
  // wouldn't match in `mergeReExports`).
389
451
  const canonical = walkSameNameCanonical(exportSymbol, immediateAlias, checker);
390
- const canonicalDecl = canonical.valueDeclaration ?? canonical.declarations?.[0];
391
- const canonicalSource = canonicalDecl?.getSourceFile();
452
+ const canonicalSource = getPrimaryDeclarationSourceFile(canonical);
392
453
  const canonicalFile = canonicalSource
393
454
  ? stripVirtualSuffix(canonicalSource.fileName)
394
455
  : originalFileName;
@@ -408,32 +469,7 @@ export const analyzeExports = (sourceFile, checker, options, diagnostics) => {
408
469
  // in the re-exporting module so the local content has a place to live.
409
470
  // Without local content, fall through to the alsoExportedFrom link only.
410
471
  if (localTsdoc) {
411
- let decl;
412
- if (originalSource.fileName.endsWith(SVELTE_VIRTUAL_SUFFIX)) {
413
- const componentName = getComponentName(originalModule);
414
- decl = {
415
- name: reExportName,
416
- kind: 'component',
417
- aliasOf: {
418
- module: originalModule,
419
- name: componentName,
420
- },
421
- };
422
- }
423
- else {
424
- const { declaration: analyzed } = analyzeDeclaration(aliasedSymbol, originalSource, checker, diagnostics, isExternalFile);
425
- // Same-name Position 3 alias: `aliasOf.name` is the canonical's
426
- // own symbol name (`'default'` for default-slot canonicals,
427
- // the original identifier otherwise).
428
- const canonicalName = analyzed.name;
429
- analyzed.name = reExportName;
430
- analyzed.aliasOf = {
431
- module: originalModule,
432
- name: canonicalName,
433
- };
434
- analyzed.sourceLine = undefined;
435
- decl = analyzed;
436
- }
472
+ const decl = synthesizeCrossFileAlias(reExportName, aliasedSymbol, originalSource, originalModule, specifierLine, checker, diagnostics, isExternalFile);
437
473
  applyToDeclaration(decl, localTsdoc);
438
474
  declarations.push({ declaration: decl, nodocs: !!localTsdoc.nodocs });
439
475
  }
@@ -446,18 +482,56 @@ export const analyzeExports = (sourceFile, checker, options, diagnostics) => {
446
482
  isSource(canonicalFile, options)) {
447
483
  reExports.push({
448
484
  name: reExportName,
449
- originalModule: extractPath(canonicalFile, options),
485
+ module: extractPath(canonicalFile, options),
486
+ ...(specifierTypeOnly ? { typeOnly: true } : {}),
487
+ ...(specifierLine !== undefined ? { sourceLine: specifierLine } : {}),
450
488
  });
451
489
  }
452
490
  }
453
491
  continue;
454
492
  }
455
- // Re-export from external module (node_modules) - skip entirely
493
+ // Re-export from an external module. Direct forms
494
+ // (`export {x} from 'pkg'`, `export * as ns from 'pkg'`) are
495
+ // captured as externalReExports — but only when the statement's
496
+ // *immediate* target is itself external: chains that reach a
497
+ // package through another source module stay silent (that module
498
+ // owns the entry), as do import-then-export forms (their
499
+ // specifier lives on an import statement, and their immediate
500
+ // alias is the local import binding).
501
+ const immediateExternal = checker.getImmediateAliasedSymbol(exportSymbol);
502
+ const immediateExternalFile = immediateExternal && getPrimaryDeclarationFile(immediateExternal);
503
+ if (!immediateExternalFile || isSource(immediateExternalFile, options))
504
+ continue;
505
+ if (!local?.statement.moduleSpecifier)
506
+ continue;
507
+ if (!ts.isStringLiteral(local.statement.moduleSpecifier))
508
+ continue;
509
+ if (parseComment(local.statement, sourceFile)?.nodocs)
510
+ continue;
511
+ const originalName = ts.isExportSpecifier(local.node)
512
+ ? local.node.propertyName?.text
513
+ : undefined;
514
+ externalReExports.push({
515
+ name: exportSymbol.name,
516
+ specifier: local.statement.moduleSpecifier.text,
517
+ ...(originalName !== undefined ? { originalName } : {}),
518
+ ...(specifierTypeOnly ? { typeOnly: true } : {}),
519
+ sourceLine: specifierLine,
520
+ });
456
521
  continue;
457
522
  }
458
523
  // Within-file alias (export { x as y }) - fall through to normal analysis
459
524
  }
460
525
  }
526
+ // Star-projected exports surface as the target module's own symbols —
527
+ // no Alias flag, declarations in a foreign file (`export * from './a'`
528
+ // merges a.ts's export table; there is no per-name alias node). Their
529
+ // encoding is `starExports`; analyzing them here would duplicate the
530
+ // canonical declaration into this module (triggering spurious
531
+ // duplicate_declaration diagnostics, with sourceLine pointing into
532
+ // the foreign file).
533
+ if (!isAlias && !isDeclaredInFile(exportSymbol, currentFileName))
534
+ continue;
461
535
  // Normal export or within-file alias - declared in this file.
462
536
  // For within-file aliases (export { x } or export { x as y }), resolve to
463
537
  // the aliased symbol so that inferDeclarationKind sees the actual declaration
@@ -478,15 +552,12 @@ export const analyzeExports = (sourceFile, checker, options, diagnostics) => {
478
552
  // The aliased symbol's declaration (e.g., svelte2tsx-generated const) may lack JSDoc,
479
553
  // but the export statement (e.g., /** Doc */ export { greet }) may have it.
480
554
  if (isAlias) {
481
- const exportDecl = exportSymbol.declarations?.[0];
482
- const exportStatement = exportDecl?.parent.parent; // ExportSpecifier → NamedExports → ExportDeclaration
483
- if (exportStatement && ts.isExportDeclaration(exportStatement)) {
484
- const exportTsdoc = parseComment(exportStatement, sourceFile);
485
- if (exportTsdoc) {
486
- applyToDeclaration(declaration, exportTsdoc);
487
- if (exportTsdoc.nodocs) {
488
- nodocs = true;
489
- }
555
+ const local = getLocalExportStatement(exportSymbol, sourceFile);
556
+ const exportTsdoc = local ? parseComment(local.statement, sourceFile) : undefined;
557
+ if (exportTsdoc) {
558
+ applyToDeclaration(declaration, exportTsdoc);
559
+ if (exportTsdoc.nodocs) {
560
+ nodocs = true;
490
561
  }
491
562
  }
492
563
  }
@@ -499,6 +570,8 @@ export const analyzeExports = (sourceFile, checker, options, diagnostics) => {
499
570
  declarations,
500
571
  reExports,
501
572
  starExports,
573
+ externalReExports,
574
+ externalStarExports,
502
575
  };
503
576
  };
504
577
  /**
@@ -621,36 +694,42 @@ const stripModuleTag = (text) => {
621
694
  return filtered.join('\n').trim();
622
695
  };
623
696
  /**
624
- * Extract star exports (`export * from './module'`) from a source file.
697
+ * Extract star exports (`export * from './module'` / `'pkg'`) from a source file.
698
+ *
699
+ * Uses the type checker to resolve module specifiers: source modules land in
700
+ * `starExports` (as `sourceRoot`-relative paths), external modules in
701
+ * `externalStarExports` (specifier as written). Unresolvable specifiers
702
+ * (missing package, typo) are silently skipped.
625
703
  *
626
- * Uses the type checker to resolve module specifiers and filters to source modules only
627
- * (excludes node_modules). Unresolvable modules (external packages) are silently skipped.
704
+ * Statement-level `@nodocs` suppresses the entry — the same rule as the other
705
+ * re-export encodings (same-name edges and renamed aliases).
628
706
  */
629
707
  const extractStarExports = (sourceFile, checker, options) => {
630
708
  const starExports = [];
709
+ const externalStarExports = [];
631
710
  for (const statement of sourceFile.statements) {
632
711
  if (ts.isExportDeclaration(statement) &&
633
712
  !statement.exportClause && // No exportClause means `export *`
634
713
  statement.moduleSpecifier &&
635
714
  ts.isStringLiteral(statement.moduleSpecifier)) {
715
+ if (parseComment(statement, sourceFile)?.nodocs)
716
+ continue;
636
717
  // Use the type checker to resolve the module - it has already resolved all imports
637
718
  // during program creation, so this leverages TypeScript's full module resolution
638
719
  const moduleSymbol = checker.getSymbolAtLocation(statement.moduleSpecifier);
639
- if (moduleSymbol) {
640
- // Get the source file from the module symbol's declarations
641
- const moduleDecl = moduleSymbol.valueDeclaration ?? moduleSymbol.declarations?.[0];
642
- if (moduleDecl) {
643
- const resolvedSource = moduleDecl.getSourceFile();
644
- // Normalize virtual paths for Svelte files
645
- const resolvedPath = stripVirtualSuffix(resolvedSource.fileName);
646
- // Only include star exports from source modules (not node_modules)
647
- if (isSource(resolvedPath, options)) {
648
- starExports.push(extractPath(resolvedPath, options));
649
- }
720
+ // Virtual paths for Svelte files are normalized by getPrimaryDeclarationFile
721
+ const resolvedPath = moduleSymbol && getPrimaryDeclarationFile(moduleSymbol);
722
+ if (resolvedPath) {
723
+ if (isSource(resolvedPath, options)) {
724
+ starExports.push(extractPath(resolvedPath, options));
725
+ }
726
+ else {
727
+ // External package — record the specifier as written
728
+ externalStarExports.push(statement.moduleSpecifier.text);
650
729
  }
651
730
  }
652
- // If module couldn't be resolved (external package, etc.), skip it
731
+ // If the module couldn't be resolved (missing package, typo), skip it
653
732
  }
654
733
  }
655
- return starExports;
734
+ return { starExports, externalStarExports };
656
735
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-docinfo",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "static analysis for TypeScript and Svelte",
5
5
  "glyph": "📜",
6
6
  "logo": "logo.svg",
@@ -51,15 +51,15 @@
51
51
  },
52
52
  "devDependencies": {
53
53
  "@fuzdev/blake3_wasm": "^0.1.1",
54
- "@fuzdev/fuz_code": "^0.45.1",
55
- "@fuzdev/fuz_css": "^0.61.1",
56
- "@fuzdev/fuz_ui": "^0.197.0",
57
- "@fuzdev/fuz_util": "^0.63.1",
58
- "@fuzdev/gro": "^0.200.0",
54
+ "@fuzdev/fuz_code": "^0.46.0",
55
+ "@fuzdev/fuz_css": "^0.62.0",
56
+ "@fuzdev/fuz_ui": "^0.202.0",
57
+ "@fuzdev/fuz_util": "^0.65.1",
58
+ "@fuzdev/gro": "^0.204.0",
59
59
  "@ryanatkn/eslint-config": "^0.12.1",
60
60
  "@sveltejs/acorn-typescript": "^1.0.9",
61
61
  "@sveltejs/adapter-static": "^3.0.10",
62
- "@sveltejs/kit": "^2.61.1",
62
+ "@sveltejs/kit": "^2.64.0",
63
63
  "@sveltejs/package": "^2.5.7",
64
64
  "@sveltejs/vite-plugin-svelte": "^6.2.4",
65
65
  "@types/estree": "^1.0.8",
@@ -72,8 +72,8 @@
72
72
  "magic-string": "^0.30.21",
73
73
  "prettier": "^3.7.4",
74
74
  "prettier-plugin-svelte": "^3.4.1",
75
- "svelte": "^5.56.0",
76
- "svelte-check": "^4.3.5",
75
+ "svelte": "^5.56.3",
76
+ "svelte-check": "^4.6.0",
77
77
  "svelte2tsx": "^0.7.55",
78
78
  "tslib": "^2.8.1",
79
79
  "typescript-eslint": "^8.48.1",