From ce87e1f0d9ce53d62da8269bac54cc7e96e864ab Mon Sep 17 00:00:00 2001 From: "Lyu, Wei-Da" <36730922+jasonlyu123@users.noreply.github.com> Date: Fri, 8 Nov 2024 20:14:25 +0800 Subject: [PATCH 01/26] fix: move globrex to dependencies (#2576) --- packages/language-server/package.json | 2 +- pnpm-lock.yaml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/language-server/package.json b/packages/language-server/package.json index 8206c67d0..e9bd7c68d 100644 --- a/packages/language-server/package.json +++ b/packages/language-server/package.json @@ -46,7 +46,6 @@ "@types/node": "^18.0.0", "@types/sinon": "^7.5.2", "cross-env": "^7.0.2", - "globrex": "^0.1.2", "mocha": "^9.2.0", "sinon": "^11.0.0", "ts-node": "^10.0.0" @@ -57,6 +56,7 @@ "chokidar": "^4.0.1", "estree-walker": "^2.0.1", "fdir": "^6.2.0", + "globrex": "^0.1.2", "lodash": "^4.17.21", "prettier": "~3.3.3", "prettier-plugin-svelte": "^3.2.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0852a1bb4..4148832ee 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -39,6 +39,9 @@ importers: fdir: specifier: ^6.2.0 version: 6.2.0 + globrex: + specifier: ^0.1.2 + version: 0.1.2 lodash: specifier: ^4.17.21 version: 4.17.21 @@ -100,9 +103,6 @@ importers: cross-env: specifier: ^7.0.2 version: 7.0.3 - globrex: - specifier: ^0.1.2 - version: 0.1.2 mocha: specifier: ^9.2.0 version: 9.2.2 From 168e5689727e3d60d7ca1dfc5deeb2d7234d34ea Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Fri, 8 Nov 2024 13:15:51 +0100 Subject: [PATCH 02/26] chore: run CI with node 20 (#2574) openvsx currently fails on deploy because it requires it --- .github/workflows/CI.yml | 6 +++--- .github/workflows/DeployExtensionsProd.yml | 2 +- .github/workflows/DeploySvelte2tsxProd.yml | 2 +- .github/workflows/DeploySvelteCheckProd.yml | 2 +- .github/workflows/DeploySvelteLanguageServerProd.yml | 2 +- .github/workflows/DeployTypescriptPluginProd.yaml | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 7eef59a51..2da0b03ac 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -10,7 +10,7 @@ jobs: - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: - node-version: "18.x" + node-version: "20.x" cache: pnpm # Get projects set up @@ -31,7 +31,7 @@ jobs: - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: - node-version: "18.x" + node-version: "20.x" cache: pnpm # Lets us use one-liner JSON manipulations the package.json files @@ -56,7 +56,7 @@ jobs: - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: - node-version: "18.x" + node-version: "20.x" cache: pnpm # Get projects set up diff --git a/.github/workflows/DeployExtensionsProd.yml b/.github/workflows/DeployExtensionsProd.yml index 3080bb857..6f89f669c 100644 --- a/.github/workflows/DeployExtensionsProd.yml +++ b/.github/workflows/DeployExtensionsProd.yml @@ -14,7 +14,7 @@ jobs: - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: - node-version: "18.x" + node-version: "20.x" registry-url: "https://registry.npmjs.org" cache: pnpm diff --git a/.github/workflows/DeploySvelte2tsxProd.yml b/.github/workflows/DeploySvelte2tsxProd.yml index 1f31d1908..100316e6b 100644 --- a/.github/workflows/DeploySvelte2tsxProd.yml +++ b/.github/workflows/DeploySvelte2tsxProd.yml @@ -17,7 +17,7 @@ jobs: - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: - node-version: "18.x" + node-version: "20.x" registry-url: "https://registry.npmjs.org" cache: pnpm diff --git a/.github/workflows/DeploySvelteCheckProd.yml b/.github/workflows/DeploySvelteCheckProd.yml index bbfc47610..f5fc54cad 100644 --- a/.github/workflows/DeploySvelteCheckProd.yml +++ b/.github/workflows/DeploySvelteCheckProd.yml @@ -17,7 +17,7 @@ jobs: - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: - node-version: "18.x" + node-version: "20.x" registry-url: "https://registry.npmjs.org" cache: pnpm diff --git a/.github/workflows/DeploySvelteLanguageServerProd.yml b/.github/workflows/DeploySvelteLanguageServerProd.yml index 67cb708fa..768448427 100644 --- a/.github/workflows/DeploySvelteLanguageServerProd.yml +++ b/.github/workflows/DeploySvelteLanguageServerProd.yml @@ -17,7 +17,7 @@ jobs: - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: - node-version: "18.x" + node-version: "20.x" registry-url: "https://registry.npmjs.org" cache: pnpm diff --git a/.github/workflows/DeployTypescriptPluginProd.yaml b/.github/workflows/DeployTypescriptPluginProd.yaml index 90d1048c0..a93e95a29 100644 --- a/.github/workflows/DeployTypescriptPluginProd.yaml +++ b/.github/workflows/DeployTypescriptPluginProd.yaml @@ -17,7 +17,7 @@ jobs: - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: - node-version: "18.x" + node-version: "20.x" registry-url: "https://registry.npmjs.org" cache: pnpm From 7bfe801237edaa5f4e4da88c05c7ccf7ab85ba3b Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Sat, 9 Nov 2024 12:52:37 +0100 Subject: [PATCH 03/26] fix: ensure code lenses from different sources are merged --- packages/language-server/src/plugins/PluginHost.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/language-server/src/plugins/PluginHost.ts b/packages/language-server/src/plugins/PluginHost.ts index bbb3eac46..d1613134f 100644 --- a/packages/language-server/src/plugins/PluginHost.ts +++ b/packages/language-server/src/plugins/PluginHost.ts @@ -633,12 +633,13 @@ export class PluginHost implements LSProvider, OnWatchFileChanges { throw new Error('Cannot call methods on an unopened document'); } - return await this.execute( + const result = await this.execute( 'getCodeLens', [document], - ExecuteMode.FirstNonNull, + ExecuteMode.Collect, 'smart' ); + return flatten(result.filter(Boolean)); } async getFoldingRanges(textDocument: TextDocumentIdentifier): Promise { From 5bd466314fe5ec1e8209b78f758f215a374c3cbf Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Sat, 9 Nov 2024 13:03:36 +0100 Subject: [PATCH 04/26] fix: make it possible to disable runes/legacy mode code lens --- packages/language-server/README.md | 4 ++++ packages/language-server/src/ls-config.ts | 2 ++ packages/language-server/src/plugins/svelte/SveltePlugin.ts | 2 ++ packages/svelte-vscode/package.json | 6 ++++++ 4 files changed, 14 insertions(+) diff --git a/packages/language-server/README.md b/packages/language-server/README.md index 7aa5a74fc..9372d9687 100644 --- a/packages/language-server/README.md +++ b/packages/language-server/README.md @@ -281,6 +281,10 @@ Enable code actions for Svelte. _Default_: `true` Enable selection range for Svelte. _Default_: `true` +##### `svelte.plugin.svelte.runesLegacyModeCodeLens.enable` + +Whether or not to show a code lens at the top of Svelte files indicating if they are in runes mode or legacy mode. Only visible in Svelte 5 projects. _Default_: `true` + ##### `svelte.plugin.svelte.defaultScriptLanguage` The default language to use when generating new script tags in Svelte. _Default_: `none` diff --git a/packages/language-server/src/ls-config.ts b/packages/language-server/src/ls-config.ts index 3f5469940..abe59c736 100644 --- a/packages/language-server/src/ls-config.ts +++ b/packages/language-server/src/ls-config.ts @@ -63,6 +63,7 @@ const defaultLSConfig: LSConfig = { hover: { enable: true }, codeActions: { enable: true }, selectionRange: { enable: true }, + runesLegacyModeCodeLens: { enable: true }, defaultScriptLanguage: 'none' } }; @@ -188,6 +189,7 @@ export interface LSSvelteConfig { selectionRange: { enable: boolean; }; + runesLegacyModeCodeLens: { enable: boolean }; defaultScriptLanguage: 'none' | 'ts'; } diff --git a/packages/language-server/src/plugins/svelte/SveltePlugin.ts b/packages/language-server/src/plugins/svelte/SveltePlugin.ts index aedc32e54..f3d941716 100644 --- a/packages/language-server/src/plugins/svelte/SveltePlugin.ts +++ b/packages/language-server/src/plugins/svelte/SveltePlugin.ts @@ -51,6 +51,8 @@ export class SveltePlugin constructor(private configManager: LSConfigManager) {} async getCodeLens(document: Document): Promise { + if (!this.featureEnabled('runesLegacyModeCodeLens')) return null; + const doc = await this.getSvelteDoc(document); if (!doc.isSvelte5) return null; diff --git a/packages/svelte-vscode/package.json b/packages/svelte-vscode/package.json index 6bfd184c1..07a9c2542 100644 --- a/packages/svelte-vscode/package.json +++ b/packages/svelte-vscode/package.json @@ -384,6 +384,12 @@ "title": "Svelte: Rename", "description": "Enable rename/move Svelte files functionality" }, + "svelte.plugin.svelte.runesLegacyModeCodeLens.enable": { + "type": "boolean", + "default": true, + "title": "Svelte: Legacy/Runes mode Code Lens", + "description": "Show a code lens at the top of Svelte files indicating if they are in runes mode or legacy mode. Only visible in Svelte 5 projects." + }, "svelte.plugin.svelte.defaultScriptLanguage": { "type": "string", "default": "none", From 0698bc77e8ab6558ea8d983fae624b6f7c295dc7 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Sat, 9 Nov 2024 13:16:09 +0100 Subject: [PATCH 05/26] fix: infer types for `$bindable`, infer function type from arrow function #2577 --- .../src/svelte2tsx/nodes/ExportedNames.ts | 43 ++++++++++++------- .../runes-best-effort-types.v5/expectedv2.ts | 4 +- .../runes-best-effort-types.v5/input.svelte | 2 +- .../expectedv2.ts | 6 +-- .../input.svelte | 2 +- 5 files changed, 34 insertions(+), 23 deletions(-) diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/ExportedNames.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/ExportedNames.ts index c01098d63..e4b05eaf4 100644 --- a/packages/svelte2tsx/src/svelte2tsx/nodes/ExportedNames.ts +++ b/packages/svelte2tsx/src/svelte2tsx/nodes/ExportedNames.ts @@ -277,22 +277,33 @@ export class ExportedNames { props.push(`form: import('./$types.js').ActionData`); } } else if (element.initializer) { - const type = ts.isAsExpression(element.initializer) - ? element.initializer.type.getText() - : ts.isStringLiteral(element.initializer) - ? 'string' - : ts.isNumericLiteral(element.initializer) - ? 'number' - : element.initializer.kind === ts.SyntaxKind.TrueKeyword || - element.initializer.kind === ts.SyntaxKind.FalseKeyword - ? 'boolean' - : ts.isIdentifier(element.initializer) - ? `typeof ${element.initializer.text}` - : ts.isObjectLiteralExpression(element.initializer) - ? 'Record' - : ts.isArrayLiteralExpression(element.initializer) - ? 'unknown[]' - : 'unknown'; + 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 + ? 'unknown' + : 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) + ? 'unknown[]' + : 'unknown'; props.push(`${name}?: ${type}`); } else { props.push(`${name}: unknown`); diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/runes-best-effort-types.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/runes-best-effort-types.v5/expectedv2.ts index 7947bc2b2..7404a44ab 100644 --- a/packages/svelte2tsx/test/svelte2tsx/samples/runes-best-effort-types.v5/expectedv2.ts +++ b/packages/svelte2tsx/test/svelte2tsx/samples/runes-best-effort-types.v5/expectedv2.ts @@ -1,10 +1,10 @@ /// ;function render() { - let/** @typedef {{ a: unknown, b?: boolean, c?: number, d?: string, e?: unknown, f?: Record, g?: typeof foo, h?: unknown[] }} $$ComponentProps *//** @type {$$ComponentProps} */ { a, b = true, c = 1, d = '', e = null, f = {}, g = foo, h = [] } = $props(); + let/** @typedef {{ a: unknown, b?: boolean, c?: number, d?: string, e?: unknown, f?: Record, g?: typeof foo, h?: unknown[], i?: unknown, j?: unknown, k?: number, l?: Function }} $$ComponentProps *//** @type {$$ComponentProps} */ { a, b = true, c = 1, d = '', e = null, f = {}, g = foo, h = [], i = undefined, j = $bindable(), k = $bindable(1), l = () => {} } = $props(); ; async () => {}; -return { props: /** @type {$$ComponentProps} */({}), exports: {}, bindings: __sveltets_$$bindings(''), slots: {}, events: {} }} +return { props: /** @type {$$ComponentProps} */({}), exports: {}, bindings: __sveltets_$$bindings('j', 'k'), 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/runes-best-effort-types.v5/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/runes-best-effort-types.v5/input.svelte index b4dc7f14b..73baf2a20 100644 --- a/packages/svelte2tsx/test/svelte2tsx/samples/runes-best-effort-types.v5/input.svelte +++ b/packages/svelte2tsx/test/svelte2tsx/samples/runes-best-effort-types.v5/input.svelte @@ -1,3 +1,3 @@ diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-best-effort-types.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-best-effort-types.v5/expectedv2.ts index 6f830be1c..455e1b9b6 100644 --- a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-best-effort-types.v5/expectedv2.ts +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-best-effort-types.v5/expectedv2.ts @@ -1,10 +1,10 @@ /// ;function render() { -/*Ωignore_startΩ*/;type $$ComponentProps = { a: unknown, b?: boolean, c?: number, d?: string, e?: unknown, f?: Record, g?: typeof foo, h?: Bar, i?: Baz, j?: unknown[] };/*Ωignore_endΩ*/ - let { a, b = true, c = 1, d = '', e = null, f = {}, g = foo, h = null as Bar, i = null as any as Baz, j = [] }: $$ComponentProps = $props(); +/*Ωignore_startΩ*/;type $$ComponentProps = { a: unknown, b?: boolean, c?: number, d?: string, e?: unknown, f?: Record, g?: typeof foo, h?: Bar, i?: Baz, j?: unknown[], k?: unknown, l?: unknown, m?: number, n?: Function };/*Ωignore_endΩ*/ + let { a, b = true, c = 1, d = '', e = null, f = {}, g = foo, h = null as Bar, i = null as any as Baz, j = [], k = undefined, l = $bindable(), m = $bindable(1), n = () => {} }: $$ComponentProps = $props(); ; async () => {}; -return { props: {} as any as $$ComponentProps, exports: {}, bindings: __sveltets_$$bindings(''), slots: {}, events: {} }} +return { props: {} as any as $$ComponentProps, exports: {}, bindings: __sveltets_$$bindings('l', 'm'), 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-best-effort-types.v5/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-best-effort-types.v5/input.svelte index cda8c476f..c4ce94411 100644 --- a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-best-effort-types.v5/input.svelte +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-best-effort-types.v5/input.svelte @@ -1,3 +1,3 @@ From 49c7e58ff001d742b6ceabee5012ea344ab2b172 Mon Sep 17 00:00:00 2001 From: SHYAKA Davis <87414827+shyakadavis@users.noreply.github.com> Date: Mon, 11 Nov 2024 16:18:09 +0200 Subject: [PATCH 06/26] chore: update Svelte documentation links for template syntax (#2579) --- .../src/plugins/svelte/features/SvelteTags.ts | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/language-server/src/plugins/svelte/features/SvelteTags.ts b/packages/language-server/src/plugins/svelte/features/SvelteTags.ts index 12d53a1b5..6589667d4 100644 --- a/packages/language-server/src/plugins/svelte/features/SvelteTags.ts +++ b/packages/language-server/src/plugins/svelte/features/SvelteTags.ts @@ -23,7 +23,7 @@ Await blocks allow you to branch on the three possible states of a Promise — p \`{#await expression}...{:then name}...{/await}\`\\ \`{#await expression then name}...{/await}\`\\ \\ -https://svelte.dev/docs#template-syntax-await +https://svelte.dev/docs/svelte/await `, each: `\`{#each ...}\`\\ Iterating over lists of values can be done with an each block. @@ -33,7 +33,7 @@ Iterating over lists of values can be done with an each block. \`{#each expression as name, index (key)}...{/each}\`\\ \`{#each expression as name}...{:else}...{/each}\`\\ \\ -https://svelte.dev/docs#template-syntax-each +https://svelte.dev/docs/svelte/each `, if: `\`{#if ...}\`\\ Content that is conditionally rendered can be wrapped in an if block. @@ -42,7 +42,7 @@ Content that is conditionally rendered can be wrapped in an if block. \`{#if expression}...{:else if expression}...{/if}\`\\ \`{#if expression}...{:else}...{/if}\`\\ \\ -https://svelte.dev/docs#template-syntax-if +https://svelte.dev/docs/svelte/if `, key: `\`{#key expression}...{/key}\`\\ Key blocks destroy and recreate their contents when the value of an expression changes.\\ @@ -51,14 +51,22 @@ When used around components, this will cause them to be reinstantiated and reini #### Usage: \`{#key expression}...{/key}\`\\ \\ -https://svelte.dev/docs#template-syntax-key +https://svelte.dev/docs/svelte/key `, snippet: `\`{#snippet identifier(parameter)}...{/snippet}\`\\ Snippets allow you to create reusable UI blocks you can render with the {@render ...} tag. They also function as slot props for components. +#### Usage: +\`{#snippet identifier(parameter)}...{/snippet}\`\\ +\\ +https://svelte.dev/docs/svelte/snippet `, render: `\`{@render ...}\`\\ Renders a snippet with the given parameters. +#### Usage: +\`{@render identifier(parameter)}\`\\ +\\ +https://svelte.dev/docs/svelte/@render `, html: `\`{@html ...}\`\\ @@ -72,7 +80,7 @@ If the data comes from an untrusted source, you must sanitize it, ` + #### Usage: \`{@html expression}\`\\ \\ -https://svelte.dev/docs#template-syntax-html +https://svelte.dev/docs/svelte/@html `, debug: `\`{@debug ...}\`\\ @@ -84,14 +92,14 @@ It accepts a comma-separated list of variable names (not arbitrary expressions). \`{@debug}\` \`{@debug var1, var2, ..., varN}\`\\ \\ -https://svelte.dev/docs#template-syntax-debug +https://svelte.dev/docs/svelte/@debug `, const: `\`{@const ...}\`\\ Defines a local constant}\\ #### Usage: \`{@const a = b + c}\`\\ \\ -https://svelte.dev/docs/special-tags#const +https://svelte.dev/docs/svelte/@const ` }; From bf1b8ef01eaa0a9deb175cfac73fe1286d115c71 Mon Sep 17 00:00:00 2001 From: Yuri <22861150+YpsilonTM@users.noreply.github.com> Date: Mon, 11 Nov 2024 15:22:14 +0100 Subject: [PATCH 07/26] fix: adjust generated `+layout.svelte` file contents (#2580) --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- .../generateFiles/templates/layout.ts | 19 ++++++++++++++----- .../sveltekit/generateFiles/templates/page.ts | 8 ++++---- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/packages/svelte-vscode/src/sveltekit/generateFiles/templates/layout.ts b/packages/svelte-vscode/src/sveltekit/generateFiles/templates/layout.ts index 8a1dac68d..d078e334f 100644 --- a/packages/svelte-vscode/src/sveltekit/generateFiles/templates/layout.ts +++ b/packages/svelte-vscode/src/sveltekit/generateFiles/templates/layout.ts @@ -5,29 +5,38 @@ const defaultScriptTemplate = ` /** @type {import('./$types').LayoutData} */ export let data; + + `; const tsSv5ScriptTemplate = ` + +{@render children()} `; const tsScriptTemplate = ` + + `; const jsSv5ScriptTemplate = ` + +{@render children()} `; const scriptTemplate: ReadonlyMap = new Map([ diff --git a/packages/svelte-vscode/src/sveltekit/generateFiles/templates/page.ts b/packages/svelte-vscode/src/sveltekit/generateFiles/templates/page.ts index 3015afdbb..ecbbfa57f 100644 --- a/packages/svelte-vscode/src/sveltekit/generateFiles/templates/page.ts +++ b/packages/svelte-vscode/src/sveltekit/generateFiles/templates/page.ts @@ -10,22 +10,22 @@ const defaultScriptTemplate = ` const tsSv5ScriptTemplate = ` `; const tsScriptTemplate = ` `; const jsSv5ScriptTemplate = ` `; From 437b0e12cc6fb966d298b9eddd1e5e82bf1dbdad Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Mon, 11 Nov 2024 15:47:08 +0100 Subject: [PATCH 08/26] fix: use user's Svelte parser when formatting if necessary (#2578) This makes formatting "just work" if users use Svelte 5 but don't have prettier (and the plugin) installed in their workspace Depends on https://github.com/sveltejs/prettier-plugin-svelte/pull/471 #2542 --- packages/language-server/package.json | 2 +- .../src/plugins/svelte/SveltePlugin.ts | 15 +++++++++++---- pnpm-lock.yaml | 10 +++++----- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/packages/language-server/package.json b/packages/language-server/package.json index e9bd7c68d..46f970633 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.6", + "prettier-plugin-svelte": "^3.2.8", "svelte": "^4.2.19", "svelte2tsx": "workspace:~", "typescript": "^5.6.3", diff --git a/packages/language-server/src/plugins/svelte/SveltePlugin.ts b/packages/language-server/src/plugins/svelte/SveltePlugin.ts index f3d941716..3fe7c49a5 100644 --- a/packages/language-server/src/plugins/svelte/SveltePlugin.ts +++ b/packages/language-server/src/plugins/svelte/SveltePlugin.ts @@ -127,11 +127,9 @@ export class SveltePlugin /** * Prettier v2 can't use v3 plugins and vice versa. Therefore, we need to check * which version of prettier is used in the workspace and import the correct - * version of the Svelte plugin. If user uses Prettier >= 3 and has no Svelte plugin - * then fall back to our built-in versions which are both v2 and compatible with + * version of the Svelte plugin. If user uses Prettier < 3 and has no Svelte plugin + * then fall back to our built-in versions which are both v3 and compatible with * each other. - * TODO switch this around at some point to load Prettier v3 by default because it's - * more likely that users have that installed. */ const importFittingPrettier = async () => { const getConfig = async (p: any) => { @@ -206,6 +204,15 @@ export class SveltePlugin return []; } + if (isFallback || !(await hasSveltePluginLoaded(prettier, resolvedPlugins))) { + // If the user uses Svelte 5 but doesn't have prettier installed, we need to provide + // the compiler path to the plugin so it can use its parser method; else it will crash. + const svelteCompilerInfo = getPackageInfo('svelte', filePath); + if (svelteCompilerInfo.version.major >= 5) { + config.svelte5CompilerPath = svelteCompilerInfo.path + '/compiler'; + } + } + // Prettier v3 format is async, v2 is not const formattedCode = await prettier.format(document.getText(), { ...config, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4148832ee..39dbfbe20 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.6 - version: 3.2.6(prettier@3.3.3)(svelte@4.2.19) + specifier: ^3.2.8 + version: 3.2.8(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.6: - resolution: {integrity: sha512-Y1XWLw7vXUQQZmgv1JAEiLcErqUniAF2wO7QJsw8BVMvpLET2dI5WpEIEJx1r11iHVdSMzQxivyfrH9On9t2IQ==} + prettier-plugin-svelte@3.2.8: + resolution: {integrity: sha512-PAHmmU5cGZdnhW4mWhmvxuG2PVbbHIxUuPOdUKvfE+d4Qt2d29iU5VWrPdsaW5YqVEE0nqhlvN4eoKmVMpIF3Q==} 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.6(prettier@3.3.3)(svelte@4.2.19): + prettier-plugin-svelte@3.2.8(prettier@3.3.3)(svelte@4.2.19): dependencies: prettier: 3.3.3 svelte: 4.2.19 From 77160ff7956744311ce3fc8c65b99eb7ae9e0dc7 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Tue, 12 Nov 2024 11:48:01 +0100 Subject: [PATCH 09/26] fix: fall back to `any` instead of `unknown` for untyped `$props` (#2582) The idea was to be conservative about it, but this causes problems for people not wanting strict type checking but only a basic form of it. Falling back to `any` is the more pragmatic choice #2556 --- .../src/svelte2tsx/nodes/ExportedNames.ts | 17 ++++++++--------- .../runes-best-effort-types.v5/expectedv2.ts | 2 +- .../samples/runes-bindable.v5/expectedv2.ts | 2 +- .../runes-looking-like-stores.v5/expectedv2.ts | 2 +- .../ts-runes-best-effort-types.v5/expectedv2.ts | 2 +- .../samples/ts-runes-bindable.v5/expectedv2.ts | 2 +- 6 files changed, 13 insertions(+), 14 deletions(-) diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/ExportedNames.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/ExportedNames.ts index e4b05eaf4..d3083bb88 100644 --- a/packages/svelte2tsx/src/svelte2tsx/nodes/ExportedNames.ts +++ b/packages/svelte2tsx/src/svelte2tsx/nodes/ExportedNames.ts @@ -284,7 +284,7 @@ export class ExportedNames { ? element.initializer.arguments[0] : element.initializer; const type = !initializer - ? 'unknown' + ? 'any' : ts.isAsExpression(initializer) ? initializer.type.getText() : ts.isStringLiteral(initializer) @@ -300,13 +300,13 @@ export class ExportedNames { : ts.isArrowFunction(initializer) ? 'Function' : ts.isObjectLiteralExpression(initializer) - ? 'Record' + ? 'Record' : ts.isArrayLiteralExpression(initializer) - ? 'unknown[]' - : 'unknown'; + ? 'any[]' + : 'any'; props.push(`${name}?: ${type}`); } else { - props.push(`${name}: unknown`); + props.push(`${name}: any`); } } } @@ -317,15 +317,14 @@ export class ExportedNames { if (props.length > 0) { propsStr = - `{ ${props.join(', ')} }` + - (withUnknown ? ' & Record' : ''); + `{ ${props.join(', ')} }` + (withUnknown ? ' & Record' : ''); } else if (withUnknown) { - propsStr = 'Record'; + propsStr = 'Record'; } else { propsStr = 'Record'; } } else { - propsStr = 'Record'; + propsStr = 'Record'; } // Create a virtual type alias for the unnamed generic and reuse it for the props return type diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/runes-best-effort-types.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/runes-best-effort-types.v5/expectedv2.ts index 7404a44ab..72cffb46c 100644 --- a/packages/svelte2tsx/test/svelte2tsx/samples/runes-best-effort-types.v5/expectedv2.ts +++ b/packages/svelte2tsx/test/svelte2tsx/samples/runes-best-effort-types.v5/expectedv2.ts @@ -1,7 +1,7 @@ /// ;function render() { - let/** @typedef {{ a: unknown, b?: boolean, c?: number, d?: string, e?: unknown, f?: Record, g?: typeof foo, h?: unknown[], i?: unknown, j?: unknown, k?: number, l?: Function }} $$ComponentProps *//** @type {$$ComponentProps} */ { a, b = true, c = 1, d = '', e = null, f = {}, g = foo, h = [], i = undefined, j = $bindable(), k = $bindable(1), l = () => {} } = $props(); + let/** @typedef {{ a: any, b?: boolean, c?: number, d?: string, e?: any, f?: Record, g?: typeof foo, h?: any[], i?: any, j?: any, k?: number, l?: Function }} $$ComponentProps *//** @type {$$ComponentProps} */ { a, b = true, c = 1, d = '', e = null, f = {}, g = foo, h = [], i = undefined, j = $bindable(), k = $bindable(1), l = () => {} } = $props(); ; async () => {}; return { props: /** @type {$$ComponentProps} */({}), exports: {}, bindings: __sveltets_$$bindings('j', 'k'), slots: {}, events: {} }} diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/runes-bindable.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/runes-bindable.v5/expectedv2.ts index 5379c6643..f392054d8 100644 --- a/packages/svelte2tsx/test/svelte2tsx/samples/runes-bindable.v5/expectedv2.ts +++ b/packages/svelte2tsx/test/svelte2tsx/samples/runes-bindable.v5/expectedv2.ts @@ -1,7 +1,7 @@ /// ;function render() { - let/** @typedef {{ a: unknown, b?: unknown }} $$ComponentProps *//** @type {$$ComponentProps} */ { a, b = $bindable() } = $props(); + let/** @typedef {{ a: any, b?: any }} $$ComponentProps *//** @type {$$ComponentProps} */ { a, b = $bindable() } = $props(); ; async () => {}; return { props: /** @type {$$ComponentProps} */({}), exports: {}, bindings: __sveltets_$$bindings('b'), slots: {}, events: {} }} diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/runes-looking-like-stores.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/runes-looking-like-stores.v5/expectedv2.ts index 2abbbff95..555322c0a 100644 --- a/packages/svelte2tsx/test/svelte2tsx/samples/runes-looking-like-stores.v5/expectedv2.ts +++ b/packages/svelte2tsx/test/svelte2tsx/samples/runes-looking-like-stores.v5/expectedv2.ts @@ -1,7 +1,7 @@ /// ;function render() { - let/** @typedef {{ props: unknown }} $$ComponentProps *//** @type {$$ComponentProps} */ { props } = $props(); + let/** @typedef {{ props: any }} $$ComponentProps *//** @type {$$ComponentProps} */ { props } = $props(); let state = $state(0); let derived = $derived(state * 2); ; diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-best-effort-types.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-best-effort-types.v5/expectedv2.ts index 455e1b9b6..b819a0c69 100644 --- a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-best-effort-types.v5/expectedv2.ts +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-best-effort-types.v5/expectedv2.ts @@ -1,6 +1,6 @@ /// ;function render() { -/*Ωignore_startΩ*/;type $$ComponentProps = { a: unknown, b?: boolean, c?: number, d?: string, e?: unknown, f?: Record, g?: typeof foo, h?: Bar, i?: Baz, j?: unknown[], k?: unknown, l?: unknown, m?: number, n?: Function };/*Ωignore_endΩ*/ +/*Ωignore_startΩ*/;type $$ComponentProps = { a: any, b?: boolean, c?: number, d?: string, e?: any, f?: Record, g?: typeof foo, h?: Bar, i?: Baz, j?: any[], k?: any, l?: any, m?: number, n?: Function };/*Ωignore_endΩ*/ let { a, b = true, c = 1, d = '', e = null, f = {}, g = foo, h = null as Bar, i = null as any as Baz, j = [], k = undefined, l = $bindable(), m = $bindable(1), n = () => {} }: $$ComponentProps = $props(); ; async () => {}; diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-bindable.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-bindable.v5/expectedv2.ts index f2686212b..22c641cf8 100644 --- a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-bindable.v5/expectedv2.ts +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-bindable.v5/expectedv2.ts @@ -1,6 +1,6 @@ /// ;function render() { -/*Ωignore_startΩ*/;type $$ComponentProps = { a: unknown, b?: unknown, c?: number };/*Ωignore_endΩ*/ +/*Ωignore_startΩ*/;type $$ComponentProps = { a: any, b?: any, c?: number };/*Ωignore_endΩ*/ let { a, b = $bindable(), c = $bindable(0) as number }: $$ComponentProps = $props(); ; async () => {}; From 1b205c280a27fb248d1b1c0b9bcbebbd7f5abc7f Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Thu, 14 Nov 2024 10:33:23 +0100 Subject: [PATCH 10/26] 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 11/26] 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 12/26] 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 13/26] 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 14/26] 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 15/26] 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 16/26] 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 17/26] 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 18/26] 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 19/26] 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 20/26] 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 21/26] 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 22/26] 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 23/26] 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 24/26] 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 25/26] 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 26/26] 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) ------------------------------------------------------------------------------------------------------------------------------------------------------ */} {/**