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/.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/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/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..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, @@ -491,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 53f18d42e43a..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 @@ -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)) + ); + + const assignments = declarations.map((declarator) => { + let init = /** @type {Expression} */ (declarator.init); + if (dev) init = b.call('$.disposable', init); + + return b.assignment('=', declarator.id, init); + }); + + return assignments.length === 1 ? assignments[0] : b.sequence(assignments); + } + 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/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 }; /** 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..6738d31de673 --- /dev/null +++ b/packages/svelte/src/internal/client/resource-management/index.js @@ -0,0 +1,28 @@ +import { teardown } from '../reactivity/effects.js'; + +/** + * @param {...any} disposables + */ +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](); + } + }); +} + +/** + * 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) { + // @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'); + } + + return value; +} 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" 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..9ee6e8b5b294 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/using-top-level/_expected/client/index.svelte.js @@ -0,0 +1,34 @@ +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); + + 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 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}

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 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