From d7367dc522ac90ab55887b361ec8cc76e3b8e819 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Fri, 24 Nov 2023 12:31:17 +0000 Subject: [PATCH 1/4] feat: effect-root-rune feat: add $effect.root rune update doc update doc fix validation --- .changeset/rare-pears-whisper.md | 5 +++ packages/svelte/src/compiler/errors.js | 2 + .../compiler/phases/2-analyze/validation.js | 8 ++++ .../client/visitors/javascript-runes.js | 21 +++++++++- .../svelte/src/compiler/phases/constants.js | 10 ++++- .../svelte/src/internal/client/runtime.js | 15 +++++++ packages/svelte/src/internal/index.js | 3 +- packages/svelte/src/main/ambient.d.ts | 28 +++++++++++++ .../samples/effect-root/_config.js | 32 +++++++++++++++ .../samples/effect-root/main.svelte | 25 ++++++++++++ .../routes/docs/content/01-api/02-runes.md | 40 +++++++++++++++++++ 11 files changed, 185 insertions(+), 4 deletions(-) create mode 100644 .changeset/rare-pears-whisper.md create mode 100644 packages/svelte/tests/runtime-runes/samples/effect-root/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/effect-root/main.svelte diff --git a/.changeset/rare-pears-whisper.md b/.changeset/rare-pears-whisper.md new file mode 100644 index 000000000000..05dc333b319b --- /dev/null +++ b/.changeset/rare-pears-whisper.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +feat: add $effect.root rune diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index 700ec21e19e0..a5b8f66b5786 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -176,6 +176,8 @@ const runes = { 'invalid-state-location': () => `$state() can only be used as a variable declaration initializer or a class field`, 'invalid-effect-location': () => `$effect() can only be used as an expression statement`, + 'invalid-effect-root-location': () => + `$effect.root() can only be used as a variable declaration initializer`, /** * @param {boolean} is_binding * @param {boolean} show_details diff --git a/packages/svelte/src/compiler/phases/2-analyze/validation.js b/packages/svelte/src/compiler/phases/2-analyze/validation.js index e799c38851a7..c823f50c36e2 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/validation.js +++ b/packages/svelte/src/compiler/phases/2-analyze/validation.js @@ -519,6 +519,14 @@ function validate_call_expression(node, scope, path) { error(node, 'invalid-rune-args-length', '$effect.active', [0]); } } + + if (rune === '$effect.root') { + if (node.arguments.length < 1 || node.arguments.length > 2) { + error(node, 'invalid-rune-args-length', '$effect.root', [1, 2]); + } + if (parent.type === 'VariableDeclarator') return; + error(node, 'invalid-effect-root-location'); + } } /** diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js index 1aa78b14af20..34f8c5e57978 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js @@ -208,8 +208,18 @@ export const javascript_visitors_runes = { // TODO continue; } - const args = /** @type {import('estree').CallExpression} */ (declarator.init).arguments; + + if (rune === '$effect.root') { + const serialized_args = /** @type {import('estree').Expression[]} */ ( + args.map((arg) => visit(arg)) + ); + declarations.push( + b.declarator(declarator.id, b.call('$.user_root_effect', ...serialized_args)) + ); + continue; + } + const value = args.length === 0 ? b.id('undefined') @@ -292,13 +302,20 @@ export const javascript_visitors_runes = { context.next(); }, - CallExpression(node, { state, next }) { + CallExpression(node, { state, next, visit }) { const rune = get_rune(node, state.scope); if (rune === '$effect.active') { return b.call('$.effect_active'); } + if (rune === '$effect.root') { + const args = /** @type {import('estree').Expression[]} */ ( + node.arguments.map((arg) => visit(arg)) + ); + return b.call('$.user_root_effect', ...args); + } + next(); } }; diff --git a/packages/svelte/src/compiler/phases/constants.js b/packages/svelte/src/compiler/phases/constants.js index 0b06c5eda09c..c646016f8d6e 100644 --- a/packages/svelte/src/compiler/phases/constants.js +++ b/packages/svelte/src/compiler/phases/constants.js @@ -70,7 +70,15 @@ export const ElementBindings = [ 'indeterminate' ]; -export const Runes = ['$state', '$props', '$derived', '$effect', '$effect.pre', '$effect.active']; +export const Runes = [ + '$state', + '$props', + '$derived', + '$effect', + '$effect.pre', + '$effect.active', + '$effect.root' +]; /** * Whitespace inside one of these elements will not result in diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index db5cf7be30e3..dc567a4ca99e 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -1184,6 +1184,21 @@ export function user_effect(init) { return effect; } +/** + * @param {() => void | (() => void)} init + * @param {() => void} [on_parent_cleanup] + * @returns {() => void} + */ +export function user_root_effect(init, on_parent_cleanup) { + const effect = managed_render_effect(init); + if (current_effect !== null && typeof on_parent_cleanup === 'function') { + push_destroy_fn(current_effect, on_parent_cleanup); + } + return () => { + destroy_signal(effect); + }; +} + /** * @param {() => void | (() => void)} init * @returns {import('./types.js').EffectSignal} diff --git a/packages/svelte/src/internal/index.js b/packages/svelte/src/internal/index.js index be6459b7dff6..d179a3ec9e9f 100644 --- a/packages/svelte/src/internal/index.js +++ b/packages/svelte/src/internal/index.js @@ -36,7 +36,8 @@ export { pop, push, reactive_import, - effect_active + effect_active, + user_root_effect } from './client/runtime.js'; export * from './client/validate.js'; diff --git a/packages/svelte/src/main/ambient.d.ts b/packages/svelte/src/main/ambient.d.ts index f32f1863e94a..7d1bb3e109bd 100644 --- a/packages/svelte/src/main/ambient.d.ts +++ b/packages/svelte/src/main/ambient.d.ts @@ -90,6 +90,34 @@ declare namespace $effect { * https://svelte-5-preview.vercel.app/docs/runes#$effect-active */ export function active(): boolean; + + /** + * The `$effect.root` rune is an advanced feature that creates a non-tracked scope that doesn't auto-cleanup. This is useful for + * nested effects that you want to manually control. This rune also allows for creation of effects outside of the component + * initialisation phase. + * + * Example: + * ```svelte + * + * + * + * ``` + * + * https://svelte-5-preview.vercel.app/docs/runes#$effect-root + */ + export function root(fn: () => void | (() => void), fn2?: () => void): () => void; } /** diff --git a/packages/svelte/tests/runtime-runes/samples/effect-root/_config.js b/packages/svelte/tests/runtime-runes/samples/effect-root/_config.js new file mode 100644 index 000000000000..b5e2a1a8086f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/effect-root/_config.js @@ -0,0 +1,32 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + get props() { + return { log: [] }; + }, + + async test({ assert, target, component }) { + const [b1, b2, b3] = target.querySelectorAll('button'); + + flushSync(() => { + b1.click(); + b2.click(); + }); + + assert.deepEqual(component.log, [0, 1]); + + flushSync(() => { + b3.click(); + }); + + assert.deepEqual(component.log, [0, 1, 'cleanup 1', 'cleanup 2']); + + flushSync(() => { + b1.click(); + b2.click(); + }); + + assert.deepEqual(component.log, [0, 1, 'cleanup 1', 'cleanup 2']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/effect-root/main.svelte b/packages/svelte/tests/runtime-runes/samples/effect-root/main.svelte new file mode 100644 index 000000000000..eaefccacb8bc --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/effect-root/main.svelte @@ -0,0 +1,25 @@ + + + + + diff --git a/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md b/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md index 7c3ee0953aef..a2de65b83989 100644 --- a/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md +++ b/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md @@ -186,6 +186,46 @@ The `$effect.active` rune is an advanced feature that tells you whether or not t This allows you to (for example) add things like subscriptions without causing memory leaks, by putting them in child effects. +## `$effect.root` + +The `$effect.root` rune is an advanced feature that creates a non-tracked scope that doesn't auto-cleanup. This is useful for +nested effects that you want to manually control. This rune also allows for creation of effects outside of the component initialisation phase. + +> `$effect.root` can only be used in variable declaration initializer, this is to ensure the return signature (the cleanup function) is always used. + +```svelte + +``` + +If the `$effect.root` was created within within another active effect (such as during component initialisation) then it might +be desirable to know when that active effect gets disposed and cleaned up. `$effect.root` takes an optional second arugment, +which is a function callback for when this happens in case. This allows you to cleanup the effect root too if needed. + +```svelte + +``` + ## `$props` To declare component props, use the `$props` rune: From e13b35e069da972ce6ca0bdb615dec9428b93ce8 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Sat, 25 Nov 2023 00:41:04 +0000 Subject: [PATCH 2/4] cleanup logic --- packages/svelte/src/compiler/errors.js | 2 -- .../src/compiler/phases/2-analyze/validation.js | 2 -- .../3-transform/client/visitors/javascript-runes.js | 13 +------------ .../src/routes/docs/content/01-api/02-runes.md | 2 -- 4 files changed, 1 insertion(+), 18 deletions(-) diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index a5b8f66b5786..700ec21e19e0 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -176,8 +176,6 @@ const runes = { 'invalid-state-location': () => `$state() can only be used as a variable declaration initializer or a class field`, 'invalid-effect-location': () => `$effect() can only be used as an expression statement`, - 'invalid-effect-root-location': () => - `$effect.root() can only be used as a variable declaration initializer`, /** * @param {boolean} is_binding * @param {boolean} show_details diff --git a/packages/svelte/src/compiler/phases/2-analyze/validation.js b/packages/svelte/src/compiler/phases/2-analyze/validation.js index c823f50c36e2..c95fb5894d8c 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/validation.js +++ b/packages/svelte/src/compiler/phases/2-analyze/validation.js @@ -524,8 +524,6 @@ function validate_call_expression(node, scope, path) { if (node.arguments.length < 1 || node.arguments.length > 2) { error(node, 'invalid-rune-args-length', '$effect.root', [1, 2]); } - if (parent.type === 'VariableDeclarator') return; - error(node, 'invalid-effect-root-location'); } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js index 34f8c5e57978..f3b0d2b90842 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js @@ -135,7 +135,7 @@ export const javascript_visitors_runes = { for (const declarator of node.declarations) { const init = declarator.init; const rune = get_rune(init, state.scope); - if (!rune || rune === '$effect.active') { + if (!rune || rune === '$effect.active' || rune === '$effect.root') { if (init != null && is_hoistable_function(init)) { const hoistable_function = visit(init); state.hoisted.push( @@ -209,17 +209,6 @@ export const javascript_visitors_runes = { continue; } const args = /** @type {import('estree').CallExpression} */ (declarator.init).arguments; - - if (rune === '$effect.root') { - const serialized_args = /** @type {import('estree').Expression[]} */ ( - args.map((arg) => visit(arg)) - ); - declarations.push( - b.declarator(declarator.id, b.call('$.user_root_effect', ...serialized_args)) - ); - continue; - } - const value = args.length === 0 ? b.id('undefined') diff --git a/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md b/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md index a2de65b83989..78095c4cb00e 100644 --- a/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md +++ b/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md @@ -191,8 +191,6 @@ This allows you to (for example) add things like subscriptions without causing m The `$effect.root` rune is an advanced feature that creates a non-tracked scope that doesn't auto-cleanup. This is useful for nested effects that you want to manually control. This rune also allows for creation of effects outside of the component initialisation phase. -> `$effect.root` can only be used in variable declaration initializer, this is to ensure the return signature (the cleanup function) is always used. - ```svelte diff --git a/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md b/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md index b76537a159cc..058a480d1ac7 100644 --- a/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md +++ b/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md @@ -207,23 +207,6 @@ nested effects that you want to manually control. This rune also allows for crea ``` -If the `$effect.root` was created within within another active effect (such as during component initialisation) then it might -be desirable to know when that active effect gets disposed and cleaned up. `$effect.root` takes an optional second arugment, -which is a function callback for when this happens in case. This allows you to cleanup the effect root too if needed. - -```svelte - -``` - ## `$props` To declare component props, use the `$props` rune: