From 65f47540911b274e91ba93faf023d2c6b73de3d8 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 17 Jun 2025 09:06:41 -0400 Subject: [PATCH 1/8] fix: support `using` keyword --- .changeset/dull-cheetahs-watch.md | 5 +++ packages/svelte/package.json | 4 +-- .../src/compiler/phases/1-parse/acorn.js | 4 +-- packages/svelte/test.js | 12 +++++++ pnpm-lock.yaml | 32 ++++++++----------- 5 files changed, 35 insertions(+), 22 deletions(-) create mode 100644 .changeset/dull-cheetahs-watch.md create mode 100644 packages/svelte/test.js diff --git a/.changeset/dull-cheetahs-watch.md b/.changeset/dull-cheetahs-watch.md new file mode 100644 index 000000000000..be518a65b278 --- /dev/null +++ b/.changeset/dull-cheetahs-watch.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: support `using` keyword diff --git a/packages/svelte/package.json b/packages/svelte/package.json index d2fbdb32f74c..2c076c42c713 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -164,9 +164,9 @@ "dependencies": { "@ampproject/remapping": "^2.3.0", "@jridgewell/sourcemap-codec": "^1.5.0", - "@types/estree": "^1.0.5", - "acorn": "^8.12.1", "@sveltejs/acorn-typescript": "^1.0.5", + "@types/estree": "^1.0.5", + "acorn": "^8.15.0", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", diff --git a/packages/svelte/src/compiler/phases/1-parse/acorn.js b/packages/svelte/src/compiler/phases/1-parse/acorn.js index 26a09abb66b7..cdcec809bd29 100644 --- a/packages/svelte/src/compiler/phases/1-parse/acorn.js +++ b/packages/svelte/src/compiler/phases/1-parse/acorn.js @@ -36,7 +36,7 @@ export function parse(source, typescript, is_script) { ast = parser.parse(source, { onComment, sourceType: 'module', - ecmaVersion: 16, + ecmaVersion: 'latest', locations: true }); } finally { @@ -64,7 +64,7 @@ export function parse_expression_at(source, typescript, index) { const ast = parser.parseExpressionAt(source, index, { onComment, sourceType: 'module', - ecmaVersion: 16, + ecmaVersion: 'latest', locations: true }); diff --git a/packages/svelte/test.js b/packages/svelte/test.js new file mode 100644 index 000000000000..6510c0def69b --- /dev/null +++ b/packages/svelte/test.js @@ -0,0 +1,12 @@ +import { parse } from 'acorn'; + +const code = ` +using bar = baz(); +`; + +const ast = parse(code, { + sourceType: 'module', + ecmaVersion: 'latest' +}); + +console.log(JSON.stringify(ast, null, ' ')); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cfbc54df3363..fa1121c99a3b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -67,13 +67,13 @@ importers: version: 1.5.0 '@sveltejs/acorn-typescript': specifier: ^1.0.5 - version: 1.0.5(acorn@8.14.0) + version: 1.0.5(acorn@8.15.0) '@types/estree': specifier: ^1.0.5 version: 1.0.6 acorn: - specifier: ^8.12.1 - version: 8.14.0 + specifier: ^8.15.0 + version: 8.15.0 aria-query: specifier: ^5.3.1 version: 5.3.1 @@ -908,13 +908,13 @@ packages: peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - acorn@8.14.0: - resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} + acorn@8.14.1: + resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} engines: {node: '>=0.4.0'} hasBin: true - acorn@8.14.1: - resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} engines: {node: '>=0.4.0'} hasBin: true @@ -2990,9 +2990,9 @@ snapshots: eslint-visitor-keys: 3.4.3 espree: 9.6.1 - '@sveltejs/acorn-typescript@1.0.5(acorn@8.14.0)': + '@sveltejs/acorn-typescript@1.0.5(acorn@8.15.0)': dependencies: - acorn: 8.14.0 + acorn: 8.15.0 '@sveltejs/eslint-config@8.1.0(@stylistic/eslint-plugin-js@1.8.0(eslint@9.9.1))(eslint-config-prettier@9.1.0(eslint@9.9.1))(eslint-plugin-n@17.16.1(eslint@9.9.1)(typescript@5.5.4))(eslint-plugin-svelte@2.38.0(eslint@9.9.1)(svelte@packages+svelte))(eslint@9.9.1)(typescript-eslint@8.26.0(eslint@9.9.1)(typescript@5.5.4))(typescript@5.5.4)': dependencies: @@ -3231,18 +3231,14 @@ snapshots: loupe: 3.1.3 tinyrainbow: 1.2.0 - acorn-jsx@5.3.2(acorn@8.14.0): - dependencies: - acorn: 8.14.0 - acorn-jsx@5.3.2(acorn@8.14.1): dependencies: acorn: 8.14.1 - acorn@8.14.0: {} - acorn@8.14.1: {} + acorn@8.15.0: {} + agent-base@7.1.1: dependencies: debug: 4.4.0 @@ -3606,8 +3602,8 @@ snapshots: espree@10.1.0: dependencies: - acorn: 8.14.0 - acorn-jsx: 5.3.2(acorn@8.14.0) + acorn: 8.14.1 + acorn-jsx: 5.3.2(acorn@8.14.1) eslint-visitor-keys: 4.2.0 espree@9.6.1: @@ -4457,7 +4453,7 @@ snapshots: terser@5.27.0: dependencies: '@jridgewell/source-map': 0.3.6 - acorn: 8.14.0 + acorn: 8.15.0 commander: 2.20.3 source-map-support: 0.5.21 From 68a38d4b167e4fe058d6be5f06e0ab6a76362a3b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 17 Jun 2025 09:08:27 -0400 Subject: [PATCH 2/8] delete test file --- packages/svelte/test.js | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 packages/svelte/test.js diff --git a/packages/svelte/test.js b/packages/svelte/test.js deleted file mode 100644 index 6510c0def69b..000000000000 --- a/packages/svelte/test.js +++ /dev/null @@ -1,12 +0,0 @@ -import { parse } from 'acorn'; - -const code = ` -using bar = baz(); -`; - -const ast = parse(code, { - sourceType: 'module', - ecmaVersion: 'latest' -}); - -console.log(JSON.stringify(ast, null, ' ')); From 0fd39219d34b319ae770842c3f85e62ca4ca695a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 17 Jun 2025 09:18:07 -0400 Subject: [PATCH 3/8] fix --- .../parser-legacy/samples/each-block-destructured/output.json | 3 ++- .../samples/script-context-module-unquoted/output.json | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/svelte/tests/parser-legacy/samples/each-block-destructured/output.json b/packages/svelte/tests/parser-legacy/samples/each-block-destructured/output.json index d19f5cbbfd8c..637da24aea98 100644 --- a/packages/svelte/tests/parser-legacy/samples/each-block-destructured/output.json +++ b/packages/svelte/tests/parser-legacy/samples/each-block-destructured/output.json @@ -259,7 +259,8 @@ "kind": "let" }, "specifiers": [], - "source": null + "source": null, + "attributes": [] } ], "sourceType": "module" diff --git a/packages/svelte/tests/parser-legacy/samples/script-context-module-unquoted/output.json b/packages/svelte/tests/parser-legacy/samples/script-context-module-unquoted/output.json index 03a526f04e37..64250cb302e4 100644 --- a/packages/svelte/tests/parser-legacy/samples/script-context-module-unquoted/output.json +++ b/packages/svelte/tests/parser-legacy/samples/script-context-module-unquoted/output.json @@ -169,7 +169,8 @@ "kind": "const" }, "specifiers": [], - "source": null + "source": null, + "attributes": [] } ], "sourceType": "module" From 8bb4083b67ca92c4989730c7af64c6b66b4b5dec Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 17 Jun 2025 11:42:21 -0400 Subject: [PATCH 4/8] feat: link top-level `using` declarations in components to lifecycle --- .changeset/nasty-hotels-clap.md | 5 ++++ .../src/compiler/phases/2-analyze/index.js | 3 +- .../3-transform/client/transform-client.js | 4 +++ .../client/visitors/VariableDeclaration.js | 21 +++++++++++++- .../svelte/src/compiler/phases/types.d.ts | 4 +++ packages/svelte/src/internal/client/index.js | 1 + .../client/resource-management/index.js | 26 +++++++++++++++++ .../samples/using-top-level/Child.svelte | 12 ++++++++ .../samples/using-top-level/_config.js | 18 ++++++++++++ .../samples/using-top-level/main.svelte | 13 +++++++++ .../samples/using-top-level/_config.js | 7 +++++ .../expected/client/index.svelte.js | 28 +++++++++++++++++++ .../expected/server/index.svelte.js | 28 +++++++++++++++++++ .../samples/using-top-level/index.svelte | 12 ++++++++ 14 files changed, 180 insertions(+), 2 deletions(-) create mode 100644 .changeset/nasty-hotels-clap.md create mode 100644 packages/svelte/src/internal/client/resource-management/index.js create mode 100644 packages/svelte/tests/runtime-runes/samples/using-top-level/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/using-top-level/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/using-top-level/main.svelte create mode 100644 packages/svelte/tests/snapshot/samples/using-top-level/_config.js create mode 100644 packages/svelte/tests/snapshot/samples/using-top-level/expected/client/index.svelte.js create mode 100644 packages/svelte/tests/snapshot/samples/using-top-level/expected/server/index.svelte.js create mode 100644 packages/svelte/tests/snapshot/samples/using-top-level/index.svelte diff --git a/.changeset/nasty-hotels-clap.md b/.changeset/nasty-hotels-clap.md new file mode 100644 index 000000000000..a94f2e15ca9f --- /dev/null +++ b/.changeset/nasty-hotels-clap.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: link top-level `using` declarations in components to lifecycle diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index fded183b86c3..23fab6cba917 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -472,7 +472,8 @@ export function analyze_component(root, source, options) { source, undefined_exports: new Map(), snippet_renderers: new Map(), - snippets: new Set() + snippets: new Set(), + disposable: [] }; if (!runes) { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index e2e006c14bec..0e0dc5560784 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -362,6 +362,10 @@ export function client_component(analysis, options) { .../** @type {ESTree.Statement[]} */ (template.body) ]); + if (analysis.disposable.length > 0) { + component_block.body.push(b.stmt(b.call('$.dispose', ...analysis.disposable))); + } + if (!analysis.runes) { // Bind static exports to props so that people can access them with bind:x for (const { name, alias } of analysis.exports) { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js index 53f18d42e43a..5d066b0df8fe 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js @@ -16,7 +16,7 @@ import { get_value } from './shared/declarations.js'; */ export function VariableDeclaration(node, context) { /** @type {VariableDeclarator[]} */ - const declarations = []; + let declarations = []; if (context.state.analysis.runes) { for (const declarator of node.declarations) { @@ -343,8 +343,27 @@ export function VariableDeclaration(node, context) { return b.empty; } + let kind = node.kind; + + // @ts-expect-error + if (kind === 'using' && context.state.is_instance && context.path.length === 1) { + context.state.analysis.disposable.push( + ...node.declarations.map((declarator) => /** @type {Identifier} */ (declarator.id)) + ); + + if (dev) { + declarations = declarations.map((declarator) => ({ + ...declarator, + init: b.call('$.disposable', /** @type {Expression} */ (declarator.init)) + })); + } + + kind = 'const'; + } + return { ...node, + kind, declarations }; } diff --git a/packages/svelte/src/compiler/phases/types.d.ts b/packages/svelte/src/compiler/phases/types.d.ts index 67cbd75ff86f..617c96f19446 100644 --- a/packages/svelte/src/compiler/phases/types.d.ts +++ b/packages/svelte/src/compiler/phases/types.d.ts @@ -100,6 +100,10 @@ export interface ComponentAnalysis extends Analysis { * Every snippet that is declared locally */ snippets: Set; + /** + * An array of any `using` declarations + */ + disposable: Identifier[]; } declare module 'estree' { diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 60f9af912060..7153bbc6fa33 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -118,6 +118,7 @@ export { update_pre_prop, update_prop } from './reactivity/props.js'; +export { dispose, disposable } from './resource-management/index.js'; export { invalidate_store, store_mutate, diff --git a/packages/svelte/src/internal/client/resource-management/index.js b/packages/svelte/src/internal/client/resource-management/index.js new file mode 100644 index 000000000000..b0874d515371 --- /dev/null +++ b/packages/svelte/src/internal/client/resource-management/index.js @@ -0,0 +1,26 @@ +import { teardown } from '../reactivity/effects.js'; + +/** + * @param {...any} disposables + */ +export function dispose(...disposables) { + teardown(() => { + for (const disposable of disposables) { + disposable?.[Symbol.dispose](); + } + }); +} + +/** + * In dev, check that a value used with `using` is in fact disposable. We need this + * because we're replacing `using foo = ...` with `const foo = ...` if the + * declaration is at the top level of a component + * @param {any} value + */ +export function disposable(value) { + if (value != null && !value[Symbol.dispose]) { + throw new TypeError('Symbol(Symbol.dispose) is not a function'); + } + + return value; +} diff --git a/packages/svelte/tests/runtime-runes/samples/using-top-level/Child.svelte b/packages/svelte/tests/runtime-runes/samples/using-top-level/Child.svelte new file mode 100644 index 000000000000..4598860358c0 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/using-top-level/Child.svelte @@ -0,0 +1,12 @@ + + +

{x.message}

diff --git a/packages/svelte/tests/runtime-runes/samples/using-top-level/_config.js b/packages/svelte/tests/runtime-runes/samples/using-top-level/_config.js new file mode 100644 index 000000000000..81eafa339b6f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/using-top-level/_config.js @@ -0,0 +1,18 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + // TODO unskip this for applicable node versions, once supported + skip: true, + + html: `

hello

`, + + test({ assert, target, logs }) { + const [button] = target.querySelectorAll('button'); + + flushSync(() => button.click()); + assert.htmlEqual(target.innerHTML, ``); + + assert.deepEqual(logs, ['disposing hello']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/using-top-level/main.svelte b/packages/svelte/tests/runtime-runes/samples/using-top-level/main.svelte new file mode 100644 index 000000000000..1afc3a5abd2d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/using-top-level/main.svelte @@ -0,0 +1,13 @@ + + + + +{#if message} + +{/if} diff --git a/packages/svelte/tests/snapshot/samples/using-top-level/_config.js b/packages/svelte/tests/snapshot/samples/using-top-level/_config.js new file mode 100644 index 000000000000..ed0ead960bdb --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/using-top-level/_config.js @@ -0,0 +1,7 @@ +import { test } from '../../test'; + +export default test({ + compileOptions: { + dev: true + } +}); diff --git a/packages/svelte/tests/snapshot/samples/using-top-level/expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/using-top-level/expected/client/index.svelte.js new file mode 100644 index 000000000000..4f635db261b3 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/using-top-level/expected/client/index.svelte.js @@ -0,0 +1,28 @@ +import 'svelte/internal/disclose-version'; + +Using_top_level[$.FILENAME] = 'packages/svelte/tests/snapshot/samples/using-top-level/index.svelte'; + +import * as $ from 'svelte/internal/client'; + +var root = $.add_locations($.from_html(`

`), Using_top_level[$.FILENAME], [[12, 0]]); + +export default function Using_top_level($$anchor, $$props) { + $.check_target(new.target); + $.push($$props, true, Using_top_level); + + const x = $.disposable({ + message: $$props.message, + [Symbol.dispose]() { + console.log(...$.log_if_contains_state('log', `disposing ${$$props.message}`)); + } + }); + + var p = root(); + var text = $.child(p, true); + + $.reset(p); + $.template_effect(() => $.set_text(text, x.message)); + $.append($$anchor, p); + $.dispose(x); + return $.pop({ ...$.legacy_api() }); +} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/using-top-level/expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/using-top-level/expected/server/index.svelte.js new file mode 100644 index 000000000000..6411f7431edc --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/using-top-level/expected/server/index.svelte.js @@ -0,0 +1,28 @@ +Using_top_level[$.FILENAME] = 'packages/svelte/tests/snapshot/samples/using-top-level/index.svelte'; + +import * as $ from 'svelte/internal/server'; + +function Using_top_level($$payload, $$props) { + $.push(Using_top_level); + + let { message } = $$props; + + using x = { + message, + [Symbol.dispose]() { + console.log(`disposing ${message}`); + } + }; + + $$payload.out += `

`; + $.push_element($$payload, 'p', 12, 0); + $$payload.out += `${$.escape(x.message)}

`; + $.pop_element(); + $.pop(); +} + +Using_top_level.render = function () { + throw new Error('Component.render(...) is no longer valid in Svelte 5. See https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes for more information'); +}; + +export default Using_top_level; \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/using-top-level/index.svelte b/packages/svelte/tests/snapshot/samples/using-top-level/index.svelte new file mode 100644 index 000000000000..4598860358c0 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/using-top-level/index.svelte @@ -0,0 +1,12 @@ + + +

{x.message}

From ef92395a770463f508f437b5bc718d61ce1d3f52 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 17 Jun 2025 11:52:43 -0400 Subject: [PATCH 5/8] oops --- .../{expected => _expected}/client/index.svelte.js | 0 .../{expected => _expected}/server/index.svelte.js | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename packages/svelte/tests/snapshot/samples/using-top-level/{expected => _expected}/client/index.svelte.js (100%) rename packages/svelte/tests/snapshot/samples/using-top-level/{expected => _expected}/server/index.svelte.js (100%) diff --git a/packages/svelte/tests/snapshot/samples/using-top-level/expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/using-top-level/_expected/client/index.svelte.js similarity index 100% rename from packages/svelte/tests/snapshot/samples/using-top-level/expected/client/index.svelte.js rename to packages/svelte/tests/snapshot/samples/using-top-level/_expected/client/index.svelte.js diff --git a/packages/svelte/tests/snapshot/samples/using-top-level/expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/using-top-level/_expected/server/index.svelte.js similarity index 100% rename from packages/svelte/tests/snapshot/samples/using-top-level/expected/server/index.svelte.js rename to packages/svelte/tests/snapshot/samples/using-top-level/_expected/server/index.svelte.js From 56a396ec1a36d4daf66531e43af987b5e74aa1f8 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 17 Jun 2025 11:56:40 -0400 Subject: [PATCH 6/8] ignore ts errors --- .../svelte/src/internal/client/resource-management/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/svelte/src/internal/client/resource-management/index.js b/packages/svelte/src/internal/client/resource-management/index.js index b0874d515371..6738d31de673 100644 --- a/packages/svelte/src/internal/client/resource-management/index.js +++ b/packages/svelte/src/internal/client/resource-management/index.js @@ -6,6 +6,7 @@ import { teardown } from '../reactivity/effects.js'; export function dispose(...disposables) { teardown(() => { for (const disposable of disposables) { + // @ts-ignore Symbol.dispose may or may not exist as far as TypeScript is concerned disposable?.[Symbol.dispose](); } }); @@ -18,6 +19,7 @@ export function dispose(...disposables) { * @param {any} value */ export function disposable(value) { + // @ts-ignore Symbol.dispose may or may not exist as far as TypeScript is concerned if (value != null && !value[Symbol.dispose]) { throw new TypeError('Symbol(Symbol.dispose) is not a function'); } From 97b78b37b852adfe532f26bfd293a049328c7be4 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 17 Jun 2025 12:31:27 -0400 Subject: [PATCH 7/8] wrap in try-catch --- .../3-transform/client/transform-client.js | 16 ++++++--- .../client/visitors/VariableDeclaration.js | 14 ++++---- .../svelte/src/compiler/utils/builders.js | 33 ++++++++++++++++++- 3 files changed, 50 insertions(+), 13 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index 0e0dc5560784..3805c15efda2 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -350,7 +350,7 @@ export function client_component(analysis, options) { const push_args = [b.id('$$props'), b.literal(analysis.runes)]; if (dev) push_args.push(b.id(analysis.name)); - const component_block = b.block([ + let component_block = b.block([ ...store_setup, ...legacy_reactive_declarations, ...group_binding_declarations, @@ -362,10 +362,6 @@ export function client_component(analysis, options) { .../** @type {ESTree.Statement[]} */ (template.body) ]); - if (analysis.disposable.length > 0) { - component_block.body.push(b.stmt(b.call('$.dispose', ...analysis.disposable))); - } - if (!analysis.runes) { // Bind static exports to props so that people can access them with bind:x for (const { name, alias } of analysis.exports) { @@ -495,6 +491,16 @@ export function client_component(analysis, options) { body = [...imports, ...state.module_level_snippets, ...body]; + if (analysis.disposable.length > 0) { + component_block = b.block([ + b.declaration( + 'var', + analysis.disposable.map((id) => b.declarator(id)) + ), + b.try(component_block.body, null, [b.stmt(b.call('$.dispose', ...analysis.disposable))]) + ]); + } + const component = b.function_declaration( b.id(analysis.name), should_inject_props ? [b.id('$$anchor'), b.id('$$props')] : [b.id('$$anchor')], diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js index 5d066b0df8fe..a8aea14d5610 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js @@ -351,14 +351,14 @@ export function VariableDeclaration(node, context) { ...node.declarations.map((declarator) => /** @type {Identifier} */ (declarator.id)) ); - if (dev) { - declarations = declarations.map((declarator) => ({ - ...declarator, - init: b.call('$.disposable', /** @type {Expression} */ (declarator.init)) - })); - } + const assignments = declarations.map((declarator) => { + let init = /** @type {Expression} */ (declarator.init); + if (dev) init = b.call('$.disposable', init); + + return b.assignment('=', declarator.id, init); + }); - kind = 'const'; + return assignments.length === 1 ? assignments[0] : b.sequence(assignments); } return { diff --git a/packages/svelte/src/compiler/utils/builders.js b/packages/svelte/src/compiler/utils/builders.js index 736738d19f15..22d2c2c9af78 100644 --- a/packages/svelte/src/compiler/utils/builders.js +++ b/packages/svelte/src/compiler/utils/builders.js @@ -635,6 +635,35 @@ export function throw_error(str) { }; } +/** + * @param {ESTree.Statement[]} body + * @param {ESTree.CatchClause | null} handler + * @param {ESTree.Statement[] | null} finalizer + * @returns {ESTree.TryStatement} + */ +function try_builder(body, handler, finalizer) { + return { + type: 'TryStatement', + block: block(body), + handler, + finalizer: finalizer && block(finalizer) + }; +} + +/** + * + * @param {ESTree.Pattern | null} param + * @param {ESTree.Statement[]} body + * @returns {ESTree.CatchClause} + */ +function catch_clause(param, body) { + return { + type: 'CatchClause', + param, + body: block(body) + }; +} + export { await_builder as await, let_builder as let, @@ -648,7 +677,9 @@ export { if_builder as if, this_instance as this, null_instance as null, - debugger_builder as debugger + debugger_builder as debugger, + try_builder as try, + catch_clause as catch }; /** From 8f5a073a20c846fa7eb6df765d859ac5bc3c04e1 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 17 Jun 2025 12:57:33 -0400 Subject: [PATCH 8/8] update test --- .../_expected/client/index.svelte.js | 40 +++++++++++-------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/packages/svelte/tests/snapshot/samples/using-top-level/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/using-top-level/_expected/client/index.svelte.js index 4f635db261b3..9ee6e8b5b294 100644 --- a/packages/svelte/tests/snapshot/samples/using-top-level/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/using-top-level/_expected/client/index.svelte.js @@ -8,21 +8,27 @@ var root = $.add_locations($.from_html(`

`), Using_top_level[$.FILENAME], export default function Using_top_level($$anchor, $$props) { $.check_target(new.target); - $.push($$props, true, Using_top_level); - - const x = $.disposable({ - message: $$props.message, - [Symbol.dispose]() { - console.log(...$.log_if_contains_state('log', `disposing ${$$props.message}`)); - } - }); - - var p = root(); - var text = $.child(p, true); - - $.reset(p); - $.template_effect(() => $.set_text(text, x.message)); - $.append($$anchor, p); - $.dispose(x); - return $.pop({ ...$.legacy_api() }); + + var x; + + try { + $.push($$props, true, Using_top_level); + + x = $.disposable({ + message: $$props.message, + [Symbol.dispose]() { + console.log(...$.log_if_contains_state('log', `disposing ${$$props.message}`)); + } + }) + + var p = root(); + var text = $.child(p, true); + + $.reset(p); + $.template_effect(() => $.set_text(text, x.message)); + $.append($$anchor, p); + return $.pop({ ...$.legacy_api() }); + } finally { + $.dispose(x); + } } \ No newline at end of file pFad - Phonifier reborn

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

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


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy