From 1ffce92d903eaf8cd1b8e4a520f3e8d581f619fe Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Thu, 5 Dec 2024 14:55:15 +0100 Subject: [PATCH 01/62] docs: note before/afterUpdate breaking change (#14567) ...about slotted content behavior Related to #14564 --- documentation/docs/07-misc/07-v5-migration-guide.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/documentation/docs/07-misc/07-v5-migration-guide.md b/documentation/docs/07-misc/07-v5-migration-guide.md index 687f8e93b11f..09ea84f26c8a 100644 --- a/documentation/docs/07-misc/07-v5-migration-guide.md +++ b/documentation/docs/07-misc/07-v5-migration-guide.md @@ -823,6 +823,8 @@ The `foreign` namespace was only useful for Svelte Native, which we're planning `afterUpdate` callbacks in a parent component will now run after `afterUpdate` callbacks in any child components. +`beforeUpdate/afterUpdate` no longer run when the component contains a `` and its content is updated. + Both functions are disallowed in runes mode — use `$effect.pre(...)` and `$effect(...)` instead. ### `contenteditable` behavior change From 73b3cf72d024d758507ba5386c99169fcadd247b Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Thu, 5 Dec 2024 14:56:28 +0100 Subject: [PATCH 02/62] fix: treat `undefined` and `null` the same for the initial input value (#14562) * fix: treat `undefined` and `null` the same for the initial input value Fixes #14558 * test * same for checked --- .changeset/cool-clocks-film.md | 5 + .../client/dom/elements/attributes.js | 20 +++- .../samples/form-default-value/_config.js | 106 ++++++++++++------ .../samples/form-default-value/main.svelte | 96 +++++++++++----- 4 files changed, 161 insertions(+), 66 deletions(-) create mode 100644 .changeset/cool-clocks-film.md diff --git a/.changeset/cool-clocks-film.md b/.changeset/cool-clocks-film.md new file mode 100644 index 000000000000..a38b2e19d497 --- /dev/null +++ b/.changeset/cool-clocks-film.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: treat `undefined` and `null` the same for the initial input value diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index 2229c1a36135..0276069eee49 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -60,13 +60,19 @@ export function remove_input_defaults(input) { export function set_value(element, value) { // @ts-expect-error var attributes = (element.__attributes ??= {}); + if ( - attributes.value === (attributes.value = value) || + attributes.value === + (attributes.value = + // treat null and undefined the same for the initial value + value ?? undefined) || // @ts-expect-error // `progress` elements always need their value set when its `0` (element.value === value && (value !== 0 || element.nodeName !== 'PROGRESS')) - ) + ) { return; + } + // @ts-expect-error element.value = value; } @@ -79,7 +85,15 @@ export function set_checked(element, checked) { // @ts-expect-error var attributes = (element.__attributes ??= {}); - if (attributes.checked === (attributes.checked = checked)) return; + if ( + attributes.checked === + (attributes.checked = + // treat null and undefined the same for the initial value + checked ?? undefined) + ) { + return; + } + // @ts-expect-error element.checked = checked; } diff --git a/packages/svelte/tests/runtime-runes/samples/form-default-value/_config.js b/packages/svelte/tests/runtime-runes/samples/form-default-value/_config.js index 3ae8b223bea1..5ef72aaa8ec2 100644 --- a/packages/svelte/tests/runtime-runes/samples/form-default-value/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/form-default-value/_config.js @@ -37,8 +37,9 @@ export default test({ const after_reset = []; const reset = /** @type {HTMLInputElement} */ (target.querySelector('input[type=reset]')); - const [test1, test2, test3, test4, test5, test12] = target.querySelectorAll('div'); - const [test6, test7, test8, test9] = target.querySelectorAll('select'); + const [test1, test2, test3, test4, test5, test6, test7, test14] = + target.querySelectorAll('div'); + const [test8, test9, test10, test11] = target.querySelectorAll('select'); const [ test1_span, test2_span, @@ -48,7 +49,9 @@ export default test({ test6_span, test7_span, test8_span, - test9_span + test9_span, + test10_span, + test11_span ] = target.querySelectorAll('span'); { @@ -74,8 +77,8 @@ export default test({ { /** @type {NodeListOf} */ const inputs = test2.querySelectorAll('input, textarea'); - check_inputs(inputs, 'value', 'y'); - assert.htmlEqual(test2_span.innerHTML, 'y y y y'); + check_inputs(inputs, 'value', 'x'); + assert.htmlEqual(test2_span.innerHTML, 'x x x x'); for (const input of inputs) { set_input(input, 'value', 'foo'); @@ -85,125 +88,164 @@ export default test({ assert.htmlEqual(test2_span.innerHTML, 'foo foo foo foo'); after_reset.push(() => { + console.log('-------------'); check_inputs(inputs, 'value', 'x'); assert.htmlEqual(test2_span.innerHTML, 'x x x x'); }); } + { + /** @type {NodeListOf} */ + const inputs = test3.querySelectorAll('input, textarea'); + check_inputs(inputs, 'value', 'y'); + assert.htmlEqual(test3_span.innerHTML, 'y y y y'); + + for (const input of inputs) { + set_input(input, 'value', 'foo'); + } + flushSync(); + check_inputs(inputs, 'value', 'foo'); + assert.htmlEqual(test3_span.innerHTML, 'foo foo foo foo'); + + after_reset.push(() => { + check_inputs(inputs, 'value', 'x'); + assert.htmlEqual(test3_span.innerHTML, 'x x x x'); + }); + } + { /** @type {NodeListOf} */ - const inputs = test3.querySelectorAll('input'); + const inputs = test4.querySelectorAll('input'); check_inputs(inputs, 'checked', true); - assert.htmlEqual(test3_span.innerHTML, 'true true'); + assert.htmlEqual(test4_span.innerHTML, 'true true'); for (const input of inputs) { set_input(input, 'checked', false); } flushSync(); check_inputs(inputs, 'checked', false); - assert.htmlEqual(test3_span.innerHTML, 'false false'); + assert.htmlEqual(test4_span.innerHTML, 'false false'); after_reset.push(() => { check_inputs(inputs, 'checked', true); - assert.htmlEqual(test3_span.innerHTML, 'true true'); + assert.htmlEqual(test4_span.innerHTML, 'true true'); }); } { /** @type {NodeListOf} */ - const inputs = test4.querySelectorAll('input'); + const inputs = test5.querySelectorAll('input'); + check_inputs(inputs, 'checked', true); + assert.htmlEqual(test5_span.innerHTML, 'true true'); + + for (const input of inputs) { + set_input(input, 'checked', false); + } + flushSync(); check_inputs(inputs, 'checked', false); - assert.htmlEqual(test4_span.innerHTML, 'false false'); + assert.htmlEqual(test5_span.innerHTML, 'false false'); after_reset.push(() => { check_inputs(inputs, 'checked', true); - assert.htmlEqual(test4_span.innerHTML, 'true true'); + assert.htmlEqual(test5_span.innerHTML, 'true true'); }); } { /** @type {NodeListOf} */ - const inputs = test5.querySelectorAll('input'); + const inputs = test6.querySelectorAll('input'); + check_inputs(inputs, 'checked', false); + assert.htmlEqual(test6_span.innerHTML, 'false false'); + + after_reset.push(() => { + check_inputs(inputs, 'checked', true); + assert.htmlEqual(test6_span.innerHTML, 'true true'); + }); + } + + { + /** @type {NodeListOf} */ + const inputs = test7.querySelectorAll('input'); check_inputs(inputs, 'checked', true); - assert.htmlEqual(test5_span.innerHTML, 'true'); + assert.htmlEqual(test7_span.innerHTML, 'true'); after_reset.push(() => { check_inputs(inputs, 'checked', false); - assert.htmlEqual(test5_span.innerHTML, 'false'); + assert.htmlEqual(test7_span.innerHTML, 'false'); }); } { /** @type {NodeListOf} */ - const options = test6.querySelectorAll('option'); + const options = test8.querySelectorAll('option'); check_inputs(options, 'selected', [false, true, false]); - assert.htmlEqual(test6_span.innerHTML, 'b'); + assert.htmlEqual(test8_span.innerHTML, 'b'); select_option(options[2]); flushSync(); check_inputs(options, 'selected', [false, false, true]); - assert.htmlEqual(test6_span.innerHTML, 'c'); + assert.htmlEqual(test8_span.innerHTML, 'c'); after_reset.push(() => { check_inputs(options, 'selected', [false, true, false]); - assert.htmlEqual(test6_span.innerHTML, 'b'); + assert.htmlEqual(test8_span.innerHTML, 'b'); }); } { /** @type {NodeListOf} */ - const options = test7.querySelectorAll('option'); + const options = test9.querySelectorAll('option'); check_inputs(options, 'selected', [false, true, false]); - assert.htmlEqual(test7_span.innerHTML, 'b'); + assert.htmlEqual(test9_span.innerHTML, 'b'); select_option(options[2]); flushSync(); check_inputs(options, 'selected', [false, false, true]); - assert.htmlEqual(test7_span.innerHTML, 'c'); + assert.htmlEqual(test9_span.innerHTML, 'c'); after_reset.push(() => { check_inputs(options, 'selected', [false, true, false]); - assert.htmlEqual(test7_span.innerHTML, 'b'); + assert.htmlEqual(test9_span.innerHTML, 'b'); }); } { /** @type {NodeListOf} */ - const options = test8.querySelectorAll('option'); + const options = test10.querySelectorAll('option'); check_inputs(options, 'selected', [false, false, true]); - assert.htmlEqual(test8_span.innerHTML, 'c'); + assert.htmlEqual(test10_span.innerHTML, 'c'); select_option(options[0]); flushSync(); check_inputs(options, 'selected', [true, false, false]); - assert.htmlEqual(test8_span.innerHTML, 'a'); + assert.htmlEqual(test10_span.innerHTML, 'a'); after_reset.push(() => { check_inputs(options, 'selected', [false, true, false]); - assert.htmlEqual(test8_span.innerHTML, 'b'); + assert.htmlEqual(test10_span.innerHTML, 'b'); }); } { /** @type {NodeListOf} */ - const options = test9.querySelectorAll('option'); + const options = test11.querySelectorAll('option'); check_inputs(options, 'selected', [false, false, true]); - assert.htmlEqual(test9_span.innerHTML, 'c'); + assert.htmlEqual(test11_span.innerHTML, 'c'); select_option(options[0]); flushSync(); check_inputs(options, 'selected', [true, false, false]); - assert.htmlEqual(test9_span.innerHTML, 'a'); + assert.htmlEqual(test11_span.innerHTML, 'a'); after_reset.push(() => { check_inputs(options, 'selected', [false, true, false]); - assert.htmlEqual(test9_span.innerHTML, 'b'); + assert.htmlEqual(test11_span.innerHTML, 'b'); }); } { /** @type {NodeListOf} */ - const inputs = test12.querySelectorAll('input, textarea'); + const inputs = test14.querySelectorAll('input, textarea'); assert.equal(inputs[0].value, 'x'); assert.equal(/** @type {HTMLInputElement} */ (inputs[1]).checked, true); assert.equal(inputs[2].value, 'x'); diff --git a/packages/svelte/tests/runtime-runes/samples/form-default-value/main.svelte b/packages/svelte/tests/runtime-runes/samples/form-default-value/main.svelte index 35d495300b70..4d6a1a00b83a 100644 --- a/packages/svelte/tests/runtime-runes/samples/form-default-value/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/form-default-value/main.svelte @@ -7,25 +7,37 @@ let value6 = $state(); let value7 = $state(); let value8 = $state(); - let value9 = $state('y'); - let value10 = $state('y'); - let value11 = $state('y'); - let value12 = $state('y'); - let value13 = $state('y'); - let value14 = $state('y'); - let value15 = $state('y'); - let value16 = $state('y'); + let value9 = $state(null); + let value10 = $state(null); + let value11 = $state(null); + let value12 = $state(null); + let value13 = $state(null); + let value14 = $state(null); + let value15 = $state(null); + let value16 = $state(null); + let value17 = $state('y'); + let value18 = $state('y'); + let value19 = $state('y'); + let value20 = $state('y'); + let value21 = $state('y'); + let value22 = $state('y'); + let value23 = $state('y'); + let value24 = $state('y'); let checked1 = $state(); let checked2 = $state(); let checked3 = $state(); let checked4 = $state(); - let checked5 = $state(false); - let checked6 = $state(false); - let checked7 = $state(false); - let checked8 = $state(false); - let checked9 = $state(true); - let checked10 = $state(true); + let checked5 = $state(null); + let checked6 = $state(null); + let checked7 = $state(null); + let checked8 = $state(null); + let checked9 = $state(false); + let checked10 = $state(false); + let checked11 = $state(false); + let checked12 = $state(false); + let checked13 = $state(true); + let checked14 = $state(true); let selected1 = $state(); @@ -53,7 +65,7 @@ - +
@@ -65,27 +77,47 @@
+ +
+ + + + + + + + +
+

Input checked

-
+
- -
+ +
+ +
+ + + + +
+ -
- - +
+ +
@@ -138,7 +170,7 @@

Static values

-
+
@@ -151,13 +183,15 @@ Bound values: {value1} {value3} {value6} {value8} {value9} {value12} {value14} {value16} - {checked2} {checked4} - {checked6} {checked8} - {checked10} - {selected1} - {selected2} - {selected3} - {selected4} - {selected5} - {selected6} + {value17} {value20} {value22} {value24} + {checked2} {checked4} + {checked6} {checked8} + {checked10} {checked12} + {checked14} + {selected1} + {selected2} + {selected3} + {selected4} + {selected5} + {selected6}

From 0a9890bb1ef748c3b5bb41c99dfe8343e3a61dbb Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Thu, 5 Dec 2024 16:24:40 +0100 Subject: [PATCH 03/62] feat: provide `MediaQuery` / `prefersReducedMotion` (#14422) * feat: provide `MediaQuery` / `prefersReducedMotion` closes #5346 * matches -> current, server fallback * createStartStopNotifier * test polyfill * more tests fixes * feedback * rename * tweak, types * hnnnggh * mark as pure * fix type check * notify -> subscribe * add links to inline docs * better API, more docs * add example to prefersReducedMotion * add example for MediaQuery * typo * fix example * tweak docs * changesets * note when APIs were added * add note * regenerate --------- Co-authored-by: Rich Harris --- .changeset/popular-worms-repeat.md | 5 + .changeset/quiet-tables-cheat.md | 5 + packages/svelte/src/motion/index.js | 30 ++++++ .../src/reactivity/create-subscriber.js | 81 ++++++++++++++++ .../svelte/src/reactivity/index-client.js | 2 + .../svelte/src/reactivity/index-server.js | 18 ++++ packages/svelte/src/reactivity/media-query.js | 41 ++++++++ packages/svelte/src/store/index-client.js | 51 +++------- packages/svelte/tests/helpers.js | 13 +++ packages/svelte/tests/motion/test.ts | 2 + packages/svelte/tsconfig.json | 1 + packages/svelte/types/index.d.ts | 93 +++++++++++++++++++ 12 files changed, 305 insertions(+), 37 deletions(-) create mode 100644 .changeset/popular-worms-repeat.md create mode 100644 .changeset/quiet-tables-cheat.md create mode 100644 packages/svelte/src/reactivity/create-subscriber.js create mode 100644 packages/svelte/src/reactivity/media-query.js diff --git a/.changeset/popular-worms-repeat.md b/.changeset/popular-worms-repeat.md new file mode 100644 index 000000000000..68d9f9a3e80e --- /dev/null +++ b/.changeset/popular-worms-repeat.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: add `createSubscriber` function for creating reactive values that depend on subscriptions diff --git a/.changeset/quiet-tables-cheat.md b/.changeset/quiet-tables-cheat.md new file mode 100644 index 000000000000..92e9c266cc90 --- /dev/null +++ b/.changeset/quiet-tables-cheat.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: add reactive `MediaQuery` class, and a `prefersReducedMotion` class instance diff --git a/packages/svelte/src/motion/index.js b/packages/svelte/src/motion/index.js index 10f52502d372..f4262a565024 100644 --- a/packages/svelte/src/motion/index.js +++ b/packages/svelte/src/motion/index.js @@ -1,2 +1,32 @@ +import { MediaQuery } from 'svelte/reactivity'; + export * from './spring.js'; export * from './tweened.js'; + +/** + * A [media query](https://svelte.dev/docs/svelte/svelte-reactivity#MediaQuery) that matches if the user [prefers reduced motion](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion). + * + * ```svelte + * + * + * + * + * {#if visible} + *

+ * flies in, unless the user prefers reduced motion + *

+ * {/if} + * ``` + * @type {MediaQuery} + * @since 5.7.0 + */ +export const prefersReducedMotion = /*@__PURE__*/ new MediaQuery( + '(prefers-reduced-motion: reduce)' +); diff --git a/packages/svelte/src/reactivity/create-subscriber.js b/packages/svelte/src/reactivity/create-subscriber.js new file mode 100644 index 000000000000..63deca62ea8b --- /dev/null +++ b/packages/svelte/src/reactivity/create-subscriber.js @@ -0,0 +1,81 @@ +import { get, tick, untrack } from '../internal/client/runtime.js'; +import { effect_tracking, render_effect } from '../internal/client/reactivity/effects.js'; +import { source } from '../internal/client/reactivity/sources.js'; +import { increment } from './utils.js'; + +/** + * Returns a `subscribe` function that, if called in an effect (including expressions in the template), + * calls its `start` callback with an `update` function. Whenever `update` is called, the effect re-runs. + * + * If `start` returns a function, it will be called when the effect is destroyed. + * + * If `subscribe` is called in multiple effects, `start` will only be called once as long as the effects + * are active, and the returned teardown function will only be called when all effects are destroyed. + * + * It's best understood with an example. Here's an implementation of [`MediaQuery`](https://svelte.dev/docs/svelte/svelte-reactivity#MediaQuery): + * + * ```js + * import { createSubscriber } from 'svelte/reactivity'; + * import { on } from 'svelte/events'; + * + * export class MediaQuery { + * #query; + * #subscribe; + * + * constructor(query) { + * this.#query = window.matchMedia(`(${query})`); + * + * this.#subscribe = createSubscriber((update) => { + * // when the `change` event occurs, re-run any effects that read `this.current` + * const off = on(this.#query, 'change', update); + * + * // stop listening when all the effects are destroyed + * return () => off(); + * }); + * } + * + * get current() { + * this.#subscribe(); + * + * // Return the current state of the query, whether or not we're in an effect + * return this.#query.matches; + * } + * } + * ``` + * @param {(update: () => void) => (() => void) | void} start + * @since 5.7.0 + */ +export function createSubscriber(start) { + let subscribers = 0; + let version = source(0); + /** @type {(() => void) | void} */ + let stop; + + return () => { + if (effect_tracking()) { + get(version); + + render_effect(() => { + if (subscribers === 0) { + stop = untrack(() => start(() => increment(version))); + } + + subscribers += 1; + + return () => { + tick().then(() => { + // Only count down after timeout, else we would reach 0 before our own render effect reruns, + // but reach 1 again when the tick callback of the prior teardown runs. That would mean we + // re-subcribe unnecessarily and create a memory leak because the old subscription is never cleaned up. + subscribers -= 1; + + if (subscribers === 0) { + stop?.(); + stop = undefined; + } + }); + }; + }); + } + }; +} diff --git a/packages/svelte/src/reactivity/index-client.js b/packages/svelte/src/reactivity/index-client.js index 2757688a5958..3eb9b95333ab 100644 --- a/packages/svelte/src/reactivity/index-client.js +++ b/packages/svelte/src/reactivity/index-client.js @@ -3,3 +3,5 @@ export { SvelteSet } from './set.js'; export { SvelteMap } from './map.js'; export { SvelteURL } from './url.js'; export { SvelteURLSearchParams } from './url-search-params.js'; +export { MediaQuery } from './media-query.js'; +export { createSubscriber } from './create-subscriber.js'; diff --git a/packages/svelte/src/reactivity/index-server.js b/packages/svelte/src/reactivity/index-server.js index 6240469ec36f..6a6c9dcf1360 100644 --- a/packages/svelte/src/reactivity/index-server.js +++ b/packages/svelte/src/reactivity/index-server.js @@ -3,3 +3,21 @@ export const SvelteSet = globalThis.Set; export const SvelteMap = globalThis.Map; export const SvelteURL = globalThis.URL; export const SvelteURLSearchParams = globalThis.URLSearchParams; + +export class MediaQuery { + current; + /** + * @param {string} query + * @param {boolean} [matches] + */ + constructor(query, matches = false) { + this.current = matches; + } +} + +/** + * @param {any} _ + */ +export function createSubscriber(_) { + return () => {}; +} diff --git a/packages/svelte/src/reactivity/media-query.js b/packages/svelte/src/reactivity/media-query.js new file mode 100644 index 000000000000..a2be0adc91e2 --- /dev/null +++ b/packages/svelte/src/reactivity/media-query.js @@ -0,0 +1,41 @@ +import { createSubscriber } from './create-subscriber.js'; +import { on } from '../events/index.js'; + +/** + * Creates a media query and provides a `current` property that reflects whether or not it matches. + * + * Use it carefully — during server-side rendering, there is no way to know what the correct value should be, potentially causing content to change upon hydration. + * If you can use the media query in CSS to achieve the same effect, do that. + * + * ```svelte + * + * + *

{large.current ? 'large screen' : 'small screen'}

+ * ``` + * @since 5.7.0 + */ +export class MediaQuery { + #query; + #subscribe = createSubscriber((update) => { + return on(this.#query, 'change', update); + }); + + get current() { + this.#subscribe(); + + return this.#query.matches; + } + + /** + * @param {string} query A media query string + * @param {boolean} [matches] Fallback value for the server + */ + constructor(query, matches) { + // For convenience (and because people likely forget them) we add the parentheses; double parentheses are not a problem + this.#query = window.matchMedia(`(${query})`); + } +} diff --git a/packages/svelte/src/store/index-client.js b/packages/svelte/src/store/index-client.js index f2f1dfc4eba1..ae6806ec763f 100644 --- a/packages/svelte/src/store/index-client.js +++ b/packages/svelte/src/store/index-client.js @@ -1,14 +1,11 @@ /** @import { Readable, Writable } from './public.js' */ -import { noop } from '../internal/shared/utils.js'; import { effect_root, effect_tracking, render_effect } from '../internal/client/reactivity/effects.js'; -import { source } from '../internal/client/reactivity/sources.js'; -import { get as get_source, tick } from '../internal/client/runtime.js'; -import { increment } from '../reactivity/utils.js'; import { get, writable } from './shared/index.js'; +import { createSubscriber } from '../reactivity/create-subscriber.js'; export { derived, get, readable, readonly, writable } from './shared/index.js'; @@ -109,43 +106,23 @@ export function toStore(get, set) { */ export function fromStore(store) { let value = /** @type {V} */ (undefined); - let version = source(0); - let subscribers = 0; - let unsubscribe = noop; + const subscribe = createSubscriber((update) => { + let ran = false; - function current() { - if (effect_tracking()) { - get_source(version); + const unsubscribe = store.subscribe((v) => { + value = v; + if (ran) update(); + }); - render_effect(() => { - if (subscribers === 0) { - let ran = false; - - unsubscribe = store.subscribe((v) => { - value = v; - if (ran) increment(version); - }); - - ran = true; - } - - subscribers += 1; - - return () => { - tick().then(() => { - // Only count down after timeout, else we would reach 0 before our own render effect reruns, - // but reach 1 again when the tick callback of the prior teardown runs. That would mean we - // re-subcribe unnecessarily and create a memory leak because the old subscription is never cleaned up. - subscribers -= 1; - - if (subscribers === 0) { - unsubscribe(); - } - }); - }; - }); + ran = true; + + return unsubscribe; + }); + function current() { + if (effect_tracking()) { + subscribe(); return value; } diff --git a/packages/svelte/tests/helpers.js b/packages/svelte/tests/helpers.js index 002ebf2e38ff..45b90240c99a 100644 --- a/packages/svelte/tests/helpers.js +++ b/packages/svelte/tests/helpers.js @@ -172,3 +172,16 @@ export function write(file, contents) { fs.writeFileSync(file, contents); } + +// Guard because not all test contexts load this with JSDOM +if (typeof window !== 'undefined') { + // @ts-expect-error JS DOM doesn't support it + Window.prototype.matchMedia = (media) => { + return { + matches: false, + media, + addEventListener: () => {}, + removeEventListener: () => {} + }; + }; +} diff --git a/packages/svelte/tests/motion/test.ts b/packages/svelte/tests/motion/test.ts index 05971b5cab65..b6554e5e56ed 100644 --- a/packages/svelte/tests/motion/test.ts +++ b/packages/svelte/tests/motion/test.ts @@ -1,3 +1,5 @@ +// @vitest-environment jsdom +import '../helpers.js'; // for the matchMedia polyfill import { describe, it, assert } from 'vitest'; import { get } from 'svelte/store'; import { spring, tweened } from 'svelte/motion'; diff --git a/packages/svelte/tsconfig.json b/packages/svelte/tsconfig.json index 017b92629866..380307901edd 100644 --- a/packages/svelte/tsconfig.json +++ b/packages/svelte/tsconfig.json @@ -25,6 +25,7 @@ "svelte/motion": ["./src/motion/public.d.ts"], "svelte/server": ["./src/server/index.d.ts"], "svelte/store": ["./src/store/public.d.ts"], + "svelte/reactivity": ["./src/reactivity/index-client.js"], "#compiler": ["./src/compiler/types/index.d.ts"], "#client": ["./src/internal/client/types.d.ts"], "#server": ["./src/internal/server/types.d.ts"], diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 5a5cc86ae6c3..46a2137ae6e4 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -1637,6 +1637,7 @@ declare module 'svelte/legacy' { } declare module 'svelte/motion' { + import type { MediaQuery } from 'svelte/reactivity'; export interface Spring extends Readable { set: (new_value: T, opts?: SpringUpdateOpts) => Promise; update: (fn: Updater, opts?: SpringUpdateOpts) => Promise; @@ -1683,6 +1684,30 @@ declare module 'svelte/motion' { easing?: (t: number) => number; interpolate?: (a: T, b: T) => (t: number) => T; } + /** + * A [media query](https://svelte.dev/docs/svelte/svelte-reactivity#MediaQuery) that matches if the user [prefers reduced motion](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion). + * + * ```svelte + * + * + * + * + * {#if visible} + *

+ * flies in, unless the user prefers reduced motion + *

+ * {/if} + * ``` + * @since 5.7.0 + */ + export const prefersReducedMotion: MediaQuery; /** * The spring function in Svelte creates a store whose value is animated, with a motion that simulates the behavior of a spring. This means when the value changes, instead of transitioning at a steady rate, it "bounces" like a spring would, depending on the physics parameters provided. This adds a level of realism to the transitions and can enhance the user experience. * @@ -1727,6 +1752,74 @@ declare module 'svelte/reactivity' { [REPLACE](params: URLSearchParams): void; #private; } + /** + * Creates a media query and provides a `current` property that reflects whether or not it matches. + * + * Use it carefully — during server-side rendering, there is no way to know what the correct value should be, potentially causing content to change upon hydration. + * If you can use the media query in CSS to achieve the same effect, do that. + * + * ```svelte + * + * + *

{large.current ? 'large screen' : 'small screen'}

+ * ``` + * @since 5.7.0 + */ + export class MediaQuery { + /** + * @param query A media query string + * @param matches Fallback value for the server + */ + constructor(query: string, matches?: boolean | undefined); + get current(): boolean; + #private; + } + /** + * Returns a `subscribe` function that, if called in an effect (including expressions in the template), + * calls its `start` callback with an `update` function. Whenever `update` is called, the effect re-runs. + * + * If `start` returns a function, it will be called when the effect is destroyed. + * + * If `subscribe` is called in multiple effects, `start` will only be called once as long as the effects + * are active, and the returned teardown function will only be called when all effects are destroyed. + * + * It's best understood with an example. Here's an implementation of [`MediaQuery`](https://svelte.dev/docs/svelte/svelte-reactivity#MediaQuery): + * + * ```js + * import { createSubscriber } from 'svelte/reactivity'; + * import { on } from 'svelte/events'; + * + * export class MediaQuery { + * #query; + * #subscribe; + * + * constructor(query) { + * this.#query = window.matchMedia(`(${query})`); + * + * this.#subscribe = createSubscriber((update) => { + * // when the `change` event occurs, re-run any effects that read `this.current` + * const off = on(this.#query, 'change', update); + * + * // stop listening when all the effects are destroyed + * return () => off(); + * }); + * } + * + * get current() { + * this.#subscribe(); + * + * // Return the current state of the query, whether or not we're in an effect + * return this.#query.matches; + * } + * } + * ``` + * @since 5.7.0 + */ + export function createSubscriber(start: (update: () => void) => (() => void) | void): () => void; export {}; } From 7f5172745dee298a71f0b3502e225d999f1a11b0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 10:32:37 -0500 Subject: [PATCH 04/62] Version Packages (#14570) Co-authored-by: github-actions[bot] --- .changeset/cool-clocks-film.md | 5 ----- .changeset/popular-worms-repeat.md | 5 ----- .changeset/quiet-tables-cheat.md | 5 ----- packages/svelte/CHANGELOG.md | 12 ++++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 6 files changed, 14 insertions(+), 17 deletions(-) delete mode 100644 .changeset/cool-clocks-film.md delete mode 100644 .changeset/popular-worms-repeat.md delete mode 100644 .changeset/quiet-tables-cheat.md diff --git a/.changeset/cool-clocks-film.md b/.changeset/cool-clocks-film.md deleted file mode 100644 index a38b2e19d497..000000000000 --- a/.changeset/cool-clocks-film.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: treat `undefined` and `null` the same for the initial input value diff --git a/.changeset/popular-worms-repeat.md b/.changeset/popular-worms-repeat.md deleted file mode 100644 index 68d9f9a3e80e..000000000000 --- a/.changeset/popular-worms-repeat.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': minor ---- - -feat: add `createSubscriber` function for creating reactive values that depend on subscriptions diff --git a/.changeset/quiet-tables-cheat.md b/.changeset/quiet-tables-cheat.md deleted file mode 100644 index 92e9c266cc90..000000000000 --- a/.changeset/quiet-tables-cheat.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': minor ---- - -feat: add reactive `MediaQuery` class, and a `prefersReducedMotion` class instance diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 6fc56c836cee..9b9ff754be2c 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,17 @@ # svelte +## 5.7.0 + +### Minor Changes + +- feat: add `createSubscriber` function for creating reactive values that depend on subscriptions ([#14422](https://github.com/sveltejs/svelte/pull/14422)) + +- feat: add reactive `MediaQuery` class, and a `prefersReducedMotion` class instance ([#14422](https://github.com/sveltejs/svelte/pull/14422)) + +### Patch Changes + +- fix: treat `undefined` and `null` the same for the initial input value ([#14562](https://github.com/sveltejs/svelte/pull/14562)) + ## 5.6.2 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 68d6eeed9ea7..11313d14ab82 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.6.2", + "version": "5.7.0", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 88f83720a173..a69d2f6b32aa 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -6,5 +6,5 @@ * https://svelte.dev/docs/svelte-compiler#svelte-version * @type {string} */ -export const VERSION = '5.6.2'; +export const VERSION = '5.7.0'; export const PUBLIC_VERSION = '5'; From 6a6b4ec36a11bb4434311c4735ff846b79c00ba0 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 5 Dec 2024 13:31:02 -0500 Subject: [PATCH 05/62] fix deprecation notices (#14574) --- packages/svelte/src/index-client.js | 6 +++--- packages/svelte/src/index.d.ts | 12 ++++++------ packages/svelte/types/index.d.ts | 18 +++++++++--------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/svelte/src/index-client.js b/packages/svelte/src/index-client.js index 72811c8f17c9..587d76623331 100644 --- a/packages/svelte/src/index-client.js +++ b/packages/svelte/src/index-client.js @@ -83,7 +83,7 @@ function create_custom_event(type, detail, { bubbles = false, cancelable = false * }>(); * ``` * - * @deprecated Use callback props and/or the `$host()` rune instead — see https://svelte.dev/docs/svelte/v5-migration-guide#Event-changes-Component-events + * @deprecated Use callback props and/or the `$host()` rune instead — see [migration guide](https://svelte.dev/docs/svelte/v5-migration-guide#Event-changes-Component-events) * @template {Record} [EventMap = any] * @returns {EventDispatcher} */ @@ -122,7 +122,7 @@ export function createEventDispatcher() { * * In runes mode use `$effect.pre` instead. * - * @deprecated Use `$effect.pre` instead — see https://svelte.dev/docs/svelte/$effect#$effect.pre + * @deprecated Use [`$effect.pre`](https://svelte.dev/docs/svelte/$effect#$effect.pre) instead * @param {() => void} fn * @returns {void} */ @@ -145,7 +145,7 @@ export function beforeUpdate(fn) { * * In runes mode use `$effect` instead. * - * @deprecated Use `$effect` instead — see https://svelte.dev/docs/svelte/$effect + * @deprecated Use [`$effect`](https://svelte.dev/docs/svelte/$effect) instead * @param {() => void} fn * @returns {void} */ diff --git a/packages/svelte/src/index.d.ts b/packages/svelte/src/index.d.ts index b8ba8b6f0a75..e157ce76e2f3 100644 --- a/packages/svelte/src/index.d.ts +++ b/packages/svelte/src/index.d.ts @@ -53,7 +53,7 @@ export class SvelteComponent< /** * @deprecated This constructor only exists when using the `asClassComponent` compatibility helper, which * is a stop-gap solution. Migrate towards using `mount` instead. See - * https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes for more info. + * [migration guide](https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes) for more info. */ constructor(options: ComponentConstructorOptions>); /** @@ -83,14 +83,14 @@ export class SvelteComponent< /** * @deprecated This method only exists when using one of the legacy compatibility helpers, which - * is a stop-gap solution. See https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes + * is a stop-gap solution. See [migration guide](https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes) * for more info. */ $destroy(): void; /** * @deprecated This method only exists when using one of the legacy compatibility helpers, which - * is a stop-gap solution. See https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes + * is a stop-gap solution. See [migration guide](https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes) * for more info. */ $on>( @@ -100,7 +100,7 @@ export class SvelteComponent< /** * @deprecated This method only exists when using one of the legacy compatibility helpers, which - * is a stop-gap solution. See https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes + * is a stop-gap solution. See [migration guide](https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes) * for more info. */ $set(props: Partial): void; @@ -153,13 +153,13 @@ export interface Component< ): { /** * @deprecated This method only exists when using one of the legacy compatibility helpers, which - * is a stop-gap solution. See https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes + * is a stop-gap solution. See [migration guide](https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes) * for more info. */ $on?(type: string, callback: (e: any) => void): () => void; /** * @deprecated This method only exists when using one of the legacy compatibility helpers, which - * is a stop-gap solution. See https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes + * is a stop-gap solution. See [migration guide](https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes) * for more info. */ $set?(props: Partial): void; diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 46a2137ae6e4..db6ac88c2c2a 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -50,7 +50,7 @@ declare module 'svelte' { /** * @deprecated This constructor only exists when using the `asClassComponent` compatibility helper, which * is a stop-gap solution. Migrate towards using `mount` instead. See - * https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes for more info. + * [migration guide](https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes) for more info. */ constructor(options: ComponentConstructorOptions>); /** @@ -80,14 +80,14 @@ declare module 'svelte' { /** * @deprecated This method only exists when using one of the legacy compatibility helpers, which - * is a stop-gap solution. See https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes + * is a stop-gap solution. See [migration guide](https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes) * for more info. */ $destroy(): void; /** * @deprecated This method only exists when using one of the legacy compatibility helpers, which - * is a stop-gap solution. See https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes + * is a stop-gap solution. See [migration guide](https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes) * for more info. */ $on>( @@ -97,7 +97,7 @@ declare module 'svelte' { /** * @deprecated This method only exists when using one of the legacy compatibility helpers, which - * is a stop-gap solution. See https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes + * is a stop-gap solution. See [migration guide](https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes) * for more info. */ $set(props: Partial): void; @@ -150,13 +150,13 @@ declare module 'svelte' { ): { /** * @deprecated This method only exists when using one of the legacy compatibility helpers, which - * is a stop-gap solution. See https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes + * is a stop-gap solution. See [migration guide](https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes) * for more info. */ $on?(type: string, callback: (e: any) => void): () => void; /** * @deprecated This method only exists when using one of the legacy compatibility helpers, which - * is a stop-gap solution. See https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes + * is a stop-gap solution. See [migration guide](https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes) * for more info. */ $set?(props: Partial): void; @@ -385,7 +385,7 @@ declare module 'svelte' { * }>(); * ``` * - * @deprecated Use callback props and/or the `$host()` rune instead — see https://svelte.dev/docs/svelte/v5-migration-guide#Event-changes-Component-events + * @deprecated Use callback props and/or the `$host()` rune instead — see [migration guide](https://svelte.dev/docs/svelte/v5-migration-guide#Event-changes-Component-events) * */ export function createEventDispatcher = any>(): EventDispatcher; /** @@ -395,7 +395,7 @@ declare module 'svelte' { * * In runes mode use `$effect.pre` instead. * - * @deprecated Use `$effect.pre` instead — see https://svelte.dev/docs/svelte/$effect#$effect.pre + * @deprecated Use [`$effect.pre`](https://svelte.dev/docs/svelte/$effect#$effect.pre) instead * */ export function beforeUpdate(fn: () => void): void; /** @@ -405,7 +405,7 @@ declare module 'svelte' { * * In runes mode use `$effect` instead. * - * @deprecated Use `$effect` instead — see https://svelte.dev/docs/svelte/$effect + * @deprecated Use [`$effect`](https://svelte.dev/docs/svelte/$effect) instead * */ export function afterUpdate(fn: () => void): void; /** From ca67aa1b3405ba5de36ab240131a44beea1134d1 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Thu, 5 Dec 2024 21:17:59 +0100 Subject: [PATCH 06/62] fix: ensure bindings always take precedence over spreads (#14575) --- .changeset/little-berries-worry.md | 5 +++ .../client/visitors/shared/component.js | 36 +++++++++++++------ .../server/visitors/shared/component.js | 35 ++++++++++++------ .../bind-and-spread-precedence/_config.js | 9 +++++ .../bind-and-spread-precedence/input.svelte | 5 +++ .../bind-and-spread-precedence/main.svelte | 11 ++++++ 6 files changed, 81 insertions(+), 20 deletions(-) create mode 100644 .changeset/little-berries-worry.md create mode 100644 packages/svelte/tests/runtime-runes/samples/bind-and-spread-precedence/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/bind-and-spread-precedence/input.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/bind-and-spread-precedence/main.svelte diff --git a/.changeset/little-berries-worry.md b/.changeset/little-berries-worry.md new file mode 100644 index 000000000000..aad4de715a50 --- /dev/null +++ b/.changeset/little-berries-worry.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: ensure bindings always take precedence over spreads diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js index 5dde60b3b414..aa7be93cb57e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js @@ -20,6 +20,8 @@ import { determine_slot } from '../../../../../utils/slot.js'; export function build_component(node, component_name, context, anchor = context.state.node) { /** @type {Array} */ const props_and_spreads = []; + /** @type {Array<() => void>} */ + const delayed_props = []; /** @type {ExpressionStatement[]} */ const lets = []; @@ -63,14 +65,23 @@ export function build_component(node, component_name, context, anchor = context. /** * @param {Property} prop + * @param {boolean} [delay] */ - function push_prop(prop) { - const current = props_and_spreads.at(-1); - const current_is_props = Array.isArray(current); - const props = current_is_props ? current : []; - props.push(prop); - if (!current_is_props) { - props_and_spreads.push(props); + function push_prop(prop, delay = false) { + const do_push = () => { + const current = props_and_spreads.at(-1); + const current_is_props = Array.isArray(current); + const props = current_is_props ? current : []; + props.push(prop); + if (!current_is_props) { + props_and_spreads.push(props); + } + }; + + if (delay) { + delayed_props.push(do_push); + } else { + do_push(); } } @@ -202,22 +213,27 @@ export function build_component(node, component_name, context, anchor = context. attribute.expression.type === 'Identifier' && context.state.scope.get(attribute.expression.name)?.kind === 'store_sub'; + // Delay prop pushes so bindings come at the end, to avoid spreads overwriting them if (is_store_sub) { push_prop( - b.get(attribute.name, [b.stmt(b.call('$.mark_store_binding')), b.return(expression)]) + b.get(attribute.name, [b.stmt(b.call('$.mark_store_binding')), b.return(expression)]), + true ); } else { - push_prop(b.get(attribute.name, [b.return(expression)])); + push_prop(b.get(attribute.name, [b.return(expression)]), true); } const assignment = b.assignment('=', attribute.expression, b.id('$$value')); push_prop( - b.set(attribute.name, [b.stmt(/** @type {Expression} */ (context.visit(assignment)))]) + b.set(attribute.name, [b.stmt(/** @type {Expression} */ (context.visit(assignment)))]), + true ); } } } + delayed_props.forEach((fn) => fn()); + if (slot_scope_applies_to_itself) { context.state.init.push(...lets); } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js index 79df3cdd04c6..7cabfb06c527 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js @@ -13,6 +13,8 @@ import { is_element_node } from '../../../../nodes.js'; export function build_inline_component(node, expression, context) { /** @type {Array} */ const props_and_spreads = []; + /** @type {Array<() => void>} */ + const delayed_props = []; /** @type {Property[]} */ const custom_css_props = []; @@ -49,14 +51,23 @@ export function build_inline_component(node, expression, context) { /** * @param {Property} prop + * @param {boolean} [delay] */ - function push_prop(prop) { - const current = props_and_spreads.at(-1); - const current_is_props = Array.isArray(current); - const props = current_is_props ? current : []; - props.push(prop); - if (!current_is_props) { - props_and_spreads.push(props); + function push_prop(prop, delay = false) { + const do_push = () => { + const current = props_and_spreads.at(-1); + const current_is_props = Array.isArray(current); + const props = current_is_props ? current : []; + props.push(prop); + if (!current_is_props) { + props_and_spreads.push(props); + } + }; + + if (delay) { + delayed_props.push(do_push); + } else { + do_push(); } } @@ -81,11 +92,12 @@ export function build_inline_component(node, expression, context) { const value = build_attribute_value(attribute.value, context, false, true); push_prop(b.prop('init', b.key(attribute.name), value)); } else if (attribute.type === 'BindDirective' && attribute.name !== 'this') { - // TODO this needs to turn the whole thing into a while loop because the binding could be mutated eagerly in the child + // Delay prop pushes so bindings come at the end, to avoid spreads overwriting them push_prop( b.get(attribute.name, [ b.return(/** @type {Expression} */ (context.visit(attribute.expression))) - ]) + ]), + true ); push_prop( b.set(attribute.name, [ @@ -95,11 +107,14 @@ export function build_inline_component(node, expression, context) { ) ), b.stmt(b.assignment('=', b.id('$$settled'), b.false)) - ]) + ]), + true ); } } + delayed_props.forEach((fn) => fn()); + /** @type {Statement[]} */ const snippet_declarations = []; diff --git a/packages/svelte/tests/runtime-runes/samples/bind-and-spread-precedence/_config.js b/packages/svelte/tests/runtime-runes/samples/bind-and-spread-precedence/_config.js new file mode 100644 index 000000000000..79e707148605 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-and-spread-precedence/_config.js @@ -0,0 +1,9 @@ +import { test } from '../../test'; + +export default test({ + ssrHtml: ``, + + test({ assert, target }) { + assert.equal(target.querySelector('input')?.value, 'foo'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/bind-and-spread-precedence/input.svelte b/packages/svelte/tests/runtime-runes/samples/bind-and-spread-precedence/input.svelte new file mode 100644 index 000000000000..f19f2477077d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-and-spread-precedence/input.svelte @@ -0,0 +1,5 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/bind-and-spread-precedence/main.svelte b/packages/svelte/tests/runtime-runes/samples/bind-and-spread-precedence/main.svelte new file mode 100644 index 000000000000..b4dea5b5885d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-and-spread-precedence/main.svelte @@ -0,0 +1,11 @@ + + + - * - * {#if visible} - *

- * flies in, unless the user prefers reduced motion - *

- * {/if} - * ``` - * @since 5.7.0 - */ - export const prefersReducedMotion: MediaQuery; /** * The spring function in Svelte creates a store whose value is animated, with a motion that simulates the behavior of a spring. This means when the value changes, instead of transitioning at a steady rate, it "bounces" like a spring would, depending on the physics parameters provided. This adds a level of realism to the transitions and can enhance the user experience. * + * @deprecated Use [`Spring`](https://svelte.dev/docs/svelte/svelte-motion#Spring) instead * */ export function spring(value?: T | undefined, opts?: SpringOpts | undefined): Spring; /** * A tweened store in Svelte is a special type of store that provides smooth transitions between state values over time. * + * @deprecated Use [`Tween`](https://svelte.dev/docs/svelte/svelte-motion#Tween) instead * */ export function tweened(value?: T | undefined, defaults?: TweenedOptions | undefined): Tweened; + /** + * A wrapper for a value that tweens smoothly to its target value. Changes to `tween.target` will cause `tween.current` to + * move towards it over time, taking account of the `delay`, `duration` and `easing` options. + * + * ```svelte + * + * + * + * + * ``` + * @since 5.8.0 + */ + export class Tween { + /** + * Create a tween whose value is bound to the return value of `fn`. This must be called + * inside an effect root (for example, during component initialisation). + * + * ```svelte + * + * ``` + * + */ + static of(fn: () => U, options?: TweenedOptions | undefined): Tween; + + constructor(value: T, options?: TweenedOptions); + /** + * Sets `tween.target` to `value` and returns a `Promise` that resolves if and when `tween.current` catches up to it. + * + * If `options` are provided, they will override the tween's defaults. + * */ + set(value: T, options?: TweenedOptions | undefined): Promise; + get current(): T; + set target(v: T); + get target(): T; + #private; + } export {}; } From 947d4adf3beaa6fa92deb347ad1273b58001133b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 6 Dec 2024 09:16:48 -0500 Subject: [PATCH 09/62] Version Packages (#14583) Co-authored-by: github-actions[bot] --- .changeset/tame-bottles-switch.md | 5 ----- packages/svelte/CHANGELOG.md | 6 ++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .changeset/tame-bottles-switch.md diff --git a/.changeset/tame-bottles-switch.md b/.changeset/tame-bottles-switch.md deleted file mode 100644 index c597f5ea997c..000000000000 --- a/.changeset/tame-bottles-switch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': minor ---- - -feat: add `Spring` and `Tween` classes to `svelte/motion` diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index b19cfb355b1e..861347492478 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.8.0 + +### Minor Changes + +- feat: add `Spring` and `Tween` classes to `svelte/motion` ([#11519](https://github.com/sveltejs/svelte/pull/11519)) + ## 5.7.1 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 6a01e01ecb86..97dedffbfc76 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.7.1", + "version": "5.8.0", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 331506a55a2f..f0574202b588 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -6,5 +6,5 @@ * https://svelte.dev/docs/svelte-compiler#svelte-version * @type {string} */ -export const VERSION = '5.7.1'; +export const VERSION = '5.8.0'; export const PUBLIC_VERSION = '5'; From 7086709767fabfe6a93e0a2463f66c3bd314072c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 6 Dec 2024 09:59:36 -0500 Subject: [PATCH 10/62] add since tag to Spring (#14585) --- packages/svelte/src/motion/public.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/svelte/src/motion/public.d.ts b/packages/svelte/src/motion/public.d.ts index 8fb9b9e66a1b..5a6a7e0fae4c 100644 --- a/packages/svelte/src/motion/public.d.ts +++ b/packages/svelte/src/motion/public.d.ts @@ -34,6 +34,7 @@ export interface Spring extends Readable { * * * ``` + * @since 5.8.0 */ export class Spring { constructor(value: T, options?: SpringOpts); From 8433a7169b51b9b7e232d7cc4f875327ad8a2a78 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 6 Dec 2024 10:03:17 -0500 Subject: [PATCH 11/62] fix: reinstate `prefersReducedMotion` (#14586) * reinstate prefersReducedMotion * changeset --- .changeset/large-humans-report.md | 5 +++++ packages/svelte/src/motion/public.d.ts | 2 +- packages/svelte/types/index.d.ts | 25 +++++++++++++++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 .changeset/large-humans-report.md diff --git a/.changeset/large-humans-report.md b/.changeset/large-humans-report.md new file mode 100644 index 000000000000..6ba09ef148d9 --- /dev/null +++ b/.changeset/large-humans-report.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: reinstate missing prefersReducedMotion export diff --git a/packages/svelte/src/motion/public.d.ts b/packages/svelte/src/motion/public.d.ts index 5a6a7e0fae4c..4e74d4b76f06 100644 --- a/packages/svelte/src/motion/public.d.ts +++ b/packages/svelte/src/motion/public.d.ts @@ -85,4 +85,4 @@ export interface Tweened extends Readable { update(updater: Updater, opts?: TweenedOptions): Promise; } -export { spring, tweened, Tween } from './index.js'; +export { prefersReducedMotion, spring, tweened, Tween } from './index.js'; diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index f3f9580dc6dc..f6b5b21f8085 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -1637,6 +1637,7 @@ declare module 'svelte/legacy' { } declare module 'svelte/motion' { + import type { MediaQuery } from 'svelte/reactivity'; // TODO we do declaration merging here in order to not have a breaking change (renaming the Spring interface) // this means both the Spring class and the Spring interface are merged into one with some things only // existing on one side. In Svelte 6, remove the type definition and move the jsdoc onto the class in spring.js @@ -1767,6 +1768,30 @@ declare module 'svelte/motion' { easing?: (t: number) => number; interpolate?: (a: T, b: T) => (t: number) => T; } + /** + * A [media query](https://svelte.dev/docs/svelte/svelte-reactivity#MediaQuery) that matches if the user [prefers reduced motion](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion). + * + * ```svelte + * + * + * + * + * {#if visible} + *

+ * flies in, unless the user prefers reduced motion + *

+ * {/if} + * ``` + * @since 5.7.0 + */ + export const prefersReducedMotion: MediaQuery; /** * The spring function in Svelte creates a store whose value is animated, with a motion that simulates the behavior of a spring. This means when the value changes, instead of transitioning at a steady rate, it "bounces" like a spring would, depending on the physics parameters provided. This adds a level of realism to the transitions and can enhance the user experience. * From ad87572adc4647915cc0250b376e5726552d1dda Mon Sep 17 00:00:00 2001 From: brunnerh Date: Fri, 6 Dec 2024 16:04:32 +0100 Subject: [PATCH 12/62] docs: Import .svelte.js files with explicit extension. (#14584) --- documentation/docs/06-runtime/01-stores.md | 2 +- documentation/docs/06-runtime/02-context.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/documentation/docs/06-runtime/01-stores.md b/documentation/docs/06-runtime/01-stores.md index 9cff5a754f87..12d5576b407c 100644 --- a/documentation/docs/06-runtime/01-stores.md +++ b/documentation/docs/06-runtime/01-stores.md @@ -49,7 +49,7 @@ export const userState = $state({ ```svelte

User name: {userState.name}

diff --git a/documentation/docs/06-runtime/02-context.md b/documentation/docs/06-runtime/02-context.md index 62dd0c6a9e7b..30799215b6eb 100644 --- a/documentation/docs/06-runtime/02-context.md +++ b/documentation/docs/06-runtime/02-context.md @@ -22,7 +22,7 @@ export const myGlobalState = $state({ ```svelte ``` From 65fdcec55df996f06bf5cc162a1e16e9ed35cddf Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 6 Dec 2024 15:33:07 -0500 Subject: [PATCH 13/62] chore: regenerate types (#14592) --- packages/svelte/types/index.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index f6b5b21f8085..0d761919a8e0 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -1671,6 +1671,7 @@ declare module 'svelte/motion' { * * * ``` + * @since 5.8.0 */ export class Spring { constructor(value: T, options?: SpringOpts); From 98286349b23e88be77533df577bc8998bc15cc46 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 6 Dec 2024 15:40:52 -0500 Subject: [PATCH 14/62] Version Packages (#14593) Co-authored-by: github-actions[bot] --- .changeset/large-humans-report.md | 5 ----- packages/svelte/CHANGELOG.md | 6 ++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .changeset/large-humans-report.md diff --git a/.changeset/large-humans-report.md b/.changeset/large-humans-report.md deleted file mode 100644 index 6ba09ef148d9..000000000000 --- a/.changeset/large-humans-report.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: reinstate missing prefersReducedMotion export diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 861347492478..4031110fa799 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.8.1 + +### Patch Changes + +- fix: reinstate missing prefersReducedMotion export ([#14586](https://github.com/sveltejs/svelte/pull/14586)) + ## 5.8.0 ### Minor Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 97dedffbfc76..c751a598db53 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.8.0", + "version": "5.8.1", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index f0574202b588..3061318cb034 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -6,5 +6,5 @@ * https://svelte.dev/docs/svelte-compiler#svelte-version * @type {string} */ -export const VERSION = '5.8.0'; +export const VERSION = '5.8.1'; export const PUBLIC_VERSION = '5'; From 08e2cf25b03707f4ca6e44a1e6ca0480190c14a1 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 6 Dec 2024 16:08:58 -0500 Subject: [PATCH 15/62] chore: fix sandbox (#14596) --- playgrounds/sandbox/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playgrounds/sandbox/package.json b/playgrounds/sandbox/package.json index 944e79621e0d..654a517c9fb6 100644 --- a/playgrounds/sandbox/package.json +++ b/playgrounds/sandbox/package.json @@ -6,7 +6,7 @@ "scripts": { "prepare": "node scripts/create-app-svelte.js", "dev": "vite --host", - "ssr": "node ./ssr-dev.js", + "ssr": "node --conditions=development ./ssr-dev.js", "build": "vite build --outDir dist/client && vite build --outDir dist/server --ssr ssr-prod.js", "prod": "npm run build && node dist/server/ssr-prod", "preview": "vite preview" From 1a0b822f4826b82454962a94c072519530b7c126 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 6 Dec 2024 17:41:41 -0500 Subject: [PATCH 16/62] fix: always run `if` block code the first time (#14597) * fix: always run `if` block code the first time * fix --- .changeset/rare-cheetahs-laugh.md | 5 +++++ .../svelte/src/internal/client/dom/blocks/if.js | 8 ++++---- .../samples/if-block-mismatch-2/_config.js | 15 +++++++++++++++ .../samples/if-block-mismatch-2/_expected.html | 1 + .../samples/if-block-mismatch-2/main.svelte | 13 +++++++++++++ .../samples/if-block-mismatch/_expected.html | 1 + .../samples/if-block-mismatch/main.svelte | 2 +- 7 files changed, 40 insertions(+), 5 deletions(-) create mode 100644 .changeset/rare-cheetahs-laugh.md create mode 100644 packages/svelte/tests/hydration/samples/if-block-mismatch-2/_config.js create mode 100644 packages/svelte/tests/hydration/samples/if-block-mismatch-2/_expected.html create mode 100644 packages/svelte/tests/hydration/samples/if-block-mismatch-2/main.svelte create mode 100644 packages/svelte/tests/hydration/samples/if-block-mismatch/_expected.html diff --git a/.changeset/rare-cheetahs-laugh.md b/.changeset/rare-cheetahs-laugh.md new file mode 100644 index 000000000000..2637b50b3c38 --- /dev/null +++ b/.changeset/rare-cheetahs-laugh.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: always run `if` block code the first time diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js index 6a880f28bc98..36790c05c135 100644 --- a/packages/svelte/src/internal/client/dom/blocks/if.js +++ b/packages/svelte/src/internal/client/dom/blocks/if.js @@ -9,7 +9,7 @@ import { set_hydrating } from '../hydration.js'; import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js'; -import { HYDRATION_START_ELSE } from '../../../../constants.js'; +import { HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js'; /** * @param {TemplateNode} node @@ -30,8 +30,8 @@ export function if_block(node, fn, elseif = false) { /** @type {Effect | null} */ var alternate_effect = null; - /** @type {boolean | null} */ - var condition = null; + /** @type {UNINITIALIZED | boolean | null} */ + var condition = UNINITIALIZED; var flags = elseif ? EFFECT_TRANSPARENT : 0; @@ -54,7 +54,7 @@ export function if_block(node, fn, elseif = false) { if (hydrating) { const is_else = /** @type {Comment} */ (anchor).data === HYDRATION_START_ELSE; - if (condition === is_else) { + if (!!condition === is_else) { // Hydration mismatch: remove everything inside the anchor and start fresh. // This could happen with `{#if browser}...{/if}`, for example anchor = remove_nodes(); diff --git a/packages/svelte/tests/hydration/samples/if-block-mismatch-2/_config.js b/packages/svelte/tests/hydration/samples/if-block-mismatch-2/_config.js new file mode 100644 index 000000000000..ffde9ee303b5 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/if-block-mismatch-2/_config.js @@ -0,0 +1,15 @@ +import { test } from '../../test'; + +// even {#if true} or {#if false} should be kept as an if block, because it could be {#if browser} originally, +// which is then different between client and server. +export default test({ + server_props: { + condition: true + }, + + props: { + condition: false + }, + + trim_whitespace: false +}); diff --git a/packages/svelte/tests/hydration/samples/if-block-mismatch-2/_expected.html b/packages/svelte/tests/hydration/samples/if-block-mismatch-2/_expected.html new file mode 100644 index 000000000000..08a3809de9cc --- /dev/null +++ b/packages/svelte/tests/hydration/samples/if-block-mismatch-2/_expected.html @@ -0,0 +1 @@ +
hello diff --git a/packages/svelte/tests/hydration/samples/if-block-mismatch-2/main.svelte b/packages/svelte/tests/hydration/samples/if-block-mismatch-2/main.svelte new file mode 100644 index 000000000000..3136406698b3 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/if-block-mismatch-2/main.svelte @@ -0,0 +1,13 @@ + + +{#if condition} + +{/if} + +
+ +
+ +hello diff --git a/packages/svelte/tests/hydration/samples/if-block-mismatch/_expected.html b/packages/svelte/tests/hydration/samples/if-block-mismatch/_expected.html new file mode 100644 index 000000000000..79cf2cf35f0f --- /dev/null +++ b/packages/svelte/tests/hydration/samples/if-block-mismatch/_expected.html @@ -0,0 +1 @@ +

foo

diff --git a/packages/svelte/tests/hydration/samples/if-block-mismatch/main.svelte b/packages/svelte/tests/hydration/samples/if-block-mismatch/main.svelte index c6799c5f95fc..552c43410162 100644 --- a/packages/svelte/tests/hydration/samples/if-block-mismatch/main.svelte +++ b/packages/svelte/tests/hydration/samples/if-block-mismatch/main.svelte @@ -2,7 +2,7 @@ let { condition } = $props(); -{#if true} +{#if condition}

foo

{:else}

bar

From 5771b455c0caf860bb063499feb2100acad4fbd5 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Sun, 8 Dec 2024 12:28:37 +0000 Subject: [PATCH 17/62] feat: add support for bind getter/setters (#14307) * feat: add support for bind getters/setters * different direction * oops * oops * build * add changeset and tests * move validation * add comment * build * bind:group error * simpler to just keep it as a SequenceExpression * fix * lint * fix * move validation to visitor * fix * no longer needed * fix * parser changes are no longer needed * simplify * simplify * update messages * docs --------- Co-authored-by: Rich Harris Co-authored-by: Simon Holthausen --- .changeset/slimy-donkeys-hang.md | 5 + .../docs/03-template-syntax/11-bind.md | 24 ++ .../98-reference/.generated/compile-errors.md | 14 +- .../messages/compile-errors/template.md | 10 +- packages/svelte/src/compiler/errors.js | 23 +- .../2-analyze/visitors/BindDirective.js | 214 ++++++++++-------- .../client/visitors/BindDirective.js | 74 +++--- .../client/visitors/RegularElement.js | 10 +- .../client/visitors/shared/component.js | 113 +++++---- .../client/visitors/shared/utils.js | 12 +- .../server/visitors/shared/component.js | 52 +++-- .../server/visitors/shared/element.js | 18 +- .../src/compiler/types/legacy-nodes.d.ts | 5 +- .../svelte/src/compiler/types/template.d.ts | 5 +- .../samples/bind-getter-setter-2/Child.svelte | 11 + .../samples/bind-getter-setter-2/_config.js | 9 + .../samples/bind-getter-setter-2/main.svelte | 11 + .../samples/bind-getter-setter/Child.svelte | 12 + .../samples/bind-getter-setter/_config.js | 20 ++ .../samples/bind-getter-setter/main.svelte | 16 ++ .../bind_group_invalid_expression/errors.json | 14 ++ .../input.svelte | 12 + packages/svelte/types/index.d.ts | 4 +- 23 files changed, 471 insertions(+), 217 deletions(-) create mode 100644 .changeset/slimy-donkeys-hang.md create mode 100644 packages/svelte/tests/runtime-runes/samples/bind-getter-setter-2/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/bind-getter-setter-2/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/bind-getter-setter-2/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/bind-getter-setter/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/bind-getter-setter/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/bind-getter-setter/main.svelte create mode 100644 packages/svelte/tests/validator/samples/bind_group_invalid_expression/errors.json create mode 100644 packages/svelte/tests/validator/samples/bind_group_invalid_expression/input.svelte diff --git a/.changeset/slimy-donkeys-hang.md b/.changeset/slimy-donkeys-hang.md new file mode 100644 index 000000000000..d63141660eed --- /dev/null +++ b/.changeset/slimy-donkeys-hang.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +feat: add support for bind getters/setters diff --git a/documentation/docs/03-template-syntax/11-bind.md b/documentation/docs/03-template-syntax/11-bind.md index fe3cf727e285..90046c8c456d 100644 --- a/documentation/docs/03-template-syntax/11-bind.md +++ b/documentation/docs/03-template-syntax/11-bind.md @@ -12,10 +12,34 @@ The general syntax is `bind:property={expression}`, where `expression` is an _lv ``` + Svelte creates an event listener that updates the bound value. If an element already has a listener for the same event, that listener will be fired before the bound value is updated. Most bindings are _two-way_, meaning that changes to the value will affect the element and vice versa. A few bindings are _readonly_, meaning that changing their value will have no effect on the element. +## Function bindings + +You can also use `bind:property={get, set}`, where `get` and `set` are functions, allowing you to perform validation and transformation: + +```svelte + value, + (v) => value = v.toLowerCase()} +/> +``` + +In the case of readonly bindings like [dimension bindings](#Dimensions), the `get` value should be `null`: + +```svelte +
...
+``` + +> [!NOTE] +> Function bindings are available in Svelte 5.9.0 and newer. + ## `` A `bind:value` directive on an `` element binds the input's `value` property: diff --git a/documentation/docs/98-reference/.generated/compile-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md index 3bd162d8d74d..d726d25fa188 100644 --- a/documentation/docs/98-reference/.generated/compile-errors.md +++ b/documentation/docs/98-reference/.generated/compile-errors.md @@ -78,10 +78,16 @@ Sequence expressions are not allowed as attribute/directive values in runes mode Attribute values containing `{...}` must be enclosed in quote marks, unless the value only contains the expression ``` +### bind_group_invalid_expression + +``` +`bind:group` can only bind to an Identifier or MemberExpression +``` + ### bind_invalid_expression ``` -Can only bind to an Identifier or MemberExpression +Can only bind to an Identifier or MemberExpression or a `{get, set}` pair ``` ### bind_invalid_name @@ -94,6 +100,12 @@ Can only bind to an Identifier or MemberExpression `bind:%name%` is not a valid binding. %explanation% ``` +### bind_invalid_parens + +``` +`bind:%name%={get, set}` must not have surrounding parentheses +``` + ### bind_invalid_target ``` diff --git a/packages/svelte/messages/compile-errors/template.md b/packages/svelte/messages/compile-errors/template.md index 9621a6457ba9..02961b61fccc 100644 --- a/packages/svelte/messages/compile-errors/template.md +++ b/packages/svelte/messages/compile-errors/template.md @@ -50,9 +50,13 @@ > Attribute values containing `{...}` must be enclosed in quote marks, unless the value only contains the expression +## bind_group_invalid_expression + +> `bind:group` can only bind to an Identifier or MemberExpression + ## bind_invalid_expression -> Can only bind to an Identifier or MemberExpression +> Can only bind to an Identifier or MemberExpression or a `{get, set}` pair ## bind_invalid_name @@ -60,6 +64,10 @@ > `bind:%name%` is not a valid binding. %explanation% +## bind_invalid_parens + +> `bind:%name%={get, set}` must not have surrounding parentheses + ## bind_invalid_target > `bind:%name%` can only be used with %elements% diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index 901ea1983ea7..1a4525ef5cb9 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -716,12 +716,21 @@ export function attribute_unquoted_sequence(node) { } /** - * Can only bind to an Identifier or MemberExpression + * `bind:group` can only bind to an Identifier or MemberExpression + * @param {null | number | NodeLike} node + * @returns {never} + */ +export function bind_group_invalid_expression(node) { + e(node, "bind_group_invalid_expression", "`bind:group` can only bind to an Identifier or MemberExpression"); +} + +/** + * Can only bind to an Identifier or MemberExpression or a `{get, set}` pair * @param {null | number | NodeLike} node * @returns {never} */ export function bind_invalid_expression(node) { - e(node, "bind_invalid_expression", "Can only bind to an Identifier or MemberExpression"); + e(node, "bind_invalid_expression", "Can only bind to an Identifier or MemberExpression or a `{get, set}` pair"); } /** @@ -735,6 +744,16 @@ export function bind_invalid_name(node, name, explanation) { e(node, "bind_invalid_name", explanation ? `\`bind:${name}\` is not a valid binding. ${explanation}` : `\`bind:${name}\` is not a valid binding`); } +/** + * `bind:%name%={get, set}` must not have surrounding parentheses + * @param {null | number | NodeLike} node + * @param {string} name + * @returns {never} + */ +export function bind_invalid_parens(node, name) { + e(node, "bind_invalid_parens", `\`bind:${name}={get, set}\` must not have surrounding parentheses`); +} + /** * `bind:%name%` can only be used with %elements% * @param {null | number | NodeLike} node diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js index 5b56d9ddac38..b06236538008 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js @@ -17,102 +17,6 @@ import { is_content_editable_binding, is_svg } from '../../../../utils.js'; * @param {Context} context */ export function BindDirective(node, context) { - validate_no_const_assignment(node, node.expression, context.state.scope, true); - - const assignee = node.expression; - const left = object(assignee); - - if (left === null) { - e.bind_invalid_expression(node); - } - - const binding = context.state.scope.get(left.name); - - if (assignee.type === 'Identifier') { - // reassignment - if ( - node.name !== 'this' && // bind:this also works for regular variables - (!binding || - (binding.kind !== 'state' && - binding.kind !== 'raw_state' && - binding.kind !== 'prop' && - binding.kind !== 'bindable_prop' && - binding.kind !== 'each' && - binding.kind !== 'store_sub' && - !binding.updated)) // TODO wut? - ) { - e.bind_invalid_value(node.expression); - } - - if (context.state.analysis.runes && binding?.kind === 'each') { - e.each_item_invalid_assignment(node); - } - - if (binding?.kind === 'snippet') { - e.snippet_parameter_assignment(node); - } - } - - if (node.name === 'group') { - if (!binding) { - throw new Error('Cannot find declaration for bind:group'); - } - - // Traverse the path upwards and find all EachBlocks who are (indirectly) contributing to bind:group, - // i.e. one of their declarations is referenced in the binding. This allows group bindings to work - // correctly when referencing a variable declared in an EachBlock by using the index of the each block - // entries as keys. - const each_blocks = []; - const [keypath, expression_ids] = extract_all_identifiers_from_expression(node.expression); - let ids = expression_ids; - - let i = context.path.length; - while (i--) { - const parent = context.path[i]; - - if (parent.type === 'EachBlock') { - const references = ids.filter((id) => parent.metadata.declarations.has(id.name)); - - if (references.length > 0) { - parent.metadata.contains_group_binding = true; - - each_blocks.push(parent); - ids = ids.filter((id) => !references.includes(id)); - ids.push(...extract_all_identifiers_from_expression(parent.expression)[1]); - } - } - } - - // The identifiers that make up the binding expression form they key for the binding group. - // If the same identifiers in the same order are used in another bind:group, they will be in the same group. - // (there's an edge case where `bind:group={a[i]}` will be in a different group than `bind:group={a[j]}` even when i == j, - // but this is a limitation of the current static analysis we do; it also never worked in Svelte 4) - const bindings = expression_ids.map((id) => context.state.scope.get(id.name)); - let group_name; - - outer: for (const [[key, b], group] of context.state.analysis.binding_groups) { - if (b.length !== bindings.length || key !== keypath) continue; - for (let i = 0; i < bindings.length; i++) { - if (bindings[i] !== b[i]) continue outer; - } - group_name = group; - } - - if (!group_name) { - group_name = context.state.scope.root.unique('binding_group'); - context.state.analysis.binding_groups.set([keypath, bindings], group_name); - } - - node.metadata = { - binding_group_name: group_name, - parent_each_blocks: each_blocks - }; - } - - if (binding?.kind === 'each' && binding.metadata?.inside_rest) { - w.bind_invalid_each_rest(binding.node, binding.node.name); - } - const parent = context.path.at(-1); if ( @@ -218,5 +122,123 @@ export function BindDirective(node, context) { } } + // When dealing with bind getters/setters skip the specific binding validation + // Group bindings aren't supported for getter/setters so we don't need to handle + // the metadata + if (node.expression.type === 'SequenceExpression') { + if (node.name === 'group') { + e.bind_group_invalid_expression(node); + } + + let i = /** @type {number} */ (node.expression.start); + while (context.state.analysis.source[--i] !== '{') { + if (context.state.analysis.source[i] === '(') { + e.bind_invalid_parens(node, node.name); + } + } + + if (node.expression.expressions.length !== 2) { + e.bind_invalid_expression(node); + } + + return; + } + + validate_no_const_assignment(node, node.expression, context.state.scope, true); + + const assignee = node.expression; + const left = object(assignee); + + if (left === null) { + e.bind_invalid_expression(node); + } + + const binding = context.state.scope.get(left.name); + + if (assignee.type === 'Identifier') { + // reassignment + if ( + node.name !== 'this' && // bind:this also works for regular variables + (!binding || + (binding.kind !== 'state' && + binding.kind !== 'raw_state' && + binding.kind !== 'prop' && + binding.kind !== 'bindable_prop' && + binding.kind !== 'each' && + binding.kind !== 'store_sub' && + !binding.updated)) // TODO wut? + ) { + e.bind_invalid_value(node.expression); + } + + if (context.state.analysis.runes && binding?.kind === 'each') { + e.each_item_invalid_assignment(node); + } + + if (binding?.kind === 'snippet') { + e.snippet_parameter_assignment(node); + } + } + + if (node.name === 'group') { + if (!binding) { + throw new Error('Cannot find declaration for bind:group'); + } + + // Traverse the path upwards and find all EachBlocks who are (indirectly) contributing to bind:group, + // i.e. one of their declarations is referenced in the binding. This allows group bindings to work + // correctly when referencing a variable declared in an EachBlock by using the index of the each block + // entries as keys. + const each_blocks = []; + const [keypath, expression_ids] = extract_all_identifiers_from_expression(node.expression); + let ids = expression_ids; + + let i = context.path.length; + while (i--) { + const parent = context.path[i]; + + if (parent.type === 'EachBlock') { + const references = ids.filter((id) => parent.metadata.declarations.has(id.name)); + + if (references.length > 0) { + parent.metadata.contains_group_binding = true; + + each_blocks.push(parent); + ids = ids.filter((id) => !references.includes(id)); + ids.push(...extract_all_identifiers_from_expression(parent.expression)[1]); + } + } + } + + // The identifiers that make up the binding expression form they key for the binding group. + // If the same identifiers in the same order are used in another bind:group, they will be in the same group. + // (there's an edge case where `bind:group={a[i]}` will be in a different group than `bind:group={a[j]}` even when i == j, + // but this is a limitation of the current static analysis we do; it also never worked in Svelte 4) + const bindings = expression_ids.map((id) => context.state.scope.get(id.name)); + let group_name; + + outer: for (const [[key, b], group] of context.state.analysis.binding_groups) { + if (b.length !== bindings.length || key !== keypath) continue; + for (let i = 0; i < bindings.length; i++) { + if (bindings[i] !== b[i]) continue outer; + } + group_name = group; + } + + if (!group_name) { + group_name = context.state.scope.root.unique('binding_group'); + context.state.analysis.binding_groups.set([keypath, bindings], group_name); + } + + node.metadata = { + binding_group_name: group_name, + parent_each_blocks: each_blocks + }; + } + + if (binding?.kind === 'each' && binding.metadata?.inside_rest) { + w.bind_invalid_each_rest(binding.node, binding.node.name); + } + context.next(); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/BindDirective.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/BindDirective.js index 79969240c771..f129e059f2a7 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/BindDirective.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/BindDirective.js @@ -1,4 +1,4 @@ -/** @import { CallExpression, Expression, MemberExpression } from 'estree' */ +/** @import { CallExpression, Expression, MemberExpression, Pattern } from 'estree' */ /** @import { AST, SvelteNode } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import { dev, is_ignored } from '../../../../state.js'; @@ -13,41 +13,50 @@ import { build_bind_this, validate_binding } from './shared/utils.js'; * @param {ComponentContext} context */ export function BindDirective(node, context) { - const expression = node.expression; + const expression = /** @type {Expression} */ (context.visit(node.expression)); const property = binding_properties[node.name]; const parent = /** @type {SvelteNode} */ (context.path.at(-1)); - if ( - dev && - context.state.analysis.runes && - expression.type === 'MemberExpression' && - (node.name !== 'this' || - context.path.some( - ({ type }) => - type === 'IfBlock' || type === 'EachBlock' || type === 'AwaitBlock' || type === 'KeyBlock' - )) && - !is_ignored(node, 'binding_property_non_reactive') - ) { - validate_binding( - context.state, - node, - /**@type {MemberExpression} */ (context.visit(expression)) - ); - } + let get, set; - const get = b.thunk(/** @type {Expression} */ (context.visit(expression))); + if (expression.type === 'SequenceExpression') { + [get, set] = expression.expressions; + } else { + if ( + dev && + context.state.analysis.runes && + expression.type === 'MemberExpression' && + (node.name !== 'this' || + context.path.some( + ({ type }) => + type === 'IfBlock' || + type === 'EachBlock' || + type === 'AwaitBlock' || + type === 'KeyBlock' + )) && + !is_ignored(node, 'binding_property_non_reactive') + ) { + validate_binding(context.state, node, expression); + } - /** @type {Expression | undefined} */ - let set = b.unthunk( - b.arrow( - [b.id('$$value')], - /** @type {Expression} */ (context.visit(b.assignment('=', expression, b.id('$$value')))) - ) - ); + get = b.thunk(expression); - if (get === set) { - set = undefined; + /** @type {Expression | undefined} */ + set = b.unthunk( + b.arrow( + [b.id('$$value')], + /** @type {Expression} */ ( + context.visit( + b.assignment('=', /** @type {Pattern} */ (node.expression), b.id('$$value')) + ) + ) + ) + ); + + if (get === set) { + set = undefined; + } } /** @type {CallExpression} */ @@ -162,7 +171,7 @@ export function BindDirective(node, context) { break; case 'this': - call = build_bind_this(expression, context.state.node, context); + call = build_bind_this(node.expression, context.state.node, context); break; case 'textContent': @@ -213,10 +222,7 @@ export function BindDirective(node, context) { if (value !== undefined) { group_getter = b.thunk( - b.block([ - b.stmt(build_attribute_value(value, context).value), - b.return(/** @type {Expression} */ (context.visit(expression))) - ]) + b.block([b.stmt(build_attribute_value(value, context).value), b.return(expression)]) ); } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js index 3c0be589c363..2c2c287f1275 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js @@ -450,6 +450,11 @@ function setup_select_synchronization(value_binding, context) { if (context.state.analysis.runes) return; let bound = value_binding.expression; + + if (bound.type === 'SequenceExpression') { + return; + } + while (bound.type === 'MemberExpression') { bound = /** @type {Identifier | MemberExpression} */ (bound.object); } @@ -484,10 +489,7 @@ function setup_select_synchronization(value_binding, context) { b.call( '$.template_effect', b.thunk( - b.block([ - b.stmt(/** @type {Expression} */ (context.visit(value_binding.expression))), - b.stmt(invalidator) - ]) + b.block([b.stmt(/** @type {Expression} */ (context.visit(bound))), b.stmt(invalidator)]) ) ) ) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js index aa7be93cb57e..c94c1e1b0ec8 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js @@ -1,4 +1,4 @@ -/** @import { BlockStatement, Expression, ExpressionStatement, Identifier, MemberExpression, Property, Statement } from 'estree' */ +/** @import { BlockStatement, Expression, ExpressionStatement, Identifier, MemberExpression, Pattern, Property, SequenceExpression, Statement } from 'estree' */ /** @import { AST, TemplateNode } from '#compiler' */ /** @import { ComponentContext } from '../../types.js' */ import { dev, is_ignored } from '../../../../../state.js'; @@ -44,7 +44,7 @@ export function build_component(node, component_name, context, anchor = context. /** @type {Property[]} */ const custom_css_props = []; - /** @type {Identifier | MemberExpression | null} */ + /** @type {Identifier | MemberExpression | SequenceExpression | null} */ let bind_this = null; /** @type {ExpressionStatement[]} */ @@ -174,60 +174,83 @@ export function build_component(node, component_name, context, anchor = context. } else if (attribute.type === 'BindDirective') { const expression = /** @type {Expression} */ (context.visit(attribute.expression)); - if ( - dev && - expression.type === 'MemberExpression' && - context.state.analysis.runes && - !is_ignored(node, 'binding_property_non_reactive') - ) { - validate_binding(context.state, attribute, expression); + if (dev && attribute.name !== 'this' && attribute.expression.type !== 'SequenceExpression') { + const left = object(attribute.expression); + let binding; + + if (left?.type === 'Identifier') { + binding = context.state.scope.get(left.name); + } + + // Only run ownership addition on $state fields. + // Theoretically someone could create a `$state` while creating `$state.raw` or inside a `$derived.by`, + // but that feels so much of an edge case that it doesn't warrant a perf hit for the common case. + if (binding?.kind !== 'derived' && binding?.kind !== 'raw_state') { + binding_initializers.push( + b.stmt( + b.call( + b.id('$.add_owner_effect'), + b.thunk(expression), + b.id(component_name), + is_ignored(node, 'ownership_invalid_binding') && b.true + ) + ) + ); + } } - if (attribute.name === 'this') { - bind_this = attribute.expression; + if (expression.type === 'SequenceExpression') { + if (attribute.name === 'this') { + bind_this = attribute.expression; + } else { + const [get, set] = expression.expressions; + const get_id = b.id(context.state.scope.generate('bind_get')); + const set_id = b.id(context.state.scope.generate('bind_set')); + + context.state.init.push(b.var(get_id, get)); + context.state.init.push(b.var(set_id, set)); + + push_prop(b.get(attribute.name, [b.return(b.call(get_id))])); + push_prop(b.set(attribute.name, [b.stmt(b.call(set_id, b.id('$$value')))])); + } } else { - if (dev) { - const left = object(attribute.expression); - let binding; - if (left?.type === 'Identifier') { - binding = context.state.scope.get(left.name); - } - // Only run ownership addition on $state fields. - // Theoretically someone could create a `$state` while creating `$state.raw` or inside a `$derived.by`, - // but that feels so much of an edge case that it doesn't warrant a perf hit for the common case. - if (binding?.kind !== 'derived' && binding?.kind !== 'raw_state') { - binding_initializers.push( - b.stmt( - b.call( - b.id('$.add_owner_effect'), - b.thunk(expression), - b.id(component_name), - is_ignored(node, 'ownership_invalid_binding') && b.true - ) - ) + if ( + dev && + expression.type === 'MemberExpression' && + context.state.analysis.runes && + !is_ignored(node, 'binding_property_non_reactive') + ) { + validate_binding(context.state, attribute, expression); + } + + if (attribute.name === 'this') { + bind_this = attribute.expression; + } else { + const is_store_sub = + attribute.expression.type === 'Identifier' && + context.state.scope.get(attribute.expression.name)?.kind === 'store_sub'; + + // Delay prop pushes so bindings come at the end, to avoid spreads overwriting them + if (is_store_sub) { + push_prop( + b.get(attribute.name, [b.stmt(b.call('$.mark_store_binding')), b.return(expression)]), + true ); + } else { + push_prop(b.get(attribute.name, [b.return(expression)]), true); } - } - const is_store_sub = - attribute.expression.type === 'Identifier' && - context.state.scope.get(attribute.expression.name)?.kind === 'store_sub'; + const assignment = b.assignment( + '=', + /** @type {Pattern} */ (attribute.expression), + b.id('$$value') + ); - // Delay prop pushes so bindings come at the end, to avoid spreads overwriting them - if (is_store_sub) { push_prop( - b.get(attribute.name, [b.stmt(b.call('$.mark_store_binding')), b.return(expression)]), + b.set(attribute.name, [b.stmt(/** @type {Expression} */ (context.visit(assignment)))]), true ); - } else { - push_prop(b.get(attribute.name, [b.return(expression)]), true); } - - const assignment = b.assignment('=', attribute.expression, b.id('$$value')); - push_prop( - b.set(attribute.name, [b.stmt(/** @type {Expression} */ (context.visit(assignment)))]), - true - ); } } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js index 59beacbb0c30..11f76aa025e7 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js @@ -1,4 +1,4 @@ -/** @import { Expression, ExpressionStatement, Identifier, MemberExpression, Statement, Super } from 'estree' */ +/** @import { Expression, ExpressionStatement, Identifier, MemberExpression, SequenceExpression, Statement, Super } from 'estree' */ /** @import { AST, SvelteNode } from '#compiler' */ /** @import { ComponentClientTransformState } from '../../types' */ import { walk } from 'zimmerframe'; @@ -143,11 +143,16 @@ export function build_update_assignment(state, id, init, value, update) { /** * Serializes `bind:this` for components and elements. - * @param {Identifier | MemberExpression} expression + * @param {Identifier | MemberExpression | SequenceExpression} expression * @param {Expression} value * @param {import('zimmerframe').Context} context */ export function build_bind_this(expression, value, { state, visit }) { + if (expression.type === 'SequenceExpression') { + const [get, set] = /** @type {SequenceExpression} */ (visit(expression)).expressions; + return b.call('$.bind_this', value, set, get); + } + /** @type {Identifier[]} */ const ids = []; @@ -224,6 +229,9 @@ export function build_bind_this(expression, value, { state, visit }) { * @param {MemberExpression} expression */ export function validate_binding(state, binding, expression) { + if (binding.expression.type === 'SequenceExpression') { + return; + } // If we are referencing a $store.foo then we don't need to add validation const left = object(binding.expression); const left_binding = left && state.scope.get(left.name); diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js index 7cabfb06c527..0d0444433564 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js @@ -1,4 +1,4 @@ -/** @import { BlockStatement, Expression, Pattern, Property, Statement } from 'estree' */ +/** @import { BlockStatement, Expression, Pattern, Property, SequenceExpression, Statement } from 'estree' */ /** @import { AST, TemplateNode } from '#compiler' */ /** @import { ComponentContext } from '../../types.js' */ import { empty_comment, build_attribute_value } from './utils.js'; @@ -92,24 +92,38 @@ export function build_inline_component(node, expression, context) { const value = build_attribute_value(attribute.value, context, false, true); push_prop(b.prop('init', b.key(attribute.name), value)); } else if (attribute.type === 'BindDirective' && attribute.name !== 'this') { - // Delay prop pushes so bindings come at the end, to avoid spreads overwriting them - push_prop( - b.get(attribute.name, [ - b.return(/** @type {Expression} */ (context.visit(attribute.expression))) - ]), - true - ); - push_prop( - b.set(attribute.name, [ - b.stmt( - /** @type {Expression} */ ( - context.visit(b.assignment('=', attribute.expression, b.id('$$value'))) - ) - ), - b.stmt(b.assignment('=', b.id('$$settled'), b.false)) - ]), - true - ); + if (attribute.expression.type === 'SequenceExpression') { + const [get, set] = /** @type {SequenceExpression} */ (context.visit(attribute.expression)) + .expressions; + const get_id = b.id(context.state.scope.generate('bind_get')); + const set_id = b.id(context.state.scope.generate('bind_set')); + + context.state.init.push(b.var(get_id, get)); + context.state.init.push(b.var(set_id, set)); + + push_prop(b.get(attribute.name, [b.return(b.call(get_id))])); + push_prop(b.set(attribute.name, [b.stmt(b.call(set_id, b.id('$$value')))])); + } else { + // Delay prop pushes so bindings come at the end, to avoid spreads overwriting them + push_prop( + b.get(attribute.name, [ + b.return(/** @type {Expression} */ (context.visit(attribute.expression))) + ]), + true + ); + + push_prop( + b.set(attribute.name, [ + b.stmt( + /** @type {Expression} */ ( + context.visit(b.assignment('=', attribute.expression, b.id('$$value'))) + ) + ), + b.stmt(b.assignment('=', b.id('$$settled'), b.false)) + ]), + true + ); + } } } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js index 434447727b33..d626bb08db30 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js @@ -110,14 +110,17 @@ export function build_element_attributes(node, context) { const binding = binding_properties[attribute.name]; if (binding?.omit_in_ssr) continue; + let expression = /** @type {Expression} */ (context.visit(attribute.expression)); + + if (expression.type === 'SequenceExpression') { + expression = b.call(expression.expressions[0]); + } + if (is_content_editable_binding(attribute.name)) { - content = /** @type {Expression} */ (context.visit(attribute.expression)); + content = expression; } else if (attribute.name === 'value' && node.name === 'textarea') { - content = b.call( - '$.escape', - /** @type {Expression} */ (context.visit(attribute.expression)) - ); - } else if (attribute.name === 'group') { + content = b.call('$.escape', expression); + } else if (attribute.name === 'group' && attribute.expression.type !== 'SequenceExpression') { const value_attribute = /** @type {AST.Attribute | undefined} */ ( node.attributes.find((attr) => attr.type === 'Attribute' && attr.name === 'value') ); @@ -130,6 +133,7 @@ export function build_element_attributes(node, context) { is_text_attribute(attr) && attr.value[0].data === 'checkbox' ); + attributes.push( create_attribute('checked', -1, -1, [ { @@ -159,7 +163,7 @@ export function build_element_attributes(node, context) { type: 'ExpressionTag', start: -1, end: -1, - expression: attribute.expression, + expression, metadata: { expression: create_expression_metadata() } diff --git a/packages/svelte/src/compiler/types/legacy-nodes.d.ts b/packages/svelte/src/compiler/types/legacy-nodes.d.ts index 2bd5fbbfa6d2..0013f5c17a60 100644 --- a/packages/svelte/src/compiler/types/legacy-nodes.d.ts +++ b/packages/svelte/src/compiler/types/legacy-nodes.d.ts @@ -6,7 +6,8 @@ import type { Identifier, MemberExpression, ObjectExpression, - Pattern + Pattern, + SequenceExpression } from 'estree'; interface BaseNode { @@ -49,7 +50,7 @@ export interface LegacyBinding extends BaseNode { /** The 'x' in `bind:x` */ name: string; /** The y in `bind:x={y}` */ - expression: Identifier | MemberExpression; + expression: Identifier | MemberExpression | SequenceExpression; } export interface LegacyBody extends BaseElement { diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index a409cf570489..b8724f28dc94 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -14,7 +14,8 @@ import type { Pattern, Program, ChainExpression, - SimpleCallExpression + SimpleCallExpression, + SequenceExpression } from 'estree'; import type { Scope } from '../phases/scope'; @@ -187,7 +188,7 @@ export namespace AST { /** The 'x' in `bind:x` */ name: string; /** The y in `bind:x={y}` */ - expression: Identifier | MemberExpression; + expression: Identifier | MemberExpression | SequenceExpression; /** @internal */ metadata: { binding_group_name: Identifier; diff --git a/packages/svelte/tests/runtime-runes/samples/bind-getter-setter-2/Child.svelte b/packages/svelte/tests/runtime-runes/samples/bind-getter-setter-2/Child.svelte new file mode 100644 index 000000000000..0026309d449f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-getter-setter-2/Child.svelte @@ -0,0 +1,11 @@ + + +
div, v => div = v}>123
diff --git a/packages/svelte/tests/runtime-runes/samples/bind-getter-setter-2/_config.js b/packages/svelte/tests/runtime-runes/samples/bind-getter-setter-2/_config.js new file mode 100644 index 000000000000..1d51c8eead1e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-getter-setter-2/_config.js @@ -0,0 +1,9 @@ +import { test } from '../../test'; + +export default test({ + async test({ assert, target, logs }) { + assert.htmlEqual(target.innerHTML, `
123
`); + + assert.deepEqual(logs, ['123', '123']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/bind-getter-setter-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/bind-getter-setter-2/main.svelte new file mode 100644 index 000000000000..21646e745a4e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-getter-setter-2/main.svelte @@ -0,0 +1,11 @@ + + + child, v => child = v} /> diff --git a/packages/svelte/tests/runtime-runes/samples/bind-getter-setter/Child.svelte b/packages/svelte/tests/runtime-runes/samples/bind-getter-setter/Child.svelte new file mode 100644 index 000000000000..bea5849ec76b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-getter-setter/Child.svelte @@ -0,0 +1,12 @@ + + + a, + (v) => { + console.log('b', v); + a = v; + }} +/> diff --git a/packages/svelte/tests/runtime-runes/samples/bind-getter-setter/_config.js b/packages/svelte/tests/runtime-runes/samples/bind-getter-setter/_config.js new file mode 100644 index 000000000000..158d1e6f63e0 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-getter-setter/_config.js @@ -0,0 +1,20 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; +import { assert_ok } from '../../../suite'; + +export default test({ + async test({ assert, target, logs }) { + const input = target.querySelector('input'); + + assert_ok(input); + + input.value = '2'; + input.dispatchEvent(new window.Event('input')); + + flushSync(); + + assert.htmlEqual(target.innerHTML, ``); + + assert.deepEqual(logs, ['b', '2', 'a', '2']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/bind-getter-setter/main.svelte b/packages/svelte/tests/runtime-runes/samples/bind-getter-setter/main.svelte new file mode 100644 index 000000000000..191a4234764d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-getter-setter/main.svelte @@ -0,0 +1,16 @@ + + + + + a, + (v) => { + console.log('a', v); + a = v; + }} +/> + diff --git a/packages/svelte/tests/validator/samples/bind_group_invalid_expression/errors.json b/packages/svelte/tests/validator/samples/bind_group_invalid_expression/errors.json new file mode 100644 index 000000000000..f85363106b01 --- /dev/null +++ b/packages/svelte/tests/validator/samples/bind_group_invalid_expression/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "bind_group_invalid_expression", + "message": "`bind:group` can only bind to an Identifier or MemberExpression", + "start": { + "line": 8, + "column": 38 + }, + "end": { + "line": 8, + "column": 84 + } + } +] diff --git a/packages/svelte/tests/validator/samples/bind_group_invalid_expression/input.svelte b/packages/svelte/tests/validator/samples/bind_group_invalid_expression/input.svelte new file mode 100644 index 000000000000..3f8afe76550f --- /dev/null +++ b/packages/svelte/tests/validator/samples/bind_group_invalid_expression/input.svelte @@ -0,0 +1,12 @@ + + +{#each values as value} + +{/each} + +

{selected.name}

diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 0d761919a8e0..61a34dcb8e93 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -606,7 +606,7 @@ declare module 'svelte/animate' { } declare module 'svelte/compiler' { - import type { Expression, Identifier, ArrayExpression, ArrowFunctionExpression, VariableDeclaration, VariableDeclarator, MemberExpression, ObjectExpression, Pattern, Program, ChainExpression, SimpleCallExpression } from 'estree'; + import type { Expression, Identifier, ArrayExpression, ArrowFunctionExpression, VariableDeclaration, VariableDeclarator, MemberExpression, ObjectExpression, Pattern, Program, ChainExpression, SimpleCallExpression, SequenceExpression } from 'estree'; import type { SourceMap } from 'magic-string'; import type { Location } from 'locate-character'; /** @@ -1047,7 +1047,7 @@ declare module 'svelte/compiler' { /** The 'x' in `bind:x` */ name: string; /** The y in `bind:x={y}` */ - expression: Identifier | MemberExpression; + expression: Identifier | MemberExpression | SequenceExpression; } /** A `class:` directive */ From 57f8ca6e3c13d680d642bf7938d2a869e04043dc Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 8 Dec 2024 07:31:14 -0500 Subject: [PATCH 18/62] oops --- .changeset/slimy-donkeys-hang.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/slimy-donkeys-hang.md b/.changeset/slimy-donkeys-hang.md index d63141660eed..b491d78b4c9e 100644 --- a/.changeset/slimy-donkeys-hang.md +++ b/.changeset/slimy-donkeys-hang.md @@ -1,5 +1,5 @@ --- -'svelte': patch +'svelte': minor --- feat: add support for bind getters/setters From 301744f1f7f45946f799f82e63f072303e740071 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 8 Dec 2024 07:32:51 -0500 Subject: [PATCH 19/62] Version Packages (#14598) Co-authored-by: github-actions[bot] --- .changeset/rare-cheetahs-laugh.md | 5 ----- .changeset/slimy-donkeys-hang.md | 5 ----- packages/svelte/CHANGELOG.md | 10 ++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) delete mode 100644 .changeset/rare-cheetahs-laugh.md delete mode 100644 .changeset/slimy-donkeys-hang.md diff --git a/.changeset/rare-cheetahs-laugh.md b/.changeset/rare-cheetahs-laugh.md deleted file mode 100644 index 2637b50b3c38..000000000000 --- a/.changeset/rare-cheetahs-laugh.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: always run `if` block code the first time diff --git a/.changeset/slimy-donkeys-hang.md b/.changeset/slimy-donkeys-hang.md deleted file mode 100644 index b491d78b4c9e..000000000000 --- a/.changeset/slimy-donkeys-hang.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': minor ---- - -feat: add support for bind getters/setters diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 4031110fa799..fea90ca0eab6 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,15 @@ # svelte +## 5.9.0 + +### Minor Changes + +- feat: add support for bind getters/setters ([#14307](https://github.com/sveltejs/svelte/pull/14307)) + +### Patch Changes + +- fix: always run `if` block code the first time ([#14597](https://github.com/sveltejs/svelte/pull/14597)) + ## 5.8.1 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index c751a598db53..e5afd8e13065 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.8.1", + "version": "5.9.0", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 3061318cb034..f5369fe169e8 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -6,5 +6,5 @@ * https://svelte.dev/docs/svelte-compiler#svelte-version * @type {string} */ -export const VERSION = '5.8.1'; +export const VERSION = '5.9.0'; export const PUBLIC_VERSION = '5'; From c1c59e77a54109e6dd868e8ee7884caf9a275f5a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 8 Dec 2024 07:38:01 -0500 Subject: [PATCH 20/62] docs: where the hell did this come from? (#14613) --- documentation/docs/03-template-syntax/03-each.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/documentation/docs/03-template-syntax/03-each.md b/documentation/docs/03-template-syntax/03-each.md index df0ba4d8f59c..70666f6a5798 100644 --- a/documentation/docs/03-template-syntax/03-each.md +++ b/documentation/docs/03-template-syntax/03-each.md @@ -23,8 +23,6 @@ Iterating over values can be done with an each block. The values in question can ``` -You can use each blocks to iterate over any array or array-like value — that is, any object with a `length` property. - An each block can also specify an _index_, equivalent to the second argument in an `array.map(...)` callback: ```svelte From c66bf178aae18c2b4d8ac189a48cf10c47e4d417 Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Mon, 9 Dec 2024 13:39:31 +0100 Subject: [PATCH 21/62] fix: mark subtree dynamic for bind with sequence expressions (#14626) --- .changeset/green-pandas-study.md | 5 +++++ .../phases/2-analyze/visitors/BindDirective.js | 3 +++ .../samples/bind-getter-setter/_config.js | 14 ++++++++++---- .../samples/bind-getter-setter/main.svelte | 9 +++++++++ 4 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 .changeset/green-pandas-study.md diff --git a/.changeset/green-pandas-study.md b/.changeset/green-pandas-study.md new file mode 100644 index 000000000000..869599055cb0 --- /dev/null +++ b/.changeset/green-pandas-study.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: mark subtree dynamic for bind with sequence expressions diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js index b06236538008..b4de1925df24 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js @@ -11,6 +11,7 @@ import * as w from '../../../warnings.js'; import { binding_properties } from '../../bindings.js'; import fuzzymatch from '../../1-parse/utils/fuzzymatch.js'; import { is_content_editable_binding, is_svg } from '../../../../utils.js'; +import { mark_subtree_dynamic } from './shared/fragment.js'; /** * @param {AST.BindDirective} node @@ -141,6 +142,8 @@ export function BindDirective(node, context) { e.bind_invalid_expression(node); } + mark_subtree_dynamic(context.path); + return; } diff --git a/packages/svelte/tests/runtime-runes/samples/bind-getter-setter/_config.js b/packages/svelte/tests/runtime-runes/samples/bind-getter-setter/_config.js index 158d1e6f63e0..dd5c387405e0 100644 --- a/packages/svelte/tests/runtime-runes/samples/bind-getter-setter/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/bind-getter-setter/_config.js @@ -4,17 +4,23 @@ import { assert_ok } from '../../../suite'; export default test({ async test({ assert, target, logs }) { - const input = target.querySelector('input'); - - assert_ok(input); + const [input, checkbox] = target.querySelectorAll('input'); input.value = '2'; input.dispatchEvent(new window.Event('input')); flushSync(); - assert.htmlEqual(target.innerHTML, ``); + assert.htmlEqual( + target.innerHTML, + `
` + ); assert.deepEqual(logs, ['b', '2', 'a', '2']); + + flushSync(() => { + checkbox.click(); + }); + assert.deepEqual(logs, ['b', '2', 'a', '2', 'check', false]); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/bind-getter-setter/main.svelte b/packages/svelte/tests/runtime-runes/samples/bind-getter-setter/main.svelte index 191a4234764d..f6d908fba196 100644 --- a/packages/svelte/tests/runtime-runes/samples/bind-getter-setter/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/bind-getter-setter/main.svelte @@ -2,6 +2,7 @@ import Child from './Child.svelte'; let a = $state(0); + let check = $state(true); @@ -14,3 +15,11 @@ }} /> +
+ check, + (v)=>{ + console.log('check', v); + check = v; + }} /> +
\ No newline at end of file From 0a10c59517e77a0ed0b9fb51fab44a450a3710e7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2024 09:29:51 -0500 Subject: [PATCH 22/62] Version Packages (#14628) Co-authored-by: github-actions[bot] --- .changeset/green-pandas-study.md | 5 ----- packages/svelte/CHANGELOG.md | 6 ++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .changeset/green-pandas-study.md diff --git a/.changeset/green-pandas-study.md b/.changeset/green-pandas-study.md deleted file mode 100644 index 869599055cb0..000000000000 --- a/.changeset/green-pandas-study.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: mark subtree dynamic for bind with sequence expressions diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index fea90ca0eab6..00ef29347533 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.9.1 + +### Patch Changes + +- fix: mark subtree dynamic for bind with sequence expressions ([#14626](https://github.com/sveltejs/svelte/pull/14626)) + ## 5.9.0 ### Minor Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index e5afd8e13065..5d662af00c3f 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.9.0", + "version": "5.9.1", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index f5369fe169e8..20ff578fbeac 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -6,5 +6,5 @@ * https://svelte.dev/docs/svelte-compiler#svelte-version * @type {string} */ -export const VERSION = '5.9.0'; +export const VERSION = '5.9.1'; export const PUBLIC_VERSION = '5'; From 38171f60ead8d702f50f6b5c23633d2ae4d85be6 Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Mon, 9 Dec 2024 16:48:34 +0100 Subject: [PATCH 23/62] fix: allow exports with source from script module even if no bind is present (#14620) * fix: allow exports with source from script module even if no bind is present * chore: move test to validator --- .changeset/four-carrots-burn.md | 5 +++++ packages/svelte/src/compiler/phases/2-analyze/index.js | 2 +- .../export-not-defined-module-with-source/errors.json | 1 + .../export-not-defined-module-with-source/input.svelte | 3 +++ 4 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 .changeset/four-carrots-burn.md create mode 100644 packages/svelte/tests/validator/samples/export-not-defined-module-with-source/errors.json create mode 100644 packages/svelte/tests/validator/samples/export-not-defined-module-with-source/input.svelte diff --git a/.changeset/four-carrots-burn.md b/.changeset/four-carrots-burn.md new file mode 100644 index 000000000000..39cefcc4b76e --- /dev/null +++ b/.changeset/four-carrots-burn.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: allow exports with source from script module even if no bind is present diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 8f1efd7f635f..9e29813ee336 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -698,7 +698,7 @@ export function analyze_component(root, source, options) { } for (const node of analysis.module.ast.body) { - if (node.type === 'ExportNamedDeclaration' && node.specifiers !== null) { + if (node.type === 'ExportNamedDeclaration' && node.specifiers !== null && node.source == null) { for (const specifier of node.specifiers) { if (specifier.local.type !== 'Identifier') continue; diff --git a/packages/svelte/tests/validator/samples/export-not-defined-module-with-source/errors.json b/packages/svelte/tests/validator/samples/export-not-defined-module-with-source/errors.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/packages/svelte/tests/validator/samples/export-not-defined-module-with-source/errors.json @@ -0,0 +1 @@ +[] diff --git a/packages/svelte/tests/validator/samples/export-not-defined-module-with-source/input.svelte b/packages/svelte/tests/validator/samples/export-not-defined-module-with-source/input.svelte new file mode 100644 index 000000000000..df50ebc1fa66 --- /dev/null +++ b/packages/svelte/tests/validator/samples/export-not-defined-module-with-source/input.svelte @@ -0,0 +1,3 @@ + From 11764632b9d64621bfbf86cd1d3a65adda1dfd09 Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Mon, 9 Dec 2024 17:22:42 +0100 Subject: [PATCH 24/62] fix: deconflict `get_name` for literal class properties (#14607) --- .changeset/stupid-buckets-drum.md | 5 ++++ .../3-transform/client/visitors/ClassBody.js | 24 +++++++++++++++---- .../_config.js | 3 +++ .../main.svelte | 6 +++++ 4 files changed, 34 insertions(+), 4 deletions(-) create mode 100644 .changeset/stupid-buckets-drum.md create mode 100644 packages/svelte/tests/runtime-runes/samples/class-state-conflicting-get-name/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/class-state-conflicting-get-name/main.svelte diff --git a/.changeset/stupid-buckets-drum.md b/.changeset/stupid-buckets-drum.md new file mode 100644 index 000000000000..57d6f015f786 --- /dev/null +++ b/.changeset/stupid-buckets-drum.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: deconflict `get_name` for literal class properties diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js index 5e842a82febf..7b3a9a4d0e29 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js @@ -23,6 +23,9 @@ export function ClassBody(node, context) { /** @type {Map} */ const private_state = new Map(); + /** @type {Map<(MethodDefinition|PropertyDefinition)["key"], string>} */ + const definition_names = new Map(); + /** @type {string[]} */ const private_ids = []; @@ -34,9 +37,12 @@ export function ClassBody(node, context) { definition.key.type === 'Literal') ) { const type = definition.key.type; - const name = get_name(definition.key); + const name = get_name(definition.key, public_state); if (!name) continue; + // we store the deconflicted name in the map so that we can access it later + definition_names.set(definition.key, name); + const is_private = type === 'PrivateIdentifier'; if (is_private) private_ids.push(name); @@ -96,7 +102,7 @@ export function ClassBody(node, context) { definition.key.type === 'PrivateIdentifier' || definition.key.type === 'Literal') ) { - const name = get_name(definition.key); + const name = definition_names.get(definition.key); if (!name) continue; const is_private = definition.key.type === 'PrivateIdentifier'; @@ -210,10 +216,20 @@ export function ClassBody(node, context) { /** * @param {Identifier | PrivateIdentifier | Literal} node + * @param {Map} public_state */ -function get_name(node) { +function get_name(node, public_state) { if (node.type === 'Literal') { - return node.value?.toString().replace(regex_invalid_identifier_chars, '_'); + let name = node.value?.toString().replace(regex_invalid_identifier_chars, '_'); + + // the above could generate conflicts because it has to generate a valid identifier + // so stuff like `0` and `1` or `state%` and `state^` will result in the same string + // so we have to de-conflict. We can only check `public_state` because private state + // can't have literal keys + while (name && public_state.has(name)) { + name = '_' + name; + } + return name; } else { return node.name; } diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-conflicting-get-name/_config.js b/packages/svelte/tests/runtime-runes/samples/class-state-conflicting-get-name/_config.js new file mode 100644 index 000000000000..f47bee71df87 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-state-conflicting-get-name/_config.js @@ -0,0 +1,3 @@ +import { test } from '../../test'; + +export default test({}); diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-conflicting-get-name/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-state-conflicting-get-name/main.svelte new file mode 100644 index 000000000000..aec1e67cc675 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-state-conflicting-get-name/main.svelte @@ -0,0 +1,6 @@ + \ No newline at end of file From c6fca0200981a8be0a73f9602803b37f8ff1c45b Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Mon, 9 Dec 2024 18:39:13 +0100 Subject: [PATCH 25/62] docs: more details for errors/warnings on the site (#14632) * docs: more details for errors/warnings on the site Related to #11305 * Apply suggestions from code review Co-authored-by: Rich Harris * fix in correct place * tab not spaces * tweaks * fix * Apply suggestions from code review * regenerate --------- Co-authored-by: Rich Harris --- .../.generated/client-warnings.md | 84 +++++++++++++ .../.generated/compile-warnings.md | 117 ++++++++++++++++++ .../98-reference/.generated/server-errors.md | 2 + .../98-reference/.generated/shared-errors.md | 42 +++++++ .../.generated/shared-warnings.md | 9 ++ .../messages/client-warnings/warnings.md | 84 +++++++++++++ .../messages/compile-warnings/script.md | 91 ++++++++++++++ .../svelte/messages/compile-warnings/style.md | 14 +++ .../messages/compile-warnings/template.md | 12 ++ .../messages/server-errors/lifecycle.md | 2 + .../svelte/messages/shared-errors/errors.md | 42 +++++++ .../messages/shared-warnings/warnings.md | 9 ++ 12 files changed, 508 insertions(+) diff --git a/documentation/docs/98-reference/.generated/client-warnings.md b/documentation/docs/98-reference/.generated/client-warnings.md index ef19a28994bd..b0490b84ffd9 100644 --- a/documentation/docs/98-reference/.generated/client-warnings.md +++ b/documentation/docs/98-reference/.generated/client-warnings.md @@ -66,6 +66,31 @@ The easiest way to log a value as it changes over time is to use the [`$inspect` The `%attribute%` attribute on `%html%` changed its value between server and client renders. The client value, `%value%`, will be ignored in favour of the server value ``` +Certain attributes like `src` on an `` element will not be repaired during hydration, i.e. the server value will be kept. That's because updating these attributes can cause the image to be refetched (or in the case of an ` -
- - diff --git a/sites/svelte-5-preview/src/lib/Output/PaneWithPanel.svelte b/sites/svelte-5-preview/src/lib/Output/PaneWithPanel.svelte deleted file mode 100644 index 9018a50bee48..000000000000 --- a/sites/svelte-5-preview/src/lib/Output/PaneWithPanel.svelte +++ /dev/null @@ -1,83 +0,0 @@ - - - -
- -
- -
-
- - -
- -
- -
-
-
- - diff --git a/sites/svelte-5-preview/src/lib/Output/ReplProxy.js b/sites/svelte-5-preview/src/lib/Output/ReplProxy.js deleted file mode 100644 index 0e45887bd727..000000000000 --- a/sites/svelte-5-preview/src/lib/Output/ReplProxy.js +++ /dev/null @@ -1,96 +0,0 @@ -let uid = 1; - -export default class ReplProxy { - /** @type {HTMLIFrameElement} */ - iframe; - - /** @type {import("./proxy").Handlers} */ - handlers; - - /** @type {Map void, reject: (value: any) => void }>} */ - pending_cmds = new Map(); - - /** @param {MessageEvent} event */ - handle_event = (event) => { - if (event.source !== this.iframe.contentWindow) return; - - const { action, args } = event.data; - - switch (action) { - case 'cmd_error': - case 'cmd_ok': - return this.handle_command_message(event.data); - case 'fetch_progress': - return this.handlers.on_fetch_progress(args.remaining); - case 'error': - return this.handlers.on_error(event.data); - case 'unhandledrejection': - return this.handlers.on_unhandled_rejection(event.data); - case 'console': - return this.handlers.on_console(event.data); - } - }; - - /** - * @param {HTMLIFrameElement} iframe - * @param {import("./proxy").Handlers} handlers - */ - constructor(iframe, handlers) { - this.iframe = iframe; - this.handlers = handlers; - - window.addEventListener('message', this.handle_event, false); - } - - destroy() { - window.removeEventListener('message', this.handle_event); - } - - /** - * @param {string} action - * @param {any} args - */ - iframe_command(action, args) { - return new Promise((resolve, reject) => { - const cmd_id = uid++; - - this.pending_cmds.set(cmd_id, { resolve, reject }); - - this.iframe.contentWindow?.postMessage({ action, cmd_id, args }, '*'); - }); - } - - /** - * @param {{ action: string; cmd_id: number; message: string; stack: any; args: any; }} cmd_data - */ - handle_command_message(cmd_data) { - let action = cmd_data.action; - let id = cmd_data.cmd_id; - let handler = this.pending_cmds.get(id); - - if (handler) { - this.pending_cmds.delete(id); - if (action === 'cmd_error') { - let { message, stack } = cmd_data; - let e = new Error(message); - e.stack = stack; - handler.reject(e); - } - - if (action === 'cmd_ok') { - handler.resolve(cmd_data.args); - } - } else { - console.error('command not found', id, cmd_data, [...this.pending_cmds.keys()]); - } - } - - /** @param {string} script */ - eval(script) { - return this.iframe_command('eval', { script }); - } - - handle_links() { - return this.iframe_command('catch_clicks', {}); - } -} diff --git a/sites/svelte-5-preview/src/lib/Output/Viewer.svelte b/sites/svelte-5-preview/src/lib/Output/Viewer.svelte deleted file mode 100644 index db506ada3bfd..000000000000 --- a/sites/svelte-5-preview/src/lib/Output/Viewer.svelte +++ /dev/null @@ -1,337 +0,0 @@ - - -
- -
- - - {#if $bundle?.error} - - {/if} -
- -
- -
- -
- -
-
- -
- {#if error} - - {:else if status || !$bundle} - {status || 'loading Svelte compiler...'} - {/if} -
-
- - diff --git a/sites/svelte-5-preview/src/lib/Output/console/Console.svelte b/sites/svelte-5-preview/src/lib/Output/console/Console.svelte deleted file mode 100644 index e2a880aff8bb..000000000000 --- a/sites/svelte-5-preview/src/lib/Output/console/Console.svelte +++ /dev/null @@ -1,46 +0,0 @@ - - -
- {#each logs as log} - - {/each} -
- - diff --git a/sites/svelte-5-preview/src/lib/Output/console/ConsoleLine.svelte b/sites/svelte-5-preview/src/lib/Output/console/ConsoleLine.svelte deleted file mode 100644 index 36635c713049..000000000000 --- a/sites/svelte-5-preview/src/lib/Output/console/ConsoleLine.svelte +++ /dev/null @@ -1,292 +0,0 @@ - - -{#if log.command === 'table'} - -{/if} - -
- -
- {#if log.count && log.count > 1} - {log.count} - {/if} - - {#if log.stack || log.command === 'group'} - {'\u25B6'} - {/if} - - {#if log.command === 'clear'} - Console was cleared - {:else if log.command === 'unclonable'} - Message could not be cloned. Open devtools to see it - {:else if log.command === 'table'} - - {:else} - - {#each format_args(log.args) as part} - - {#if !part.formatted} - {' '} - {/if}{#if part.type === 'value'} - - {:else} - {part.value} - {/if} - {/each} - - {/if} -
- - {#if log.stack && !log.collapsed} -
- {#each log.stack as line} - {line.label} - {line.location} - {/each} -
- {/if} - - {#each new Array(depth) as _, idx} -
- {/each} -
- -{#if log.command === 'group' && !log.collapsed} - {#each log.logs ?? [] as childLog} - - {/each} -{/if} - - diff --git a/sites/svelte-5-preview/src/lib/Output/console/ConsoleTable.svelte b/sites/svelte-5-preview/src/lib/Output/console/ConsoleTable.svelte deleted file mode 100644 index cba9721807c7..000000000000 --- a/sites/svelte-5-preview/src/lib/Output/console/ConsoleTable.svelte +++ /dev/null @@ -1,145 +0,0 @@ - - -
- - - - - - {#each table.columns as column} - - {/each} - - - - - {#each table.rows as row} - - - - {#each row.values as value} - - {/each} - - {/each} - -
(index){column}
- {#if typeof row.key === 'string'} - {row.key} - {:else} - - {/if} - - {#if typeof value === 'string'} - {value} - {:else} - - {/if} -
-
- - diff --git a/sites/svelte-5-preview/src/lib/Output/console/console.d.ts b/sites/svelte-5-preview/src/lib/Output/console/console.d.ts deleted file mode 100644 index 540e0b3b020a..000000000000 --- a/sites/svelte-5-preview/src/lib/Output/console/console.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -export type Log = { - command: 'info' | 'warn' | 'error' | 'table' | 'group' | 'clear' | 'unclonable'; - action?: 'console'; - args?: any[]; - collapsed?: boolean; - expanded?: boolean; - count?: number; - logs?: Log[]; - stack?: Array<{ - label?: string; - location?: string; - }>; - data?: any; - columns?: string[]; -}; diff --git a/sites/svelte-5-preview/src/lib/Output/get-location-from-stack.js b/sites/svelte-5-preview/src/lib/Output/get-location-from-stack.js deleted file mode 100644 index 3ac3e457d6b3..000000000000 --- a/sites/svelte-5-preview/src/lib/Output/get-location-from-stack.js +++ /dev/null @@ -1,42 +0,0 @@ -import { decode } from '@jridgewell/sourcemap-codec'; - -/** - * @param {string} stack - * @param {import('@jridgewell/sourcemap-codec').SourceMapMappings} map - * @returns - */ -export default function getLocationFromStack(stack, map) { - if (!stack) return; - const last = stack.split('\n')[1]; - const match = /:(\d+):(\d+)\)$/.exec(last); - - if (!match) return null; - - const line = +match[1]; - const column = +match[2]; - - return trace({ line, column }, map); -} - -/** - * - * @param {Omit} loc - * @param {*} map - * @returns - */ -function trace(loc, map) { - const mappings = decode(map.mappings); - const segments = mappings[loc.line - 1]; - - for (let i = 0; i < segments.length; i += 1) { - const segment = segments[i]; - if (segment[0] === loc.column) { - const [, sourceIndex, line, column] = segment; - const source = map.sources[sourceIndex ?? 0].slice(2); - - return { source, line: (line ?? 0) + 1, column }; - } - } - - return null; -} diff --git a/sites/svelte-5-preview/src/lib/Output/proxy.d.ts b/sites/svelte-5-preview/src/lib/Output/proxy.d.ts deleted file mode 100644 index b3f9fa8d1ae4..000000000000 --- a/sites/svelte-5-preview/src/lib/Output/proxy.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type Handlers = Record< - 'on_fetch_progress' | 'on_error' | 'on_unhandled_rejection' | 'on_console', - (data: any) => void ->; diff --git a/sites/svelte-5-preview/src/lib/Output/srcdoc/index.html b/sites/svelte-5-preview/src/lib/Output/srcdoc/index.html deleted file mode 100644 index 202a5f973a04..000000000000 --- a/sites/svelte-5-preview/src/lib/Output/srcdoc/index.html +++ /dev/null @@ -1,287 +0,0 @@ - - - - - - - - - diff --git a/sites/svelte-5-preview/src/lib/Repl.svelte b/sites/svelte-5-preview/src/lib/Repl.svelte deleted file mode 100644 index f43f5f899b84..000000000000 --- a/sites/svelte-5-preview/src/lib/Repl.svelte +++ /dev/null @@ -1,438 +0,0 @@ - - - - -
-
- -
- - -
- -
- -
-
-
- - {#if $toggleable} - - {/if} -
- - diff --git a/sites/svelte-5-preview/src/lib/autocomplete.js b/sites/svelte-5-preview/src/lib/autocomplete.js deleted file mode 100644 index b535c7ff0416..000000000000 --- a/sites/svelte-5-preview/src/lib/autocomplete.js +++ /dev/null @@ -1,209 +0,0 @@ -import { snippetCompletion } from '@codemirror/autocomplete'; -import { syntaxTree } from '@codemirror/language'; - -/** @typedef {(node: import('@lezer/common').SyntaxNode, context: import('@codemirror/autocomplete').CompletionContext, selected: import('./types').File) => boolean} Test */ - -/** - * Returns `true` if `$bindable()` is valid - * @type {Test} - */ -function is_bindable(node, context) { - // disallow outside `let { x = $bindable }` - if (node.parent?.name !== 'PatternProperty') return false; - if (node.parent.parent?.name !== 'ObjectPattern') return false; - if (node.parent.parent.parent?.name !== 'VariableDeclaration') return false; - - let last = node.parent.parent.parent.lastChild; - if (!last) return true; - - // if the declaration is incomplete, assume the best - if (last.name === 'ObjectPattern' || last.name === 'Equals' || last.name === '⚠') { - return true; - } - - if (last.name === ';') { - last = last.prevSibling; - if (!last || last.name === '⚠') return true; - } - - // if the declaration is complete, only return true if it is a `$props()` declaration - return ( - last.name === 'CallExpression' && - last.firstChild?.name === 'VariableName' && - context.state.sliceDoc(last.firstChild.from, last.firstChild.to) === '$props' - ); -} - -/** - * Returns `true` if `$props()` is valid - * TODO only allow in `.svelte` files, and only at the top level - * @type {Test} - */ -function is_props(node, _, selected) { - if (selected.type !== 'svelte') return false; - - return ( - node.name === 'VariableName' && - node.parent?.name === 'VariableDeclaration' && - node.parent.parent?.name === 'Script' - ); -} - -/** - * Returns `true` is this is a valid place to declare state - * @type {Test} - */ -function is_state(node) { - let parent = node.parent; - - if (node.name === '.' || node.name === 'PropertyName') { - if (parent?.name !== 'MemberExpression') return false; - parent = parent.parent; - } - - if (!parent) return false; - - return parent.name === 'VariableDeclaration' || parent.name === 'PropertyDeclaration'; -} - -/** - * Returns `true` if we're already in a valid call expression, e.g. - * changing an existing `$state()` to `$state.raw()` - * @type {Test} - */ -function is_state_call(node) { - let parent = node.parent; - - if (node.name === '.' || node.name === 'PropertyName') { - if (parent?.name !== 'MemberExpression') return false; - parent = parent.parent; - } - - if (parent?.name !== 'CallExpression') { - return false; - } - - parent = parent.parent; - if (!parent) return false; - - return parent.name === 'VariableDeclaration' || parent.name === 'PropertyDeclaration'; -} - -/** @type {Test} */ -function is_statement(node) { - if (node.name === 'VariableName') { - return node.parent?.name === 'ExpressionStatement'; - } - - if (node.name === '.' || node.name === 'PropertyName') { - return node.parent?.parent?.name === 'ExpressionStatement'; - } - - return false; -} - -/** @type {Array<{ snippet: string, test?: Test }>} */ -const runes = [ - { snippet: '$state(${})', test: is_state }, - { snippet: '$state', test: is_state_call }, - { snippet: '$props()', test: is_props }, - { snippet: '$derived(${});', test: is_state }, - { snippet: '$derived', test: is_state_call }, - { snippet: '$derived.by(() => {\n\t${}\n});', test: is_state }, - { snippet: '$derived.by', test: is_state_call }, - { snippet: '$effect(() => {\n\t${}\n});', test: is_statement }, - { snippet: '$effect.pre(() => {\n\t${}\n});', test: is_statement }, - { snippet: '$state.raw(${});', test: is_state }, - { snippet: '$state.raw', test: is_state_call }, - { snippet: '$bindable()', test: is_bindable }, - { snippet: '$effect.root(() => {\n\t${}\n})' }, - { snippet: '$state.snapshot(${})' }, - { snippet: '$effect.tracking()' }, - { snippet: '$inspect(${});', test: is_statement } -]; - -const options = runes.map(({ snippet, test }, i) => ({ - option: snippetCompletion(snippet, { - type: 'keyword', - boost: runes.length - i, - label: snippet.includes('(') ? snippet.slice(0, snippet.indexOf('(')) : snippet - }), - test -})); - -/** - * @param {import('@codemirror/autocomplete').CompletionContext} context - * @param {import('./types.js').File} selected - * @param {import('./types.js').File[]} files - */ -export function autocomplete(context, selected, files) { - let node = syntaxTree(context.state).resolveInner(context.pos, -1); - - if (node.name === 'String' && node.parent?.name === 'ImportDeclaration') { - const modules = [ - 'svelte', - 'svelte/animate', - 'svelte/easing', - 'svelte/events', - 'svelte/legacy', - 'svelte/motion', - 'svelte/reactivity', - 'svelte/store', - 'svelte/transition' - ]; - - for (const file of files) { - if (file === selected) continue; - modules.push(`./${file.name}.${file.type}`); - } - - return { - from: node.from + 1, - options: modules.map((label) => ({ - label, - type: 'string' - })) - }; - } - - if ( - selected.type !== 'svelte' && - (selected.type !== 'js' || !selected.name.endsWith('.svelte')) - ) { - return false; - } - - if (node.name === 'VariableName' || node.name === 'PropertyName' || node.name === '.') { - // special case — `$inspect(...).with(...)` is the only rune that 'returns' - // an 'object' with a 'method' - if (node.name === 'PropertyName' || node.name === '.') { - if ( - node.parent?.name === 'MemberExpression' && - node.parent.firstChild?.name === 'CallExpression' && - node.parent.firstChild.firstChild?.name === 'VariableName' && - context.state.sliceDoc( - node.parent.firstChild.firstChild.from, - node.parent.firstChild.firstChild.to - ) === '$inspect' - ) { - const open = context.matchBefore(/\.\w*/); - if (!open) return null; - - return { - from: open.from, - options: [snippetCompletion('.with(${})', { type: 'keyword', label: '.with' })] - }; - } - } - - const open = context.matchBefore(/\$[\w\.]*/); - if (!open) return null; - - return { - from: open.from, - options: options - .filter((option) => (option.test ? option.test(node, context, selected) : true)) - .map((option) => option.option) - }; - } -} diff --git a/sites/svelte-5-preview/src/lib/context.js b/sites/svelte-5-preview/src/lib/context.js deleted file mode 100644 index e983e6d147af..000000000000 --- a/sites/svelte-5-preview/src/lib/context.js +++ /dev/null @@ -1,13 +0,0 @@ -import { getContext, setContext } from 'svelte'; - -const key = Symbol('repl'); - -/** @returns {import("./types").ReplContext} */ -export function get_repl_context() { - return getContext(key); -} - -/** @param {import("./types").ReplContext} value */ -export function set_repl_context(value) { - setContext(key, value); -} diff --git a/sites/svelte-5-preview/src/lib/index.js b/sites/svelte-5-preview/src/lib/index.js deleted file mode 100644 index 969b64140852..000000000000 --- a/sites/svelte-5-preview/src/lib/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './Repl.svelte'; diff --git a/sites/svelte-5-preview/src/lib/theme.js b/sites/svelte-5-preview/src/lib/theme.js deleted file mode 100644 index 867e144acc50..000000000000 --- a/sites/svelte-5-preview/src/lib/theme.js +++ /dev/null @@ -1,153 +0,0 @@ -import { HighlightStyle, syntaxHighlighting } from '@codemirror/language'; -import { EditorView } from '@codemirror/view'; -import { tags as t } from '@lezer/highlight'; - -const ERROR_HUE = 0; -const WARNING_HUE = 40; - -const WARNING_FG = `hsl(${WARNING_HUE} 100% 60%)`; -const WARNING_BG = `hsl(${WARNING_HUE} 100% 40% / 0.5)`; - -const ERROR_FG = `hsl(${ERROR_HUE} 100% 40%)`; -const ERROR_BG = `hsl(${ERROR_HUE} 100% 40% / 0.5)`; - -/** - * @param {string} content - * @param {string} attrs - */ -function svg(content, attrs = `viewBox="0 0 40 40"`) { - return `url('data:image/svg+xml,${encodeURIComponent( - content - )}')`; -} - -/** - * @param {string} color - */ -function underline(color) { - return svg( - ``, - `width="6" height="4"` - ); -} - -const svelteThemeStyles = EditorView.theme( - { - '&': { - color: 'var(--sk-code-base)', - backgroundColor: 'transparent' - }, - - '.cm-content': { - caretColor: 'var(--sk-theme-3)' - }, - - '.cm-cursor, .cm-dropCursor': { borderLeftColor: 'var(--sk-theme-3)' }, - '&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': - { backgroundColor: 'var(--sk-selection-color)' }, - - '.cm-panels': { backgroundColor: 'var(--sk-back-2)', color: 'var(--sk-text-2)' }, - '.cm-panels.cm-panels-top': { borderBottom: '2px solid black' }, - '.cm-panels.cm-panels-bottom': { borderTop: '2px solid black' }, - - '.cm-searchMatch': { - backgroundColor: 'var(--sk-theme-2)' - // outline: '1px solid #457dff', - }, - '.cm-searchMatch.cm-searchMatch-selected': { - backgroundColor: '#6199ff2f' - }, - - '.cm-activeLine': { backgroundColor: '#6699ff0b' }, - '.cm-selectionMatch': { backgroundColor: '#aafe661a' }, - - '&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': { - backgroundColor: '#bad0f847' - }, - - '.cm-gutters': { - backgroundColor: 'var(--sk-back-3)', - border: 'none' - }, - - '.cm-activeLineGutter': { - backgroundColor: 'var(--sk-back-4)' - }, - - '.cm-foldPlaceholder': { - backgroundColor: 'transparent', - border: 'none', - color: '#ddd' - }, - - // https://github.com/codemirror/lint/blob/271b35f5d31a7e3645eaccbfec608474022098e1/src/lint.ts#L620 - '.cm-lintRange': { - backgroundPosition: 'left bottom', - backgroundRepeat: 'repeat-x', - paddingBottom: '4px' - }, - '.cm-lintRange-error': { - backgroundImage: underline(ERROR_FG) - }, - '.cm-lintRange-warning': { - backgroundImage: underline(WARNING_FG) - }, - '.cm-tooltip .cm-tooltip-arrow:before': { - borderTopColor: 'transparent', - borderBottomColor: 'transparent' - }, - '.cm-tooltip .cm-tooltip-arrow:after': { - borderTopColor: 'var(--sk-back-3)', - borderBottomColor: 'var(--sk-back-3)' - }, - '.cm-tooltip-autocomplete': { - color: 'var(--sk-text-2) !important', - perspective: '1px', - '& > ul > li[aria-selected]': { - backgroundColor: 'var(--sk-back-4)', - color: 'var(--sk-text-1) !important' - } - } - }, - { dark: true } -); - -/// The highlighting style for code in the One Dark theme. -const svelteHighlightStyle = HighlightStyle.define([ - { tag: t.keyword, color: 'var(--sk-code-keyword)' }, - { - tag: [t.name, t.deleted, t.character, t.propertyName, t.macroName], - color: 'var(--sk-code-base)' - }, - { tag: [t.function(t.variableName), t.labelName], color: 'var(--sk-code-tags)' }, - { tag: [t.color, t.constant(t.name), t.standard(t.name)], color: 'var(--sk-code-base)' }, - { tag: [t.definition(t.name), t.separator], color: 'var(--sk-code-base)' }, - { - tag: [ - t.typeName, - t.className, - t.number, - t.changed, - t.annotation, - t.modifier, - t.self, - t.namespace - ], - color: 'var(--sk-code-tags)' - }, - { - tag: [t.operator, t.operatorKeyword, t.url, t.escape, t.regexp, t.link, t.special(t.string)], - color: 'var(--sk-code-base)' - }, - { tag: [t.meta, t.comment], color: 'var(--sk-code-comment)' }, - { tag: t.strong, fontWeight: 'bold' }, - { tag: t.emphasis, fontStyle: 'italic' }, - { tag: t.strikethrough, textDecoration: 'line-through' }, - { tag: t.link, color: 'var(--sk-code-base)', textDecoration: 'underline' }, - { tag: t.heading, fontWeight: 'bold', color: 'var(--sk-text-1)' }, - { tag: [t.atom, t.bool], color: 'var(--sk-code-atom)' }, - { tag: [t.processingInstruction, t.string, t.inserted], color: 'var(--sk-code-string)' }, - { tag: t.invalid, color: '#ff008c' } -]); - -export const svelteTheme = [svelteThemeStyles, syntaxHighlighting(svelteHighlightStyle)]; diff --git a/sites/svelte-5-preview/src/lib/types.d.ts b/sites/svelte-5-preview/src/lib/types.d.ts deleted file mode 100644 index a758846d293b..000000000000 --- a/sites/svelte-5-preview/src/lib/types.d.ts +++ /dev/null @@ -1,77 +0,0 @@ -import type { EditorState } from '@codemirror/state'; -import { OutputChunk, RollupError } from '@rollup/browser'; -import type { Readable, Writable } from 'svelte/store'; -import type { CompileOptions, CompileError } from 'svelte/compiler'; - -export type Lang = 'js' | 'svelte' | 'json' | 'md' | 'css' | (string & Record); - -type StartOrEnd = { - line: number; - column: number; - character: number; -}; - -export type MessageDetails = { - start: StartOrEnd; - end: StartOrEnd; - filename: string; - message: string; -}; - -export type Warning = MessageDetails; - -export type Bundle = { - uid: number; - client: OutputChunk | null; - error: (RollupError & CompileError) | null; - server: OutputChunk | null; - imports: string[]; - warnings: Warning[]; -}; - -export type File = { - name: string; - source: string; - type: Lang; - modified?: boolean; -}; - -export type ReplState = { - files: File[]; - selected_name: string; - selected: File | null; - bundle: Bundle | null; - bundling: Promise; - bundler: import('./Bundler').default | null; - compile_options: CompileOptions; - cursor_pos: number; - toggleable: boolean; - module_editor: import('./CodeMirror.svelte').default | null; -}; - -export type ReplContext = { - files: Writable; - selected_name: Writable; - selected: Readable; - bundle: Writable; - bundling: Writable; - bundler: Writable; - compile_options: Writable; - cursor_pos: Writable; - toggleable: Writable; - module_editor: Writable; - - EDITOR_STATE_MAP: Map; - - // Methods - rebundle(): Promise; - migrate(): Promise; - handle_select(filename: string): Promise; - handle_change( - event: CustomEvent<{ - value: string; - }> - ): Promise; - go_to_warning_pos(item?: MessageDetails): Promise; - clear_state(): void; -}; diff --git a/sites/svelte-5-preview/src/lib/utils.js b/sites/svelte-5-preview/src/lib/utils.js deleted file mode 100644 index d378e1d51744..000000000000 --- a/sites/svelte-5-preview/src/lib/utils.js +++ /dev/null @@ -1,16 +0,0 @@ -/** - * @param {number} min - * @param {number} max - * @param {number} value - */ -export const clamp = (min, max, value) => Math.max(min, Math.min(max, value)); - -/** - * @param {number} ms - */ -export const sleep = (ms) => new Promise((f) => setTimeout(f, ms)); - -/** @param {import('./types').File} file */ -export function get_full_filename(file) { - return `${file.name}.${file.type}`; -} diff --git a/sites/svelte-5-preview/src/lib/workers/bundler/index.js b/sites/svelte-5-preview/src/lib/workers/bundler/index.js deleted file mode 100644 index 5a289fff7dcf..000000000000 --- a/sites/svelte-5-preview/src/lib/workers/bundler/index.js +++ /dev/null @@ -1,576 +0,0 @@ -/// - -import '../patch_window.js'; -import { sleep } from '$lib/utils.js'; -import { rollup } from '@rollup/browser'; -import { DEV } from 'esm-env'; -import * as resolve from 'resolve.exports'; -import commonjs from './plugins/commonjs.js'; -import glsl from './plugins/glsl.js'; -import json from './plugins/json.js'; -import replace from './plugins/replace.js'; -import loop_protect from './plugins/loop-protect.js'; - -/** @type {string} */ -var pkg_name; - -/** @type {string} */ -let packages_url; - -/** @type {string} */ -let svelte_url; - -/** @type {number} */ -let current_id; - -/** @type {(arg?: never) => void} */ -let fulfil_ready; -const ready = new Promise((f) => { - fulfil_ready = f; -}); - -/** - * @type {{ - * compile: typeof import('svelte/compiler').compile; - * compileModule: typeof import('svelte/compiler').compileModule; - * VERSION: string; - * }} - */ -let svelte; - -self.addEventListener( - 'message', - /** @param {MessageEvent} event */ async (event) => { - switch (event.data.type) { - case 'init': { - ({ packages_url, svelte_url } = event.data); - - const { version } = await fetch(`${svelte_url}/package.json`).then((r) => r.json()); - console.log(`Using Svelte compiler version ${version}`); - - const compiler = await fetch(`${svelte_url}/compiler/index.js`).then((r) => r.text()); - (0, eval)(compiler + '\n//# sourceURL=compiler/index.js@' + version); - - svelte = globalThis.svelte; - - fulfil_ready(); - break; - } - - case 'bundle': { - await ready; - const { uid, files } = event.data; - - if (files.length === 0) return; - - current_id = uid; - - setTimeout(async () => { - if (current_id !== uid) return; - - const result = await bundle({ uid, files }); - - if (JSON.stringify(result.error) === JSON.stringify(ABORT)) return; - if (result && uid === current_id) postMessage(result); - }); - - break; - } - } - } -); - -/** @type {Record<'client' | 'server', Map }>>} */ -let cached = { - client: new Map(), - server: new Map() -}; - -const ABORT = { aborted: true }; - -/** @type {Map>} */ -const FETCH_CACHE = new Map(); - -/** - * @param {string} url - * @param {number} uid - */ -async function fetch_if_uncached(url, uid) { - if (FETCH_CACHE.has(url)) { - return FETCH_CACHE.get(url); - } - - // TODO: investigate whether this is necessary - await sleep(50); - if (uid !== current_id) throw ABORT; - - const promise = fetch(url) - .then(async (r) => { - if (!r.ok) throw new Error(await r.text()); - - return { - url: r.url, - body: await r.text() - }; - }) - .catch((err) => { - FETCH_CACHE.delete(url); - throw err; - }); - - FETCH_CACHE.set(url, promise); - return promise; -} - -/** - * @param {string} url - * @param {number} uid - */ -async function follow_redirects(url, uid) { - const res = await fetch_if_uncached(url, uid); - return res?.url; -} - -/** - * - * @param {number} major - * @param {number} minor - * @param {number} patch - * @returns {number} - */ -function compare_to_version(major, minor, patch) { - const v = svelte.VERSION.match(/^(\d+)\.(\d+)\.(\d+)/); - - // @ts-ignore - return +v[1] - major || +v[2] - minor || +v[3] - patch; -} - -function is_v4() { - return compare_to_version(4, 0, 0) >= 0; -} - -function is_v5() { - return compare_to_version(5, 0, 0) >= 0; -} - -function is_legacy_package_structure() { - return compare_to_version(3, 4, 4) <= 0; -} - -function has_loopGuardTimeout_feature() { - return compare_to_version(3, 14, 0) >= 0; -} - -/** - * - * @param {Record} pkg - * @param {string} subpath - * @param {number} uid - * @param {string} pkg_url_base - */ -async function resolve_from_pkg(pkg, subpath, uid, pkg_url_base) { - // match legacy Rollup logic — pkg.svelte takes priority over pkg.exports - if (typeof pkg.svelte === 'string' && subpath === '.') { - return pkg.svelte; - } - - // modern - if (pkg.exports) { - try { - const [resolved] = - resolve.exports(pkg, subpath, { - browser: true, - conditions: ['svelte', 'development'] - }) ?? []; - - return resolved; - } catch { - throw `no matched export path was found in "${pkg_name}/package.json"`; - } - } - - // legacy - if (subpath === '.') { - let resolved_id = resolve.legacy(pkg, { - fields: ['browser', 'module', 'main'] - }); - - if (typeof resolved_id === 'object' && !Array.isArray(resolved_id)) { - const subpath = resolved_id['.']; - if (subpath === false) return 'data:text/javascript,export {}'; - - resolved_id = - subpath ?? - resolve.legacy(pkg, { - fields: ['module', 'main'] - }); - } - - if (!resolved_id) { - // last ditch — try to match index.js/index.mjs - for (const index_file of ['index.mjs', 'index.js']) { - try { - const indexUrl = new URL(index_file, `${pkg_url_base}/`).href; - return (await follow_redirects(indexUrl, uid)) ?? ''; - } catch { - // maybe the next option will be successful - } - } - - throw `could not find entry point in "${pkg_name}/package.json"`; - } - - return resolved_id; - } - - if (typeof pkg.browser === 'object') { - // this will either return `pkg.browser[subpath]` or `subpath` - return resolve.legacy(pkg, { - browser: subpath - }); - } - - return subpath; -} - -/** - * @param {number} uid - * @param {'client' | 'server'} mode - * @param {typeof cached['client']} cache - * @param {Map} local_files_lookup - */ -async function get_bundle(uid, mode, cache, local_files_lookup) { - let bundle; - - /** A set of package names (without subpaths) to include in pkg.devDependencies when downloading an app */ - /** @type {Set} */ - const imports = new Set(); - - /** @type {import('$lib/types.js').Warning[]} */ - const warnings = []; - - /** @type {{ message: string }[]} */ - const all_warnings = []; - - /** @type {typeof cache} */ - const new_cache = new Map(); - - /** @type {import('@rollup/browser').Plugin} */ - const repl_plugin = { - name: 'svelte-repl', - async resolveId(importee, importer) { - if (uid !== current_id) throw ABORT; - - if (importee === 'esm-env') return importee; - - const v5 = is_v5(); - const v4 = !v5 && is_v4(); - - if (!v5) { - // importing from Svelte - if (importee === `svelte`) - return v4 ? `${svelte_url}/src/runtime/index.js` : `${svelte_url}/index.mjs`; - - if (importee.startsWith(`svelte/`)) { - const sub_path = importee.slice(7); - if (v4) { - return `${svelte_url}/src/runtime/${sub_path}/index.js`; - } - - return is_legacy_package_structure() - ? `${svelte_url}/${sub_path}.mjs` - : `${svelte_url}/${sub_path}/index.mjs`; - } - } - - // importing from another file in REPL - if (local_files_lookup.has(importee) && (!importer || local_files_lookup.has(importer))) - return importee; - if (local_files_lookup.has(importee + '.js')) return importee + '.js'; - if (local_files_lookup.has(importee + '.json')) return importee + '.json'; - - // remove trailing slash - if (importee.endsWith('/')) importee = importee.slice(0, -1); - - // importing from a URL - if (/^https?:/.test(importee)) return importee; - - if (importee.startsWith('.')) { - if (importer && local_files_lookup.has(importer)) { - // relative import in a REPL file - // should've matched above otherwise importee doesn't exist - console.error(`Cannot find file "${importee}" imported by "${importer}" in the REPL`); - return; - } else { - // relative import in an external file - const url = new URL(importee, importer).href; - self.postMessage({ type: 'status', uid, message: `resolving ${url}` }); - - return await follow_redirects(url, uid); - } - } else { - // fetch from unpkg - self.postMessage({ type: 'status', uid, message: `resolving ${importee}` }); - - const match = /^((?:@[^/]+\/)?[^/]+)(\/.+)?$/.exec(importee); - if (!match) { - return console.error(`Invalid import "${importee}"`); - } - - const pkg_name = match[1]; - const subpath = `.${match[2] ?? ''}`; - - // if this was imported by one of our files, add it to the `imports` set - if (importer && local_files_lookup.has(importer)) { - imports.add(pkg_name); - } - - const fetch_package_info = async () => { - try { - const pkg_url = await follow_redirects( - `${pkg_name === 'svelte' ? '' : packages_url}/${pkg_name}/package.json`, - uid - ); - - if (!pkg_url) throw new Error(); - - const pkg_json = (await fetch_if_uncached(pkg_url, uid))?.body; - const pkg = JSON.parse(pkg_json ?? '""'); - - const pkg_url_base = pkg_url.replace(/\/package\.json$/, ''); - - return { - pkg, - pkg_url_base - }; - } catch (_e) { - throw new Error(`Error fetching "${pkg_name}" from unpkg. Does the package exist?`); - } - }; - - const { pkg, pkg_url_base } = await fetch_package_info(); - - try { - const resolved_id = await resolve_from_pkg(pkg, subpath, uid, pkg_url_base); - return new URL(resolved_id + '', `${pkg_url_base}/`).href; - } catch (reason) { - throw new Error(`Cannot import "${importee}": ${reason}.`); - } - } - }, - async load(resolved) { - if (uid !== current_id) throw ABORT; - - if (resolved === 'esm-env') { - return `export const BROWSER = true; export const DEV = true`; - } - - const cached_file = local_files_lookup.get(resolved); - if (cached_file) return cached_file.source; - - if (!FETCH_CACHE.has(resolved)) { - self.postMessage({ type: 'status', uid, message: `fetching ${resolved}` }); - } - - const res = await fetch_if_uncached(resolved, uid); - return res?.body; - }, - transform(code, id) { - if (uid !== current_id) throw ABORT; - - self.postMessage({ type: 'status', uid, message: `bundling ${id}` }); - - if (!/\.(svelte|js)$/.test(id)) return null; - - const name = id.split('/').pop()?.split('.')[0]; - - const cached_id = cache.get(id); - let result; - - if (cached_id && cached_id.code === code) { - result = cached_id.result; - } else if (id.endsWith('.svelte')) { - result = svelte.compile(code, { - filename: name + '.svelte', - generate: 'client', - dev: true - }); - - if (result.css) { - result.js.code += - '\n\n' + - ` - const $$__style = document.createElement('style'); - $$__style.textContent = ${JSON.stringify(result.css.code)}; - document.head.append($$__style); - `.replace(/\t/g, ''); - } - } else if (id.endsWith('.svelte.js')) { - result = svelte.compileModule(code, { - filename: name + '.js', - generate: 'client', - dev: true - }); - if (!result) { - return null; - } - } else { - return null; - } - - new_cache.set(id, { code, result }); - - // @ts-expect-error - (result.warnings || result.stats?.warnings)?.forEach((warning) => { - // This is required, otherwise postMessage won't work - // @ts-ignore - delete warning.toString; - // TODO remove stats post-launch - // @ts-ignore - warnings.push(warning); - }); - - /** @type {import('@rollup/browser').TransformResult} */ - const transform_result = { - code: result.js.code, - map: result.js.map - }; - - return transform_result; - } - }; - - try { - bundle = await rollup({ - input: './__entry.js', - plugins: [ - repl_plugin, - commonjs, - json, - glsl, - loop_protect, - replace({ - 'process.env.NODE_ENV': JSON.stringify('production') - }) - ], - inlineDynamicImports: true, - onwarn(warning) { - all_warnings.push({ - message: warning.message - }); - } - }); - - return { - bundle, - imports: Array.from(imports), - cache: new_cache, - error: null, - warnings, - all_warnings - }; - } catch (error) { - return { error, imports: null, bundle: null, cache: new_cache, warnings, all_warnings }; - } -} - -/** - * @param {{ uid: number; files: import('$lib/types.js').File[] }} param0 - * @returns - */ -async function bundle({ uid, files }) { - if (!DEV) { - console.clear(); - console.log(`running Svelte compiler version %c${svelte.VERSION}`, 'font-weight: bold'); - } - - /** @type {Map} */ - const lookup = new Map(); - - lookup.set('./__entry.js', { - name: '__entry', - source: ` - export { mount, unmount, untrack } from 'svelte'; - export {default as App} from './App.svelte'; - `, - type: 'js', - modified: false - }); - - files.forEach((file) => { - const path = `./${file.name}.${file.type}`; - lookup.set(path, file); - }); - - /** @type {Awaited>} */ - let client = await get_bundle(uid, 'client', cached.client, lookup); - let error; - - try { - if (client.error) { - throw client.error; - } - - cached.client = client.cache; - - const client_result = ( - await client.bundle?.generate({ - format: 'iife', - exports: 'named' - // sourcemap: 'inline' - }) - )?.output[0]; - - const server = false // TODO how can we do SSR? - ? await get_bundle(uid, 'server', cached.server, lookup) - : null; - - if (server) { - cached.server = server.cache; - if (server.error) { - throw server.error; - } - } - - const server_result = server - ? ( - await server.bundle?.generate({ - format: 'iife', - name: 'SvelteComponent', - exports: 'named' - // sourcemap: 'inline' - }) - )?.output?.[0] - : null; - - return { - uid, - client: client_result, - server: server_result, - imports: client.imports, - warnings: client.warnings, - error: null - }; - } catch (err) { - console.error(err); - - /** @type {Error} */ - // @ts-ignore - const e = error || err; - - // @ts-ignore - delete e.toString; - - return { - uid, - client: null, - server: null, - imports: null, - warnings: client.warnings, - error: Object.assign({}, e, { - message: e.message, - stack: e.stack - }) - }; - } -} diff --git a/sites/svelte-5-preview/src/lib/workers/bundler/plugins/commonjs.js b/sites/svelte-5-preview/src/lib/workers/bundler/plugins/commonjs.js deleted file mode 100644 index 9e0a92dbddc5..000000000000 --- a/sites/svelte-5-preview/src/lib/workers/bundler/plugins/commonjs.js +++ /dev/null @@ -1,58 +0,0 @@ -import { parse } from 'acorn'; -import { walk } from 'zimmerframe'; - -const require = `function require(id) { - if (id in __repl_lookup) return __repl_lookup[id]; - throw new Error(\`Cannot require modules dynamically (\${id})\`); -}`; - -/** @type {import('@rollup/browser').Plugin} */ -export default { - name: 'commonjs', - - transform: (code, id) => { - if (!/\b(require|module|exports)\b/.test(code)) return; - - try { - const ast = parse(code, { - ecmaVersion: 'latest' - }); - - /** @type {string[]} */ - const requires = []; - - walk(/** @type {import('estree').Node} */ (ast), null, { - CallExpression: (node) => { - if (node.callee.type === 'Identifier' && node.callee.name === 'require') { - if (node.arguments.length !== 1) return; - const arg = node.arguments[0]; - if (arg.type !== 'Literal' || typeof arg.value !== 'string') return; - - requires.push(arg.value); - } - } - }); - - const imports = requires.map((id, i) => `import __repl_${i} from '${id}';`).join('\n'); - const lookup = `const __repl_lookup = { ${requires - .map((id, i) => `'${id}': __repl_${i}`) - .join(', ')} };`; - - const transformed = [ - imports, - lookup, - require, - `const exports = {}; const module = { exports };`, - code, - `export default module.exports;` - ].join('\n\n'); - - return { - code: transformed, - map: null - }; - } catch (err) { - return null; - } - } -}; diff --git a/sites/svelte-5-preview/src/lib/workers/bundler/plugins/glsl.js b/sites/svelte-5-preview/src/lib/workers/bundler/plugins/glsl.js deleted file mode 100644 index 51e7e062a4d9..000000000000 --- a/sites/svelte-5-preview/src/lib/workers/bundler/plugins/glsl.js +++ /dev/null @@ -1,12 +0,0 @@ -/** @type {import('@rollup/browser').Plugin} */ -export default { - name: 'glsl', - transform: (code, id) => { - if (!id.endsWith('.glsl')) return; - - return { - code: `export default ${JSON.stringify(code)};`, - map: null - }; - } -}; diff --git a/sites/svelte-5-preview/src/lib/workers/bundler/plugins/json.js b/sites/svelte-5-preview/src/lib/workers/bundler/plugins/json.js deleted file mode 100644 index 2f79b289e4e5..000000000000 --- a/sites/svelte-5-preview/src/lib/workers/bundler/plugins/json.js +++ /dev/null @@ -1,12 +0,0 @@ -/** @type {import('@rollup/browser').Plugin} */ -export default { - name: 'json', - transform: (code, id) => { - if (!id.endsWith('.json')) return; - - return { - code: `export default ${code};`, - map: null - }; - } -}; diff --git a/sites/svelte-5-preview/src/lib/workers/bundler/plugins/loop-protect.js b/sites/svelte-5-preview/src/lib/workers/bundler/plugins/loop-protect.js deleted file mode 100644 index 9cb4a8e25e6a..000000000000 --- a/sites/svelte-5-preview/src/lib/workers/bundler/plugins/loop-protect.js +++ /dev/null @@ -1,111 +0,0 @@ -import { parse } from 'acorn'; -import { print } from 'esrap'; -import { walk } from 'zimmerframe'; - -const TIMEOUT = 100; - -const regex = /\b(for|while)\b/; - -/** - * - * @param {string} code - * @returns {import('estree').Statement} - */ -function parse_statement(code) { - return /** @type {import('estree').Statement} */ (parse(code, { ecmaVersion: 'latest' }).body[0]); -} - -const declaration = parse_statement(` - const __start = Date.now(); -`); - -const check = parse_statement(` - if (Date.now() > __start + ${TIMEOUT}) { - throw new Error('Infinite loop detected'); - } -`); - -/** - * - * @param {import('estree').Node[]} path - * @returns {null | import('estree').FunctionExpression | import('estree').FunctionDeclaration | import('estree').ArrowFunctionExpression} - */ -export function get_current_function(path) { - for (let i = path.length - 1; i >= 0; i--) { - const node = path[i]; - if ( - node.type === 'FunctionDeclaration' || - node.type === 'FunctionExpression' || - node.type === 'ArrowFunctionExpression' - ) { - return node; - } - } - return null; -} - -/** - * @template {import('estree').DoWhileStatement | import('estree').ForStatement | import('estree').WhileStatement} Statement - * @param {Statement} node - * @param {import('zimmerframe').Context} context - * @returns {import('estree').Node | void} - */ -function loop_protect(node, context) { - const current_function = get_current_function(context.path); - - if (current_function === null || (!current_function.async && !current_function.generator)) { - const body = /** @type {import('estree').Statement} */ (context.visit(node.body)); - - const statements = body.type === 'BlockStatement' ? [...body.body] : [body]; - - /** @type {import('estree').BlockStatement} */ - const replacement = { - type: 'BlockStatement', - body: [ - declaration, - { - .../** @type {Statement} */ (context.next() ?? node), - body: { - type: 'BlockStatement', - body: [...statements, check] - } - } - ] - }; - - return replacement; - } - - context.next(); -} - -/** @type {import('@rollup/browser').Plugin} */ -export default { - name: 'loop-protect', - transform: (code, id) => { - // only applies to local files, not imports - if (!id.startsWith('./')) return; - - // only applies to JS and Svelte files - if (!id.endsWith('.js') && !id.endsWith('.svelte')) return; - - // fast path - if (!regex.test(code)) return; - - const ast = parse(code, { - ecmaVersion: 'latest', - sourceType: 'module' - }); - - const transformed = walk(/** @type {import('estree').Node} */ (ast), null, { - WhileStatement: loop_protect, - DoWhileStatement: loop_protect, - ForStatement: loop_protect - }); - - // nothing changed - if (ast === transformed) return null; - - return print(transformed); - } -}; diff --git a/sites/svelte-5-preview/src/lib/workers/bundler/plugins/replace.js b/sites/svelte-5-preview/src/lib/workers/bundler/plugins/replace.js deleted file mode 100644 index 6ccdeffed827..000000000000 --- a/sites/svelte-5-preview/src/lib/workers/bundler/plugins/replace.js +++ /dev/null @@ -1,72 +0,0 @@ -/** @param {string} str */ -function escape(str) { - return str.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&'); -} - -/** @param {unknown} functionOrValue */ -function ensureFunction(functionOrValue) { - if (typeof functionOrValue === 'function') { - return functionOrValue; - } - return function () { - return functionOrValue; - }; -} - -/** - * @param {string} a - * @param {string} b - */ -function longest(a, b) { - return b.length - a.length; -} - -/** @param {Record} object */ -function mapToFunctions(object) { - return Object.keys(object).reduce( - /** @param {Record} functions */ function (functions, key) { - functions[key] = ensureFunction(object[key]); - return functions; - }, - {} - ); -} - -/** - * @param {Record} options - * @returns {import('@rollup/browser').Plugin} - */ -function replace(options) { - const functionValues = mapToFunctions(options); - const keys = Object.keys(functionValues).sort(longest).map(escape); - - const pattern = new RegExp('\\b(' + keys.join('|') + ')\\b', 'g'); - - return { - name: 'replace', - - transform: function transform(code, id) { - let hasReplacements = false; - let match; - let start; - let end; - let replacement; - - code = code.replace(pattern, (_, key) => { - hasReplacements = true; - return String(functionValues[key](id)); - }); - - if (!hasReplacements) { - return null; - } - - return { - code, - map: null - }; - } - }; -} - -export default replace; diff --git a/sites/svelte-5-preview/src/lib/workers/compiler/index.js b/sites/svelte-5-preview/src/lib/workers/compiler/index.js deleted file mode 100644 index 9247894dd6e3..000000000000 --- a/sites/svelte-5-preview/src/lib/workers/compiler/index.js +++ /dev/null @@ -1,154 +0,0 @@ -/// -self.window = self; //TODO: still need?: egregious hack to get magic-string to work in a worker - -/** - * @type {{ - * parse: typeof import('svelte/compiler').parse; - * compile: typeof import('svelte/compiler').compile; - * compileModule: typeof import('svelte/compiler').compileModule; - * VERSION: string; - * }} - */ -let svelte; - -/** @type {(arg?: never) => void} */ -let fulfil_ready; -const ready = new Promise((f) => { - fulfil_ready = f; -}); - -self.addEventListener( - 'message', - /** @param {MessageEvent} event */ - async (event) => { - switch (event.data.type) { - case 'init': - const { svelte_url } = event.data; - - const { version } = await fetch(`${svelte_url}/package.json`) - .then((r) => r.json()) - .catch(() => ({ version: 'experimental' })); - - const compiler = await fetch(`${svelte_url}/compiler/index.js`).then((r) => r.text()); - (0, eval)(compiler + '\n//# sourceURL=compiler/index.js@' + version); - - svelte = globalThis.svelte; - - fulfil_ready(); - break; - - case 'compile': - await ready; - postMessage(compile(event.data)); - break; - - case 'migrate': - await ready; - postMessage(migrate(event.data)); - break; - } - } -); - -const common_options = { - dev: false, - css: false -}; - -/** @param {import("../workers").CompileMessageData} param0 */ -function compile({ id, source, options, return_ast }) { - try { - const css = `/* Select a component to see compiled CSS */`; - - if (options.filename.endsWith('.svelte')) { - const compiled = svelte.compile(source, { - ...options, - discloseVersion: false // less visual noise in the output tab - }); - - const { js, css, warnings, metadata } = compiled; - - const ast = return_ast ? svelte.parse(source, { modern: true }) : undefined; - - return { - id, - result: { - js: js.code, - css: css?.code || `/* Add a tag to see compiled CSS */`, - error: null, - warnings: warnings.map((warning) => warning.toJSON()), - metadata, - ast - } - }; - } else if (options.filename.endsWith('.svelte.js')) { - const compiled = svelte.compileModule(source, { - filename: options.filename, - generate: options.generate, - dev: options.dev - }); - - if (compiled) { - return { - id, - result: { - js: compiled.js.code, - css, - error: null, - warnings: compiled.warnings.map((warning) => warning.toJSON()), - metadata: compiled.metadata - } - }; - } - } - - return { - id, - result: { - js: `// Select a component, or a '.svelte.js' module that uses runes, to see compiled output`, - css, - error: null, - warnings: [], - metadata: null - } - }; - } catch (err) { - // @ts-ignore - let message = `/*\nError compiling ${err.filename ?? 'component'}:\n${err.message}\n*/`; - - return { - id, - result: { - js: message, - css: message, - error: { - message: err.message, - position: err.position - }, - warnings: [], - metadata: null - } - }; - } -} - -/** @param {import("../workers").MigrateMessageData} param0 */ -function migrate({ id, source, filename }) { - try { - const result = svelte.migrate(source, { filename }); - - return { - id, - result - }; - } catch (err) { - // @ts-ignore - let message = `/*\nError migrating ${err.filename ?? 'component'}:\n${err.message}\n*/`; - - return { - id, - result: { code: source }, - error: message - }; - } -} diff --git a/sites/svelte-5-preview/src/lib/workers/jsconfig.json b/sites/svelte-5-preview/src/lib/workers/jsconfig.json deleted file mode 100644 index 60351b754815..000000000000 --- a/sites/svelte-5-preview/src/lib/workers/jsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "include": ["./**/*"], - "compilerOptions": { - "paths": { - "svelte": ["../../../static/svelte/main"], - "svelte/*": ["../../../static/svelte/*"] - } - } -} diff --git a/sites/svelte-5-preview/src/lib/workers/patch_window.js b/sites/svelte-5-preview/src/lib/workers/patch_window.js deleted file mode 100644 index ff7057c9c28f..000000000000 --- a/sites/svelte-5-preview/src/lib/workers/patch_window.js +++ /dev/null @@ -1 +0,0 @@ -self.window = self; // hack for magic-sring and rollup inline sourcemaps diff --git a/sites/svelte-5-preview/src/lib/workers/workers.d.ts b/sites/svelte-5-preview/src/lib/workers/workers.d.ts deleted file mode 100644 index e66e075c14f1..000000000000 --- a/sites/svelte-5-preview/src/lib/workers/workers.d.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { CompileOptions, File } from '../types'; - -export type CompileMessageData = { - id: number; - type: 'compile' | 'init'; - source: string; - options: CompileOptions; - is_entry: boolean; - return_ast: boolean; - svelte_url?: string; - result: { - js: string; - css: string; - ast?: import('svelte/types/compiler/interfaces').Ast; - metadata?: { - runes: boolean; - }; - }; -}; - -export type BundleMessageData = { - uid: number; - type: 'init' | 'bundle' | 'status'; - message: string; - packages_url: string; - svelte_url: string; - files: File[]; -}; - -export type MigrateMessageData = { - id: number; - result: { code: string }; - error?: string; -}; diff --git a/sites/svelte-5-preview/src/routes/+error.svelte b/sites/svelte-5-preview/src/routes/+error.svelte deleted file mode 100644 index 6d6d8a7d7cb5..000000000000 --- a/sites/svelte-5-preview/src/routes/+error.svelte +++ /dev/null @@ -1,73 +0,0 @@ - - - - {$page.status} - - -
- {#if online} - {#if $page.status === 404} -

Not found!

-

- If you were expecting to find something here, please drop by the - Discord chatroom - and let us know, or raise an issue on - GitHub. Thanks! -

- {:else} -

Yikes!

-

Something went wrong when we tried to render this page.

- {#if $page.error.message} -

{$page.status}: {$page.error.message}

- {:else} -

Encountered a {$page.status} error.

- {/if} -

Please try reloading the page.

-

- If the error persists, please drop by the - Discord chatroom - and let us know, or raise an issue on - GitHub. Thanks! -

- {/if} - {:else} -

It looks like you're offline

-

Reload the page once you've found the internet.

- {/if} -
- - diff --git a/sites/svelte-5-preview/src/routes/+layout.server.js b/sites/svelte-5-preview/src/routes/+layout.server.js deleted file mode 100644 index 640c4c57df10..000000000000 --- a/sites/svelte-5-preview/src/routes/+layout.server.js +++ /dev/null @@ -1,12 +0,0 @@ -export const prerender = true; - -/** @type {import('@sveltejs/adapter-vercel').EdgeConfig} */ -export const config = { - runtime: 'edge' -}; - -export const load = async ({ fetch }) => { - const nav_data = await fetch('/nav.json').then((r) => r.json()); - - return { nav_links: nav_data }; -}; diff --git a/sites/svelte-5-preview/src/routes/+layout.svelte b/sites/svelte-5-preview/src/routes/+layout.svelte deleted file mode 100644 index 99b72d0fade5..000000000000 --- a/sites/svelte-5-preview/src/routes/+layout.svelte +++ /dev/null @@ -1,101 +0,0 @@ - - - - Svelte 5 preview - - - - - - - - - - - - - - diff --git a/sites/svelte-5-preview/src/routes/+page.svelte b/sites/svelte-5-preview/src/routes/+page.svelte deleted file mode 100644 index b54c36ee5b58..000000000000 --- a/sites/svelte-5-preview/src/routes/+page.svelte +++ /dev/null @@ -1,98 +0,0 @@ - - - { - if (!setting_hash) { - change_from_hash(); - } - - setting_hash = false; - }} -/> - - diff --git a/sites/svelte-5-preview/src/routes/defaults.js b/sites/svelte-5-preview/src/routes/defaults.js deleted file mode 100644 index f1bdbbbb3548..000000000000 --- a/sites/svelte-5-preview/src/routes/defaults.js +++ /dev/null @@ -1,21 +0,0 @@ -export const default_files = () => [ - { - name: 'App', - type: 'svelte', - source: ` - - - - ` - .replace(/^\t{3}/gm, '') - .trim() - } -]; diff --git a/sites/svelte-5-preview/src/routes/docs/+layout.server.js b/sites/svelte-5-preview/src/routes/docs/+layout.server.js deleted file mode 100644 index cbb6433bf9be..000000000000 --- a/sites/svelte-5-preview/src/routes/docs/+layout.server.js +++ /dev/null @@ -1,13 +0,0 @@ -export async function load({ url }) { - if (url.pathname === '/docs') { - return { - sections: [] - }; - } - - const { get_docs_data, get_docs_list } = await import('./render.js'); - - return { - sections: get_docs_list(await get_docs_data()) - }; -} diff --git a/sites/svelte-5-preview/src/routes/docs/+layout.svelte b/sites/svelte-5-preview/src/routes/docs/+layout.svelte deleted file mode 100644 index 097fde98ff0e..000000000000 --- a/sites/svelte-5-preview/src/routes/docs/+layout.svelte +++ /dev/null @@ -1,110 +0,0 @@ - - -
-
- -
- -
- {#if category} -

{category}

- {/if} - {#if title} -

{title}

- {/if} - - -
-
- - diff --git a/sites/svelte-5-preview/src/routes/docs/+page.js b/sites/svelte-5-preview/src/routes/docs/+page.js deleted file mode 100644 index fba7f30e4b6b..000000000000 --- a/sites/svelte-5-preview/src/routes/docs/+page.js +++ /dev/null @@ -1,5 +0,0 @@ -import { redirect } from '@sveltejs/kit'; - -export function load() { - redirect(307, '/docs/introduction'); -} diff --git a/sites/svelte-5-preview/src/routes/docs/[slug]/+page.server.js b/sites/svelte-5-preview/src/routes/docs/[slug]/+page.server.js deleted file mode 100644 index 25c78ba28132..000000000000 --- a/sites/svelte-5-preview/src/routes/docs/[slug]/+page.server.js +++ /dev/null @@ -1,19 +0,0 @@ -import { error } from '@sveltejs/kit'; - -export async function entries() { - const { get_docs_data } = await import('../render.js'); - - const data = await get_docs_data(); - return data[0].pages.map((page) => ({ slug: page.slug })); -} - -export async function load({ params }) { - const { get_docs_data, get_parsed_docs } = await import('../render.js'); - - const data = await get_docs_data(); - const processed_page = await get_parsed_docs(data, params.slug); - - if (!processed_page) error(404); - - return { page: processed_page }; -} diff --git a/sites/svelte-5-preview/src/routes/docs/[slug]/+page.svelte b/sites/svelte-5-preview/src/routes/docs/[slug]/+page.svelte deleted file mode 100644 index bb5c166711f7..000000000000 --- a/sites/svelte-5-preview/src/routes/docs/[slug]/+page.svelte +++ /dev/null @@ -1,76 +0,0 @@ - - - - {data.page?.title} • Docs • Svelte 5 preview - - - - - - -
- - - {@html data.page.content} -
- -
-
- previous - - {#if prev} - {prev.title} - {/if} -
- -
- next - {#if next} - {next.title} - {/if} -
-
- - diff --git a/sites/svelte-5-preview/src/routes/docs/content/01-api/01-introduction.md b/sites/svelte-5-preview/src/routes/docs/content/01-api/01-introduction.md deleted file mode 100644 index 2e3cb987c050..000000000000 --- a/sites/svelte-5-preview/src/routes/docs/content/01-api/01-introduction.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: Introduction ---- - -Welcome to the Svelte 5 preview documentation! This is intended as a resource for people who already have some familiarity with Svelte and want to learn about the new runes API, which you can learn about in the [Introducing runes](https://svelte.dev/blog/runes) blog post. - -You can try runes for yourself in the [playground](/), or learn more about our plans via the [FAQ](/docs/faq). 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 deleted file mode 100644 index 84062a1cfaee..000000000000 --- a/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md +++ /dev/null @@ -1,700 +0,0 @@ ---- -title: Runes ---- - -Svelte 5 introduces _runes_, a powerful set of primitives for controlling reactivity inside your Svelte components and — for the first time — inside `.svelte.js` and `.svelte.ts` modules. - -Runes are function-like symbols that provide instructions to the Svelte compiler. You don't need to import them from anywhere — when you use Svelte, they're part of the language. - -When you [opt in to runes mode](#how-to-opt-in), the non-runes features listed in the 'What this replaces' sections are no longer available. - -> Check out the [Introducing runes](https://svelte.dev/blog/runes) blog post before diving into the docs! - -## `$state` - -Reactive state is declared with the `$state` rune: - -```svelte - - - -``` - -You can also use `$state` in class fields (whether public or private): - -```js -// @errors: 7006 2554 -class Todo { - done = $state(false); - text = $state(); - - constructor(text) { - this.text = text; - } -} -``` - -> In this example, the compiler transforms `done` and `text` into `get`/`set` methods on the class prototype referencing private fields - -Only plain objects and arrays [are made deeply reactive](/#H4sIAAAAAAAAE42QwWrDMBBEf2URhUhUNEl7c21DviPOwZY3jVpZEtIqUBz9e-UUt9BTj7M784bdmZ21wciq48xsPyGr2MF7Jhl9-kXEKxrCoqNLQS2TOqqgPbWd7cgggU3TgCFCAw-RekJ-3Et4lvByEq-drbe_dlsPichZcFYZrT6amQto2pXw5FO88FUYtG90gUfYi3zvWrYL75vxL57zfA07_zfr23k1vjtt-aZ0bQTcbrDL5ZifZcAxKeS8lzDc8X0xDhJ2ItdbX1jlOZMb9VnjyCoKCfMpfwG975NFVwEAAA==) by wrapping them with [`Proxies`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy): - -```svelte - - - - - - -

- {numbers.join(' + ') || 0} - = - {numbers.reduce((a, b) => a + b, 0)} -

-``` - -### What this replaces - -In non-runes mode, a `let` declaration is treated as reactive state if it is updated at some point. Unlike `$state(...)`, which works anywhere in your app, `let` only behaves this way at the top level of a component. - -## `$state.raw` - -State declared with `$state.raw` cannot be mutated; it can only be _reassigned_. In other words, rather than assigning to a property of an object, or using an array method like `push`, replace the object or array altogether if you'd like to update it: - -```diff - - -- - -- -+ - -

- {numbers.join(' + ') || 0} - = - {numbers.reduce((a, b) => a + b, 0)} -

-``` - -This can improve performance with large arrays and objects that you weren't planning to mutate anyway, since it avoids the cost of making them reactive. Note that raw state can _contain_ reactive state (for example, a raw array of reactive objects). - -## `$state.snapshot` - -To take a static snapshot of a deeply reactive `$state` proxy, use `$state.snapshot`: - -```svelte - -``` - -This is handy when you want to pass some state to an external library or API that doesn't expect a proxy, such as `structuredClone`. - -## `$derived` - -Derived state is declared with the `$derived` rune: - -```diff - - - - -+

{count} doubled is {doubled}

-``` - -The expression inside `$derived(...)` should be free of side-effects. Svelte will disallow state changes (e.g. `count++`) inside derived expressions. - -As with `$state`, you can mark class fields as `$derived`. - -### What this replaces - -If the value of a reactive variable is being computed it should be replaced with `$derived` whether it previously took the form of `$: double = count * 2` or `$: { double = count * 2; }` There are some important differences to be aware of: - -- With the `$derived` rune, the value of `double` is always current (for example if you update `count` then immediately `console.log(double)`). With `$:` declarations, values are not updated until right before Svelte updates the DOM -- In non-runes mode, Svelte determines the dependencies of `double` by statically analysing the `count * 2` expression. If you refactor it... - ```js - // @errors: 2304 - const doubleCount = () => count * 2; - $: double = doubleCount(); - ``` - ...that dependency information is lost, and `double` will no longer update when `count` changes. With runes, dependencies are instead tracked at runtime. -- In non-runes mode, reactive statements are ordered _topologically_, meaning that in a case like this... - ```js - // @errors: 2304 - $: triple = double + count; - $: double = count * 2; - ``` - ...`double` will be calculated first despite the source order. In runes mode, `triple` cannot reference `double` before it has been declared. - -## `$derived.by` - -Sometimes you need to create complex derivations that don't fit inside a short expression. In these cases, you can use `$derived.by` which accepts a function as its argument. - -```svelte - - - -``` - -In essence, `$derived(expression)` is equivalent to `$derived.by(() => expression)`. - -## `$effect` - -To run _side-effects_ when the component is mounted to the DOM, and when values change, we can use the `$effect` rune ([demo](/#H4sIAAAAAAAAE31T24rbMBD9lUG7kAQ2sbdlX7xOYNk_aB_rQhRpbAsU2UiTW0P-vbrYubSlYGzmzMzROTPymdVKo2PFjzMzfIusYB99z14YnfoQuD1qQh-7bmdFQEonrOppVZmKNBI49QthCc-OOOH0LZ-9jxnR6c7eUpOnuv6KeT5JFdcqbvbcBcgDz1jXKGg6ncFyBedYR6IzLrAZwiN5vtSxaJA-EzadfJEjKw11C6GR22-BLH8B_wxdByWpvUYtqqal2XB6RVkG1CoHB6U1WJzbnYFDiwb3aGEdDa3Bm1oH12sQLTcNPp7r56m_00mHocSG97_zd7ICUXonA5fwKbPbkE2ZtMJGGVkEdctzQi4QzSwr9prnFYNk5hpmqVuqPQjNnfOJoMF22lUsrq_UfIN6lfSVyvQ7grB3X2mjMZYO3XO9w-U5iLx42qg29md3BP_ni5P4gy9ikTBlHxjLzAtPDlyYZmRdjAbGq7HprEQ7p64v4LU_guu0kvAkhBim3nMplWl8FreQD-CW20aZR0wq12t-KqDWeBywhvexKC3memmDwlHAv9q4Vo2ZK8KtK0CgX7u9J8wXbzdKv-nRnfF_2baTqlYoWUF2h5efl9-n0O6koAMAAA==)): - -```svelte - - - -``` - -The function passed to `$effect` will run when the component mounts, and will re-run after any changes to the values it reads that were declared with `$state` or `$derived` (including those passed in with `$props`). Re-runs are batched (i.e. changing `color` and `size` in the same moment won't cause two separate runs), and happen after any DOM updates have been applied. - -Values that are read asynchronously — after an `await` or inside a `setTimeout`, for example — will _not_ be tracked. Here, the canvas will be repainted when `color` changes, but not when `size` changes ([demo](/#H4sIAAAAAAAAE31T24rbMBD9lUG7kCxsbG_LvrhOoPQP2r7VhSjy2BbIspHGuTT436tLnMtSCiaOzpw5M2dGPrNaKrQs_3VmmnfIcvZ1GNgro9PgD3aPitCdbT8a4ZHCCiMH2pS6JIUEVv5BWMOzJU64fM9evswR0ave3EKLp7r-jFm2iIwri-s9tx5ywDPWNQpaLl9gvYFz4JHotfVqmvBITi9mJA3St4gtF5-qWZUuvEQo5Oa7F8tewT2XrIOsqL2eWpRNS7eGSkpToFZaOEilwODKjBoOLWrco4FtsLQF0XLdoE2S5LGmm6X6QSflBxKod8IW6afssB8_uAslndJuJNA9hWKw9VO91pmJ92XunHlu_J1nMDk8_p_8q0hvO9NFtA47qavcW12fIzJBmM26ZG9ZVjKIs7ke05hdyT0Ixa11Ad-P6ZUtWbgNheI7VJvYQiH14Bz5a-SYxvtwIqHonqsR12ff8ORkQ-chP70T-L9eGO4HvYAFwRh9UCxS13h0YP2CgmoyG5h3setNhWZF_ZDD23AE2ytZwZMQ4jLYgVeV1I2LYgfZBey4aaR-xCppB8VPOdQKjxes4UMgxcVcvwHf4dzAv9K4ko1eScLO5iDQXQFzL5gl7zdJt-nZnXYfbddXspZYsZzMiNPv6S8Bl41G7wMAAA==)): - -```ts -// @filename: index.ts -declare let canvas: { - width: number; - height: number; - getContext( - type: '2d', - options?: CanvasRenderingContext2DSettings - ): CanvasRenderingContext2D; -}; -declare let color: string; -declare let size: number; - -// ---cut--- -$effect(() => { - const context = canvas.getContext('2d'); - context.clearRect(0, 0, canvas.width, canvas.height); - - // this will re-run whenever `color` changes... - context.fillStyle = color; - - setTimeout(() => { - // ...but not when `size` changes - context.fillRect(0, 0, size, size); - }, 0); -}); -``` - -An effect only reruns when the object it reads changes, not when a property inside it changes. (If you want to observe changes _inside_ an object at dev time, you can use [`$inspect`](#$inspect).) - -```svelte - - - - -

{state.value} doubled is {derived.value}

-``` - -An effect only depends on the values that it read the last time it ran. If `a` is true, changes to `b` will [not cause this effect to rerun](/#H4sIAAAAAAAAE3WQ0WrDMAxFf0U1hTow1vcsMfQ7lj3YjlxEXTvEymC4_vfFC6Ewtidxde8RkrJw5DGJ9j2LoO8oWnGZJvEi-GuqIn2iZ1x1istsa6dLdqaJ1RAG9sigoYdjYs0onfYJm7fdMX85q3dE59CylA30CnJtDWxjSNHjq49XeZqXEChcT9usLUAOpIbHA0yzM78oColGhDVofLS3neZSS6mqOz-XD51ZmGOAGKwne-vztk-956CL0kAJsi7decupf4l658EUZX4I8yTWt93jSI5wFC3PC5aP8g0Aje5DcQEAAA==): - -```ts -let a = false; -let b = false; -// ---cut--- -$effect(() => { - console.log('running'); - - if (a || b) { - console.log('inside if block'); - } -}); -``` - -You can return a function from `$effect`, which will run immediately before the effect re-runs, and before it is destroyed ([demo](/#H4sIAAAAAAAAE42SzW6DMBCEX2Vl5RDaVCQ9JoDUY--9lUox9lKsGBvZC1GEePcaKPnpqSe86_m0M2t6ViqNnu0_e2Z4jWzP3pqGbRhdmrHwHWrCUHvbOjF2Ei-caijLTU4aCYRtDUEKK0-ccL2NDstNrbRWHoU10t8Eu-121gTVCssSBa3XEaQZ9GMrpziGj0p5OAccCgSHwmEgJZwrNNihg6MyhK7j-gii4uYb_YyGUZ5guQwzPdL7b_U4ZNSOvp9T2B3m1rB5cLx4zMkhtc7AHz7YVCVwEFzrgosTBMuNs52SKDegaPbvWnMH8AhUXaNUIY6-hHCldQhUIcyLCFlfAuHvkCKaYk8iYevGGgy2wyyJnpy9oLwG0sjdNe2yhGhJN32HsUzi2xOapNpl_bSLIYnDeeoVLZE1YI3QSpzSfo7-8J5PKbwOmdf2jC6JZyD7HxpPaMk93aHhF6utVKVCyfbkWhy-hh9Z3o_2nQIAAA==)). - -```svelte - - -

{count}

- - - -``` - -### When not to use `$effect` - -In general, `$effect` is best considered something of an escape hatch — useful for things like analytics and direct DOM manipulation — rather than a tool you should use frequently. In particular, avoid using it to synchronise state. Instead of this... - -```svelte - -``` - -...do this: - -```svelte - -``` - -> For things that are more complicated than a simple expression like `count * 2`, you can also use [`$derived.by`](#$derived-by). - -You might be tempted to do something convoluted with effects to link one value to another. The following example shows two inputs for "money spent" and "money left" that are connected to each other. If you update one, the other should update accordingly. Don't use effects for this ([demo](/#H4sIAAAAAAAACpVRQWrDMBD8ihA5ONDG7qEXxQ70HXUPir0KgrUsrHWIMf57pXWdlFIKPe6MZmZnNUtjEYJU77N0ugOp5Jv38knS5NMQroAEcQ79ODQJKUMzWE-n2tWEQIJ60igq8VIUxw0LHhxFbBdIE2TF_s4gmG8Ea5mM9A6MgYaybC-qk5gTlDT8fg15Xo3ZbPlTti2w6ZLNQ1bmjw6uRH0G5DqldX6MjWL1qpaDdheopThb16qrxhGqmX0X0elbNbP3InKWfjH5hvKYku7u_wtKC_-aw8Q9Jk0_UgJNCOvvJHC7SGuDRz0pYRBuxxW7aK9EcXiFbr0NX4bl8cO7vrXGQisVDSMsH8sniirsuSsCAAA=)): - -```svelte - - - - - -``` - -Instead, use callbacks where possible ([demo](/#H4sIAAAAAAAACo1SMW6EMBD8imWluFNyQIo0HERKf13KkMKB5WTJGAsvp0OIv8deMEEJRcqdmZ1ZjzzyWiqwPP0YuRYN8JS_GcOfOA7GD_YGCsHNtu270iOZLTtp8LXQBSpAhi0KxXL2nCTngFkDGh32YFEgHJLjyiioNwTtEunoutclylaz3lSOfPceBziy0ZMFBs9HiFB0V8DoJlQP55ldfOdjTvMBRE275hcn33gv2_vWITh4e3GwzuKfNnSmxBcoKiaT2vSuG1diXvBO6CsUnJFrPpLhxFpNonzcvHdijbjnI0VNLCavRR8HlEYfvcb9O9mf_if4QuBOLqnXWD_9SrU4KJg_ggdDm5W0RokhZbWC-1LiVZiUJdELNJvqaN39raatZC2h4il2PUyf0zcIbC-7lgIAAA==)): - -```svelte - - - - - -``` - -If you need to use bindings, for whatever reason (for example when you want some kind of "writable `$derived`"), consider using getters and setters to synchronise state ([demo](/#H4sIAAAAAAAACpVRQW7DIBD8CkI9JFIau4deiB2p7yg9kHhtIWGMYG3Fsvh7ARs3qnrpCWZGM8MuC22lAkfZ50K16IEy-mEMPVGcTQRuAoUQsBtGe49M5e5WGrxyzVEBEhxQKFKTt7K8ZM4Z0Bi4F4cC4VAeo7JpCtooLRFz7AIzCTXC4ZgpjhZwtHpLfl3TLqvoT-vpdt_0ZMy92TllVzx8AFXx83pdKXEDlQappDZjmCUMXXNqhe6AU3KTumGppV5StCe9eNRLivekSNZNKTKbYGza0_9XFPdzTvc_257kvTJyvxodzgrWP4pkXlEjnVFiZqRV8NiW0wnDSHl-hz4RPm0p2cO390MjWwkNZWhD5Zf_BkCCa6AxAgAA)): - -```svelte - - - - - -``` - -If you absolutely have to update `$state` within an effect and run into an infinite loop because you read and write to the same `$state`, use [untrack](functions#untrack). - -### What this replaces - -The portions of `$: {}` that are triggering side-effects can be replaced with `$effect` while being careful to migrate updates of reactive variables to use `$derived`. There are some important differences: - -- Effects only run in the browser, not during server-side rendering -- They run after the DOM has been updated, whereas `$:` statements run immediately _before_ -- You can return a cleanup function that will be called whenever the effect refires - -Additionally, you may prefer to use effects in some places where you previously used `onMount` and `afterUpdate` (the latter of which will be deprecated in Svelte 5). There are some differences between these APIs as `$effect` should not be used to compute reactive values and will be triggered each time a referenced reactive variable changes (unless using `untrack`). - -## `$effect.pre` - -In rare cases, you may need to run code _before_ the DOM updates. For this we can use the `$effect.pre` rune: - -```svelte - - -
- {#each messages as message} -

{message}

- {/each} -
-``` - -Apart from the timing, `$effect.pre` works exactly like [`$effect`](#$effect) — refer to its documentation for more info. - -### What this replaces - -Previously, you would have used `beforeUpdate`, which — like `afterUpdate` — is deprecated in Svelte 5. - -## `$effect.tracking` - -The `$effect.tracking` rune is an advanced feature that tells you whether or not the code is running inside a tracking context, such as an effect or inside your template ([demo](/#H4sIAAAAAAAACn3PQWrDMBAF0KtMRSA2xPFeUQU5R92FUUZBVB4N1rgQjO9eKSlkEcjyfz6PmVX5EDEr_bUqGidUWp2Z1UHJjWvIvxgFS85pmV1tTHZzYLEDDeIS5RTxGNO12QcClyZOhCSQURbW-wPs0Ht0cpR5dD-Brk3bnqDvwY8xYzGK8j9pmhY-Lay1eqUfm3eizEsFZWtPA5n-eSYZtkUQnDiOghrWV2IzPVswH113d6DrbHl6SpfgA16UruX2vf0BWo7W2y8BAAA=)): - -```svelte - - -

in template: {$effect.tracking()}

-``` - -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: - -```js -let { optionalProp = 42, requiredProp } = $props(); -``` - -You can use familiar destructuring syntax to rename props, in cases where you need to (for example) use a reserved word like `catch` in ``: - -```js -let { catch: theCatch } = $props(); -``` - -To get all properties, use rest syntax: - -```js -let { a, b, c, ...everythingElse } = $props(); -``` - -You can also use an identifier: - -```js -let props = $props(); -``` - -If you're using TypeScript, you can declare the prop types: - - -```ts -interface MyProps { - required: string; - optional?: number; - partOfEverythingElse?: boolean; -}; - -let { required, optional, ...everythingElse }: MyProps = $props(); -``` - -> In an earlier preview, `$props()` took a type argument. This caused bugs, since in a case like this... -> -> ```ts -> // @errors: 2558 -> let { x = 42 } = $props<{ x?: string }>(); -> ``` -> -> ...TypeScript [widens the type](https://www.typescriptlang.org/play?#code/CYUwxgNghgTiAEAzArgOzAFwJYHtXwBIAHGHIgZwB4AVeAXnilQE8A+ACgEoAueagbgBQgiCAzwA3vAAe9eABYATPAC+c4qQqUp03uQwwsqAOaqOnIfCsB6a-AB6AfiA) of `x` to be `string | number`, instead of erroring. - -If you're using JavaScript, you can declare the prop types using JSDoc: - -```js -/** @type {{ x: string }} */ -let { x } = $props(); - -// or use @typedef if you want to document the properties: - -/** - * @typedef {Object} MyProps - * @property {string} y Some documentation - */ - -/** @type {MyProps} */ -let { y } = $props(); -``` - -By default props are treated as readonly, meaning reassignments will not propagate upwards and mutations will result in a warning at runtime in development mode. You will also get a runtime error when trying to `bind:` to a readonly prop in a parent component. To declare props as bindable, use [`$bindable()`](#$bindable). - -### What this replaces - -`$props` replaces the `export let` and `export { x as y }` syntax for declaring props. It also replaces `$$props` and `$$restProps`, and the little-known `interface $$Props {...}` construct. - -Note that you can still use `export const` and `export function` to expose things to users of your component (if they're using `bind:this`, for example). - -## `$bindable` - -To declare props as bindable, use `$bindable()`. Besides using them as regular props, the parent can (_can_, not _must_) then also `bind:` to them. - -```svelte - -``` - -You can pass an argument to `$bindable()`. This argument is used as a fallback value when the property is `undefined`. - -```svelte - -``` - -Note that the parent is not allowed to pass `undefined` to a property with a fallback if it `bind:`s to that property. - -## `$inspect` - -The `$inspect` rune is roughly equivalent to `console.log`, with the exception that it will re-run whenever its -argument changes. `$inspect` tracks reactive state deeply, meaning that updating something inside an object -or array using [fine-grained reactivity](/docs/fine-grained-reactivity) will cause it to re-fire. ([Demo:](/#H4sIAAAAAAAACkWQ0YqDQAxFfyUMhSotdZ-tCvu431AXtGOqQ2NmmMm0LOK_r7Utfby5JzeXTOpiCIPKT5PidkSVq2_n1F7Jn3uIcEMSXHSw0evHpAjaGydVzbUQCmgbWaCETZBWMPlKj29nxBDaHj_edkAiu12JhdkYDg61JGvE_s2nR8gyuBuiJZuDJTyQ7eE-IEOzog1YD80Lb0APLfdYc5F9qnFxjiKWwbImo6_llKRQVs-2u91c_bD2OCJLkT3JZasw7KLA2XCX31qKWE6vIzNk1fKE0XbmYrBTufiI8-_8D2cUWBA_AQAA)) - -```svelte - - - - -``` - -`$inspect` returns a property `with`, which you can invoke with a callback, which will then be invoked instead of `console.log`. The first argument to the callback is either `"init"` or `"update"`, all following arguments are the values passed to `$inspect`. [Demo:](/#H4sIAAAAAAAACkVQ24qDMBD9lSEUqlTqPlsj7ON-w7pQG8c2VCchmVSK-O-bKMs-DefKYRYx6BG9qL4XQd2EohKf1opC8Nsm4F84MkbsTXAqMbVXTltuWmp5RAZlAjFIOHjuGLOP_BKVqB00eYuKs82Qn2fNjyxLtcWeyUE2sCRry3qATQIpJRyD7WPVMf9TW-7xFu53dBcoSzAOrsqQNyOe2XUKr0Xi5kcMvdDB2wSYO-I9vKazplV1-T-d6ltgNgSG1KjVUy7ZtmdbdjqtzRcphxMS1-XubOITJtPrQWMvKnYB15_1F7KKadA_AQAA) - -```svelte - - - -``` - -A convenient way to find the origin of some change is to pass `console.trace` to `with`: - -```js -// @errors: 2304 -$inspect(stuff).with(console.trace); -``` - -> `$inspect` only works during development. - -## `$host` - -Retrieves the `this` reference of the custom element that contains this component. Example: - -```svelte - - - - - -``` - -> Only available inside custom element components, and only on the client-side - -## How to opt in - -Current Svelte code will continue to work without any adjustments. Components using the Svelte 4 syntax can use components using runes and vice versa. - -The easiest way to opt in to runes mode is to just start using them in your code. Alternatively, you can force the compiler into runes or non-runes mode either on a per-component basis... - - -```svelte - - - -``` - -...or for your entire app: - -```js -/// file: svelte.config.js -export default { - compilerOptions: { - runes: true - } -}; -``` diff --git a/sites/svelte-5-preview/src/routes/docs/content/01-api/03-snippets.md b/sites/svelte-5-preview/src/routes/docs/content/01-api/03-snippets.md deleted file mode 100644 index b3fe34d21a3b..000000000000 --- a/sites/svelte-5-preview/src/routes/docs/content/01-api/03-snippets.md +++ /dev/null @@ -1,269 +0,0 @@ ---- -title: Snippets ---- - -Snippets, and _render tags_, are a way to create reusable chunks of markup inside your components. Instead of writing duplicative code like [this](/#H4sIAAAAAAAAE5VUYW-kIBD9K8Tmsm2yXXRzvQ-s3eR-R-0HqqOQKhAZb9sz_vdDkV1t000vRmHewMx7w2AflbIGG7GnPlK8gYhFv42JthG-m9Gwf6BGcLbVXZuPSGrzVho8ZirDGpDIhldgySN5GpEMez9kaNuckY1ANJZRamRuu2ZnhEZt6a84pvs43mzD4pMsUDDi8DMkQFYCGdkvsJwblFq5uCik9bmJ4JZwUkv1eoknWigX2eGNN6aGXa6bjV8ybP-X7sM36T58SVcrIIV2xVIaA41xeD5kKqWXuqpUJEefOqVuOkL9DfBchGrzWfu0vb-RpTd3o-zBR045Ga3HfuE5BmJpKauuhbPtENlUF2sqR9jqpsPSxWsMrlngyj3VJiyYjJXb1-lMa7IWC-iSk2M5Zzh-SJjShe-siq5kpZRPs55BbSGU5YPyte4vVV_VfFXxVb10dSLf17pS2lM5HnpPxw4Zpv6x-F57p0jI3OKlVnhv5V9wPQrNYQQ9D_f6aGHlC89fq1Z3qmDkJCTCweOGF4VUFSPJvD_DhreVdA0eu8ehJJ5x91dBaBkpWm3ureCFPt3uzRv56d4kdp-2euG38XZ6dsnd3ZmPG9yRBCrzRUvi-MccOdwz3qE-fOZ7AwAhlrtTUx3c76vRhSwlFBHDtoPhefgHX3dM0PkEAAA=)... - -```svelte -{#each images as image} - {#if image.href} - -
- {image.caption} -
{image.caption}
-
-
- {:else} -
- {image.caption} -
{image.caption}
-
- {/if} -{/each} -``` - -...you can write [this](/#H4sIAAAAAAAAE5VUYW-bMBD9KxbRlERKY4jWfSA02n5H6QcXDmwVbMs-lnaI_z6D7TTt1moTAnPvzvfenQ_GpBEd2CS_HxPJekjy5IfWyS7BFz0b9id0CM62ajDVjBS2MkLjqZQldoBE9KwFS-7I_YyUOPqlRGuqnKw5orY5pVpUduj3mitUln5LU3pI0_UuBp9FjTwnDr9AHETLMSeHK6xiGoWSLi9yYT034cwSRjohn17zcQPNFTs8s153sK9Uv_Yh0-5_5d7-o9zbD-UqCaRWrllSYZQxLw_HUhb0ta-y4NnJUxfUvc7QuLJSaO0a3oh2MLBZat8u-wsPnXzKQvTtVVF34xK5d69ThFmHEQ4SpzeVRediTG8rjD5vBSeN3E5JyHh6R1DQK9-iml5kjzQUN_lSgVU8DhYLx7wwjSvRkMDvTjiwF4zM1kXZ7DlF1eN3A7IG85e-zRrYEjjm0FkI4Cc7Ripm0pHOChexhcWXzreeZyRMU6Mk3ljxC9w4QH-cQZ_b3T5pjHxk1VNr1CDrnJy5QDh6XLO6FrLNSRb2l9gz0wo3S6m7HErSgLsPGMHkpDZK31jOanXeHPQz-eruLHUP0z6yTbpbrn223V70uMXNSpQSZjpL0y8hcxxpNqA6_ql3BQAxlxvfpQ_uT9GrWjQC6iRHM8D0MP0GQsIi92QEAAA=): - -```diff -+{#snippet figure(image)} -
- {image.caption} -
{image.caption}
-
-+{/snippet} - -{#each images as image} - {#if image.href} - -+ {@render figure(image)} - - {:else} -+ {@render figure(image)} - {/if} -{/each} -``` - -Snippet parameters can be destructured ([demo](/#H4sIAAAAAAAAE5VTYW-bMBD9KyeiKYlEY4jWfSAk2n5H6QcXDmwVbMs2SzuL_z6DTRqp2rQJ2Ycfd_ced2eXtLxHkxRPLhF0wKRIfiiVpIl9V_PB_MTeoj8bOep6RkpTa67spRKV7dECH2iHBs7wNCOVdcFU1ui6gC2zVpmCEMVrMw4HxaSVhnzLMnLMsm26Ol95Y1kBHr9BDHnHbAHHO6ymynIpfF7LuAncwKgBCj0Xrx_5mMb2jh3f6KB6PNRy2AaXKf1fuY__KPfxj3KlQGikL5aQdpUxm-dTJUryUVdRsvwSqEviX2fIbYzgSvmCt7wbNe4ceMUpRIoUFkkpBBkw7ZfMZXC-BLKSDx3Q3p5djJrA-SR-X4K9DdHT6u-jo-flFlKSO3ThIDcSR6LIKUhGWrN1QGhs16LLbXgbjoe5U1PkozCfzu7uy2WtpfuuUTSo1_9ffPZrJKGLoyuwNxjBv0Q4wmdSR2aFi9jS2Pc-FIrlEKeilcI-GP4LfVtxOM1gyO1XSLp6vtD6tdNyFE0BV8YtngKuaNNw0RWQx_jKDlR33M9E5h-PQhZxfxEt6gIaLdWDYbSR191RvcFXv_LMb7p7obssXZ5Dvt_f9HgzdzZKibOZZ9mXmHkdTTpaefqsd4OIay4_hksd_I0fZMNbjk1SWD3i9Dz9BpdEPu8sBAAA)): - -```svelte -{#snippet figure({ src, caption, width, height })} -
- {caption} -
{caption}
-
-{/snippet} -``` - -Like function declarations, snippets can have an arbitrary number of parameters, which can have default values. You cannot use rest parameters however. - -## Snippet scope - -Snippets can be declared anywhere inside your component. They can reference values declared outside themselves, for example in the ` - -{#snippet hello(name)} -

hello {name}! {message}!

-{/snippet} - -{@render hello('alice')} -{@render hello('bob')} -``` - -...and they are 'visible' to everything in the same lexical scope (i.e. siblings, and children of those siblings): - -```svelte -
- {#snippet x()} - {#snippet y()}...{/snippet} - - - {@render y()} - {/snippet} - - - {@render y()} -
- - -{@render x()} -``` - -Snippets can reference themselves and each other ([demo](/#H4sIAAAAAAAAE2WPTQqDMBCFrxLiRqH1Zysi7TlqF1YnENBJSGJLCYGeo5tesUeosfYH3c2bee_jjaWMd6BpfrAU6x5oTvdS0g01V-mFPkNnYNRaDKrxGxto5FKCIaeu1kYwFkauwsoUWtZYPh_3W5FMY4U2mb3egL9kIwY0rbhgiO-sDTgjSEqSTvIDs-jiOP7i_MHuFGAL6p9BtiSbOTl0GtzCuihqE87cqtyam6WRGz_vRcsZh5bmRg3gju4Fptq_kzQBAAA=)): - -```svelte -{#snippet blastoff()} - 🚀 -{/snippet} - -{#snippet countdown(n)} - {#if n > 0} - {n}... - {@render countdown(n - 1)} - {:else} - {@render blastoff()} - {/if} -{/snippet} - -{@render countdown(10)} -``` - -## Passing snippets to components - -Within the template, snippets are values just like any other. As such, they can be passed to components as props ([demo](/#H4sIAAAAAAAAE41SwY6bMBD9lRGplKQlYRMpF5ZF7T_0ttmDwSZYJbZrT9pGlv-9g4Fkk-xhxYV5vHlvhjc-aWQnXJK_-kSxo0jy5IcxSZrg2fSF-yM6FFQ7fbJ1jxSuttJguVd7lEejLcJPVnUCGquPMF9nsVoPjfNnohGx1sohMU4SHbzAa4_t0UNvmcOcGUNDzFP4jeccdikYK2v6sIWQ3lErpui5cDdPF_LmkVy3wlp5Vd5e2U_rHYSe_kYjFtl1KeVnTkljBEIrGBd2sYy8AtsyLlBk9DYhJHtTR_UbBDWybkR8NkqHWyOr_y74ZMNLz9f9AoG6ePkOJLMHLBp-xISvcPf11r0YUuMM2Ysfkgngh5XphUYKkJWU_FFz2UjBkxztSYT0cihR4LOn0tGaPrql439N-7Uh0Dl8MVYbt1jeJ1Fg7xDb_Uw2Y18YQqZ_S2U5FH1pS__dCkWMa3C0uR0pfQRTg89kE4bLLLDS_Dxy_Eywuo1TAnPAw4fqY1rvtH3W9w35ZZMgvU3jq8LhedwkguCHRhT_cMU6eVA5dKLB5wGutCWjlTOslupAxxrxceKoD2hzhe2qbmXHF1v1bbOcNCtW_zpYfVI8h5kQ4qY3mueHTlesW2C7TOEO4hcdwzgf3Nc7cZxUKKC4yuNhvIX_MlV_Xk0EAAA=)): - -```svelte - - -{#snippet header()} - fruit - qty - price - total -{/snippet} - -{#snippet row(d)} - {d.name} - {d.qty} - {d.price} - {d.qty * d.price} -{/snippet} - - -``` - -As an authoring convenience, snippets declared directly _inside_ a component implicitly become props _on_ the component ([demo](/#H4sIAAAAAAAAE41Sy27bMBD8lYVcwHYrW4kBXxRFaP-htzgHSqQsojLJkuu2BqF_74qUrfhxCHQRh7MzO9z1SSM74ZL8zSeKHUSSJz-MSdIET2Y4uD-iQ0Fnp4-2HpDC1VYaLHdqh_JgtEX4yapOQGP1AebrLJzWsXD-QjQi1lo5JMZRooNXeBuwHXoYLHOYM2OoiXkKv_GUwzYFY2VNFxvo0xtqxRR9F-7z04X8fE-uW2GtnJQ3E_tpvYV-oL9Ti0U2hVJFjMMZslcfW-5DWj9zShojEFrBuLCLZR_9CmzLQCwy-psw8rxBgvkNhhpZd8F8NppE7Stbq_8u-GTKS8_XQ9Keqnl5BZP1AzTYP2bDV7i7_9hLEeda0iocNJeNFDzJ0R5Fn142JzA-uzsdBfLhldPxPdMhIPS0H1-M1cYtlnejwdBDfBXZjHXTFOg4BhuOtvTfrVDEmAZG2ew5ezYV-Ew2fVzVAivNTyPHzwSr29AlMAe8f6g-zuWDts-GusAmdBSkv3P7qnB4GpMEEHwsRPEPV6yTe5VDJxp8iXClLRmtnGG1VHva3oCPHQd9QJsrbFd1Kzu-2Khvz8uzZsXqX3urj4rnMBNCXNUG83zf6Yp1C2yXKdxA_KJjGOfRfb0Vh7MKDShEuV-M9_4_nq6svF4EAAA=)): - -```svelte - -
- {#snippet header()} - - - - - {/snippet} - - {#snippet row(d)} - - - - - {/snippet} -
fruitqtypricetotal{d.name}{d.qty}{d.price}{d.qty * d.price}
-``` - -Any content inside the component tags that is _not_ a snippet declaration implicitly becomes part of the `children` snippet ([demo](/#H4sIAAAAAAAAE41S247aMBD9lVFYCegGsiDxks1G7T_0bdkHJ3aI1cR27aEtsvzvtZ0LZeGhiiJ5js-cmTMemzS8YybJ320iSM-SPPmmVJImeFEhML9Yh8zHRp51HZDC1JorLI_iiLxXUiN8J1XHoNGyh-U2i9F2SFy-epon1lIY9IwzRwNv8B6wI1oIJXNYEqV8E8sUfuIlh0MKSvPaX-zBpZ-oFRH-m7m7l5m8uyfXLdOaX5X3V_bL9gAu0D98i0V2NSWKwQ4lSN7s0LKLbgtsyxgXmT9NiBe-iaP-DYISSTcj4bcLI7hSDEHL3yu6dkPfBdLS0m1o3nk-LW9gX-gBGss9ZsMXuLu32VjZBdfRaelft5eUN5zRJEd9Zi6dlyEy_ncdOm_IxsGlULe8o5qJNFgE5x_9SWmpzGp9N2-MXQxz4c2cOQ-lZWQyF0Jd2q_-mjI9U1fr4FBPE8iuKTbjjRt2sMBK0svIsQtG6jb2CsQAdQ_1x9f5R9tmIS-yPToK-tNkQRQGL6ObCIIdEpH9wQ3p-Enk0LEGXwe4ktoX2hhFai5Ofi0jPnYc9QF1LrDdRK-rvXjerSfNitQ_TlqeBc1hwRi7yY3F81MnK9KtsF2n8Amis44ilA7VtwfWTyr-kaKV-_X4cH8BTOhfRzcEAAA=)): - -```diff - -- {#snippet header()} -- -- -- -- -- {/snippet} -+ -+ -+ -+ - - -
fruitqtypricetotalfruitqtypricetotal
-``` - -```diff - - - -- {#if header} -+ {#if children} - -- {@render header()} -+ {@render children()} - - {/if} - - -
-``` - -> Note that you cannot have a prop called `children` if you also have content inside the component — for this reason, you should avoid having props with that name - -## Typing snippets - -Snippets implement the `Snippet` interface imported from `'svelte'`: - -```diff -- -``` - -With this change, red squigglies will appear if you try and use the component without providing a `data` prop and a `row` snippet. Notice that the type argument provided to `Snippet` is a tuple, since snippets can have multiple parameters. - -We can tighten things up further by declaring a generic, so that `data` and `row` refer to the same type: - -```diff -- -``` - -## Creating snippets programmatically - -In advanced scenarios, you may need to create a snippet programmatically. For this, you can use [`createRawSnippet`](/docs/imports#svelte-createrawsnippet) - -## Snippets and slots - -In Svelte 4, content can be passed to components using [slots](https://svelte.dev/docs/special-elements#slot). Snippets are more powerful and flexible, and as such slots are deprecated in Svelte 5. - -They continue to work, however, and you can mix and match snippets and slots in your components. - -When using custom elements, you should still use `` like before. In a future version, when Svelte removes its internal version of slots, it will leave those slots as-is, i.e. output a regular DOM tag instead of transforming it. diff --git a/sites/svelte-5-preview/src/routes/docs/content/01-api/04-event-handlers.md b/sites/svelte-5-preview/src/routes/docs/content/01-api/04-event-handlers.md deleted file mode 100644 index 5124ae291d3e..000000000000 --- a/sites/svelte-5-preview/src/routes/docs/content/01-api/04-event-handlers.md +++ /dev/null @@ -1,205 +0,0 @@ ---- -title: Event handlers ---- - -Event handlers have been given a facelift in Svelte 5. Whereas in Svelte 4 we use the `on:` directive to attach an event listener to an element, in Svelte 5 they are properties like any other: - -```diff - - -- -``` - -Since they're just properties, you can use the normal shorthand syntax... - -```svelte - - - -``` - -...though when using a named event handler function it's usually better to use a more descriptive name. - -Traditional `on:` event handlers will continue to work, but are deprecated in Svelte 5. - -## Component events - -In Svelte 4, components could emit events by creating a dispatcher with [`createEventDispatcher`](https://svelte.dev/docs/svelte#createeventdispatcher). - -This function is deprecated in Svelte 5. Instead, components should accept _callback props_ - which means you then pass functions as properties to these components ([demo](/#H4sIAAAAAAAACo1US27bMBC9yoBtELu2ZDmAG0CRhPYG3VddyPIwIUKRgjiOkwrcd9VFL5BV75cjFKQo2e5_IQnzeW-GM3zqGRcSDUs_9kxVDbKUvW9btmT01DrDPKAkZEtm9L6rnSczdSdaKkpVkmha3RF82Dct8E43cBmvnBEPsMsbl-QeiQRGfEbI4bWhinC23sxvxsh23xk6hnglDfqoKonvVU1CK-jQIM3m0HtOCmzrzVCDRg4P9j5bqmx1bFZlrjPfteKyIsz7WasP2M0hL85YFzn4QGAWHGbeX8D1Zj41S90-1LHuvcM_kp4QJPNhDNFpCUew8i32rwQfCnjObLsn0gq0qqWo7_Pez8AWCg-wraTUWmWrIcevIzNtpaCWlTF5ybZaNyUrXp6_fc9WLlKUqk9RGrS_SR7oSgaGniTmJTN1JTGFPomTNbzxbduSFcORXp6_fvEkE_FKcOun7PE-zRcIM2i1EW6NKXDxiLswWomcUkiCRbo9Ggexo7sU1klyETx3KG7v6MzFtaLIdea9D4eRCB8pqqS4VSnUqGhapRQKo4nnZmxNuJQIH1CRSUFpNV0g94nDbMajUFep8TB-SJDEV-YcoXUzpldKNNWQ7d1JvDHAdXeout0Z6t09PvGuatDAKT65gB7CMpL4LdjBfbU5819vxoAbz0lkcA9aCJthS9boneACdyx119guJ_E7jfyv-p10ewhqWkJQAFin5LbTrZkdJe5v-1HiXvzn6vz5rs-8hAJ7EJUtgn1y7f8ADN1MwGD_G-gBUWSLaModfnA-kELvvxb-Bl8sbLGY4L_O-5P9ATwVcA54BQAA)): - -```svelte - - - { - size += power; - if (size > 75) burst = true; - }} - deflate={(power) => { - if (size > 0) size -= power; - }} -/> - -{#if burst} - - 💥 -{:else} - - 🎈 - -{/if} -``` - -```svelte - - - - - -Pump power: {power} - -``` - -## Bubbling events - -Instead of doing ` -``` - -Note that this also means you can 'spread' event handlers onto the element along with other props: - -```svelte - - - -``` - -## Event modifiers - -In Svelte 4, you can add event modifiers to handlers: - -```svelte - -``` - -Modifiers are specific to `on:` and as such do not work with modern event handlers. Adding things like `event.preventDefault()` inside the handler itself is preferable, since all the logic lives in one place rather than being split between handler and modifiers. - -Since event handlers are just functions, you can create your own wrappers as necessary: - -```svelte - - - -``` - -There are three modifiers — `capture`, `passive` and `nonpassive` — that can't be expressed as wrapper functions, since they need to be applied when the event handler is bound rather than when it runs. - -For `capture`, we add the modifier to the event name: - -```svelte - -``` - -Changing the [`passive`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#using_passive_listeners) option of an event handler, meanwhile, is not something to be done lightly. If you have a use case for it — and you probably don't! - then you will need to use an action to apply the event handler yourself. - -## Multiple event handlers - -In Svelte 4, this is possible: - -```svelte - -``` - -This is something of an anti-pattern, since it impedes readability (if there are many attributes, it becomes harder to spot that there are two handlers unless they are right next to each other) and implies that the two handlers are independent, when in fact something like `event.stopImmediatePropagation()` inside `one` would prevent `two` from being called. - -Duplicate attributes/properties on elements — which now includes event handlers — are not allowed. Instead, do this: - -```svelte - -``` - -When spreading props, local event handlers must go _after_ the spread, or they risk being overwritten: - -```svelte - -``` - -## Why the change? - -By deprecating `createEventDispatcher` and the `on:` directive in favour of callback props and normal element properties, we: - -- reduce Svelte's learning curve -- remove boilerplate, particularly around `createEventDispatcher` -- remove the overhead of creating `CustomEvent` objects for events that may not even have listeners -- add the ability to spread event handlers -- add the ability to know which event handlers were provided to a component -- add the ability to express whether a given event handler is required or optional -- increase type safety (previously, it was effectively impossible for Svelte to guarantee that a component didn't emit a particular event) diff --git a/sites/svelte-5-preview/src/routes/docs/content/01-api/05-imports.md b/sites/svelte-5-preview/src/routes/docs/content/01-api/05-imports.md deleted file mode 100644 index 7cbec56e17ac..000000000000 --- a/sites/svelte-5-preview/src/routes/docs/content/01-api/05-imports.md +++ /dev/null @@ -1,277 +0,0 @@ ---- -title: Imports ---- - -As well as runes, Svelte 5 introduces a handful of new things you can import, alongside existing ones like `getContext`, `setContext` and `tick`. - -## `svelte` - -### `flushSync` - -Forces any pending effects (including DOM updates) to be applied immediately, rather than in the future. This is mainly useful in a testing context — you'll rarely need it in application code. - -```svelte - - -{count} - -``` - -### `mount` - -Instantiates a component and mounts it to the given target: - -```js -// @errors: 2322 -import { mount } from 'svelte'; -import App from './App.svelte'; - -const app = mount(App, { - target: document.querySelector('#app'), - props: { some: 'property' } -}); -``` - -Note that unlike calling `new App(...)` in Svelte 4, things like effects (including `onMount` callbacks, and action functions) will not run during `mount`. If you need to force pending effects to run (in the context of a test, for example) you can do so with `flushSync()`. - -### `hydrate` - -Like `mount`, but will reuse up any HTML rendered by Svelte's SSR output (from the [`render`](#svelte-server-render) function) inside the target and make it interactive: - -```js -// @errors: 2322 -import { hydrate } from 'svelte'; -import App from './App.svelte'; - -const app = hydrate(App, { - target: document.querySelector('#app'), - props: { some: 'property' } -}); -``` - -As with `mount`, effects will not run during `hydrate` — use `flushSync()` immediately afterwards if you need them to. - -### `unmount` - -Unmounts a component created with [`mount`](#svelte-mount) or [`hydrate`](#svelte-hydrate): - -```js -// @errors: 1109 -import { mount, unmount } from 'svelte'; -import App from './App.svelte'; - -const app = mount(App, {...}); - -// later -unmount(app); -``` - -### `untrack` - -To prevent something from being treated as an `$effect`/`$derived` dependency, use `untrack`: - -```svelte - -``` - -### `createRawSnippet` - -An advanced API designed for people building frameworks that integrate with Svelte, `createRawSnippet` allows you to create [snippets](/docs/snippets) programmatically for use with `{@render ...}` tags: - -```js -import { createRawSnippet } from 'svelte'; - -const greet = createRawSnippet((name) => { - return { - render: () => ` -

Hello ${name()}!

- `, - setup: (node) => { - $effect(() => { - node.textContent = `Hello ${name()}!`; - }); - } - }; -}); -``` - -The `render` function is called during server-side rendering, or during `mount` (but not during `hydrate`, because it already ran on the server), and must return HTML representing a single element. - -The `setup` function is called during `mount` or `hydrate` with that same element as its sole argument. It is responsible for ensuring that the DOM is updated when the arguments change their value — in this example, when `name` changes: - -```svelte -{@render greet(name)} -``` - -If `setup` returns a function, it will be called when the snippet is unmounted. If the snippet is fully static, you can omit the `setup` function altogether. - -## `svelte/reactivity` - -Svelte provides reactive `SvelteMap`, `SvelteSet`, `SvelteDate` and `SvelteURL` classes. These can be imported from `svelte/reactivity` and used just like their native counterparts. [Demo:](https://svelte-5-preview.vercel.app/#H4sIAAAAAAAAE32QwUrEMBBAf2XMpQrb9t7tFrx7UjxZYWM6NYFkEpJJ16X03yWK9OQeZ3iPecwqZmMxie5tFSQdik48hiAOgq-hDGlByygOIvkcVdn0SUUTeBhpZOOCjwwrvPxgr89PsMEcvYPqV2wjSsVmMXytjiMVR3lKDDlaOAHhZVfvK80cUte2-CVdsNgo79ogWVcPx5H6dj9M_V1dg9KSPjEBe2CNCZumgboeRuoNhczwYWjqFmkzntYcbROiZ6-83f5HtE9c3nADKUF_yEi9jnvQxVgLOUySEc464nwGSRMsRiEsGJO8mVeEbRAH4fxkZoOT6Dhm3N63b9_bGfOlAQAA) - -```svelte - - - - - - - -
- - - -``` - -## `svelte/events` - -Where possible, event handlers added with [attributes like `onclick`](/docs/event-handlers) use a technique called _event delegation_. It works by creating a single handler for each event type on the root DOM element, rather than creating a handler for each element, resulting in better performance and memory usage. - -Delegated event handlers run after other event handlers. In other words, a handler added programmatically with [`addEventListener`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener) will run _before_ a handler added declaratively with `onclick`, regardless of their relative position in the DOM ([demo](/#H4sIAAAAAAAAE41Sy2rDMBD8lUUXJxDiu-sYeugt_YK6h8RaN6LyykgrQzH6965shxJooQc_RhrNzA6aVW8sBlW9zYouA6pKPY-jOij-GjMIE1pGwcFF3-WVOnTejNy01LIZRucZZnD06iIxJOi9G6BYjxVPmZQfiwzaTBkL2ti73R5ODcwLiftIHRtHcLuQtuhlc9tpuSyBbyZAuLloNfhIELBzpO8E-Q_O4tG6j13hIqO_y0BvPOpiv0bhtJ1Y3pLoeNH6ZULiswmMJLZFZ033WRzuAvstdMseOXqCh9SriMfBTfgPnZxg-aYM6_KnS6pFCK6GdJVHPc0C01JyfY0slUnHi-JpfgjwSzUycdgmfOjFEP3RS1qdhJ8dYMDFt1yNmxxU0jRyCwanTW9Qq4p9xPSevgHI3m43QAIAAA==)). It also means that calling `event.stopPropagation()` inside a declarative handler _won't_ prevent the programmatic handler (created inside an action, for example) from running. - -To preserve the relative order, use `on` rather than `addEventListener` ([demo](/#H4sIAAAAAAAAE3VRy26DMBD8lZUvECkqdwpI_YB-QdJDgpfGqlkjex2pQv73rnmoStQeMB52dnZmmdVgLAZVn2ZFlxFVrd6mSR0Vf08ZhDtaRsHBRd_nL03ovZm4O9OZzTg5zzCDo3cXiSHB4N0IxdpWvD6RnuoV3pE4rLT8WGTQ5p6xoE20LA_QdjAvJB4i9WxE6nYhbdFLcaucuaqAbyZAuLloNfhIELB3pHeC3IOz-GLdZ1m4yOh3GRiMR10cViucto7l9MjRk9gvxdsRit6a_qs47q1rT8qvpvpdDjXChqshXWdT7SwwLVtrrpElnAguSu38EPCPEOItbF4eEhiifxKkdZLw8wQYcZlbrYO7bFTcdPJbR6fNYFCrmn3E9JF-AJZOg9MRAgAA)): - -```js -// @filename: index.ts -const element: Element = null as any; -// ---cut--- -import { on } from 'svelte/events'; - -const off = on(element, 'click', () => { - console.log('element was clicked'); -}); - -// later, if we need to remove the event listener: -off(); -``` - -`on` also accepts an optional fourth argument which matches the options argument for `addEventListener`. - -## `svelte/server` - -### `render` - -Only available on the server and when compiling with the `server` option. Takes a component and returns an object with `body` and `head` properties on it, which you can use to populate the HTML when server-rendering your app: - -```js -// @errors: 2724 2305 2307 -import { render } from 'svelte/server'; -import App from './App.svelte'; - -const result = render(App, { - props: { some: 'property' } -}); -``` - -If the `css` compiler option was set to `'injected'`, ` diff --git a/sites/svelte-5-preview/src/routes/status/data.json/+server.js b/sites/svelte-5-preview/src/routes/status/data.json/+server.js deleted file mode 100644 index cfa65b0065f0..000000000000 --- a/sites/svelte-5-preview/src/routes/status/data.json/+server.js +++ /dev/null @@ -1,6 +0,0 @@ -import { json } from '@sveltejs/kit'; -import results from '../results.json'; - -export function GET() { - return json(results); -} diff --git a/sites/svelte-5-preview/src/routes/svelte/[...path]/+server.js b/sites/svelte-5-preview/src/routes/svelte/[...path]/+server.js deleted file mode 100644 index 4e2254243526..000000000000 --- a/sites/svelte-5-preview/src/routes/svelte/[...path]/+server.js +++ /dev/null @@ -1,37 +0,0 @@ -import compiler_js from '../../../../../../packages/svelte/compiler/index.js?url'; -import package_json from '../../../../../../packages/svelte/package.json?url'; -import { read } from '$app/server'; - -const files = import.meta.glob('../../../../../../packages/svelte/src/**/*.js', { - eager: true, - query: '?url', - import: 'default' -}); - -const prefix = '../../../../../../packages/svelte/'; - -export const prerender = true; - -export function entries() { - const entries = Object.keys(files).map((path) => ({ path: path.replace(prefix, '') })); - entries.push({ path: 'compiler/index.js' }, { path: 'package.json' }); - return entries; -} - -// service worker requests files under this path to load the compiler and runtime -export async function GET({ params }) { - let file = ''; - - if (params.path === 'compiler/index.js') { - file = compiler_js; - } else if (params.path === 'package.json') { - file = package_json; - } else { - file = /** @type {string} */ (files[prefix + params.path]); - - // remove query string added by Vite when changing source code locally - file = file.split('?')[0]; - } - - return read(file); -} diff --git a/sites/svelte-5-preview/static/favicon.png b/sites/svelte-5-preview/static/favicon.png deleted file mode 100644 index 825b9e65af7c104cfb07089bb28659393b4f2097..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1571 zcmV+;2Hg3HP)Px)-AP12RCwC$UE6KzI1p6{F2N z1VK2vi|pOpn{~#djwYcWXTI_im_u^TJgMZ4JMOsSj!0ma>B?-(Hr@X&W@|R-$}W@Z zgj#$x=!~7LGqHW?IO8+*oE1MyDp!G=L0#^lUx?;!fXv@l^6SvTnf^ac{5OurzC#ZMYc20lI%HhX816AYVs1T3heS1*WaWH z%;x>)-J}YB5#CLzU@GBR6sXYrD>Vw(Fmt#|JP;+}<#6b63Ike{Fuo!?M{yEffez;| zp!PfsuaC)>h>-AdbnwN13g*1LowNjT5?+lFVd#9$!8Z9HA|$*6dQ8EHLu}U|obW6f z2%uGv?vr=KNq7YYa2Roj;|zooo<)lf=&2yxM@e`kM$CmCR#x>gI>I|*Ubr({5Y^rb zghxQU22N}F51}^yfDSt786oMTc!W&V;d?76)9KXX1 z+6Okem(d}YXmmOiZq$!IPk5t8nnS{%?+vDFz3BevmFNgpIod~R{>@#@5x9zJKEHLHv!gHeK~n)Ld!M8DB|Kfe%~123&Hz1Z(86nU7*G5chmyDe ziV7$pB7pJ=96hpxHv9rCR29%bLOXlKU<_13_M8x)6;P8E1Kz6G<&P?$P^%c!M5`2` zfY2zg;VK5~^>TJGQzc+33-n~gKt{{of8GzUkWmU110IgI0DLxRIM>0US|TsM=L|@F z0Bun8U!cRB7-2apz=y-7*UxOxz@Z0)@QM)9wSGki1AZ38ceG7Q72z5`i;i=J`ILzL z@iUO?SBBG-0cQuo+an4TsLy-g-x;8P4UVwk|D8{W@U1Zi z!M)+jqy@nQ$p?5tsHp-6J304Q={v-B>66$P0IDx&YT(`IcZ~bZfmn11#rXd7<5s}y zBi9eim&zQc0Dk|2>$bs0PnLmDfMP5lcXRY&cvJ=zKxI^f0%-d$tD!`LBf9^jMSYUA zI8U?CWdY@}cRq6{5~y+)#h1!*-HcGW@+gZ4B};0OnC~`xQOyH19z*TA!!BJ%9s0V3F?CAJ{hTd#*tf+ur-W9MOURF-@B77_-OshsY}6 zOXRY=5%C^*26z?l)1=$bz30!so5tfABdSYzO+H=CpV~aaUefmjvfZ3Ttu9W&W3Iu6 zROlh0MFA5h;my}8lB0tAV-Rvc2Zs_CCSJnx@d`**$idgy-iMob4dJWWw|21b4NB=LfsYp0Aeh{Ov)yztQi;eL4y5 zMi>8^SzKqk8~k?UiQK^^-5d8c%bV?$F8%X~czyiaKCI2=UH Date: Thu, 12 Dec 2024 00:22:30 +0100 Subject: [PATCH 45/62] fix: correctly handle ssr for `reactivity/window` (#14681) --- .changeset/khaki-guests-switch.md | 5 +++++ packages/svelte/src/reactivity/window/index.js | 8 +++++--- .../samples/reactivity-window/_expected.html | 1 + .../samples/reactivity-window/main.svelte | 14 ++++++++++++++ packages/svelte/types/index.d.ts | 2 +- 5 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 .changeset/khaki-guests-switch.md create mode 100644 packages/svelte/tests/server-side-rendering/samples/reactivity-window/_expected.html create mode 100644 packages/svelte/tests/server-side-rendering/samples/reactivity-window/main.svelte diff --git a/.changeset/khaki-guests-switch.md b/.changeset/khaki-guests-switch.md new file mode 100644 index 000000000000..f32e71084bef --- /dev/null +++ b/.changeset/khaki-guests-switch.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: correctly handle ssr for `reactivity/window` diff --git a/packages/svelte/src/reactivity/window/index.js b/packages/svelte/src/reactivity/window/index.js index 16e8b7b87b9a..8c50a5c440df 100644 --- a/packages/svelte/src/reactivity/window/index.js +++ b/packages/svelte/src/reactivity/window/index.js @@ -124,7 +124,7 @@ export const online = new ReactiveValue( * `devicePixelRatio.current` is a reactive view of `window.devicePixelRatio`. On the server it is `undefined`. * Note that behaviour differs between browsers — on Chrome it will respond to the current zoom level, * on Firefox and Safari it won't. - * @type {{ get current(): number }} + * @type {{ get current(): number | undefined }} * @since 5.11.0 */ export const devicePixelRatio = /* @__PURE__ */ new (class DevicePixelRatio { @@ -144,11 +144,13 @@ export const devicePixelRatio = /* @__PURE__ */ new (class DevicePixelRatio { } constructor() { - this.#update(); + if (BROWSER) { + this.#update(); + } } get current() { get(this.#dpr); - return window.devicePixelRatio; + return BROWSER ? window.devicePixelRatio : undefined; } })(); diff --git a/packages/svelte/tests/server-side-rendering/samples/reactivity-window/_expected.html b/packages/svelte/tests/server-side-rendering/samples/reactivity-window/_expected.html new file mode 100644 index 000000000000..ee65cb76c7d7 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/reactivity-window/_expected.html @@ -0,0 +1 @@ +

devicePixelRatio:

innerHeight:

innerWidth:

online:

outerHeight:

outerWidth:

screenLeft:

screenTop:

scrollX:

scrollY:

\ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/reactivity-window/main.svelte b/packages/svelte/tests/server-side-rendering/samples/reactivity-window/main.svelte new file mode 100644 index 000000000000..e84e41bf637b --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/reactivity-window/main.svelte @@ -0,0 +1,14 @@ + + +

devicePixelRatio: {devicePixelRatio.current}

+

innerHeight: {innerHeight.current}

+

innerWidth: {innerWidth.current}

+

online: {online.current}

+

outerHeight: {outerHeight.current}

+

outerWidth: {outerWidth.current}

+

screenLeft: {screenLeft.current}

+

screenTop: {screenTop.current}

+

scrollX: {scrollX.current}

+

scrollY: {scrollY.current}

\ No newline at end of file diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 206f9931f50c..435476d7033a 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -2015,7 +2015,7 @@ declare module 'svelte/reactivity/window' { * @since 5.11.0 */ export const devicePixelRatio: { - get current(): number; + get current(): number | undefined; }; class ReactiveValue { From 7aa80fc2a7ae1e622ec2c2b49d4654fa8aef257a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 11 Dec 2024 18:29:35 -0500 Subject: [PATCH 46/62] Version Packages (#14682) Co-authored-by: github-actions[bot] --- .changeset/khaki-guests-switch.md | 5 ----- packages/svelte/CHANGELOG.md | 6 ++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .changeset/khaki-guests-switch.md diff --git a/.changeset/khaki-guests-switch.md b/.changeset/khaki-guests-switch.md deleted file mode 100644 index f32e71084bef..000000000000 --- a/.changeset/khaki-guests-switch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: correctly handle ssr for `reactivity/window` diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index f4b7c18d7dc5..978e841bf83b 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.11.2 + +### Patch Changes + +- fix: correctly handle ssr for `reactivity/window` ([#14681](https://github.com/sveltejs/svelte/pull/14681)) + ## 5.11.1 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index f461a4b4c3a2..e95341a0bdef 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.11.1", + "version": "5.11.2", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 1cedefa3149b..e264eace2c12 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -6,5 +6,5 @@ * https://svelte.dev/docs/svelte-compiler#svelte-version * @type {string} */ -export const VERSION = '5.11.1'; +export const VERSION = '5.11.2'; export const PUBLIC_VERSION = '5'; From 8ba1b9ddd0bc65b9a790030aa8b7c73ae2990543 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Thu, 12 Dec 2024 10:42:10 +0000 Subject: [PATCH 47/62] fix: avoid mutation validation for invalidate_inner_signals (#14688) * fix: avoid mutation validation for invalidate_inner_signals * add test * Update packages/svelte/src/internal/client/runtime.js --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- .changeset/strong-pandas-provide.md | 5 +++++ .../svelte/src/internal/client/runtime.js | 7 ++++--- .../binding-interop-derived/Comp.svelte | 12 ++++++++++++ .../binding-interop-derived/_config.js | 5 +++++ .../binding-interop-derived/main.svelte | 19 +++++++++++++++++++ 5 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 .changeset/strong-pandas-provide.md create mode 100644 packages/svelte/tests/runtime-runes/samples/binding-interop-derived/Comp.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/binding-interop-derived/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/binding-interop-derived/main.svelte diff --git a/.changeset/strong-pandas-provide.md b/.changeset/strong-pandas-provide.md new file mode 100644 index 000000000000..0fe7e70c6d6a --- /dev/null +++ b/.changeset/strong-pandas-provide.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: avoid mutation validation for invalidate_inner_signals diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 4928419d16af..5d53ca336079 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -29,7 +29,7 @@ import { } from './constants.js'; import { flush_tasks } from './dom/task.js'; import { add_owner } from './dev/ownership.js'; -import { mutate, set, source } from './reactivity/sources.js'; +import { internal_set, set, source } from './reactivity/sources.js'; import { destroy_derived, execute_derived, update_derived } from './reactivity/deriveds.js'; import * as e from './errors.js'; import { lifecycle_outside_component } from '../shared/errors.js'; @@ -960,11 +960,12 @@ export function invalidate_inner_signals(fn) { if ((signal.f & LEGACY_DERIVED_PROP) !== 0) { for (const dep of /** @type {Derived} */ (signal).deps || []) { if ((dep.f & DERIVED) === 0) { - mutate(dep, null /* doesnt matter */); + // Use internal_set instead of set here and below to avoid mutation validation + internal_set(dep, dep.v); } } } else { - mutate(signal, null /* doesnt matter */); + internal_set(signal, signal.v); } } } diff --git a/packages/svelte/tests/runtime-runes/samples/binding-interop-derived/Comp.svelte b/packages/svelte/tests/runtime-runes/samples/binding-interop-derived/Comp.svelte new file mode 100644 index 000000000000..c3092997481b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/binding-interop-derived/Comp.svelte @@ -0,0 +1,12 @@ + + +{@render children({ props: snippetProps })} diff --git a/packages/svelte/tests/runtime-runes/samples/binding-interop-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/binding-interop-derived/_config.js new file mode 100644 index 000000000000..e52264c793c7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/binding-interop-derived/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + html: '' +}); diff --git a/packages/svelte/tests/runtime-runes/samples/binding-interop-derived/main.svelte b/packages/svelte/tests/runtime-runes/samples/binding-interop-derived/main.svelte new file mode 100644 index 000000000000..5900ddc84645 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/binding-interop-derived/main.svelte @@ -0,0 +1,19 @@ + + + + + + {#snippet children({ props })} + + {/snippet} + From ef8bd6adeb238f2d8ccc8c04547e9e16cb932c25 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 12 Dec 2024 12:16:51 -0500 Subject: [PATCH 48/62] fix: better handle hydration of script/style elements (alternative) (#14683) * alternative approach to #14624 * changeset * fix * lint --- .changeset/rotten-yaks-nail.md | 5 +++++ .../src/internal/client/dom/blocks/svelte-element.js | 6 ++++++ packages/svelte/src/internal/server/index.js | 9 +++------ packages/svelte/src/utils.js | 8 ++++++++ .../svelte/tests/hydration/samples/script/_config.js | 11 +++++++++++ .../tests/hydration/samples/script/_expected.html | 1 + .../svelte/tests/hydration/samples/script/main.svelte | 1 + 7 files changed, 35 insertions(+), 6 deletions(-) create mode 100644 .changeset/rotten-yaks-nail.md create mode 100644 packages/svelte/tests/hydration/samples/script/_config.js create mode 100644 packages/svelte/tests/hydration/samples/script/_expected.html create mode 100644 packages/svelte/tests/hydration/samples/script/main.svelte diff --git a/.changeset/rotten-yaks-nail.md b/.changeset/rotten-yaks-nail.md new file mode 100644 index 000000000000..bbe9b777ae81 --- /dev/null +++ b/.changeset/rotten-yaks-nail.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: better handle hydration of script/style elements diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js index 823b9a436253..35d2f223aed5 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js @@ -21,6 +21,7 @@ import { component_context, active_effect } from '../../runtime.js'; import { DEV } from 'esm-env'; import { EFFECT_TRANSPARENT } from '../../constants.js'; import { assign_nodes } from '../template.js'; +import { is_raw_text_element } from '../../../../utils.js'; /** * @param {Comment | Element} node @@ -116,6 +117,11 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio assign_nodes(element, element); if (render_fn) { + if (hydrating && is_raw_text_element(next_tag)) { + // prevent hydration glitches + element.append(document.createComment('')); + } + // If hydrating, use the existing ssr comment as the anchor so that the // inner open and close methods can pick up the existing nodes correctly var child_anchor = /** @type {TemplateNode} */ ( diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index b944c602b884..b8371b7e008f 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -16,7 +16,7 @@ import { DEV } from 'esm-env'; import { current_component, pop, push } from './context.js'; import { EMPTY_COMMENT, BLOCK_CLOSE, BLOCK_OPEN } from './hydration.js'; import { validate_store } from '../shared/validate.js'; -import { is_boolean_attribute, is_void } from '../../utils.js'; +import { is_boolean_attribute, is_raw_text_element, is_void } from '../../utils.js'; import { reset_elements } from './dev.js'; // https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 @@ -24,9 +24,6 @@ import { reset_elements } from './dev.js'; const INVALID_ATTR_NAME_CHAR_REGEX = /[\s'">/=\u{FDD0}-\u{FDEF}\u{FFFE}\u{FFFF}\u{1FFFE}\u{1FFFF}\u{2FFFE}\u{2FFFF}\u{3FFFE}\u{3FFFF}\u{4FFFE}\u{4FFFF}\u{5FFFE}\u{5FFFF}\u{6FFFE}\u{6FFFF}\u{7FFFE}\u{7FFFF}\u{8FFFE}\u{8FFFF}\u{9FFFE}\u{9FFFF}\u{AFFFE}\u{AFFFF}\u{BFFFE}\u{BFFFF}\u{CFFFE}\u{CFFFF}\u{DFFFE}\u{DFFFF}\u{EFFFE}\u{EFFFF}\u{FFFFE}\u{FFFFF}\u{10FFFE}\u{10FFFF}]/u; -/** List of elements that require raw contents and should not have SSR comments put in them */ -const RAW_TEXT_ELEMENTS = ['textarea', 'script', 'style', 'title']; - /** * @param {Payload} to_copy * @returns {Payload} @@ -64,13 +61,13 @@ export function element(payload, tag, attributes_fn = noop, children_fn = noop) payload.out += ''; if (tag) { - payload.out += `<${tag} `; + payload.out += `<${tag}`; attributes_fn(); payload.out += `>`; if (!is_void(tag)) { children_fn(); - if (!RAW_TEXT_ELEMENTS.includes(tag)) { + if (!is_raw_text_element(tag)) { payload.out += EMPTY_COMMENT; } payload.out += ``; diff --git a/packages/svelte/src/utils.js b/packages/svelte/src/utils.js index 75171c17865a..932440800795 100644 --- a/packages/svelte/src/utils.js +++ b/packages/svelte/src/utils.js @@ -441,3 +441,11 @@ const RUNES = /** @type {const} */ ([ export function is_rune(name) { return RUNES.includes(/** @type {RUNES[number]} */ (name)); } + +/** List of elements that require raw contents and should not have SSR comments put in them */ +const RAW_TEXT_ELEMENTS = /** @type {const} */ (['textarea', 'script', 'style', 'title']); + +/** @param {string} name */ +export function is_raw_text_element(name) { + return RAW_TEXT_ELEMENTS.includes(/** @type {RAW_TEXT_ELEMENTS[number]} */ (name)); +} diff --git a/packages/svelte/tests/hydration/samples/script/_config.js b/packages/svelte/tests/hydration/samples/script/_config.js new file mode 100644 index 000000000000..4723e4e454bc --- /dev/null +++ b/packages/svelte/tests/hydration/samples/script/_config.js @@ -0,0 +1,11 @@ +import { test } from '../../test'; + +export default test({ + snapshot(target) { + const script = target.querySelector('script'); + + return { + script + }; + } +}); diff --git a/packages/svelte/tests/hydration/samples/script/_expected.html b/packages/svelte/tests/hydration/samples/script/_expected.html new file mode 100644 index 000000000000..b3a4d922196b --- /dev/null +++ b/packages/svelte/tests/hydration/samples/script/_expected.html @@ -0,0 +1 @@ + diff --git a/packages/svelte/tests/hydration/samples/script/main.svelte b/packages/svelte/tests/hydration/samples/script/main.svelte new file mode 100644 index 000000000000..3904d47f730f --- /dev/null +++ b/packages/svelte/tests/hydration/samples/script/main.svelte @@ -0,0 +1 @@ +{"{}"} From 432db95358b3a8ad5a81e7958109b024ff2f4a8e Mon Sep 17 00:00:00 2001 From: James Glenn <47917431+JR-G@users.noreply.github.com> Date: Thu, 12 Dec 2024 19:19:13 +0000 Subject: [PATCH 49/62] docs: Update the linked playgrounds in the snippet docs (#14676) * Update the linked playgrounds in the snippet docs * Apply suggestions from code review --------- Co-authored-by: Rich Harris --- documentation/docs/03-template-syntax/06-snippet.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/documentation/docs/03-template-syntax/06-snippet.md b/documentation/docs/03-template-syntax/06-snippet.md index f8148f3dc30b..c9951d3f3414 100644 --- a/documentation/docs/03-template-syntax/06-snippet.md +++ b/documentation/docs/03-template-syntax/06-snippet.md @@ -112,7 +112,7 @@ Snippets can reference themselves and each other ([demo](/playground/untitled#H4 ## Passing snippets to components -Within the template, snippets are values just like any other. As such, they can be passed to components as props ([demo](/playground/untitled#H4sIAAAAAAAAE41SwY6bMBD9lRGplKQlYRMpF5ZF7T_0ttmDwSZYJbZrT9pGlv-9g4Fkk-xhxYV5vHlvhjc-aWQnXJK_-kSxo0jy5IcxSZrg2fSF-yM6FFQ7fbJ1jxSuttJguVd7lEejLcJPVnUCGquPMF9nsVoPjfNnohGx1sohMU4SHbzAa4_t0UNvmcOcGUNDzFP4jeccdikYK2v6sIWQ3lErpui5cDdPF_LmkVy3wlp5Vd5e2U_rHYSe_kYjFtl1KeVnTkljBEIrGBd2sYy8AtsyLlBk9DYhJHtTR_UbBDWybkR8NkqHWyOr_y74ZMNLz9f9AoG6ePkOJLMHLBp-xISvcPf11r0YUuMM2Ysfkgngh5XphUYKkJWU_FFz2UjBkxztSYT0cihR4LOn0tGaPrql439N-7Uh0Dl8MVYbt1jeJ1Fg7xDb_Uw2Y18YQqZ_S2U5FH1pS__dCkWMa3C0uR0pfQRTg89kE4bLLLDS_Dxy_Eywuo1TAnPAw4fqY1rvtH3W9w35ZZMgvU3jq8LhedwkguCHRhT_cMU6eVA5dKLB5wGutCWjlTOslupAxxrxceKoD2hzhe2qbmXHF1v1bbOcNCtW_zpYfVI8h5kQ4qY3mueHTlesW2C7TOEO4hcdwzgf3Nc7cZxUKKC4yuNhvIX_MlV_Xk0EAAA=)): +Within the template, snippets are values just like any other. As such, they can be passed to components as props ([demo](/playground/untitled#H4sIAAAAAAAAE3VS247aMBD9lZGpBGwDASRegonaPvQL2qdlH5zYEKvBNvbQLbL875VzAcKyj3PmzJnLGU8UOwqSkd8KJdaCk4TsZS0cyV49wYuJuQiQpGd-N2bu_ooaI1YwJ57hpVYoFDqSEepKKw3mO7VDeTTaIvxiRS1gb_URxvO0ibrS8WanIrHUyiHs7Vmigy28RmyHHmKvDMbMmFq4cQInvGSwTsBYWYoMVhCSB2rBFFPsyl0uruTlR3JZCWvlTXl1Yy_mawiR_rbZKZrellJ-5JQ0RiBUgnFhJ9OGR7HKmwVoilXeIye8DOJGfYCgRlZ3iE876TBsZPX7hPdteO75PC4QaIo8vwNPePmANQ2fMeEFHrLD7rR1jTNkW986E8C3KwfwVr8HSHOSEBT_kGRozyIkn_zQveXDL3rIfPJHtUDwzShJd_Qk3gQCbOGLsdq4yfTRJopRuin3I7nv6kL7ARRjmLdBDG3uv1mhuLA3V2mKtqNEf_oCn8p9aN-WYqH5peP4kWBl1UwJzAEPT9U7K--0fRrrWnPTXpCm1_EVdXjpNmlA8G1hPPyM1fKgMqjFHjctXGjLhZ05w0qpDhksGrybuNEHtJnCalZWsuaTlfq6nPaaBSv_HKw-K57BjzOiVj9ZKQYKzQjZodYFqydYTRN4gPhVzTDO2xnma3HsVWjaLjT8nbfwHy7Q5f2dBAAA)): ```svelte + +
+

Input/Textarea value

+ +
+ + + + + + + + +
+ + +
+ + + + + + + + +
+ + +
+ + + + + + + + +
+ +

Input checked

+ +
+ + + + +
+ + +
+ + + + +
+ + +
+ + + + +
+ + +
+ + +
+ + + +

Select (single)

+ + + + + + + + + + + + +

Select (multiple)

+ + + + + + + + +

Static values

+
+ + + +
+ + +
+ +

+ Bound values: + {value1} {value3} {value6} {value8} + {value9} {value12} {value14} {value16} + {value17} {value20} {value22} {value24} + {checked2} {checked4} + {checked6} {checked8} + {checked10} {checked12} + {checked14} + {selected1} + {selected2} + {selected3} + {selected4} + {selected5} + {selected6} +

diff --git a/packages/svelte/tests/runtime-runes/samples/form-default-value/_config.js b/packages/svelte/tests/runtime-runes/samples/form-default-value/_config.js index 5ef72aaa8ec2..35ab6e8ece44 100644 --- a/packages/svelte/tests/runtime-runes/samples/form-default-value/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/form-default-value/_config.js @@ -68,7 +68,6 @@ export default test({ assert.htmlEqual(test1_span.innerHTML, 'foo foo foo foo'); after_reset.push(() => { - console.log('-------------'); check_inputs(inputs, 'value', 'x'); assert.htmlEqual(test1_span.innerHTML, 'x x x x'); }); @@ -88,7 +87,6 @@ export default test({ assert.htmlEqual(test2_span.innerHTML, 'foo foo foo foo'); after_reset.push(() => { - console.log('-------------'); check_inputs(inputs, 'value', 'x'); assert.htmlEqual(test2_span.innerHTML, 'x x x x'); }); From 65db40986035e0d20b8e1da8e4006e16d2c12971 Mon Sep 17 00:00:00 2001 From: waedi Date: Thu, 12 Dec 2024 20:22:06 +0100 Subject: [PATCH 51/62] docs: typo in ## script_context_deprecated (#14694) * Fix typo in ## script_context_deprecated Changed +++context+++ to +++module+++ * regenerate --------- Co-authored-by: Rich Harris --- documentation/docs/98-reference/.generated/compile-warnings.md | 2 +- packages/svelte/messages/compile-warnings/template.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/documentation/docs/98-reference/.generated/compile-warnings.md b/documentation/docs/98-reference/.generated/compile-warnings.md index 88b1f278a679..f6f2585df23d 100644 --- a/documentation/docs/98-reference/.generated/compile-warnings.md +++ b/documentation/docs/98-reference/.generated/compile-warnings.md @@ -775,7 +775,7 @@ Reassignments of module-level declarations will not cause reactive statements to ``` ```svelte - ``` diff --git a/packages/svelte/messages/compile-warnings/template.md b/packages/svelte/messages/compile-warnings/template.md index b15b01241b8c..33e635bdb2c7 100644 --- a/packages/svelte/messages/compile-warnings/template.md +++ b/packages/svelte/messages/compile-warnings/template.md @@ -57,7 +57,7 @@ This code will work when the component is rendered on the client (which is why t > `context="module"` is deprecated, use the `module` attribute instead ```svelte - ``` From 88c2d6ea36f1b9bd6d1f52788c4e5a258c30868b Mon Sep 17 00:00:00 2001 From: Yang Pan <77009679+panyang05@users.noreply.github.com> Date: Thu, 12 Dec 2024 14:43:01 -0500 Subject: [PATCH 52/62] fix: Allow unquoted slash in attributes (#14615) * test: add sample for unquoted attributes * fix: handle unquoted slash in attributes * docs: allow unquoted slash in attributes * test: add additional sample for unquoted href attributes * fix: improve handling of self-closing tags with unquoted attributes * Update .changeset/long-boxes-flow.md --------- Co-authored-by: Rich Harris --- .changeset/long-boxes-flow.md | 5 ++ .../compiler/phases/1-parse/state/element.js | 20 ++++- .../samples/attribute-unquoted/input.svelte | 4 +- .../samples/attribute-unquoted/output.json | 80 ++++++++++++++++++- 4 files changed, 105 insertions(+), 4 deletions(-) create mode 100644 .changeset/long-boxes-flow.md diff --git a/.changeset/long-boxes-flow.md b/.changeset/long-boxes-flow.md new file mode 100644 index 000000000000..d249354b6769 --- /dev/null +++ b/.changeset/long-boxes-flow.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: allow unquoted slash in attributes diff --git a/packages/svelte/src/compiler/phases/1-parse/state/element.js b/packages/svelte/src/compiler/phases/1-parse/state/element.js index 45350bb1aec5..cd5cdd3e6e2c 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/element.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/element.js @@ -504,8 +504,24 @@ function read_attribute(parser) { let value = true; if (parser.eat('=')) { parser.allow_whitespace(); - value = read_attribute_value(parser); - end = parser.index; + + if (parser.template[parser.index] === '/' && parser.template[parser.index + 1] === '>') { + const char_start = parser.index; + parser.index++; // consume '/' + value = [ + { + start: char_start, + end: char_start + 1, + type: 'Text', + raw: '/', + data: '/' + } + ]; + end = parser.index; + } else { + value = read_attribute_value(parser); + end = parser.index; + } } else if (parser.match_regex(regex_starts_with_quote_characters)) { e.expected_token(parser.index, '='); } diff --git a/packages/svelte/tests/parser-legacy/samples/attribute-unquoted/input.svelte b/packages/svelte/tests/parser-legacy/samples/attribute-unquoted/input.svelte index 4bab0df72f3e..527d6eebf104 100644 --- a/packages/svelte/tests/parser-legacy/samples/attribute-unquoted/input.svelte +++ b/packages/svelte/tests/parser-legacy/samples/attribute-unquoted/input.svelte @@ -1 +1,3 @@ -
\ No newline at end of file +
+home +home \ No newline at end of file diff --git a/packages/svelte/tests/parser-legacy/samples/attribute-unquoted/output.json b/packages/svelte/tests/parser-legacy/samples/attribute-unquoted/output.json index 5df4d66ab668..ab2912a2c019 100644 --- a/packages/svelte/tests/parser-legacy/samples/attribute-unquoted/output.json +++ b/packages/svelte/tests/parser-legacy/samples/attribute-unquoted/output.json @@ -2,7 +2,7 @@ "html": { "type": "Fragment", "start": 0, - "end": 21, + "end": 62, "children": [ { "type": "Element", @@ -27,6 +27,84 @@ } ], "children": [] + }, + { + "type": "Text", + "start": 21, + "end": 22, + "raw": "\n", + "data": "\n" + }, + { + "type": "Element", + "start": 22, + "end": 40, + "name": "a", + "attributes": [ + { + "type": "Attribute", + "start": 25, + "end": 31, + "name": "href", + "value": [ + { + "start": 30, + "end": 31, + "type": "Text", + "raw": "/", + "data": "/" + } + ] + } + ], + "children": [ + { + "type": "Text", + "start": 32, + "end": 36, + "raw": "home", + "data": "home" + } + ] + }, + { + "type": "Text", + "start": 40, + "end": 41, + "raw": "\n", + "data": "\n" + }, + { + "type": "Element", + "start": 41, + "end": 62, + "name": "a", + "attributes": [ + { + "type": "Attribute", + "start": 44, + "end": 53, + "name": "href", + "value": [ + { + "start": 49, + "end": 53, + "type": "Text", + "raw": "/foo", + "data": "/foo" + } + ] + } + ], + "children": [ + { + "type": "Text", + "start": 54, + "end": 58, + "raw": "home", + "data": "home" + } + ] } ] } From 780041a51e1425167c30875dd54d906028128eff Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 14:55:51 -0500 Subject: [PATCH 53/62] Version Packages (#14689) Co-authored-by: github-actions[bot] --- .changeset/long-boxes-flow.md | 5 ----- .changeset/rotten-yaks-nail.md | 5 ----- .changeset/silent-tips-cover.md | 5 ----- .changeset/strong-pandas-provide.md | 5 ----- packages/svelte/CHANGELOG.md | 12 ++++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 7 files changed, 14 insertions(+), 22 deletions(-) delete mode 100644 .changeset/long-boxes-flow.md delete mode 100644 .changeset/rotten-yaks-nail.md delete mode 100644 .changeset/silent-tips-cover.md delete mode 100644 .changeset/strong-pandas-provide.md diff --git a/.changeset/long-boxes-flow.md b/.changeset/long-boxes-flow.md deleted file mode 100644 index d249354b6769..000000000000 --- a/.changeset/long-boxes-flow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: allow unquoted slash in attributes diff --git a/.changeset/rotten-yaks-nail.md b/.changeset/rotten-yaks-nail.md deleted file mode 100644 index bbe9b777ae81..000000000000 --- a/.changeset/rotten-yaks-nail.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: better handle hydration of script/style elements diff --git a/.changeset/silent-tips-cover.md b/.changeset/silent-tips-cover.md deleted file mode 100644 index 1f51572cda24..000000000000 --- a/.changeset/silent-tips-cover.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: make `defaultValue` work with spread diff --git a/.changeset/strong-pandas-provide.md b/.changeset/strong-pandas-provide.md deleted file mode 100644 index 0fe7e70c6d6a..000000000000 --- a/.changeset/strong-pandas-provide.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: avoid mutation validation for invalidate_inner_signals diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 978e841bf83b..6f3380ff5a72 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,17 @@ # svelte +## 5.11.3 + +### Patch Changes + +- fix: allow unquoted slash in attributes ([#14615](https://github.com/sveltejs/svelte/pull/14615)) + +- fix: better handle hydration of script/style elements ([#14683](https://github.com/sveltejs/svelte/pull/14683)) + +- fix: make `defaultValue` work with spread ([#14640](https://github.com/sveltejs/svelte/pull/14640)) + +- fix: avoid mutation validation for invalidate_inner_signals ([#14688](https://github.com/sveltejs/svelte/pull/14688)) + ## 5.11.2 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index e95341a0bdef..abcda8613de9 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.11.2", + "version": "5.11.3", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index e264eace2c12..39d89acd5316 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -6,5 +6,5 @@ * https://svelte.dev/docs/svelte-compiler#svelte-version * @type {string} */ -export const VERSION = '5.11.2'; +export const VERSION = '5.11.3'; export const PUBLIC_VERSION = '5'; From 2e0dcd78722d457f4c9e4b6db4ef4cb3ab26c037 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Thu, 12 Dec 2024 20:09:29 +0000 Subject: [PATCH 54/62] fix: ensure if block paths retain correct template namespacing (#14685) * fix: ensure if block paths retain correct template namespacing * add tests * address feedback * address feedback * simplify --------- Co-authored-by: Rich Harris --- .changeset/giant-moons-accept.md | 5 +++++ .../src/compiler/phases/3-transform/utils.js | 19 ++++++++++++++++++- .../svg-namespace-if-block/Child.svelte | 8 ++++++++ .../samples/svg-namespace-if-block/_config.js | 14 ++++++++++++++ .../svg-namespace-if-block/main.svelte | 7 +++++++ 5 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 .changeset/giant-moons-accept.md create mode 100644 packages/svelte/tests/runtime-runes/samples/svg-namespace-if-block/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/svg-namespace-if-block/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/svg-namespace-if-block/main.svelte diff --git a/.changeset/giant-moons-accept.md b/.changeset/giant-moons-accept.md new file mode 100644 index 000000000000..7940371d6fbb --- /dev/null +++ b/.changeset/giant-moons-accept.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: ensure if block paths retain correct template namespacing diff --git a/packages/svelte/src/compiler/phases/3-transform/utils.js b/packages/svelte/src/compiler/phases/3-transform/utils.js index 14fd3aa2e849..ffd07dd26a4c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/utils.js @@ -347,7 +347,24 @@ export function infer_namespace(namespace, parent, nodes) { } } - return namespace; + /** @type {Namespace | null} */ + let new_namespace = null; + + // Check the elements within the fragment and look for consistent namespaces. + // If we have no namespaces or they are mixed, then fallback to existing namespace + for (const node of nodes) { + if (node.type !== 'RegularElement') continue; + + if (node.metadata.mathml) { + new_namespace = new_namespace === null || new_namespace === 'mathml' ? 'mathml' : 'html'; + } else if (node.metadata.svg) { + new_namespace = new_namespace === null || new_namespace === 'svg' ? 'svg' : 'html'; + } else { + return 'html'; + } + } + + return new_namespace ?? namespace; } /** diff --git a/packages/svelte/tests/runtime-runes/samples/svg-namespace-if-block/Child.svelte b/packages/svelte/tests/runtime-runes/samples/svg-namespace-if-block/Child.svelte new file mode 100644 index 000000000000..53e6203ddef6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/svg-namespace-if-block/Child.svelte @@ -0,0 +1,8 @@ + +{#if true} + + + +{:else} +
lol
+{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/svg-namespace-if-block/_config.js b/packages/svelte/tests/runtime-runes/samples/svg-namespace-if-block/_config.js new file mode 100644 index 000000000000..22a2469bfb2b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/svg-namespace-if-block/_config.js @@ -0,0 +1,14 @@ +import { test, ok } from '../../test'; + +export default test({ + html: ``, + test({ assert, target }) { + const g = target.querySelector('g'); + const rect = target.querySelector('rect'); + ok(g); + ok(rect); + + assert.equal(g.namespaceURI, 'http://www.w3.org/2000/svg'); + assert.equal(rect.namespaceURI, 'http://www.w3.org/2000/svg'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/svg-namespace-if-block/main.svelte b/packages/svelte/tests/runtime-runes/samples/svg-namespace-if-block/main.svelte new file mode 100644 index 000000000000..8f6154462fa1 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/svg-namespace-if-block/main.svelte @@ -0,0 +1,7 @@ + + + + + From 61a0da8a5fdf5ac86431ceadfae0f54d38dc9a66 Mon Sep 17 00:00:00 2001 From: Mateusz Kadlubowski Date: Fri, 13 Dec 2024 04:15:40 +0800 Subject: [PATCH 55/62] feat: Expose more AST types from `"svelte/compiler"` (#14601) * add missing `SvelteBoundary` in `ElementLike` * make union of AST types public and exportable with `AST` namespace * apply AST types change to codebase * changeset * manually generate types * Add `AttributeLike` type * export namespace `Css` inside `AST` * manually generate types again * exported `Css` -> `CSS` * `Css` -> `AST.CSS` * fix Prettier issue * Apply suggestions from code review --------- Co-authored-by: Rich Harris --- .changeset/short-papayas-relate.md | 5 + packages/svelte/src/compiler/legacy.js | 6 +- packages/svelte/src/compiler/migrate/index.js | 10 +- .../src/compiler/phases/1-parse/index.js | 4 +- .../compiler/phases/1-parse/read/script.js | 4 +- .../src/compiler/phases/1-parse/read/style.js | 40 +++---- .../compiler/phases/1-parse/state/element.js | 18 +-- .../phases/2-analyze/css/css-analyze.js | 18 +-- .../phases/2-analyze/css/css-prune.js | 58 +++++----- .../compiler/phases/2-analyze/css/css-warn.js | 8 +- .../compiler/phases/2-analyze/css/utils.js | 14 +-- .../src/compiler/phases/2-analyze/index.js | 8 +- .../src/compiler/phases/2-analyze/types.d.ts | 8 +- .../visitors/AssignmentExpression.js | 1 - .../phases/2-analyze/visitors/Attribute.js | 4 +- .../2-analyze/visitors/CallExpression.js | 4 +- .../2-analyze/visitors/LabeledStatement.js | 4 +- .../phases/2-analyze/visitors/SnippetBlock.js | 4 +- .../phases/2-analyze/visitors/shared/a11y.js | 6 +- .../2-analyze/visitors/shared/attribute.js | 4 +- .../2-analyze/visitors/shared/fragment.js | 4 +- .../3-transform/client/transform-client.js | 10 +- .../phases/3-transform/client/types.d.ts | 10 +- .../phases/3-transform/client/utils.js | 4 +- .../client/visitors/BindDirective.js | 6 +- .../client/visitors/shared/component.js | 4 +- .../client/visitors/shared/events.js | 4 +- .../client/visitors/shared/fragment.js | 6 +- .../client/visitors/shared/utils.js | 6 +- .../compiler/phases/3-transform/css/index.js | 24 ++-- .../3-transform/server/transform-server.js | 10 +- .../phases/3-transform/server/types.d.ts | 10 +- .../server/visitors/AssignmentExpression.js | 4 +- .../server/visitors/shared/component.js | 4 +- .../server/visitors/shared/element.js | 4 +- .../server/visitors/shared/utils.js | 4 +- .../compiler/phases/3-transform/types.d.ts | 4 +- .../src/compiler/phases/3-transform/utils.js | 20 ++-- packages/svelte/src/compiler/phases/css.js | 4 +- packages/svelte/src/compiler/phases/nodes.js | 4 +- packages/svelte/src/compiler/phases/scope.js | 26 ++--- .../svelte/src/compiler/phases/types.d.ts | 8 +- packages/svelte/src/compiler/state.js | 8 +- packages/svelte/src/compiler/types/css.d.ts | 2 +- packages/svelte/src/compiler/types/index.d.ts | 6 +- .../src/compiler/types/legacy-nodes.d.ts | 4 +- .../svelte/src/compiler/types/template.d.ts | 101 ++++++++-------- packages/svelte/src/compiler/utils/ast.js | 4 +- packages/svelte/src/compiler/utils/slot.js | 4 +- packages/svelte/types/index.d.ts | 109 +++++++++++------- 50 files changed, 343 insertions(+), 303 deletions(-) create mode 100644 .changeset/short-papayas-relate.md diff --git a/.changeset/short-papayas-relate.md b/.changeset/short-papayas-relate.md new file mode 100644 index 000000000000..430c507a0cfc --- /dev/null +++ b/.changeset/short-papayas-relate.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: expose more AST types from `"svelte/compiler"` diff --git a/packages/svelte/src/compiler/legacy.js b/packages/svelte/src/compiler/legacy.js index 2d90988936a8..e3f88c8f1d23 100644 --- a/packages/svelte/src/compiler/legacy.js +++ b/packages/svelte/src/compiler/legacy.js @@ -1,5 +1,5 @@ /** @import { Expression } from 'estree' */ -/** @import { AST, SvelteNode, TemplateNode } from '#compiler' */ +/** @import { AST } from '#compiler' */ /** @import * as Legacy from './types/legacy-nodes.js' */ import { walk } from 'zimmerframe'; import { @@ -11,7 +11,7 @@ import { extract_svelte_ignore } from './utils/extract_svelte_ignore.js'; /** * Some of the legacy Svelte AST nodes remove whitespace from the start and end of their children. - * @param {TemplateNode[]} nodes + * @param {AST.TemplateNode[]} nodes */ function remove_surrounding_whitespace_nodes(nodes) { const first = nodes.at(0); @@ -40,7 +40,7 @@ function remove_surrounding_whitespace_nodes(nodes) { * @returns {Legacy.LegacyRoot} */ export function convert(source, ast) { - const root = /** @type {SvelteNode | Legacy.LegacySvelteNode} */ (ast); + const root = /** @type {AST.SvelteNode | Legacy.LegacySvelteNode} */ (ast); return /** @type {Legacy.LegacyRoot} */ ( walk(root, null, { diff --git a/packages/svelte/src/compiler/migrate/index.js b/packages/svelte/src/compiler/migrate/index.js index 88f9bbf0eed6..1bb7a69a20f9 100644 --- a/packages/svelte/src/compiler/migrate/index.js +++ b/packages/svelte/src/compiler/migrate/index.js @@ -2,7 +2,7 @@ /** @import { Visitors } from 'zimmerframe' */ /** @import { ComponentAnalysis } from '../phases/types.js' */ /** @import { Scope, ScopeRoot } from '../phases/scope.js' */ -/** @import { AST, Binding, SvelteNode, ValidatedCompileOptions } from '#compiler' */ +/** @import { AST, Binding, ValidatedCompileOptions } from '#compiler' */ import MagicString from 'magic-string'; import { walk } from 'zimmerframe'; import { parse } from '../phases/1-parse/index.js'; @@ -479,7 +479,7 @@ export function migrate(source, { filename, use_ts } = {}) { * }} State */ -/** @type {Visitors} */ +/** @type {Visitors} */ const instance_script = { _(node, { state, next }) { // @ts-expect-error @@ -1050,7 +1050,7 @@ function trim_block(state, start, end) { } } -/** @type {Visitors} */ +/** @type {Visitors} */ const template = { Identifier(node, { state, path }) { handle_identifier(node, state, path); @@ -1410,7 +1410,7 @@ const template = { /** * @param {AST.RegularElement | AST.SvelteElement | AST.SvelteComponent | AST.Component | AST.SlotElement | AST.SvelteFragment} node - * @param {SvelteNode[]} path + * @param {AST.SvelteNode[]} path * @param {State} state */ function migrate_slot_usage(node, path, state) { @@ -1580,7 +1580,7 @@ function migrate_slot_usage(node, path, state) { /** * @param {VariableDeclarator} declarator * @param {State} state - * @param {SvelteNode[]} path + * @param {AST.SvelteNode[]} path */ function extract_type_and_comment(declarator, state, path) { const str = state.str; diff --git a/packages/svelte/src/compiler/phases/1-parse/index.js b/packages/svelte/src/compiler/phases/1-parse/index.js index 7639c2f0eda8..c3a8a098d319 100644 --- a/packages/svelte/src/compiler/phases/1-parse/index.js +++ b/packages/svelte/src/compiler/phases/1-parse/index.js @@ -1,4 +1,4 @@ -/** @import { AST, TemplateNode } from '#compiler' */ +/** @import { AST } from '#compiler' */ // @ts-expect-error acorn type definitions are borked in the release we use import { isIdentifierStart, isIdentifierChar } from 'acorn'; import fragment from './state/fragment.js'; @@ -28,7 +28,7 @@ export class Parser { /** Whether we're parsing in TypeScript mode */ ts = false; - /** @type {TemplateNode[]} */ + /** @type {AST.TemplateNode[]} */ stack = []; /** @type {AST.Fragment[]} */ diff --git a/packages/svelte/src/compiler/phases/1-parse/read/script.js b/packages/svelte/src/compiler/phases/1-parse/read/script.js index 87367aff0805..9d9ed3a1efdf 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/script.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/script.js @@ -1,5 +1,5 @@ /** @import { Program } from 'estree' */ -/** @import { AST, Directive } from '#compiler' */ +/** @import { AST } from '#compiler' */ /** @import { Parser } from '../index.js' */ import * as acorn from '../acorn.js'; import { regex_not_newline_characters } from '../../patterns.js'; @@ -16,7 +16,7 @@ const ALLOWED_ATTRIBUTES = ['context', 'generics', 'lang', 'module']; /** * @param {Parser} parser * @param {number} start - * @param {Array} attributes + * @param {Array} attributes * @returns {AST.Script} */ export function read_script(parser, start, attributes) { diff --git a/packages/svelte/src/compiler/phases/1-parse/read/style.js b/packages/svelte/src/compiler/phases/1-parse/read/style.js index aa835a1d96fe..29e8a0e54143 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/style.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/style.js @@ -1,4 +1,4 @@ -/** @import { AST, Css, Directive } from '#compiler' */ +/** @import { AST } from '#compiler' */ /** @import { Parser } from '../index.js' */ import * as e from '../../../errors.js'; @@ -18,8 +18,8 @@ const REGEX_HTML_COMMENT_CLOSE = /-->/; /** * @param {Parser} parser * @param {number} start - * @param {Array} attributes - * @returns {Css.StyleSheet} + * @param {Array} attributes + * @returns {AST.CSS.StyleSheet} */ export default function read_style(parser, start, attributes) { const content_start = parser.index; @@ -49,7 +49,7 @@ export default function read_style(parser, start, attributes) { * @returns {any[]} */ function read_body(parser, close) { - /** @type {Array} */ + /** @type {Array} */ const children = []; while (parser.index < parser.template.length) { @@ -71,7 +71,7 @@ function read_body(parser, close) { /** * @param {Parser} parser - * @returns {Css.Atrule} + * @returns {AST.CSS.Atrule} */ function read_at_rule(parser) { const start = parser.index; @@ -81,7 +81,7 @@ function read_at_rule(parser) { const prelude = read_value(parser); - /** @type {Css.Block | null} */ + /** @type {AST.CSS.Block | null} */ let block = null; if (parser.match('{')) { @@ -104,7 +104,7 @@ function read_at_rule(parser) { /** * @param {Parser} parser - * @returns {Css.Rule} + * @returns {AST.CSS.Rule} */ function read_rule(parser) { const start = parser.index; @@ -126,10 +126,10 @@ function read_rule(parser) { /** * @param {Parser} parser * @param {boolean} [inside_pseudo_class] - * @returns {Css.SelectorList} + * @returns {AST.CSS.SelectorList} */ function read_selector_list(parser, inside_pseudo_class = false) { - /** @type {Css.ComplexSelector[]} */ + /** @type {AST.CSS.ComplexSelector[]} */ const children = []; allow_comment_or_whitespace(parser); @@ -162,18 +162,18 @@ function read_selector_list(parser, inside_pseudo_class = false) { /** * @param {Parser} parser * @param {boolean} [inside_pseudo_class] - * @returns {Css.ComplexSelector} + * @returns {AST.CSS.ComplexSelector} */ function read_selector(parser, inside_pseudo_class = false) { const list_start = parser.index; - /** @type {Css.RelativeSelector[]} */ + /** @type {AST.CSS.RelativeSelector[]} */ const children = []; /** - * @param {Css.Combinator | null} combinator + * @param {AST.CSS.Combinator | null} combinator * @param {number} start - * @returns {Css.RelativeSelector} + * @returns {AST.CSS.RelativeSelector} */ function create_selector(combinator, start) { return { @@ -190,7 +190,7 @@ function read_selector(parser, inside_pseudo_class = false) { }; } - /** @type {Css.RelativeSelector} */ + /** @type {AST.CSS.RelativeSelector} */ let relative_selector = create_selector(null, parser.index); while (parser.index < parser.template.length) { @@ -247,7 +247,7 @@ function read_selector(parser, inside_pseudo_class = false) { } else if (parser.eat(':')) { const name = read_identifier(parser); - /** @type {null | Css.SelectorList} */ + /** @type {null | AST.CSS.SelectorList} */ let args = null; if (parser.eat('(')) { @@ -372,7 +372,7 @@ function read_selector(parser, inside_pseudo_class = false) { /** * @param {Parser} parser - * @returns {Css.Combinator | null} + * @returns {AST.CSS.Combinator | null} */ function read_combinator(parser) { const start = parser.index; @@ -407,14 +407,14 @@ function read_combinator(parser) { /** * @param {Parser} parser - * @returns {Css.Block} + * @returns {AST.CSS.Block} */ function read_block(parser) { const start = parser.index; parser.eat('{', true); - /** @type {Array} */ + /** @type {Array} */ const children = []; while (parser.index < parser.template.length) { @@ -441,7 +441,7 @@ function read_block(parser) { * Reads a declaration, rule or at-rule * * @param {Parser} parser - * @returns {Css.Declaration | Css.Rule | Css.Atrule} + * @returns {AST.CSS.Declaration | AST.CSS.Rule | AST.CSS.Atrule} */ function read_block_item(parser) { if (parser.match('@')) { @@ -460,7 +460,7 @@ function read_block_item(parser) { /** * @param {Parser} parser - * @returns {Css.Declaration} + * @returns {AST.CSS.Declaration} */ function read_declaration(parser) { const start = parser.index; diff --git a/packages/svelte/src/compiler/phases/1-parse/state/element.js b/packages/svelte/src/compiler/phases/1-parse/state/element.js index cd5cdd3e6e2c..2b6a88f7bd73 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/element.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/element.js @@ -1,5 +1,5 @@ /** @import { Expression } from 'estree' */ -/** @import { AST, Directive, ElementLike, TemplateNode } from '#compiler' */ +/** @import { AST } from '#compiler' */ /** @import { Parser } from '../index.js' */ import { is_void } from '../../../../utils.js'; import read_expression from '../read/expression.js'; @@ -28,7 +28,7 @@ export const regex_valid_component_name = // (must start with uppercase letter if no dots, can contain dots) /^(?:\p{Lu}[$\u200c\u200d\p{ID_Continue}.]*|\p{ID_Start}[$\u200c\u200d\p{ID_Continue}]*(?:\.[$\u200c\u200d\p{ID_Continue}]+)+)$/u; -/** @type {Map} */ +/** @type {Map} */ const root_only_meta_tags = new Map([ ['svelte:head', 'SvelteHead'], ['svelte:options', 'SvelteOptions'], @@ -37,7 +37,7 @@ const root_only_meta_tags = new Map([ ['svelte:body', 'SvelteBody'] ]); -/** @type {Map} */ +/** @type {Map} */ const meta_tags = new Map([ ...root_only_meta_tags, ['svelte:element', 'SvelteElement'], @@ -137,7 +137,7 @@ export default function element(parser) { ? 'SlotElement' : 'RegularElement'; - /** @type {ElementLike} */ + /** @type {AST.ElementLike} */ const element = type === 'RegularElement' ? { @@ -155,7 +155,7 @@ export default function element(parser) { path: [] } } - : /** @type {ElementLike} */ ({ + : /** @type {AST.ElementLike} */ ({ type, start, end: -1, @@ -358,7 +358,7 @@ export default function element(parser) { } } -/** @param {TemplateNode[]} stack */ +/** @param {AST.TemplateNode[]} stack */ function parent_is_head(stack) { let i = stack.length; while (i--) { @@ -369,7 +369,7 @@ function parent_is_head(stack) { return false; } -/** @param {TemplateNode[]} stack */ +/** @param {AST.TemplateNode[]} stack */ function parent_is_shadowroot_template(stack) { // https://developer.chrome.com/docs/css-ui/declarative-shadow-dom#building_a_declarative_shadow_root let i = stack.length; @@ -433,7 +433,7 @@ function read_static_attribute(parser) { /** * @param {Parser} parser - * @returns {AST.Attribute | AST.SpreadAttribute | Directive | null} + * @returns {AST.Attribute | AST.SpreadAttribute | AST.Directive | null} */ function read_attribute(parser) { const start = parser.index; @@ -564,7 +564,7 @@ function read_attribute(parser) { } } - /** @type {Directive} */ + /** @type {AST.Directive} */ const directive = { start, end, diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js index 1dd2d9ae7c36..b8c88a102394 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js @@ -1,5 +1,5 @@ /** @import { ComponentAnalysis } from '../../types.js' */ -/** @import { Css } from '#compiler' */ +/** @import { AST } from '#compiler' */ /** @import { Visitors } from 'zimmerframe' */ import { walk } from 'zimmerframe'; import * as e from '../../../errors.js'; @@ -8,17 +8,17 @@ import { is_global, is_unscoped_pseudo_class } from './utils.js'; /** * @typedef {Visitors< - * Css.Node, + * AST.CSS.Node, * { * keyframes: string[]; - * rule: Css.Rule | null; + * rule: AST.CSS.Rule | null; * } * >} CssVisitors */ /** * True if is `:global` - * @param {Css.SimpleSelector} simple_selector + * @param {AST.CSS.SimpleSelector} simple_selector */ function is_global_block_selector(simple_selector) { return ( @@ -112,7 +112,7 @@ const css_visitors = { } }, RelativeSelector(node, context) { - const parent = /** @type {Css.ComplexSelector} */ (context.path.at(-1)); + const parent = /** @type {AST.CSS.ComplexSelector} */ (context.path.at(-1)); if ( node.combinator != null && @@ -149,7 +149,7 @@ const css_visitors = { if (node.metadata.is_global_like || node.metadata.is_global) { // So that nested selectors like `:root:not(.x)` are not marked as unused for (const child of node.selectors) { - walk(/** @type {Css.Node} */ (child), null, { + walk(/** @type {AST.CSS.Node} */ (child), null, { ComplexSelector(node, context) { node.metadata.used = true; context.next(); @@ -177,7 +177,7 @@ const css_visitors = { if (idx !== -1) { is_global_block = true; for (let i = idx + 1; i < child.selectors.length; i++) { - walk(/** @type {Css.Node} */ (child.selectors[i]), null, { + walk(/** @type {AST.CSS.Node} */ (child.selectors[i]), null, { ComplexSelector(node) { node.metadata.used = true; } @@ -240,7 +240,7 @@ const css_visitors = { }); }, NestingSelector(node, context) { - const rule = /** @type {Css.Rule} */ (context.state.rule); + const rule = /** @type {AST.CSS.Rule} */ (context.state.rule); const parent_rule = rule.metadata.parent_rule; if (!parent_rule) { @@ -271,7 +271,7 @@ const css_visitors = { }; /** - * @param {Css.StyleSheet} stylesheet + * @param {AST.CSS.StyleSheet} stylesheet * @param {ComponentAnalysis} analysis */ export function analyze_css(stylesheet, analysis) { diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js index d017b215f2af..35bc675166ae 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js @@ -14,7 +14,7 @@ const whitelist_attribute_selector = new Map([ ['dialog', ['open']] ]); -/** @type {Compiler.Css.Combinator} */ +/** @type {Compiler.AST.CSS.Combinator} */ const descendant_combinator = { type: 'Combinator', name: ' ', @@ -22,7 +22,7 @@ const descendant_combinator = { end: -1 }; -/** @type {Compiler.Css.RelativeSelector} */ +/** @type {Compiler.AST.CSS.RelativeSelector} */ const nesting_selector = { type: 'RelativeSelector', start: -1, @@ -51,11 +51,11 @@ const seen = new Set(); /** * - * @param {Compiler.Css.StyleSheet} stylesheet + * @param {Compiler.AST.CSS.StyleSheet} stylesheet * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element */ export function prune(stylesheet, element) { - walk(/** @type {Compiler.Css.Node} */ (stylesheet), null, { + walk(/** @type {Compiler.AST.CSS.Node} */ (stylesheet), null, { Rule(node, context) { if (node.metadata.is_global_block) { context.visit(node.prelude); @@ -69,7 +69,11 @@ export function prune(stylesheet, element) { seen.clear(); if ( - apply_selector(selectors, /** @type {Compiler.Css.Rule} */ (node.metadata.rule), element) + apply_selector( + selectors, + /** @type {Compiler.AST.CSS.Rule} */ (node.metadata.rule), + element + ) ) { node.metadata.used = true; } @@ -86,7 +90,7 @@ export function prune(stylesheet, element) { * Also searches them for any existing `&` selectors and adds one if none are found. * This ensures we traverse up to the parent rule when the inner selectors match and we're * trying to see if the parent rule also matches. - * @param {Compiler.Css.ComplexSelector} node + * @param {Compiler.AST.CSS.ComplexSelector} node */ function get_relative_selectors(node) { const selectors = truncate(node); @@ -124,7 +128,7 @@ function get_relative_selectors(node) { /** * Discard trailing `:global(...)` selectors, these are unused for scoping purposes - * @param {Compiler.Css.ComplexSelector} node + * @param {Compiler.AST.CSS.ComplexSelector} node */ function truncate(node) { const i = node.children.findLastIndex(({ metadata, selectors }) => { @@ -152,8 +156,8 @@ function truncate(node) { } /** - * @param {Compiler.Css.RelativeSelector[]} relative_selectors - * @param {Compiler.Css.Rule} rule + * @param {Compiler.AST.CSS.RelativeSelector[]} relative_selectors + * @param {Compiler.AST.CSS.Rule} rule * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element * @returns {boolean} */ @@ -178,9 +182,9 @@ function apply_selector(relative_selectors, rule, element) { } /** - * @param {Compiler.Css.RelativeSelector} relative_selector - * @param {Compiler.Css.RelativeSelector[]} parent_selectors - * @param {Compiler.Css.Rule} rule + * @param {Compiler.AST.CSS.RelativeSelector} relative_selector + * @param {Compiler.AST.CSS.RelativeSelector[]} parent_selectors + * @param {Compiler.AST.CSS.Rule} rule * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node * @returns {boolean} */ @@ -263,8 +267,8 @@ function apply_combinator(relative_selector, parent_selectors, rule, node) { * it's a `:global(...)` or unscopeable selector, or * is an `:is(...)` or `:where(...)` selector that contains * a global selector - * @param {Compiler.Css.RelativeSelector} selector - * @param {Compiler.Css.Rule} rule + * @param {Compiler.AST.CSS.RelativeSelector} selector + * @param {Compiler.AST.CSS.Rule} rule */ function is_global(selector, rule) { if (selector.metadata.is_global || selector.metadata.is_global_like) { @@ -272,7 +276,7 @@ function is_global(selector, rule) { } for (const s of selector.selectors) { - /** @type {Compiler.Css.SelectorList | null} */ + /** @type {Compiler.AST.CSS.SelectorList | null} */ let selector_list = null; let owner = rule; @@ -283,7 +287,7 @@ function is_global(selector, rule) { } if (s.type === 'NestingSelector') { - owner = /** @type {Compiler.Css.Rule} */ (rule.metadata.parent_rule); + owner = /** @type {Compiler.AST.CSS.Rule} */ (rule.metadata.parent_rule); selector_list = owner.prelude; } @@ -306,8 +310,8 @@ const regex_backslash_and_following_character = /\\(.)/g; /** * Ensure that `element` satisfies each simple selector in `relative_selector` * - * @param {Compiler.Css.RelativeSelector} relative_selector - * @param {Compiler.Css.Rule} rule + * @param {Compiler.AST.CSS.RelativeSelector} relative_selector + * @param {Compiler.AST.CSS.Rule} rule * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element * @returns {boolean} */ @@ -352,7 +356,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element) const seen = new Set(); /** - * @param {Compiler.SvelteNode} node + * @param {Compiler.AST.SvelteNode} node * @param {{ is_child: boolean }} state */ function walk_children(node, state) { @@ -389,7 +393,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element) // upwards and back-to-front, we need to first check the selectors inside :has(...), then check the rest of the // selector in a way that is similar to ancestor matching. In a sense, we're treating `.x:has(.y)` as `.x .y`. for (const has_selector of has_selectors) { - const complex_selectors = /** @type {Compiler.Css.SelectorList} */ (has_selector.args) + const complex_selectors = /** @type {Compiler.AST.CSS.SelectorList} */ (has_selector.args) .children; let matched = false; @@ -578,7 +582,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element) case 'NestingSelector': { let matched = false; - const parent = /** @type {Compiler.Css.Rule} */ (rule.metadata.parent_rule); + const parent = /** @type {Compiler.AST.CSS.Rule} */ (rule.metadata.parent_rule); for (const complex_selector of parent.prelude.children) { if ( @@ -611,9 +615,9 @@ function get_following_sibling_elements(element, include_self) { const path = element.metadata.path; let i = path.length; - /** @type {Compiler.SvelteNode} */ + /** @type {Compiler.AST.SvelteNode} */ let start = element; - let nodes = /** @type {Compiler.SvelteNode[]} */ ( + let nodes = /** @type {Compiler.AST.SvelteNode[]} */ ( /** @type {Compiler.AST.Fragment} */ (path[0]).nodes ); @@ -639,7 +643,7 @@ function get_following_sibling_elements(element, include_self) { const seen = new Set(); - /** @param {Compiler.SvelteNode} node */ + /** @param {Compiler.AST.SvelteNode} node */ function get_siblings(node) { walk(node, null, { RegularElement(node) { @@ -836,7 +840,7 @@ function get_possible_element_siblings(node, adjacent_only, seen = new Set()) { const result = new Map(); const path = node.metadata.path; - /** @type {Compiler.SvelteNode} */ + /** @type {Compiler.AST.SvelteNode} */ let current = node; let i = path.length; @@ -1008,7 +1012,7 @@ function higher_existence(exist1, exist2) { } /** - * @param {Compiler.SvelteNode[]} children + * @param {Compiler.AST.SvelteNode[]} children * @param {boolean} adjacent_only */ function loop_child(children, adjacent_only) { @@ -1038,7 +1042,7 @@ function loop_child(children, adjacent_only) { } /** - * @param {Compiler.SvelteNode} node + * @param {Compiler.AST.SvelteNode} node * @returns {node is Compiler.AST.IfBlock | Compiler.AST.EachBlock | Compiler.AST.AwaitBlock | Compiler.AST.KeyBlock | Compiler.AST.SlotElement} */ function is_block(node) { diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-warn.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-warn.js index eab67327e2cc..238c83f00ed1 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-warn.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-warn.js @@ -1,17 +1,17 @@ /** @import { Visitors } from 'zimmerframe' */ -/** @import { Css } from '#compiler' */ +/** @import { AST } from '#compiler' */ import { walk } from 'zimmerframe'; import * as w from '../../../warnings.js'; import { is_keyframes_node } from '../../css.js'; /** - * @param {Css.StyleSheet} stylesheet + * @param {AST.CSS.StyleSheet} stylesheet */ export function warn_unused(stylesheet) { walk(stylesheet, { stylesheet }, visitors); } -/** @type {Visitors} */ +/** @type {Visitors} */ const visitors = { Atrule(node, context) { if (!is_keyframes_node(node)) { @@ -28,7 +28,7 @@ const visitors = { !node.metadata.used && // prevent double-marking of `.unused:is(.unused)` (context.path.at(-2)?.type !== 'PseudoClassSelector' || - /** @type {Css.ComplexSelector} */ (context.path.at(-4))?.metadata.used) + /** @type {AST.CSS.ComplexSelector} */ (context.path.at(-4))?.metadata.used) ) { const content = context.state.stylesheet.content; const text = content.styles.substring(node.start - content.start, node.end - content.start); diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/utils.js b/packages/svelte/src/compiler/phases/2-analyze/css/utils.js index 07171a23bb9d..d3fd71ec395b 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/utils.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/utils.js @@ -1,4 +1,4 @@ -/** @import { AST, Css } from '#compiler' */ +/** @import { AST } from '#compiler' */ /** @import { Node } from 'estree' */ const UNKNOWN = {}; @@ -36,7 +36,7 @@ export function get_possible_values(chunk) { /** * Returns all parent rules; root is last - * @param {Css.Rule | null} rule + * @param {AST.CSS.Rule | null} rule */ export function get_parent_rules(rule) { const rules = []; @@ -51,8 +51,8 @@ export function get_parent_rules(rule) { /** * True if is `:global(...)` or `:global` and no pseudo class that is scoped. - * @param {Css.RelativeSelector} relative_selector - * @returns {relative_selector is Css.RelativeSelector & { selectors: [Css.PseudoClassSelector, ...Array] }} + * @param {AST.CSS.RelativeSelector} relative_selector + * @returns {relative_selector is AST.CSS.RelativeSelector & { selectors: [AST.CSS.PseudoClassSelector, ...Array] }} */ export function is_global(relative_selector) { const first = relative_selector.selectors[0]; @@ -72,7 +72,7 @@ export function is_global(relative_selector) { /** * `true` if is a pseudo class that cannot be or is not scoped - * @param {Css.SimpleSelector} selector + * @param {AST.CSS.SimpleSelector} selector */ export function is_unscoped_pseudo_class(selector) { return ( @@ -96,8 +96,8 @@ export function is_unscoped_pseudo_class(selector) { /** * True if is `:global(...)` or `:global`, irrespective of whether or not there are any pseudo classes that are scoped. * Difference to `is_global`: `:global(x):has(y)` is `true` for `is_outer_global` but `false` for `is_global`. - * @param {Css.RelativeSelector} relative_selector - * @returns {relative_selector is Css.RelativeSelector & { selectors: [Css.PseudoClassSelector, ...Array] }} + * @param {AST.CSS.RelativeSelector} relative_selector + * @returns {relative_selector is AST.CSS.RelativeSelector & { selectors: [AST.CSS.PseudoClassSelector, ...Array] }} */ export function is_outer_global(relative_selector) { const first = relative_selector.selectors[0]; diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 9e29813ee336..042e88fa2f83 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -1,5 +1,5 @@ /** @import { Expression, Node, Program } from 'estree' */ -/** @import { Binding, AST, SvelteNode, ValidatedCompileOptions, ValidatedModuleCompileOptions } from '#compiler' */ +/** @import { Binding, AST, ValidatedCompileOptions, ValidatedModuleCompileOptions } from '#compiler' */ /** @import { AnalysisState, Visitors } from './types' */ /** @import { Analysis, ComponentAnalysis, Js, ReactiveStatement, Template } from '../types' */ import { walk } from 'zimmerframe'; @@ -525,7 +525,7 @@ export function analyze_component(root, source, options) { // more legacy nonsense: if an `each` binding is reassigned/mutated, // treat the expression as being mutated as well - walk(/** @type {SvelteNode} */ (template.ast), null, { + walk(/** @type {AST.SvelteNode} */ (template.ast), null, { EachBlock(node) { const scope = /** @type {Scope} */ (template.scopes.get(node)); @@ -608,7 +608,7 @@ export function analyze_component(root, source, options) { reactive_statements: new Map() }; - walk(/** @type {SvelteNode} */ (ast), state, visitors); + walk(/** @type {AST.SvelteNode} */ (ast), state, visitors); } // warn on any nonstate declarations that are a) reassigned and b) referenced in the template @@ -677,7 +677,7 @@ export function analyze_component(root, source, options) { function_depth: scope.function_depth }; - walk(/** @type {SvelteNode} */ (ast), state, visitors); + walk(/** @type {AST.SvelteNode} */ (ast), state, visitors); } for (const [name, binding] of instance.scope.declarations) { diff --git a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts index dedbe95ace7e..b4ca4dc26278 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts +++ b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts @@ -1,11 +1,11 @@ import type { Scope } from '../scope.js'; import type { ComponentAnalysis, ReactiveStatement } from '../types.js'; -import type { ExpressionMetadata, AST, ValidatedCompileOptions, SvelteNode } from '#compiler'; +import type { AST, ExpressionMetadata, ValidatedCompileOptions } from '#compiler'; import type { LabeledStatement } from 'estree'; export interface AnalysisState { scope: Scope; - scopes: Map; + scopes: Map; analysis: ComponentAnalysis; options: ValidatedCompileOptions; ast_type: 'instance' | 'template' | 'module'; @@ -31,11 +31,11 @@ export interface AnalysisState { } export type Context = import('zimmerframe').Context< - SvelteNode, + AST.SvelteNode, State >; export type Visitors = import('zimmerframe').Visitors< - SvelteNode, + AST.SvelteNode, State >; diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/AssignmentExpression.js index 54e5b46486ad..a64c89cd88f1 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/AssignmentExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AssignmentExpression.js @@ -1,5 +1,4 @@ /** @import { AssignmentExpression } from 'estree' */ -/** @import { SvelteNode } from '#compiler' */ /** @import { Context } from '../types' */ import { extract_identifiers, object } from '../../../utils/ast.js'; import { validate_assignment } from './shared/utils.js'; diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js index 6c050d966a22..6eb9faca6d4e 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js @@ -1,5 +1,5 @@ /** @import { ArrowFunctionExpression, Expression, FunctionDeclaration, FunctionExpression } from 'estree' */ -/** @import { AST, DelegatedEvent, SvelteNode } from '#compiler' */ +/** @import { AST, DelegatedEvent } from '#compiler' */ /** @import { Context } from '../types' */ import { cannot_be_set_statically, is_capture_event, is_delegated } from '../../../../utils.js'; import { @@ -16,7 +16,7 @@ import { mark_subtree_dynamic } from './shared/fragment.js'; export function Attribute(node, context) { context.next(); - const parent = /** @type {SvelteNode} */ (context.path.at(-1)); + const parent = /** @type {AST.SvelteNode} */ (context.path.at(-1)); if (parent.type === 'RegularElement') { // special case