diff --git a/.changeset/rare-pears-whisper.md b/.changeset/rare-pears-whisper.md new file mode 100644 index 0000000000..05dc333b31 --- /dev/null +++ b/.changeset/rare-pears-whisper.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +feat: add $effect.root rune diff --git a/.changeset/seven-ravens-check.md b/.changeset/seven-ravens-check.md index 26063b2313..0bbfad1b22 100644 --- a/.changeset/seven-ravens-check.md +++ b/.changeset/seven-ravens-check.md @@ -1,5 +1,5 @@ --- -'svelte': minor +'svelte': patch --- feat: support type definition in {@const} diff --git a/.changeset/slimy-clouds-talk.md b/.changeset/slimy-clouds-talk.md index f92019e7d3..6ff1219045 100644 --- a/.changeset/slimy-clouds-talk.md +++ b/.changeset/slimy-clouds-talk.md @@ -2,4 +2,4 @@ 'svelte': patch --- -feat: ignore href attributes when hydrating +feat: ignore `src`, `srcset`, and `href` attributes when hydrating diff --git a/.changeset/thin-foxes-lick.md b/.changeset/thin-foxes-lick.md new file mode 100644 index 0000000000..904c84cc5c --- /dev/null +++ b/.changeset/thin-foxes-lick.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: improve `` generated code diff --git a/.changeset/witty-camels-warn.md b/.changeset/witty-camels-warn.md new file mode 100644 index 0000000000..cdcccd6ac6 --- /dev/null +++ b/.changeset/witty-camels-warn.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: prevent some unused variable creation diff --git a/packages/svelte/src/compiler/phases/2-analyze/validation.js b/packages/svelte/src/compiler/phases/2-analyze/validation.js index e799c38851..e871808c2c 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/validation.js +++ b/packages/svelte/src/compiler/phases/2-analyze/validation.js @@ -519,6 +519,12 @@ 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) { + error(node, 'invalid-rune-args-length', '$effect.root', [1]); + } + } } /** 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 579e771618..802c2beb96 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,12 @@ 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' || rune.startsWith('$log')) { + if ( + !rune || + rune === '$effect.active' || + rune === '$effect.root' || + rune.startsWith('$log') + ) { if (init != null && is_hoistable_function(init)) { const hoistable_function = visit(init); state.hoisted.push( @@ -208,7 +213,6 @@ export const javascript_visitors_runes = { // TODO continue; } - const args = /** @type {import('estree').CallExpression} */ (declarator.init).arguments; const value = args.length === 0 @@ -336,6 +340,13 @@ export const javascript_visitors_runes = { return b.unary('void', b.literal(0)); } + 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/3-transform/client/visitors/template.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js index 9dd6775a6a..855a795284 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js @@ -347,6 +347,15 @@ function serialize_element_spread_attributes(attributes, context, element, eleme * @returns {boolean} */ function serialize_dynamic_element_spread_attributes(attributes, context, element_id) { + if (attributes.length === 0) { + if (context.state.analysis.stylesheet.id) { + context.state.init.push( + b.stmt(b.call('$.class_name', element_id, b.literal(context.state.analysis.stylesheet.id))) + ); + } + return false; + } + let is_reactive = false; /** @type {import('estree').Expression[]} */ @@ -1589,37 +1598,8 @@ function serialize_template_literal(values, visit, state) { if (node.type === 'ExpressionTag' && node.metadata.contains_call_expression) { contains_call_expression = true; } - let expression = visit(node.expression); - if (node.expression.type === 'Identifier') { - const name = node.expression.name; - const binding = scope.get(name); - // When we combine expressions as part of a single template element, we might - // be referencing variables that can be mutated, but are not actually state. - // In order to prevent this undesired behavior, we need ensure we cache the - // latest value we have of that variable before we process the template, enforcing - // the value remains static through the lifetime of the template. - if (binding !== null && binding.kind === 'normal' && binding.mutated) { - let has_already_cached = false; - // Check if we already create a const of this expression - for (let node of state.init) { - if ( - node.type === 'VariableDeclaration' && - node.declarations[0].id.type === 'Identifier' && - node.declarations[0].id.name === name + '_const' - ) { - has_already_cached = true; - expression = b.id(name + '_const'); - break; - } - } - if (!has_already_cached) { - const tmp_id = scope.generate(name + '_const'); - state.init.push(b.const(tmp_id, expression)); - expression = b.id(tmp_id); - } - } - } - expressions.push(b.call('$.stringify', expression)); + + expressions.push(b.call('$.stringify', visit(node.expression))); quasis.push(b.quasi('', i + 1 === values.length)); } } @@ -2104,7 +2084,9 @@ export const template_visitors = { '$.element', context.state.node, get_tag, - b.arrow([element_id, b.id('$$anchor')], b.block(inner)), + inner.length === 0 + ? /** @type {any} */ (undefined) + : b.arrow([element_id, b.id('$$anchor')], b.block(inner)), namespace === 'http://www.w3.org/2000/svg' ? b.literal(true) : /** @type {any} */ (undefined) diff --git a/packages/svelte/src/compiler/phases/constants.js b/packages/svelte/src/compiler/phases/constants.js index 649205938d..794ae7b252 100644 --- a/packages/svelte/src/compiler/phases/constants.js +++ b/packages/svelte/src/compiler/phases/constants.js @@ -77,6 +77,7 @@ export const Runes = [ '$effect', '$effect.pre', '$effect.active', + '$effect.root', '$log', '$log.break', '$log.trace', diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index 4445474441..de46586d36 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -1542,7 +1542,7 @@ function swap_block_dom(block, from, to) { /** * @param {Comment} anchor_node * @param {() => string} tag_fn - * @param {null | ((element: Element, anchor: Node) => void)} render_fn + * @param {undefined | ((element: Element, anchor: Node) => void)} render_fn * @param {any} is_svg * @returns {void} */ @@ -1582,7 +1582,7 @@ export function element(anchor_node, tag_fn, render_fn, is_svg = false) { block.d = null; } element = next_element; - if (element !== null && render_fn !== null) { + if (element !== null && render_fn !== undefined) { let anchor; if (current_hydration_fragment !== null) { // Use the existing ssr comment as the anchor so that the inner open and close diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 28590fbe3d..93582d37fe 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -1250,6 +1250,17 @@ export function user_effect(init) { return effect; } +/** + * @param {() => void | (() => void)} init + * @returns {() => void} + */ +export function user_root_effect(init) { + const effect = managed_render_effect(init); + 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 8b85a7c77a..a840853e8b 100644 --- a/packages/svelte/src/internal/index.js +++ b/packages/svelte/src/internal/index.js @@ -37,6 +37,7 @@ export { push, reactive_import, effect_active, + user_root_effect, log, log_trace, log_break, diff --git a/packages/svelte/src/main/ambient.d.ts b/packages/svelte/src/main/ambient.d.ts index 21c2358028..6b7a17d1f4 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)): () => 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 0000000000..b5e2a1a808 --- /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 0000000000..d646bea2c4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/effect-root/main.svelte @@ -0,0 +1,27 @@ + + + + + diff --git a/packages/svelte/tests/snapshot/samples/svelte-element/_config.js b/packages/svelte/tests/snapshot/samples/svelte-element/_config.js new file mode 100644 index 0000000000..f47bee71df --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/svelte-element/_config.js @@ -0,0 +1,3 @@ +import { test } from '../../test'; + +export default test({}); diff --git a/packages/svelte/tests/snapshot/samples/svelte-element/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/svelte-element/_expected/client/index.svelte.js new file mode 100644 index 0000000000..263b00e526 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/svelte-element/_expected/client/index.svelte.js @@ -0,0 +1,17 @@ +// index.svelte (Svelte VERSION) +// Note: compiler output will change before 5.0 is released! +import "svelte/internal/disclose-version"; +import * as $ from "svelte/internal"; + +export default function Svelte_element($$anchor, $$props) { + $.push($$props, true); + + let tag = $.prop_source($$props, "tag", 'hr', false); + /* Init */ + var fragment = $.comment($$anchor); + var node = $.child_frag(fragment); + + $.element(node, () => $.get(tag)); + $.close_frag($$anchor, fragment); + $.pop(); +} diff --git a/packages/svelte/tests/snapshot/samples/svelte-element/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/svelte-element/_expected/server/index.svelte.js new file mode 100644 index 0000000000..6c4e6ee8b0 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/svelte-element/_expected/server/index.svelte.js @@ -0,0 +1,27 @@ +// index.svelte (Svelte VERSION) +// Note: compiler output will change before 5.0 is released! +import * as $ from "svelte/internal/server"; + +export default function Svelte_element($$payload, $$props) { + $.push(true); + + let { tag = 'hr' } = $$props; + const anchor = $.create_anchor($$payload); + + $$payload.out += `${anchor}`; + + if (tag) { + const anchor_1 = $.create_anchor($$payload); + + $$payload.out += `<${tag}>`; + + if (!$.VoidElements.has(tag)) { + $$payload.out += `${anchor_1}`; + $$payload.out += `${anchor_1}`; + } + } + + $$payload.out += `${anchor}`; + $.bind_props($$props, { tag }); + $.pop(); +} diff --git a/packages/svelte/tests/snapshot/samples/svelte-element/index.svelte b/packages/svelte/tests/snapshot/samples/svelte-element/index.svelte new file mode 100644 index 0000000000..cd543173a1 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/svelte-element/index.svelte @@ -0,0 +1,5 @@ + + + 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 8f2568c71b..84b71e7d6e 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,27 @@ 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. + +```svelte + +``` + ## `$props` To declare component props, use the `$props` rune: