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
package/dist/svelte.js ADDED
@@ -0,0 +1,898 @@
1
+ /**
2
+ * Svelte component analysis helpers.
3
+ *
4
+ * Extracts metadata from Svelte components using svelte2tsx transformations:
5
+ *
6
+ * - Component props with types and JSDoc
7
+ * - Component-level documentation
8
+ * - Type information
9
+ *
10
+ * Workflow: Transform Svelte to TypeScript via svelte2tsx, parse the transformed
11
+ * TypeScript with the TS Compiler API, extract component-level JSDoc from original source.
12
+ *
13
+ * **Svelte 5 only**: The svelte2tsx output format changed significantly between versions.
14
+ * This module requires Svelte 5+ and will throw a clear error if an older version is detected.
15
+ * There is no Svelte 4 compatibility layer.
16
+ *
17
+ * @see `typescript-exports.ts` for `analyzeExports`, `extractModuleComment`
18
+ * @see `typescript-extract-shared.ts` for `parseGenericParam`, `filterIntersectionProperties`
19
+ * @see `typescript-program.ts` for `IsExternalFile`, `createIsExternalFile`
20
+ * @see `tsdoc.ts` for `parseComment`, `applyToDeclaration`
21
+ * @see `source.ts` for `SourceFileInfo`, `getComponentName`
22
+ *
23
+ * @module
24
+ */
25
+ import { TraceMap, originalPositionFor } from '@jridgewell/trace-mapping';
26
+ import { VERSION } from 'svelte/compiler';
27
+ import { svelte2tsx } from 'svelte2tsx';
28
+ import ts from 'typescript';
29
+ import { parseComment, applyToDeclaration, hasModuleTag } from './tsdoc.js';
30
+ import { createIsExternalFile } from './typescript-program.js';
31
+ import { parseGenericParam, filterIntersectionProperties } from './typescript-extract-shared.js';
32
+ import { extractModuleComment, analyzeExports } from './typescript-exports.js';
33
+ import { getComponentName, SVELTE_VIRTUAL_SUFFIX } from './source.js';
34
+ import { extractDependencies } from './source-config.js';
35
+ import {} from './diagnostics.js';
36
+ import { toPosixPath } from './paths.js';
37
+ /**
38
+ * Pre-transform a Svelte source file via svelte2tsx.
39
+ *
40
+ * Produces a `SvelteVirtualFile` containing the transformed TypeScript content
41
+ * and source map. The virtual file can be included in a TypeScript program
42
+ * (via `createAnalysisProgram({ virtualFiles })`) so that the checker can
43
+ * resolve imported types, `<script module>` exports, and re-exports.
44
+ *
45
+ * Errors at ingest are returned via `diagnostics` rather than thrown:
46
+ * - svelte2tsx throws → `transform_failed`, `virtual: undefined`
47
+ * - source map construction fails → `source_map_failed`, virtual populated
48
+ *
49
+ * Diagnostic file paths are the original `.svelte` source ID; downstream
50
+ * normalization rewrites them to project-root-relative form.
51
+ *
52
+ * @param sourceFile - the Svelte source file with content loaded
53
+ * @returns virtual file data (or `undefined` on transform failure) plus ingest diagnostics
54
+ * @throws Error if Svelte version is below 5 (checked once on first call)
55
+ */
56
+ export const transformSvelteSource = (sourceFile) => {
57
+ assertSvelteVersion();
58
+ // Defensively posixify the id so direct callers (power users invoking
59
+ // `transformSvelteSource` outside an `AnalysisSession`) can't slip a
60
+ // backslash id through and produce a backslash `virtualPath` that
61
+ // mismatches POSIX-keyed virtual maps downstream. No-op when the session
62
+ // already posixified at ingest.
63
+ const posixId = toPosixPath(sourceFile.id);
64
+ const diagnostics = [];
65
+ const isTsFile = /lang\s*=\s*["']ts["']/.test(sourceFile.content);
66
+ let tsResult;
67
+ try {
68
+ tsResult = svelte2tsx(sourceFile.content, {
69
+ filename: posixId,
70
+ isTsFile,
71
+ emitOnTemplateError: true,
72
+ });
73
+ }
74
+ catch (err) {
75
+ diagnostics.push({
76
+ kind: 'transform_failed',
77
+ file: posixId,
78
+ message: `svelte2tsx failed to transform Svelte source: ${err instanceof Error ? err.message : String(err)}`,
79
+ severity: 'error',
80
+ });
81
+ return { virtual: undefined, diagnostics };
82
+ }
83
+ const virtualPath = posixId + SVELTE_VIRTUAL_SUFFIX;
84
+ let sourceMap = null;
85
+ try {
86
+ sourceMap = new TraceMap(tsResult.map);
87
+ }
88
+ catch (err) {
89
+ diagnostics.push({
90
+ kind: 'source_map_failed',
91
+ file: posixId,
92
+ message: `Failed to parse svelte2tsx source map: ${err instanceof Error ? err.message : String(err)}. Line/column positions for this file will reference virtual TypeScript output instead of the original Svelte source.`,
93
+ severity: 'warning',
94
+ });
95
+ }
96
+ return {
97
+ virtual: {
98
+ virtualPath,
99
+ content: tsResult.code,
100
+ sourceMap,
101
+ lang: isTsFile ? undefined : 'js',
102
+ },
103
+ diagnostics,
104
+ };
105
+ };
106
+ /**
107
+ * svelte2tsx generated identifier names (magic strings from svelte2tsx output).
108
+ * These are implementation details of svelte2tsx that we rely on for props extraction.
109
+ */
110
+ const SVELTE2TSX_IDENTIFIERS = {
111
+ /** Type alias containing component props in non-generic components. */
112
+ COMPONENT_PROPS: '$$ComponentProps',
113
+ /** Function containing props type in generic components. */
114
+ RENDER_FUNCTION: '$$render',
115
+ /** Class declaration for generic components. */
116
+ RENDER_CLASS: '__sveltets_Render',
117
+ /** Function call marking bindable props. */
118
+ BINDINGS_FUNCTION: '__sveltets_$$bindings',
119
+ /** Identifier for `$props()` rune. */
120
+ PROPS_RUNE: '$props',
121
+ /** Identifier for `$bindable()` rune. */
122
+ BINDABLE_RUNE: '$bindable',
123
+ };
124
+ /**
125
+ * Lazily validated Svelte major version.
126
+ * `null` = not yet checked, `number` = validated major version.
127
+ */
128
+ let svelteMajorVersion = null;
129
+ /**
130
+ * Assert Svelte 5+ is installed (lazy, runs once on first use).
131
+ * Throws a clear error message if an older version is detected.
132
+ */
133
+ const assertSvelteVersion = () => {
134
+ if (svelteMajorVersion !== null)
135
+ return;
136
+ const [major] = VERSION.split('.');
137
+ svelteMajorVersion = parseInt(major, 10);
138
+ if (svelteMajorVersion < 5) {
139
+ throw new Error(`Svelte ${VERSION} detected but Svelte 5+ is required for source analysis. ` +
140
+ `The svelte2tsx output format changed significantly between versions.`);
141
+ }
142
+ };
143
+ /**
144
+ * Extract generic type parameters from Svelte component.
145
+ *
146
+ * svelte2tsx preserves generic parameters from the component's `generics` attribute
147
+ * in both the `$$render` function and `__sveltets_Render` class. This function
148
+ * searches for these declarations and extracts their type parameters.
149
+ *
150
+ * @param virtualSource - the svelte2tsx transformed TypeScript source
151
+ * @returns array of `GenericParamJson`, or undefined if no generics found
152
+ */
153
+ const extractGenericParams = (virtualSource) => {
154
+ let genericParams;
155
+ // Search for $$render function or __sveltets_Render class
156
+ ts.forEachChild(virtualSource, (node) => {
157
+ // Check for function declaration named $$render
158
+ if (ts.isFunctionDeclaration(node) &&
159
+ node.name?.text === SVELTE2TSX_IDENTIFIERS.RENDER_FUNCTION) {
160
+ if (node.typeParameters?.length) {
161
+ genericParams = node.typeParameters.map(parseGenericParam);
162
+ }
163
+ }
164
+ // Check for class declaration named __sveltets_Render
165
+ else if (ts.isClassDeclaration(node) &&
166
+ node.name?.text === SVELTE2TSX_IDENTIFIERS.RENDER_CLASS) {
167
+ if (node.typeParameters?.length) {
168
+ genericParams = node.typeParameters.map(parseGenericParam);
169
+ }
170
+ }
171
+ });
172
+ return genericParams;
173
+ };
174
+ /**
175
+ * Extract the original source line for the component's `<script>` tag.
176
+ *
177
+ * Finds the `$$render` function in the virtual source and maps its position
178
+ * back to the original `.svelte` file via the source map. Falls back to line 1
179
+ * when no source map is available or the mapping fails.
180
+ */
181
+ const extractComponentSourceLine = (virtualSource, sourceMap) => {
182
+ if (sourceMap) {
183
+ for (const statement of virtualSource.statements) {
184
+ if (ts.isFunctionDeclaration(statement) &&
185
+ statement.name?.text === SVELTE2TSX_IDENTIFIERS.RENDER_FUNCTION) {
186
+ const pos = virtualSource.getLineAndCharacterOfPosition(statement.getStart());
187
+ const original = originalPositionFor(sourceMap, {
188
+ line: pos.line + 1,
189
+ column: pos.character,
190
+ });
191
+ if (original.line !== null) {
192
+ return original.line;
193
+ }
194
+ }
195
+ }
196
+ }
197
+ return 1;
198
+ };
199
+ /**
200
+ * Extract the content of the main `<script>` tag from Svelte source.
201
+ *
202
+ * Matches `<script>` with any attributes (e.g., `lang`, `generics`) but excludes
203
+ * module scripts (`<script module>`, `<script context="module">`).
204
+ *
205
+ * @returns the script tag content, or undefined if no matching script tag is found
206
+ */
207
+ export const extractScriptContent = (svelteSource) => {
208
+ // Match <script> with any attributes, excluding module scripts:
209
+ // - Svelte 5: <script module> or <script lang="ts" module>
210
+ // - Svelte 4: <script context="module"> or <script lang="ts" context="module">
211
+ // Uses global regex to skip module scripts and find the instance script
212
+ const scriptRegex = /<script(\s+[^>]*)?>([^]*?)<\/script>/gi;
213
+ let match;
214
+ while ((match = scriptRegex.exec(svelteSource)) !== null) {
215
+ const attrs = match[1] ?? '';
216
+ // Skip module scripts (Svelte 5 `module` attribute or Svelte 4 `context="module"`)
217
+ if (/\bmodule\b/.test(attrs))
218
+ continue;
219
+ return match[2];
220
+ }
221
+ return undefined;
222
+ };
223
+ /**
224
+ * Extract `@module` comment from HTML comments in Svelte source.
225
+ *
226
+ * Scans all `<!-- ... -->` comments for one containing `@module` at the
227
+ * start of a line. This allows `@component` and `@module` to coexist as
228
+ * separate HTML comments. Works for template-only components.
229
+ *
230
+ * @param svelteSource - the full Svelte source code
231
+ * @returns the cleaned module comment text, or undefined if no `@module` HTML comment found
232
+ */
233
+ export const extractHtmlModuleComment = (svelteSource) => {
234
+ const commentRegex = /<!--([^]*?)-->/g;
235
+ let match;
236
+ while ((match = commentRegex.exec(svelteSource)) !== null) {
237
+ const rawContent = match[1];
238
+ // Clean: normalize CRLF, strip leading/trailing whitespace per line, trim
239
+ const cleaned = rawContent
240
+ .replace(/\r\n/g, '\n')
241
+ .split('\n')
242
+ .map((line) => line.trim())
243
+ .join('\n')
244
+ .trim();
245
+ if (!cleaned)
246
+ continue;
247
+ // Check for `@module` as a proper tag (at start of line, not mentioned in prose)
248
+ if (!/(?:^|\n)@module\b/.test(cleaned))
249
+ continue;
250
+ // Strip the `@module` tag line and return
251
+ const lines = cleaned.split('\n');
252
+ const filtered = lines.filter((line) => !/^\s*@module\b/.test(line));
253
+ const result = filtered.join('\n').trim();
254
+ return result || undefined;
255
+ }
256
+ return undefined;
257
+ };
258
+ /**
259
+ * Extract module-level comment from Svelte script content.
260
+ *
261
+ * Parses the script content as TypeScript and delegates to `extractModuleComment`
262
+ * for the shared `@module` tag detection logic.
263
+ *
264
+ * @param scriptContent - the content of the `<script>` tag
265
+ * @returns the cleaned module comment text, or undefined if none found
266
+ */
267
+ export const extractSvelteModuleComment = (scriptContent) => {
268
+ // Parse the script content as TypeScript and reuse the shared extraction logic
269
+ const sourceFile = ts.createSourceFile('script.ts', scriptContent, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
270
+ return extractModuleComment(sourceFile);
271
+ };
272
+ /**
273
+ * Check if the original Svelte source contains an HTML `@component` comment.
274
+ *
275
+ * Only checks HTML comments (`<!-- @component ... -->`), not JSDoc in `<script>`.
276
+ * Used for duplicate `docComment` detection.
277
+ */
278
+ const hasHtmlComponentComment = (svelteSource) => {
279
+ // Match HTML comments and check for @component tag
280
+ const commentRegex = /<!--([^]*?)-->/g;
281
+ let match;
282
+ while ((match = commentRegex.exec(svelteSource)) !== null) {
283
+ const content = match[1];
284
+ if (/(?:^|\n)\s*@component\b/.test(content))
285
+ return true;
286
+ }
287
+ return false;
288
+ };
289
+ /**
290
+ * Check if the given `<script>` content has a non-`@module` JSDoc comment that
291
+ * would serve as a component `docComment`.
292
+ *
293
+ * Used for duplicate `docComment` detection — when both an HTML `@component`
294
+ * comment and a script JSDoc exist. Caller pre-extracts the script content so
295
+ * this check shares it with `extractSvelteModuleComment` further down.
296
+ */
297
+ const hasScriptDocComment = (scriptContent) => {
298
+ if (!scriptContent)
299
+ return false;
300
+ const sourceFile = ts.createSourceFile('script.ts', scriptContent, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
301
+ // Walk statements looking for a non-@module JSDoc comment
302
+ for (const statement of sourceFile.statements) {
303
+ if (ts.isVariableStatement(statement) && !hasModuleTag(statement)) {
304
+ const tsdoc = parseComment(statement, sourceFile);
305
+ if (tsdoc)
306
+ return true;
307
+ }
308
+ }
309
+ return false;
310
+ };
311
+ /**
312
+ * Extract component-level TSDoc comment from svelte2tsx transformed output.
313
+ *
314
+ * svelte2tsx places component-level JSDoc inside the `$$render()` function,
315
+ * attached to a variable statement (usually before the props destructuring).
316
+ * This function searches the AST recursively to find it.
317
+ */
318
+ const extractComponentTsdoc = (sourceFile) => {
319
+ let foundTsdoc = undefined;
320
+ // Recursively search for component-level JSDoc
321
+ function visit(node) {
322
+ if (foundTsdoc)
323
+ return; // Already found, stop searching
324
+ // Skip PropertySignature nodes - those are prop-level JSDoc, not component-level
325
+ if (ts.isPropertySignature(node)) {
326
+ return; // Don't recurse into property signatures
327
+ }
328
+ // Check for JSDoc on VariableStatement or VariableDeclaration
329
+ // Component-level JSDoc is attached to these node types
330
+ // Skip @module comments — those are module-level, not component-level
331
+ if (ts.isVariableStatement(node) || ts.isVariableDeclaration(node)) {
332
+ if (!hasModuleTag(node)) {
333
+ const tsdoc = parseComment(node, sourceFile);
334
+ if (tsdoc) {
335
+ foundTsdoc = tsdoc;
336
+ return;
337
+ }
338
+ }
339
+ }
340
+ // Continue searching child nodes
341
+ ts.forEachChild(node, visit);
342
+ }
343
+ visit(sourceFile);
344
+ return foundTsdoc;
345
+ };
346
+ /**
347
+ * Map a virtual file position back to the original `.svelte` file via source map.
348
+ *
349
+ * @returns mapped `{line, column}` (1-based), falling back to virtual file positions when unmappable
350
+ */
351
+ const mapVirtualPosition = (node, sourceFile, sourceMap) => {
352
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
353
+ if (sourceMap) {
354
+ const original = originalPositionFor(sourceMap, { line: line + 1, column: character });
355
+ if (original.line !== null) {
356
+ return { line: original.line, column: original.column + 1 };
357
+ }
358
+ }
359
+ return { line: line + 1, column: character + 1 };
360
+ };
361
+ /**
362
+ * Assemble a `ComponentPropJsonInput` from extracted metadata.
363
+ */
364
+ const assemblePropInfo = (propName, typeString, optional, propDecl, propSourceFile, propsDefaults, bindableProps, parameters) => {
365
+ const tsdoc = propDecl && propSourceFile ? parseComment(propDecl, propSourceFile) : undefined;
366
+ const result = {
367
+ name: propName,
368
+ type: typeString,
369
+ optional,
370
+ description: tsdoc?.text || undefined,
371
+ // Default value: AST first (source of truth), then @default tag (fallback)
372
+ defaultValue: propsDefaults.get(propName) ?? tsdoc?.defaultValue,
373
+ bindable: bindableProps.has(propName),
374
+ examples: tsdoc?.examples,
375
+ deprecatedMessage: tsdoc?.deprecatedMessage,
376
+ seeAlso: tsdoc?.seeAlso,
377
+ throws: tsdoc?.throws,
378
+ since: tsdoc?.since,
379
+ };
380
+ // Only set parameters when there are actual params to expose.
381
+ // Bare Snippet/Snippet<[]> doesn't set this — consumers use the type string for detection.
382
+ if (parameters && parameters.length > 0) {
383
+ result.parameters = parameters;
384
+ }
385
+ return result;
386
+ };
387
+ /**
388
+ * Extract all props-related metadata in a single AST traversal.
389
+ *
390
+ * Combines the logic from:
391
+ * - `svelteExtractBindableProps` — finds `__sveltets_$$bindings('prop1', ...)`
392
+ * - `extractProps_defaults` — finds `let { prop1 = 'value' } = $props()`
393
+ * - `svelteFindPropsTypeName` — finds `let { ... }: TypeName = $props()`
394
+ *
395
+ * @param virtualSource - the svelte2tsx transformed TypeScript source
396
+ * @returns combined metadata from single traversal
397
+ */
398
+ const extractPropsMetadata = (virtualSource) => {
399
+ const bindableProps = new Set();
400
+ const propsDefaults = new Map();
401
+ let propsTypeName;
402
+ function visit(node) {
403
+ // Extract bindable props from __sveltets_$$bindings call
404
+ if (ts.isCallExpression(node)) {
405
+ const expr = node.expression;
406
+ if (ts.isIdentifier(expr) && expr.text === SVELTE2TSX_IDENTIFIERS.BINDINGS_FUNCTION) {
407
+ // Extract string literal arguments
408
+ for (const arg of node.arguments) {
409
+ if (ts.isStringLiteral(arg)) {
410
+ bindableProps.add(arg.text);
411
+ }
412
+ }
413
+ }
414
+ }
415
+ // Extract defaults and type name from $props() call
416
+ if (ts.isVariableDeclaration(node)) {
417
+ if (node.initializer && ts.isCallExpression(node.initializer)) {
418
+ const expr = node.initializer.expression;
419
+ // Check if it's $props() call
420
+ if (ts.isIdentifier(expr) && expr.text === SVELTE2TSX_IDENTIFIERS.PROPS_RUNE) {
421
+ // Extract type annotation name
422
+ if (!propsTypeName && node.type && ts.isTypeReferenceNode(node.type)) {
423
+ if (ts.isIdentifier(node.type.typeName)) {
424
+ propsTypeName = node.type.typeName.text;
425
+ }
426
+ }
427
+ // Extract defaults from binding pattern
428
+ if (ts.isObjectBindingPattern(node.name)) {
429
+ for (const element of node.name.elements) {
430
+ if (ts.isBindingElement(element) && element.initializer) {
431
+ const propName = ts.isIdentifier(element.name) ? element.name.text : undefined;
432
+ if (!propName)
433
+ continue;
434
+ // Skip $bindable() with no args (no default), but extract argument if present
435
+ if (ts.isCallExpression(element.initializer)) {
436
+ const expr = element.initializer.expression;
437
+ if (ts.isIdentifier(expr) && expr.text === SVELTE2TSX_IDENTIFIERS.BINDABLE_RUNE) {
438
+ // Only extract if has argument: $bindable('value') → 'value'
439
+ if (element.initializer.arguments.length > 0) {
440
+ const arg = element.initializer.arguments[0];
441
+ // Skip $bindable(undefined) - same semantic as $bindable()
442
+ if (!(ts.isIdentifier(arg) && arg.text === 'undefined')) {
443
+ propsDefaults.set(propName, arg.getText());
444
+ }
445
+ }
446
+ // Skip $bindable() with no args
447
+ continue;
448
+ }
449
+ }
450
+ // Skip explicit undefined (same semantic as no default)
451
+ if (ts.isIdentifier(element.initializer) &&
452
+ element.initializer.text === 'undefined') {
453
+ continue;
454
+ }
455
+ // Regular default value
456
+ propsDefaults.set(propName, element.initializer.getText());
457
+ }
458
+ }
459
+ }
460
+ }
461
+ }
462
+ }
463
+ ts.forEachChild(node, visit);
464
+ }
465
+ visit(virtualSource);
466
+ return { bindableProps, propsDefaults, propsTypeName };
467
+ };
468
+ // ── Snippet Detection ──────────────────────────────────────────────────────
469
+ /**
470
+ * Check if a type string represents a `Snippet` type.
471
+ *
472
+ * Uses the already-resolved type string (from `checker.typeToString`) for reliable
473
+ * detection. `Snippet` is an interface (not a type alias), so `aliasSymbol` is
474
+ * not available — type string matching is the reliable detection path.
475
+ */
476
+ export const isSnippetTypeString = (typeString) => {
477
+ return typeString === 'Snippet<[]>' || typeString.startsWith('Snippet<[');
478
+ };
479
+ /**
480
+ * Extract structured parameters from a `Snippet<[...]>` type.
481
+ *
482
+ * `Snippet` is an interface, so type arguments are on `TypeReference` (accessed
483
+ * via `checker.getTypeArguments`), not on `aliasTypeArguments` (which is only
484
+ * for type aliases).
485
+ *
486
+ * Returns full `ParameterJson` input objects (with `optional` and `rest` always set)
487
+ * for runtime consistency with `extractSignatureParameters` in `typescript-extract-shared.ts`.
488
+ *
489
+ * @returns array of parameter info for the snippet's tuple type arguments,
490
+ * or `[]` for bare `Snippet` / `Snippet<[]>`
491
+ */
492
+ export const extractSnippetParameters = (snippetType, checker) => {
493
+ // Snippet<T> is an interface — type args accessed via TypeReference, not aliasTypeArguments
494
+ const typeArgs = checker.getTypeArguments(snippetType);
495
+ const tupleType = typeArgs[0];
496
+ if (!tupleType || !checker.isTupleType(tupleType))
497
+ return [];
498
+ const tupleRef = tupleType;
499
+ const elementTypes = checker.getTypeArguments(tupleRef);
500
+ if (elementTypes.length === 0)
501
+ return [];
502
+ const target = tupleRef.target;
503
+ const params = [];
504
+ for (let i = 0; i < elementTypes.length; i++) {
505
+ const elementType = elementTypes[i];
506
+ const label = target.labeledElementDeclarations?.[i];
507
+ const name = label && ts.isNamedTupleMember(label) ? label.name.text : `arg${i}`;
508
+ const optional = !!(target.elementFlags[i] & ts.ElementFlags.Optional);
509
+ params.push({ name, type: checker.typeToString(elementType), optional, rest: false });
510
+ }
511
+ return params;
512
+ };
513
+ /**
514
+ * Check if a return type string matches svelte2tsx's snippet return type pattern.
515
+ *
516
+ * svelte2tsx transforms exported snippets into arrow functions with
517
+ * `ReturnType<import('svelte').Snippet>` as the return type annotation.
518
+ * The resolved return type includes `unique symbol` (from Svelte's non-exported
519
+ * `SnippetReturn` unique symbol) intersected with the branded render message.
520
+ * We match on both parts to avoid false positives — the branded message alone
521
+ * could theoretically be crafted by user code, but the `unique symbol` intersection
522
+ * cannot since `SnippetReturn` is not exported from svelte.
523
+ */
524
+ export const isSnippetReturnType = (returnType) => {
525
+ return (returnType.includes('{@render ...} must be called with a Snippet') &&
526
+ returnType.includes('unique symbol'));
527
+ };
528
+ /**
529
+ * Synthesize a `Snippet<[...]>` type string from structured parameters.
530
+ *
531
+ * Used for `kind: 'snippet'` declarations where the raw svelte2tsx type
532
+ * is implementation noise. Produces type strings consistent with how
533
+ * the checker formats Snippet types on props.
534
+ */
535
+ export const synthesizeSnippetTypeSignature = (parameters) => {
536
+ const inner = parameters.map((p) => `${p.name}: ${p.type}`).join(', ');
537
+ return `Snippet<[${inner}]>`;
538
+ };
539
+ // ── Checker-Backed Analysis ─────────────────────────────────────────────────
540
+ /**
541
+ * Check if a symbol name is an internal svelte2tsx identifier.
542
+ *
543
+ * Filters generated identifiers that should not appear in documentation output:
544
+ * `$$ComponentProps`, `$$render`, `__sveltets_Render`, and the synthesized
545
+ * component class/type alias `<ComponentName>__SvelteComponent_`.
546
+ */
547
+ const isSvelte2tsxInternal = (name) => {
548
+ return (name.startsWith('$$') || name.startsWith('__sveltets_') || name.endsWith('__SvelteComponent_'));
549
+ };
550
+ /**
551
+ * Detect whether the props type accepts a `Snippet`-typed `children` prop.
552
+ *
553
+ * Resolves the `children` symbol on the (unfiltered) props type, strips
554
+ * `undefined` for optional props, and checks the resulting type — including
555
+ * each branch of a union — against `isSnippetTypeString`. Returns `false`
556
+ * for non-Snippet `children` (e.g. `string`) and emits a `svelte_prop_failed`
557
+ * warning when type resolution throws so the false negative is observable.
558
+ */
559
+ const detectChildrenSnippet = (propsType, propsTypeNode, checker, diagnostics, componentName, filePath) => {
560
+ const childrenSym = propsType.getProperty('children');
561
+ if (!childrenSym)
562
+ return false;
563
+ try {
564
+ let childrenType = checker.getTypeOfSymbolAtLocation(childrenSym, propsTypeNode);
565
+ if (childrenSym.flags & ts.SymbolFlags.Optional) {
566
+ childrenType = checker.getNonNullableType(childrenType);
567
+ }
568
+ if (isSnippetTypeString(checker.typeToString(childrenType)))
569
+ return true;
570
+ if (childrenType.isUnion()) {
571
+ return childrenType.types.some((t) => isSnippetTypeString(checker.typeToString(t)));
572
+ }
573
+ return false;
574
+ }
575
+ catch (err) {
576
+ diagnostics.push({
577
+ kind: 'svelte_prop_failed',
578
+ file: filePath,
579
+ message: `Failed to resolve type for "children" in ${componentName} while detecting acceptsChildren: ${err instanceof Error ? err.message : String(err)}`,
580
+ severity: 'warning',
581
+ componentName,
582
+ propName: 'children',
583
+ });
584
+ return false;
585
+ }
586
+ };
587
+ /**
588
+ * Extract props from svelte2tsx output using the TypeScript checker.
589
+ *
590
+ * Uses `checker.getTypeAtLocation()` to resolve the props type,
591
+ * including imported types that are not locally defined.
592
+ */
593
+ const extractPropsViaChecker = (virtualSource, checker, componentName, filePath, sourceMap, diagnostics, propsDefaults, bindableProps, isExternalFile) => {
594
+ // Find the $props() call and resolve its type via the checker
595
+ let propsType;
596
+ let propsTypeNode;
597
+ let propsTypeName;
598
+ const findPropsType = (node) => {
599
+ if (propsType)
600
+ return;
601
+ if (ts.isVariableDeclaration(node) &&
602
+ node.initializer &&
603
+ ts.isCallExpression(node.initializer)) {
604
+ const expr = node.initializer.expression;
605
+ if (ts.isIdentifier(expr) && expr.text === SVELTE2TSX_IDENTIFIERS.PROPS_RUNE) {
606
+ if (node.type) {
607
+ try {
608
+ propsType = checker.getTypeAtLocation(node.type);
609
+ propsTypeNode = node.type;
610
+ if (ts.isTypeReferenceNode(node.type) && ts.isIdentifier(node.type.typeName)) {
611
+ propsTypeName = node.type.typeName.text;
612
+ }
613
+ }
614
+ catch (_) {
615
+ // Fall through — propsType stays undefined
616
+ }
617
+ }
618
+ }
619
+ }
620
+ ts.forEachChild(node, findPropsType);
621
+ };
622
+ findPropsType(virtualSource);
623
+ if (!propsType || !propsTypeNode) {
624
+ // If $props() was used with a type name but the checker couldn't resolve it,
625
+ // emit a diagnostic (same as the legacy path)
626
+ if (propsTypeName) {
627
+ diagnostics.push({
628
+ kind: 'svelte_prop_failed',
629
+ file: filePath,
630
+ message: `Component "${componentName}" uses $props() with type "${propsTypeName}" but the checker could not resolve it. This may indicate an incompatible svelte2tsx version.`,
631
+ severity: 'warning',
632
+ componentName,
633
+ propName: propsTypeName,
634
+ });
635
+ }
636
+ return { props: [], acceptsChildren: false };
637
+ }
638
+ // Detect `acceptsChildren` via type inference: `children` must resolve to a
639
+ // `Snippet<...>` type. Checking the symbol name alone (the previous approach)
640
+ // misreports a non-Snippet `children` (e.g. `children: string`) as accepting
641
+ // children. The lookup runs on the unfiltered intersection so inherited
642
+ // `children` from `SvelteHTMLElements`/`DOMAttributes` (declared as `Snippet`)
643
+ // is honored even when its declaration lives in node_modules.
644
+ const allProperties = propsType.getProperties();
645
+ const acceptsChildren = detectChildrenSnippet(propsType, propsTypeNode, checker, diagnostics, componentName, filePath);
646
+ // For intersection types, filter out properties from external sources (node_modules)
647
+ const intersectionResult = filterIntersectionProperties(propsType, propsTypeNode, checker, isExternalFile);
648
+ const properties = intersectionResult?.properties ?? allProperties;
649
+ const props = [];
650
+ for (const prop of properties) {
651
+ const propDecl = prop.valueDeclaration || prop.declarations?.[0];
652
+ // Check optionality via symbol flags (computed before type resolution
653
+ // since getNonNullableType needs it to strip the `undefined` union member)
654
+ const optional = (prop.flags & ts.SymbolFlags.Optional) !== 0;
655
+ // Get type string via checker
656
+ let typeString = 'any';
657
+ let snippetParams;
658
+ try {
659
+ let propType = checker.getTypeOfSymbolAtLocation(prop, propsTypeNode);
660
+ // For optional properties, the checker includes `undefined` in the union.
661
+ // Strip it to match the declared type (e.g., `number` not `number | undefined`).
662
+ if (optional) {
663
+ propType = checker.getNonNullableType(propType);
664
+ }
665
+ typeString = checker.typeToString(propType);
666
+ // Detect Snippet type via type string, then extract structured parameters.
667
+ // After getNonNullableType, propType is the Snippet<...> TypeReference directly.
668
+ if (isSnippetTypeString(typeString)) {
669
+ snippetParams = extractSnippetParameters(propType, checker);
670
+ }
671
+ }
672
+ catch (err) {
673
+ // Map position if possible
674
+ let finalLine;
675
+ let finalColumn;
676
+ if (propDecl && sourceMap) {
677
+ const propSource = propDecl.getSourceFile();
678
+ if (propSource.fileName === virtualSource.fileName) {
679
+ ({ line: finalLine, column: finalColumn } = mapVirtualPosition(propDecl, propSource, sourceMap));
680
+ }
681
+ }
682
+ diagnostics.push({
683
+ kind: 'svelte_prop_failed',
684
+ file: filePath,
685
+ line: finalLine,
686
+ column: finalColumn,
687
+ message: `Failed to resolve type for prop "${prop.name}" in ${componentName}, falling back to 'any': ${err instanceof Error ? err.message : String(err)}`,
688
+ severity: 'warning',
689
+ componentName,
690
+ propName: prop.name,
691
+ });
692
+ }
693
+ const propSourceFile = propDecl?.getSourceFile();
694
+ props.push(assemblePropInfo(prop.name, typeString, optional, propDecl, propSourceFile, propsDefaults, bindableProps, snippetParams));
695
+ }
696
+ return { props, intersectionTypes: intersectionResult?.intersectionTypes, acceptsChildren };
697
+ };
698
+ /**
699
+ * Analyze a Svelte module using checker-backed analysis.
700
+ *
701
+ * Requires the svelte2tsx virtual output to be included in the TypeScript program
702
+ * (via `createAnalysisProgram({ virtualFiles })`). Provides full type resolution for:
703
+ * - Imported prop types (`let {x}: ImportedProps = $props()`)
704
+ * - `<script module>` exports (constants, types, re-exports)
705
+ * - Star exports and re-exports from Svelte files
706
+ *
707
+ * @param sourceFile - the original Svelte source file
708
+ * @param modulePath - module path relative to source root
709
+ * @param checker - TypeScript type checker (from the program containing virtual files)
710
+ * @param options - module source options for path extraction
711
+ * @param diagnostics - diagnostics collector for non-fatal issues
712
+ * @param program - TypeScript program containing the virtual file
713
+ * @param virtualFile - pre-transformed virtual file data
714
+ * @returns module analysis with declarations, re-exports, and star exports;
715
+ * `undefined` if the virtual file is not found in the program
716
+ */
717
+ export const analyzeSvelteModule = (sourceFile, modulePath, checker, options, diagnostics, program, virtualFile) => {
718
+ // Look up the virtual source file in the program
719
+ const virtualTsSource = program.getSourceFile(virtualFile.virtualPath);
720
+ if (!virtualTsSource) {
721
+ diagnostics.push({
722
+ kind: 'module_skipped',
723
+ file: modulePath,
724
+ message: `Virtual file not found in program: ${virtualFile.virtualPath}`,
725
+ severity: 'warning',
726
+ reason: 'not_in_program',
727
+ });
728
+ return undefined;
729
+ }
730
+ // 1. Use analyzeExports for full checker-backed analysis (same as .ts files)
731
+ const { declarations: rawDeclarations, reExports, starExports, moduleComment: scriptModuleComment, } = analyzeExports(virtualTsSource, checker, options, diagnostics);
732
+ // 2. Filter internal svelte2tsx symbols and the default export (generated component class),
733
+ // and reclassify exported snippets from 'function' to 'snippet'
734
+ const moduleDeclarations = [];
735
+ for (const d of rawDeclarations) {
736
+ // The svelte2tsx-emitted component default export (`export default Foo__SvelteComponent_`)
737
+ // surfaces here as `name: 'default'`. The component declaration itself is
738
+ // synthesized fresh below — drop the auto-generated alias.
739
+ const name = d.declaration.name;
740
+ if (name === undefined)
741
+ continue;
742
+ if (name === 'default')
743
+ continue;
744
+ if (isSvelte2tsxInternal(name))
745
+ continue;
746
+ // Reclassify exported snippets: svelte2tsx transforms {#snippet} + export
747
+ // into arrow functions with ReturnType<import('svelte').Snippet> return type.
748
+ if (d.declaration.kind === 'function' && d.declaration.returnType) {
749
+ if (isSnippetReturnType(d.declaration.returnType)) {
750
+ d.declaration.kind = 'snippet';
751
+ // Synthesize a clean Snippet type string instead of the svelte2tsx noise
752
+ const params = d.declaration.parameters ?? [];
753
+ d.declaration.typeSignature = synthesizeSnippetTypeSignature(params);
754
+ // Snippets don't have meaningful return types or overloads
755
+ delete d.declaration.returnType;
756
+ delete d.declaration.returnDescription;
757
+ delete d.declaration.overloads;
758
+ }
759
+ }
760
+ moduleDeclarations.push(d);
761
+ }
762
+ // 2b. Remap source lines for module-level exports using source map.
763
+ // analyzeExports extracts sourceLine from virtual file positions — remap to original .svelte.
764
+ if (virtualFile.sourceMap && moduleDeclarations.length > 0) {
765
+ // Build name→node map from virtual source statements
766
+ const nodesByName = new Map();
767
+ for (const stmt of virtualTsSource.statements) {
768
+ if (ts.isVariableStatement(stmt)) {
769
+ for (const decl of stmt.declarationList.declarations) {
770
+ if (ts.isIdentifier(decl.name)) {
771
+ nodesByName.set(decl.name.text, decl);
772
+ }
773
+ }
774
+ }
775
+ else if (ts.isFunctionDeclaration(stmt) && stmt.name) {
776
+ nodesByName.set(stmt.name.text, stmt);
777
+ }
778
+ else if (ts.isClassDeclaration(stmt) && stmt.name) {
779
+ nodesByName.set(stmt.name.text, stmt);
780
+ }
781
+ else if (ts.isInterfaceDeclaration(stmt)) {
782
+ nodesByName.set(stmt.name.text, stmt);
783
+ }
784
+ else if (ts.isTypeAliasDeclaration(stmt)) {
785
+ nodesByName.set(stmt.name.text, stmt);
786
+ }
787
+ else if (ts.isEnumDeclaration(stmt)) {
788
+ nodesByName.set(stmt.name.text, stmt);
789
+ }
790
+ }
791
+ for (const d of moduleDeclarations) {
792
+ if (d.declaration.name === undefined)
793
+ continue;
794
+ const node = nodesByName.get(d.declaration.name);
795
+ if (node) {
796
+ const mapped = mapVirtualPosition(node, virtualTsSource, virtualFile.sourceMap);
797
+ if (mapped.line !== undefined) {
798
+ d.declaration.sourceLine = mapped.line;
799
+ }
800
+ }
801
+ }
802
+ }
803
+ // 3. Synthesize component declaration
804
+ const componentName = getComponentName(modulePath);
805
+ const componentDecl = {
806
+ name: componentName,
807
+ kind: 'component',
808
+ };
809
+ // Propagate script language to component declaration
810
+ if (virtualFile.lang) {
811
+ componentDecl.lang = virtualFile.lang;
812
+ }
813
+ const isExternalFile = createIsExternalFile(options);
814
+ // Extract props via checker (resolves imported types)
815
+ const { bindableProps, propsDefaults } = extractPropsMetadata(virtualTsSource);
816
+ const { props, intersectionTypes, acceptsChildren: propsAcceptsChildren, } = extractPropsViaChecker(virtualTsSource, checker, componentName, modulePath, virtualFile.sourceMap, diagnostics, propsDefaults, bindableProps, isExternalFile);
817
+ if (props.length > 0) {
818
+ componentDecl.props = props;
819
+ }
820
+ if (intersectionTypes?.length) {
821
+ componentDecl.intersects = intersectionTypes;
822
+ }
823
+ // Determine acceptsChildren via two paths:
824
+ // Path A: children found in props type (from extractPropsViaChecker, before intersection filtering)
825
+ // Path B: implicit children usage in template (for components without $props() children declaration)
826
+ let acceptsChildren = propsAcceptsChildren;
827
+ if (!acceptsChildren) {
828
+ // Path B: scan virtual source for __sveltets_2_ensureSnippet(children pattern
829
+ acceptsChildren = virtualFile.content.includes('__sveltets_2_ensureSnippet(children');
830
+ }
831
+ if (acceptsChildren) {
832
+ componentDecl.acceptsChildren = true;
833
+ }
834
+ // Extract generic params
835
+ const genericParams = extractGenericParams(virtualTsSource);
836
+ if (genericParams?.length) {
837
+ componentDecl.genericParams = genericParams;
838
+ }
839
+ // Extract component-level TSDoc from virtual source
840
+ const componentTsdoc = extractComponentTsdoc(virtualTsSource);
841
+ applyToDeclaration(componentDecl, componentTsdoc);
842
+ // Extract source line via source map (maps $$render back to <script> tag)
843
+ componentDecl.sourceLine = extractComponentSourceLine(virtualTsSource, virtualFile.sourceMap);
844
+ // Extract script content once — shared by the duplicate-doc check below
845
+ // and the module comment extraction in step 4. Avoids re-running the
846
+ // `<script>` regex on the raw Svelte source twice per file.
847
+ const scriptContent = extractScriptContent(sourceFile.content);
848
+ // Warn if both HTML @component and script JSDoc provide docComment
849
+ if (componentDecl.docComment &&
850
+ hasHtmlComponentComment(sourceFile.content) &&
851
+ hasScriptDocComment(scriptContent)) {
852
+ diagnostics.push({
853
+ kind: 'duplicate_comment',
854
+ commentType: 'doc_comment',
855
+ file: modulePath,
856
+ message: 'Both HTML @component comment and JSDoc in <script> provide component documentation. Using JSDoc.',
857
+ severity: 'warning',
858
+ });
859
+ }
860
+ // 4. Extract module comment from original Svelte source (not virtual)
861
+ // Priority: instance <script> @module > <script module> @module > HTML <!-- @module -->
862
+ const instanceModuleComment = scriptContent
863
+ ? extractSvelteModuleComment(scriptContent)
864
+ : undefined;
865
+ const htmlModuleComment = extractHtmlModuleComment(sourceFile.content);
866
+ const moduleComment = instanceModuleComment ?? scriptModuleComment ?? htmlModuleComment;
867
+ // Warn if multiple @module sources exist
868
+ const moduleCommentSources = [
869
+ instanceModuleComment,
870
+ scriptModuleComment,
871
+ htmlModuleComment,
872
+ ].filter(Boolean);
873
+ if (moduleCommentSources.length > 1) {
874
+ diagnostics.push({
875
+ kind: 'duplicate_comment',
876
+ commentType: 'module_comment',
877
+ file: modulePath,
878
+ message: `Multiple @module comments found (${[instanceModuleComment && 'JSDoc in <script>', scriptModuleComment && 'JSDoc in <script module>', htmlModuleComment && 'HTML comment'].filter(Boolean).join(', ')}). Using first found.`,
879
+ severity: 'warning',
880
+ });
881
+ }
882
+ // 5. Combine: component declaration first (primary export), then <script module> exports
883
+ const allDeclarations = [
884
+ { declaration: componentDecl, nodocs: false },
885
+ ...moduleDeclarations,
886
+ ];
887
+ // 6. Extract dependencies
888
+ const { dependencies, dependents } = extractDependencies(sourceFile, options);
889
+ return {
890
+ path: modulePath,
891
+ moduleComment,
892
+ declarations: allDeclarations,
893
+ dependencies,
894
+ dependents,
895
+ starExports,
896
+ reExports,
897
+ };
898
+ };