diff --git a/.changeset/clever-sloths-push.md b/.changeset/clever-sloths-push.md new file mode 100644 index 000000000000..91e2612c4d76 --- /dev/null +++ b/.changeset/clever-sloths-push.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +breaking: remove unstate(), replace with $state.snapshot rune diff --git a/packages/svelte/src/ambient.d.ts b/packages/svelte/src/ambient.d.ts index f923aace5ab1..b65346dd7d43 100644 --- a/packages/svelte/src/ambient.d.ts +++ b/packages/svelte/src/ambient.d.ts @@ -42,6 +42,26 @@ declare namespace $state { */ export function frozen(initial: T): Readonly; export function frozen(): Readonly | undefined; + /** + * To take a static snapshot of a deeply reactive `$state` proxy, use `$state.snapshot`: + * + * Example: + * ```ts + * + * ``` + * + * https://svelte-5-preview.vercel.app/docs/runes#$state.snapshot + * + * @param state The value to snapshot + */ + export function snapshot(state: T): T; } /** diff --git a/packages/svelte/src/compiler/phases/2-analyze/validation.js b/packages/svelte/src/compiler/phases/2-analyze/validation.js index 6e279057268c..7d77003c9d1f 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/validation.js +++ b/packages/svelte/src/compiler/phases/2-analyze/validation.js @@ -865,6 +865,12 @@ function validate_call_expression(node, scope, path) { error(node, 'invalid-rune-args-length', rune, [1]); } } + + if (rune === '$state.snapshot') { + if (node.arguments.length !== 1) { + error(node, 'invalid-rune-args-length', rune, [1]); + } + } } /** diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js index 5c4169b409ee..c4b87e8d58e1 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js @@ -388,6 +388,13 @@ export const javascript_visitors_runes = { return b.call('$.effect_active'); } + if (rune === '$state.snapshot') { + return b.call( + '$.snapshot', + /** @type {import('estree').Expression} */ (context.visit(node.arguments[0])) + ); + } + if (rune === '$effect.root') { const args = /** @type {import('estree').Expression[]} */ ( node.arguments.map((arg) => context.visit(arg)) diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js index 8c502449d41e..f8bbeafbb7f8 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js @@ -793,6 +793,10 @@ const javascript_visitors_runes = { return b.literal(false); } + if (rune === '$state.snapshot') { + return /** @type {import('estree').Expression} */ (context.visit(node.arguments[0])); + } + if (rune === '$inspect' || rune === '$inspect().with') { return transform_inspect_rune(node, context); } diff --git a/packages/svelte/src/compiler/phases/constants.js b/packages/svelte/src/compiler/phases/constants.js index c61015e92a40..0fec2e894838 100644 --- a/packages/svelte/src/compiler/phases/constants.js +++ b/packages/svelte/src/compiler/phases/constants.js @@ -31,6 +31,7 @@ export const PassiveEvents = ['wheel', 'touchstart', 'touchmove', 'touchend', 't export const Runes = /** @type {const} */ ([ '$state', '$state.frozen', + '$state.snapshot', '$props', '$bindable', '$derived', diff --git a/packages/svelte/src/index-client.js b/packages/svelte/src/index-client.js index 215a3379ff90..ba810969257e 100644 --- a/packages/svelte/src/index-client.js +++ b/packages/svelte/src/index-client.js @@ -177,8 +177,6 @@ export function flushSync(fn) { flush_sync(fn); } -export { unstate } from './internal/client/proxy.js'; - export { hydrate, mount, unmount } from './internal/client/render.js'; export { diff --git a/packages/svelte/src/index-server.js b/packages/svelte/src/index-server.js index aacf4dd711e8..bf03521c9658 100644 --- a/packages/svelte/src/index-server.js +++ b/packages/svelte/src/index-server.js @@ -33,14 +33,4 @@ export function unmount() { export async function tick() {} -/** - * @template T - * @param {T} value - * @returns {T} - */ -export function unstate(value) { - // There's no signals/proxies on the server, so just return the value - return value; -} - export { getAllContexts, getContext, hasContext, setContext } from './internal/server/context.js'; diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index 71a8c50c3e1f..db529a38b560 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -23,7 +23,7 @@ import { resume_effect } from '../../reactivity/effects.js'; import { source, mutable_source, set } from '../../reactivity/sources.js'; -import { is_array, is_frozen, map_get, map_set } from '../../utils.js'; +import { is_array, is_frozen } from '../../utils.js'; import { STATE_SYMBOL } from '../../constants.js'; /** diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 86c8fbc7da6d..278e4c1dcd0f 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -127,7 +127,7 @@ export { validate_store } from './validate.js'; export { raf } from './timing.js'; -export { proxy, unstate } from './proxy.js'; +export { proxy, snapshot } from './proxy.js'; export { create_custom_element } from './dom/elements/custom-element.js'; export { child, diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js index a6297559ab08..24d4567debc4 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -140,7 +140,7 @@ function unwrap(value, already_unwrapped) { * @param {T} value * @returns {T} */ -export function unstate(value) { +export function snapshot(value) { return /** @type {T} */ ( unwrap(/** @type {import('#client').ProxyStateObject} */ (value), new Map()) ); diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index c1b8ee3e441a..4082d90fa088 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -7,7 +7,7 @@ import { object_freeze, object_prototype } from './utils.js'; -import { unstate } from './proxy.js'; +import { snapshot } from './proxy.js'; import { destroy_effect, effect, user_pre_effect } from './reactivity/effects.js'; import { EFFECT, @@ -1166,26 +1166,26 @@ export function deep_read(value, visited = new Set()) { } /** - * Like `unstate`, but recursively traverses into normal arrays/objects to find potential states in them. + * Like `snapshot`, but recursively traverses into normal arrays/objects to find potential states in them. * @param {any} value * @param {Map} visited * @returns {any} */ -function deep_unstate(value, visited = new Map()) { +function deep_snapshot(value, visited = new Map()) { if (typeof value === 'object' && value !== null && !visited.has(value)) { - const unstated = unstate(value); + const unstated = snapshot(value); if (unstated !== value) { visited.set(value, unstated); return unstated; } const prototype = get_prototype_of(value); - // Only deeply unstate plain objects and arrays + // Only deeply snapshot plain objects and arrays if (prototype === object_prototype || prototype === array_prototype) { let contains_unstated = false; /** @type {any} */ const nested_unstated = Array.isArray(value) ? [] : {}; for (let key in value) { - const result = deep_unstate(value[key], visited); + const result = deep_snapshot(value[key], visited); nested_unstated[key] = result; if (result !== value[key]) { contains_unstated = true; @@ -1213,7 +1213,7 @@ export function inspect(get_value, inspect = console.log) { user_pre_effect(() => { const fn = () => { - const value = untrack(() => get_value().map((v) => deep_unstate(v))); + const value = untrack(() => get_value().map((v) => deep_snapshot(v))); if (value.length === 2 && typeof value[1] === 'function' && !warned_inspect_changed) { // eslint-disable-next-line no-console console.warn( @@ -1301,9 +1301,9 @@ if (DEV) { */ export function freeze(value) { if (typeof value === 'object' && value != null && !is_frozen(value)) { - // If the object is already proxified, then unstate the value + // If the object is already proxified, then snapshot the value if (STATE_SYMBOL in value) { - return object_freeze(unstate(value)); + return object_freeze(snapshot(value)); } // Otherwise freeze the object object_freeze(value); diff --git a/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-snapshot-args/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-snapshot-args/_config.js new file mode 100644 index 000000000000..c701cfac7eb8 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-snapshot-args/_config.js @@ -0,0 +1,8 @@ +import { test } from '../../test'; + +export default test({ + error: { + code: 'invalid-rune-args-length', + message: '$state.snapshot can only be called with 1 argument' + } +}); diff --git a/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-snapshot-args/main.svelte b/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-snapshot-args/main.svelte new file mode 100644 index 000000000000..5ff573f4748d --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-snapshot-args/main.svelte @@ -0,0 +1,3 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/unstate/_config.js b/packages/svelte/tests/runtime-runes/samples/state-snapshot/_config.js similarity index 100% rename from packages/svelte/tests/runtime-runes/samples/unstate/_config.js rename to packages/svelte/tests/runtime-runes/samples/state-snapshot/_config.js diff --git a/packages/svelte/tests/runtime-runes/samples/unstate/main.svelte b/packages/svelte/tests/runtime-runes/samples/state-snapshot/main.svelte similarity index 57% rename from packages/svelte/tests/runtime-runes/samples/unstate/main.svelte rename to packages/svelte/tests/runtime-runes/samples/state-snapshot/main.svelte index cb014acff38b..ac438dacc49d 100644 --- a/packages/svelte/tests/runtime-runes/samples/unstate/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/state-snapshot/main.svelte @@ -1,7 +1,5 @@ - + diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 0342dc5a24fe..95c3e6395ed7 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -297,7 +297,6 @@ declare module 'svelte' { export function flushSync(fn?: (() => void) | undefined): void; /** Anything except a function */ type NotFunction = T extends Function ? never : T; - export function unstate(value: T): T; /** * Mounts a component to the given target and returns the exports and potentially the props (if compiled with `accessors: true`) of the component * @@ -2551,6 +2550,26 @@ declare namespace $state { */ export function frozen(initial: T): Readonly; export function frozen(): Readonly | undefined; + /** + * To take a static snapshot of a deeply reactive `$state` proxy, use `$state.snapshot`: + * + * Example: + * ```ts + * + * ``` + * + * https://svelte-5-preview.vercel.app/docs/runes#$state.snapshot + * + * @param state The value to snapshot + */ + export function snapshot(state: T): T; } /** diff --git a/sites/svelte-5-preview/src/lib/CodeMirror.svelte b/sites/svelte-5-preview/src/lib/CodeMirror.svelte index 530e1e59c2bd..347cf7d93882 100644 --- a/sites/svelte-5-preview/src/lib/CodeMirror.svelte +++ b/sites/svelte-5-preview/src/lib/CodeMirror.svelte @@ -205,27 +205,28 @@ return { from: word.from - 1, options: [ - { label: '$state', type: 'keyword', boost: 10 }, - { label: '$props', type: 'keyword', boost: 9 }, - { label: '$derived', type: 'keyword', boost: 8 }, + { label: '$state', type: 'keyword', boost: 12 }, + { label: '$props', type: 'keyword', boost: 11 }, + { label: '$derived', type: 'keyword', boost: 10 }, snip('$derived.by(() => {\n\t${}\n});', { label: '$derived.by', type: 'keyword', - boost: 7 + boost: 9 }), - snip('$effect(() => {\n\t${}\n});', { label: '$effect', type: 'keyword', boost: 6 }), + snip('$effect(() => {\n\t${}\n});', { label: '$effect', type: 'keyword', boost: 8 }), snip('$effect.pre(() => {\n\t${}\n});', { label: '$effect.pre', type: 'keyword', - boost: 5 + boost: 7 }), - { label: '$state.frozen', type: 'keyword', boost: 4 }, - { label: '$bindable', type: 'keyword', boost: 4 }, + { label: '$state.frozen', type: 'keyword', boost: 6 }, + { label: '$bindable', type: 'keyword', boost: 5 }, snip('$effect.root(() => {\n\t${}\n});', { label: '$effect.root', type: 'keyword', - boost: 3 + boost: 4 }), + { label: '$state.snapshot', type: 'keyword', boost: 3 }, snip('$effect.active()', { label: '$effect.active', type: 'keyword', diff --git a/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md b/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md index 135404ec5b4e..64dc5d02e944 100644 --- a/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md +++ b/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md @@ -40,7 +40,7 @@ class Todo { > In this example, the compiler transforms `done` and `text` into `get`/`set` methods on the class prototype referencing private fields -Objects and arrays [are made deeply reactive](/#H4sIAAAAAAAAE42QwWrDMBBEf2URhUhUNEl7c21DviPOwZY3jVpZEtIqUBz9e-UUt9BTj7M784bdmZ21wciq48xsPyGr2MF7Jhl9-kXEKxrCoqNLQS2TOqqgPbWd7cgggU3TgCFCAw-RekJ-3Et4lvByEq-drbe_dlsPichZcFYZrT6amQto2pXw5FO88FUYtG90gUfYi3zvWrYL75vxL57zfA07_zfr23k1vjtt-aZ0bQTcbrDL5ZifZcAxKeS8lzDc8X0xDhJ2ItdbX1jlOZMb9VnjyCoKCfMpfwG975NFVwEAAA==): +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 +``` + +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`. + +> Note that `$state.snapshot` will clone the data when removing reactivity. If the value passed isn't a `$state` proxy, it will be returned as-is. + ## `$derived` Derived state is declared with the `$derived` rune: diff --git a/sites/svelte-5-preview/src/routes/docs/content/01-api/05-functions.md b/sites/svelte-5-preview/src/routes/docs/content/01-api/05-functions.md index a0b1651109e7..6e39ce4848b8 100644 --- a/sites/svelte-5-preview/src/routes/docs/content/01-api/05-functions.md +++ b/sites/svelte-5-preview/src/routes/docs/content/01-api/05-functions.md @@ -23,27 +23,6 @@ To prevent something from being treated as an `$effect`/`$derived` dependency, u ``` -## `unstate` - -To remove reactivity from objects and arrays created with `$state`, use `unstate`: - -```svelte - -``` - -This is handy when you want to pass some state to an external library or API that doesn't expect a reactive object – such as `structuredClone`. - -> Note that `unstate` will return a new object from the input when removing reactivity. If the object passed isn't reactive, it will be returned as is. - ## `mount` Instantiates a component and mounts it to the given target: