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 01/17] 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 02/17] 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 03/17] 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 () => {}; From fba28b2ddf5da7834ba380c605fdf8a37e658c5d Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Sat, 16 Nov 2024 19:00:17 +0100 Subject: [PATCH 04/17] fix: detect shadowed variables/types during type hoisting (#2590) #2589 --- .../src/svelte2tsx/nodes/Generics.ts | 6 ++ .../svelte2tsx/nodes/HoistableInterfaces.ts | 68 ++++++++++++++++--- .../processInstanceScriptContent.ts | 7 +- .../expectedv2.ts | 34 ++++++++++ .../input.svelte | 8 +++ .../expectedv2.ts | 15 ++++ .../input.svelte | 8 +++ .../expectedv2.ts | 15 ++++ .../input.svelte | 8 +++ 9 files changed, 158 insertions(+), 11 deletions(-) create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-3.v5/expectedv2.ts create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-3.v5/input.svelte create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-4.v5/expectedv2.ts create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-4.v5/input.svelte create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-5.v5/expectedv2.ts create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-5.v5/input.svelte diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/Generics.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/Generics.ts index 79d1918a4..982626652 100644 --- a/packages/svelte2tsx/src/svelte2tsx/nodes/Generics.ts +++ b/packages/svelte2tsx/src/svelte2tsx/nodes/Generics.ts @@ -5,8 +5,10 @@ import { surroundWithIgnoreComments } from '../../utils/ignore'; import { throwError } from '../utils/error'; export class Generics { + /** The whole `T extends boolean` */ private definitions: string[] = []; private typeReferences: string[] = []; + /** The `T` in `T extends boolean` */ private references: string[] = []; genericsAttr: Node | undefined; @@ -93,6 +95,10 @@ export class Generics { return this.typeReferences; } + getReferences() { + return this.references; + } + toDefinitionString(addIgnore = false) { const surround = addIgnore ? surroundWithIgnoreComments : (str: string) => str; return this.definitions.length ? surround(`<${this.definitions.join(',')}>`) : ''; diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/HoistableInterfaces.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/HoistableInterfaces.ts index 4337c3503..0ad8d535b 100644 --- a/packages/svelte2tsx/src/svelte2tsx/nodes/HoistableInterfaces.ts +++ b/packages/svelte2tsx/src/svelte2tsx/nodes/HoistableInterfaces.ts @@ -167,11 +167,16 @@ export class HoistableInterfaces { } }); - this.interface_map.set(interface_name, { - type_deps: type_dependencies, - value_deps: value_dependencies, - node - }); + if (this.import_type_set.has(interface_name)) { + // shadowed; delete because we can't hoist + this.import_type_set.delete(interface_name); + } else { + this.interface_map.set(interface_name, { + type_deps: type_dependencies, + value_deps: value_dependencies, + node + }); + } } // Handle Type Alias Declarations @@ -188,12 +193,46 @@ export class HoistableInterfaces { generics ); - this.interface_map.set(alias_name, { - type_deps: type_dependencies, - value_deps: value_dependencies, - node + if (this.import_type_set.has(alias_name)) { + // shadowed; delete because we can't hoist + this.import_type_set.delete(alias_name); + } else { + this.interface_map.set(alias_name, { + type_deps: type_dependencies, + value_deps: value_dependencies, + node + }); + } + } + + // Handle top-level declarations: They could shadow module declarations; delete them from the set of allowed import values + if (ts.isVariableStatement(node)) { + node.declarationList.declarations.forEach((declaration) => { + if (ts.isIdentifier(declaration.name)) { + this.import_value_set.delete(declaration.name.text); + } }); } + + if (ts.isFunctionDeclaration(node) && node.name) { + this.import_value_set.delete(node.name.text); + } + + if (ts.isClassDeclaration(node) && node.name) { + this.import_value_set.delete(node.name.text); + } + + if (ts.isEnumDeclaration(node)) { + this.import_value_set.delete(node.name.text); + } + + if (ts.isTypeAliasDeclaration(node)) { + this.import_type_set.delete(node.name.text); + } + + if (ts.isInterfaceDeclaration(node)) { + this.import_type_set.delete(node.name.text); + } } analyze$propsRune( @@ -280,9 +319,18 @@ export class HoistableInterfaces { /** * 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) { + moveHoistableInterfaces( + str: MagicString, + astOffset: number, + scriptStart: number, + generics: string[] + ) { if (!this.props_interface.name) return; + for (const generic of generics) { + this.import_type_set.delete(generic); + } + const hoistable = this.determineHoistableInterfaces(); if (hoistable.has(this.props_interface.name)) { for (const [, node] of hoistable) { diff --git a/packages/svelte2tsx/src/svelte2tsx/processInstanceScriptContent.ts b/packages/svelte2tsx/src/svelte2tsx/processInstanceScriptContent.ts index e7d66870c..5b504ec6a 100644 --- a/packages/svelte2tsx/src/svelte2tsx/processInstanceScriptContent.ts +++ b/packages/svelte2tsx/src/svelte2tsx/processInstanceScriptContent.ts @@ -318,7 +318,12 @@ export function processInstanceScriptContent( transformInterfacesToTypes(tsAst, str, astOffset, nodesToMove); } - exportedNames.hoistableInterfaces.moveHoistableInterfaces(str, astOffset, script.start); + exportedNames.hoistableInterfaces.moveHoistableInterfaces( + str, + astOffset, + script.start, + generics.getReferences() + ); return { exportedNames, diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-3.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-3.v5/expectedv2.ts new file mode 100644 index 000000000..bec5d85d2 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-3.v5/expectedv2.ts @@ -0,0 +1,34 @@ +/// +; + type SomeType = T; + type T = unknown; +;;function render() { +;type $$ComponentProps = { someProp: SomeType; }; + let { someProp }:/*Ωignore_startΩ*/$$ComponentProps/*Ωignore_endΩ*/ = $props(); +; +async () => { + +}; +return { props: {} as any as $$ComponentProps, exports: {}, bindings: __sveltets_$$bindings(''), slots: {}, events: {} }} +class __sveltets_Render { + props() { + return render().props; + } + events() { + return render().events; + } + slots() { + return render().slots; + } + bindings() { return __sveltets_$$bindings(''); } + exports() { return {}; } +} + +interface $$IsomorphicComponent { + new (options: import('svelte').ComponentConstructorOptions['props']>>): import('svelte').SvelteComponent['props']>, ReturnType<__sveltets_Render['events']>, ReturnType<__sveltets_Render['slots']>> & { $$bindings?: ReturnType<__sveltets_Render['bindings']> } & ReturnType<__sveltets_Render['exports']>; + (internal: unknown, props: ReturnType<__sveltets_Render['props']> & {}): ReturnType<__sveltets_Render['exports']>; + z_$$bindings?: ReturnType<__sveltets_Render['bindings']>; +} +const Input__SvelteComponent_: $$IsomorphicComponent = null as any; +/*Ωignore_startΩ*/type Input__SvelteComponent_ = InstanceType>; +/*Ωignore_endΩ*/export default Input__SvelteComponent_; \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-3.v5/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-3.v5/input.svelte new file mode 100644 index 000000000..af13570b3 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-3.v5/input.svelte @@ -0,0 +1,8 @@ + + + diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-4.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-4.v5/expectedv2.ts new file mode 100644 index 000000000..d7fe39298 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-4.v5/expectedv2.ts @@ -0,0 +1,15 @@ +/// +; + let a = ''; +;;function render() { + + let a = true;;type $$ComponentProps = { someProp: typeof a }; + let { someProp }:/*Ω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-false-4.v5/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-4.v5/input.svelte new file mode 100644 index 000000000..a892a8c9f --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-4.v5/input.svelte @@ -0,0 +1,8 @@ + + + diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-5.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-5.v5/expectedv2.ts new file mode 100644 index 000000000..9455873cb --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-5.v5/expectedv2.ts @@ -0,0 +1,15 @@ +/// +; + type Shadowed = string; +;;function render() { + + type Shadowed = boolean;;type $$ComponentProps = { someProp: Shadowed }; + let { someProp }:/*Ω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-false-5.v5/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-5.v5/input.svelte new file mode 100644 index 000000000..3b2b98957 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-5.v5/input.svelte @@ -0,0 +1,8 @@ + + + From b6cac97870191678e53388fb2055152b21f69f9f Mon Sep 17 00:00:00 2001 From: "Lyu, Wei-Da" <36730922+jasonlyu123@users.noreply.github.com> Date: Wed, 20 Nov 2024 18:36:34 +0800 Subject: [PATCH 05/17] fix: use original file path casing for shim files (#2591) #2584 Use original casing here: people could have their VS Code extensions in a case insensitive folder but their project in a case sensitive one; and if we copy the shims into the case sensitive part it would break when canonicalizing it. --- .../src/plugins/typescript/service.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/language-server/src/plugins/typescript/service.ts b/packages/language-server/src/plugins/typescript/service.ts index 47321a531..4002f5c75 100644 --- a/packages/language-server/src/plugins/typescript/service.ts +++ b/packages/language-server/src/plugins/typescript/service.ts @@ -377,7 +377,7 @@ async function createLanguageService( : undefined; const changedFilesForExportCache = new Set(); - const svelteTsxFiles = getSvelteShimFiles(); + const svelteTsxFilesToOriginalCasing = getSvelteShimFiles(); let languageServiceReducedMode = false; let projectVersion = 0; @@ -700,7 +700,10 @@ async function createLanguageService( ...clientFiles.filter( (file) => !canonicalProjectFileNames.has(getCanonicalFileName(file)) ), - ...svelteTsxFiles + // Use original casing here, too: people could have their VS Code extensions in a case insensitive + // folder but their project in a case sensitive one; and if we copy the shims into the case sensitive + // part it would break when canonicalizing it. + ...svelteTsxFilesToOriginalCasing.values() ]) ); } @@ -1220,14 +1223,17 @@ async function createLanguageService( svelteTsPath, docContext.isSvelteCheck ? undefined : tsconfigPath || workspacePath ); - const result = new FileSet(tsSystem.useCaseSensitiveFileNames); + const pathToOriginalCasing = new Map(); + for (const file of svelteTsxFiles) { + const normalizedPath = normalizePath(file); + pathToOriginalCasing.set(getCanonicalFileName(normalizedPath), normalizedPath); + } - svelteTsxFiles.forEach((f) => result.add(normalizePath(f))); - return result; + return pathToOriginalCasing; } function isShimFiles(filePath: string) { - return svelteTsxFiles.has(normalizePath(filePath)); + return svelteTsxFilesToOriginalCasing.has(getCanonicalFileName(normalizePath(filePath))); } } From c21de6648823a5a8ca60122b1987bd765e81e539 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Wed, 20 Nov 2024 12:04:56 +0100 Subject: [PATCH 06/17] fix: don't move appended content from previous node while hoisting interface (#2596) #2592 --- .../src/svelte2tsx/nodes/HoistableInterfaces.ts | 17 +++++++++++++++-- .../ts-runes-hoistable-props-1.v5/expectedv2.ts | 3 +++ .../ts-runes-hoistable-props-2.v5/expectedv2.ts | 1 + .../ts-runes-hoistable-props-4.v5/expectedv2.ts | 2 ++ 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/HoistableInterfaces.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/HoistableInterfaces.ts index 0ad8d535b..d64776d27 100644 --- a/packages/svelte2tsx/src/svelte2tsx/nodes/HoistableInterfaces.ts +++ b/packages/svelte2tsx/src/svelte2tsx/nodes/HoistableInterfaces.ts @@ -333,8 +333,21 @@ export class HoistableInterfaces { 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); + for (const [name, node] of hoistable) { + let pos = node.pos + astOffset; + + // node.pos includes preceeding whitespace, which could mean we accidentally also move stuff appended to a previous node + if (name !== '$$ComponentProps') { + if (str.original[pos] === '\r') { + pos++; + } + if (/\s/.test(str.original[pos])) { + pos++; + str.prependRight(pos, '\n'); + } + } + + str.move(pos, node.end + astOffset, scriptStart); } } } 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 index c14fcdf06..36c5a9cc3 100644 --- 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 @@ -16,6 +16,9 @@ };function render() { + + + let { a, b }: Props = $props(); ; async () => { 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 index 664f10006..145efb553 100644 --- 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 @@ -8,6 +8,7 @@ };type $$ComponentProps = { a: Dependency, b: string };;function render() { + let { a, b }:/*Ωignore_startΩ*/$$ComponentProps/*Ωignore_endΩ*/ = $props(); ; async () => { 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 index 1fedd1021..f16fc5f24 100644 --- 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 @@ -9,6 +9,8 @@ };function render() { + + let { foo }: Props = $props(); ; async () => {}; From a1b4a6430a83c21e4d1abb1f1a48f20a0dea3680 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Wed, 20 Nov 2024 12:44:34 +0100 Subject: [PATCH 07/17] fix: ensure hoisted interfaces are moved after hoisted imports (#2597) #2594 As a drive-by, I also ensures that the dts generation knows about the hoisted interfaces so it does not transform them into types anymore --- .../svelte2tsx/nodes/HoistableInterfaces.ts | 8 +++++- .../processInstanceScriptContent.ts | 25 +++++++++++------ .../expected/TestRunes2.svelte.d.ts | 6 ++-- .../expectedv2.ts | 6 ++-- .../expectedv2.ts | 4 +-- .../expectedv2.ts | 4 +-- .../expectedv2.ts | 28 +++++++++++++++++++ .../input.svelte | 17 +++++++++++ .../samples/ts-runes.v5/expectedv2.ts | 2 +- .../expectedv2.ts | 2 +- 10 files changed, 81 insertions(+), 21 deletions(-) create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-5.v5/expectedv2.ts create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-5.v5/input.svelte diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/HoistableInterfaces.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/HoistableInterfaces.ts index d64776d27..647926ed0 100644 --- a/packages/svelte2tsx/src/svelte2tsx/nodes/HoistableInterfaces.ts +++ b/packages/svelte2tsx/src/svelte2tsx/nodes/HoistableInterfaces.ts @@ -332,6 +332,7 @@ export class HoistableInterfaces { } const hoistable = this.determineHoistableInterfaces(); + if (hoistable.has(this.props_interface.name)) { for (const [name, node] of hoistable) { let pos = node.pos + astOffset; @@ -343,12 +344,17 @@ export class HoistableInterfaces { } if (/\s/.test(str.original[pos])) { pos++; - str.prependRight(pos, '\n'); } + + // jsdoc comments would be ignored if they are on the same line as the ;, so we add a newline, too + str.prependRight(pos, ';\n'); + str.appendLeft(node.end + astOffset, ';'); } str.move(pos, node.end + astOffset, scriptStart); } + + return hoistable; } } diff --git a/packages/svelte2tsx/src/svelte2tsx/processInstanceScriptContent.ts b/packages/svelte2tsx/src/svelte2tsx/processInstanceScriptContent.ts index 5b504ec6a..4790739cb 100644 --- a/packages/svelte2tsx/src/svelte2tsx/processInstanceScriptContent.ts +++ b/packages/svelte2tsx/src/svelte2tsx/processInstanceScriptContent.ts @@ -310,21 +310,30 @@ export function processInstanceScriptContent( moveNode(node, str, astOffset, script.start, tsAst); } + const hoisted = exportedNames.hoistableInterfaces.moveHoistableInterfaces( + str, + astOffset, + script.start + 1, // +1 because imports are also moved at that position, and we want to move interfaces after imports + generics.getReferences() + ); + if (mode === 'dts') { // Transform interface declarations to type declarations because indirectly // using interfaces inside the return type of a function is forbidden. // This is not a problem for intellisense/type inference but it will // break dts generation (file will not be generated). - transformInterfacesToTypes(tsAst, str, astOffset, nodesToMove); + if (hoisted) { + transformInterfacesToTypes( + tsAst, + str, + astOffset, + [...hoisted.values()].concat(nodesToMove) + ); + } else { + transformInterfacesToTypes(tsAst, str, astOffset, nodesToMove); + } } - exportedNames.hoistableInterfaces.moveHoistableInterfaces( - str, - astOffset, - script.start, - generics.getReferences() - ); - return { exportedNames, events, 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 index 885a90203..1cbf712b3 100644 --- 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 @@ -1,9 +1,9 @@ +import type { X } from './x'; /** asd */ -type Props = { +interface 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/svelte2tsx/samples/ts-runes-hoistable-props-1.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-1.v5/expectedv2.ts index 36c5a9cc3..2687887c0 100644 --- 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 @@ -1,13 +1,13 @@ /// ; let value = 1; -; - type NoComma = true +;;; + type NoComma = true;; type Dependency = { a: number; b: typeof value; c: NoComma - } + };; /** A comment */ interface Props { 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 index 145efb553..556d5bb46 100644 --- 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 @@ -1,11 +1,11 @@ /// ; let value = 1; -; +;;; interface Dependency { a: number; b: typeof value; - };type $$ComponentProps = { a: Dependency, b: string };;function render() { + };;type $$ComponentProps = { a: Dependency, b: string };function render() { 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 index f16fc5f24..28978197d 100644 --- 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 @@ -1,8 +1,8 @@ /// - +;; interface Dependency { a: number; - } + };; interface Props { [k: string]: Dependency; diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-5.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-5.v5/expectedv2.ts new file mode 100644 index 000000000..e3b3517c3 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-5.v5/expectedv2.ts @@ -0,0 +1,28 @@ +/// +; + import X from './X'; +;; + +import { readable } from 'svelte/store'; +; + + /** I should not be sandwitched between the imports */ + interface Props { + foo?: string; + };function render() { + + + + const store = readable(1)/*Ωignore_startΩ*/;let $store = __sveltets_2_store_get(store);/*Ωignore_endΩ*/ + + let { foo }: Props = $props() +; +async () => { + + + +$store;}; +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-5.v5/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-5.v5/input.svelte new file mode 100644 index 000000000..91254d3c8 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-5.v5/input.svelte @@ -0,0 +1,17 @@ + + + + +{$store} 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 b9ee4758c..b15ca4992 100644 --- a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes.v5/expectedv2.ts +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes.v5/expectedv2.ts @@ -1,5 +1,5 @@ /// -;type $$ComponentProps = { a: number, b: string };;function render() { +;;type $$ComponentProps = { a: number, b: string };function render() { let { a, b }:/*Ωignore_startΩ*/$$ComponentProps/*Ωignore_endΩ*/ = $props(); let x = $state(0); 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 2bdf2e4ea..51e17c6ce 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,5 +1,5 @@ /// -;type $$ComponentProps = {form: boolean, data: true };;function render() { +;;type $$ComponentProps = {form: boolean, data: true };function render() { const snapshot: any = {}; let { form, data }:/*Ωignore_startΩ*/$$ComponentProps/*Ωignore_endΩ*/ = $props(); From 078f9a086cc85fc5386cc5af6923b0d3ae32f70c Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Wed, 20 Nov 2024 15:13:56 +0100 Subject: [PATCH 08/17] feat: support `` (#2598) companion to https://github.com/sveltejs/svelte/pull/14211 test skipped while feature is not merged yet --- .../src/plugins/html/dataProvider.ts | 23 +++++++++++++++++++ .../svelte2tsx/src/htmlxtojsx_v2/index.ts | 8 +++++-- .../src/htmlxtojsx_v2/nodes/SnippetBlock.ts | 23 ++++++++++++++----- .../samples/.svelte-boundary.v5/expectedv2.js | 5 ++++ .../samples/.svelte-boundary.v5/input.svelte | 6 +++++ 5 files changed, 57 insertions(+), 8 deletions(-) create mode 100644 packages/svelte2tsx/test/htmlx2jsx/samples/.svelte-boundary.v5/expectedv2.js create mode 100644 packages/svelte2tsx/test/htmlx2jsx/samples/.svelte-boundary.v5/input.svelte diff --git a/packages/language-server/src/plugins/html/dataProvider.ts b/packages/language-server/src/plugins/html/dataProvider.ts index 3031552c4..30495b6bd 100644 --- a/packages/language-server/src/plugins/html/dataProvider.ts +++ b/packages/language-server/src/plugins/html/dataProvider.ts @@ -296,6 +296,17 @@ const svelteTags: ITagData[] = [ 'Named slots allow consumers to target specific areas. They can also have fallback content.' } ] + }, + { + name: 'svelte:boundary', + description: + 'Represents a boundary in the application. Can catch errors and show fallback UI', + attributes: [ + { + name: 'onerror', + description: 'Called when an error occured within the boundary' + } + ] } ]; @@ -419,6 +430,18 @@ export const svelteHtmlDataProvider = newHTMLDataProvider('svelte-builtin', { })) ?? [] }); +const originalProvideAttributes = + svelteHtmlDataProvider.provideAttributes.bind(svelteHtmlDataProvider); + +svelteHtmlDataProvider.provideAttributes = (tag: string) => { + if (tag === 'svelte:boundary' || tag === 'svelte:options') { + // We don't want the global attributes for these tags + return svelteTags.find((t) => t.name === tag)?.attributes ?? []; + } + + return originalProvideAttributes(tag); +}; + function isEvent(attr: IAttributeData) { return attr.name.startsWith('on'); } diff --git a/packages/svelte2tsx/src/htmlxtojsx_v2/index.ts b/packages/svelte2tsx/src/htmlxtojsx_v2/index.ts index 0fced1e1c..95f8cec4f 100644 --- a/packages/svelte2tsx/src/htmlxtojsx_v2/index.ts +++ b/packages/svelte2tsx/src/htmlxtojsx_v2/index.ts @@ -88,8 +88,10 @@ export function convertHtmlxToJsx( handleSnippet( str, node, - element instanceof InlineComponent && - estreeTypedParent.type === 'InlineComponent' + (element instanceof InlineComponent && + estreeTypedParent.type === 'InlineComponent') || + (element instanceof Element && + element.tagName === 'svelte:boundary') ? element : undefined ); @@ -133,6 +135,7 @@ export function convertHtmlxToJsx( case 'Title': case 'Document': case 'Body': + case 'SvelteBoundary': case 'Slot': case 'SlotTemplate': if (node.name !== '!DOCTYPE') { @@ -236,6 +239,7 @@ export function convertHtmlxToJsx( case 'Head': case 'Title': case 'Body': + case 'SvelteBoundary': case 'Document': case 'Slot': case 'SlotTemplate': diff --git a/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/SnippetBlock.ts b/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/SnippetBlock.ts index 4a8957700..701e1b83a 100644 --- a/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/SnippetBlock.ts +++ b/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/SnippetBlock.ts @@ -3,6 +3,7 @@ import { BaseNode } from '../../interfaces'; import { transform, TransformationArray } from '../utils/node-utils'; import { InlineComponent } from './InlineComponent'; import { IGNORE_POSITION_COMMENT, surroundWithIgnoreComments } from '../../utils/ignore'; +import { Element } from './Element'; /** * Transform #snippet into a function @@ -28,7 +29,7 @@ import { IGNORE_POSITION_COMMENT, surroundWithIgnoreComments } from '../../utils export function handleSnippet( str: MagicString, snippetBlock: BaseNode, - component?: InlineComponent + component?: InlineComponent | Element ): void { const isImplicitProp = component !== undefined; const endSnippet = str.original.lastIndexOf('{', snippetBlock.end - 1); @@ -64,6 +65,7 @@ export function handleSnippet( if (isImplicitProp) { str.overwrite(snippetBlock.start, snippetBlock.expression.start, '', { contentOnly: true }); const transforms: TransformationArray = ['(']; + if (parameters) { transforms.push(parameters); const [start, end] = parameters; @@ -74,12 +76,21 @@ export function handleSnippet( } else { str.overwrite(snippetBlock.expression.end, startEnd, '', { contentOnly: true }); } + transforms.push(')' + afterParameters); transforms.push([startEnd, snippetBlock.end]); - component.addImplicitSnippetProp( - [snippetBlock.expression.start, snippetBlock.expression.end], - transforms - ); + + if (component instanceof InlineComponent) { + component.addImplicitSnippetProp( + [snippetBlock.expression.start, snippetBlock.expression.end], + transforms + ); + } else { + component.addAttribute( + [[snippetBlock.expression.start, snippetBlock.expression.end]], + transforms + ); + } } else { const transforms: TransformationArray = [ 'const ', @@ -149,7 +160,7 @@ export function handleImplicitChildren(componentNode: BaseNode, component: Inlin } export function hoistSnippetBlock(str: MagicString, blockOrEl: BaseNode) { - if (blockOrEl.type === 'InlineComponent') { + if (blockOrEl.type === 'InlineComponent' || blockOrEl.type === 'SvelteBoundary') { // implicit props, handled in InlineComponent return; } diff --git a/packages/svelte2tsx/test/htmlx2jsx/samples/.svelte-boundary.v5/expectedv2.js b/packages/svelte2tsx/test/htmlx2jsx/samples/.svelte-boundary.v5/expectedv2.js new file mode 100644 index 000000000..1f398adda --- /dev/null +++ b/packages/svelte2tsx/test/htmlx2jsx/samples/.svelte-boundary.v5/expectedv2.js @@ -0,0 +1,5 @@ + { svelteHTML.createElement("svelte:boundary", { "onerror":e => e,failed:(e) => { async ()/*Ωignore_positionΩ*/ => { + { svelteHTML.createElement("p", {}); e; } + };return __sveltets_2_any(0)},}); { const $$_sliaFtahTtnenopmoC1C = __sveltets_2_ensureComponent(ComponentThatFails); new $$_sliaFtahTtnenopmoC1C({ target: __sveltets_2_any(), props: {}});} + + } \ No newline at end of file diff --git a/packages/svelte2tsx/test/htmlx2jsx/samples/.svelte-boundary.v5/input.svelte b/packages/svelte2tsx/test/htmlx2jsx/samples/.svelte-boundary.v5/input.svelte new file mode 100644 index 000000000..7bcb64b7c --- /dev/null +++ b/packages/svelte2tsx/test/htmlx2jsx/samples/.svelte-boundary.v5/input.svelte @@ -0,0 +1,6 @@ + e}> + + {#snippet failed(e)} +

error: {e}

+ {/snippet} +
From cd1758b16f4cb6b176980b2c868efa3c98d30343 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Wed, 20 Nov 2024 15:38:44 +0100 Subject: [PATCH 09/17] feat: support `` (#2599) --- packages/language-server/src/plugins/html/dataProvider.ts | 6 ++++++ packages/svelte2tsx/src/htmlxtojsx_v2/index.ts | 2 ++ .../test/htmlx2jsx/samples/.svelte-html.v5/expectedv2.js | 1 + .../test/htmlx2jsx/samples/.svelte-html.v5/input.svelte | 1 + 4 files changed, 10 insertions(+) create mode 100644 packages/svelte2tsx/test/htmlx2jsx/samples/.svelte-html.v5/expectedv2.js create mode 100644 packages/svelte2tsx/test/htmlx2jsx/samples/.svelte-html.v5/input.svelte diff --git a/packages/language-server/src/plugins/html/dataProvider.ts b/packages/language-server/src/plugins/html/dataProvider.ts index 30495b6bd..df998d61c 100644 --- a/packages/language-server/src/plugins/html/dataProvider.ts +++ b/packages/language-server/src/plugins/html/dataProvider.ts @@ -200,6 +200,12 @@ const svelteTags: ITagData[] = [ } ] }, + { + name: 'svelte:html', + description: + 'This element allows you to add properties and listeners to events on `document.documentElement`. This is useful for attributes such as `lang` which influence how the browser interprets the content.', + attributes: [] + }, { name: 'svelte:document', description: diff --git a/packages/svelte2tsx/src/htmlxtojsx_v2/index.ts b/packages/svelte2tsx/src/htmlxtojsx_v2/index.ts index 95f8cec4f..913cc54bc 100644 --- a/packages/svelte2tsx/src/htmlxtojsx_v2/index.ts +++ b/packages/svelte2tsx/src/htmlxtojsx_v2/index.ts @@ -135,6 +135,7 @@ export function convertHtmlxToJsx( case 'Title': case 'Document': case 'Body': + case 'SvelteHTML': case 'SvelteBoundary': case 'Slot': case 'SlotTemplate': @@ -239,6 +240,7 @@ export function convertHtmlxToJsx( case 'Head': case 'Title': case 'Body': + case 'SvelteHTML': case 'SvelteBoundary': case 'Document': case 'Slot': diff --git a/packages/svelte2tsx/test/htmlx2jsx/samples/.svelte-html.v5/expectedv2.js b/packages/svelte2tsx/test/htmlx2jsx/samples/.svelte-html.v5/expectedv2.js new file mode 100644 index 000000000..23e947d26 --- /dev/null +++ b/packages/svelte2tsx/test/htmlx2jsx/samples/.svelte-html.v5/expectedv2.js @@ -0,0 +1 @@ + { svelteHTML.createElement("svelte:html", { "lang":`de`,}); } \ No newline at end of file diff --git a/packages/svelte2tsx/test/htmlx2jsx/samples/.svelte-html.v5/input.svelte b/packages/svelte2tsx/test/htmlx2jsx/samples/.svelte-html.v5/input.svelte new file mode 100644 index 000000000..cd4ed850b --- /dev/null +++ b/packages/svelte2tsx/test/htmlx2jsx/samples/.svelte-html.v5/input.svelte @@ -0,0 +1 @@ + From 050ecc1b27a537a305492ef48edc03bb6064fbcb Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Thu, 21 Nov 2024 12:07:58 +0100 Subject: [PATCH 10/17] chore: consolidate template walking logic (#2600) right now some of the logic of walking the template is within the svelte2tsx part, some in the htmlxtojsx part. This makes it hard to decide where to put new stuff and represents an arbitrary barrier for features that may need to work across both. This PR therefore moves all the logic from the svelte2tsx part into the htmlxtojsx part. --- .../svelte2tsx/src/htmlxtojsx_v2/index.ts | 286 +++++++++++++++-- packages/svelte2tsx/src/svelte2tsx/index.ts | 298 +----------------- 2 files changed, 268 insertions(+), 316 deletions(-) diff --git a/packages/svelte2tsx/src/htmlxtojsx_v2/index.ts b/packages/svelte2tsx/src/htmlxtojsx_v2/index.ts index 913cc54bc..acc2f9c82 100644 --- a/packages/svelte2tsx/src/htmlxtojsx_v2/index.ts +++ b/packages/svelte2tsx/src/htmlxtojsx_v2/index.ts @@ -28,8 +28,39 @@ import { handleText } from './nodes/Text'; import { handleTransitionDirective } from './nodes/Transition'; import { handleImplicitChildren, handleSnippet, hoistSnippetBlock } from './nodes/SnippetBlock'; import { handleRenderTag } from './nodes/RenderTag'; +import { ComponentDocumentation } from '../svelte2tsx/nodes/ComponentDocumentation'; +import { ScopeStack } from '../svelte2tsx/utils/Scope'; +import { Stores } from '../svelte2tsx/nodes/Stores'; +import { Scripts } from '../svelte2tsx/nodes/Scripts'; +import { SlotHandler } from '../svelte2tsx/nodes/slot'; +import TemplateScope from '../svelte2tsx/nodes/TemplateScope'; +import { + handleScopeAndResolveForSlot, + handleScopeAndResolveLetVarForSlot +} from '../svelte2tsx/nodes/handleScopeAndResolveForSlot'; +import { EventHandler } from '../svelte2tsx/nodes/event-handler'; +import { ComponentEvents } from '../svelte2tsx/nodes/ComponentEvents'; -type Walker = (node: TemplateNode, parent: BaseNode, prop: string, index: number) => void; +export interface TemplateProcessResult { + /** + * The HTML part of the Svelte AST. + */ + htmlAst: TemplateNode; + uses$$props: boolean; + uses$$restProps: boolean; + uses$$slots: boolean; + slots: Map>; + scriptTag: BaseNode; + moduleScriptTag: BaseNode; + /** Start/end positions of snippets that should be moved to the instance script or possibly even module script */ + rootSnippets: Array<[number, number]>; + /** To be added later as a comment on the default class export */ + componentDocumentation: ComponentDocumentation; + events: ComponentEvents; + resolvedStores: string[]; + usesAccessors: boolean; + isRunes: boolean; +} function stripDoctype(str: MagicString): void { const regex = /(\n)?/i; @@ -46,18 +77,19 @@ function stripDoctype(str: MagicString): void { export function convertHtmlxToJsx( str: MagicString, ast: TemplateNode, - onWalk: Walker = null, - onLeave: Walker = null, + tags: BaseNode[], options: { - svelte5Plus: boolean; - preserveAttributeCase?: boolean; + emitOnTemplateError?: boolean; + namespace?: string; + accessors?: boolean; + mode?: 'ts' | 'dts'; typingsNamespace?: string; + svelte5Plus: boolean; } = { svelte5Plus: false } -) { - const htmlx = str.original; - options = { preserveAttributeCase: false, ...options }; +): TemplateProcessResult { options.typingsNamespace = options.typingsNamespace || 'svelteHTML'; - htmlx; + const preserveAttributeCase = options.namespace === 'foreign'; + stripDoctype(str); const rootSnippets: Array<[number, number]> = []; @@ -65,17 +97,131 @@ export function convertHtmlxToJsx( const pendingSnippetHoistCheck = new Set(); + let uses$$props = false; + let uses$$restProps = false; + let uses$$slots = false; + let usesAccessors = !!options.accessors; + let isRunes = false; + + const componentDocumentation = new ComponentDocumentation(); + + //track if we are in a declaration scope + const isDeclaration = { value: false }; + + //track $store variables since we are only supposed to give top level scopes special treatment, and users can declare $blah variables at higher scopes + //which prevents us just changing all instances of Identity that start with $ + + const scopeStack = new ScopeStack(); + const stores = new Stores(scopeStack, isDeclaration); + const scripts = new Scripts(ast); + + const handleSvelteOptions = (node: BaseNode) => { + for (let i = 0; i < node.attributes.length; i++) { + const optionName = node.attributes[i].name; + const optionValue = node.attributes[i].value; + + switch (optionName) { + case 'accessors': + if (Array.isArray(optionValue)) { + if (optionValue[0].type === 'MustacheTag') { + usesAccessors = optionValue[0].expression.value; + } + } else { + usesAccessors = true; + } + break; + case 'runes': + isRunes = true; + break; + } + } + }; + + const handleIdentifier = (node: BaseNode) => { + if (node.name === '$$props') { + uses$$props = true; + return; + } + if (node.name === '$$restProps') { + uses$$restProps = true; + return; + } + + if (node.name === '$$slots') { + uses$$slots = true; + return; + } + }; + + const handleStyleTag = (node: BaseNode) => { + str.remove(node.start, node.end); + }; + + const slotHandler = new SlotHandler(str.original); + let templateScope = new TemplateScope(); + + const handleComponentLet = (component: BaseNode) => { + templateScope = templateScope.child(); + const lets = slotHandler.getSlotConsumerOfComponent(component); + + for (const { letNode, slotName } of lets) { + handleScopeAndResolveLetVarForSlot({ + letNode, + slotName, + slotHandler, + templateScope, + component + }); + } + }; + + const handleScopeAndResolveForSlotInner = ( + identifierDef: BaseNode, + initExpression: BaseNode, + owner: BaseNode + ) => { + handleScopeAndResolveForSlot({ + identifierDef, + initExpression, + slotHandler, + templateScope, + owner + }); + }; + + const eventHandler = new EventHandler(); + walk(ast as any, { - enter: (estreeTypedNode, estreeTypedParent, prop: string, index: number) => { + enter: (estreeTypedNode, estreeTypedParent, prop: string) => { const node = estreeTypedNode as TemplateNode; const parent = estreeTypedParent as BaseNode; + if ( + prop == 'params' && + (parent.type == 'FunctionDeclaration' || parent.type == 'ArrowFunctionExpression') + ) { + isDeclaration.value = true; + } + if (prop == 'id' && parent.type == 'VariableDeclarator') { + isDeclaration.value = true; + } + try { switch (node.type) { + case 'Identifier': + handleIdentifier(node); + stores.handleIdentifier(node, parent, prop); + eventHandler.handleIdentifier(node, parent, prop); + break; case 'IfBlock': handleIf(str, node); break; case 'EachBlock': + templateScope = templateScope.child(); + + if (node.context) { + handleScopeAndResolveForSlotInner(node.context, node.expression, node); + } handleEach(str, node); break; case 'ElseBlock': @@ -84,7 +230,13 @@ export function convertHtmlxToJsx( case 'KeyBlock': handleKey(str, node); break; + case 'BlockStatement': + case 'FunctionDeclaration': + case 'ArrowFunctionExpression': + scopeStack.push(); + break; case 'SnippetBlock': + scopeStack.push(); handleSnippet( str, node, @@ -96,7 +248,7 @@ export function convertHtmlxToJsx( : undefined ); if (parent === ast) { - // root snippet -> move to instance script + // root snippet -> move to instance script or possibly even module script rootSnippets.push([node.start, node.end]); } else { pendingSnippetHoistCheck.add(parent); @@ -106,6 +258,7 @@ export function convertHtmlxToJsx( handleMustacheTag(str, node, parent); break; case 'RawMustacheTag': + scripts.checkIfContainsScriptTag(node); handleRawHtml(str, node); break; case 'DebugTag': @@ -127,6 +280,7 @@ export function convertHtmlxToJsx( if (options.svelte5Plus) { handleImplicitChildren(node, element as InlineComponent); } + handleComponentLet(node); break; case 'Element': case 'Options': @@ -139,6 +293,14 @@ export function convertHtmlxToJsx( case 'SvelteBoundary': case 'Slot': case 'SlotTemplate': + if (node.type === 'Element') { + scripts.checkIfElementIsScriptTag(node, parent); + } else if (node.type === 'Options') { + handleSvelteOptions(node); + } else if (node.type === 'Slot') { + slotHandler.handleSlot(node, templateScope); + } + if (node.name !== '!DOCTYPE') { if (element) { element.child = new Element( @@ -154,6 +316,7 @@ export function convertHtmlxToJsx( } break; case 'Comment': + componentDocumentation.handleComment(node); handleComment(str, node); break; case 'Binding': @@ -173,12 +336,15 @@ export function convertHtmlxToJsx( handleStyleDirective(str, node as StyleDirective, element as Element); break; case 'Action': + stores.handleDirective(node, str); handleActionDirective(node as BaseDirective, element as Element); break; case 'Transition': + stores.handleDirective(node, str); handleTransitionDirective(str, node as BaseDirective, element as Element); break; case 'Animation': + stores.handleDirective(node, str); handleAnimateDirective(str, node as BaseDirective, element as Element); break; case 'Attribute': @@ -186,7 +352,7 @@ export function convertHtmlxToJsx( str, node as Attribute, parent, - options.preserveAttributeCase, + preserveAttributeCase, options.svelte5Plus, element ); @@ -195,6 +361,7 @@ export function convertHtmlxToJsx( handleSpread(node, element); break; case 'EventHandler': + eventHandler.handleEventHandler(node, parent); handleEventHandler(str, node as BaseDirective, element); break; case 'Let': @@ -202,7 +369,7 @@ export function convertHtmlxToJsx( str, node, parent, - options.preserveAttributeCase, + preserveAttributeCase, options.svelte5Plus, element ); @@ -210,9 +377,29 @@ export function convertHtmlxToJsx( case 'Text': handleText(str, node as Text, parent); break; - } - if (onWalk) { - onWalk(node, parent, prop, index); + case 'Style': + handleStyleTag(node); + break; + case 'VariableDeclarator': + isDeclaration.value = true; + break; + case 'AwaitBlock': + templateScope = templateScope.child(); + if (node.value) { + handleScopeAndResolveForSlotInner( + node.value, + node.expression, + node.then + ); + } + if (node.error) { + handleScopeAndResolveForSlotInner( + node.error, + node.expression, + node.catch + ); + } + break; } } catch (e) { console.error('Error walking node ', node, e); @@ -220,17 +407,37 @@ export function convertHtmlxToJsx( } }, - leave: (estreeTypedNode, estreeTypedParent, prop: string, index: number) => { + leave: (estreeTypedNode, estreeTypedParent, prop: string) => { const node = estreeTypedNode as TemplateNode; const parent = estreeTypedParent as BaseNode; + if ( + prop == 'params' && + (parent.type == 'FunctionDeclaration' || parent.type == 'ArrowFunctionExpression') + ) { + isDeclaration.value = false; + } + + if (prop == 'id' && parent.type == 'VariableDeclarator') { + isDeclaration.value = false; + } + const onTemplateScopeLeave = () => { + templateScope = templateScope.parent; + }; + try { switch (node.type) { - case 'IfBlock': + case 'BlockStatement': + case 'FunctionDeclaration': + case 'ArrowFunctionExpression': + case 'SnippetBlock': + scopeStack.pop(); break; case 'EachBlock': + onTemplateScopeLeave(); break; case 'AwaitBlock': + onTemplateScopeLeave(); handleAwait(str, node); break; case 'InlineComponent': @@ -245,15 +452,15 @@ export function convertHtmlxToJsx( case 'Document': case 'Slot': case 'SlotTemplate': + if (node.type === 'InlineComponent') { + onTemplateScopeLeave(); + } if (node.name !== '!DOCTYPE') { element.performTransformation(); element = element.parent; } break; } - if (onLeave) { - onLeave(node, parent, prop, index); - } } catch (e) { console.error('Error leaving node ', node); throw e; @@ -261,11 +468,39 @@ export function convertHtmlxToJsx( } }); + // hoist inner snippets to top of containing element for (const node of pendingSnippetHoistCheck) { hoistSnippetBlock(str, node); } - return rootSnippets; + // resolve scripts + const { scriptTag, moduleScriptTag } = scripts.getTopLevelScriptTags(); + if (options.mode !== 'ts') { + scripts.blankOtherScriptTags(str); + } + + //resolve stores + const resolvedStores = stores.getStoreNames(); + + return { + htmlAst: ast, + moduleScriptTag, + scriptTag, + rootSnippets, + slots: slotHandler.getSlotDef(), + events: new ComponentEvents( + eventHandler, + tags.some((tag) => tag.attributes?.some((a) => a.name === 'strictEvents')), + str + ), + uses$$props, + uses$$restProps, + uses$$slots, + componentDocumentation, + resolvedStores, + usesAccessors, + isRunes + }; } /** @@ -281,10 +516,13 @@ export function htmlx2jsx( svelte5Plus: boolean; } ) { - const ast = parseHtmlx(htmlx, parse, { ...options }).htmlxAst; + const { htmlxAst, tags } = parseHtmlx(htmlx, parse, { ...options }); const str = new MagicString(htmlx); - convertHtmlxToJsx(str, ast, null, null, options); + convertHtmlxToJsx(str, htmlxAst, tags, { + ...options, + namespace: options?.preserveAttributeCase ? 'foreign' : undefined + }); return { map: str.generateMap({ hires: true }), diff --git a/packages/svelte2tsx/src/svelte2tsx/index.ts b/packages/svelte2tsx/src/svelte2tsx/index.ts index 2daf71419..06f343ac4 100644 --- a/packages/svelte2tsx/src/svelte2tsx/index.ts +++ b/packages/svelte2tsx/src/svelte2tsx/index.ts @@ -1,51 +1,15 @@ -import { Node } from 'estree-walker'; import MagicString from 'magic-string'; -import { convertHtmlxToJsx } from '../htmlxtojsx_v2'; +import { convertHtmlxToJsx, TemplateProcessResult } from '../htmlxtojsx_v2'; import { parseHtmlx } from '../utils/htmlxparser'; -import { ComponentDocumentation } from './nodes/ComponentDocumentation'; -import { ComponentEvents } from './nodes/ComponentEvents'; -import { EventHandler } from './nodes/event-handler'; +import { addComponentExport } from './addComponentExport'; +import { createRenderFunction } from './createRenderFunction'; import { ExportedNames } from './nodes/ExportedNames'; -import { - handleScopeAndResolveForSlot, - handleScopeAndResolveLetVarForSlot -} from './nodes/handleScopeAndResolveForSlot'; +import { Generics } from './nodes/Generics'; import { ImplicitStoreValues } from './nodes/ImplicitStoreValues'; -import { Scripts } from './nodes/Scripts'; -import { SlotHandler } from './nodes/slot'; -import { Stores } from './nodes/Stores'; -import TemplateScope from './nodes/TemplateScope'; import { processInstanceScriptContent } from './processInstanceScriptContent'; import { createModuleAst, ModuleAst, processModuleScriptTag } from './processModuleScriptTag'; -import { ScopeStack } from './utils/Scope'; -import { Generics } from './nodes/Generics'; -import { addComponentExport } from './addComponentExport'; -import { createRenderFunction } from './createRenderFunction'; -// @ts-ignore -import { TemplateNode } from 'svelte/types/compiler/interfaces'; import path from 'path'; -import { VERSION, parse } from 'svelte/compiler'; - -type TemplateProcessResult = { - /** - * The HTML part of the Svelte AST. - */ - htmlAst: TemplateNode; - uses$$props: boolean; - uses$$restProps: boolean; - uses$$slots: boolean; - slots: Map>; - scriptTag: Node; - moduleScriptTag: Node; - /** Start/end positions of snippets that should be moved to the instance script */ - rootSnippets: Array<[number, number]>; - /** To be added later as a comment on the default class export */ - componentDocumentation: ComponentDocumentation; - events: ComponentEvents; - resolvedStores: string[]; - usesAccessors: boolean; - isRunes: boolean; -}; +import { parse, VERSION } from 'svelte/compiler'; function processSvelteTemplate( str: MagicString, @@ -60,257 +24,7 @@ function processSvelteTemplate( } ): TemplateProcessResult { const { htmlxAst, tags } = parseHtmlx(str.original, parse, options); - - let uses$$props = false; - let uses$$restProps = false; - let uses$$slots = false; - let usesAccessors = !!options.accessors; - let isRunes = false; - - const componentDocumentation = new ComponentDocumentation(); - - //track if we are in a declaration scope - const isDeclaration = { value: false }; - - //track $store variables since we are only supposed to give top level scopes special treatment, and users can declare $blah variables at higher scopes - //which prevents us just changing all instances of Identity that start with $ - - const scopeStack = new ScopeStack(); - const stores = new Stores(scopeStack, isDeclaration); - const scripts = new Scripts(htmlxAst); - - const handleSvelteOptions = (node: Node) => { - for (let i = 0; i < node.attributes.length; i++) { - const optionName = node.attributes[i].name; - const optionValue = node.attributes[i].value; - - switch (optionName) { - case 'accessors': - if (Array.isArray(optionValue)) { - if (optionValue[0].type === 'MustacheTag') { - usesAccessors = optionValue[0].expression.value; - } - } else { - usesAccessors = true; - } - break; - case 'runes': - isRunes = true; - break; - } - } - }; - - const handleIdentifier = (node: Node) => { - if (node.name === '$$props') { - uses$$props = true; - return; - } - if (node.name === '$$restProps') { - uses$$restProps = true; - return; - } - - if (node.name === '$$slots') { - uses$$slots = true; - return; - } - }; - - const handleStyleTag = (node: Node) => { - str.remove(node.start, node.end); - }; - - const slotHandler = new SlotHandler(str.original); - let templateScope = new TemplateScope(); - - const handleEach = (node: Node) => { - templateScope = templateScope.child(); - - if (node.context) { - handleScopeAndResolveForSlotInner(node.context, node.expression, node); - } - }; - - const handleAwait = (node: Node) => { - templateScope = templateScope.child(); - if (node.value) { - handleScopeAndResolveForSlotInner(node.value, node.expression, node.then); - } - if (node.error) { - handleScopeAndResolveForSlotInner(node.error, node.expression, node.catch); - } - }; - - const handleComponentLet = (component: Node) => { - templateScope = templateScope.child(); - const lets = slotHandler.getSlotConsumerOfComponent(component); - - for (const { letNode, slotName } of lets) { - handleScopeAndResolveLetVarForSlot({ - letNode, - slotName, - slotHandler, - templateScope, - component - }); - } - }; - - const handleScopeAndResolveForSlotInner = ( - identifierDef: Node, - initExpression: Node, - owner: Node - ) => { - handleScopeAndResolveForSlot({ - identifierDef, - initExpression, - slotHandler, - templateScope, - owner - }); - }; - - const eventHandler = new EventHandler(); - - const onHtmlxWalk = (node: Node, parent: Node, prop: string) => { - if ( - prop == 'params' && - (parent.type == 'FunctionDeclaration' || parent.type == 'ArrowFunctionExpression') - ) { - isDeclaration.value = true; - } - if (prop == 'id' && parent.type == 'VariableDeclarator') { - isDeclaration.value = true; - } - - switch (node.type) { - case 'Comment': - componentDocumentation.handleComment(node); - break; - case 'Options': - handleSvelteOptions(node); - break; - case 'Identifier': - handleIdentifier(node); - stores.handleIdentifier(node, parent, prop); - eventHandler.handleIdentifier(node, parent, prop); - break; - case 'Transition': - case 'Action': - case 'Animation': - stores.handleDirective(node, str); - break; - case 'Slot': - slotHandler.handleSlot(node, templateScope); - break; - case 'Style': - handleStyleTag(node); - break; - case 'Element': - scripts.checkIfElementIsScriptTag(node, parent); - break; - case 'RawMustacheTag': - scripts.checkIfContainsScriptTag(node); - break; - case 'BlockStatement': - scopeStack.push(); - break; - case 'FunctionDeclaration': - scopeStack.push(); - break; - case 'ArrowFunctionExpression': - scopeStack.push(); - break; - case 'EventHandler': - eventHandler.handleEventHandler(node, parent); - break; - case 'VariableDeclarator': - isDeclaration.value = true; - break; - case 'EachBlock': - handleEach(node); - break; - case 'AwaitBlock': - handleAwait(node); - break; - case 'InlineComponent': - handleComponentLet(node); - break; - } - }; - - const onHtmlxLeave = (node: Node, parent: Node, prop: string, _index: number) => { - if ( - prop == 'params' && - (parent.type == 'FunctionDeclaration' || parent.type == 'ArrowFunctionExpression') - ) { - isDeclaration.value = false; - } - - if (prop == 'id' && parent.type == 'VariableDeclarator') { - isDeclaration.value = false; - } - const onTemplateScopeLeave = () => { - templateScope = templateScope.parent; - }; - - switch (node.type) { - case 'BlockStatement': - scopeStack.pop(); - break; - case 'FunctionDeclaration': - scopeStack.pop(); - break; - case 'ArrowFunctionExpression': - scopeStack.pop(); - break; - case 'EachBlock': - onTemplateScopeLeave(); - break; - case 'AwaitBlock': - onTemplateScopeLeave(); - break; - case 'InlineComponent': - onTemplateScopeLeave(); - break; - } - }; - - const rootSnippets = convertHtmlxToJsx(str, htmlxAst, onHtmlxWalk, onHtmlxLeave, { - preserveAttributeCase: options?.namespace == 'foreign', - typingsNamespace: options.typingsNamespace, - svelte5Plus: options.svelte5Plus - }); - - // resolve scripts - const { scriptTag, moduleScriptTag } = scripts.getTopLevelScriptTags(); - if (options.mode !== 'ts') { - scripts.blankOtherScriptTags(str); - } - - //resolve stores - const resolvedStores = stores.getStoreNames(); - - return { - htmlAst: htmlxAst, - moduleScriptTag, - scriptTag, - rootSnippets, - slots: slotHandler.getSlotDef(), - events: new ComponentEvents( - eventHandler, - tags.some((tag) => tag.attributes?.some((a) => a.name === 'strictEvents')), - str - ), - uses$$props, - uses$$restProps, - uses$$slots, - componentDocumentation, - resolvedStores, - usesAccessors, - isRunes - }; + return convertHtmlxToJsx(str, htmlxAst, tags, options); } export function svelte2tsx( From 9a5a6af600e958a2819387b36e6ca7bc0051a8a3 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Thu, 21 Nov 2024 13:36:16 +0100 Subject: [PATCH 11/17] feat: hoist snippets to module context if possible (#2601) https://github.com/sveltejs/svelte/issues/10350 --- .../svelte2tsx/src/htmlxtojsx_v2/index.ts | 21 ++++++-- .../src/svelte2tsx/createRenderFunction.ts | 6 --- packages/svelte2tsx/src/svelte2tsx/index.ts | 27 +++++++++- .../svelte2tsx/nodes/HoistableInterfaces.ts | 4 ++ .../snippet-module-hoist-1.v5/expectedv2.ts | 50 +++++++++++++++++++ .../snippet-module-hoist-1.v5/input.svelte | 40 +++++++++++++++ .../snippet-module-hoist-2.v5/expectedv2.ts | 21 ++++++++ .../snippet-module-hoist-2.v5/input.svelte | 12 +++++ .../snippet-module-hoist-3.v5/expectedv2.ts | 17 +++++++ .../snippet-module-hoist-3.v5/input.svelte | 11 ++++ 10 files changed, 199 insertions(+), 10 deletions(-) create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/snippet-module-hoist-1.v5/expectedv2.ts create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/snippet-module-hoist-1.v5/input.svelte create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/snippet-module-hoist-2.v5/expectedv2.ts create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/snippet-module-hoist-2.v5/input.svelte create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/snippet-module-hoist-3.v5/expectedv2.ts create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/snippet-module-hoist-3.v5/input.svelte diff --git a/packages/svelte2tsx/src/htmlxtojsx_v2/index.ts b/packages/svelte2tsx/src/htmlxtojsx_v2/index.ts index acc2f9c82..2e5ac14d3 100644 --- a/packages/svelte2tsx/src/htmlxtojsx_v2/index.ts +++ b/packages/svelte2tsx/src/htmlxtojsx_v2/index.ts @@ -40,6 +40,7 @@ import { } from '../svelte2tsx/nodes/handleScopeAndResolveForSlot'; import { EventHandler } from '../svelte2tsx/nodes/event-handler'; import { ComponentEvents } from '../svelte2tsx/nodes/ComponentEvents'; +import { analyze } from 'periscopic'; export interface TemplateProcessResult { /** @@ -53,7 +54,7 @@ export interface TemplateProcessResult { scriptTag: BaseNode; moduleScriptTag: BaseNode; /** Start/end positions of snippets that should be moved to the instance script or possibly even module script */ - rootSnippets: Array<[number, number]>; + rootSnippets: Array<[start: number, end: number, globals: Map]>; /** To be added later as a comment on the default class export */ componentDocumentation: ComponentDocumentation; events: ComponentEvents; @@ -92,7 +93,7 @@ export function convertHtmlxToJsx( stripDoctype(str); - const rootSnippets: Array<[number, number]> = []; + const rootSnippets: Array<[number, number, Map]> = []; let element: Element | InlineComponent | undefined; const pendingSnippetHoistCheck = new Set(); @@ -249,7 +250,21 @@ export function convertHtmlxToJsx( ); if (parent === ast) { // root snippet -> move to instance script or possibly even module script - rootSnippets.push([node.start, node.end]); + const result = analyze({ + type: 'FunctionDeclaration', + start: -1, + end: -1, + id: node.expression, + params: node.parameters ?? [], + body: { + type: 'BlockStatement', + start: -1, + end: -1, + body: node.children as any[] // wrong AST, but periscopic doesn't care + } + }); + + rootSnippets.push([node.start, node.end, result.globals]); } else { pendingSnippetHoistCheck.add(parent); } diff --git a/packages/svelte2tsx/src/svelte2tsx/createRenderFunction.ts b/packages/svelte2tsx/src/svelte2tsx/createRenderFunction.ts index ab568ce0e..6a929fe94 100644 --- a/packages/svelte2tsx/src/svelte2tsx/createRenderFunction.ts +++ b/packages/svelte2tsx/src/svelte2tsx/createRenderFunction.ts @@ -8,7 +8,6 @@ export interface CreateRenderFunctionPara extends InstanceScriptProcessResult { str: MagicString; scriptTag: Node; scriptDestination: number; - rootSnippets: Array<[number, number]>; slots: Map>; events: ComponentEvents; uses$$SlotsInterface: boolean; @@ -20,7 +19,6 @@ export function createRenderFunction({ str, scriptTag, scriptDestination, - rootSnippets, slots, events, exportedNames, @@ -82,10 +80,6 @@ export function createRenderFunction({ ); } - for (const rootSnippet of rootSnippets) { - str.move(rootSnippet[0], rootSnippet[1], scriptTagEnd); - } - const scriptEndTagStart = htmlx.lastIndexOf('<', scriptTag.end - 1); // wrap template with callback str.overwrite(scriptEndTagStart, scriptTag.end, `${slotsDeclaration};\nasync () => {`, { diff --git a/packages/svelte2tsx/src/svelte2tsx/index.ts b/packages/svelte2tsx/src/svelte2tsx/index.ts index 06f343ac4..0b48cc607 100644 --- a/packages/svelte2tsx/src/svelte2tsx/index.ts +++ b/packages/svelte2tsx/src/svelte2tsx/index.ts @@ -139,7 +139,6 @@ export function svelte2tsx( str, scriptTag, scriptDestination: instanceScriptTarget, - rootSnippets, slots, events, exportedNames, @@ -164,6 +163,32 @@ export function svelte2tsx( ), moduleAst ); + if (!scriptTag) { + moduleAst.tsAst.forEachChild((node) => + exportedNames.hoistableInterfaces.analyzeModuleScriptNode(node) + ); + } + } + + if (moduleScriptTag || scriptTag) { + const allowed = exportedNames.hoistableInterfaces.getAllowedValues(); + for (const [start, end, globals] of rootSnippets) { + const hoist_to_module = + moduleScriptTag && + (globals.size === 0 || [...globals.keys()].every((id) => allowed.has(id))); + + if (hoist_to_module) { + str.move( + start, + end, + scriptTag + ? scriptTag.start + 1 // +1 because imports are also moved at that position, and we want to move interfaces after imports + : moduleScriptTag.end + ); + } else if (scriptTag) { + str.move(start, end, renderFunctionStart); + } + } } addComponentExport({ diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/HoistableInterfaces.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/HoistableInterfaces.ts index 647926ed0..093a4a132 100644 --- a/packages/svelte2tsx/src/svelte2tsx/nodes/HoistableInterfaces.ts +++ b/packages/svelte2tsx/src/svelte2tsx/nodes/HoistableInterfaces.ts @@ -358,6 +358,10 @@ export class HoistableInterfaces { } } + getAllowedValues() { + return this.import_value_set; + } + /** * Collects type and value dependencies from a given TypeNode. * @param type_node The TypeNode to analyze. diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/snippet-module-hoist-1.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/snippet-module-hoist-1.v5/expectedv2.ts new file mode 100644 index 000000000..69587fe79 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/snippet-module-hoist-1.v5/expectedv2.ts @@ -0,0 +1,50 @@ +/// +; + let module = true; +;; + +import { imported } from './x'; + const hoistable1/*Ωignore_positionΩ*/ = ()/*Ωignore_startΩ*/: ReturnType/*Ωignore_endΩ*/ => { async ()/*Ωignore_positionΩ*/ => { + { svelteHTML.createElement("div", {}); } +};return __sveltets_2_any(0)}; const hoistable2/*Ωignore_positionΩ*/ = (bar)/*Ωignore_startΩ*/: ReturnType/*Ωignore_endΩ*/ => { async ()/*Ωignore_positionΩ*/ => { + { svelteHTML.createElement("div", {});bar; } +};return __sveltets_2_any(0)}; const hoistable3/*Ωignore_positionΩ*/ = (bar: string)/*Ωignore_startΩ*/: ReturnType/*Ωignore_endΩ*/ => { async ()/*Ωignore_positionΩ*/ => { + { svelteHTML.createElement("div", {});bar; } +};return __sveltets_2_any(0)}; const hoistable4/*Ωignore_positionΩ*/ = (foo)/*Ωignore_startΩ*/: ReturnType/*Ωignore_endΩ*/ => { async ()/*Ωignore_positionΩ*/ => { + { svelteHTML.createElement("div", {});foo; } +};return __sveltets_2_any(0)}; const hoistable5/*Ωignore_positionΩ*/ = ()/*Ωignore_startΩ*/: ReturnType/*Ωignore_endΩ*/ => { async ()/*Ωignore_positionΩ*/ => { + { svelteHTML.createElement("button", { "onclick":e => e,}); } +};return __sveltets_2_any(0)}; const hoistable6/*Ωignore_positionΩ*/ = ()/*Ωignore_startΩ*/: ReturnType/*Ωignore_endΩ*/ => { async ()/*Ωignore_positionΩ*/ => { + { svelteHTML.createElement("div", {});module; } +};return __sveltets_2_any(0)}; const hoistable7/*Ωignore_positionΩ*/ = ()/*Ωignore_startΩ*/: ReturnType/*Ωignore_endΩ*/ => { async ()/*Ωignore_positionΩ*/ => { + { svelteHTML.createElement("div", {});imported; } +};return __sveltets_2_any(0)};function render() { + const not_hoistable/*Ωignore_positionΩ*/ = ()/*Ωignore_startΩ*/: ReturnType/*Ωignore_endΩ*/ => { async ()/*Ωignore_positionΩ*/ => { + { svelteHTML.createElement("div", {});foo; } +};return __sveltets_2_any(0)}; + + let foo = true; +; +async () => { + + + + + + + + + + + + + + + + + +}; +return { props: /** @type {Record} */ ({}), exports: {}, bindings: "", slots: {}, events: {} }} +const Input__SvelteComponent_ = __sveltets_2_isomorphic_component(__sveltets_2_partial(__sveltets_2_with_any_event(render()))); +/*Ωignore_startΩ*/type Input__SvelteComponent_ = InstanceType; +/*Ωignore_endΩ*/export default Input__SvelteComponent_; \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/snippet-module-hoist-1.v5/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/snippet-module-hoist-1.v5/input.svelte new file mode 100644 index 000000000..924dc61aa --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/snippet-module-hoist-1.v5/input.svelte @@ -0,0 +1,40 @@ + + + + +{#snippet hoistable1()} +
hello
+{/snippet} + +{#snippet hoistable2(bar)} +
{bar}
+{/snippet} + +{#snippet hoistable3(bar: string)} +
{bar}
+{/snippet} + +{#snippet hoistable4(foo)} +
{foo}
+{/snippet} + +{#snippet hoistable5()} + +{/snippet} + +{#snippet hoistable6()} +
{module}
+{/snippet} + +{#snippet hoistable7()} +
{imported}
+{/snippet} + +{#snippet not_hoistable()} +
{foo}
+{/snippet} diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/snippet-module-hoist-2.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/snippet-module-hoist-2.v5/expectedv2.ts new file mode 100644 index 000000000..90505a1e5 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/snippet-module-hoist-2.v5/expectedv2.ts @@ -0,0 +1,21 @@ +/// +; +import { imported } from './x'; +function render() { + const hoistable/*Ωignore_positionΩ*/ = ()/*Ωignore_startΩ*/: ReturnType/*Ωignore_endΩ*/ => { async ()/*Ωignore_positionΩ*/ => { + { svelteHTML.createElement("div", {}); } +};return __sveltets_2_any(0)}; const not_hoistable/*Ωignore_positionΩ*/ = ()/*Ωignore_startΩ*/: ReturnType/*Ωignore_endΩ*/ => { async ()/*Ωignore_positionΩ*/ => { + { svelteHTML.createElement("div", {});foo; } +};return __sveltets_2_any(0)}; + + let foo = true; +; +async () => { + + + +}; +return { props: /** @type {Record} */ ({}), exports: {}, bindings: "", slots: {}, events: {} }} +const Input__SvelteComponent_ = __sveltets_2_isomorphic_component(__sveltets_2_partial(__sveltets_2_with_any_event(render()))); +/*Ωignore_startΩ*/type Input__SvelteComponent_ = InstanceType; +/*Ωignore_endΩ*/export default Input__SvelteComponent_; \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/snippet-module-hoist-2.v5/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/snippet-module-hoist-2.v5/input.svelte new file mode 100644 index 000000000..dd221c944 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/snippet-module-hoist-2.v5/input.svelte @@ -0,0 +1,12 @@ + + +{#snippet hoistable()} +
hello
+{/snippet} + +{#snippet not_hoistable()} +
{foo}
+{/snippet} diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/snippet-module-hoist-3.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/snippet-module-hoist-3.v5/expectedv2.ts new file mode 100644 index 000000000..a48aa0ae9 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/snippet-module-hoist-3.v5/expectedv2.ts @@ -0,0 +1,17 @@ +/// +; + let foo = true; +; const hoistable1/*Ωignore_positionΩ*/ = ()/*Ωignore_startΩ*/: ReturnType/*Ωignore_endΩ*/ => { async ()/*Ωignore_positionΩ*/ => { + { svelteHTML.createElement("div", {}); } +};return __sveltets_2_any(0)}; const hoistable2/*Ωignore_positionΩ*/ = ()/*Ωignore_startΩ*/: ReturnType/*Ωignore_endΩ*/ => { async ()/*Ωignore_positionΩ*/ => { + { svelteHTML.createElement("div", {});foo; } +};return __sveltets_2_any(0)};;function render() { +async () => { + + + +}; +return { props: /** @type {Record} */ ({}), exports: {}, bindings: "", slots: {}, events: {} }} +const Input__SvelteComponent_ = __sveltets_2_isomorphic_component(__sveltets_2_partial(__sveltets_2_with_any_event(render()))); +/*Ωignore_startΩ*/type Input__SvelteComponent_ = InstanceType; +/*Ωignore_endΩ*/export default Input__SvelteComponent_; \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/snippet-module-hoist-3.v5/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/snippet-module-hoist-3.v5/input.svelte new file mode 100644 index 000000000..fb72eb302 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/snippet-module-hoist-3.v5/input.svelte @@ -0,0 +1,11 @@ + + +{#snippet hoistable1()} +
hello
+{/snippet} + +{#snippet hoistable2()} +
{foo}
+{/snippet} From 695c660e001c118617a61661e627c0b5fb021086 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Thu, 21 Nov 2024 15:58:42 +0100 Subject: [PATCH 12/17] chore: bump prettier-plugin-svelte --- packages/language-server/package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/language-server/package.json b/packages/language-server/package.json index 46f970633..6d7a5d4ca 100644 --- a/packages/language-server/package.json +++ b/packages/language-server/package.json @@ -59,7 +59,7 @@ "globrex": "^0.1.2", "lodash": "^4.17.21", "prettier": "~3.3.3", - "prettier-plugin-svelte": "^3.2.8", + "prettier-plugin-svelte": "^3.3.0", "svelte": "^4.2.19", "svelte2tsx": "workspace:~", "typescript": "^5.6.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 39dbfbe20..6e1703cb9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -49,8 +49,8 @@ importers: specifier: ~3.3.3 version: 3.3.3 prettier-plugin-svelte: - specifier: ^3.2.8 - version: 3.2.8(prettier@3.3.3)(svelte@4.2.19) + specifier: ^3.3.0 + version: 3.3.0(prettier@3.3.3)(svelte@4.2.19) svelte: specifier: ^4.2.19 version: 4.2.19 @@ -1100,8 +1100,8 @@ packages: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} - prettier-plugin-svelte@3.2.8: - resolution: {integrity: sha512-PAHmmU5cGZdnhW4mWhmvxuG2PVbbHIxUuPOdUKvfE+d4Qt2d29iU5VWrPdsaW5YqVEE0nqhlvN4eoKmVMpIF3Q==} + prettier-plugin-svelte@3.3.0: + resolution: {integrity: sha512-iNoYiQUx4zwqbQDW/bk0WR75w+QiY4fHJQpGQ5v8Yr7X5m7YoSvs2buUnhoYFXNAL32ULVmrjPSc0vVOHJsO0Q==} peerDependencies: prettier: ^3.0.0 svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0 @@ -2168,7 +2168,7 @@ snapshots: picomatch@2.3.1: {} - prettier-plugin-svelte@3.2.8(prettier@3.3.3)(svelte@4.2.19): + prettier-plugin-svelte@3.3.0(prettier@3.3.3)(svelte@4.2.19): dependencies: prettier: 3.3.3 svelte: 4.2.19 From be4412509e1fed62b39665bd340079848ecb6c41 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Fri, 22 Nov 2024 10:22:27 +0100 Subject: [PATCH 13/17] feat: support `bind:value={get, set}` Companion to https://github.com/sveltejs/svelte/pull/14307 --- packages/svelte2tsx/repl/index.svelte | 13 +-- .../src/htmlxtojsx_v2/nodes/Binding.ts | 87 +++++++++++-------- packages/svelte2tsx/svelte-shims-v4.d.ts | 2 + .../samples/.binding-get-set.v5/expectedv2.js | 8 ++ .../samples/.binding-get-set.v5/input.svelte | 8 ++ 5 files changed, 77 insertions(+), 41 deletions(-) create mode 100644 packages/svelte2tsx/test/htmlx2jsx/samples/.binding-get-set.v5/expectedv2.js create mode 100644 packages/svelte2tsx/test/htmlx2jsx/samples/.binding-get-set.v5/input.svelte diff --git a/packages/svelte2tsx/repl/index.svelte b/packages/svelte2tsx/repl/index.svelte index 4b1efc883..9334f7ac0 100644 --- a/packages/svelte2tsx/repl/index.svelte +++ b/packages/svelte2tsx/repl/index.svelte @@ -1,7 +1,8 @@ - + + v, new_v => v = new_v} /> - +
+
+ + + v, new_v => v = new_v} /> diff --git a/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/Binding.ts b/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/Binding.ts index 5bb17cca7..73baf0352 100644 --- a/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/Binding.ts +++ b/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/Binding.ts @@ -9,6 +9,7 @@ import { BaseDirective, BaseNode } from '../../interfaces'; import { Element } from './Element'; import { InlineComponent } from './InlineComponent'; import { surroundWithIgnoreComments } from '../../utils/ignore'; +import { SequenceExpression } from 'estree'; /** * List of binding names that are transformed to sth like `binding = variable`. @@ -58,47 +59,54 @@ export function handleBinding( preserveBind: boolean, isSvelte5Plus: boolean ): void { - // bind group on input - if (element instanceof Element && attr.name == 'group' && parent.name == 'input') { - // add reassignment to force TS to widen the type of the declaration (in case it's never reassigned anywhere else) - appendOneWayBinding(attr, ' = __sveltets_2_any(null)', element); - return; - } + const isGetSetBinding = attr.expression.type === 'SequenceExpression'; - // bind this - if (attr.name === 'this' && supportsBindThis.includes(parent.type)) { - // bind:this is effectively only works bottom up - the variable is updated by the element, not - // the other way round. So we check if the instance is assignable to the variable. - // Note: If the component unmounts (it's inside an if block, or svelte:component this={null}, - // the value becomes null, but we don't add it to the clause because it would introduce - // worse DX for the 99% use case, and because null !== undefined which others might use to type the declaration. - appendOneWayBinding(attr, ` = ${element.name}`, element); - return; - } + if (!isGetSetBinding) { + // bind group on input + if (element instanceof Element && attr.name == 'group' && parent.name == 'input') { + // add reassignment to force TS to widen the type of the declaration (in case it's never reassigned anywhere else) + appendOneWayBinding(attr, ' = __sveltets_2_any(null)', element); + return; + } - // one way binding - if (oneWayBindingAttributes.has(attr.name) && element instanceof Element) { - appendOneWayBinding(attr, `= ${element.name}.${attr.name}`, element); - return; - } + // bind this + if (attr.name === 'this' && supportsBindThis.includes(parent.type)) { + // bind:this is effectively only works bottom up - the variable is updated by the element, not + // the other way round. So we check if the instance is assignable to the variable. + // Note: If the component unmounts (it's inside an if block, or svelte:component this={null}, + // the value becomes null, but we don't add it to the clause because it would introduce + // worse DX for the 99% use case, and because null !== undefined which others might use to type the declaration. + appendOneWayBinding(attr, ` = ${element.name}`, element); + return; + } - // one way binding whose property is not on the element - if (oneWayBindingAttributesNotOnElement.has(attr.name) && element instanceof Element) { + // one way binding + if (oneWayBindingAttributes.has(attr.name) && element instanceof Element) { + appendOneWayBinding(attr, `= ${element.name}.${attr.name}`, element); + return; + } + + // one way binding whose property is not on the element + if (oneWayBindingAttributesNotOnElement.has(attr.name) && element instanceof Element) { + element.appendToStartEnd([ + [attr.expression.start, getEnd(attr.expression)], + `= ${surroundWithIgnoreComments( + `null as ${oneWayBindingAttributesNotOnElement.get(attr.name)}` + )};` + ]); + return; + } + + // add reassignment to force TS to widen the type of the declaration (in case it's never reassigned anywhere else) + const expressionStr = str.original.substring( + attr.expression.start, + getEnd(attr.expression) + ); element.appendToStartEnd([ - [attr.expression.start, getEnd(attr.expression)], - `= ${surroundWithIgnoreComments( - `null as ${oneWayBindingAttributesNotOnElement.get(attr.name)}` - )};` + surroundWithIgnoreComments(`() => ${expressionStr} = __sveltets_2_any(null);`) ]); - return; } - // add reassignment to force TS to widen the type of the declaration (in case it's never reassigned anywhere else) - const expressionStr = str.original.substring(attr.expression.start, getEnd(attr.expression)); - element.appendToStartEnd([ - surroundWithIgnoreComments(`() => ${expressionStr} = __sveltets_2_any(null);`) - ]); - // other bindings which are transformed to normal attributes/props const isShorthand = attr.expression.start === attr.start + 'bind:'.length; const name: TransformationArray = @@ -122,11 +130,20 @@ export function handleBinding( ] ]; + const [get, set] = isGetSetBinding ? (attr.expression as SequenceExpression).expressions : []; const value: TransformationArray | undefined = isShorthand ? preserveBind && element instanceof Element ? [rangeWithTrailingPropertyAccess(str.original, attr.expression)] : undefined - : [rangeWithTrailingPropertyAccess(str.original, attr.expression)]; + : isGetSetBinding + ? [ + '__sveltets_2_get_set_binding(', + [get.start, get.end], + ',', + rangeWithTrailingPropertyAccess(str.original, set), + ')' + ] + : [rangeWithTrailingPropertyAccess(str.original, attr.expression)]; if (isSvelte5Plus && element instanceof InlineComponent) { // To check if property is actually bindable diff --git a/packages/svelte2tsx/svelte-shims-v4.d.ts b/packages/svelte2tsx/svelte-shims-v4.d.ts index 2d36a539d..fcaf92f65 100644 --- a/packages/svelte2tsx/svelte-shims-v4.d.ts +++ b/packages/svelte2tsx/svelte-shims-v4.d.ts @@ -263,6 +263,8 @@ type __sveltets_2_PropsWithChildren = Props & : {}); declare function __sveltets_2_runes_constructor(render: {props: Props }): import("svelte").ComponentConstructorOptions; +declare function __sveltets_2_get_set_binding(get: (() => T) | null | undefined, set: (t: T) => void): T; + declare function __sveltets_$$bindings(...bindings: Bindings): Bindings[number]; declare function __sveltets_2_fn_component< diff --git a/packages/svelte2tsx/test/htmlx2jsx/samples/.binding-get-set.v5/expectedv2.js b/packages/svelte2tsx/test/htmlx2jsx/samples/.binding-get-set.v5/expectedv2.js new file mode 100644 index 000000000..370353fb4 --- /dev/null +++ b/packages/svelte2tsx/test/htmlx2jsx/samples/.binding-get-set.v5/expectedv2.js @@ -0,0 +1,8 @@ + { svelteHTML.createElement("input", { "bind:value":__sveltets_2_get_set_binding(get,set),});} + { svelteHTML.createElement("input", { "bind:value":__sveltets_2_get_set_binding(() => v,new_v => v = new_v),});} + + { svelteHTML.createElement("div", { "bind:clientWidth":__sveltets_2_get_set_binding(null,set),});} + { svelteHTML.createElement("div", { "bind:contentRect":__sveltets_2_get_set_binding(null,set),});} + + { const $$_tupnI0C = __sveltets_2_ensureComponent(Input); const $$_tupnI0 = new $$_tupnI0C({ target: __sveltets_2_any(), props: { value:__sveltets_2_get_set_binding(get,set),}});$$_tupnI0.$$bindings = 'value';} + { const $$_tupnI0C = __sveltets_2_ensureComponent(Input); const $$_tupnI0 = new $$_tupnI0C({ target: __sveltets_2_any(), props: { value:__sveltets_2_get_set_binding(() => v,new_v => v = new_v),}});$$_tupnI0.$$bindings = 'value';} \ No newline at end of file diff --git a/packages/svelte2tsx/test/htmlx2jsx/samples/.binding-get-set.v5/input.svelte b/packages/svelte2tsx/test/htmlx2jsx/samples/.binding-get-set.v5/input.svelte new file mode 100644 index 000000000..9334f7ac0 --- /dev/null +++ b/packages/svelte2tsx/test/htmlx2jsx/samples/.binding-get-set.v5/input.svelte @@ -0,0 +1,8 @@ + + v, new_v => v = new_v} /> + +
+
+ + + v, new_v => v = new_v} /> From cda5c864afa2c0ab6d30462a12117b1105d78aed Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Fri, 22 Nov 2024 12:27:24 +0100 Subject: [PATCH 14/17] fix: preserve `bind:...` mapping on elements for better source maps else an error like "X is not assignable to bind:Y" is shown on the previous attribute, if there is one --- packages/svelte2tsx/src/htmlxtojsx_v2/nodes/Binding.ts | 7 +------ .../test/htmlx2jsx/samples/binding/expected-svelte5.js | 6 +++--- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/Binding.ts b/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/Binding.ts index 73baf0352..d74c8486b 100644 --- a/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/Binding.ts +++ b/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/Binding.ts @@ -114,12 +114,7 @@ export function handleBinding( ? // HTML typings - preserve the bind: prefix isShorthand ? [`"${str.original.substring(attr.start, attr.end)}"`] - : [ - `"${str.original.substring( - attr.start, - str.original.lastIndexOf('=', attr.expression.start) - )}"` - ] + : ['"', [attr.start, str.original.lastIndexOf('=', attr.expression.start)], '"'] : // Other typings - remove the bind: prefix isShorthand ? [[attr.expression.start, attr.expression.end]] diff --git a/packages/svelte2tsx/test/htmlx2jsx/samples/binding/expected-svelte5.js b/packages/svelte2tsx/test/htmlx2jsx/samples/binding/expected-svelte5.js index 6c881fec9..c2c0e6690 100644 --- a/packages/svelte2tsx/test/htmlx2jsx/samples/binding/expected-svelte5.js +++ b/packages/svelte2tsx/test/htmlx2jsx/samples/binding/expected-svelte5.js @@ -1,6 +1,6 @@ - { svelteHTML.createElement("input", { "type":`text`,"bind:value":test,});/*Ωignore_startΩ*/() => test = __sveltets_2_any(null);/*Ωignore_endΩ*/} - { svelteHTML.createElement("input", { "type":`text`,"bind:value":test,});/*Ωignore_startΩ*/() => test = __sveltets_2_any(null);/*Ωignore_endΩ*/} - { svelteHTML.createElement("input", { "type":`text`,"bind:value":test,});/*Ωignore_startΩ*/() => test = __sveltets_2_any(null);/*Ωignore_endΩ*/} + { svelteHTML.createElement("input", { "type":`text`,"bind:value":test,});/*Ωignore_startΩ*/() => test = __sveltets_2_any(null);/*Ωignore_endΩ*/} + { svelteHTML.createElement("input", { "type":`text`,"bind:value":test,});/*Ωignore_startΩ*/() => test = __sveltets_2_any(null);/*Ωignore_endΩ*/} + { svelteHTML.createElement("input", { "type":`text`,"bind:value":test,});/*Ωignore_startΩ*/() => test = __sveltets_2_any(null);/*Ωignore_endΩ*/} { const $$_tupnI0C = __sveltets_2_ensureComponent(Input); const $$_tupnI0 = new $$_tupnI0C({ target: __sveltets_2_any(), props: { "type":`text`,value:test,}});/*Ωignore_startΩ*/() => test = __sveltets_2_any(null);/*Ωignore_endΩ*/$$_tupnI0.$$bindings = 'value';} { const $$_tupnI0C = __sveltets_2_ensureComponent(Input); const $$_tupnI0 = new $$_tupnI0C({ target: __sveltets_2_any(), props: { "type":`text`,value:test,}});/*Ωignore_startΩ*/() => test = __sveltets_2_any(null);/*Ωignore_endΩ*/$$_tupnI0.$$bindings = 'value';} From 10820f9817eb49432bac0fd7434de3512c285a55 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Fri, 22 Nov 2024 21:02:38 +0100 Subject: [PATCH 15/17] fix: ensure organize imports doesn't mess with generated $$Component type #2594 --- .../src/svelte2tsx/nodes/HoistableInterfaces.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/HoistableInterfaces.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/HoistableInterfaces.ts index 093a4a132..7ee4de9b9 100644 --- a/packages/svelte2tsx/src/svelte2tsx/nodes/HoistableInterfaces.ts +++ b/packages/svelte2tsx/src/svelte2tsx/nodes/HoistableInterfaces.ts @@ -337,8 +337,11 @@ export class HoistableInterfaces { for (const [name, node] of hoistable) { let pos = node.pos + astOffset; - // node.pos includes preceeding whitespace, which could mean we accidentally also move stuff appended to a previous node - if (name !== '$$ComponentProps') { + if (name === '$$ComponentProps') { + // So that organize imports doesn't mess with the types + str.prependRight(pos, '\n'); + } else { + // node.pos includes preceeding whitespace, which could mean we accidentally also move stuff appended to a previous node if (str.original[pos] === '\r') { pos++; } @@ -346,7 +349,8 @@ export class HoistableInterfaces { pos++; } - // jsdoc comments would be ignored if they are on the same line as the ;, so we add a newline, too + // jsdoc comments would be ignored if they are on the same line as the ;, so we add a newline, too. + // Also helps with organize imports not messing with the types str.prependRight(pos, ';\n'); str.appendLeft(node.end + astOffset, ';'); } From 0bf5836857f5ab461077bb299ed35c21c278fdb8 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Fri, 22 Nov 2024 21:02:54 +0100 Subject: [PATCH 16/17] chore: pin TS to 5.6 for now until we properly investigated supporting it --- package.json | 2 +- packages/language-server/package.json | 2 +- packages/svelte-check/package.json | 2 +- packages/svelte-vscode/package.json | 2 +- packages/svelte2tsx/package.json | 2 +- packages/typescript-plugin/package.json | 2 +- pnpm-lock.yaml | 12 ++++++------ 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 438c5c68b..78483ce60 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "lint": "prettier --check ." }, "dependencies": { - "typescript": "^5.6.3" + "typescript": "~5.6.3" }, "devDependencies": { "cross-env": "^7.0.2", diff --git a/packages/language-server/package.json b/packages/language-server/package.json index 6d7a5d4ca..3021cb374 100644 --- a/packages/language-server/package.json +++ b/packages/language-server/package.json @@ -62,7 +62,7 @@ "prettier-plugin-svelte": "^3.3.0", "svelte": "^4.2.19", "svelte2tsx": "workspace:~", - "typescript": "^5.6.3", + "typescript": "~5.6.3", "typescript-auto-import-cache": "^0.3.5", "vscode-css-languageservice": "~6.3.0", "vscode-html-languageservice": "~5.3.0", diff --git a/packages/svelte-check/package.json b/packages/svelte-check/package.json index 8cf77806e..df5d66db3 100644 --- a/packages/svelte-check/package.json +++ b/packages/svelte-check/package.json @@ -54,7 +54,7 @@ "rollup-plugin-copy": "^3.4.0", "svelte": "^4.2.19", "svelte-language-server": "workspace:*", - "typescript": "^5.6.3", + "typescript": "~5.6.3", "vscode-languageserver": "8.0.2", "vscode-languageserver-protocol": "3.17.2", "vscode-languageserver-types": "3.17.2", diff --git a/packages/svelte-vscode/package.json b/packages/svelte-vscode/package.json index 07a9c2542..30ab2dd03 100644 --- a/packages/svelte-vscode/package.json +++ b/packages/svelte-vscode/package.json @@ -732,7 +732,7 @@ "@types/vscode": "^1.67", "js-yaml": "^3.14.0", "tslib": "^2.4.0", - "typescript": "^5.6.3", + "typescript": "~5.6.3", "vscode-tmgrammar-test": "^0.0.11" }, "dependencies": { diff --git a/packages/svelte2tsx/package.json b/packages/svelte2tsx/package.json index 7a9f1758c..c608983da 100644 --- a/packages/svelte2tsx/package.json +++ b/packages/svelte2tsx/package.json @@ -40,7 +40,7 @@ "svelte": "~4.2.19", "tiny-glob": "^0.2.6", "tslib": "^2.4.0", - "typescript": "^5.6.3" + "typescript": "~5.6.3" }, "peerDependencies": { "svelte": "^3.55 || ^4.0.0-next.0 || ^4.0 || ^5.0.0-next.0", diff --git a/packages/typescript-plugin/package.json b/packages/typescript-plugin/package.json index b3e934521..9f41a5dfe 100644 --- a/packages/typescript-plugin/package.json +++ b/packages/typescript-plugin/package.json @@ -24,7 +24,7 @@ "license": "MIT", "devDependencies": { "@types/node": "^18.0.0", - "typescript": "^5.6.3", + "typescript": "~5.6.3", "svelte": "^4.2.19" }, "dependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6e1703cb9..8e27e7797 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,7 +9,7 @@ importers: .: dependencies: typescript: - specifier: ^5.6.3 + specifier: ~5.6.3 version: 5.6.3 devDependencies: cross-env: @@ -58,7 +58,7 @@ importers: specifier: workspace:~ version: link:../svelte2tsx typescript: - specifier: ^5.6.3 + specifier: ~5.6.3 version: 5.6.3 typescript-auto-import-cache: specifier: ^0.3.5 @@ -168,7 +168,7 @@ importers: specifier: workspace:* version: link:../language-server typescript: - specifier: ^5.6.3 + specifier: ~5.6.3 version: 5.6.3 vscode-languageserver: specifier: 8.0.2 @@ -217,7 +217,7 @@ importers: specifier: ^2.4.0 version: 2.5.2 typescript: - specifier: ^5.6.3 + specifier: ~5.6.3 version: 5.6.3 vscode-tmgrammar-test: specifier: ^0.0.11 @@ -299,7 +299,7 @@ importers: specifier: ^2.4.0 version: 2.5.2 typescript: - specifier: ^5.6.3 + specifier: ~5.6.3 version: 5.6.3 packages/typescript-plugin: @@ -318,7 +318,7 @@ importers: specifier: ^4.2.19 version: 4.2.19 typescript: - specifier: ^5.6.3 + specifier: ~5.6.3 version: 5.6.3 packages: From fda35fedbbcc877b210f3b4644ac5edfe720234a Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Fri, 22 Nov 2024 21:06:15 +0100 Subject: [PATCH 17/17] chore: update tests --- .../htmlx2jsx/samples/binding/expectedv2.js | 6 ++-- .../samples/element-attributes/mappings.jsx | 29 ++++++++++--------- .../samples/large-sample-1/mappings.jsx | 6 ++-- 3 files changed, 21 insertions(+), 20 deletions(-) diff --git a/packages/svelte2tsx/test/htmlx2jsx/samples/binding/expectedv2.js b/packages/svelte2tsx/test/htmlx2jsx/samples/binding/expectedv2.js index 132f731ac..5b3a9a052 100644 --- a/packages/svelte2tsx/test/htmlx2jsx/samples/binding/expectedv2.js +++ b/packages/svelte2tsx/test/htmlx2jsx/samples/binding/expectedv2.js @@ -1,6 +1,6 @@ - { svelteHTML.createElement("input", { "type":`text`,"bind:value":test,});/*Ωignore_startΩ*/() => test = __sveltets_2_any(null);/*Ωignore_endΩ*/} - { svelteHTML.createElement("input", { "type":`text`,"bind:value":test,});/*Ωignore_startΩ*/() => test = __sveltets_2_any(null);/*Ωignore_endΩ*/} - { svelteHTML.createElement("input", { "type":`text`,"bind:value":test,});/*Ωignore_startΩ*/() => test = __sveltets_2_any(null);/*Ωignore_endΩ*/} + { svelteHTML.createElement("input", { "type":`text`,"bind:value":test,});/*Ωignore_startΩ*/() => test = __sveltets_2_any(null);/*Ωignore_endΩ*/} + { svelteHTML.createElement("input", { "type":`text`,"bind:value":test,});/*Ωignore_startΩ*/() => test = __sveltets_2_any(null);/*Ωignore_endΩ*/} + { svelteHTML.createElement("input", { "type":`text`,"bind:value":test,});/*Ωignore_startΩ*/() => test = __sveltets_2_any(null);/*Ωignore_endΩ*/} { const $$_tupnI0C = __sveltets_2_ensureComponent(Input); new $$_tupnI0C({ target: __sveltets_2_any(), props: { "type":`text`,value:test,}});/*Ωignore_startΩ*/() => test = __sveltets_2_any(null);/*Ωignore_endΩ*/} { const $$_tupnI0C = __sveltets_2_ensureComponent(Input); new $$_tupnI0C({ target: __sveltets_2_any(), props: { "type":`text`,value:test,}});/*Ωignore_startΩ*/() => test = __sveltets_2_any(null);/*Ωignore_endΩ*/} diff --git a/packages/svelte2tsx/test/sourcemaps/samples/element-attributes/mappings.jsx b/packages/svelte2tsx/test/sourcemaps/samples/element-attributes/mappings.jsx index b5a478a40..f6aa46383 100644 --- a/packages/svelte2tsx/test/sourcemaps/samples/element-attributes/mappings.jsx +++ b/packages/svelte2tsx/test/sourcemaps/samples/element-attributes/mappings.jsx @@ -133,24 +133,25 @@ async•()•=>•{•{•svelteHTML.createElement("element",•{"foo":true,});} ↲ />↲ [original] line 21 (rest generated at line 13) ------------------------------------------------------------------------------------------------------------------------------------------------------ */} - { svelteHTML.createElement("element", { "bind:foo":bar,});/*Ωignore_startΩ*/() => bar = __sveltets_2_any(null);/*Ωignore_endΩ*/}}; {/** -•{•svelteHTML.createElement("element",•{••"bind:foo":bar,});/*Ωignore_startΩ*/()•=>•bar•=•__sveltets_2_any(null);/*Ωignore_endΩ*/}};↲ [generated] line 15 -•{•svelteHTML.createElement("element",•{ "bind:foo": [generated] subset -< element ↲ + { svelteHTML.createElement("element", { "bind:foo":bar,});/*Ωignore_startΩ*/() => bar = __sveltets_2_any(null);/*Ωignore_endΩ*/}}; {/** +•{•svelteHTML.createElement("element",•{•••"bind:foo":bar,});/*Ωignore_startΩ*/()•=>•bar•=•__sveltets_2_any(null);/*Ωignore_endΩ*/}};↲ [generated] line 15 +•{•svelteHTML.createElement("element",•{ " [generated] subset +< element ↲ •bar•=•__sveltets_2_any(null);/*Ωignore_endΩ*/}};↲ [generated] line 15 - • bar,});/*Ωignore_startΩ*/()•=>•bar•=•__sveltets_2_any(null);/*Ωignore_endΩ*/}};↲ [generated] subset - • bar} -• bar} -••••bind:foo={bar}↲ [original] line 24 +•{•svelteHTML.createElement("element",•{•••"bind:foo":bar,});/*Ωignore_startΩ*/()•=>•bar•=•__sveltets_2_any(null);/*Ωignore_endΩ*/}};↲ [generated] line 15 + •• bind:foo":bar,});/*Ωignore_startΩ*/()•=>•bar•=•__sveltets_2_any(null);/*Ωignore_endΩ*/}};↲ [generated] subset + •{ bind:foo= bar} + #== Order-breaking mappings +• bind:foo={bar} +••••bind:foo={bar}↲ [original] line 24 -•{•svelteHTML.createElement("element",•{••"bind:foo":bar,});/*Ωignore_startΩ*/()•=>•bar•=•__sveltets_2_any(null);/*Ωignore_endΩ*/}};↲ [generated] line 15 - • [generated] subset - / +•{•svelteHTML.createElement("element",•{•••"bind:foo":bar,});/*Ωignore_startΩ*/()•=>•bar•=•__sveltets_2_any(null);/*Ωignore_endΩ*/}};↲ [generated] line 15 + • [generated] subset + / / -/> [original] line 25 +/> [original] line 25 ------------------------------------------------------------------------------------------------------------------------------------------------------ */} return { props: /** @type {Record} */ ({}), slots: {}, events: {} }} diff --git a/packages/svelte2tsx/test/sourcemaps/samples/large-sample-1/mappings.jsx b/packages/svelte2tsx/test/sourcemaps/samples/large-sample-1/mappings.jsx index 44c3de9a8..419f8ad51 100644 --- a/packages/svelte2tsx/test/sourcemaps/samples/large-sample-1/mappings.jsx +++ b/packages/svelte2tsx/test/sourcemaps/samples/large-sample-1/mappings.jsx @@ -368,9 +368,9 @@ s ↲ ------------------------------------------------------------------------------------------------------------------------------------------------------ */} { svelteHTML.createElement("svelte:window", { "bind:innerWidth":width,});/*Ωignore_startΩ*/() => width = __sveltets_2_any(null);/*Ωignore_endΩ*/} {/** ••{•svelteHTML.createElement("svelte:window",•{•"bind:innerWidth":width,});/*Ωignore_startΩ*/()•=>•width•=•__sveltets_2_any(null);/*Ωignore_endΩ*/}↲ [generated] line 133 -<> ib width} ↲ - #=============================================# Order-breaking mappings -< bi width} >↲ +<> { bind:innerWidth= width} ↲ + #=============================================#= Order-breaking mappings +< bind:innerWidth={width} >↲ ↲ [original] line 269 (rest generated at line 134) ------------------------------------------------------------------------------------------------------------------------------------------------------ */} {/**