diff --git a/packages/svelte2tsx/src/svelte2tsx/index.ts b/packages/svelte2tsx/src/svelte2tsx/index.ts index 87e6779c4..2daf71419 100644 --- a/packages/svelte2tsx/src/svelte2tsx/index.ts +++ b/packages/svelte2tsx/src/svelte2tsx/index.ts @@ -16,7 +16,7 @@ import { SlotHandler } from './nodes/slot'; import { Stores } from './nodes/Stores'; import TemplateScope from './nodes/TemplateScope'; import { processInstanceScriptContent } from './processInstanceScriptContent'; -import { processModuleScriptTag } from './processModuleScriptTag'; +import { createModuleAst, ModuleAst, processModuleScriptTag } from './processModuleScriptTag'; import { ScopeStack } from './utils/Scope'; import { Generics } from './nodes/Generics'; import { addComponentExport } from './addComponentExport'; @@ -362,7 +362,11 @@ export function svelte2tsx( */ let instanceScriptTarget = 0; + let moduleAst: ModuleAst | undefined; + if (moduleScriptTag) { + moduleAst = createModuleAst(str, moduleScriptTag); + if (moduleScriptTag.start != 0) { //move our module tag to the top str.move(moduleScriptTag.start, moduleScriptTag.end, 0); @@ -398,7 +402,7 @@ export function svelte2tsx( events, implicitStoreValues, options.mode, - /**hasModuleScripts */ !!moduleScriptTag, + moduleAst, options?.isTsFile, basename, svelte5Plus, @@ -443,7 +447,8 @@ export function svelte2tsx( implicitStoreValues.getAccessedStores(), renderFunctionStart, scriptTag || options.mode === 'ts' ? undefined : (input) => `;${input}<>` - ) + ), + moduleAst ); } diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/ExportedNames.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/ExportedNames.ts index c01098d63..27db33cfc 100644 --- a/packages/svelte2tsx/src/svelte2tsx/nodes/ExportedNames.ts +++ b/packages/svelte2tsx/src/svelte2tsx/nodes/ExportedNames.ts @@ -4,6 +4,7 @@ import { internalHelpers } from '../../helpers'; import { surroundWithIgnoreComments } from '../../utils/ignore'; import { preprendStr, overwriteStr } from '../../utils/magic-string'; import { findExportKeyword, getLastLeadingDoc, isInterfaceOrTypeDeclaration } from '../utils/tsAst'; +import { HoistableInterfaces } from './HoistableInterfaces'; export function is$$PropsDeclaration( node: ts.Node @@ -21,6 +22,7 @@ interface ExportedName { } export class ExportedNames { + public hoistableInterfaces = new HoistableInterfaces(); public usesAccessors = false; /** * Uses the `$$Props` type @@ -35,7 +37,9 @@ export class ExportedNames { * If using TS, this returns the generic string, if using JS, returns the `@type {..}` string. */ private $props = { + /** The JSDoc type; not set when TS type exists */ comment: '', + /** The TS type */ type: '', bindings: [] as string[] }; @@ -173,10 +177,13 @@ export class ExportedNames { } } + // Easy mode: User uses TypeScript and typed the $props() rune if (node.initializer.typeArguments?.length > 0 || node.type) { + this.hoistableInterfaces.analyze$propsRune(node); + const generic_arg = node.initializer.typeArguments?.[0] || node.type; const generic = generic_arg.getText(); - if (!generic.includes('{')) { + if (ts.isTypeReferenceNode(generic_arg)) { this.$props.type = generic; } else { // Create a virtual type alias for the unnamed generic and reuse it for the props return type @@ -199,13 +206,28 @@ export class ExportedNames { surroundWithIgnoreComments(this.$props.type) ); } - } else { - if (!this.isTsFile) { - const text = node.getSourceFile().getFullText(); - let start = -1; - let comment: string; - // reverse because we want to look at the last comment before the node first - for (const c of [...(ts.getLeadingCommentRanges(text, node.pos) || [])].reverse()) { + + return; + } + + // Hard mode: User uses JSDoc or didn't type the $props() rune + if (!this.isTsFile) { + const text = node.getSourceFile().getFullText(); + let start = -1; + let comment: string; + // reverse because we want to look at the last comment before the node first + for (const c of [...(ts.getLeadingCommentRanges(text, node.pos) || [])].reverse()) { + const potential_match = text.substring(c.pos, c.end); + if (/@type\b/.test(potential_match)) { + comment = potential_match; + start = c.pos + this.astOffset; + break; + } + } + if (!comment) { + for (const c of [ + ...(ts.getLeadingCommentRanges(text, node.parent.pos) || []).reverse() + ]) { const potential_match = text.substring(c.pos, c.end); if (/@type\b/.test(potential_match)) { comment = potential_match; @@ -213,131 +235,119 @@ export class ExportedNames { break; } } - if (!comment) { - for (const c of [ - ...(ts.getLeadingCommentRanges(text, node.parent.pos) || []).reverse() - ]) { - const potential_match = text.substring(c.pos, c.end); - if (/@type\b/.test(potential_match)) { - comment = potential_match; - start = c.pos + this.astOffset; - break; - } - } - } - - if (comment && /\/\*\*[^@]*?@type\s*{\s*{.*}\s*}\s*\*\//.test(comment)) { - // Create a virtual type alias for the unnamed generic and reuse it for the props return type - // so that rename, find references etc works seamlessly across components - this.$props.comment = '/** @type {$$ComponentProps} */'; - const type_start = this.str.original.indexOf('@type', start); - this.str.overwrite(type_start, type_start + 5, '@typedef'); - const end = this.str.original.indexOf('*/', start); - this.str.overwrite(end, end + 2, ' $$ComponentProps */' + this.$props.comment); - } else { - // Complex comment or simple `@type {AType}` comment which we just use as-is. - // For the former this means things like rename won't work properly across components. - this.$props.comment = comment || ''; - } } - if (this.$props.comment) { - return; + if (comment && /\/\*\*[^@]*?@type\s*{\s*{.*}\s*}\s*\*\//.test(comment)) { + // Create a virtual type alias for the unnamed generic and reuse it for the props return type + // so that rename, find references etc works seamlessly across components + this.$props.comment = '/** @type {$$ComponentProps} */'; + const type_start = this.str.original.indexOf('@type', start); + this.str.overwrite(type_start, type_start + 5, '@typedef'); + const end = this.str.original.indexOf('*/', start); + this.str.overwrite(end, end + 2, ' $$ComponentProps */' + this.$props.comment); + } else { + // Complex comment or simple `@type {AType}` comment which we just use as-is. + // For the former this means things like rename won't work properly across components. + this.$props.comment = comment || ''; } + } - // Do a best-effort to extract the props from the object literal - let propsStr = ''; - let withUnknown = false; - let props = []; - - const isKitRouteFile = internalHelpers.isKitRouteFile(this.basename); - const isKitLayoutFile = isKitRouteFile && this.basename.includes('layout'); - - if (ts.isObjectBindingPattern(node.name)) { - for (const element of node.name.elements) { - if ( - !ts.isIdentifier(element.name) || - (element.propertyName && !ts.isIdentifier(element.propertyName)) || - !!element.dotDotDotToken - ) { - withUnknown = true; - } else { - const name = element.propertyName - ? (element.propertyName as ts.Identifier).text - : element.name.text; - if (isKitRouteFile) { - if (name === 'data') { - props.push( - `data: import('./$types.js').${ - isKitLayoutFile ? 'LayoutData' : 'PageData' - }` - ); - } - if (name === 'form' && !isKitLayoutFile) { - props.push(`form: import('./$types.js').ActionData`); - } - } else if (element.initializer) { - const type = ts.isAsExpression(element.initializer) - ? element.initializer.type.getText() - : ts.isStringLiteral(element.initializer) - ? 'string' - : ts.isNumericLiteral(element.initializer) - ? 'number' - : element.initializer.kind === ts.SyntaxKind.TrueKeyword || - element.initializer.kind === ts.SyntaxKind.FalseKeyword - ? 'boolean' - : ts.isIdentifier(element.initializer) - ? `typeof ${element.initializer.text}` - : ts.isObjectLiteralExpression(element.initializer) - ? 'Record' - : ts.isArrayLiteralExpression(element.initializer) - ? 'unknown[]' - : 'unknown'; - props.push(`${name}?: ${type}`); - } else { - props.push(`${name}: unknown`); + if (this.$props.comment) { + // User uses JsDoc + return; + } + + // Do a best-effort to extract the props from the object literal + let propsStr = ''; + let withUnknown = false; + let props = []; + + const isKitRouteFile = internalHelpers.isKitRouteFile(this.basename); + const isKitLayoutFile = isKitRouteFile && this.basename.includes('layout'); + + if (ts.isObjectBindingPattern(node.name)) { + for (const element of node.name.elements) { + if ( + !ts.isIdentifier(element.name) || + (element.propertyName && !ts.isIdentifier(element.propertyName)) || + !!element.dotDotDotToken + ) { + withUnknown = true; + } else { + const name = element.propertyName + ? (element.propertyName as ts.Identifier).text + : element.name.text; + if (isKitRouteFile) { + if (name === 'data') { + props.push( + `data: import('./$types.js').${ + isKitLayoutFile ? 'LayoutData' : 'PageData' + }` + ); } + if (name === 'form' && !isKitLayoutFile) { + props.push(`form: import('./$types.js').ActionData`); + } + } else if (element.initializer) { + const type = ts.isAsExpression(element.initializer) + ? element.initializer.type.getText() + : ts.isStringLiteral(element.initializer) + ? 'string' + : ts.isNumericLiteral(element.initializer) + ? 'number' + : element.initializer.kind === ts.SyntaxKind.TrueKeyword || + element.initializer.kind === ts.SyntaxKind.FalseKeyword + ? 'boolean' + : ts.isIdentifier(element.initializer) + ? `typeof ${element.initializer.text}` + : ts.isObjectLiteralExpression(element.initializer) + ? 'Record' + : ts.isArrayLiteralExpression(element.initializer) + ? 'unknown[]' + : 'unknown'; + props.push(`${name}?: ${type}`); + } else { + props.push(`${name}: unknown`); } } + } - if (isKitLayoutFile) { - props.push(`children: import('svelte').Snippet`); - } + if (isKitLayoutFile) { + props.push(`children: import('svelte').Snippet`); + } - if (props.length > 0) { - propsStr = - `{ ${props.join(', ')} }` + - (withUnknown ? ' & Record' : ''); - } else if (withUnknown) { - propsStr = 'Record'; - } else { - propsStr = 'Record'; - } - } else { + if (props.length > 0) { + propsStr = + `{ ${props.join(', ')} }` + (withUnknown ? ' & Record' : ''); + } else if (withUnknown) { propsStr = 'Record'; + } else { + propsStr = 'Record'; } + } else { + propsStr = 'Record'; + } - // Create a virtual type alias for the unnamed generic and reuse it for the props return type - // so that rename, find references etc works seamlessly across components - if (this.isTsFile) { - this.$props.type = '$$ComponentProps'; - if (props.length > 0 || withUnknown) { - preprendStr( - this.str, - node.parent.pos + this.astOffset, - surroundWithIgnoreComments(`;type $$ComponentProps = ${propsStr};`) - ); - preprendStr(this.str, node.name.end + this.astOffset, `: ${this.$props.type}`); - } - } else { - this.$props.comment = '/** @type {$$ComponentProps} */'; - if (props.length > 0 || withUnknown) { - preprendStr( - this.str, - node.pos + this.astOffset, - `/** @typedef {${propsStr}} $$ComponentProps */${this.$props.comment}` - ); - } + // Create a virtual type alias for the unnamed generic and reuse it for the props return type + // so that rename, find references etc works seamlessly across components + if (this.isTsFile) { + this.$props.type = '$$ComponentProps'; + if (props.length > 0 || withUnknown) { + preprendStr( + this.str, + node.parent.pos + this.astOffset, + surroundWithIgnoreComments(`;type $$ComponentProps = ${propsStr};`) + ); + preprendStr(this.str, node.name.end + this.astOffset, `: ${this.$props.type}`); + } + } else { + this.$props.comment = '/** @type {$$ComponentProps} */'; + if (props.length > 0 || withUnknown) { + preprendStr( + this.str, + node.pos + this.astOffset, + `/** @typedef {${propsStr}} $$ComponentProps */${this.$props.comment}` + ); } } } diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/HoistableInterfaces.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/HoistableInterfaces.ts new file mode 100644 index 000000000..99f85e365 --- /dev/null +++ b/packages/svelte2tsx/src/svelte2tsx/nodes/HoistableInterfaces.ts @@ -0,0 +1,320 @@ +import ts from 'typescript'; +import MagicString from 'magic-string'; + +/** + * Collects all imports and module-level declarations to then find out which interfaces/types are hoistable. + */ +export class HoistableInterfaces { + private import_value_set: Set = new Set(); + private import_type_set: Set = new Set(); + private interface_map: Map< + string, + { type_deps: Set; value_deps: Set; node: ts.Node } + > = new Map(); + private props_interface = { + name: '', + node: null as ts.Node | null, + type_deps: new Set(), + value_deps: new Set() + }; + + analyzeModuleScriptNode(node: ts.Node) { + // Handle Import Declarations + if (ts.isImportDeclaration(node) && node.importClause) { + const is_type_only = node.importClause.isTypeOnly; + + if ( + node.importClause.namedBindings && + ts.isNamedImports(node.importClause.namedBindings) + ) { + node.importClause.namedBindings.elements.forEach((element) => { + const import_name = element.name.text; + if (is_type_only) { + this.import_type_set.add(import_name); + } else { + this.import_value_set.add(import_name); + } + }); + } + + // Handle default imports + if (node.importClause.name) { + const default_import = node.importClause.name.text; + if (is_type_only) { + this.import_type_set.add(default_import); + } else { + this.import_value_set.add(default_import); + } + } + + // Handle namespace imports + if ( + node.importClause.namedBindings && + ts.isNamespaceImport(node.importClause.namedBindings) + ) { + const namespace_import = node.importClause.namedBindings.name.text; + if (is_type_only) { + this.import_type_set.add(namespace_import); + } else { + this.import_value_set.add(namespace_import); + } + } + } + + // Handle top-level declarations + if (ts.isVariableStatement(node)) { + node.declarationList.declarations.forEach((declaration) => { + if (ts.isIdentifier(declaration.name)) { + this.import_value_set.add(declaration.name.text); + } + }); + } + + if (ts.isFunctionDeclaration(node) && node.name) { + this.import_value_set.add(node.name.text); + } + + if (ts.isClassDeclaration(node) && node.name) { + this.import_value_set.add(node.name.text); + } + + if (ts.isEnumDeclaration(node)) { + this.import_value_set.add(node.name.text); + } + + if (ts.isTypeAliasDeclaration(node)) { + this.import_type_set.add(node.name.text); + } + + if (ts.isInterfaceDeclaration(node)) { + this.import_type_set.add(node.name.text); + } + } + + analyzeInstanceScriptNode(node: ts.Node) { + // Handle Import Declarations + if (ts.isImportDeclaration(node) && node.importClause) { + const is_type_only = node.importClause.isTypeOnly; + + if ( + node.importClause.namedBindings && + ts.isNamedImports(node.importClause.namedBindings) + ) { + node.importClause.namedBindings.elements.forEach((element) => { + const import_name = element.name.text; + if (is_type_only) { + this.import_type_set.add(import_name); + } else { + this.import_value_set.add(import_name); + } + }); + } + + // Handle default imports + if (node.importClause.name) { + const default_import = node.importClause.name.text; + if (is_type_only) { + this.import_type_set.add(default_import); + } else { + this.import_value_set.add(default_import); + } + } + + // Handle namespace imports + if ( + node.importClause.namedBindings && + ts.isNamespaceImport(node.importClause.namedBindings) + ) { + const namespace_import = node.importClause.namedBindings.name.text; + if (is_type_only) { + this.import_type_set.add(namespace_import); + } else { + this.import_value_set.add(namespace_import); + } + } + } + + // Handle Interface Declarations + if (ts.isInterfaceDeclaration(node)) { + const interface_name = node.name.text; + const type_dependencies: Set = new Set(); + const value_dependencies: Set = new Set(); + const generics = node.typeParameters?.map((param) => param.name.text) ?? []; + + node.members.forEach((member) => { + if (ts.isPropertySignature(member) && member.type) { + this.collectTypeDependencies( + member.type, + type_dependencies, + value_dependencies, + generics + ); + } + }); + + this.interface_map.set(interface_name, { + type_deps: type_dependencies, + value_deps: value_dependencies, + node + }); + } + + // Handle Type Alias Declarations + if (ts.isTypeAliasDeclaration(node)) { + const alias_name = node.name.text; + const type_dependencies: Set = new Set(); + const value_dependencies: Set = new Set(); + const generics = node.typeParameters?.map((param) => param.name.text) ?? []; + + this.collectTypeDependencies( + node.type, + type_dependencies, + value_dependencies, + generics + ); + + this.interface_map.set(alias_name, { + type_deps: type_dependencies, + value_deps: value_dependencies, + node + }); + } + } + + analyze$propsRune( + node: ts.VariableDeclaration & { + initializer: ts.CallExpression & { expression: ts.Identifier }; + } + ) { + if (node.initializer.typeArguments?.length > 0 || node.type) { + const generic_arg = node.initializer.typeArguments?.[0] || node.type; + if (ts.isTypeReferenceNode(generic_arg)) { + const name = this.getEntityNameText(generic_arg.typeName); + const interface_node = this.interface_map.get(name); + if (interface_node) { + this.props_interface.name = name; + this.props_interface.type_deps = interface_node.type_deps; + this.props_interface.value_deps = interface_node.value_deps; + } + } else { + this.props_interface.name = '$$ComponentProps'; + this.props_interface.node = generic_arg; + this.collectTypeDependencies( + generic_arg, + this.props_interface.type_deps, + this.props_interface.value_deps, + [] + ); + } + } + } + + /** + * Traverses the AST to collect import statements and top-level interfaces, + * then determines which interfaces can be hoisted. + * @param source_file The TypeScript source file to analyze. + * @returns An object containing sets of value imports, type imports, and hoistable interfaces. + */ + private determineHoistableInterfaces() { + const hoistable_interfaces: Map = new Map(); + let progress = true; + + while (progress) { + progress = false; + + for (const [interface_name, deps] of this.interface_map.entries()) { + if (hoistable_interfaces.has(interface_name)) { + continue; + } + + const can_hoist = [...deps.type_deps, ...deps.value_deps].every((dep) => { + return ( + this.import_type_set.has(dep) || + this.import_value_set.has(dep) || + hoistable_interfaces.has(dep) + ); + }); + + if (can_hoist) { + hoistable_interfaces.set(interface_name, deps.node); + progress = true; + } + } + } + + if (this.props_interface.name === '$$ComponentProps') { + const can_hoist = [ + ...this.props_interface.type_deps, + ...this.props_interface.value_deps + ].every((dep) => { + return ( + this.import_type_set.has(dep) || + this.import_value_set.has(dep) || + hoistable_interfaces.has(dep) + ); + }); + + if (can_hoist) { + hoistable_interfaces.set(this.props_interface.name, this.props_interface.node); + } + } + + return hoistable_interfaces; + } + + /** + * Moves all interfaces that can be hoisted to the top of the script, if the $props rune's type is hoistable. + */ + moveHoistableInterfaces(str: MagicString, astOffset: number, scriptStart: number) { + if (!this.props_interface.name) return; + + const hoistable = this.determineHoistableInterfaces(); + if (hoistable.has(this.props_interface.name)) { + for (const [, node] of hoistable) { + str.move(node.pos + astOffset, node.end + astOffset, scriptStart); + } + } + } + + /** + * Collects type and value dependencies from a given TypeNode. + * @param type_node The TypeNode to analyze. + * @param type_dependencies The set to collect type dependencies into. + * @param value_dependencies The set to collect value dependencies into. + */ + private collectTypeDependencies( + type_node: ts.TypeNode, + type_dependencies: Set, + value_dependencies: Set, + generics: string[] + ) { + const walk = (node: ts.Node) => { + if (ts.isTypeReferenceNode(node)) { + const type_name = this.getEntityNameText(node.typeName); + if (!generics.includes(type_name)) { + type_dependencies.add(type_name); + } + } else if (ts.isTypeQueryNode(node)) { + // Handle 'typeof' expressions: e.g., foo: typeof bar + value_dependencies.add(this.getEntityNameText(node.exprName)); + } + + ts.forEachChild(node, walk); + }; + + type_node.forEachChild(walk); + } + + /** + * Retrieves the full text of an EntityName (handles nested names). + * @param entity_name The EntityName to extract text from. + * @returns The full name as a string. + */ + private getEntityNameText(entity_name: ts.EntityName): string { + if (ts.isIdentifier(entity_name)) { + return entity_name.text; + } else { + return this.getEntityNameText(entity_name.left) + '.' + entity_name.right.text; + } + } +} diff --git a/packages/svelte2tsx/src/svelte2tsx/processInstanceScriptContent.ts b/packages/svelte2tsx/src/svelte2tsx/processInstanceScriptContent.ts index 044aeb83a..e7d66870c 100644 --- a/packages/svelte2tsx/src/svelte2tsx/processInstanceScriptContent.ts +++ b/packages/svelte2tsx/src/svelte2tsx/processInstanceScriptContent.ts @@ -16,6 +16,7 @@ import { handleImportDeclaration } from './nodes/handleImportDeclaration'; import { InterfacesAndTypes } from './nodes/InterfacesAndTypes'; +import { ModuleAst } from './processModuleScriptTag'; export interface InstanceScriptProcessResult { exportedNames: ExportedNames; @@ -39,7 +40,7 @@ export function processInstanceScriptContent( events: ComponentEvents, implicitStoreValues: ImplicitStoreValues, mode: 'ts' | 'dts', - hasModuleScript: boolean, + moduleAst: ModuleAst | undefined, isTSFile: boolean, basename: string, isSvelte5Plus: boolean, @@ -66,6 +67,12 @@ export function processInstanceScriptContent( const generics = new Generics(str, astOffset, script); const interfacesAndTypes = new InterfacesAndTypes(); + if (moduleAst) { + moduleAst.tsAst.forEachChild((n) => + exportedNames.hoistableInterfaces.analyzeModuleScriptNode(n) + ); + } + const implicitTopLevelNames = new ImplicitTopLevelNames(str, astOffset); let uses$$props = false; let uses$$restProps = false; @@ -159,6 +166,10 @@ export function processInstanceScriptContent( type onLeaveCallback = () => void; const onLeaveCallbacks: onLeaveCallback[] = []; + if (parent === tsAst) { + exportedNames.hoistableInterfaces.analyzeInstanceScriptNode(node); + } + generics.addIfIsGeneric(node); if (is$$EventsDeclaration(node)) { @@ -290,7 +301,7 @@ export function processInstanceScriptContent( implicitTopLevelNames.modifyCode(rootScope.declared); implicitStoreValues.modifyCode(astOffset, str); - handleFirstInstanceImport(tsAst, astOffset, hasModuleScript, str); + handleFirstInstanceImport(tsAst, astOffset, !!moduleAst, str); // move interfaces and types out of the render function if they are referenced // by a $$Generic, otherwise it will be used before being defined after the transformation @@ -307,6 +318,8 @@ export function processInstanceScriptContent( transformInterfacesToTypes(tsAst, str, astOffset, nodesToMove); } + exportedNames.hoistableInterfaces.moveHoistableInterfaces(str, astOffset, script.start); + return { exportedNames, events, diff --git a/packages/svelte2tsx/src/svelte2tsx/processModuleScriptTag.ts b/packages/svelte2tsx/src/svelte2tsx/processModuleScriptTag.ts index fa5f89a89..d66d4630b 100644 --- a/packages/svelte2tsx/src/svelte2tsx/processModuleScriptTag.ts +++ b/packages/svelte2tsx/src/svelte2tsx/processModuleScriptTag.ts @@ -9,11 +9,13 @@ import { throwError } from './utils/error'; import { is$$SlotsDeclaration } from './nodes/slot'; import { is$$PropsDeclaration } from './nodes/ExportedNames'; -export function processModuleScriptTag( - str: MagicString, - script: Node, - implicitStoreValues: ImplicitStoreValues -) { +export interface ModuleAst { + htmlx: string; + tsAst: ts.SourceFile; + astOffset: number; +} + +export function createModuleAst(str: MagicString, script: Node): ModuleAst { const htmlx = str.original; const scriptContent = htmlx.substring(script.content.start, script.content.end); const tsAst = ts.createSourceFile( @@ -25,6 +27,17 @@ export function processModuleScriptTag( ); const astOffset = script.content.start; + return { htmlx, tsAst, astOffset }; +} + +export function processModuleScriptTag( + str: MagicString, + script: Node, + implicitStoreValues: ImplicitStoreValues, + moduleAst: ModuleAst +) { + const { htmlx, tsAst, astOffset } = moduleAst; + const generics = new Generics(str, astOffset, script); if (generics.genericsAttr) { const start = htmlx.indexOf('generics', script.start); diff --git a/packages/svelte2tsx/test/emitDts/samples/typescript-runes.v5/expected/TestRunes.svelte.d.ts b/packages/svelte2tsx/test/emitDts/samples/typescript-runes.v5/expected/TestRunes.svelte.d.ts deleted file mode 100644 index 350d1d487..000000000 --- a/packages/svelte2tsx/test/emitDts/samples/typescript-runes.v5/expected/TestRunes.svelte.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -declare const TestRunes: import("svelte").Component<{ - foo: string; - bar?: number; -}, { - baz: () => void; -}, "bar">; -type TestRunes = ReturnType; -export default TestRunes; diff --git a/packages/svelte2tsx/test/emitDts/samples/typescript-runes.v5/expected/TestRunes1.svelte.d.ts b/packages/svelte2tsx/test/emitDts/samples/typescript-runes.v5/expected/TestRunes1.svelte.d.ts new file mode 100644 index 000000000..015490b7f --- /dev/null +++ b/packages/svelte2tsx/test/emitDts/samples/typescript-runes.v5/expected/TestRunes1.svelte.d.ts @@ -0,0 +1,9 @@ +type $$ComponentProps = { + foo: string; + bar?: number; +}; +declare const TestRunes1: import("svelte").Component<$$ComponentProps, { + baz: () => void; +}, "bar">; +type TestRunes1 = ReturnType; +export default TestRunes1; diff --git a/packages/svelte2tsx/test/emitDts/samples/typescript-runes.v5/expected/TestRunes2.svelte.d.ts b/packages/svelte2tsx/test/emitDts/samples/typescript-runes.v5/expected/TestRunes2.svelte.d.ts new file mode 100644 index 000000000..885a90203 --- /dev/null +++ b/packages/svelte2tsx/test/emitDts/samples/typescript-runes.v5/expected/TestRunes2.svelte.d.ts @@ -0,0 +1,9 @@ +/** asd */ +type Props = { + foo: string; + bar?: X; +}; +import type { X } from './x'; +declare const TestRunes2: import("svelte").Component; +type TestRunes2 = ReturnType; +export default TestRunes2; diff --git a/packages/svelte2tsx/test/emitDts/samples/typescript-runes.v5/src/TestRunes.svelte b/packages/svelte2tsx/test/emitDts/samples/typescript-runes.v5/src/TestRunes1.svelte similarity index 100% rename from packages/svelte2tsx/test/emitDts/samples/typescript-runes.v5/src/TestRunes.svelte rename to packages/svelte2tsx/test/emitDts/samples/typescript-runes.v5/src/TestRunes1.svelte diff --git a/packages/svelte2tsx/test/emitDts/samples/typescript-runes.v5/src/TestRunes2.svelte b/packages/svelte2tsx/test/emitDts/samples/typescript-runes.v5/src/TestRunes2.svelte new file mode 100644 index 000000000..c331da40d --- /dev/null +++ b/packages/svelte2tsx/test/emitDts/samples/typescript-runes.v5/src/TestRunes2.svelte @@ -0,0 +1,9 @@ + diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-1.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-1.v5/expectedv2.ts new file mode 100644 index 000000000..c14fcdf06 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-1.v5/expectedv2.ts @@ -0,0 +1,27 @@ +/// +; + let value = 1; +; + type NoComma = true + type Dependency = { + a: number; + b: typeof value; + c: NoComma + } + + /** A comment */ + interface Props { + a: Dependency; + b: T; + };function render() { + + + let { a, b }: Props = $props(); +; +async () => { + +}; +return { props: {} as any as Props, exports: {}, bindings: __sveltets_$$bindings(''), slots: {}, events: {} }} +const Input__SvelteComponent_ = __sveltets_2_fn_component(render()); +type Input__SvelteComponent_ = ReturnType; +export default Input__SvelteComponent_; \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-1.v5/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-1.v5/input.svelte new file mode 100644 index 000000000..9e642a6ea --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-1.v5/input.svelte @@ -0,0 +1,20 @@ + + + diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-2.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-2.v5/expectedv2.ts new file mode 100644 index 000000000..664f10006 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-2.v5/expectedv2.ts @@ -0,0 +1,19 @@ +/// +; + let value = 1; +; + interface Dependency { + a: number; + b: typeof value; + };type $$ComponentProps = { a: Dependency, b: string };;function render() { + + + let { a, b }:/*Ωignore_startΩ*/$$ComponentProps/*Ωignore_endΩ*/ = $props(); +; +async () => { + +}; +return { props: {} as any as $$ComponentProps, exports: {}, bindings: __sveltets_$$bindings(''), slots: {}, events: {} }} +const Input__SvelteComponent_ = __sveltets_2_fn_component(render()); +type Input__SvelteComponent_ = ReturnType; +export default Input__SvelteComponent_; \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-2.v5/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-2.v5/input.svelte new file mode 100644 index 000000000..958dc103e --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-2.v5/input.svelte @@ -0,0 +1,12 @@ + + + diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes.v5/expectedv2.ts index e248ca3cf..b9ee4758c 100644 --- a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes.v5/expectedv2.ts +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes.v5/expectedv2.ts @@ -1,6 +1,6 @@ /// -;function render() { -;type $$ComponentProps = { a: number, b: string }; +;type $$ComponentProps = { a: number, b: string };;function render() { + let { a, b }:/*Ωignore_startΩ*/$$ComponentProps/*Ωignore_endΩ*/ = $props(); let x = $state(0); let y = $derived(x * 2); diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-sveltekit-autotypes-$props-rune-unchanged.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/ts-sveltekit-autotypes-$props-rune-unchanged.v5/expectedv2.ts index 04b6fd6ba..2bdf2e4ea 100644 --- a/packages/svelte2tsx/test/svelte2tsx/samples/ts-sveltekit-autotypes-$props-rune-unchanged.v5/expectedv2.ts +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-sveltekit-autotypes-$props-rune-unchanged.v5/expectedv2.ts @@ -1,7 +1,7 @@ /// -;function render() { +;type $$ComponentProps = {form: boolean, data: true };;function render() { - const snapshot: any = {};;type $$ComponentProps = {form: boolean, data: true }; + const snapshot: any = {}; let { form, data }:/*Ωignore_startΩ*/$$ComponentProps/*Ωignore_endΩ*/ = $props(); ; async () => {};