feat: link top-level `using` declarations in components to lifecycle

using-dispose
Rich Harris 3 months ago
parent 0fd39219d3
commit 8bb4083b67

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

@ -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) {

@ -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) {

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

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

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

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

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

@ -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>
Loading…
Cancel
Save