diff --git a/.changeset/slow-bikes-serve.md b/.changeset/slow-bikes-serve.md new file mode 100644 index 0000000000..5e1d654353 --- /dev/null +++ b/.changeset/slow-bikes-serve.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: inline primitive constants in attribute values during SSR diff --git a/documentation/docs/06-runtime/02-context.md b/documentation/docs/06-runtime/02-context.md index aee51c9a78..439272df8f 100644 --- a/documentation/docs/06-runtime/02-context.md +++ b/documentation/docs/06-runtime/02-context.md @@ -163,6 +163,8 @@ export const [getCounter, setCounter] = createContext(); Svelte will warn you if you get it wrong. +Similarly, to pass primitive values through context, use functions as described in [Passing state into functions]($state#Passing-state-into-functions). + ## Component testing When writing [component tests](testing#Unit-and-component-tests-with-Vitest-Component-testing), it can be useful to create a wrapper component that sets the context in order to check the behaviour of a component that uses it. As of version 5.49, you can do this sort of thing: diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 1255d8c6a0..36528c741b 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,19 @@ # svelte +## 5.55.8 + +### Patch Changes + +- fix(print): handle `svelte:body` and fix keyframe percentage double-printing ([#18234](https://github.com/sveltejs/svelte/pull/18234)) + +- fix: execute uninitialized derived even if it's destroyed ([#18228](https://github.com/sveltejs/svelte/pull/18228)) + +- fix: use named symbols everywhere ([#18238](https://github.com/sveltejs/svelte/pull/18238)) + +- fix: don't run teardown effects when deriveds are unfreezed ([#18227](https://github.com/sveltejs/svelte/pull/18227)) + +- fix: unset context synchronously in `run` ([#18236](https://github.com/sveltejs/svelte/pull/18236)) + ## 5.55.7 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 0ec2f54c4e..0f4e9cb5cd 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.55.7", + "version": "5.55.8", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js index 9b3ac3ad78..62e1c44094 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js @@ -229,18 +229,25 @@ export function build_attribute_value( ? node.data.replace(regex_whitespaces_strict, ' ') : node.data; } else { - expressions.push( - b.call( - '$.stringify', - transform( - /** @type {Expression} */ (context.visit(node.expression)), - node.metadata.expression - ) - ) - ); + const evaluated = context.state.scope.evaluate(node.expression); - quasi = b.quasi('', i + 1 === value.length); - quasis.push(quasi); + if (evaluated.is_known) { + quasi.value.cooked += (evaluated.value ?? '') + ''; + } else { + const expression = transform( + /** @type {Expression} */ (context.visit(node.expression)), + node.metadata.expression + ); + + expressions.push( + evaluated.is_string && evaluated.is_defined + ? expression + : b.call('$.stringify', expression) + ); + + quasi = b.quasi('', i + 1 === value.length); + quasis.push(quasi); + } } } @@ -248,7 +255,9 @@ export function build_attribute_value( quasi.value.raw = sanitize_template_string(/** @type {string} */ (quasi.value.cooked)); } - return b.template(quasis, expressions); + return expressions.length > 0 + ? b.template(quasis, expressions) + : b.literal(/** @type {string} */ (quasi.value.cooked)); } /** diff --git a/packages/svelte/src/compiler/print/index.js b/packages/svelte/src/compiler/print/index.js index 26dc1b88e8..c511ee6da0 100644 --- a/packages/svelte/src/compiler/print/index.js +++ b/packages/svelte/src/compiler/print/index.js @@ -247,7 +247,7 @@ const css_visitors = { }, Percentage(node, context) { - context.write(`${node.value}%`); + context.write(node.value); }, PseudoClassSelector(node, context) { @@ -417,6 +417,7 @@ const svelte_visitors = (comments) => ({ const is_block_element = child_node.type === 'RegularElement' || child_node.type === 'Component' || + child_node.type === 'SvelteBody' || child_node.type === 'SvelteHead' || child_node.type === 'SvelteFragment' || child_node.type === 'SvelteBoundary' || @@ -821,6 +822,10 @@ const svelte_visitors = (comments) => ({ context.write(''); }, + SvelteBody(node, context) { + base_element(node, context, comments); + }, + SvelteBoundary(node, context) { base_element(node, context, comments); }, diff --git a/packages/svelte/src/constants.js b/packages/svelte/src/constants.js index a3a109b943..1e721b7d30 100644 --- a/packages/svelte/src/constants.js +++ b/packages/svelte/src/constants.js @@ -32,7 +32,7 @@ export const ELEMENT_IS_NAMESPACED = 1; export const ELEMENT_PRESERVE_ATTRIBUTE_CASE = 1 << 1; export const ELEMENT_IS_INPUT = 1 << 2; -export const UNINITIALIZED = Symbol(); +export const UNINITIALIZED = Symbol('uninitialized'); // Dev-time component properties export const FILENAME = Symbol('filename'); diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index d0e8b6860f..fe97fd10d9 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -308,15 +308,21 @@ export function run(thunks) { .then(() => { restore(); - if (errored) { - throw errored.error; + try { + if (errored) { + throw errored.error; + } + + if (aborted(active)) { + throw STALE_REACTION; + } + + return fn(); + } finally { + // We gotta unset context directly in case the function returns a promise, in which case + // unset_context in .finally() would be too late ... + unset_context(); } - - if (aborted(active)) { - throw STALE_REACTION; - } - - return fn(); }) .catch(handle_error); @@ -325,6 +331,7 @@ export function run(thunks) { promise.finally(() => { blocker.settled = true; + // ... but we also need it after such a promise has resolved in case it restores our context unset_context(); }); } diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 83e23dfab6..8d240bca31 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -338,7 +338,12 @@ export function execute_derived(derived) { var prev_active_effect = active_effect; var parent = derived.parent; - if (!is_destroying_effect && parent !== null && (parent.f & (DESTROYED | INERT)) !== 0) { + if ( + !is_destroying_effect && + parent !== null && + derived.v !== UNINITIALIZED && // if it was never evaluated before, it's guaranteed to fail downstream, so we try to execute instead + (parent.f & (DESTROYED | INERT)) !== 0 + ) { w.derived_inert(); return derived.v; @@ -447,8 +452,8 @@ export function freeze_derived_effects(derived) { // make it a noop so it doesn't get called again if the derived // is unfrozen. we don't set it to `null`, because the existence // of a teardown function is what determines whether the - // effect runs again during unfreezing - e.teardown = noop; + // effect runs again during unfreezing (but not for teardown-only effects) + if (e.fn !== null) e.teardown = noop; e.ac = null; remove_reactions(e, 0); @@ -466,7 +471,7 @@ export function unfreeze_derived_effects(derived) { for (const e of derived.effects) { // if the effect was previously frozen — indicated by the presence // of a teardown function — unfreeze it - if (e.teardown) { + if (e.teardown && e.fn !== null) { update_effect(e); } } diff --git a/packages/svelte/src/internal/client/reactivity/store.js b/packages/svelte/src/internal/client/reactivity/store.js index 7124e23db8..726765b619 100644 --- a/packages/svelte/src/internal/client/reactivity/store.js +++ b/packages/svelte/src/internal/client/reactivity/store.js @@ -21,7 +21,7 @@ export let legacy_is_updating_store = false; */ let is_store_binding = false; -let IS_UNMOUNTED = Symbol(); +let IS_UNMOUNTED = Symbol('unmounted'); /** * Gets the current value of a store. If the store isn't subscribed to yet, it will create a proxy diff --git a/packages/svelte/src/reactivity/url-search-params.js b/packages/svelte/src/reactivity/url-search-params.js index 2381e11875..2e70bf518d 100644 --- a/packages/svelte/src/reactivity/url-search-params.js +++ b/packages/svelte/src/reactivity/url-search-params.js @@ -4,7 +4,7 @@ import { tag } from '../internal/client/dev/tracing.js'; import { get } from '../internal/client/runtime.js'; import { get_current_url } from './url.js'; -export const REPLACE = Symbol(); +export const REPLACE = Symbol('replace'); /** * A reactive version of the built-in [`URLSearchParams`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams) object. diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 6b0ed0aea8..446904cd14 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.55.7'; +export const VERSION = '5.55.8'; export const PUBLIC_VERSION = '5'; diff --git a/packages/svelte/tests/print/samples/css-keyframes-percent/input.svelte b/packages/svelte/tests/print/samples/css-keyframes-percent/input.svelte new file mode 100644 index 0000000000..b2a0e3aa7a --- /dev/null +++ b/packages/svelte/tests/print/samples/css-keyframes-percent/input.svelte @@ -0,0 +1,7 @@ + diff --git a/packages/svelte/tests/print/samples/css-keyframes-percent/output.svelte b/packages/svelte/tests/print/samples/css-keyframes-percent/output.svelte new file mode 100644 index 0000000000..e35a5780fa --- /dev/null +++ b/packages/svelte/tests/print/samples/css-keyframes-percent/output.svelte @@ -0,0 +1,13 @@ + diff --git a/packages/svelte/tests/print/samples/style/output.svelte b/packages/svelte/tests/print/samples/style/output.svelte index 03168cccae..9fc5e3fbba 100644 --- a/packages/svelte/tests/print/samples/style/output.svelte +++ b/packages/svelte/tests/print/samples/style/output.svelte @@ -19,7 +19,7 @@ from { opacity: 0; } - 50%% { + 50% { opacity: 0.5; } to { diff --git a/packages/svelte/tests/print/samples/svelte-body/input.svelte b/packages/svelte/tests/print/samples/svelte-body/input.svelte new file mode 100644 index 0000000000..a4c26db5ce --- /dev/null +++ b/packages/svelte/tests/print/samples/svelte-body/input.svelte @@ -0,0 +1 @@ + diff --git a/packages/svelte/tests/print/samples/svelte-body/output.svelte b/packages/svelte/tests/print/samples/svelte-body/output.svelte new file mode 100644 index 0000000000..e13eaae979 --- /dev/null +++ b/packages/svelte/tests/print/samples/svelte-body/output.svelte @@ -0,0 +1 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/async-run-isolated-batch/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-run-isolated-batch/Child.svelte new file mode 100644 index 0000000000..6f84dc319e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-run-isolated-batch/Child.svelte @@ -0,0 +1,7 @@ + + +{x} {y} diff --git a/packages/svelte/tests/runtime-runes/samples/async-run-isolated-batch/_config.js b/packages/svelte/tests/runtime-runes/samples/async-run-isolated-batch/_config.js new file mode 100644 index 0000000000..a0dc38c660 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-run-isolated-batch/_config.js @@ -0,0 +1,26 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [show, resolve, count] = target.querySelectorAll('button'); + + show.click(); + await tick(); + resolve.click(); + await tick(); + count.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ' ' + ); + + resolve.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + '1 2 ' + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-run-isolated-batch/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-run-isolated-batch/main.svelte new file mode 100644 index 0000000000..0a5346cc47 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-run-isolated-batch/main.svelte @@ -0,0 +1,23 @@ + + + + +{#if show} + +{/if} + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-eager-const/_config.js b/packages/svelte/tests/runtime-runes/samples/async-state-eager-const/_config.js new file mode 100644 index 0000000000..1511d443e5 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-eager-const/_config.js @@ -0,0 +1,21 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + compileOptions: { dev: true }, // for testing that teardown effect in eager $.get(loaded) doesn't lead to a crash (because it means REACTION_RAN is set, which means unfreeze_derived runs) + async test({ assert, target }) { + const [count, shift] = target.querySelectorAll('button'); + + shift.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `

0

`); + + count.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `

0 (...)

`); + + shift.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `

1

`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-eager-const/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-state-eager-const/main.svelte new file mode 100644 index 0000000000..f0e2f1cdac --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-eager-const/main.svelte @@ -0,0 +1,21 @@ + + + + + + + {@const loaded = $state.eager(count) === count} +

{await push(count)} {loaded ? '' : '(...)'}

+ + {#snippet pending()}{/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/transition-derived-uninitialized/Child.svelte b/packages/svelte/tests/runtime-runes/samples/transition-derived-uninitialized/Child.svelte new file mode 100644 index 0000000000..20bd606d9b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/transition-derived-uninitialized/Child.svelte @@ -0,0 +1,10 @@ + + +
+ hello +
diff --git a/packages/svelte/tests/runtime-runes/samples/transition-derived-uninitialized/_config.js b/packages/svelte/tests/runtime-runes/samples/transition-derived-uninitialized/_config.js new file mode 100644 index 0000000000..734c84a0c4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/transition-derived-uninitialized/_config.js @@ -0,0 +1,32 @@ +import { flushSync } from 'svelte'; +import { raf } from '../../../animation-helpers'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [fly_in, fly_out] = target.querySelectorAll('button'); + + fly_in.click(); + flushSync(); + raf.tick(25); + assert.htmlEqual( + target.innerHTML, + ` + + +
hello
+ ` + ); + + fly_out.click(); + flushSync(); + raf.tick(50); + assert.htmlEqual( + target.innerHTML, + ` + + + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/transition-derived-uninitialized/main.svelte b/packages/svelte/tests/runtime-runes/samples/transition-derived-uninitialized/main.svelte new file mode 100644 index 0000000000..4fc3395af8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/transition-derived-uninitialized/main.svelte @@ -0,0 +1,11 @@ + + + + + +{#if show} + +{/if} diff --git a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js index b7e5b50eb8..45f7fb88cb 100644 --- a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js @@ -2,9 +2,9 @@ import 'svelte/internal/init-operations'; import 'svelte/internal/disclose-version'; import * as $ from 'svelte/internal/client'; -var root = $.from_html(`

`, 1); +var root = $.from_html(`

`, 1); -export default function Nullish_coallescence_omittance($$anchor) { +export default function Nullish_coallescence_omittance($$anchor, $$props) { let name = 'world'; let count = $.state(0); var fragment = root(); @@ -24,7 +24,14 @@ export default function Nullish_coallescence_omittance($$anchor) { var h1_1 = $.sibling(button, 2); h1_1.textContent = 'Hello, world'; - $.template_effect(() => $.set_text(text, `Count is ${$.get(count) ?? ''}`)); + + var div = $.sibling(h1_1, 2); + + $.template_effect(() => { + $.set_text(text, `Count is ${$.get(count) ?? ''}`); + $.set_attribute(div, 'title', `Hello, world ${$.get(count) ?? ''} 1 ${typeof $$props.value} ${$$props.value ?? ''}`); + }); + $.delegated('click', button, () => $.update(count)); $.append($$anchor, fragment); } diff --git a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/server/index.svelte.js index a7e580acb8..357a887c9b 100644 --- a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/server/index.svelte.js @@ -1,8 +1,9 @@ import * as $ from 'svelte/internal/server'; -export default function Nullish_coallescence_omittance($$renderer) { +export default function Nullish_coallescence_omittance($$renderer, $$props) { let name = 'world'; let count = 0; + let { value } = $$props; - $$renderer.push(`

Hello, world!

123

Hello, world

`); + $$renderer.push(`

Hello, world!

123

Hello, world

`); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/index.svelte b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/index.svelte index a67c574fee..ed88f5a269 100644 --- a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/index.svelte +++ b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/index.svelte @@ -1,8 +1,10 @@

Hello, {null}{name}!

{1 ?? 'stuff'}{2 ?? 'more stuff'}{3 ?? 'even more stuff'} -

Hello, {name ?? 'earth' ?? null}

\ No newline at end of file +

Hello, {name ?? 'earth' ?? null}

+