From a60ba2abaf5f3cac24b98a1e3c77c520bf4b54a8 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Wed, 17 Apr 2024 15:24:26 +0100 Subject: [PATCH 1/5] feat: add $lazy rune --- packages/svelte/src/ambient.d.ts | 18 +++ packages/svelte/src/compiler/errors.js | 2 + .../compiler/phases/2-analyze/validation.js | 8 ++ .../client/visitors/javascript-runes.js | 126 ++++++++++++++++++ .../svelte/src/compiler/phases/constants.js | 3 +- packages/svelte/src/internal/client/index.js | 1 + .../src/internal/client/reactivity/lazy.js | 24 ++++ packages/svelte/types/index.d.ts | 2 + .../src/lib/CodeMirror.svelte | 23 ++-- 9 files changed, 195 insertions(+), 12 deletions(-) create mode 100644 packages/svelte/src/internal/client/reactivity/lazy.js diff --git a/packages/svelte/src/ambient.d.ts b/packages/svelte/src/ambient.d.ts index b65346dd7d43..fd7120973701 100644 --- a/packages/svelte/src/ambient.d.ts +++ b/packages/svelte/src/ambient.d.ts @@ -252,3 +252,21 @@ declare function $inspect( * https://svelte-5-preview.vercel.app/docs/runes#$host */ declare function $host(): El; + +/** + * Creates a lazy object or array property binding, similar to that of a getter/setter. If passed + * a single argument, the lazy property binding with be read-only. + * + * ```svelte + * let count = $state(0); + * let double = $derived(count * 2); + * + * let object = { + * count: $lazy(count, value => count = value), + * double: $lazy(double), + * }; + * ``` + * + * https://svelte-5-preview.vercel.app/docs/runes#$lazy + */ +declare function $lazy(value: V, setter: (value: V) => unknown): V; diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index 8281ab50d8b3..66468a6e8dae 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -189,6 +189,8 @@ const runes = { 'invalid-effect-location': () => `$effect() can only be used as an expression statement`, 'invalid-host-location': () => `$host() can only be used inside custom element component instances`, + 'invalid-lazy-location': () => + `$lazy() can only be used as the property value within an object or array literal expression`, /** * @param {boolean} is_binding * @param {boolean} show_details diff --git a/packages/svelte/src/compiler/phases/2-analyze/validation.js b/packages/svelte/src/compiler/phases/2-analyze/validation.js index 7d77003c9d1f..4794d68f3677 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/validation.js +++ b/packages/svelte/src/compiler/phases/2-analyze/validation.js @@ -808,6 +808,14 @@ function validate_call_expression(node, scope, path) { error(node, 'invalid-props-location'); } + if (rune === '$lazy') { + if (node.arguments.length === 0 || node.arguments.length > 2) { + error(node, 'invalid-rune-args-length', rune, [1, 2]); + } + if (parent.type === 'Property' || parent.type === 'ArrayExpression') return; + error(node, 'invalid-lazy-location'); + } + if (rune === '$bindable') { if (parent.type === 'AssignmentPattern' && path.at(-3)?.type === 'ObjectPattern') { const declarator = path.at(-4); 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 7857909aa523..6fbca9e88723 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 @@ -398,6 +398,132 @@ export const javascript_visitors_runes = { context.next(); }, + ArrayExpression(node, context) { + const elements = node.elements; + let new_elements = []; + /** @type {import('estree').Expression[]} */ + const lazy_call_parts = []; + let has_lazy = false; + + for (const element of elements) { + const rune = get_rune(element, context.state.scope); + + if (rune === '$lazy' && element?.type === 'CallExpression') { + const args = element.arguments; + if (new_elements.length > 0) { + lazy_call_parts.push(b.array(new_elements)); + new_elements = []; + } + has_lazy = true; + const lazy_object_properties = [ + b.prop( + 'init', + b.id('get'), + b.thunk(/** @type {import('estree').Expression} */ (context.visit(args[0]))) + ) + ]; + const second_arg = args[1]; + + if ( + second_arg && + (second_arg.type === 'ArrowFunctionExpression' || + second_arg.type === 'FunctionExpression') + ) { + lazy_object_properties.push( + b.prop( + 'init', + b.id('set'), + /** @type {import('estree').Expression} */ (context.visit(second_arg)) + ) + ); + } + + lazy_call_parts.push(b.object(lazy_object_properties)); + } else if (element === null) { + new_elements.push(null); + } else { + new_elements.push(/** @type {import('estree').Expression} */ (context.visit(element))); + } + } + + if (has_lazy) { + if (new_elements.length > 0) { + lazy_call_parts.push(b.array(new_elements)); + } + return b.call('$.lazy_array', ...lazy_call_parts); + } + + context.next(); + }, + ObjectExpression(node, context) { + const properties = node.properties; + const new_properties = []; + let has_lazy = false; + + for (const property of properties) { + if (property.type === 'Property') { + const value = property.value; + const rune = get_rune(value, context.state.scope); + + if (rune === '$lazy' && value.type === 'CallExpression') { + const key = /** @type {import('estree').Expression} */ (property.key); + const args = value.arguments; + has_lazy = true; + new_properties.push( + b.prop( + 'get', + key, + b.function( + null, + [], + b.block([ + b.return(/** @type {import('estree').Expression} */ (context.visit(args[0]))) + ]) + ) + ) + ); + const second_arg = args[1]; + if ( + second_arg && + (second_arg.type === 'ArrowFunctionExpression' || + second_arg.type === 'FunctionExpression') + ) { + new_properties.push( + b.prop( + 'set', + key, + b.function( + null, + second_arg.params, + second_arg.body.type === 'BlockStatement' + ? /** @type {import('estree').BlockStatement} */ ( + context.visit(second_arg.body) + ) + : b.block([ + b.stmt( + /** @type {import('estree').Expression} */ ( + context.visit(second_arg.body) + ) + ) + ]) + ) + ) + ); + } + } else { + new_properties.push(/** @type {import('estree').Property} */ (context.visit(property))); + } + } else { + new_properties.push(/** @type {import('estree').Property} */ (context.visit(property))); + } + } + + if (has_lazy) { + return { ...node, properties: new_properties }; + } + + context.next(); + }, CallExpression(node, context) { const rune = get_rune(node, context.state.scope); diff --git a/packages/svelte/src/compiler/phases/constants.js b/packages/svelte/src/compiler/phases/constants.js index 0fec2e894838..1170fca761f0 100644 --- a/packages/svelte/src/compiler/phases/constants.js +++ b/packages/svelte/src/compiler/phases/constants.js @@ -42,7 +42,8 @@ export const Runes = /** @type {const} */ ([ '$effect.root', '$inspect', '$inspect().with', - '$host' + '$host', + '$lazy' ]); /** diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 18bd4740c862..509ef4a93045 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -145,3 +145,4 @@ export { validate_snippet, validate_void_dynamic_element } from '../shared/validate.js'; +export { lazy_array } from './reactivity/lazy.js'; diff --git a/packages/svelte/src/internal/client/reactivity/lazy.js b/packages/svelte/src/internal/client/reactivity/lazy.js new file mode 100644 index 000000000000..9818948f367f --- /dev/null +++ b/packages/svelte/src/internal/client/reactivity/lazy.js @@ -0,0 +1,24 @@ +import { is_array } from '../utils'; + +/** + * @param {Array unknown, set: (v: unknown) => unknown }>} parts + * @returns {unknown[]} + */ +export function lazy_array(...parts) { + const arr = []; + + for (var i = 0; i < parts.length; i++) { + var part = parts[i]; + + if (is_array(part)) { + arr.push(...part); + } else { + Object.defineProperty(arr, arr.length, { + enumerable: true, + ...part + }); + } + } + + return arr; +} diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index a47745a63aaa..92ecd180cfb0 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -2777,4 +2777,6 @@ declare function $inspect( */ declare function $host(): El; +declare function $lazy(value: V, setter: (value: V) => unknown): V; + //# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/sites/svelte-5-preview/src/lib/CodeMirror.svelte b/sites/svelte-5-preview/src/lib/CodeMirror.svelte index 347cf7d93882..b8216999e702 100644 --- a/sites/svelte-5-preview/src/lib/CodeMirror.svelte +++ b/sites/svelte-5-preview/src/lib/CodeMirror.svelte @@ -205,33 +205,34 @@ return { from: word.from - 1, options: [ - { label: '$state', type: 'keyword', boost: 12 }, - { label: '$props', type: 'keyword', boost: 11 }, - { label: '$derived', type: 'keyword', boost: 10 }, + { label: '$state', type: 'keyword', boost: 13 }, + { label: '$props', type: 'keyword', boost: 12 }, + { label: '$derived', type: 'keyword', boost: 11 }, snip('$derived.by(() => {\n\t${}\n});', { label: '$derived.by', type: 'keyword', - boost: 9 + boost: 10 }), - snip('$effect(() => {\n\t${}\n});', { label: '$effect', type: 'keyword', boost: 8 }), + snip('$effect(() => {\n\t${}\n});', { label: '$effect', type: 'keyword', boost: 9 }), snip('$effect.pre(() => {\n\t${}\n});', { label: '$effect.pre', type: 'keyword', - boost: 7 + boost: 8 }), - { label: '$state.frozen', type: 'keyword', boost: 6 }, - { label: '$bindable', type: 'keyword', boost: 5 }, + { label: '$state.frozen', type: 'keyword', boost: 7 }, + { label: '$bindable', type: 'keyword', boost: 6 }, snip('$effect.root(() => {\n\t${}\n});', { label: '$effect.root', type: 'keyword', - boost: 4 + boost: 5 }), - { label: '$state.snapshot', type: 'keyword', boost: 3 }, + { label: '$state.snapshot', type: 'keyword', boost: 4 }, snip('$effect.active()', { label: '$effect.active', type: 'keyword', - boost: 2 + boost: 3 }), + { label: '$lazy', type: 'keyword', boost: 2 }, { label: '$inspect', type: 'keyword', boost: 1 } ] }; From 1c082a50d3fca1650d6790c5617e29b69d751c55 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Wed, 17 Apr 2024 15:31:43 +0100 Subject: [PATCH 2/5] types --- packages/svelte/types/index.d.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 92ecd180cfb0..568dfa0f75ee 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -2777,6 +2777,22 @@ declare function $inspect( */ declare function $host(): El; +/** + * Creates a lazy object or array property binding, similar to that of a getter/setter. If passed + * a single argument, the lazy property binding with be read-only. + * + * ```svelte + * let count = $state(0); + * let double = $derived(count * 2); + * + * let object = { + * count: $lazy(count, value => count = value), + * double: $lazy(double), + * }; + * ``` + * + * https://svelte-5-preview.vercel.app/docs/runes#$lazy + */ declare function $lazy(value: V, setter: (value: V) => unknown): V; //# sourceMappingURL=index.d.ts.map \ No newline at end of file From f31dcd0d6e62d01193394e217661fe4c6169dd4a Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Wed, 17 Apr 2024 15:44:56 +0100 Subject: [PATCH 3/5] update warning --- packages/svelte/src/compiler/phases/2-analyze/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index c1ed378ca3da..8051d5a542fc 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -892,8 +892,8 @@ const runes_scope_tweaker = { CallExpression(node, { state, next }) { const rune = get_rune(node, state.scope); - // `$inspect(foo)` should not trigger the `static-state-reference` warning - if (rune === '$inspect') { + // `$inspect(foo)` or $lazy(foo) should not trigger the `static-state-reference` warning + if (rune === '$inspect' || rune === '$lazy') { next({ ...state, function_depth: state.function_depth + 1 }); } }, From a610391a27839a573f1dcff999260bb91d41ab08 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Wed, 17 Apr 2024 15:50:22 +0100 Subject: [PATCH 4/5] ssr --- .../compiler/phases/3-transform/server/transform-server.js | 4 ++++ 1 file changed, 4 insertions(+) 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 d5402351c7ee..c2d38f63af9c 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 @@ -794,6 +794,10 @@ const javascript_visitors_runes = { return b.literal(false); } + if (rune === '$lazy') { + return /** @type {import('estree').Expression} */ (context.visit(node.arguments[0])); + } + if (rune === '$state.snapshot') { return /** @type {import('estree').Expression} */ (context.visit(node.arguments[0])); } From fe9c395235ee303d51cd0d3b95e54ea3f4ef36ac Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Wed, 17 Apr 2024 16:05:14 +0100 Subject: [PATCH 5/5] fix extension --- packages/svelte/src/internal/client/reactivity/lazy.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/reactivity/lazy.js b/packages/svelte/src/internal/client/reactivity/lazy.js index 9818948f367f..3546bb1ca7c5 100644 --- a/packages/svelte/src/internal/client/reactivity/lazy.js +++ b/packages/svelte/src/internal/client/reactivity/lazy.js @@ -1,4 +1,4 @@ -import { is_array } from '../utils'; +import { is_array } from '../utils.js'; /** * @param {Array unknown, set: (v: unknown) => unknown }>} parts