From 605f0baa9d749282b465f4a2b8a5068282d62cd0 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Thu, 7 Dec 2023 20:55:51 +0000 Subject: [PATCH 01/18] feat: add $state.raw rune fix typo fix typo --- .changeset/dry-clocks-grow.md | 5 ++++ .../src/compiler/phases/2-analyze/index.js | 30 +++++++++++-------- .../compiler/phases/2-analyze/validation.js | 7 +++-- .../3-transform/client/transform-client.js | 7 +++-- .../phases/3-transform/client/types.d.ts | 2 +- .../phases/3-transform/client/utils.js | 7 +++-- .../3-transform/client/visitors/global.js | 1 + .../client/visitors/javascript-runes.js | 21 +++++++++++-- .../3-transform/client/visitors/template.js | 1 + .../3-transform/server/transform-server.js | 3 +- .../svelte/src/compiler/phases/constants.js | 1 + packages/svelte/src/compiler/types/index.d.ts | 1 + packages/svelte/src/main/ambient.d.ts | 17 +++++++++++ .../class-private-raw-state/_config.js | 15 ++++++++++ .../class-private-raw-state/main.svelte | 19 ++++++++++++ .../samples/class-raw-state/_config.js | 15 ++++++++++ .../samples/class-raw-state/main.svelte | 8 +++++ .../samples/raw-state/_config.js | 17 +++++++++++ .../runtime-runes/samples/raw-state/log.js | 2 ++ .../samples/raw-state/main.svelte | 13 ++++++++ .../routes/docs/content/01-api/02-runes.md | 20 +++++++++++++ 21 files changed, 189 insertions(+), 23 deletions(-) create mode 100644 .changeset/dry-clocks-grow.md create mode 100644 packages/svelte/tests/runtime-runes/samples/class-private-raw-state/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/class-private-raw-state/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/class-raw-state/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/class-raw-state/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/raw-state/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/raw-state/log.js create mode 100644 packages/svelte/tests/runtime-runes/samples/raw-state/main.svelte diff --git a/.changeset/dry-clocks-grow.md b/.changeset/dry-clocks-grow.md new file mode 100644 index 000000000000..d80d717ef5a7 --- /dev/null +++ b/.changeset/dry-clocks-grow.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +feat: add $state.raw rune diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index bd43aca39846..24aa5a8e67b8 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -585,6 +585,7 @@ const legacy_scope_tweaker = { ); if ( binding.kind === 'state' || + binding.kind === 'raw_state' || (binding.kind === 'normal' && binding.declaration_kind === 'let') ) { binding.kind = 'prop'; @@ -636,18 +637,18 @@ const legacy_scope_tweaker = { const runes_scope_js_tweaker = { VariableDeclarator(node, { state }) { if (node.init?.type !== 'CallExpression') return; - if (get_rune(node.init, state.scope) === null) return; + const rune = get_rune(node.init, state.scope); + if (rune === null) return; const callee = node.init.callee; - if (callee.type !== 'Identifier') return; + if (callee.type !== 'Identifier' && callee.type !== 'MemberExpression') return; - const name = callee.name; - if (name !== '$state' && name !== '$derived') return; + if (rune !== '$state' && rune !== '$state.raw' && rune !== '$derived') return; for (const path of extract_paths(node.id)) { // @ts-ignore this fails in CI for some insane reason const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(path.node.name)); - binding.kind = name === '$state' ? 'state' : 'derived'; + binding.kind = rune === '$state' ? 'state' : rune === '$state.raw' ? 'raw_state' : 'derived'; } } }; @@ -665,28 +666,31 @@ const runes_scope_tweaker = { VariableDeclarator(node, { state }) { const init = unwrap_ts_expression(node.init); if (!init || init.type !== 'CallExpression') return; - if (get_rune(init, state.scope) === null) return; + const rune = get_rune(init, state.scope); + if (rune === null) return; const callee = init.callee; - if (callee.type !== 'Identifier') return; + if (callee.type !== 'Identifier' && callee.type !== 'MemberExpression') return; - const name = callee.name; - if (name !== '$state' && name !== '$derived' && name !== '$props') return; + if (rune !== '$state' && rune !== '$state.raw' && rune !== '$derived' && rune !== '$props') + return; for (const path of extract_paths(node.id)) { // @ts-ignore this fails in CI for some insane reason const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(path.node.name)); binding.kind = - name === '$state' + rune === '$state' ? 'state' - : name === '$derived' + : rune === '$state.raw' + ? 'raw_state' + : rune === '$derived' ? 'derived' : path.is_rest ? 'rest_prop' : 'prop'; } - if (name === '$props') { + if (rune === '$props') { for (const property of /** @type {import('estree').ObjectPattern} */ (node.id).properties) { if (property.type !== 'Property') continue; @@ -898,7 +902,7 @@ const common_visitors = { if ( node !== binding.node && - (binding.kind === 'state' || binding.kind === 'derived') && + (binding.kind === 'state' || binding.kind === 'raw_state' || binding.kind === 'derived') && context.state.function_depth === binding.scope.function_depth ) { warn(context.state.analysis.warnings, node, context.path, 'static-state-reference'); diff --git a/packages/svelte/src/compiler/phases/2-analyze/validation.js b/packages/svelte/src/compiler/phases/2-analyze/validation.js index 8dad40e2386e..5aee1a0ab0d2 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/validation.js +++ b/packages/svelte/src/compiler/phases/2-analyze/validation.js @@ -349,6 +349,7 @@ export const validation = { if ( !binding || (binding.kind !== 'state' && + binding.kind !== 'raw_state' && binding.kind !== 'prop' && binding.kind !== 'each' && binding.kind !== 'store_sub' && @@ -660,7 +661,7 @@ function validate_export(node, scope, name) { error(node, 'invalid-derived-export'); } - if (binding.kind === 'state' && binding.reassigned) { + if ((binding.kind === 'state' || binding.kind === 'raw_state') && binding.reassigned) { error(node, 'invalid-state-export'); } } @@ -834,7 +835,9 @@ function validate_no_const_assignment(node, argument, scope, is_binding) { is_binding, // This takes advantage of the fact that we don't assign initial for let directives and then/catch variables. // If we start doing that, we need another property on the binding to differentiate, or give up on the more precise error message. - binding.kind !== 'state' && (binding.kind !== 'normal' || !binding.initial) + binding.kind !== 'state' && + binding.kind !== 'raw_state' && + (binding.kind !== 'normal' || !binding.initial) ); } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index d82cdd68f28a..bc7429066506 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -233,7 +233,9 @@ export function client_component(source, analysis, options) { '$.bind_prop', b.id('$$props'), b.literal(alias ?? name), - binding?.kind === 'state' ? b.call('$.get', b.id(name)) : b.id(name) + binding?.kind === 'state' || binding?.kind === 'raw_state' + ? b.call('$.get', b.id(name)) + : b.id(name) ) ); }); @@ -241,7 +243,8 @@ export function client_component(source, analysis, options) { const properties = analysis.exports.map(({ name, alias }) => { const binding = analysis.instance.scope.get(name); const is_source = - binding?.kind === 'state' && (!state.analysis.immutable || binding.reassigned); + (binding?.kind === 'state' || binding?.kind === 'raw_state') && + (!state.analysis.immutable || binding.reassigned); // TODO This is always a getter because the `renamed-instance-exports` test wants it that way. // Should we for code size reasons make it an init in runes mode and/or non-dev mode? diff --git a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts index ef9b466bad90..498c3566bcf3 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts +++ b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts @@ -59,7 +59,7 @@ export interface ComponentClientTransformState extends ClientTransformState { } export interface StateField { - kind: 'state' | 'derived'; + kind: 'state' | 'raw_state' | 'derived'; id: PrivateIdentifier; } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/utils.js index 305dbcd543ac..cc2b7ed19228 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -92,7 +92,7 @@ export function serialize_get_binding(node, state) { } if ( - (binding.kind === 'state' && + ((binding.kind === 'state' || binding.kind === 'raw_state') && (!state.analysis.immutable || state.analysis.accessors || binding.reassigned)) || binding.kind === 'derived' || binding.kind === 'legacy_reactive' @@ -232,6 +232,7 @@ export function serialize_set_binding(node, context, fallback) { if ( binding.kind !== 'state' && + binding.kind !== 'raw_state' && binding.kind !== 'prop' && binding.kind !== 'each' && binding.kind !== 'legacy_reactive' && @@ -249,12 +250,14 @@ export function serialize_set_binding(node, context, fallback) { return b.call(left, value); } else if (is_store) { return b.call('$.store_set', serialize_get_binding(b.id(left_name), state), value); - } else { + } else if (binding.kind === 'state') { return b.call( '$.set', b.id(left_name), context.state.analysis.runes && should_proxy(value) ? b.call('$.proxy', value) : value ); + } else { + return b.call('$.set', b.id(left_name), value); } } else { if (is_store) { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/global.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/global.js index 136bb020a947..ddda0744e73b 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/global.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/global.js @@ -49,6 +49,7 @@ export const global_visitors = { // use runtime functions for smaller output if ( binding?.kind === 'state' || + binding?.kind === 'raw_state' || binding?.kind === 'each' || binding?.kind === 'legacy_reactive' || binding?.kind === 'prop' || 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 0526c9c4bf2c..05aad64c4c79 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 @@ -29,10 +29,10 @@ export const javascript_visitors_runes = { if (definition.value?.type === 'CallExpression') { const rune = get_rune(definition.value, state.scope); - if (rune === '$state' || rune === '$derived') { + if (rune === '$state' || rune === '$state.raw' || rune === '$derived') { /** @type {import('../types.js').StateField} */ const field = { - kind: rune === '$state' ? 'state' : 'derived', + kind: rune === '$state' ? 'state' : rune === '$state.raw' ? 'raw_state' : 'derived', // @ts-expect-error this is set in the next pass id: is_private ? definition.key : null }; @@ -85,6 +85,8 @@ export const javascript_visitors_runes = { value = field.kind === 'state' ? b.call('$.source', should_proxy(init) ? b.call('$.proxy', init) : init) + : field.kind === 'raw_state' + ? b.call('$.source', init) : b.call('$.derived', b.thunk(init)); } else { // if no arguments, we know it's state as `$derived()` is a compile error @@ -114,6 +116,14 @@ export const javascript_visitors_runes = { ); } + if (field.kind === 'raw_state') { + // set foo(value) { this.#foo = value; } + const value = b.id('value'); + body.push( + b.method('set', definition.key, [value], [b.stmt(b.call('$.set', member, value))]) + ); + } + if (field.kind === 'derived' && state.options.dev) { body.push( b.method( @@ -224,6 +234,13 @@ export const javascript_visitors_runes = { if (!state.analysis.immutable || state.analysis.accessors || binding.reassigned) { value = b.call('$.source', value); } + } else if (rune === '$state.raw') { + const binding = /** @type {import('#compiler').Binding} */ ( + state.scope.get(declarator.id.name) + ); + if (binding.reassigned) { + value = b.call('$.source', value); + } } else { value = b.call('$.derived', b.thunk(value)); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js index 7c06cac973fe..63a8b21ead98 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js @@ -1227,6 +1227,7 @@ function serialize_event_handler(node, { state, visit }) { if ( binding !== null && (binding.kind === 'state' || + binding.kind === 'raw_state' || binding.kind === 'legacy_reactive' || binding.kind === 'derived' || binding.kind === 'prop' || 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 79c62e396d47..d4c9173ccc47 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 @@ -446,6 +446,7 @@ function serialize_set_binding(node, context, fallback) { if ( binding.kind !== 'state' && + binding.kind !== 'raw_state' && binding.kind !== 'prop' && binding.kind !== 'each' && binding.kind !== 'legacy_reactive' && @@ -558,7 +559,7 @@ const javascript_visitors_runes = { if (node.value != null && node.value.type === 'CallExpression') { const rune = get_rune(node.value, state.scope); - if (rune === '$state' || rune === '$derived') { + if (rune === '$state' || rune === '$state.raw' || rune === '$derived') { return { ...node, value: diff --git a/packages/svelte/src/compiler/phases/constants.js b/packages/svelte/src/compiler/phases/constants.js index ec40c50c83ca..260ece234f7c 100644 --- a/packages/svelte/src/compiler/phases/constants.js +++ b/packages/svelte/src/compiler/phases/constants.js @@ -72,6 +72,7 @@ export const ElementBindings = [ export const Runes = /** @type {const} */ ([ '$state', + '$state.raw', '$props', '$derived', '$effect', diff --git a/packages/svelte/src/compiler/types/index.d.ts b/packages/svelte/src/compiler/types/index.d.ts index 6435c2020737..171bf33b3853 100644 --- a/packages/svelte/src/compiler/types/index.d.ts +++ b/packages/svelte/src/compiler/types/index.d.ts @@ -258,6 +258,7 @@ export interface Binding { | 'prop' | 'rest_prop' | 'state' + | 'raw_state' | 'derived' | 'each' | 'store_sub' diff --git a/packages/svelte/src/main/ambient.d.ts b/packages/svelte/src/main/ambient.d.ts index 9ae68610c2f6..a34144e5192f 100644 --- a/packages/svelte/src/main/ambient.d.ts +++ b/packages/svelte/src/main/ambient.d.ts @@ -17,6 +17,23 @@ declare module '*.svelte' { declare function $state(initial: T): T; declare function $state(): T | undefined; +declare namespace $state { + /** + * Declares reactive state without applying reactivity to nested properties. + * + * Example: + * ```ts + * let count = $state.raw(0); + * ``` + * + * https://svelte-5-preview.vercel.app/docs/runes#$state-raw + * + * @param initial The initial value + */ + export function $raw(initial: T): T; + export function $raw(): T | undefined; +} + /** * Declares derived state, i.e. one that depends on other state variables. * The expression inside `$derived(...)` should be free of side-effects. diff --git a/packages/svelte/tests/runtime-runes/samples/class-private-raw-state/_config.js b/packages/svelte/tests/runtime-runes/samples/class-private-raw-state/_config.js new file mode 100644 index 000000000000..436ce9979876 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-private-raw-state/_config.js @@ -0,0 +1,15 @@ +import { test } from '../../test'; + +export default test({ + html: ``, + + async test({ assert, target }) { + const btn = target.querySelector('button'); + + await btn?.click(); + assert.htmlEqual(target.innerHTML, ``); + + await btn?.click(); + assert.htmlEqual(target.innerHTML, ``); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/class-private-raw-state/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-private-raw-state/main.svelte new file mode 100644 index 000000000000..f4812b1dde28 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-private-raw-state/main.svelte @@ -0,0 +1,19 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/class-raw-state/_config.js b/packages/svelte/tests/runtime-runes/samples/class-raw-state/_config.js new file mode 100644 index 000000000000..436ce9979876 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-raw-state/_config.js @@ -0,0 +1,15 @@ +import { test } from '../../test'; + +export default test({ + html: ``, + + async test({ assert, target }) { + const btn = target.querySelector('button'); + + await btn?.click(); + assert.htmlEqual(target.innerHTML, ``); + + await btn?.click(); + assert.htmlEqual(target.innerHTML, ``); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/class-raw-state/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-raw-state/main.svelte new file mode 100644 index 000000000000..2913d1ececaa --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-raw-state/main.svelte @@ -0,0 +1,8 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/raw-state/_config.js b/packages/svelte/tests/runtime-runes/samples/raw-state/_config.js new file mode 100644 index 000000000000..1e6333bdebbf --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/raw-state/_config.js @@ -0,0 +1,17 @@ +import { test } from '../../test'; +import { log } from './log.js'; + +export default test({ + before_test() { + log.length = 0; + }, + + async test({ assert, target }) { + const [b1, b2] = target.querySelectorAll('button'); + b1.click(); + b2.click(); + await Promise.resolve(); + + assert.deepEqual(log, [0, 1]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/raw-state/log.js b/packages/svelte/tests/runtime-runes/samples/raw-state/log.js new file mode 100644 index 000000000000..d3df521f4da7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/raw-state/log.js @@ -0,0 +1,2 @@ +/** @type {any[]} */ +export const log = []; diff --git a/packages/svelte/tests/runtime-runes/samples/raw-state/main.svelte b/packages/svelte/tests/runtime-runes/samples/raw-state/main.svelte new file mode 100644 index 000000000000..137eeafd67a4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/raw-state/main.svelte @@ -0,0 +1,13 @@ + + + + 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 2cd3392e53bf..d883aab0a258 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 @@ -64,6 +64,26 @@ Objects and arrays [are made reactive](/#H4sIAAAAAAAAE42QwWrDMBBEf2URhUhUNEl7c21 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` + +Similar to `$state`, `$state.raw` is also declared and can be used in many of the same ways (including on classes). However, reactivity is _not_ applied deeply to properties of any objects or arrays used with `$state.raw`. So if you intend to use objects as state and you want to mutate their properties and have reactivity work by default, it's recommended you use `$state` instead. + +For the cases where you don't want Svelte's reactivity to apply deeply to state, and for those who might want to have more control over their data structures, you might find `$state.raw` useful. Furthermore, `$state.raw` is ideal for those who want to work with data using immutable patterns rather than mutable patterns. + +```svelte + + + +``` + ## `$derived` Derived state is declared with the `$derived` rune: From 4984b045a15ade1b33387ec80d9ad2b71c4a646f Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Fri, 8 Dec 2023 00:27:58 +0000 Subject: [PATCH 02/18] add more tests, fix example --- packages/svelte/src/main/ambient.d.ts | 12 +++++++++++- .../class-private-raw-state-object/_config.js | 15 +++++++++++++++ .../main.svelte | 19 +++++++++++++++++++ .../samples/class-raw-state-object/_config.js | 15 +++++++++++++++ .../class-raw-state-object/main.svelte | 8 ++++++++ 5 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 packages/svelte/tests/runtime-runes/samples/class-private-raw-state-object/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/class-private-raw-state-object/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/class-raw-state-object/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/class-raw-state-object/main.svelte diff --git a/packages/svelte/src/main/ambient.d.ts b/packages/svelte/src/main/ambient.d.ts index a34144e5192f..af12fe6a20ea 100644 --- a/packages/svelte/src/main/ambient.d.ts +++ b/packages/svelte/src/main/ambient.d.ts @@ -23,7 +23,17 @@ declare namespace $state { * * Example: * ```ts - * let count = $state.raw(0); + * + * + * * ``` * * https://svelte-5-preview.vercel.app/docs/runes#$state-raw diff --git a/packages/svelte/tests/runtime-runes/samples/class-private-raw-state-object/_config.js b/packages/svelte/tests/runtime-runes/samples/class-private-raw-state-object/_config.js new file mode 100644 index 000000000000..94aeebc39e02 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-private-raw-state-object/_config.js @@ -0,0 +1,15 @@ +import { test } from '../../test'; + +export default test({ + html: ``, + + async test({ assert, target }) { + const btn = target.querySelector('button'); + + await btn?.click(); + assert.htmlEqual(target.innerHTML, ``); + + await btn?.click(); + assert.htmlEqual(target.innerHTML, ``); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/class-private-raw-state-object/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-private-raw-state-object/main.svelte new file mode 100644 index 000000000000..c79deb24bb61 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-private-raw-state-object/main.svelte @@ -0,0 +1,19 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/class-raw-state-object/_config.js b/packages/svelte/tests/runtime-runes/samples/class-raw-state-object/_config.js new file mode 100644 index 000000000000..94aeebc39e02 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-raw-state-object/_config.js @@ -0,0 +1,15 @@ +import { test } from '../../test'; + +export default test({ + html: ``, + + async test({ assert, target }) { + const btn = target.querySelector('button'); + + await btn?.click(); + assert.htmlEqual(target.innerHTML, ``); + + await btn?.click(); + assert.htmlEqual(target.innerHTML, ``); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/class-raw-state-object/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-raw-state-object/main.svelte new file mode 100644 index 000000000000..0d6e3f17d5a2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-raw-state-object/main.svelte @@ -0,0 +1,8 @@ + + + From 573cd09e4216956da3531e74c6908282ddbab254 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Fri, 8 Dec 2023 00:33:01 +0000 Subject: [PATCH 03/18] add other test --- .../samples/raw-state-replace/_config.js | 11 +++++++++++ .../samples/raw-state-replace/main.svelte | 11 +++++++++++ 2 files changed, 22 insertions(+) create mode 100644 packages/svelte/tests/runtime-runes/samples/raw-state-replace/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/raw-state-replace/main.svelte diff --git a/packages/svelte/tests/runtime-runes/samples/raw-state-replace/_config.js b/packages/svelte/tests/runtime-runes/samples/raw-state-replace/_config.js new file mode 100644 index 000000000000..41d9e4061a6c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/raw-state-replace/_config.js @@ -0,0 +1,11 @@ +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [b1] = target.querySelectorAll('button'); + b1.click(); + await Promise.resolve(); + + assert.htmlEqual(target.innerHTML, ``); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/raw-state-replace/main.svelte b/packages/svelte/tests/runtime-runes/samples/raw-state-replace/main.svelte new file mode 100644 index 000000000000..4b5dc5ac59d7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/raw-state-replace/main.svelte @@ -0,0 +1,11 @@ + + + From b689c462c3a9b21ec36d0ea6b43e4c6afd1c05a9 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Tue, 12 Dec 2023 14:17:31 +0000 Subject: [PATCH 04/18] change to $state.readonly --- .changeset/dry-clocks-grow.md | 2 +- .../src/compiler/phases/2-analyze/index.js | 17 +++-- .../phases/3-transform/client/types.d.ts | 2 +- .../phases/3-transform/client/utils.js | 73 ++++++++++++------- .../3-transform/client/visitors/global.js | 2 +- .../client/visitors/javascript-runes.js | 34 ++++++--- .../3-transform/client/visitors/template.js | 2 +- .../3-transform/server/transform-server.js | 4 +- .../svelte/src/compiler/phases/constants.js | 2 +- packages/svelte/src/compiler/types/index.d.ts | 2 +- .../src/internal/client/proxy/readonly.js | 4 +- .../svelte/src/internal/client/runtime.js | 27 ++++++- packages/svelte/src/internal/client/utils.js | 2 + packages/svelte/src/internal/index.js | 3 +- packages/svelte/src/main/ambient.d.ts | 8 +- .../_config.js | 7 ++ .../log.js | 0 .../main.svelte | 12 ++- .../_config.js | 0 .../main.svelte | 2 +- .../class-raw-state-object/main.svelte | 8 -- .../_config.js | 7 ++ .../class-readonly-state-object/log.js | 2 + .../class-readonly-state-object/main.svelte | 16 ++++ .../_config.js | 0 .../main.svelte | 2 +- .../_config.js | 0 .../main.svelte | 2 +- .../{raw-state => readonly-state}/_config.js | 0 .../samples/readonly-state/log.js | 2 + .../{raw-state => readonly-state}/main.svelte | 4 +- .../routes/docs/content/01-api/02-runes.md | 10 ++- 32 files changed, 179 insertions(+), 79 deletions(-) rename packages/svelte/tests/runtime-runes/samples/{class-private-raw-state-object => class-private-readonly-state-object}/_config.js (73%) rename packages/svelte/tests/runtime-runes/samples/{raw-state => class-private-readonly-state-object}/log.js (100%) rename packages/svelte/tests/runtime-runes/samples/{class-private-raw-state-object => class-private-readonly-state-object}/main.svelte (55%) rename packages/svelte/tests/runtime-runes/samples/{class-private-raw-state => class-private-readonly-state}/_config.js (100%) rename packages/svelte/tests/runtime-runes/samples/{class-private-raw-state => class-private-readonly-state}/main.svelte (90%) delete mode 100644 packages/svelte/tests/runtime-runes/samples/class-raw-state-object/main.svelte rename packages/svelte/tests/runtime-runes/samples/{class-raw-state-object => class-readonly-state-object}/_config.js (73%) create mode 100644 packages/svelte/tests/runtime-runes/samples/class-readonly-state-object/log.js create mode 100644 packages/svelte/tests/runtime-runes/samples/class-readonly-state-object/main.svelte rename packages/svelte/tests/runtime-runes/samples/{class-raw-state => class-readonly-state}/_config.js (100%) rename packages/svelte/tests/runtime-runes/samples/{class-raw-state => class-readonly-state}/main.svelte (82%) rename packages/svelte/tests/runtime-runes/samples/{raw-state-replace => readonly-state-replace}/_config.js (100%) rename packages/svelte/tests/runtime-runes/samples/{raw-state-replace => readonly-state-replace}/main.svelte (80%) rename packages/svelte/tests/runtime-runes/samples/{raw-state => readonly-state}/_config.js (100%) create mode 100644 packages/svelte/tests/runtime-runes/samples/readonly-state/log.js rename packages/svelte/tests/runtime-runes/samples/{raw-state => readonly-state}/main.svelte (75%) diff --git a/.changeset/dry-clocks-grow.md b/.changeset/dry-clocks-grow.md index d80d717ef5a7..d7eebf931621 100644 --- a/.changeset/dry-clocks-grow.md +++ b/.changeset/dry-clocks-grow.md @@ -2,4 +2,4 @@ 'svelte': patch --- -feat: add $state.raw rune +feat: add $state.readonly rune diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 24aa5a8e67b8..67062610d407 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -585,7 +585,7 @@ const legacy_scope_tweaker = { ); if ( binding.kind === 'state' || - binding.kind === 'raw_state' || + binding.kind === 'readonly_state' || (binding.kind === 'normal' && binding.declaration_kind === 'let') ) { binding.kind = 'prop'; @@ -643,12 +643,13 @@ const runes_scope_js_tweaker = { const callee = node.init.callee; if (callee.type !== 'Identifier' && callee.type !== 'MemberExpression') return; - if (rune !== '$state' && rune !== '$state.raw' && rune !== '$derived') return; + if (rune !== '$state' && rune !== '$state.readonly' && rune !== '$derived') return; for (const path of extract_paths(node.id)) { // @ts-ignore this fails in CI for some insane reason const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(path.node.name)); - binding.kind = rune === '$state' ? 'state' : rune === '$state.raw' ? 'raw_state' : 'derived'; + binding.kind = + rune === '$state' ? 'state' : rune === '$state.readonly' ? 'readonly_state' : 'derived'; } } }; @@ -672,7 +673,7 @@ const runes_scope_tweaker = { const callee = init.callee; if (callee.type !== 'Identifier' && callee.type !== 'MemberExpression') return; - if (rune !== '$state' && rune !== '$state.raw' && rune !== '$derived' && rune !== '$props') + if (rune !== '$state' && rune !== '$state.readonly' && rune !== '$derived' && rune !== '$props') return; for (const path of extract_paths(node.id)) { @@ -681,8 +682,8 @@ const runes_scope_tweaker = { binding.kind = rune === '$state' ? 'state' - : rune === '$state.raw' - ? 'raw_state' + : rune === '$state.readonly' + ? 'readonly_state' : rune === '$derived' ? 'derived' : path.is_rest @@ -902,7 +903,9 @@ const common_visitors = { if ( node !== binding.node && - (binding.kind === 'state' || binding.kind === 'raw_state' || binding.kind === 'derived') && + (binding.kind === 'state' || + binding.kind === 'readonly_state' || + binding.kind === 'derived') && context.state.function_depth === binding.scope.function_depth ) { warn(context.state.analysis.warnings, node, context.path, 'static-state-reference'); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts index 498c3566bcf3..a82ef1fea239 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts +++ b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts @@ -59,7 +59,7 @@ export interface ComponentClientTransformState extends ClientTransformState { } export interface StateField { - kind: 'state' | 'raw_state' | 'derived'; + kind: 'state' | 'readonly_state' | 'derived'; id: PrivateIdentifier; } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/utils.js index cc2b7ed19228..78adf94f2f94 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -92,7 +92,7 @@ export function serialize_get_binding(node, state) { } if ( - ((binding.kind === 'state' || binding.kind === 'raw_state') && + ((binding.kind === 'state' || binding.kind === 'readonly_state') && (!state.analysis.immutable || state.analysis.accessors || binding.reassigned)) || binding.kind === 'derived' || binding.kind === 'legacy_reactive' @@ -162,40 +162,53 @@ export function serialize_set_binding(node, context, fallback) { // Handle class private/public state assignment cases while (left.type === 'MemberExpression') { - if ( - left.object.type === 'ThisExpression' && - left.property.type === 'PrivateIdentifier' && - context.state.private_state.has(left.property.name) - ) { + if (left.object.type === 'ThisExpression' && left.property.type === 'PrivateIdentifier') { + const private_state = context.state.private_state.get(left.property.name); const value = get_assignment_value(node, context); - if (state.in_constructor) { - // See if we should wrap value in $.proxy - if (context.state.analysis.runes && should_proxy(value)) { - const assignment = fallback(); - if (assignment.type === 'AssignmentExpression') { - assignment.right = b.call('$.proxy', value); - return assignment; + if (private_state !== undefined) { + if (state.in_constructor) { + // See if we should wrap value in $.proxy + if (context.state.analysis.runes && should_proxy_or_freeze(value)) { + const assignment = fallback(); + if (assignment.type === 'AssignmentExpression') { + assignment.right = + private_state.kind === 'readonly_state' + ? b.call('$.freeze', value) + : b.call('$.proxy', value); + return assignment; + } } + } else { + return b.call( + '$.set', + left, + context.state.analysis.runes && should_proxy_or_freeze(value) + ? private_state.kind === 'readonly_state' + ? b.call('$.freeze', value) + : b.call('$.proxy', value) + : value + ); } - } else { - return b.call( - '$.set', - left, - context.state.analysis.runes && should_proxy(value) ? b.call('$.proxy', value) : value - ); } } else if ( left.object.type === 'ThisExpression' && left.property.type === 'Identifier' && - context.state.public_state.has(left.property.name) && state.in_constructor ) { + const public_state = context.state.public_state.get(left.property.name); const value = get_assignment_value(node, context); // See if we should wrap value in $.proxy - if (context.state.analysis.runes && should_proxy(value)) { + if ( + context.state.analysis.runes && + public_state !== undefined && + should_proxy_or_freeze(value) + ) { const assignment = fallback(); if (assignment.type === 'AssignmentExpression') { - assignment.right = b.call('$.proxy', value); + assignment.right = + public_state.kind === 'readonly_state' + ? b.call('$.freeze', value) + : b.call('$.proxy', value); return assignment; } } @@ -232,7 +245,7 @@ export function serialize_set_binding(node, context, fallback) { if ( binding.kind !== 'state' && - binding.kind !== 'raw_state' && + binding.kind !== 'readonly_state' && binding.kind !== 'prop' && binding.kind !== 'each' && binding.kind !== 'legacy_reactive' && @@ -254,7 +267,17 @@ export function serialize_set_binding(node, context, fallback) { return b.call( '$.set', b.id(left_name), - context.state.analysis.runes && should_proxy(value) ? b.call('$.proxy', value) : value + context.state.analysis.runes && should_proxy_or_freeze(value) + ? b.call('$.proxy', value) + : value + ); + } else if (binding.kind === 'readonly_state') { + return b.call( + '$.set', + b.id(left_name), + context.state.analysis.runes && should_proxy_or_freeze(value) + ? b.call('$.freeze', value) + : value ); } else { return b.call('$.set', b.id(left_name), value); @@ -496,7 +519,7 @@ export function create_state_declarators(declarator, scope, value) { } /** @param {import('estree').Expression} node */ -export function should_proxy(node) { +export function should_proxy_or_freeze(node) { if ( !node || node.type === 'Literal' || diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/global.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/global.js index ddda0744e73b..f4fb635b80bb 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/global.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/global.js @@ -49,7 +49,7 @@ export const global_visitors = { // use runtime functions for smaller output if ( binding?.kind === 'state' || - binding?.kind === 'raw_state' || + binding?.kind === 'readonly_state' || binding?.kind === 'each' || binding?.kind === 'legacy_reactive' || binding?.kind === 'prop' || 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 05aad64c4c79..b41c4ddb1d8d 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 @@ -2,7 +2,7 @@ import { get_rune } from '../../../scope.js'; import { is_hoistable_function, transform_inspect_rune } from '../../utils.js'; import * as b from '../../../../utils/builders.js'; import * as assert from '../../../../utils/assert.js'; -import { create_state_declarators, get_prop_source, should_proxy } from '../utils.js'; +import { create_state_declarators, get_prop_source, should_proxy_or_freeze } from '../utils.js'; import { unwrap_ts_expression } from '../../../../utils/ast.js'; /** @type {import('../types.js').ComponentVisitors} */ @@ -29,10 +29,15 @@ export const javascript_visitors_runes = { if (definition.value?.type === 'CallExpression') { const rune = get_rune(definition.value, state.scope); - if (rune === '$state' || rune === '$state.raw' || rune === '$derived') { + if (rune === '$state' || rune === '$state.readonly' || rune === '$derived') { /** @type {import('../types.js').StateField} */ const field = { - kind: rune === '$state' ? 'state' : rune === '$state.raw' ? 'raw_state' : 'derived', + kind: + rune === '$state' + ? 'state' + : rune === '$state.readonly' + ? 'readonly_state' + : 'derived', // @ts-expect-error this is set in the next pass id: is_private ? definition.key : null }; @@ -84,9 +89,9 @@ export const javascript_visitors_runes = { value = field.kind === 'state' - ? b.call('$.source', should_proxy(init) ? b.call('$.proxy', init) : init) - : field.kind === 'raw_state' - ? b.call('$.source', init) + ? b.call('$.source', should_proxy_or_freeze(init) ? b.call('$.proxy', init) : init) + : field.kind === 'readonly_state' + ? b.call('$.source', should_proxy_or_freeze(init) ? b.call('$.freeze', init) : init) : b.call('$.derived', b.thunk(init)); } else { // if no arguments, we know it's state as `$derived()` is a compile error @@ -116,11 +121,16 @@ export const javascript_visitors_runes = { ); } - if (field.kind === 'raw_state') { + if (field.kind === 'readonly_state') { // set foo(value) { this.#foo = value; } const value = b.id('value'); body.push( - b.method('set', definition.key, [value], [b.stmt(b.call('$.set', member, value))]) + b.method( + 'set', + definition.key, + [value], + [b.stmt(b.call('$.set', member, b.call('$.freeze', value)))] + ) ); } @@ -227,17 +237,21 @@ export const javascript_visitors_runes = { const binding = /** @type {import('#compiler').Binding} */ ( state.scope.get(declarator.id.name) ); - if (should_proxy(value)) { + if (should_proxy_or_freeze(value)) { value = b.call('$.proxy', value); } if (!state.analysis.immutable || state.analysis.accessors || binding.reassigned) { value = b.call('$.source', value); } - } else if (rune === '$state.raw') { + } else if (rune === '$state.readonly') { const binding = /** @type {import('#compiler').Binding} */ ( state.scope.get(declarator.id.name) ); + if (should_proxy_or_freeze(value)) { + value = b.call('$.freeze', value); + } + if (binding.reassigned) { value = b.call('$.source', value); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js index 63a8b21ead98..daf833c90d15 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js @@ -1227,7 +1227,7 @@ function serialize_event_handler(node, { state, visit }) { if ( binding !== null && (binding.kind === 'state' || - binding.kind === 'raw_state' || + binding.kind === 'readonly_state' || binding.kind === 'legacy_reactive' || binding.kind === 'derived' || binding.kind === 'prop' || 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 d4c9173ccc47..1cc8d3c2bb08 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 @@ -446,7 +446,7 @@ function serialize_set_binding(node, context, fallback) { if ( binding.kind !== 'state' && - binding.kind !== 'raw_state' && + binding.kind !== 'readonly_state' && binding.kind !== 'prop' && binding.kind !== 'each' && binding.kind !== 'legacy_reactive' && @@ -559,7 +559,7 @@ const javascript_visitors_runes = { if (node.value != null && node.value.type === 'CallExpression') { const rune = get_rune(node.value, state.scope); - if (rune === '$state' || rune === '$state.raw' || rune === '$derived') { + if (rune === '$state' || rune === '$state.readonly' || rune === '$derived') { return { ...node, value: diff --git a/packages/svelte/src/compiler/phases/constants.js b/packages/svelte/src/compiler/phases/constants.js index 260ece234f7c..42c45625f008 100644 --- a/packages/svelte/src/compiler/phases/constants.js +++ b/packages/svelte/src/compiler/phases/constants.js @@ -72,7 +72,7 @@ export const ElementBindings = [ export const Runes = /** @type {const} */ ([ '$state', - '$state.raw', + '$state.readonly', '$props', '$derived', '$effect', diff --git a/packages/svelte/src/compiler/types/index.d.ts b/packages/svelte/src/compiler/types/index.d.ts index 171bf33b3853..ece3312e3dd5 100644 --- a/packages/svelte/src/compiler/types/index.d.ts +++ b/packages/svelte/src/compiler/types/index.d.ts @@ -258,7 +258,7 @@ export interface Binding { | 'prop' | 'rest_prop' | 'state' - | 'raw_state' + | 'readonly_state' | 'derived' | 'each' | 'store_sub' diff --git a/packages/svelte/src/internal/client/proxy/readonly.js b/packages/svelte/src/internal/client/proxy/readonly.js index 3fce78339769..a31ba1d70a0a 100644 --- a/packages/svelte/src/internal/client/proxy/readonly.js +++ b/packages/svelte/src/internal/client/proxy/readonly.js @@ -1,4 +1,4 @@ -import { define_property } from '../utils.js'; +import { define_property, is_frozen } from '../utils.js'; import { READONLY_SYMBOL, STATE_SYMBOL } from './proxy.js'; /** @@ -6,8 +6,6 @@ import { READONLY_SYMBOL, STATE_SYMBOL } from './proxy.js'; * @typedef {T & { [READONLY_SYMBOL]: Proxy }} StateObject */ -const is_frozen = Object.isFrozen; - /** * Expects a value that was wrapped with `proxy` and makes it readonly. * diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 6fb4cc927a02..f4b17d100ced 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -1,7 +1,7 @@ import { DEV } from 'esm-env'; import { subscribe_to_store } from '../../store/utils.js'; import { EMPTY_FUNC, run_all } from '../common.js'; -import { get_descriptor, get_descriptors, is_array } from './utils.js'; +import { get_descriptor, get_descriptors, is_array, is_frozen, object_freeze } from './utils.js'; import { PROPS_IS_LAZY_INITIAL, PROPS_IS_IMMUTABLE, @@ -9,7 +9,7 @@ import { PROPS_IS_UPDATED } from '../../constants.js'; import { readonly } from './proxy/readonly.js'; -import { proxy, unstate } from './proxy/proxy.js'; +import { READONLY_SYMBOL, STATE_SYMBOL, proxy, unstate } from './proxy/proxy.js'; export const SOURCE = 1; export const DERIVED = 1 << 1; @@ -1899,3 +1899,26 @@ if (DEV) { throw_rune_error('$inspect'); throw_rune_error('$props'); } + +/** + * Expects a value that was wrapped with `freeze` and makes it frozen. + * + * @template {import('./proxy/proxy.js').StateObject} T + * @param {T} value + * @returns {Readonly>} + */ +export function freeze(value) { + if (typeof value === 'object' && value != null && !is_frozen(value)) { + // If the object is already proxified, then unstate the value + if (STATE_SYMBOL in value) { + return object_freeze(unstate(value)); + } + // If the value isn't already read-only then just use that + if (DEV && READONLY_SYMBOL in value) { + return value; + } + // Otherwise freeze the object + object_freeze(value); + } + return value; +} diff --git a/packages/svelte/src/internal/client/utils.js b/packages/svelte/src/internal/client/utils.js index 627d2b34fb95..7c1b01515e76 100644 --- a/packages/svelte/src/internal/client/utils.js +++ b/packages/svelte/src/internal/client/utils.js @@ -5,6 +5,8 @@ export var array_from = Array.from; export var object_keys = Object.keys; export var object_entries = Object.entries; export var object_assign = Object.assign; +export var is_frozen = Object.isFrozen; +export var object_freeze = Object.freeze; export var define_property = Object.defineProperty; export var get_descriptor = Object.getOwnPropertyDescriptor; export var get_descriptors = Object.getOwnPropertyDescriptors; diff --git a/packages/svelte/src/internal/index.js b/packages/svelte/src/internal/index.js index 048105d0f978..709ec9dee90c 100644 --- a/packages/svelte/src/internal/index.js +++ b/packages/svelte/src/internal/index.js @@ -36,7 +36,8 @@ export { effect_active, user_root_effect, inspect, - unwrap + unwrap, + freeze } from './client/runtime.js'; export * from './client/each.js'; diff --git a/packages/svelte/src/main/ambient.d.ts b/packages/svelte/src/main/ambient.d.ts index af12fe6a20ea..7864486aa6b8 100644 --- a/packages/svelte/src/main/ambient.d.ts +++ b/packages/svelte/src/main/ambient.d.ts @@ -19,12 +19,12 @@ declare function $state(): T | undefined; declare namespace $state { /** - * Declares reactive state without applying reactivity to nested properties. + * Declares reactive read-only state. * * Example: * ```ts * - + diff --git a/packages/svelte/tests/runtime-runes/samples/class-private-raw-state/_config.js b/packages/svelte/tests/runtime-runes/samples/class-private-readonly-state/_config.js similarity index 100% rename from packages/svelte/tests/runtime-runes/samples/class-private-raw-state/_config.js rename to packages/svelte/tests/runtime-runes/samples/class-private-readonly-state/_config.js diff --git a/packages/svelte/tests/runtime-runes/samples/class-private-raw-state/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-private-readonly-state/main.svelte similarity index 90% rename from packages/svelte/tests/runtime-runes/samples/class-private-raw-state/main.svelte rename to packages/svelte/tests/runtime-runes/samples/class-private-readonly-state/main.svelte index f4812b1dde28..46001c007b9c 100644 --- a/packages/svelte/tests/runtime-runes/samples/class-private-raw-state/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/class-private-readonly-state/main.svelte @@ -1,6 +1,6 @@ - - diff --git a/packages/svelte/tests/runtime-runes/samples/class-raw-state-object/_config.js b/packages/svelte/tests/runtime-runes/samples/class-readonly-state-object/_config.js similarity index 73% rename from packages/svelte/tests/runtime-runes/samples/class-raw-state-object/_config.js rename to packages/svelte/tests/runtime-runes/samples/class-readonly-state-object/_config.js index 94aeebc39e02..6ad9dff351e4 100644 --- a/packages/svelte/tests/runtime-runes/samples/class-raw-state-object/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/class-readonly-state-object/_config.js @@ -1,8 +1,13 @@ import { test } from '../../test'; +import { log } from './log.js'; export default test({ html: ``, + before_test() { + log.length = 0; + }, + async test({ assert, target }) { const btn = target.querySelector('button'); @@ -11,5 +16,7 @@ export default test({ await btn?.click(); assert.htmlEqual(target.innerHTML, ``); + + assert.deepEqual(log, ['read only', 'read only']); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/class-readonly-state-object/log.js b/packages/svelte/tests/runtime-runes/samples/class-readonly-state-object/log.js new file mode 100644 index 000000000000..d3df521f4da7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-readonly-state-object/log.js @@ -0,0 +1,2 @@ +/** @type {any[]} */ +export const log = []; diff --git a/packages/svelte/tests/runtime-runes/samples/class-readonly-state-object/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-readonly-state-object/main.svelte new file mode 100644 index 000000000000..36647c256c1f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-readonly-state-object/main.svelte @@ -0,0 +1,16 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/class-raw-state/_config.js b/packages/svelte/tests/runtime-runes/samples/class-readonly-state/_config.js similarity index 100% rename from packages/svelte/tests/runtime-runes/samples/class-raw-state/_config.js rename to packages/svelte/tests/runtime-runes/samples/class-readonly-state/_config.js diff --git a/packages/svelte/tests/runtime-runes/samples/class-raw-state/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-readonly-state/main.svelte similarity index 82% rename from packages/svelte/tests/runtime-runes/samples/class-raw-state/main.svelte rename to packages/svelte/tests/runtime-runes/samples/class-readonly-state/main.svelte index 2913d1ececaa..cbe05424232c 100644 --- a/packages/svelte/tests/runtime-runes/samples/class-raw-state/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/class-readonly-state/main.svelte @@ -1,6 +1,6 @@ diff --git a/packages/svelte/tests/runtime-runes/samples/raw-state-replace/_config.js b/packages/svelte/tests/runtime-runes/samples/readonly-state-replace/_config.js similarity index 100% rename from packages/svelte/tests/runtime-runes/samples/raw-state-replace/_config.js rename to packages/svelte/tests/runtime-runes/samples/readonly-state-replace/_config.js diff --git a/packages/svelte/tests/runtime-runes/samples/raw-state-replace/main.svelte b/packages/svelte/tests/runtime-runes/samples/readonly-state-replace/main.svelte similarity index 80% rename from packages/svelte/tests/runtime-runes/samples/raw-state-replace/main.svelte rename to packages/svelte/tests/runtime-runes/samples/readonly-state-replace/main.svelte index 4b5dc5ac59d7..9d7c4c1bd271 100644 --- a/packages/svelte/tests/runtime-runes/samples/raw-state-replace/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/readonly-state-replace/main.svelte @@ -1,5 +1,5 @@ diff --git a/packages/svelte/tests/runtime-runes/samples/class-private-readonly-state/_config.js b/packages/svelte/tests/runtime-runes/samples/class-frozen-state/_config.js similarity index 100% rename from packages/svelte/tests/runtime-runes/samples/class-private-readonly-state/_config.js rename to packages/svelte/tests/runtime-runes/samples/class-frozen-state/_config.js diff --git a/packages/svelte/tests/runtime-runes/samples/class-readonly-state/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-frozen-state/main.svelte similarity index 82% rename from packages/svelte/tests/runtime-runes/samples/class-readonly-state/main.svelte rename to packages/svelte/tests/runtime-runes/samples/class-frozen-state/main.svelte index cbe05424232c..a723976ea969 100644 --- a/packages/svelte/tests/runtime-runes/samples/class-readonly-state/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/class-frozen-state/main.svelte @@ -1,6 +1,6 @@ diff --git a/packages/svelte/tests/runtime-runes/samples/class-readonly-state-object/_config.js b/packages/svelte/tests/runtime-runes/samples/class-private-frozen-state-object/_config.js similarity index 100% rename from packages/svelte/tests/runtime-runes/samples/class-readonly-state-object/_config.js rename to packages/svelte/tests/runtime-runes/samples/class-private-frozen-state-object/_config.js diff --git a/packages/svelte/tests/runtime-runes/samples/class-readonly-state-object/log.js b/packages/svelte/tests/runtime-runes/samples/class-private-frozen-state-object/log.js similarity index 100% rename from packages/svelte/tests/runtime-runes/samples/class-readonly-state-object/log.js rename to packages/svelte/tests/runtime-runes/samples/class-private-frozen-state-object/log.js diff --git a/packages/svelte/tests/runtime-runes/samples/class-private-readonly-state-object/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-private-frozen-state-object/main.svelte similarity index 92% rename from packages/svelte/tests/runtime-runes/samples/class-private-readonly-state-object/main.svelte rename to packages/svelte/tests/runtime-runes/samples/class-private-frozen-state-object/main.svelte index a4b639972795..8899e2fdb4f1 100644 --- a/packages/svelte/tests/runtime-runes/samples/class-private-readonly-state-object/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/class-private-frozen-state-object/main.svelte @@ -2,7 +2,7 @@ import { log } from './log.js'; class Counter { - #count = $state.readonly(); + #count = $state.frozen(); constructor(initial_count) { this.#count = { a: initial_count }; diff --git a/packages/svelte/tests/runtime-runes/samples/class-readonly-state/_config.js b/packages/svelte/tests/runtime-runes/samples/class-private-frozen-state/_config.js similarity index 100% rename from packages/svelte/tests/runtime-runes/samples/class-readonly-state/_config.js rename to packages/svelte/tests/runtime-runes/samples/class-private-frozen-state/_config.js diff --git a/packages/svelte/tests/runtime-runes/samples/class-private-readonly-state/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-private-frozen-state/main.svelte similarity index 90% rename from packages/svelte/tests/runtime-runes/samples/class-private-readonly-state/main.svelte rename to packages/svelte/tests/runtime-runes/samples/class-private-frozen-state/main.svelte index 46001c007b9c..f509f351bbfa 100644 --- a/packages/svelte/tests/runtime-runes/samples/class-private-readonly-state/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/class-private-frozen-state/main.svelte @@ -1,6 +1,6 @@ - + +- ++ + +

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

``` -> Objects and arrays passed to `$state.frozen` will be shallowly frozen using `Object.freeze()`. If you don't want them to be frozen, then you should pass in a clone of the object or array you want to use. +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 frozen state can _contain_ reactive state (for example, a frozen array of reactive objects). + +> Objects and arrays passed to `$state.frozen` will be shallowly frozen using `Object.freeze()`. If you don't want this, pass in a clone of the object or array instead. ## `$derived` From 34581f67b9c722797f375a1b41ae154bbfab444c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 27 Dec 2023 07:22:05 +0000 Subject: [PATCH 18/18] Update sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com> --- .../svelte-5-preview/src/routes/docs/content/01-api/02-runes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 5e54fdb154b6..70a1cbb288ba 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 @@ -66,7 +66,7 @@ In non-runes mode, a `let` declaration is treated as reactive state if it is upd ## `$state.frozen` -State declared with `$state.frozen` 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: +State declared with `$state.frozen` 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