/**
* Svelte component analysis helpers.
*
* Extracts metadata from Svelte components using svelte2tsx transformations:
*
* - Component props with types and JSDoc
* - Component-level documentation
* - Type information
*
* Workflow: Transform Svelte to TypeScript via svelte2tsx, parse the transformed
* TypeScript with the TS Compiler API, extract component-level JSDoc from original source.
*
* **Svelte 5 only**: The svelte2tsx output format changed significantly between versions.
* This module requires Svelte 5+ and will throw a clear error if an older version is detected.
* There is no Svelte 4 compatibility layer.
*
* @see `typescript-exports.ts` for `analyzeExports`, `extractModuleComment`
* @see `typescript-extract-shared.ts` for `parseGenericParam`, `filterExternalProperties`
* @see `typescript-program.ts` for `IsExternalFile`, `createIsExternalFile`
* @see `tsdoc.ts` for `parseComment`, `applyToDeclaration`
* @see `source.ts` for `SourceFileInfo`, `getComponentName`
*
* @module
*/
import {TraceMap, originalPositionFor} from '@jridgewell/trace-mapping';
import {VERSION} from 'svelte/compiler';
import {svelte2tsx} from 'svelte2tsx';
import ts from 'typescript';
import type {ComponentPropJsonInput, GenericParamJson, ParameterJsonInput} from './types.js';
import type {
DeclarationJsonBuild,
DeclarationAnalysis,
ModuleAnalysis,
} from './declaration-build.js';
import {parseComment, applyToDeclaration, type TsdocParsedComment} from './tsdoc.js';
import {type IsExternalFile, createIsExternalFile} from './typescript-program.js';
import {parseGenericParam, filterExternalProperties} from './typescript-extract-shared.js';
import {
extractModuleComment,
analyzeExports,
warnModuleCommentNodocs,
} from './typescript-exports.js';
import {type SourceFileInfo, getComponentName, SVELTE_VIRTUAL_SUFFIX} from './source.js';
import {type ModuleSourceOptions, extractDependencies} from './source-config.js';
import {type Diagnostic} from './diagnostics.js';
import {toPosixPath} from './paths.js';
/** Resolved source map type (avoids repeating the verbose `InstanceType<...>` inline). */
type SourceMap = InstanceType<typeof TraceMap>;
/**
* Pre-transformed Svelte virtual file data.
*
* Produced by `transformSvelteSource` and consumed by `analyzeSvelteModule`
* to provide checker-backed analysis of Svelte components.
*/
export interface SvelteVirtualFile {
/** Path used for the virtual file in the TypeScript program. */
virtualPath: string;
/** svelte2tsx transformed TypeScript content. */
content: string;
/** Source map for position mapping back to the original `.svelte` file. */
sourceMap: SourceMap | null;
/** Script language. `undefined` means TypeScript (default), `'js'` for JavaScript-only components. */
lang: 'js' | undefined;
}
/**
* Result of `transformSvelteSource` — a virtual file (when transform succeeded)
* plus any ingest-time diagnostics produced during the transform.
*
* `virtual` is `undefined` when svelte2tsx threw; in that case `diagnostics`
* contains a `transform_failed` entry. When the transform succeeded but source
* map construction failed, `virtual` is populated and `diagnostics` contains a
* `source_map_failed` entry. The session's owned-entry stores both — the
* virtual (or `transformFailed: true` flag) and the ingest diagnostics.
*/
export interface TransformResult {
virtual: SvelteVirtualFile | undefined;
diagnostics: Array<Diagnostic>;
}
/**
* Pre-transform a Svelte source file via svelte2tsx.
*
* Produces a `SvelteVirtualFile` containing the transformed TypeScript content
* and source map. The virtual file can be included in a TypeScript program
* (via `createAnalysisProgram({ virtualFiles })`) so that the checker can
* resolve imported types, `<script module>` exports, and re-exports.
*
* Errors at ingest are returned via `diagnostics` rather than thrown:
* - svelte2tsx throws → `transform_failed`, `virtual: undefined`
* - source map construction fails → `source_map_failed`, virtual populated
*
* Diagnostic file paths are the original `.svelte` source ID; downstream
* normalization rewrites them to project-root-relative form.
*
* @param sourceFile - the Svelte source file with content loaded
* @returns virtual file data (or `undefined` on transform failure) plus ingest diagnostics
* @throws Error if Svelte version is below 5 (checked once on first call)
*/
export const transformSvelteSource = (sourceFile: SourceFileInfo): TransformResult => {
assertSvelteVersion();
// Defensively posixify the id so direct callers (power users invoking
// `transformSvelteSource` outside an `AnalysisSession`) can't slip a
// backslash id through and produce a backslash `virtualPath` that
// mismatches POSIX-keyed virtual maps downstream. No-op when the session
// already posixified at ingest.
const posixId = toPosixPath(sourceFile.id);
const diagnostics: Array<Diagnostic> = [];
const isTsFile = /lang\s*=\s*["']ts["']/.test(sourceFile.content);
let tsResult: ReturnType<typeof svelte2tsx>;
try {
tsResult = svelte2tsx(sourceFile.content, {
filename: posixId,
isTsFile,
emitOnTemplateError: true,
});
} catch (err) {
diagnostics.push({
kind: 'transform_failed',
file: posixId,
message: `svelte2tsx failed to transform Svelte source: ${err instanceof Error ? err.message : String(err)}`,
severity: 'error',
});
return {virtual: undefined, diagnostics};
}
const virtualPath = posixId + SVELTE_VIRTUAL_SUFFIX;
let sourceMap: SourceMap | null = null;
try {
sourceMap = new TraceMap(tsResult.map as unknown as ConstructorParameters<typeof TraceMap>[0]);
} catch (err) {
diagnostics.push({
kind: 'source_map_failed',
file: posixId,
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.`,
severity: 'warning',
});
}
return {
virtual: {
virtualPath,
content: tsResult.code,
sourceMap,
lang: isTsFile ? undefined : 'js',
},
diagnostics,
};
};
/**
* svelte2tsx generated identifier names (magic strings from svelte2tsx output).
* These are implementation details of svelte2tsx that we rely on for props extraction.
*/
const SVELTE2TSX_IDENTIFIERS = {
/** Type alias containing component props in non-generic components. */
COMPONENT_PROPS: '$$ComponentProps',
/** Function containing props type in generic components. */
RENDER_FUNCTION: '$$render',
/** Class declaration for generic components. */
RENDER_CLASS: '__sveltets_Render',
/** Function call marking bindable props. */
BINDINGS_FUNCTION: '__sveltets_$$bindings',
/** Identifier for `$props()` rune. */
PROPS_RUNE: '$props',
/** Identifier for `$bindable()` rune. */
BINDABLE_RUNE: '$bindable',
} as const;
/**
* Lazily validated Svelte major version.
* `null` = not yet checked, `number` = validated major version.
*/
let svelteMajorVersion: number | null = null;
/**
* Assert Svelte 5+ is installed (lazy, runs once on first use).
* Throws a clear error message if an older version is detected.
*/
const assertSvelteVersion = (): void => {
if (svelteMajorVersion !== null) return;
const [major] = VERSION.split('.');
svelteMajorVersion = parseInt(major!, 10);
if (svelteMajorVersion < 5) {
throw new Error(
`Svelte ${VERSION} detected but Svelte 5+ is required for source analysis. ` +
`The svelte2tsx output format changed significantly between versions.`,
);
}
};
/**
* Extract generic type parameters from Svelte component.
*
* svelte2tsx preserves generic parameters from the component's `generics` attribute
* in both the `$$render` function and `__sveltets_Render` class. This function
* searches for these declarations and extracts their type parameters.
*
* @param virtualSource - the svelte2tsx transformed TypeScript source
* @returns array of `GenericParamJson`, or undefined if no generics found
*/
const extractGenericParams = (
virtualSource: ts.SourceFile,
): Array<GenericParamJson> | undefined => {
let genericParams: Array<GenericParamJson> | undefined;
// Search for $$render function or __sveltets_Render class
ts.forEachChild(virtualSource, (node) => {
// Check for function declaration named $$render
if (
ts.isFunctionDeclaration(node) &&
node.name?.text === SVELTE2TSX_IDENTIFIERS.RENDER_FUNCTION
) {
if (node.typeParameters?.length) {
genericParams = node.typeParameters.map(parseGenericParam);
}
}
// Check for class declaration named __sveltets_Render
else if (
ts.isClassDeclaration(node) &&
node.name?.text === SVELTE2TSX_IDENTIFIERS.RENDER_CLASS
) {
if (node.typeParameters?.length) {
genericParams = node.typeParameters.map(parseGenericParam);
}
}
});
return genericParams;
};
/**
* Extract the original source line for the component's `<script>` tag.
*
* Finds the `$$render` function in the virtual source and maps its position
* back to the original `.svelte` file via the source map. Falls back to line 1
* when no source map is available or the mapping fails.
*/
const extractComponentSourceLine = (
virtualSource: ts.SourceFile,
sourceMap: SourceMap | null,
): number => {
if (sourceMap) {
for (const statement of virtualSource.statements) {
if (
ts.isFunctionDeclaration(statement) &&
statement.name?.text === SVELTE2TSX_IDENTIFIERS.RENDER_FUNCTION
) {
const pos = virtualSource.getLineAndCharacterOfPosition(statement.getStart());
const original = originalPositionFor(sourceMap, {
line: pos.line + 1,
column: pos.character,
});
if (original.line !== null) {
return original.line;
}
}
}
}
return 1;
};
/**
* Extract the content of the main `<script>` tag from Svelte source.
*
* Matches `<script>` with any attributes (e.g., `lang`, `generics`) but excludes
* module scripts (`<script module>`, `<script context="module">`).
*
* @returns the script tag content, or undefined if no matching script tag is found
*/
export const extractScriptContent = (svelteSource: string): string | undefined => {
// Match <script> with any attributes, excluding module scripts:
// - Svelte 5: <script module> or <script lang="ts" module>
// - Svelte 4: <script context="module"> or <script lang="ts" context="module">
// Uses global regex to skip module scripts and find the instance script
const scriptRegex = /<script(\s+[^>]*)?>([^]*?)<\/script>/gi;
let match;
while ((match = scriptRegex.exec(svelteSource)) !== null) {
const attrs = match[1] ?? '';
// Skip module scripts (Svelte 5 `module` attribute or Svelte 4 `context="module"`)
if (/\bmodule\b/.test(attrs)) continue;
return match[2];
}
return undefined;
};
/**
* Extract `@module` comment from HTML comments in Svelte source.
*
* Scans all `<!-- ... -->` comments for one containing `@module` at the
* start of a line. This allows `@component` and `@module` to coexist as
* separate HTML comments. Works for template-only components.
*
* @param svelteSource - the full Svelte source code
* @returns the cleaned module comment text, or undefined if no `@module` HTML comment found
*/
export const extractHtmlModuleComment = (svelteSource: string): string | undefined => {
const commentRegex = /<!--([^]*?)-->/g;
let match;
while ((match = commentRegex.exec(svelteSource)) !== null) {
const rawContent = match[1]!;
// Clean: normalize CRLF, strip leading/trailing whitespace per line, trim
const cleaned = rawContent
.replace(/\r\n/g, '\n')
.split('\n')
.map((line) => line.trim())
.join('\n')
.trim();
if (!cleaned) continue;
// Check for `@module` as a proper tag (at start of line, not mentioned in prose)
if (!/(?:^|\n)@module\b/.test(cleaned)) continue;
// Strip the `@module` tag line and return
const lines = cleaned.split('\n');
const filtered = lines.filter((line) => !/^\s*@module\b/.test(line));
const result = filtered.join('\n').trim();
return result || undefined;
}
return undefined;
};
/**
* Extract module-level comment from Svelte script content.
*
* Parses the script content as TypeScript and delegates to `extractModuleComment`
* for the shared `@module` tag detection logic.
*
* @param scriptContent - the content of the `<script>` tag
* @returns the cleaned module comment text, or undefined if none found
*/
export const extractSvelteModuleComment = (scriptContent: string): string | undefined => {
// Parse the script content as TypeScript and reuse the shared extraction logic
const sourceFile = ts.createSourceFile(
'script.ts',
scriptContent,
ts.ScriptTarget.Latest,
true,
ts.ScriptKind.TS,
);
return extractModuleComment(sourceFile);
};
/**
* Check if the original Svelte source contains an HTML `@component` comment.
*
* Only checks HTML comments (`<!-- @component ... -->`), not JSDoc in `<script>`.
* Used for duplicate `docComment` detection.
*/
const hasHtmlComponentComment = (svelteSource: string): boolean => {
// Match HTML comments and check for @component tag
const commentRegex = /<!--([^]*?)-->/g;
let match;
while ((match = commentRegex.exec(svelteSource)) !== null) {
const content = match[1]!;
if (/(?:^|\n)\s*@component\b/.test(content)) return true;
}
return false;
};
/**
* Check if the given `<script>` content has a non-`@module` JSDoc comment that
* would serve as a component `docComment`.
*
* Used for duplicate `docComment` detection — when both an HTML `@component`
* comment and a script JSDoc exist. Caller pre-extracts the script content so
* this check shares it with `extractSvelteModuleComment` further down.
*/
const hasScriptDocComment = (scriptContent: string | undefined): boolean => {
if (!scriptContent) return false;
const sourceFile = ts.createSourceFile(
'script.ts',
scriptContent,
ts.ScriptTarget.Latest,
true,
ts.ScriptKind.TS,
);
// Walk statements looking for a JSDoc comment (`parseComment` filters
// `@module` blocks, so module comments don't count)
for (const statement of sourceFile.statements) {
if (ts.isVariableStatement(statement)) {
const tsdoc = parseComment(statement, sourceFile);
if (tsdoc) return true;
}
}
return false;
};
/**
* Extract component-level TSDoc comment from svelte2tsx transformed output.
*
* svelte2tsx places component-level JSDoc inside the `$$render()` function,
* attached to a variable statement (usually before the props destructuring).
* This function searches the AST recursively to find it.
*/
const extractComponentTsdoc = (sourceFile: ts.SourceFile): TsdocParsedComment | undefined => {
let foundTsdoc: TsdocParsedComment | undefined = undefined;
// Recursively search for component-level JSDoc
function visit(node: ts.Node) {
if (foundTsdoc) return; // Already found, stop searching
// Skip PropertySignature nodes - those are prop-level JSDoc, not component-level
if (ts.isPropertySignature(node)) {
return; // Don't recurse into property signatures
}
// Check for JSDoc on VariableStatement or VariableDeclaration
// Component-level JSDoc is attached to these node types
// (`parseComment` filters module-level `@module` blocks)
if (ts.isVariableStatement(node) || ts.isVariableDeclaration(node)) {
const tsdoc = parseComment(node, sourceFile);
if (tsdoc) {
foundTsdoc = tsdoc;
return;
}
}
// Continue searching child nodes
ts.forEachChild(node, visit);
}
visit(sourceFile);
return foundTsdoc;
};
/**
* Map a virtual file position back to the original `.svelte` file via source map.
*
* @returns mapped `{line, column}` (1-based), falling back to virtual file positions when unmappable
*/
const mapVirtualPosition = (
node: ts.Node,
sourceFile: ts.SourceFile,
sourceMap: SourceMap | null,
): {line: number | undefined; column: number | undefined} => {
const {line, character} = sourceFile.getLineAndCharacterOfPosition(node.getStart());
if (sourceMap) {
const original = originalPositionFor(sourceMap, {line: line + 1, column: character});
if (original.line !== null) {
return {line: original.line, column: original.column + 1};
}
}
return {line: line + 1, column: character + 1};
};
/**
* Assemble a `ComponentPropJsonInput` from extracted metadata.
*/
const assemblePropInfo = (
propName: string,
typeString: string,
optional: boolean,
propDecl: ts.Node | undefined,
propSourceFile: ts.SourceFile | undefined,
propsDefaults: Map<string, string>,
bindableProps: Set<string>,
parameters?: Array<ParameterJsonInput>,
): ComponentPropJsonInput => {
const tsdoc = propDecl && propSourceFile ? parseComment(propDecl, propSourceFile) : undefined;
const result: ComponentPropJsonInput = {
name: propName,
type: typeString,
optional,
description: tsdoc?.text || undefined,
// Default value: AST first (source of truth), then @default tag (fallback)
defaultValue: propsDefaults.get(propName) ?? tsdoc?.defaultValue,
bindable: bindableProps.has(propName),
examples: tsdoc?.examples,
deprecatedMessage: tsdoc?.deprecatedMessage,
seeAlso: tsdoc?.seeAlso,
throws: tsdoc?.throws,
since: tsdoc?.since,
};
// Only set parameters when there are actual params to expose.
// Bare Snippet/Snippet<[]> doesn't set this — consumers use the type string for detection.
if (parameters && parameters.length > 0) {
result.parameters = parameters;
}
return result;
};
/**
* Metadata extracted from $props() patterns in svelte2tsx transformed output.
*/
interface PropsMetadata {
/** Props marked with `$bindable()`. */
bindableProps: Set<string>;
/** Default values from destructuring pattern. */
propsDefaults: Map<string, string>;
/** Type/interface name referenced in `$props()` call. */
propsTypeName: string | undefined;
}
/**
* Extract all props-related metadata in a single AST traversal.
*
* Combines the logic from:
* - `svelteExtractBindableProps` — finds `__sveltets_$$bindings('prop1', ...)`
* - `extractProps_defaults` — finds `let { prop1 = 'value' } = $props()`
* - `svelteFindPropsTypeName` — finds `let { ... }: TypeName = $props()`
*
* @param virtualSource - the svelte2tsx transformed TypeScript source
* @returns combined metadata from single traversal
*/
const extractPropsMetadata = (virtualSource: ts.SourceFile): PropsMetadata => {
const bindableProps: Set<string> = new Set();
const propsDefaults: Map<string, string> = new Map();
let propsTypeName: string | undefined;
function visit(node: ts.Node) {
// Extract bindable props from __sveltets_$$bindings call
if (ts.isCallExpression(node)) {
const expr = node.expression;
if (ts.isIdentifier(expr) && expr.text === SVELTE2TSX_IDENTIFIERS.BINDINGS_FUNCTION) {
// Extract string literal arguments
for (const arg of node.arguments) {
if (ts.isStringLiteral(arg)) {
bindableProps.add(arg.text);
}
}
}
}
// Extract defaults and type name from $props() call
if (ts.isVariableDeclaration(node)) {
if (node.initializer && ts.isCallExpression(node.initializer)) {
const expr = node.initializer.expression;
// Check if it's $props() call
if (ts.isIdentifier(expr) && expr.text === SVELTE2TSX_IDENTIFIERS.PROPS_RUNE) {
// Extract type annotation name
if (!propsTypeName && node.type && ts.isTypeReferenceNode(node.type)) {
if (ts.isIdentifier(node.type.typeName)) {
propsTypeName = node.type.typeName.text;
}
}
// Extract defaults from binding pattern
if (ts.isObjectBindingPattern(node.name)) {
for (const element of node.name.elements) {
if (ts.isBindingElement(element) && element.initializer) {
const propName = ts.isIdentifier(element.name) ? element.name.text : undefined;
if (!propName) continue;
// Skip $bindable() with no args (no default), but extract argument if present
if (ts.isCallExpression(element.initializer)) {
const expr = element.initializer.expression;
if (ts.isIdentifier(expr) && expr.text === SVELTE2TSX_IDENTIFIERS.BINDABLE_RUNE) {
// Only extract if has argument: $bindable('value') → 'value'
if (element.initializer.arguments.length > 0) {
const arg = element.initializer.arguments[0]!;
// Skip $bindable(undefined) - same semantic as $bindable()
if (!(ts.isIdentifier(arg) && arg.text === 'undefined')) {
propsDefaults.set(propName, arg.getText());
}
}
// Skip $bindable() with no args
continue;
}
}
// Skip explicit undefined (same semantic as no default)
if (
ts.isIdentifier(element.initializer) &&
element.initializer.text === 'undefined'
) {
continue;
}
// Regular default value
propsDefaults.set(propName, element.initializer.getText());
}
}
}
}
}
}
ts.forEachChild(node, visit);
}
visit(virtualSource);
return {bindableProps, propsDefaults, propsTypeName};
};
// ── Snippet Detection ──────────────────────────────────────────────────────
/**
* Check if a type string represents a `Snippet` type.
*
* Uses the already-resolved type string (from `checker.typeToString`) for reliable
* detection. `Snippet` is an interface (not a type alias), so `aliasSymbol` is
* not available — type string matching is the reliable detection path.
*/
export const isSnippetTypeString = (typeString: string): boolean => {
return typeString === 'Snippet<[]>' || typeString.startsWith('Snippet<[');
};
/**
* Extract structured parameters from a `Snippet<[...]>` type.
*
* `Snippet` is an interface, so type arguments are on `TypeReference` (accessed
* via `checker.getTypeArguments`), not on `aliasTypeArguments` (which is only
* for type aliases).
*
* Returns full `ParameterJson` input objects (with `optional` and `rest` always set)
* for runtime consistency with `extractSignatureParameters` in `typescript-extract-shared.ts`.
*
* @returns array of parameter info for the snippet's tuple type arguments,
* or `[]` for bare `Snippet` / `Snippet<[]>`
*/
export const extractSnippetParameters = (
snippetType: ts.Type,
checker: ts.TypeChecker,
): Array<ParameterJsonInput> => {
// Snippet<T> is an interface — type args accessed via TypeReference, not aliasTypeArguments
const typeArgs = checker.getTypeArguments(snippetType as ts.TypeReference);
const tupleType = typeArgs[0];
if (!tupleType || !checker.isTupleType(tupleType)) return [];
const tupleRef = tupleType as ts.TypeReference;
const elementTypes = checker.getTypeArguments(tupleRef);
if (elementTypes.length === 0) return [];
const target = (tupleRef as unknown as {target: ts.TupleType}).target;
const params: Array<ParameterJsonInput> = [];
for (let i = 0; i < elementTypes.length; i++) {
const elementType = elementTypes[i]!;
const label = target.labeledElementDeclarations?.[i];
const name = label && ts.isNamedTupleMember(label) ? label.name.text : `arg${i}`;
const optional = !!(target.elementFlags[i]! & ts.ElementFlags.Optional);
params.push({name, type: checker.typeToString(elementType), optional, rest: false});
}
return params;
};
/**
* Check if a return type string matches svelte2tsx's snippet return type pattern.
*
* svelte2tsx transforms exported snippets into arrow functions with
* `ReturnType<import('svelte').Snippet>` as the return type annotation.
* The resolved return type includes `unique symbol` (from Svelte's non-exported
* `SnippetReturn` unique symbol) intersected with the branded render message.
* We match on both parts to avoid false positives — the branded message alone
* could theoretically be crafted by user code, but the `unique symbol` intersection
* cannot since `SnippetReturn` is not exported from svelte.
*/
export const isSnippetReturnType = (returnType: string): boolean => {
return (
returnType.includes('{@render ...} must be called with a Snippet') &&
returnType.includes('unique symbol')
);
};
/**
* Synthesize a `Snippet<[...]>` type string from structured parameters.
*
* Used for `kind: 'snippet'` declarations where the raw svelte2tsx type
* is implementation noise. Produces type strings consistent with how
* the checker formats Snippet types on props.
*/
export const synthesizeSnippetTypeSignature = (parameters: Array<ParameterJsonInput>): string => {
const inner = parameters.map((p) => `${p.name}: ${p.type}`).join(', ');
return `Snippet<[${inner}]>`;
};
// ── Checker-Backed Analysis ─────────────────────────────────────────────────
/**
* Check if a symbol name is an internal svelte2tsx identifier.
*
* Filters generated identifiers that should not appear in documentation output:
* `$$ComponentProps`, `$$render`, `__sveltets_Render`, and the synthesized
* component class/type alias `<ComponentName>__SvelteComponent_`.
*/
const isSvelte2tsxInternal = (name: string): boolean => {
return (
name.startsWith('$$') || name.startsWith('__sveltets_') || name.endsWith('__SvelteComponent_')
);
};
/**
* Detect whether the props type accepts a `Snippet`-typed `children` prop.
*
* Resolves the `children` symbol on the (unfiltered) props type, strips
* `undefined` for optional props, and checks the resulting type — including
* each branch of a union — against `isSnippetTypeString`. Returns `false`
* for non-Snippet `children` (e.g. `string`) and emits a `svelte_prop_failed`
* warning when type resolution throws so the false negative is observable.
*/
const detectChildrenSnippet = (
propsType: ts.Type,
propsTypeNode: ts.Node,
checker: ts.TypeChecker,
diagnostics: Array<Diagnostic>,
componentName: string,
filePath: string,
): boolean => {
const childrenSym = propsType.getProperty('children');
if (!childrenSym) return false;
try {
let childrenType = checker.getTypeOfSymbolAtLocation(childrenSym, propsTypeNode);
if (childrenSym.flags & ts.SymbolFlags.Optional) {
childrenType = checker.getNonNullableType(childrenType);
}
if (isSnippetTypeString(checker.typeToString(childrenType))) return true;
if (childrenType.isUnion()) {
return childrenType.types.some((t) => isSnippetTypeString(checker.typeToString(t)));
}
return false;
} catch (err) {
diagnostics.push({
kind: 'svelte_prop_failed',
file: filePath,
message: `Failed to resolve type for "children" in ${componentName} while detecting acceptsChildren: ${err instanceof Error ? err.message : String(err)}`,
severity: 'warning',
componentName,
propName: 'children',
});
return false;
}
};
/**
* Extract props from svelte2tsx output using the TypeScript checker.
*
* Uses `checker.getTypeAtLocation()` to resolve the props type,
* including imported types that are not locally defined.
*/
const extractPropsViaChecker = (
virtualSource: ts.SourceFile,
checker: ts.TypeChecker,
componentName: string,
filePath: string,
sourceMap: SourceMap | null,
diagnostics: Array<Diagnostic>,
propsDefaults: Map<string, string>,
bindableProps: Set<string>,
isExternalFile: IsExternalFile,
): {
props: Array<ComponentPropJsonInput>;
externalTypes?: Array<string>;
acceptsChildren: boolean;
} => {
// Find the $props() call and resolve its type via the checker
let propsType: ts.Type | undefined;
let propsTypeNode: ts.Node | undefined;
let propsTypeName: string | undefined;
const findPropsType = (node: ts.Node) => {
if (propsType) return;
if (
ts.isVariableDeclaration(node) &&
node.initializer &&
ts.isCallExpression(node.initializer)
) {
const expr = node.initializer.expression;
if (ts.isIdentifier(expr) && expr.text === SVELTE2TSX_IDENTIFIERS.PROPS_RUNE) {
if (node.type) {
try {
propsType = checker.getTypeAtLocation(node.type);
propsTypeNode = node.type;
if (ts.isTypeReferenceNode(node.type) && ts.isIdentifier(node.type.typeName)) {
propsTypeName = node.type.typeName.text;
}
} catch (_) {
// Fall through — propsType stays undefined
}
}
}
}
ts.forEachChild(node, findPropsType);
};
findPropsType(virtualSource);
if (!propsType || !propsTypeNode) {
// If $props() was used with a type name but the checker couldn't resolve it,
// emit a diagnostic (same as the legacy path)
if (propsTypeName) {
diagnostics.push({
kind: 'svelte_prop_failed',
file: filePath,
message: `Component "${componentName}" uses $props() with type "${propsTypeName}" but the checker could not resolve it. This may indicate an incompatible svelte2tsx version.`,
severity: 'warning',
componentName,
propName: propsTypeName,
});
}
return {props: [], acceptsChildren: false};
}
// Detect `acceptsChildren` via type inference: `children` must resolve to a
// `Snippet<...>` type. Checking the symbol name alone (the previous approach)
// misreports a non-Snippet `children` (e.g. `children: string`) as accepting
// children. The lookup runs on the unfiltered props type so inherited
// `children` from `SvelteHTMLElements`/`DOMAttributes` (declared as `Snippet`)
// is honored even when its declaration lives in node_modules.
const acceptsChildren = detectChildrenSnippet(
propsType,
propsTypeNode,
checker,
diagnostics,
componentName,
filePath,
);
// Drop properties contributed by external types (node_modules / svelte's
// element-attribute bags like `SvelteHTMLElements['li']`); those external
// types are summarized in `intersects` rather than enumerated as props.
const {properties, externalTypes} = filterExternalProperties(
propsType,
propsTypeNode,
checker,
isExternalFile,
);
const props: Array<ComponentPropJsonInput> = [];
for (const prop of properties) {
const propDecl = prop.valueDeclaration || prop.declarations?.[0];
// Check optionality via symbol flags (computed before type resolution
// since getNonNullableType needs it to strip the `undefined` union member)
const optional = (prop.flags & ts.SymbolFlags.Optional) !== 0;
// Get type string via checker
let typeString = 'any';
let snippetParams: Array<ParameterJsonInput> | undefined;
try {
let propType = checker.getTypeOfSymbolAtLocation(prop, propsTypeNode);
// For optional properties, the checker includes `undefined` in the union.
// Strip it to match the declared type (e.g., `number` not `number | undefined`).
if (optional) {
propType = checker.getNonNullableType(propType);
}
typeString = checker.typeToString(propType);
// Detect Snippet type via type string, then extract structured parameters.
// After getNonNullableType, propType is the Snippet<...> TypeReference directly.
if (isSnippetTypeString(typeString)) {
snippetParams = extractSnippetParameters(propType, checker);
}
} catch (err) {
// Map position if possible
let finalLine: number | undefined;
let finalColumn: number | undefined;
if (propDecl && sourceMap) {
const propSource = propDecl.getSourceFile();
if (propSource.fileName === virtualSource.fileName) {
({line: finalLine, column: finalColumn} = mapVirtualPosition(
propDecl,
propSource,
sourceMap,
));
}
}
diagnostics.push({
kind: 'svelte_prop_failed',
file: filePath,
line: finalLine,
column: finalColumn,
message: `Failed to resolve type for prop "${prop.name}" in ${componentName}, falling back to 'any': ${err instanceof Error ? err.message : String(err)}`,
severity: 'warning',
componentName,
propName: prop.name,
});
}
const propSourceFile = propDecl?.getSourceFile();
props.push(
assemblePropInfo(
prop.name,
typeString,
optional,
propDecl,
propSourceFile,
propsDefaults,
bindableProps,
snippetParams,
),
);
}
return {props, externalTypes, acceptsChildren};
};
/**
* Analyze a Svelte module using checker-backed analysis.
*
* Requires the svelte2tsx virtual output to be included in the TypeScript program
* (via `createAnalysisProgram({ virtualFiles })`). Provides full type resolution for:
* - Imported prop types (`let {x}: ImportedProps = $props()`)
* - `<script module>` exports (constants, types, re-exports)
* - Star exports and re-exports from Svelte files
*
* @param sourceFile - the original Svelte source file
* @param modulePath - module path relative to source root
* @param checker - TypeScript type checker (from the program containing virtual files)
* @param options - module source options for path extraction
* @param diagnostics - diagnostics collector for non-fatal issues
* @param program - TypeScript program containing the virtual file
* @param virtualFile - pre-transformed virtual file data
* @returns module analysis with declarations, re-exports, and star exports;
* `undefined` if the virtual file is not found in the program
*/
export const analyzeSvelteModule = (
sourceFile: SourceFileInfo & {dependents?: ReadonlyArray<string>},
modulePath: string,
checker: ts.TypeChecker,
options: ModuleSourceOptions,
diagnostics: Array<Diagnostic>,
program: ts.Program,
virtualFile: SvelteVirtualFile,
): ModuleAnalysis | undefined => {
// Look up the virtual source file in the program
const virtualTsSource = program.getSourceFile(virtualFile.virtualPath);
if (!virtualTsSource) {
diagnostics.push({
kind: 'module_skipped',
file: modulePath,
message: `Virtual file not found in program: ${virtualFile.virtualPath}`,
severity: 'warning',
reason: 'not_in_program',
});
return undefined;
}
// 1. Use analyzeExports for full checker-backed analysis (same as .ts files)
const {
declarations: rawDeclarations,
reExports,
starExports,
externalReExports,
externalStarExports,
moduleComment: scriptModuleComment,
} = analyzeExports(virtualTsSource, checker, options, diagnostics);
// 2. Filter internal svelte2tsx symbols and the default export (generated component class),
// and reclassify exported snippets from 'function' to 'snippet'
const moduleDeclarations: Array<DeclarationAnalysis> = [];
for (const d of rawDeclarations) {
// The svelte2tsx-emitted component default export (`export default Foo__SvelteComponent_`)
// surfaces here as `name: 'default'`. The component declaration itself is
// synthesized fresh below — drop the auto-generated alias.
const name = d.declaration.name;
if (name === undefined) continue;
if (name === 'default') continue;
if (isSvelte2tsxInternal(name)) continue;
// Reclassify exported snippets: svelte2tsx transforms {#snippet} + export
// into arrow functions with ReturnType<import('svelte').Snippet> return type.
if (d.declaration.kind === 'function' && d.declaration.returnType) {
if (isSnippetReturnType(d.declaration.returnType)) {
d.declaration.kind = 'snippet';
// Synthesize a clean Snippet type string instead of the svelte2tsx noise
const params = d.declaration.parameters ?? [];
d.declaration.typeSignature = synthesizeSnippetTypeSignature(params);
// Snippets don't have meaningful return types or overloads
delete d.declaration.returnType;
delete d.declaration.returnDescription;
delete d.declaration.overloads;
}
}
moduleDeclarations.push(d);
}
// 2b. Remap source lines for module-level exports using source map.
// analyzeExports extracts sourceLine from virtual file positions — remap to original .svelte.
if (virtualFile.sourceMap) {
// Build name→node map from virtual source statements
const nodesByName = new Map<string, ts.Node>();
// Export-statement bindings (specifiers + `* as ns`), kept separate:
// `const foo = 1; export {foo}` would otherwise collide — the
// declaration's line should point at the const, the synthesized
// alias's at the specifier.
const exportNodesByName = new Map<string, ts.Node>();
// Virtual line → export binding node, for re-export edges. Edges
// can't be matched by name (Svelte default-slot re-keying renames
// them), but every edge's virtual sourceLine is some binding's line.
const exportNodesByLine = new Map<number, ts.Node>();
const addExportNode = (name: string, node: ts.Node) => {
exportNodesByName.set(name, node);
const line =
virtualTsSource.getLineAndCharacterOfPosition(node.getStart(virtualTsSource)).line + 1;
if (!exportNodesByLine.has(line)) exportNodesByLine.set(line, node);
};
for (const stmt of virtualTsSource.statements) {
if (ts.isVariableStatement(stmt)) {
for (const decl of stmt.declarationList.declarations) {
if (ts.isIdentifier(decl.name)) {
nodesByName.set(decl.name.text, decl);
}
}
} else if (ts.isFunctionDeclaration(stmt) && stmt.name) {
nodesByName.set(stmt.name.text, stmt);
} else if (ts.isClassDeclaration(stmt) && stmt.name) {
nodesByName.set(stmt.name.text, stmt);
} else if (ts.isInterfaceDeclaration(stmt)) {
nodesByName.set(stmt.name.text, stmt);
} else if (ts.isTypeAliasDeclaration(stmt)) {
nodesByName.set(stmt.name.text, stmt);
} else if (ts.isEnumDeclaration(stmt)) {
nodesByName.set(stmt.name.text, stmt);
} else if (ts.isExportDeclaration(stmt)) {
if (stmt.exportClause && ts.isNamedExports(stmt.exportClause)) {
for (const spec of stmt.exportClause.elements) {
addExportNode(spec.name.text, spec);
}
} else if (stmt.exportClause && ts.isNamespaceExport(stmt.exportClause)) {
addExportNode(stmt.exportClause.name.text, stmt.exportClause);
}
}
}
for (const d of moduleDeclarations) {
if (d.declaration.name === undefined) continue;
// Synthesized aliases and namespace declarations point at their
// export statement; everything else at its value declaration
const fromExportStatement =
d.declaration.aliasOf !== undefined || d.declaration.kind === 'namespace';
const node = fromExportStatement
? exportNodesByName.get(d.declaration.name)
: nodesByName.get(d.declaration.name);
if (node) {
const mapped = mapVirtualPosition(node, virtualTsSource, virtualFile.sourceMap);
if (mapped.line !== undefined) {
d.declaration.sourceLine = mapped.line;
}
}
}
// Remap edge lines by virtual line (name-independent — see above)
for (const edge of [...reExports, ...externalReExports]) {
if (edge.sourceLine === undefined) continue;
const node = exportNodesByLine.get(edge.sourceLine);
if (node) {
const mapped = mapVirtualPosition(node, virtualTsSource, virtualFile.sourceMap);
if (mapped.line !== undefined) {
edge.sourceLine = mapped.line;
}
}
}
}
// 3. Synthesize component declaration
const componentName = getComponentName(modulePath);
const componentDecl: DeclarationJsonBuild = {
name: componentName,
kind: 'component',
};
// Propagate script language to component declaration
if (virtualFile.lang) {
componentDecl.lang = virtualFile.lang;
}
const isExternalFile = createIsExternalFile(options);
// Extract props via checker (resolves imported types)
const {bindableProps, propsDefaults} = extractPropsMetadata(virtualTsSource);
const {
props,
externalTypes,
acceptsChildren: propsAcceptsChildren,
} = extractPropsViaChecker(
virtualTsSource,
checker,
componentName,
modulePath,
virtualFile.sourceMap,
diagnostics,
propsDefaults,
bindableProps,
isExternalFile,
);
if (props.length > 0) {
componentDecl.props = props;
}
if (externalTypes?.length) {
componentDecl.intersects = externalTypes;
}
// Determine acceptsChildren via two paths:
// Path A: children found in props type (from extractPropsViaChecker, before external-property filtering)
// Path B: implicit children usage in template (for components without $props() children declaration)
let acceptsChildren = propsAcceptsChildren;
if (!acceptsChildren) {
// Path B: scan virtual source for __sveltets_2_ensureSnippet(children pattern
acceptsChildren = virtualFile.content.includes('__sveltets_2_ensureSnippet(children');
}
if (acceptsChildren) {
componentDecl.acceptsChildren = true;
}
// Extract generic params
const genericParams = extractGenericParams(virtualTsSource);
if (genericParams?.length) {
componentDecl.genericParams = genericParams;
}
// Extract component-level TSDoc from virtual source
const componentTsdoc = extractComponentTsdoc(virtualTsSource);
applyToDeclaration(componentDecl, componentTsdoc);
// Extract source line via source map (maps $$render back to <script> tag)
componentDecl.sourceLine = extractComponentSourceLine(virtualTsSource, virtualFile.sourceMap);
// Extract script content once — shared by the duplicate-doc check below
// and the module comment extraction in step 4. Avoids re-running the
// `<script>` regex on the raw Svelte source twice per file.
const scriptContent = extractScriptContent(sourceFile.content);
// Warn if both HTML @component and script JSDoc provide docComment
if (
componentDecl.docComment &&
hasHtmlComponentComment(sourceFile.content) &&
hasScriptDocComment(scriptContent)
) {
diagnostics.push({
kind: 'duplicate_comment',
commentType: 'doc_comment',
file: modulePath,
message:
'Both HTML @component comment and JSDoc in <script> provide component documentation. Using JSDoc.',
severity: 'warning',
});
}
// 4. Extract module comment from original Svelte source (not virtual)
// Priority: instance <script> @module > <script module> @module > HTML <!-- @module -->
const instanceModuleComment = scriptContent
? extractSvelteModuleComment(scriptContent)
: undefined;
const htmlModuleComment = extractHtmlModuleComment(sourceFile.content);
const moduleComment = instanceModuleComment ?? scriptModuleComment ?? htmlModuleComment;
// @nodocs has no module-level meaning — warn per misplaced comment. The
// <script module> source is covered by analyzeExports on the virtual above.
warnModuleCommentNodocs(instanceModuleComment, modulePath, diagnostics);
warnModuleCommentNodocs(htmlModuleComment, modulePath, diagnostics);
// Warn if multiple @module sources exist
const moduleCommentSources = [
instanceModuleComment,
scriptModuleComment,
htmlModuleComment,
].filter(Boolean);
if (moduleCommentSources.length > 1) {
diagnostics.push({
kind: 'duplicate_comment',
commentType: 'module_comment',
file: modulePath,
message: `Multiple @module comments found (${[instanceModuleComment && 'JSDoc in <script>', scriptModuleComment && 'JSDoc in <script module>', htmlModuleComment && 'HTML comment'].filter(Boolean).join(', ')}). Using first found.`,
severity: 'warning',
});
}
// 5. Combine: component declaration first (primary export), then <script module> exports
const allDeclarations: Array<DeclarationAnalysis> = [
{declaration: componentDecl, nodocs: false},
...moduleDeclarations,
];
// 6. Extract dependencies
const {dependencies, dependents} = extractDependencies(sourceFile, options);
return {
path: modulePath,
moduleComment,
declarations: allDeclarations,
dependencies,
dependents,
starExports,
reExports,
externalReExports,
externalStarExports,
};
};
{
"path": "svelte.ts",
"declarations": [
{
"name": "SvelteVirtualFile",
"kind": "interface",
"docComment": "Pre-transformed Svelte virtual file data.\n\nProduced by `transformSvelteSource` and consumed by `analyzeSvelteModule`\nto provide checker-backed analysis of Svelte components.",
"typeSignature": "SvelteVirtualFile",
"sourceLine": 59,
"members": [
{
"name": "virtualPath",
"kind": "variable",
"docComment": "Path used for the virtual file in the TypeScript program.",
"typeSignature": "string"
},
{
"name": "content",
"kind": "variable",
"docComment": "svelte2tsx transformed TypeScript content.",
"typeSignature": "string"
},
{
"name": "sourceMap",
"kind": "variable",
"docComment": "Source map for position mapping back to the original `.svelte` file.",
"typeSignature": "SourceMap | null"
},
{
"name": "lang",
"kind": "variable",
"docComment": "Script language. `undefined` means TypeScript (default), `'js'` for JavaScript-only components.",
"typeSignature": "'js' | undefined"
}
]
},
{
"name": "TransformResult",
"kind": "interface",
"docComment": "Result of `transformSvelteSource` — a virtual file (when transform succeeded)\nplus any ingest-time diagnostics produced during the transform.\n\n`virtual` is `undefined` when svelte2tsx threw; in that case `diagnostics`\ncontains a `transform_failed` entry. When the transform succeeded but source\nmap construction failed, `virtual` is populated and `diagnostics` contains a\n`source_map_failed` entry. The session's owned-entry stores both — the\nvirtual (or `transformFailed: true` flag) and the ingest diagnostics.",
"typeSignature": "TransformResult",
"sourceLine": 80,
"members": [
{
"name": "virtual",
"kind": "variable",
"typeSignature": "SvelteVirtualFile | undefined"
},
{
"name": "diagnostics",
"kind": "variable",
"typeSignature": "Array<Diagnostic>"
}
]
},
{
"name": "transformSvelteSource",
"kind": "function",
"docComment": "Pre-transform a Svelte source file via svelte2tsx.\n\nProduces a `SvelteVirtualFile` containing the transformed TypeScript content\nand source map. The virtual file can be included in a TypeScript program\n(via `createAnalysisProgram({ virtualFiles })`) so that the checker can\nresolve imported types, `<script module>` exports, and re-exports.\n\nErrors at ingest are returned via `diagnostics` rather than thrown:\n- svelte2tsx throws → `transform_failed`, `virtual: undefined`\n- source map construction fails → `source_map_failed`, virtual populated\n\nDiagnostic file paths are the original `.svelte` source ID; downstream\nnormalization rewrites them to project-root-relative form.",
"typeSignature": "(sourceFile: SourceFileInfo): TransformResult",
"sourceLine": 104,
"throws": [
{
"type": "Error",
"description": "if Svelte version is below 5 (checked once on first call)"
}
],
"parameters": [
{
"name": "sourceFile",
"type": "SourceFileInfo",
"description": "the Svelte source file with content loaded"
}
],
"returnType": "TransformResult",
"returnDescription": "virtual file data (or `undefined` on transform failure) plus ingest diagnostics"
},
{
"name": "extractScriptContent",
"kind": "function",
"docComment": "Extract the content of the main `<script>` tag from Svelte source.\n\nMatches `<script>` with any attributes (e.g., `lang`, `generics`) but excludes\nmodule scripts (`<script module>`, `<script context=\"module\">`).",
"typeSignature": "(svelteSource: string): string | undefined",
"sourceLine": 278,
"parameters": [
{
"name": "svelteSource",
"type": "string"
}
],
"returnType": "string | undefined",
"returnDescription": "the script tag content, or undefined if no matching script tag is found"
},
{
"name": "extractHtmlModuleComment",
"kind": "function",
"docComment": "Extract `@module` comment from HTML comments in Svelte source.\n\nScans all `<!-- ... -->` comments for one containing `@module` at the\nstart of a line. This allows `@component` and `@module` to coexist as\nseparate HTML comments. Works for template-only components.",
"typeSignature": "(svelteSource: string): string | undefined",
"sourceLine": 304,
"parameters": [
{
"name": "svelteSource",
"type": "string",
"description": "the full Svelte source code"
}
],
"returnType": "string | undefined",
"returnDescription": "the cleaned module comment text, or undefined if no `@module` HTML comment found"
},
{
"name": "extractSvelteModuleComment",
"kind": "function",
"docComment": "Extract module-level comment from Svelte script content.\n\nParses the script content as TypeScript and delegates to `extractModuleComment`\nfor the shared `@module` tag detection logic.",
"typeSignature": "(scriptContent: string): string | undefined",
"sourceLine": 341,
"parameters": [
{
"name": "scriptContent",
"type": "string",
"description": "the content of the `<script>` tag"
}
],
"returnType": "string | undefined",
"returnDescription": "the cleaned module comment text, or undefined if none found"
},
{
"name": "isSnippetTypeString",
"kind": "function",
"docComment": "Check if a type string represents a `Snippet` type.\n\nUses the already-resolved type string (from `checker.typeToString`) for reliable\ndetection. `Snippet` is an interface (not a type alias), so `aliasSymbol` is\nnot available — type string matching is the reliable detection path.",
"typeSignature": "(typeString: string): boolean",
"sourceLine": 607,
"parameters": [
{
"name": "typeString",
"type": "string"
}
],
"returnType": "boolean"
},
{
"name": "extractSnippetParameters",
"kind": "function",
"docComment": "Extract structured parameters from a `Snippet<[...]>` type.\n\n`Snippet` is an interface, so type arguments are on `TypeReference` (accessed\nvia `checker.getTypeArguments`), not on `aliasTypeArguments` (which is only\nfor type aliases).\n\nReturns full `ParameterJson` input objects (with `optional` and `rest` always set)\nfor runtime consistency with `extractSignatureParameters` in `typescript-extract-shared.ts`.",
"typeSignature": "(snippetType: Type, checker: TypeChecker): { name: string; type: string; optional?: boolean | undefined; rest?: boolean | undefined; description?: string | undefined; defaultValue?: string | undefined; propertyDescriptions?: Record<...> | undefined; }[]",
"sourceLine": 624,
"parameters": [
{
"name": "snippetType",
"type": "Type"
},
{
"name": "checker",
"type": "TypeChecker"
}
],
"returnType": "{ name: string; type: string; optional?: boolean | undefined; rest?: boolean | undefined; description?: string | undefined; defaultValue?: string | undefined; propertyDescriptions?: Record<...> | undefined; }[]",
"returnDescription": "array of parameter info for the snippet's tuple type arguments,\nor `[]` for bare `Snippet` / `Snippet<[]>`"
},
{
"name": "isSnippetReturnType",
"kind": "function",
"docComment": "Check if a return type string matches svelte2tsx's snippet return type pattern.\n\nsvelte2tsx transforms exported snippets into arrow functions with\n`ReturnType<import('svelte').Snippet>` as the return type annotation.\nThe resolved return type includes `unique symbol` (from Svelte's non-exported\n`SnippetReturn` unique symbol) intersected with the branded render message.\nWe match on both parts to avoid false positives — the branded message alone\ncould theoretically be crafted by user code, but the `unique symbol` intersection\ncannot since `SnippetReturn` is not exported from svelte.",
"typeSignature": "(returnType: string): boolean",
"sourceLine": 661,
"parameters": [
{
"name": "returnType",
"type": "string"
}
],
"returnType": "boolean"
},
{
"name": "synthesizeSnippetTypeSignature",
"kind": "function",
"docComment": "Synthesize a `Snippet<[...]>` type string from structured parameters.\n\nUsed for `kind: 'snippet'` declarations where the raw svelte2tsx type\nis implementation noise. Produces type strings consistent with how\nthe checker formats Snippet types on props.",
"typeSignature": "(parameters: { name: string; type: string; optional?: boolean | undefined; rest?: boolean | undefined; description?: string | undefined; defaultValue?: string | undefined; propertyDescriptions?: Record<...> | undefined; }[]): string",
"sourceLine": 675,
"parameters": [
{
"name": "parameters",
"type": "{ name: string; type: string; optional?: boolean | undefined; rest?: boolean | undefined; description?: string | undefined; defaultValue?: string | undefined; propertyDescriptions?: Record<...> | undefined; }[]"
}
],
"returnType": "string"
},
{
"name": "analyzeSvelteModule",
"kind": "function",
"docComment": "Analyze a Svelte module using checker-backed analysis.\n\nRequires the svelte2tsx virtual output to be included in the TypeScript program\n(via `createAnalysisProgram({ virtualFiles })`). Provides full type resolution for:\n- Imported prop types (`let {x}: ImportedProps = $props()`)\n- `<script module>` exports (constants, types, re-exports)\n- Star exports and re-exports from Svelte files",
"typeSignature": "(sourceFile: SourceFileInfo & { dependents?: readonly string[] | undefined; }, modulePath: string, checker: TypeChecker, options: ModuleSourceOptions, diagnostics: ({ ...; } | ... 12 more ... | { ...; })[], program: Program, virtualFile: SvelteVirtualFile): ModuleAnalysis | undefined",
"sourceLine": 919,
"parameters": [
{
"name": "sourceFile",
"type": "SourceFileInfo & { dependents?: readonly string[] | undefined; }",
"description": "the original Svelte source file"
},
{
"name": "modulePath",
"type": "string",
"description": "module path relative to source root"
},
{
"name": "checker",
"type": "TypeChecker",
"description": "TypeScript type checker (from the program containing virtual files)"
},
{
"name": "options",
"type": "ModuleSourceOptions",
"description": "module source options for path extraction"
},
{
"name": "diagnostics",
"type": "({ symbolName: string; file: string; message: string; severity: \"error\" | \"warning\"; kind: \"type_extraction_failed\"; line?: number | undefined; column?: number | undefined; } | { functionName: string; ... 5 more ...; column?: number | undefined; } | ... 11 more ... | { ...; })[]",
"description": "diagnostics collector for non-fatal issues"
},
{
"name": "program",
"type": "Program",
"description": "TypeScript program containing the virtual file"
},
{
"name": "virtualFile",
"type": "SvelteVirtualFile",
"description": "pre-transformed virtual file data"
}
],
"returnType": "ModuleAnalysis | undefined",
"returnDescription": "module analysis with declarations, re-exports, and star exports;\n`undefined` if the virtual file is not found in the program"
}
],
"moduleComment": "Svelte component analysis helpers.\n\nExtracts metadata from Svelte components using svelte2tsx transformations:\n\n- Component props with types and JSDoc\n- Component-level documentation\n- Type information\n\nWorkflow: Transform Svelte to TypeScript via svelte2tsx, parse the transformed\nTypeScript with the TS Compiler API, extract component-level JSDoc from original source.\n\n**Svelte 5 only**: The svelte2tsx output format changed significantly between versions.\nThis module requires Svelte 5+ and will throw a clear error if an older version is detected.\nThere is no Svelte 4 compatibility layer.\n\n@see `typescript-exports.ts` for `analyzeExports`, `extractModuleComment`\n@see `typescript-extract-shared.ts` for `parseGenericParam`, `filterExternalProperties`\n@see `typescript-program.ts` for `IsExternalFile`, `createIsExternalFile`\n@see `tsdoc.ts` for `parseComment`, `applyToDeclaration`\n@see `source.ts` for `SourceFileInfo`, `getComponentName`",
"dependencies": [
"declaration-build.ts",
"diagnostics.ts",
"paths.ts",
"source-config.ts",
"source.ts",
"tsdoc.ts",
"types.ts",
"typescript-exports.ts",
"typescript-extract-shared.ts",
"typescript-program.ts"
],
"dependents": [
"analyze-core.ts",
"session.ts"
]
}