Skip to content

Commit 8bb4083

Browse files
committed
feat: link top-level using declarations in components to lifecycle
1 parent 0fd3921 commit 8bb4083

File tree

14 files changed

+180
-2
lines changed

14 files changed

+180
-2
lines changed

.changeset/nasty-hotels-clap.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': minor
3+
---
4+
5+
feat: link top-level `using` declarations in components to lifecycle

packages/svelte/src/compiler/phases/2-analyze/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -472,7 +472,8 @@ export function analyze_component(root, source, options) {
472472
source,
473473
undefined_exports: new Map(),
474474
snippet_renderers: new Map(),
475-
snippets: new Set()
475+
snippets: new Set(),
476+
disposable: []
476477
};
477478

478479
if (!runes) {

packages/svelte/src/compiler/phases/3-transform/client/transform-client.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,10 @@ export function client_component(analysis, options) {
362362
.../** @type {ESTree.Statement[]} */ (template.body)
363363
]);
364364

365+
if (analysis.disposable.length > 0) {
366+
component_block.body.push(b.stmt(b.call('$.dispose', ...analysis.disposable)));
367+
}
368+
365369
if (!analysis.runes) {
366370
// Bind static exports to props so that people can access them with bind:x
367371
for (const { name, alias } of analysis.exports) {

packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { get_value } from './shared/declarations.js';
1616
*/
1717
export function VariableDeclaration(node, context) {
1818
/** @type {VariableDeclarator[]} */
19-
const declarations = [];
19+
let declarations = [];
2020

2121
if (context.state.analysis.runes) {
2222
for (const declarator of node.declarations) {
@@ -343,8 +343,27 @@ export function VariableDeclaration(node, context) {
343343
return b.empty;
344344
}
345345

346+
let kind = node.kind;
347+
348+
// @ts-expect-error
349+
if (kind === 'using' && context.state.is_instance && context.path.length === 1) {
350+
context.state.analysis.disposable.push(
351+
...node.declarations.map((declarator) => /** @type {Identifier} */ (declarator.id))
352+
);
353+
354+
if (dev) {
355+
declarations = declarations.map((declarator) => ({
356+
...declarator,
357+
init: b.call('$.disposable', /** @type {Expression} */ (declarator.init))
358+
}));
359+
}
360+
361+
kind = 'const';
362+
}
363+
346364
return {
347365
...node,
366+
kind,
348367
declarations
349368
};
350369
}

packages/svelte/src/compiler/phases/types.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,10 @@ export interface ComponentAnalysis extends Analysis {
100100
* Every snippet that is declared locally
101101
*/
102102
snippets: Set<AST.SnippetBlock>;
103+
/**
104+
* An array of any `using` declarations
105+
*/
106+
disposable: Identifier[];
103107
}
104108

105109
declare module 'estree' {

packages/svelte/src/internal/client/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ export {
118118
update_pre_prop,
119119
update_prop
120120
} from './reactivity/props.js';
121+
export { dispose, disposable } from './resource-management/index.js';
121122
export {
122123
invalidate_store,
123124
store_mutate,
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { teardown } from '../reactivity/effects.js';
2+
3+
/**
4+
* @param {...any} disposables
5+
*/
6+
export function dispose(...disposables) {
7+
teardown(() => {
8+
for (const disposable of disposables) {
9+
disposable?.[Symbol.dispose]();
10+
}
11+
});
12+
}
13+
14+
/**
15+
* In dev, check that a value used with `using` is in fact disposable. We need this
16+
* because we're replacing `using foo = ...` with `const foo = ...` if the
17+
* declaration is at the top level of a component
18+
* @param {any} value
19+
*/
20+
export function disposable(value) {
21+
if (value != null && !value[Symbol.dispose]) {
22+
throw new TypeError('Symbol(Symbol.dispose) is not a function');
23+
}
24+
25+
return value;
26+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<script>
2+
let { message } = $props();
3+
4+
using x = {
5+
message,
6+
[Symbol.dispose]() {
7+
console.log(`disposing ${message}`);
8+
}
9+
}
10+
</script>
11+
12+
<p>{x.message}</p>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { flushSync } from 'svelte';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
// TODO unskip this for applicable node versions, once supported
6+
skip: true,
7+
8+
html: `<button>toggle</button><p>hello</p>`,
9+
10+
test({ assert, target, logs }) {
11+
const [button] = target.querySelectorAll('button');
12+
13+
flushSync(() => button.click());
14+
assert.htmlEqual(target.innerHTML, `<button>toggle</button>`);
15+
16+
assert.deepEqual(logs, ['disposing hello']);
17+
}
18+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<script>
2+
import Child from './Child.svelte';
3+
4+
let message = $state('hello');
5+
</script>
6+
7+
<button onclick={() => message = message ? null : 'hello'}>
8+
toggle
9+
</button>
10+
11+
{#if message}
12+
<Child {message} />
13+
{/if}

0 commit comments

Comments
 (0)