From 77160ff7956744311ce3fc8c65b99eb7ae9e0dc7 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Tue, 12 Nov 2024 11:48:01 +0100 Subject: [PATCH 1/4] fix: fall back to `any` instead of `unknown` for untyped `$props` (#2582) The idea was to be conservative about it, but this causes problems for people not wanting strict type checking but only a basic form of it. Falling back to `any` is the more pragmatic choice #2556 --- .../src/svelte2tsx/nodes/ExportedNames.ts | 17 ++++++++--------- .../runes-best-effort-types.v5/expectedv2.ts | 2 +- .../samples/runes-bindable.v5/expectedv2.ts | 2 +- .../runes-looking-like-stores.v5/expectedv2.ts | 2 +- .../ts-runes-best-effort-types.v5/expectedv2.ts | 2 +- .../samples/ts-runes-bindable.v5/expectedv2.ts | 2 +- 6 files changed, 13 insertions(+), 14 deletions(-) diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/ExportedNames.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/ExportedNames.ts index e4b05eaf4..d3083bb88 100644 --- a/packages/svelte2tsx/src/svelte2tsx/nodes/ExportedNames.ts +++ b/packages/svelte2tsx/src/svelte2tsx/nodes/ExportedNames.ts @@ -284,7 +284,7 @@ export class ExportedNames { ? element.initializer.arguments[0] : element.initializer; const type = !initializer - ? 'unknown' + ? 'any' : ts.isAsExpression(initializer) ? initializer.type.getText() : ts.isStringLiteral(initializer) @@ -300,13 +300,13 @@ export class ExportedNames { : ts.isArrowFunction(initializer) ? 'Function' : ts.isObjectLiteralExpression(initializer) - ? 'Record' + ? 'Record' : ts.isArrayLiteralExpression(initializer) - ? 'unknown[]' - : 'unknown'; + ? 'any[]' + : 'any'; props.push(`${name}?: ${type}`); } else { - props.push(`${name}: unknown`); + props.push(`${name}: any`); } } } @@ -317,15 +317,14 @@ export class ExportedNames { if (props.length > 0) { propsStr = - `{ ${props.join(', ')} }` + - (withUnknown ? ' & Record' : ''); + `{ ${props.join(', ')} }` + (withUnknown ? ' & Record' : ''); } else if (withUnknown) { - propsStr = 'Record'; + propsStr = 'Record'; } else { propsStr = 'Record'; } } else { - propsStr = 'Record'; + propsStr = 'Record'; } // Create a virtual type alias for the unnamed generic and reuse it for the props return type diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/runes-best-effort-types.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/runes-best-effort-types.v5/expectedv2.ts index 7404a44ab..72cffb46c 100644 --- a/packages/svelte2tsx/test/svelte2tsx/samples/runes-best-effort-types.v5/expectedv2.ts +++ b/packages/svelte2tsx/test/svelte2tsx/samples/runes-best-effort-types.v5/expectedv2.ts @@ -1,7 +1,7 @@ /// ;function render() { - let/** @typedef {{ a: unknown, b?: boolean, c?: number, d?: string, e?: unknown, f?: Record, g?: typeof foo, h?: unknown[], i?: unknown, j?: unknown, k?: number, l?: Function }} $$ComponentProps *//** @type {$$ComponentProps} */ { a, b = true, c = 1, d = '', e = null, f = {}, g = foo, h = [], i = undefined, j = $bindable(), k = $bindable(1), l = () => {} } = $props(); + let/** @typedef {{ a: any, b?: boolean, c?: number, d?: string, e?: any, f?: Record, g?: typeof foo, h?: any[], i?: any, j?: any, k?: number, l?: Function }} $$ComponentProps *//** @type {$$ComponentProps} */ { a, b = true, c = 1, d = '', e = null, f = {}, g = foo, h = [], i = undefined, j = $bindable(), k = $bindable(1), l = () => {} } = $props(); ; async () => {}; return { props: /** @type {$$ComponentProps} */({}), exports: {}, bindings: __sveltets_$$bindings('j', 'k'), slots: {}, events: {} }} diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/runes-bindable.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/runes-bindable.v5/expectedv2.ts index 5379c6643..f392054d8 100644 --- a/packages/svelte2tsx/test/svelte2tsx/samples/runes-bindable.v5/expectedv2.ts +++ b/packages/svelte2tsx/test/svelte2tsx/samples/runes-bindable.v5/expectedv2.ts @@ -1,7 +1,7 @@ /// ;function render() { - let/** @typedef {{ a: unknown, b?: unknown }} $$ComponentProps *//** @type {$$ComponentProps} */ { a, b = $bindable() } = $props(); + let/** @typedef {{ a: any, b?: any }} $$ComponentProps *//** @type {$$ComponentProps} */ { a, b = $bindable() } = $props(); ; async () => {}; return { props: /** @type {$$ComponentProps} */({}), exports: {}, bindings: __sveltets_$$bindings('b'), slots: {}, events: {} }} diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/runes-looking-like-stores.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/runes-looking-like-stores.v5/expectedv2.ts index 2abbbff95..555322c0a 100644 --- a/packages/svelte2tsx/test/svelte2tsx/samples/runes-looking-like-stores.v5/expectedv2.ts +++ b/packages/svelte2tsx/test/svelte2tsx/samples/runes-looking-like-stores.v5/expectedv2.ts @@ -1,7 +1,7 @@ /// ;function render() { - let/** @typedef {{ props: unknown }} $$ComponentProps *//** @type {$$ComponentProps} */ { props } = $props(); + let/** @typedef {{ props: any }} $$ComponentProps *//** @type {$$ComponentProps} */ { props } = $props(); let state = $state(0); let derived = $derived(state * 2); ; diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-best-effort-types.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-best-effort-types.v5/expectedv2.ts index 455e1b9b6..b819a0c69 100644 --- a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-best-effort-types.v5/expectedv2.ts +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-best-effort-types.v5/expectedv2.ts @@ -1,6 +1,6 @@ /// ;function render() { -/*Ωignore_startΩ*/;type $$ComponentProps = { a: unknown, b?: boolean, c?: number, d?: string, e?: unknown, f?: Record, g?: typeof foo, h?: Bar, i?: Baz, j?: unknown[], k?: unknown, l?: unknown, m?: number, n?: Function };/*Ωignore_endΩ*/ +/*Ωignore_startΩ*/;type $$ComponentProps = { a: any, b?: boolean, c?: number, d?: string, e?: any, f?: Record, g?: typeof foo, h?: Bar, i?: Baz, j?: any[], k?: any, l?: any, m?: number, n?: Function };/*Ωignore_endΩ*/ let { a, b = true, c = 1, d = '', e = null, f = {}, g = foo, h = null as Bar, i = null as any as Baz, j = [], k = undefined, l = $bindable(), m = $bindable(1), n = () => {} }: $$ComponentProps = $props(); ; async () => {}; diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-bindable.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-bindable.v5/expectedv2.ts index f2686212b..22c641cf8 100644 --- a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-bindable.v5/expectedv2.ts +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-bindable.v5/expectedv2.ts @@ -1,6 +1,6 @@ /// ;function render() { -/*Ωignore_startΩ*/;type $$ComponentProps = { a: unknown, b?: unknown, c?: number };/*Ωignore_endΩ*/ +/*Ωignore_startΩ*/;type $$ComponentProps = { a: any, b?: any, c?: number };/*Ωignore_endΩ*/ let { a, b = $bindable(), c = $bindable(0) as number }: $$ComponentProps = $props(); ; async () => {}; From 1b205c280a27fb248d1b1c0b9bcbebbd7f5abc7f Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Thu, 14 Nov 2024 10:33:23 +0100 Subject: [PATCH 2/4] perf: check for and return promise instead of awaiting (#2586) There are some checks in `SvelteDocument` where we do the work once in case it hasn't started yet. But we're potentially doing the work more often than necessary, because we're awaiting the result before assigning it to the "chache" and returning it. That way, if another request would come in while the promise isn't resolve yet, we would kick off another needless compile. This fixes that by assigning the promise to the "cache" instead. --- .../src/plugins/svelte/SvelteDocument.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/language-server/src/plugins/svelte/SvelteDocument.ts b/packages/language-server/src/plugins/svelte/SvelteDocument.ts index 44ee3de46..65aa1146e 100644 --- a/packages/language-server/src/plugins/svelte/SvelteDocument.ts +++ b/packages/language-server/src/plugins/svelte/SvelteDocument.ts @@ -35,8 +35,8 @@ type PositionMapper = Pick | undefined; + private compileResult: Promise | undefined; private svelteVersion: [number, number] | undefined; public script: TagInformation | null; @@ -76,12 +76,12 @@ export class SvelteDocument { const [major, minor] = this.getSvelteVersion(); if (major > 3 || (major === 3 && minor >= 32)) { - this.transpiledDoc = await TranspiledSvelteDocument.create( + this.transpiledDoc = TranspiledSvelteDocument.create( this.parent, await this.config ); } else { - this.transpiledDoc = await FallbackTranspiledSvelteDocument.create( + this.transpiledDoc = FallbackTranspiledSvelteDocument.create( this.parent, (await this.config)?.preprocess ); @@ -92,7 +92,7 @@ export class SvelteDocument { async getCompiled(): Promise { if (!this.compileResult) { - this.compileResult = await this.getCompiledWith((await this.config)?.compilerOptions); + this.compileResult = this.getCompiledWith((await this.config)?.compilerOptions); } return this.compileResult; From b83b665757702eca392564fe5ae4982e1f5b2a54 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Fri, 15 Nov 2024 13:45:50 +0100 Subject: [PATCH 3/4] fix: robustify and fix file writing - throw an error if writing didn't work (ts doesn't throw itself) - fix html path logic #2584 --- packages/svelte2tsx/package.json | 2 +- packages/svelte2tsx/src/helpers/files.ts | 26 ++++++++++++++++-------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/packages/svelte2tsx/package.json b/packages/svelte2tsx/package.json index 6fd902108..7a9f1758c 100644 --- a/packages/svelte2tsx/package.json +++ b/packages/svelte2tsx/package.json @@ -1,6 +1,6 @@ { "name": "svelte2tsx", - "version": "0.7.23", + "version": "0.7.25", "description": "Convert Svelte components to TSX for type checking", "author": "David Pershouse", "license": "MIT", diff --git a/packages/svelte2tsx/src/helpers/files.ts b/packages/svelte2tsx/src/helpers/files.ts index a4bd1d2b4..b349a9fd6 100644 --- a/packages/svelte2tsx/src/helpers/files.ts +++ b/packages/svelte2tsx/src/helpers/files.ts @@ -12,17 +12,17 @@ export function get_global_types( typesPath: string, hiddenFolderPath?: string ): string[] { - const svelteHtmlPath = isSvelte3 ? undefined : join(sveltePath, 'svelte-html.d.ts'); - const svelteHtmlPathExists = svelteHtmlPath && tsSystem.fileExists(svelteHtmlPath); - const svelteHtmlFile = svelteHtmlPathExists ? svelteHtmlPath : './svelte-jsx-v4.d.ts'; + let svelteHtmlPath = isSvelte3 ? undefined : join(sveltePath, 'svelte-html.d.ts'); + svelteHtmlPath = + svelteHtmlPath && tsSystem.fileExists(svelteHtmlPath) ? svelteHtmlPath : undefined; let svelteTsxFiles: string[]; if (isSvelte3) { svelteTsxFiles = ['./svelte-shims.d.ts', './svelte-jsx.d.ts', './svelte-native-jsx.d.ts']; } else { svelteTsxFiles = ['./svelte-shims-v4.d.ts', './svelte-native-jsx.d.ts']; - if (!svelteHtmlPathExists) { - svelteTsxFiles.push(svelteHtmlPath); + if (!svelteHtmlPath) { + svelteTsxFiles.push('./svelte-jsx-v4.d.ts'); } } svelteTsxFiles = svelteTsxFiles.map((f) => tsSystem.resolvePath(resolve(typesPath, f))); @@ -53,10 +53,20 @@ export function get_global_types( for (const f of svelteTsxFiles) { const hiddenFile = resolve(hiddenPath, basename(f)); const existing = tsSystem.readFile(hiddenFile); - const toWrite = tsSystem.readFile(f) || ''; + const toWrite = tsSystem.readFile(f); + + if (!toWrite) { + throw new Error(`Could not read file: ${f}`); + } + if (existing !== toWrite) { tsSystem.writeFile(hiddenFile, toWrite); + // TS doesn't throw an error if the file wasn't written + if (!tsSystem.fileExists(hiddenFile)) { + throw new Error(`Could not write file: ${hiddenFile}`); + } } + newFiles.push(hiddenFile); } svelteTsxFiles = newFiles; @@ -64,8 +74,8 @@ export function get_global_types( } catch (e) {} } - if (svelteHtmlPathExists) { - svelteTsxFiles.push(tsSystem.resolvePath(resolve(typesPath, svelteHtmlFile))); + if (svelteHtmlPath) { + svelteTsxFiles.push(tsSystem.resolvePath(resolve(typesPath, svelteHtmlPath))); } return svelteTsxFiles; From bf2e459926ecf318845d9a0283d7d055facb25a0 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Fri, 15 Nov 2024 13:54:40 +0100 Subject: [PATCH 4/4] fix: hoist types related to `$props` rune if possible (#2571) This allows TypeScript to resolve the type more easily, especialy when in dts mode. The advantage is that now the type would be preserved as written, whereas without it the type would be inlined/infered, i.e. the interface that declares the props would not be kept --- packages/svelte2tsx/src/svelte2tsx/index.ts | 11 +- .../src/svelte2tsx/nodes/ExportedNames.ts | 275 +++++++------- .../svelte2tsx/nodes/HoistableInterfaces.ts | 335 ++++++++++++++++++ .../processInstanceScriptContent.ts | 17 +- .../src/svelte2tsx/processModuleScriptTag.ts | 23 +- .../expected/TestRunes.svelte.d.ts | 5 +- .../expected/TestRunes.svelte.d.ts | 8 - .../expected/TestRunes1.svelte.d.ts | 9 + .../expected/TestRunes2.svelte.d.ts | 9 + .../{TestRunes.svelte => TestRunes1.svelte} | 0 .../typescript-runes.v5/src/TestRunes2.svelte | 9 + .../expectedv2.ts | 27 ++ .../input.svelte | 20 ++ .../expectedv2.ts | 19 + .../input.svelte | 12 + .../expectedv2.ts | 18 + .../input.svelte | 11 + .../expectedv2.ts | 17 + .../input.svelte | 10 + .../expectedv2.ts | 16 + .../input.svelte | 9 + .../samples/ts-runes.v5/expectedv2.ts | 4 +- .../expectedv2.ts | 4 +- 23 files changed, 713 insertions(+), 155 deletions(-) create mode 100644 packages/svelte2tsx/src/svelte2tsx/nodes/HoistableInterfaces.ts delete mode 100644 packages/svelte2tsx/test/emitDts/samples/typescript-runes.v5/expected/TestRunes.svelte.d.ts create mode 100644 packages/svelte2tsx/test/emitDts/samples/typescript-runes.v5/expected/TestRunes1.svelte.d.ts create mode 100644 packages/svelte2tsx/test/emitDts/samples/typescript-runes.v5/expected/TestRunes2.svelte.d.ts rename packages/svelte2tsx/test/emitDts/samples/typescript-runes.v5/src/{TestRunes.svelte => TestRunes1.svelte} (100%) create mode 100644 packages/svelte2tsx/test/emitDts/samples/typescript-runes.v5/src/TestRunes2.svelte create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-1.v5/expectedv2.ts create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-1.v5/input.svelte create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-2.v5/expectedv2.ts create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-2.v5/input.svelte create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-4.v5/expectedv2.ts create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-4.v5/input.svelte create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-1.v5/expectedv2.ts create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-1.v5/input.svelte create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-2.v5/expectedv2.ts create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-2.v5/input.svelte 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 d3083bb88..b8851b23c 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,141 +235,132 @@ 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 initializer = - ts.isCallExpression(element.initializer) && - ts.isIdentifier(element.initializer.expression) && - element.initializer.expression.text === '$bindable' - ? element.initializer.arguments[0] - : element.initializer; - const type = !initializer - ? 'any' - : ts.isAsExpression(initializer) - ? initializer.type.getText() - : ts.isStringLiteral(initializer) - ? 'string' - : ts.isNumericLiteral(initializer) - ? 'number' - : initializer.kind === ts.SyntaxKind.TrueKeyword || - initializer.kind === ts.SyntaxKind.FalseKeyword - ? 'boolean' - : ts.isIdentifier(initializer) && - initializer.text !== 'undefined' - ? `typeof ${initializer.text}` - : ts.isArrowFunction(initializer) - ? 'Function' - : ts.isObjectLiteralExpression(initializer) - ? 'Record' - : ts.isArrayLiteralExpression(initializer) - ? 'any[]' - : 'any'; - props.push(`${name}?: ${type}`); - } else { - props.push(`${name}: any`); + 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 initializer = + ts.isCallExpression(element.initializer) && + ts.isIdentifier(element.initializer.expression) && + element.initializer.expression.text === '$bindable' + ? element.initializer.arguments[0] + : element.initializer; + + const type = !initializer + ? 'any' + : ts.isAsExpression(initializer) + ? initializer.type.getText() + : ts.isStringLiteral(initializer) + ? 'string' + : ts.isNumericLiteral(initializer) + ? 'number' + : initializer.kind === ts.SyntaxKind.TrueKeyword || + initializer.kind === ts.SyntaxKind.FalseKeyword + ? 'boolean' + : ts.isIdentifier(initializer) && + initializer.text !== 'undefined' + ? `typeof ${initializer.text}` + : ts.isArrowFunction(initializer) + ? 'Function' + : ts.isObjectLiteralExpression(initializer) + ? 'Record' + : ts.isArrayLiteralExpression(initializer) + ? 'any[]' + : 'any'; + + props.push(`${name}?: ${type}`); + } else { + props.push(`${name}: any`); } } + } - 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..4337c3503 --- /dev/null +++ b/packages/svelte2tsx/src/svelte2tsx/nodes/HoistableInterfaces.ts @@ -0,0 +1,335 @@ +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 || element.isTypeOnly) { + 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 + ); + } else if (ts.isIndexSignatureDeclaration(member)) { + this.collectTypeDependencies( + member.type, + type_dependencies, + value_dependencies, + generics + ); + member.parameters.forEach((param) => { + this.collectTypeDependencies( + param.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); + }; + + walk(type_node); + } + + /** + * 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/javascript-runes.v5/expected/TestRunes.svelte.d.ts b/packages/svelte2tsx/test/emitDts/samples/javascript-runes.v5/expected/TestRunes.svelte.d.ts index 350d1d487..70e0a214d 100644 --- a/packages/svelte2tsx/test/emitDts/samples/javascript-runes.v5/expected/TestRunes.svelte.d.ts +++ b/packages/svelte2tsx/test/emitDts/samples/javascript-runes.v5/expected/TestRunes.svelte.d.ts @@ -1,7 +1,8 @@ -declare const TestRunes: import("svelte").Component<{ +type $$ComponentProps = { foo: string; bar?: number; -}, { +}; +declare const TestRunes: import("svelte").Component<$$ComponentProps, { baz: () => void; }, "bar">; type TestRunes = ReturnType; 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-hoistable-props-4.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-4.v5/expectedv2.ts new file mode 100644 index 000000000..1fedd1021 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-4.v5/expectedv2.ts @@ -0,0 +1,18 @@ +/// + + interface Dependency { + a: number; + } + + interface Props { + [k: string]: Dependency; + };function render() { + + + let { foo }: 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-4.v5/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-4.v5/input.svelte new file mode 100644 index 000000000..acf3829ba --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-4.v5/input.svelte @@ -0,0 +1,11 @@ + diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-1.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-1.v5/expectedv2.ts new file mode 100644 index 000000000..abd2b797d --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-1.v5/expectedv2.ts @@ -0,0 +1,17 @@ +/// +;function render() { + + interface Props { + foo: C; + } + + const a = 1; + type C = typeof a | '2' | '3'; + + let { foo }: 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-false-1.v5/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-1.v5/input.svelte new file mode 100644 index 000000000..9de8cb5c2 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-1.v5/input.svelte @@ -0,0 +1,10 @@ + diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-2.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-2.v5/expectedv2.ts new file mode 100644 index 000000000..9aaaa85e8 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-2.v5/expectedv2.ts @@ -0,0 +1,16 @@ +/// +;function render() { + + const a: string = ''; + + interface Props { + [index: typeof a]: boolean; + } + + let { foo }: 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-false-2.v5/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-2.v5/input.svelte new file mode 100644 index 000000000..38a5ded93 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-2.v5/input.svelte @@ -0,0 +1,9 @@ + 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 () => {}; pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy