From b754fdddcae88cea97445e12c62f89572a4281fa Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Fri, 19 Jan 2024 22:16:01 +0000 Subject: [PATCH 01/11] chore: add $derived.fn rune --- .changeset/nervous-spoons-relax.md | 5 +++ .../src/compiler/phases/2-analyze/index.js | 18 ++++++-- .../compiler/phases/2-analyze/validation.js | 2 +- .../phases/3-transform/client/types.d.ts | 2 +- .../client/visitors/javascript-runes.js | 38 ++++++++++++---- .../3-transform/server/transform-server.js | 19 ++++++++ .../svelte/src/compiler/phases/constants.js | 1 + .../svelte/src/internal/client/runtime.js | 10 ++++- packages/svelte/src/main/ambient.d.ts | 26 +++++++++++ .../samples/class-state-derived-fn/_config.js | 30 +++++++++++++ .../class-state-derived-fn/main.svelte | 11 +++++ .../samples/derived-fn/_config.js | 13 ++++++ .../samples/derived-fn/main.svelte | 6 +++ .../src/lib/CodeMirror.svelte | 11 +++-- .../routes/docs/content/01-api/02-runes.md | 45 +++++++++++++++++++ 15 files changed, 219 insertions(+), 18 deletions(-) create mode 100644 .changeset/nervous-spoons-relax.md create mode 100644 packages/svelte/tests/runtime-runes/samples/class-state-derived-fn/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/class-state-derived-fn/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/derived-fn/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/derived-fn/main.svelte diff --git a/.changeset/nervous-spoons-relax.md b/.changeset/nervous-spoons-relax.md new file mode 100644 index 000000000000..070d573af2ef --- /dev/null +++ b/.changeset/nervous-spoons-relax.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +chore: add $derived.fn rune diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 0c207f95080b..e322f1e05769 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -670,7 +670,13 @@ const runes_scope_js_tweaker = { const callee = node.init.callee; if (callee.type !== 'Identifier' && callee.type !== 'MemberExpression') return; - if (rune !== '$state' && rune !== '$state.frozen' && rune !== '$derived') return; + if ( + rune !== '$state' && + rune !== '$state.frozen' && + rune !== '$derived' && + rune !== '$derived.fn' + ) + return; for (const path of extract_paths(node.id)) { // @ts-ignore this fails in CI for some insane reason @@ -700,7 +706,13 @@ const runes_scope_tweaker = { const callee = init.callee; if (callee.type !== 'Identifier' && callee.type !== 'MemberExpression') return; - if (rune !== '$state' && rune !== '$state.frozen' && rune !== '$derived' && rune !== '$props') + if ( + rune !== '$state' && + rune !== '$state.frozen' && + rune !== '$derived' && + rune !== '$derived.fn' && + rune !== '$props' + ) return; for (const path of extract_paths(node.id)) { @@ -711,7 +723,7 @@ const runes_scope_tweaker = { ? 'state' : rune === '$state.frozen' ? 'frozen_state' - : rune === '$derived' + : rune === '$derived' || rune === '$derived.fn' ? 'derived' : path.is_rest ? 'rest_prop' diff --git a/packages/svelte/src/compiler/phases/2-analyze/validation.js b/packages/svelte/src/compiler/phases/2-analyze/validation.js index c07afd7484f1..848207e02789 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/validation.js +++ b/packages/svelte/src/compiler/phases/2-analyze/validation.js @@ -715,7 +715,7 @@ function validate_call_expression(node, scope, path) { error(node, 'invalid-props-location'); } - if (rune === '$state' || rune === '$derived') { + if (rune === '$state' || rune === '$derived' || rune === '$derived.fn') { if (parent.type === 'VariableDeclarator') return; if (parent.type === 'PropertyDefinition' && !parent.static && !parent.computed) return; error(node, rune === '$derived' ? 'invalid-derived-location' : 'invalid-state-location'); 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 85bf8dd278e0..d7abee355099 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 @@ -58,7 +58,7 @@ export interface ComponentClientTransformState extends ClientTransformState { } export interface StateField { - kind: 'state' | 'frozen_state' | 'derived'; + kind: 'state' | 'frozen_state' | 'derived' | 'derived_fn'; id: PrivateIdentifier; } 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 0150c30293ad..c9d3c7d7382b 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,11 +29,22 @@ export const javascript_visitors_runes = { if (definition.value?.type === 'CallExpression') { const rune = get_rune(definition.value, state.scope); - if (rune === '$state' || rune === '$state.frozen' || rune === '$derived') { + if ( + rune === '$state' || + rune === '$state.frozen' || + rune === '$derived' || + rune === '$derived.fn' + ) { /** @type {import('../types.js').StateField} */ const field = { kind: - rune === '$state' ? 'state' : rune === '$state.frozen' ? 'frozen_state' : 'derived', + rune === '$state' + ? 'state' + : rune === '$state.frozen' + ? 'frozen_state' + : rune === '$derived.fn' + ? 'derived_fn' + : 'derived', // @ts-expect-error this is set in the next pass id: is_private ? definition.key : null }; @@ -91,7 +102,9 @@ export const javascript_visitors_runes = { '$.source', should_proxy_or_freeze(init) ? b.call('$.freeze', init) : init ) - : b.call('$.derived', b.thunk(init)); + : field.kind === 'derived_fn' + ? b.call('$.derived', init) + : b.call('$.derived', b.thunk(init)); } else { // if no arguments, we know it's state as `$derived()` is a compile error value = b.call('$.source'); @@ -133,7 +146,7 @@ export const javascript_visitors_runes = { ); } - if (field.kind === 'derived' && state.options.dev) { + if ((field.kind === 'derived' || field.kind === 'derived_fn') && state.options.dev) { body.push( b.method( 'set', @@ -273,9 +286,14 @@ export const javascript_visitors_runes = { continue; } - if (rune === '$derived') { + if (rune === '$derived' || rune === '$derived.fn') { if (declarator.id.type === 'Identifier') { - declarations.push(b.declarator(declarator.id, b.call('$.derived', b.thunk(value)))); + declarations.push( + b.declarator( + declarator.id, + b.call('$.derived', rune === '$derived.fn' ? value : b.thunk(value)) + ) + ); } else { const bindings = state.scope.get_bindings(declarator); const id = state.scope.generate('derived_value'); @@ -284,9 +302,13 @@ export const javascript_visitors_runes = { b.id(id), b.call( '$.derived', - b.thunk( + b.arrow( + [b.id('$$derived')], b.block([ - b.let(declarator.id, value), + b.let( + declarator.id, + rune === '$derived.fn' ? b.call(value, b.id('$$derived')) : value + ), b.return(b.array(bindings.map((binding) => binding.node))) ]) ) 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 cc1649c3702f..4e1fee32da3c 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 @@ -558,6 +558,15 @@ const javascript_visitors_runes = { : /** @type {import('estree').Expression} */ (visit(node.value.arguments[0])) }; } + if (rune === '$derived.fn') { + return { + ...node, + value: + node.value.arguments.length === 0 + ? null + : b.call(/** @type {import('estree').Expression} */ (visit(node.value.arguments[0]))) + }; + } } next(); }, @@ -583,6 +592,16 @@ const javascript_visitors_runes = { ? b.id('undefined') : /** @type {import('estree').Expression} */ (visit(args[0])); + if (rune === '$derived.fn') { + declarations.push( + b.declarator( + /** @type {import('estree').Pattern} */ (visit(declarator.id)), + b.call(value) + ) + ); + continue; + } + if (declarator.id.type === 'Identifier') { declarations.push(b.declarator(declarator.id, value)); continue; diff --git a/packages/svelte/src/compiler/phases/constants.js b/packages/svelte/src/compiler/phases/constants.js index b8663e97f013..5d60e428e588 100644 --- a/packages/svelte/src/compiler/phases/constants.js +++ b/packages/svelte/src/compiler/phases/constants.js @@ -75,6 +75,7 @@ export const Runes = /** @type {const} */ ([ '$state.frozen', '$props', '$derived', + '$derived.fn', '$effect', '$effect.pre', '$effect.active', diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 61f0cecf7842..4908e3f7957f 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -328,6 +328,7 @@ function is_signal_dirty(signal) { */ function execute_signal_fn(signal) { const init = signal.i; + const flags = signal.f; const previous_dependencies = current_dependencies; const previous_dependencies_index = current_dependencies_index; const previous_untracked_writes = current_untracked_writes; @@ -335,7 +336,7 @@ function execute_signal_fn(signal) { const previous_block = current_block; const previous_component_context = current_component_context; const previous_skip_consumer = current_skip_consumer; - const is_render_effect = (signal.f & RENDER_EFFECT) !== 0; + const is_render_effect = (flags & RENDER_EFFECT) !== 0; const previous_untracking = current_untracking; current_dependencies = /** @type {null | import('./types.js').Signal[]} */ (null); current_dependencies_index = 0; @@ -343,7 +344,7 @@ function execute_signal_fn(signal) { current_consumer = signal; current_block = signal.b; current_component_context = signal.x; - current_skip_consumer = !is_flushing_effect && (signal.f & UNOWNED) !== 0; + current_skip_consumer = !is_flushing_effect && (flags & UNOWNED) !== 0; current_untracking = false; // Render effects are invoked when the UI is about to be updated - run beforeUpdate at that point @@ -362,6 +363,11 @@ function execute_signal_fn(signal) { /** @type {import('./types.js').Block} */ (signal.b), /** @type {import('./types.js').Signal} */ (signal) ); + } else if ((flags & DERIVED) !== 0) { + const prev_value = signal.v; + res = /** @type {(prev: V) => V} */ (init)( + /** @type {V} */ (prev_value === UNINITIALIZED ? undefined : prev_value) + ); } else { res = /** @type {() => V} */ (init)(); } diff --git a/packages/svelte/src/main/ambient.d.ts b/packages/svelte/src/main/ambient.d.ts index 63b949de7372..fae2c850e5f7 100644 --- a/packages/svelte/src/main/ambient.d.ts +++ b/packages/svelte/src/main/ambient.d.ts @@ -59,6 +59,32 @@ declare namespace $state { */ declare function $derived(expression: T): T; +declare namespace $derived { + /** + * Sometimes you need to create complex derivations which don't fit inside a short expression. + * In this case, you can resort to `$derived.fn` which accepts a function as its argument and returns its value. + * + * Example: + * ```ts + * $derived.fn(() => { + * let tmp = count; + * if (count > 10) { + * tmp += 100; + * } + * return tmp * 2; + * }); + * ``` + * + * `$derived.fn` passes the previous derived value as a single argument to the derived function. This can be useful for when + * you might want to compare the latest derived value with the previous derived value. Furthermore, you might want to pluck out + * specific properties of derived state to use in the new derived state given a certain condition – which can be useful when dealing + * with more complex state machines. + * + * https://svelte-5-preview.vercel.app/docs/runes#$derived-fn + */ + export function fn(fn: (prev: T) => T): void; +} + /** * Runs code when a component is mounted to the DOM, and then whenever its dependencies change, i.e. `$state` or `$derived` values. * The timing of the execution is after the DOM has been updated. diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-derived-fn/_config.js b/packages/svelte/tests/runtime-runes/samples/class-state-derived-fn/_config.js new file mode 100644 index 000000000000..54e9e296fc96 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-state-derived-fn/_config.js @@ -0,0 +1,30 @@ +import { test } from '../../test'; + +export default test({ + html: ` + +

doubled: 0

+ `, + + async test({ assert, target }) { + const btn = target.querySelector('button'); + + await btn?.click(); + assert.htmlEqual( + target.innerHTML, + ` + +

doubled: 2

+ ` + ); + + await btn?.click(); + assert.htmlEqual( + target.innerHTML, + ` + +

doubled: 4

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-derived-fn/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-state-derived-fn/main.svelte new file mode 100644 index 000000000000..3cb257cc6a1b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-state-derived-fn/main.svelte @@ -0,0 +1,11 @@ + + + +

doubled: {counter.doubled}

diff --git a/packages/svelte/tests/runtime-runes/samples/derived-fn/_config.js b/packages/svelte/tests/runtime-runes/samples/derived-fn/_config.js new file mode 100644 index 000000000000..09bd0a9aad8d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/derived-fn/_config.js @@ -0,0 +1,13 @@ +import { test } from '../../test'; + +export default test({ + html: ``, + + async test({ assert, target, window }) { + const btn = target.querySelector('button'); + const clickEvent = new window.Event('click', { bubbles: true }); + await btn?.dispatchEvent(clickEvent); + + assert.htmlEqual(target.innerHTML, ``); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/derived-fn/main.svelte b/packages/svelte/tests/runtime-runes/samples/derived-fn/main.svelte new file mode 100644 index 000000000000..e116a2cccfe2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/derived-fn/main.svelte @@ -0,0 +1,6 @@ + + + diff --git a/sites/svelte-5-preview/src/lib/CodeMirror.svelte b/sites/svelte-5-preview/src/lib/CodeMirror.svelte index e8997aa9f29e..e1fc1bf125c8 100644 --- a/sites/svelte-5-preview/src/lib/CodeMirror.svelte +++ b/sites/svelte-5-preview/src/lib/CodeMirror.svelte @@ -205,9 +205,14 @@ return { from: word.from - 1, options: [ - { label: '$state', type: 'keyword', boost: 9 }, - { label: '$props', type: 'keyword', boost: 8 }, - { label: '$derived', type: 'keyword', boost: 7 }, + { label: '$state', type: 'keyword', boost: 10 }, + { label: '$props', type: 'keyword', boost: 9 }, + { label: '$derived', type: 'keyword', boost: 8 }, + snip('$derived.fn(() => {\n\t${}\n});', { + label: '$derived.fn', + type: 'keyword', + boost: 7 + }), snip('$effect(() => {\n\t${}\n});', { label: '$effect', type: 'keyword', boost: 6 }), snip('$effect.pre(() => {\n\t${}\n});', { label: '$effect.pre', 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 b597307dd2a5..c919c172ca21 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 @@ -134,6 +134,51 @@ If the value of a reactive variable is being computed it should be replaced with ``` ...`double` will be calculated first despite the source order. In runes mode, `triple` cannot reference `double` before it has been declared. +### `$derived.fn` + +Sometimes you need to create complex derivations which don't fit inside a short expression. In this case, you can resort to `$derived.fn` which accepts a function as its argument and returns its value. + +```svelte + + + +``` + +`$derived.fn` passes the previous derived value as a single argument to the derived function. This can be useful for when +you might want to compare the latest derived value with the previous derived value. Furthermore, you might want to pluck out +specific properties of derived state to use in the new derived state given a certain condition – which can be useful when dealing +with more complex state machines. + +```svelte + + + +``` + ## `$effect` To run side-effects like logging or analytics whenever some specific values change, or when a component is mounted to the DOM, we can use the `$effect` rune: From 6622b7ffa1f19fdeffcfa7403cd3ee3e8a8287f0 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Fri, 19 Jan 2024 22:36:12 +0000 Subject: [PATCH 02/11] fix strange bug --- .../compiler/phases/3-transform/client/visitors/template.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 fa6c5608dffc..d795c3883249 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 @@ -1711,12 +1711,13 @@ export const template_visitors = { const declaration = node.declaration.declarations[0]; // TODO we can almost certainly share some code with $derived(...) if (declaration.id.type === 'Identifier') { + // debugger state.init.push( b.const( declaration.id, b.call( '$.derived', - b.thunk(/** @type {import('estree').Expression} */ (visit(declaration.init))) + b.arrow([], /** @type {import('estree').Expression} */ (visit(declaration.init))) ) ) ); From 25f8e128620136c1563ebea0dc27e22b26502793 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Fri, 19 Jan 2024 22:41:18 +0000 Subject: [PATCH 03/11] update types --- packages/svelte/types/index.d.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index a70f6e79927a..c19d158cc609 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -2461,6 +2461,32 @@ declare namespace $state { */ declare function $derived(expression: T): T; +declare namespace $derived { + /** + * Sometimes you need to create complex derivations which don't fit inside a short expression. + * In this case, you can resort to `$derived.fn` which accepts a function as its argument and returns its value. + * + * Example: + * ```ts + * $derived.fn(() => { + * let tmp = count; + * if (count > 10) { + * tmp += 100; + * } + * return tmp * 2; + * }); + * ``` + * + * `$derived.fn` passes the previous derived value as a single argument to the derived function. This can be useful for when + * you might want to compare the latest derived value with the previous derived value. Furthermore, you might want to pluck out + * specific properties of derived state to use in the new derived state given a certain condition – which can be useful when dealing + * with more complex state machines. + * + * https://svelte-5-preview.vercel.app/docs/runes#$derived-fn + */ + export function fn(fn: (prev: T) => T): void; +} + /** * Runs code when a component is mounted to the DOM, and then whenever its dependencies change, i.e. `$state` or `$derived` values. * The timing of the execution is after the DOM has been updated. From cb2772cadc9a1dd5f19d325a1b1d746b72e2f044 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 29 Jan 2024 09:53:49 -0500 Subject: [PATCH 04/11] remove prev stuff --- .../3-transform/client/visitors/template.js | 3 +-- .../svelte/src/internal/client/runtime.js | 10 ++------ packages/svelte/src/main/ambient.d.ts | 7 +----- .../routes/docs/content/01-api/02-runes.md | 24 ------------------- 4 files changed, 4 insertions(+), 40 deletions(-) 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 d795c3883249..fa6c5608dffc 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 @@ -1711,13 +1711,12 @@ export const template_visitors = { const declaration = node.declaration.declarations[0]; // TODO we can almost certainly share some code with $derived(...) if (declaration.id.type === 'Identifier') { - // debugger state.init.push( b.const( declaration.id, b.call( '$.derived', - b.arrow([], /** @type {import('estree').Expression} */ (visit(declaration.init))) + b.thunk(/** @type {import('estree').Expression} */ (visit(declaration.init))) ) ) ); diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 4908e3f7957f..61f0cecf7842 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -328,7 +328,6 @@ function is_signal_dirty(signal) { */ function execute_signal_fn(signal) { const init = signal.i; - const flags = signal.f; const previous_dependencies = current_dependencies; const previous_dependencies_index = current_dependencies_index; const previous_untracked_writes = current_untracked_writes; @@ -336,7 +335,7 @@ function execute_signal_fn(signal) { const previous_block = current_block; const previous_component_context = current_component_context; const previous_skip_consumer = current_skip_consumer; - const is_render_effect = (flags & RENDER_EFFECT) !== 0; + const is_render_effect = (signal.f & RENDER_EFFECT) !== 0; const previous_untracking = current_untracking; current_dependencies = /** @type {null | import('./types.js').Signal[]} */ (null); current_dependencies_index = 0; @@ -344,7 +343,7 @@ function execute_signal_fn(signal) { current_consumer = signal; current_block = signal.b; current_component_context = signal.x; - current_skip_consumer = !is_flushing_effect && (flags & UNOWNED) !== 0; + current_skip_consumer = !is_flushing_effect && (signal.f & UNOWNED) !== 0; current_untracking = false; // Render effects are invoked when the UI is about to be updated - run beforeUpdate at that point @@ -363,11 +362,6 @@ function execute_signal_fn(signal) { /** @type {import('./types.js').Block} */ (signal.b), /** @type {import('./types.js').Signal} */ (signal) ); - } else if ((flags & DERIVED) !== 0) { - const prev_value = signal.v; - res = /** @type {(prev: V) => V} */ (init)( - /** @type {V} */ (prev_value === UNINITIALIZED ? undefined : prev_value) - ); } else { res = /** @type {() => V} */ (init)(); } diff --git a/packages/svelte/src/main/ambient.d.ts b/packages/svelte/src/main/ambient.d.ts index fae2c850e5f7..d949b1ec29cb 100644 --- a/packages/svelte/src/main/ambient.d.ts +++ b/packages/svelte/src/main/ambient.d.ts @@ -75,14 +75,9 @@ declare namespace $derived { * }); * ``` * - * `$derived.fn` passes the previous derived value as a single argument to the derived function. This can be useful for when - * you might want to compare the latest derived value with the previous derived value. Furthermore, you might want to pluck out - * specific properties of derived state to use in the new derived state given a certain condition – which can be useful when dealing - * with more complex state machines. - * * https://svelte-5-preview.vercel.app/docs/runes#$derived-fn */ - export function fn(fn: (prev: T) => T): void; + export function fn(fn: () => T): void; } /** 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 c919c172ca21..d128972b26b4 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 @@ -155,30 +155,6 @@ Sometimes you need to create complex derivations which don't fit inside a short ``` -`$derived.fn` passes the previous derived value as a single argument to the derived function. This can be useful for when -you might want to compare the latest derived value with the previous derived value. Furthermore, you might want to pluck out -specific properties of derived state to use in the new derived state given a certain condition – which can be useful when dealing -with more complex state machines. - -```svelte - - - -``` - ## `$effect` To run side-effects like logging or analytics whenever some specific values change, or when a component is mounted to the DOM, we can use the `$effect` rune: From f3681d8fc73f46dd92273ca18fac60529e674608 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 29 Jan 2024 09:54:41 -0500 Subject: [PATCH 05/11] regenerate types --- packages/svelte/types/index.d.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index c19d158cc609..6819181073cf 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -2477,14 +2477,9 @@ declare namespace $derived { * }); * ``` * - * `$derived.fn` passes the previous derived value as a single argument to the derived function. This can be useful for when - * you might want to compare the latest derived value with the previous derived value. Furthermore, you might want to pluck out - * specific properties of derived state to use in the new derived state given a certain condition – which can be useful when dealing - * with more complex state machines. - * * https://svelte-5-preview.vercel.app/docs/runes#$derived-fn */ - export function fn(fn: (prev: T) => T): void; + export function fn(fn: () => T): void; } /** From e3ff46e0de7b02e197d29a2cc68d5f7e3f91c07f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 29 Jan 2024 09:58:13 -0500 Subject: [PATCH 06/11] $derived.fn -> $derived.call --- .changeset/nervous-spoons-relax.md | 2 +- .../src/compiler/phases/2-analyze/index.js | 6 ++-- .../compiler/phases/2-analyze/validation.js | 2 +- .../phases/3-transform/client/types.d.ts | 2 +- .../client/visitors/javascript-runes.js | 16 ++++----- .../3-transform/server/transform-server.js | 4 +-- .../svelte/src/compiler/phases/constants.js | 2 +- packages/svelte/src/main/ambient.d.ts | 4 +-- .../class-state-derived-fn/main.svelte | 2 +- .../samples/derived-fn/main.svelte | 2 +- packages/svelte/types/index.d.ts | 36 +++++++++---------- .../src/lib/CodeMirror.svelte | 4 +-- .../routes/docs/content/01-api/02-runes.md | 6 ++-- 13 files changed, 44 insertions(+), 44 deletions(-) diff --git a/.changeset/nervous-spoons-relax.md b/.changeset/nervous-spoons-relax.md index 070d573af2ef..9480a8ac472e 100644 --- a/.changeset/nervous-spoons-relax.md +++ b/.changeset/nervous-spoons-relax.md @@ -2,4 +2,4 @@ "svelte": patch --- -chore: add $derived.fn rune +chore: add $derived.call rune diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index e322f1e05769..e74d2ee9ab9e 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -674,7 +674,7 @@ const runes_scope_js_tweaker = { rune !== '$state' && rune !== '$state.frozen' && rune !== '$derived' && - rune !== '$derived.fn' + rune !== '$derived.call' ) return; @@ -710,7 +710,7 @@ const runes_scope_tweaker = { rune !== '$state' && rune !== '$state.frozen' && rune !== '$derived' && - rune !== '$derived.fn' && + rune !== '$derived.call' && rune !== '$props' ) return; @@ -723,7 +723,7 @@ const runes_scope_tweaker = { ? 'state' : rune === '$state.frozen' ? 'frozen_state' - : rune === '$derived' || rune === '$derived.fn' + : rune === '$derived' || rune === '$derived.call' ? 'derived' : path.is_rest ? 'rest_prop' diff --git a/packages/svelte/src/compiler/phases/2-analyze/validation.js b/packages/svelte/src/compiler/phases/2-analyze/validation.js index 848207e02789..4202e1b9fe03 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/validation.js +++ b/packages/svelte/src/compiler/phases/2-analyze/validation.js @@ -715,7 +715,7 @@ function validate_call_expression(node, scope, path) { error(node, 'invalid-props-location'); } - if (rune === '$state' || rune === '$derived' || rune === '$derived.fn') { + if (rune === '$state' || rune === '$derived' || rune === '$derived.call') { if (parent.type === 'VariableDeclarator') return; if (parent.type === 'PropertyDefinition' && !parent.static && !parent.computed) return; error(node, rune === '$derived' ? 'invalid-derived-location' : 'invalid-state-location'); 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 d7abee355099..14b628a89d54 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 @@ -58,7 +58,7 @@ export interface ComponentClientTransformState extends ClientTransformState { } export interface StateField { - kind: 'state' | 'frozen_state' | 'derived' | 'derived_fn'; + kind: 'state' | 'frozen_state' | 'derived' | 'derived_call'; id: PrivateIdentifier; } 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 c9d3c7d7382b..ecafb98b6979 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 @@ -33,7 +33,7 @@ export const javascript_visitors_runes = { rune === '$state' || rune === '$state.frozen' || rune === '$derived' || - rune === '$derived.fn' + rune === '$derived.call' ) { /** @type {import('../types.js').StateField} */ const field = { @@ -42,8 +42,8 @@ export const javascript_visitors_runes = { ? 'state' : rune === '$state.frozen' ? 'frozen_state' - : rune === '$derived.fn' - ? 'derived_fn' + : rune === '$derived.call' + ? 'derived_call' : 'derived', // @ts-expect-error this is set in the next pass id: is_private ? definition.key : null @@ -102,7 +102,7 @@ export const javascript_visitors_runes = { '$.source', should_proxy_or_freeze(init) ? b.call('$.freeze', init) : init ) - : field.kind === 'derived_fn' + : field.kind === 'derived_call' ? b.call('$.derived', init) : b.call('$.derived', b.thunk(init)); } else { @@ -146,7 +146,7 @@ export const javascript_visitors_runes = { ); } - if ((field.kind === 'derived' || field.kind === 'derived_fn') && state.options.dev) { + if ((field.kind === 'derived' || field.kind === 'derived_call') && state.options.dev) { body.push( b.method( 'set', @@ -286,12 +286,12 @@ export const javascript_visitors_runes = { continue; } - if (rune === '$derived' || rune === '$derived.fn') { + if (rune === '$derived' || rune === '$derived.call') { if (declarator.id.type === 'Identifier') { declarations.push( b.declarator( declarator.id, - b.call('$.derived', rune === '$derived.fn' ? value : b.thunk(value)) + b.call('$.derived', rune === '$derived.call' ? value : b.thunk(value)) ) ); } else { @@ -307,7 +307,7 @@ export const javascript_visitors_runes = { b.block([ b.let( declarator.id, - rune === '$derived.fn' ? b.call(value, b.id('$$derived')) : value + rune === '$derived.call' ? b.call(value, b.id('$$derived')) : value ), b.return(b.array(bindings.map((binding) => binding.node))) ]) 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 4e1fee32da3c..14b8cd21059e 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 @@ -558,7 +558,7 @@ const javascript_visitors_runes = { : /** @type {import('estree').Expression} */ (visit(node.value.arguments[0])) }; } - if (rune === '$derived.fn') { + if (rune === '$derived.call') { return { ...node, value: @@ -592,7 +592,7 @@ const javascript_visitors_runes = { ? b.id('undefined') : /** @type {import('estree').Expression} */ (visit(args[0])); - if (rune === '$derived.fn') { + if (rune === '$derived.call') { declarations.push( b.declarator( /** @type {import('estree').Pattern} */ (visit(declarator.id)), diff --git a/packages/svelte/src/compiler/phases/constants.js b/packages/svelte/src/compiler/phases/constants.js index 5d60e428e588..cb8aaef1c8e8 100644 --- a/packages/svelte/src/compiler/phases/constants.js +++ b/packages/svelte/src/compiler/phases/constants.js @@ -75,7 +75,7 @@ export const Runes = /** @type {const} */ ([ '$state.frozen', '$props', '$derived', - '$derived.fn', + '$derived.call', '$effect', '$effect.pre', '$effect.active', diff --git a/packages/svelte/src/main/ambient.d.ts b/packages/svelte/src/main/ambient.d.ts index d949b1ec29cb..116295fcf740 100644 --- a/packages/svelte/src/main/ambient.d.ts +++ b/packages/svelte/src/main/ambient.d.ts @@ -62,11 +62,11 @@ declare function $derived(expression: T): T; declare namespace $derived { /** * Sometimes you need to create complex derivations which don't fit inside a short expression. - * In this case, you can resort to `$derived.fn` which accepts a function as its argument and returns its value. + * In this case, you can resort to `$derived.call` which accepts a function as its argument and returns its value. * * Example: * ```ts - * $derived.fn(() => { + * $derived.call(() => { * let tmp = count; * if (count > 10) { * tmp += 100; diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-derived-fn/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-state-derived-fn/main.svelte index 3cb257cc6a1b..2816780c25ac 100644 --- a/packages/svelte/tests/runtime-runes/samples/class-state-derived-fn/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/class-state-derived-fn/main.svelte @@ -1,7 +1,7 @@ diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 6819181073cf..718b7e7229f8 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -988,15 +988,15 @@ declare module 'svelte/compiler' { filename?: string | undefined; } | undefined): Promise; export class CompileError extends Error { - + constructor(code: string, message: string, position: [number, number] | undefined); - + filename: CompileError_1['filename']; - + position: CompileError_1['position']; - + start: CompileError_1['start']; - + end: CompileError_1['end']; code: string; } @@ -1007,9 +1007,9 @@ declare module 'svelte/compiler' { * */ export const VERSION: string; class Scope { - + constructor(root: ScopeRoot, parent: Scope | null, porous: boolean); - + root: ScopeRoot; /** * A map of every identifier declared by this scope, and all the @@ -1033,25 +1033,25 @@ declare module 'svelte/compiler' { * which is usually an error. Block statements do not increase this value */ function_depth: number; - + declare(node: import('estree').Identifier, kind: Binding['kind'], declaration_kind: DeclarationKind, initial?: null | import('estree').Expression | import('estree').FunctionDeclaration | import('estree').ClassDeclaration | import('estree').ImportDeclaration | EachBlock): Binding; child(porous?: boolean): Scope; - + generate(preferred_name: string): string; - + get(name: string): Binding | null; - + get_bindings(node: import('estree').VariableDeclarator | LetDirective): Binding[]; - + owner(name: string): Scope | null; - + reference(node: import('estree').Identifier, path: SvelteNode[]): void; #private; } class ScopeRoot { - + conflicts: Set; - + unique(preferred_name: string): import("estree").Identifier; } interface BaseNode { @@ -2464,11 +2464,11 @@ declare function $derived(expression: T): T; declare namespace $derived { /** * Sometimes you need to create complex derivations which don't fit inside a short expression. - * In this case, you can resort to `$derived.fn` which accepts a function as its argument and returns its value. + * In this case, you can resort to `$derived.call` which accepts a function as its argument and returns its value. * * Example: * ```ts - * $derived.fn(() => { + * $derived.call(() => { * let tmp = count; * if (count > 10) { * tmp += 100; @@ -2603,4 +2603,4 @@ declare function $inspect( ...values: T ): { with: (fn: (type: 'init' | 'update', ...values: T) => void) => void }; -//# sourceMappingURL=index.d.ts.map \ No newline at end of file +//# sourceMappingURL=index.d.ts.map diff --git a/sites/svelte-5-preview/src/lib/CodeMirror.svelte b/sites/svelte-5-preview/src/lib/CodeMirror.svelte index e1fc1bf125c8..9a2438be02b2 100644 --- a/sites/svelte-5-preview/src/lib/CodeMirror.svelte +++ b/sites/svelte-5-preview/src/lib/CodeMirror.svelte @@ -208,8 +208,8 @@ { label: '$state', type: 'keyword', boost: 10 }, { label: '$props', type: 'keyword', boost: 9 }, { label: '$derived', type: 'keyword', boost: 8 }, - snip('$derived.fn(() => {\n\t${}\n});', { - label: '$derived.fn', + snip('$derived.call(() => {\n\t${}\n});', { + label: '$derived.call', type: 'keyword', boost: 7 }), 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 d128972b26b4..78d232fa81b7 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 @@ -134,14 +134,14 @@ If the value of a reactive variable is being computed it should be replaced with ``` ...`double` will be calculated first despite the source order. In runes mode, `triple` cannot reference `double` before it has been declared. -### `$derived.fn` +### `$derived.call` -Sometimes you need to create complex derivations which don't fit inside a short expression. In this case, you can resort to `$derived.fn` which accepts a function as its argument and returns its value. +Sometimes you need to create complex derivations which don't fit inside a short expression. In this case, you can resort to `$derived.call` which accepts a function as its argument and returns its value. ```svelte - ``` +In essence, `$derived(expression)` is equivalent to `$derived.call(() => expression)`. + ## `$effect` To run side-effects like logging or analytics whenever some specific values change, or when a component is mounted to the DOM, we can use the `$effect` rune: From 2a0877be0b34a6ef30392a75e9c77cfce58b2641 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 29 Jan 2024 11:23:39 -0500 Subject: [PATCH 08/11] regenerate types --- packages/svelte/types/index.d.ts | 50 ++++++++++++++++---------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 718b7e7229f8..1b0b5c221b69 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -988,15 +988,15 @@ declare module 'svelte/compiler' { filename?: string | undefined; } | undefined): Promise; export class CompileError extends Error { - + constructor(code: string, message: string, position: [number, number] | undefined); - + filename: CompileError_1['filename']; - + position: CompileError_1['position']; - + start: CompileError_1['start']; - + end: CompileError_1['end']; code: string; } @@ -1007,9 +1007,9 @@ declare module 'svelte/compiler' { * */ export const VERSION: string; class Scope { - + constructor(root: ScopeRoot, parent: Scope | null, porous: boolean); - + root: ScopeRoot; /** * A map of every identifier declared by this scope, and all the @@ -1033,25 +1033,25 @@ declare module 'svelte/compiler' { * which is usually an error. Block statements do not increase this value */ function_depth: number; - + declare(node: import('estree').Identifier, kind: Binding['kind'], declaration_kind: DeclarationKind, initial?: null | import('estree').Expression | import('estree').FunctionDeclaration | import('estree').ClassDeclaration | import('estree').ImportDeclaration | EachBlock): Binding; child(porous?: boolean): Scope; - + generate(preferred_name: string): string; - + get(name: string): Binding | null; - + get_bindings(node: import('estree').VariableDeclarator | LetDirective): Binding[]; - + owner(name: string): Scope | null; - + reference(node: import('estree').Identifier, path: SvelteNode[]): void; #private; } class ScopeRoot { - + conflicts: Set; - + unique(preferred_name: string): import("estree").Identifier; } interface BaseNode { @@ -2463,21 +2463,21 @@ declare function $derived(expression: T): T; declare namespace $derived { /** - * Sometimes you need to create complex derivations which don't fit inside a short expression. - * In this case, you can resort to `$derived.call` which accepts a function as its argument and returns its value. + * Sometimes you need to create complex derivations that don't fit inside a short expression. + * In these cases, you can use `$derived.call` which accepts a function as its argument. * * Example: * ```ts - * $derived.call(() => { - * let tmp = count; - * if (count > 10) { - * tmp += 100; - * } - * return tmp * 2; + * let total = $derived.call(() => { + * let result = 0; + * for (const n of numbers) { + * result += n; + * } + * return result; * }); * ``` * - * https://svelte-5-preview.vercel.app/docs/runes#$derived-fn + * https://svelte-5-preview.vercel.app/docs/runes#$derived-call */ export function fn(fn: () => T): void; } @@ -2603,4 +2603,4 @@ declare function $inspect( ...values: T ): { with: (fn: (type: 'init' | 'update', ...values: T) => void) => void }; -//# sourceMappingURL=index.d.ts.map +//# sourceMappingURL=index.d.ts.map \ No newline at end of file From b99c8f4e4162d3dd98897f0ff999a97a52b81b59 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 29 Jan 2024 11:37:35 -0500 Subject: [PATCH 09/11] get rid of $$derived --- .../3-transform/client/visitors/javascript-runes.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) 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 ecafb98b6979..542280a78585 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 @@ -302,13 +302,9 @@ export const javascript_visitors_runes = { b.id(id), b.call( '$.derived', - b.arrow( - [b.id('$$derived')], + b.thunk( b.block([ - b.let( - declarator.id, - rune === '$derived.call' ? b.call(value, b.id('$$derived')) : value - ), + b.let(declarator.id, rune === '$derived.call' ? b.call(value) : value), b.return(b.array(bindings.map((binding) => binding.node))) ]) ) From a3f74b71047eea0b4029e8e1ced485b62cb699e3 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 29 Jan 2024 12:07:26 -0500 Subject: [PATCH 10/11] tighten up validation etc --- packages/svelte/src/compiler/errors.js | 7 ++-- .../compiler/phases/2-analyze/validation.js | 34 ++++++++++++------- .../svelte/src/compiler/utils/builders.js | 1 + packages/svelte/src/compiler/warnings.js | 4 ++- 4 files changed, 28 insertions(+), 18 deletions(-) diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index ba617ead2d01..f748721e67fe 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -171,10 +171,9 @@ const runes = { `$props() assignment must not contain nested properties or computed keys`, 'invalid-props-location': () => `$props() can only be used at the top level of components as a variable declaration initializer`, - 'invalid-derived-location': () => - `$derived() can only be used as a variable declaration initializer or a class field`, - 'invalid-state-location': () => - `$state() can only be used as a variable declaration initializer or a class field`, + /** @param {string} rune */ + 'invalid-state-location': (rune) => + `${rune}(...) can only be used as a variable declaration initializer or a class field`, 'invalid-effect-location': () => `$effect() can only be used as an expression statement`, /** * @param {boolean} is_binding diff --git a/packages/svelte/src/compiler/phases/2-analyze/validation.js b/packages/svelte/src/compiler/phases/2-analyze/validation.js index 4202e1b9fe03..5ac6e651f498 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/validation.js +++ b/packages/svelte/src/compiler/phases/2-analyze/validation.js @@ -718,7 +718,7 @@ function validate_call_expression(node, scope, path) { if (rune === '$state' || rune === '$derived' || rune === '$derived.call') { if (parent.type === 'VariableDeclarator') return; if (parent.type === 'PropertyDefinition' && !parent.static && !parent.computed) return; - error(node, rune === '$derived' ? 'invalid-derived-location' : 'invalid-state-location'); + error(node, 'invalid-state-location', rune); } if (rune === '$effect' || rune === '$effect.pre') { @@ -786,10 +786,10 @@ export const validation_runes_js = { const args = /** @type {import('estree').CallExpression} */ (init).arguments; - if (rune === '$derived' && args.length !== 1) { - error(node, 'invalid-rune-args-length', '$derived', [1]); + if ((rune === '$derived' || rune === '$derived.call') && args.length !== 1) { + error(node, 'invalid-rune-args-length', rune, [1]); } else if (rune === '$state' && args.length > 1) { - error(node, 'invalid-rune-args-length', '$state', [0, 1]); + error(node, 'invalid-rune-args-length', rune, [0, 1]); } else if (rune === '$props') { error(node, 'invalid-props-location'); } @@ -811,7 +811,7 @@ export const validation_runes_js = { definition.value?.type === 'CallExpression' ) { const rune = get_rune(definition.value, context.state.scope); - if (rune === '$derived') { + if (rune === '$derived' || rune === '$derived.call') { private_derived_state.push(definition.key.name); } } @@ -938,14 +938,11 @@ export const validation_runes = merge(validation, a11y_validators, { context.type === 'Identifier' && (context.name === '$state' || context.name === '$derived') ) { - error( - node, - context.name === '$derived' ? 'invalid-derived-location' : 'invalid-state-location' - ); + error(node, 'invalid-state-location', context.name); } next({ ...state }); }, - VariableDeclarator(node, { state }) { + VariableDeclarator(node, { state, path }) { const init = unwrap_ts_expression(node.init); const rune = get_rune(init, state.scope); @@ -953,10 +950,21 @@ export const validation_runes = merge(validation, a11y_validators, { const args = /** @type {import('estree').CallExpression} */ (init).arguments; - if (rune === '$derived' && args.length !== 1) { - error(node, 'invalid-rune-args-length', '$derived', [1]); + if (rune === '$derived') { + const arg = args[0]; + if ( + arg.type === 'CallExpression' && + (arg.callee.type === 'ArrowFunctionExpression' || arg.callee.type === 'FunctionExpression') + ) { + warn(state.analysis.warnings, node, path, 'derived-iife'); + } + } + + // TODO some of this is duplicated with above, seems off + if ((rune === '$derived' || rune === '$derived.call') && args.length !== 1) { + error(node, 'invalid-rune-args-length', rune, [1]); } else if (rune === '$state' && args.length > 1) { - error(node, 'invalid-rune-args-length', '$state', [0, 1]); + error(node, 'invalid-rune-args-length', rune, [0, 1]); } else if (rune === '$props') { if (state.has_props_rune) { error(node, 'duplicate-props-rune'); diff --git a/packages/svelte/src/compiler/utils/builders.js b/packages/svelte/src/compiler/utils/builders.js index 1595f798792b..e926dce2bd87 100644 --- a/packages/svelte/src/compiler/utils/builders.js +++ b/packages/svelte/src/compiler/utils/builders.js @@ -395,6 +395,7 @@ export function thunk(expression) { expression.type === 'CallExpression' && expression.callee.type !== 'Super' && expression.callee.type !== 'MemberExpression' && + expression.callee.type !== 'CallExpression' && expression.arguments.length === 0 ) { return expression.callee; diff --git a/packages/svelte/src/compiler/warnings.js b/packages/svelte/src/compiler/warnings.js index 67c2dc333bbc..5c810664ba85 100644 --- a/packages/svelte/src/compiler/warnings.js +++ b/packages/svelte/src/compiler/warnings.js @@ -23,7 +23,9 @@ const runes = { `Referencing a local variable with a $ prefix will create a store subscription. Please rename ${name} to avoid the ambiguity.`, /** @param {string} name */ 'non-state-reference': (name) => - `${name} is updated, but is not declared with $state(...). Changing its value will not correctly trigger updates.` + `${name} is updated, but is not declared with $state(...). Changing its value will not correctly trigger updates.`, + 'derived-iife': () => + `Use \`$derived.call(() => {...})\` instead of \`$derived((() => {...})());\`` }; /** @satisfies {Warnings} */ From 97261bf84626bc51b762681e3e3aeb602790f9a9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 29 Jan 2024 12:23:39 -0500 Subject: [PATCH 11/11] fix tests --- .../compiler/phases/2-analyze/validation.js | 20 +++++++++---------- .../class-state-field-static/_config.js | 2 +- .../samples/runes-no-rune-each/_config.js | 2 +- .../runes-wrong-derived-placement/_config.js | 4 ++-- .../runes-wrong-state-placement/_config.js | 2 +- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/validation.js b/packages/svelte/src/compiler/phases/2-analyze/validation.js index 5ac6e651f498..6ba5149fbffc 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/validation.js +++ b/packages/svelte/src/compiler/phases/2-analyze/validation.js @@ -950,16 +950,6 @@ export const validation_runes = merge(validation, a11y_validators, { const args = /** @type {import('estree').CallExpression} */ (init).arguments; - if (rune === '$derived') { - const arg = args[0]; - if ( - arg.type === 'CallExpression' && - (arg.callee.type === 'ArrowFunctionExpression' || arg.callee.type === 'FunctionExpression') - ) { - warn(state.analysis.warnings, node, path, 'derived-iife'); - } - } - // TODO some of this is duplicated with above, seems off if ((rune === '$derived' || rune === '$derived.call') && args.length !== 1) { error(node, 'invalid-rune-args-length', rune, [1]); @@ -999,6 +989,16 @@ export const validation_runes = merge(validation, a11y_validators, { } } } + + if (rune === '$derived') { + const arg = args[0]; + if ( + arg.type === 'CallExpression' && + (arg.callee.type === 'ArrowFunctionExpression' || arg.callee.type === 'FunctionExpression') + ) { + warn(state.analysis.warnings, node, path, 'derived-iife'); + } + } }, // TODO this is a code smell. need to refactor this stuff ClassBody: validation_runes_js.ClassBody, diff --git a/packages/svelte/tests/compiler-errors/samples/class-state-field-static/_config.js b/packages/svelte/tests/compiler-errors/samples/class-state-field-static/_config.js index 27991ba42715..dca19b460619 100644 --- a/packages/svelte/tests/compiler-errors/samples/class-state-field-static/_config.js +++ b/packages/svelte/tests/compiler-errors/samples/class-state-field-static/_config.js @@ -3,7 +3,7 @@ import { test } from '../../test'; export default test({ error: { code: 'invalid-state-location', - message: '$state() can only be used as a variable declaration initializer or a class field', + message: '$state(...) can only be used as a variable declaration initializer or a class field', position: [33, 41] } }); diff --git a/packages/svelte/tests/compiler-errors/samples/runes-no-rune-each/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-no-rune-each/_config.js index 86c28248a010..9089b6accab9 100644 --- a/packages/svelte/tests/compiler-errors/samples/runes-no-rune-each/_config.js +++ b/packages/svelte/tests/compiler-errors/samples/runes-no-rune-each/_config.js @@ -3,6 +3,6 @@ import { test } from '../../test'; export default test({ error: { code: 'invalid-state-location', - message: '$state() can only be used as a variable declaration initializer or a class field' + message: '$state(...) can only be used as a variable declaration initializer or a class field' } }); diff --git a/packages/svelte/tests/compiler-errors/samples/runes-wrong-derived-placement/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-wrong-derived-placement/_config.js index a11c307ece90..73c8bf12999c 100644 --- a/packages/svelte/tests/compiler-errors/samples/runes-wrong-derived-placement/_config.js +++ b/packages/svelte/tests/compiler-errors/samples/runes-wrong-derived-placement/_config.js @@ -2,7 +2,7 @@ import { test } from '../../test'; export default test({ error: { - code: 'invalid-derived-location', - message: '$derived() can only be used as a variable declaration initializer or a class field' + code: 'invalid-state-location', + message: '$derived(...) can only be used as a variable declaration initializer or a class field' } }); diff --git a/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-placement/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-placement/_config.js index 86c28248a010..9089b6accab9 100644 --- a/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-placement/_config.js +++ b/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-placement/_config.js @@ -3,6 +3,6 @@ import { test } from '../../test'; export default test({ error: { code: 'invalid-state-location', - message: '$state() can only be used as a variable declaration initializer or a class field' + message: '$state(...) can only be used as a variable declaration initializer or a class field' } });