From 605f0baa9d749282b465f4a2b8a5068282d62cd0 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Thu, 7 Dec 2023 20:55:51 +0000 Subject: [PATCH] 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 0000000000..d80d717ef5 --- /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 bd43aca398..24aa5a8e67 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 8dad40e238..5aee1a0ab0 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 d82cdd68f2..bc74290665 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 ef9b466bad..498c3566bc 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 305dbcd543..cc2b7ed192 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 136bb020a9..ddda0744e7 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 0526c9c4bf..05aad64c4c 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 7c06cac973..63a8b21ead 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 79c62e396d..d4c9173ccc 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 ec40c50c83..260ece234f 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 6435c20207..171bf33b38 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 9ae68610c2..a34144e519 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 0000000000..436ce99798 --- /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 0000000000..f4812b1dde --- /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 0000000000..436ce99798 --- /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 0000000000..2913d1ecec --- /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 0000000000..1e6333bdeb --- /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 0000000000..d3df521f4d --- /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 0000000000..137eeafd67 --- /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 2cd3392e53..d883aab0a2 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: