pull/16192/merge
Rich Harris 3 months ago committed by GitHub
commit e214481bd8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: support `using` keyword

@ -0,0 +1,5 @@
---
'svelte': minor
---
feat: link top-level `using` declarations in components to lifecycle

@ -164,9 +164,9 @@
"dependencies": { "dependencies": {
"@ampproject/remapping": "^2.3.0", "@ampproject/remapping": "^2.3.0",
"@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/sourcemap-codec": "^1.5.0",
"@types/estree": "^1.0.5",
"acorn": "^8.12.1",
"@sveltejs/acorn-typescript": "^1.0.5", "@sveltejs/acorn-typescript": "^1.0.5",
"@types/estree": "^1.0.5",
"acorn": "^8.15.0",
"aria-query": "^5.3.1", "aria-query": "^5.3.1",
"axobject-query": "^4.1.0", "axobject-query": "^4.1.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",

@ -36,7 +36,7 @@ export function parse(source, typescript, is_script) {
ast = parser.parse(source, { ast = parser.parse(source, {
onComment, onComment,
sourceType: 'module', sourceType: 'module',
ecmaVersion: 16, ecmaVersion: 'latest',
locations: true locations: true
}); });
} finally { } finally {
@ -64,7 +64,7 @@ export function parse_expression_at(source, typescript, index) {
const ast = parser.parseExpressionAt(source, index, { const ast = parser.parseExpressionAt(source, index, {
onComment, onComment,
sourceType: 'module', sourceType: 'module',
ecmaVersion: 16, ecmaVersion: 'latest',
locations: true locations: true
}); });

@ -495,7 +495,8 @@ export function analyze_component(root, source, options) {
source, source,
undefined_exports: new Map(), undefined_exports: new Map(),
snippet_renderers: new Map(), snippet_renderers: new Map(),
snippets: new Set() snippets: new Set(),
disposable: []
}; };
if (!runes) { if (!runes) {

@ -350,7 +350,7 @@ export function client_component(analysis, options) {
const push_args = [b.id('$$props'), b.literal(analysis.runes)]; const push_args = [b.id('$$props'), b.literal(analysis.runes)];
if (dev) push_args.push(b.id(analysis.name)); if (dev) push_args.push(b.id(analysis.name));
const component_block = b.block([ let component_block = b.block([
...store_setup, ...store_setup,
...legacy_reactive_declarations, ...legacy_reactive_declarations,
...group_binding_declarations, ...group_binding_declarations,
@ -491,6 +491,16 @@ export function client_component(analysis, options) {
body = [...imports, ...state.module_level_snippets, ...body]; 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( const component = b.function_declaration(
b.id(analysis.name), b.id(analysis.name),
should_inject_props ? [b.id('$$anchor'), b.id('$$props')] : [b.id('$$anchor')], should_inject_props ? [b.id('$$anchor'), b.id('$$props')] : [b.id('$$anchor')],

@ -16,7 +16,7 @@ import { get_value } from './shared/declarations.js';
*/ */
export function VariableDeclaration(node, context) { export function VariableDeclaration(node, context) {
/** @type {VariableDeclarator[]} */ /** @type {VariableDeclarator[]} */
const declarations = []; let declarations = [];
if (context.state.analysis.runes) { if (context.state.analysis.runes) {
for (const declarator of node.declarations) { for (const declarator of node.declarations) {
@ -343,8 +343,27 @@ export function VariableDeclaration(node, context) {
return b.empty; 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 { return {
...node, ...node,
kind,
declarations declarations
}; };
} }

@ -101,6 +101,10 @@ export interface ComponentAnalysis extends Analysis {
* Every snippet that is declared locally * Every snippet that is declared locally
*/ */
snippets: Set<AST.SnippetBlock>; snippets: Set<AST.SnippetBlock>;
/**
* An array of any `using` declarations
*/
disposable: Identifier[];
} }
declare module 'estree' { declare module 'estree' {

@ -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 { export {
await_builder as await, await_builder as await,
let_builder as let, let_builder as let,
@ -648,7 +677,9 @@ export {
if_builder as if, if_builder as if,
this_instance as this, this_instance as this,
null_instance as null, null_instance as null,
debugger_builder as debugger debugger_builder as debugger,
try_builder as try,
catch_clause as catch
}; };
/** /**

@ -118,6 +118,7 @@ export {
update_pre_prop, update_pre_prop,
update_prop update_prop
} from './reactivity/props.js'; } from './reactivity/props.js';
export { dispose, disposable } from './resource-management/index.js';
export { export {
invalidate_store, invalidate_store,
store_mutate, store_mutate,

@ -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;
}

@ -259,7 +259,8 @@
"kind": "let" "kind": "let"
}, },
"specifiers": [], "specifiers": [],
"source": null "source": null,
"attributes": []
} }
], ],
"sourceType": "module" "sourceType": "module"

@ -169,7 +169,8 @@
"kind": "const" "kind": "const"
}, },
"specifiers": [], "specifiers": [],
"source": null "source": null,
"attributes": []
} }
], ],
"sourceType": "module" "sourceType": "module"

@ -0,0 +1,12 @@
<script>
let { message } = $props();
using x = {
message,
[Symbol.dispose]() {
console.log(`disposing ${message}`);
}
}
</script>
<p>{x.message}</p>

@ -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: `<button>toggle</button><p>hello</p>`,
test({ assert, target, logs }) {
const [button] = target.querySelectorAll('button');
flushSync(() => button.click());
assert.htmlEqual(target.innerHTML, `<button>toggle</button>`);
assert.deepEqual(logs, ['disposing hello']);
}
});

@ -0,0 +1,13 @@
<script>
import Child from './Child.svelte';
let message = $state('hello');
</script>
<button onclick={() => message = message ? null : 'hello'}>
toggle
</button>
{#if message}
<Child {message} />
{/if}

@ -0,0 +1,7 @@
import { test } from '../../test';
export default test({
compileOptions: {
dev: true
}
});

@ -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(`<p> </p>`), 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);
}
}

@ -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 += `<p>`;
$.push_element($$payload, 'p', 12, 0);
$$payload.out += `${$.escape(x.message)}</p>`;
$.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;

@ -0,0 +1,12 @@
<script>
let { message } = $props();
using x = {
message,
[Symbol.dispose]() {
console.log(`disposing ${message}`);
}
}
</script>
<p>{x.message}</p>

@ -67,13 +67,13 @@ importers:
version: 1.5.0 version: 1.5.0
'@sveltejs/acorn-typescript': '@sveltejs/acorn-typescript':
specifier: ^1.0.5 specifier: ^1.0.5
version: 1.0.5(acorn@8.14.0) version: 1.0.5(acorn@8.15.0)
'@types/estree': '@types/estree':
specifier: ^1.0.5 specifier: ^1.0.5
version: 1.0.6 version: 1.0.6
acorn: acorn:
specifier: ^8.12.1 specifier: ^8.15.0
version: 8.14.0 version: 8.15.0
aria-query: aria-query:
specifier: ^5.3.1 specifier: ^5.3.1
version: 5.3.1 version: 5.3.1
@ -908,13 +908,13 @@ packages:
peerDependencies: peerDependencies:
acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
acorn@8.14.0: acorn@8.14.1:
resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==}
engines: {node: '>=0.4.0'} engines: {node: '>=0.4.0'}
hasBin: true hasBin: true
acorn@8.14.1: acorn@8.15.0:
resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
engines: {node: '>=0.4.0'} engines: {node: '>=0.4.0'}
hasBin: true hasBin: true
@ -2990,9 +2990,9 @@ snapshots:
eslint-visitor-keys: 3.4.3 eslint-visitor-keys: 3.4.3
espree: 9.6.1 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: 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)': '@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: dependencies:
@ -3231,18 +3231,14 @@ snapshots:
loupe: 3.1.3 loupe: 3.1.3
tinyrainbow: 1.2.0 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): acorn-jsx@5.3.2(acorn@8.14.1):
dependencies: dependencies:
acorn: 8.14.1 acorn: 8.14.1
acorn@8.14.0: {}
acorn@8.14.1: {} acorn@8.14.1: {}
acorn@8.15.0: {}
agent-base@7.1.1: agent-base@7.1.1:
dependencies: dependencies:
debug: 4.4.0 debug: 4.4.0
@ -3606,8 +3602,8 @@ snapshots:
espree@10.1.0: espree@10.1.0:
dependencies: dependencies:
acorn: 8.14.0 acorn: 8.14.1
acorn-jsx: 5.3.2(acorn@8.14.0) acorn-jsx: 5.3.2(acorn@8.14.1)
eslint-visitor-keys: 4.2.0 eslint-visitor-keys: 4.2.0
espree@9.6.1: espree@9.6.1:
@ -4457,7 +4453,7 @@ snapshots:
terser@5.27.0: terser@5.27.0:
dependencies: dependencies:
'@jridgewell/source-map': 0.3.6 '@jridgewell/source-map': 0.3.6
acorn: 8.14.0 acorn: 8.15.0
commander: 2.20.3 commander: 2.20.3
source-map-support: 0.5.21 source-map-support: 0.5.21

Loading…
Cancel
Save