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.
- package/LICENSE +21 -0
- package/README.md +349 -0
- package/dist/analyze-core.d.ts +1386 -0
- package/dist/analyze-core.d.ts.map +1 -0
- package/dist/analyze-core.js +324 -0
- package/dist/analyze.d.ts +137 -0
- package/dist/analyze.d.ts.map +1 -0
- package/dist/analyze.js +148 -0
- package/dist/cli.d.ts +102 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +206 -0
- package/dist/concurrency.d.ts +38 -0
- package/dist/concurrency.d.ts.map +1 -0
- package/dist/concurrency.js +57 -0
- package/dist/declaration-build.d.ts +164 -0
- package/dist/declaration-build.d.ts.map +1 -0
- package/dist/declaration-build.js +15 -0
- package/dist/declaration-helpers.d.ts +169 -0
- package/dist/declaration-helpers.d.ts.map +1 -0
- package/dist/declaration-helpers.js +239 -0
- package/dist/dep-resolver.d.ts +166 -0
- package/dist/dep-resolver.d.ts.map +1 -0
- package/dist/dep-resolver.js +144 -0
- package/dist/diagnostics.d.ts +650 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +377 -0
- package/dist/discovery.d.ts +115 -0
- package/dist/discovery.d.ts.map +1 -0
- package/dist/discovery.js +126 -0
- package/dist/exports.d.ts +105 -0
- package/dist/exports.d.ts.map +1 -0
- package/dist/exports.js +274 -0
- package/dist/files.d.ts +77 -0
- package/dist/files.d.ts.map +1 -0
- package/dist/files.js +95 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/log.d.ts +32 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +11 -0
- package/dist/logo.d.ts +8 -0
- package/dist/logo.d.ts.map +1 -0
- package/dist/logo.js +9 -0
- package/dist/main.d.ts +11 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +18 -0
- package/dist/paths.d.ts +35 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +34 -0
- package/dist/postprocess.d.ts +145 -0
- package/dist/postprocess.d.ts.map +1 -0
- package/dist/postprocess.js +305 -0
- package/dist/session.d.ts +248 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +496 -0
- package/dist/source-config.d.ts +330 -0
- package/dist/source-config.d.ts.map +1 -0
- package/dist/source-config.js +356 -0
- package/dist/source.d.ts +126 -0
- package/dist/source.d.ts.map +1 -0
- package/dist/source.js +68 -0
- package/dist/svelte.d.ts +179 -0
- package/dist/svelte.d.ts.map +1 -0
- package/dist/svelte.js +898 -0
- package/dist/tsdoc.d.ts +145 -0
- package/dist/tsdoc.d.ts.map +1 -0
- package/dist/tsdoc.js +262 -0
- package/dist/types.d.ts +3381 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +588 -0
- package/dist/typescript-exports.d.ts +91 -0
- package/dist/typescript-exports.d.ts.map +1 -0
- package/dist/typescript-exports.js +656 -0
- package/dist/typescript-extract-class.d.ts +29 -0
- package/dist/typescript-extract-class.d.ts.map +1 -0
- package/dist/typescript-extract-class.js +251 -0
- package/dist/typescript-extract-function.d.ts +47 -0
- package/dist/typescript-extract-function.d.ts.map +1 -0
- package/dist/typescript-extract-function.js +108 -0
- package/dist/typescript-extract-shared.d.ts +166 -0
- package/dist/typescript-extract-shared.d.ts.map +1 -0
- package/dist/typescript-extract-shared.js +528 -0
- package/dist/typescript-extract-type-properties.d.ts +34 -0
- package/dist/typescript-extract-type-properties.d.ts.map +1 -0
- package/dist/typescript-extract-type-properties.js +241 -0
- package/dist/typescript-extract-type.d.ts +48 -0
- package/dist/typescript-extract-type.d.ts.map +1 -0
- package/dist/typescript-extract-type.js +238 -0
- package/dist/typescript-program.d.ts +211 -0
- package/dist/typescript-program.d.ts.map +1 -0
- package/dist/typescript-program.js +290 -0
- package/dist/vite.d.ts +116 -0
- package/dist/vite.d.ts.map +1 -0
- package/dist/vite.js +418 -0
- package/package.json +131 -0
- 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
|
+
};
|