From 5836c1cdbd6257ff75738a424ee1929a16794a2a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 27 Nov 2023 13:15:04 -0500 Subject: [PATCH 1/4] chore: improve output for `` (#9648) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - doesn't add spread_dynamic_element_attributes when there are no attributes — Unnecessary spread_dynamic_element_attributes call #9646 - skips the child render function altogether if there is nothing to do --- .changeset/thin-foxes-lick.md | 5 ++++ .../3-transform/client/visitors/template.js | 13 ++++++++- packages/svelte/src/internal/client/render.js | 4 +-- .../samples/svelte-element/_config.js | 3 +++ .../_expected/client/index.svelte.js | 17 ++++++++++++ .../_expected/server/index.svelte.js | 27 +++++++++++++++++++ .../samples/svelte-element/index.svelte | 5 ++++ 7 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 .changeset/thin-foxes-lick.md create mode 100644 packages/svelte/tests/snapshot/samples/svelte-element/_config.js create mode 100644 packages/svelte/tests/snapshot/samples/svelte-element/_expected/client/index.svelte.js create mode 100644 packages/svelte/tests/snapshot/samples/svelte-element/_expected/server/index.svelte.js create mode 100644 packages/svelte/tests/snapshot/samples/svelte-element/index.svelte 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/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..9aef4f5107 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[]} */ @@ -2104,7 +2113,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/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/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 @@ + + + From 48e78e420ffb279a54c28407768f850c5ff81dfa Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 27 Nov 2023 13:19:07 -0500 Subject: [PATCH 2/4] chore: remove static value handling (#9571) The deleted code ensured that a static variable wouldn't update when it's in the same text expression as a reactive variable. We solved this through emitting a warning about this instead, marking it as undefined behavior. --- .changeset/witty-camels-warn.md | 5 +++ .../3-transform/client/visitors/template.js | 33 ++----------------- 2 files changed, 7 insertions(+), 31 deletions(-) create mode 100644 .changeset/witty-camels-warn.md 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/3-transform/client/visitors/template.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js index 9aef4f5107..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 @@ -1598,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)); } } From 2660727a93c98043b3fcb00f10bf01cc2e863143 Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Mon, 27 Nov 2023 10:31:50 -0800 Subject: [PATCH 3/4] chore: tweak changeset (#9667) * chore: tweak changeset * Update .changeset/slimy-clouds-talk.md * Update seven-ravens-check.md --- .changeset/seven-ravens-check.md | 2 +- .changeset/slimy-clouds-talk.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 From 81d3e47d1c9646a5676f79170fbffd97695c1a06 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Mon, 27 Nov 2023 18:34:22 +0000 Subject: [PATCH 4/4] feat: add $effect.root rune (#9638) * feat: effect-root-rune feat: add $effect.root rune update doc update doc fix validation * cleanup logic * Update sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md * address feedback --------- Co-authored-by: Rich Harris --- .changeset/rare-pears-whisper.md | 5 +++ .../compiler/phases/2-analyze/validation.js | 6 ++++ .../client/visitors/javascript-runes.js | 12 +++++-- .../svelte/src/compiler/phases/constants.js | 10 +++++- .../svelte/src/internal/client/runtime.js | 11 +++++++ 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 | 27 ++++++++++++++++ .../routes/docs/content/01-api/02-runes.md | 21 ++++++++++++ 10 files changed, 150 insertions(+), 5 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 0000000000..05dc333b31 --- /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/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 1aa78b14af..f3b0d2b908 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( @@ -208,7 +208,6 @@ export const javascript_visitors_runes = { // TODO continue; } - const args = /** @type {import('estree').CallExpression} */ (declarator.init).arguments; const value = args.length === 0 @@ -292,13 +291,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 0b06c5eda0..c646016f8d 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 db5cf7be30..9610515563 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -1184,6 +1184,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 be6459b7df..d179a3ec9e 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 f32f1863e9..214d89e4f3 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/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 7c3ee0953a..058a480d1a 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: