Skip to content

feat: add $lazy rune #11210

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions packages/svelte/src/ambient.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,3 +252,21 @@ declare function $inspect<T extends any[]>(
* https://svelte-5-preview.vercel.app/docs/runes#$host
*/
declare function $host<El extends HTMLElement = HTMLElement>(): 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<V>(value: V, setter: (value: V) => unknown): V;
2 changes: 2 additions & 0 deletions packages/svelte/src/compiler/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions packages/svelte/src/compiler/phases/2-analyze/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
}
},
Expand Down
8 changes: 8 additions & 0 deletions packages/svelte/src/compiler/phases/2-analyze/validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]));
}
Expand Down
3 changes: 2 additions & 1 deletion packages/svelte/src/compiler/phases/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ export const Runes = /** @type {const} */ ([
'$effect.root',
'$inspect',
'$inspect().with',
'$host'
'$host',
'$lazy'
]);

/**
Expand Down
1 change: 1 addition & 0 deletions packages/svelte/src/internal/client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,4 @@ export {
validate_snippet,
validate_void_dynamic_element
} from '../shared/validate.js';
export { lazy_array } from './reactivity/lazy.js';
24 changes: 24 additions & 0 deletions packages/svelte/src/internal/client/reactivity/lazy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { is_array } from '../utils.js';

/**
* @param {Array<unknown[] | { get: () => 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;
}
18 changes: 18 additions & 0 deletions packages/svelte/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2777,4 +2777,22 @@ declare function $inspect<T extends any[]>(
*/
declare function $host<El extends HTMLElement = HTMLElement>(): 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<V>(value: V, setter: (value: V) => unknown): V;

//# sourceMappingURL=index.d.ts.map
23 changes: 12 additions & 11 deletions sites/svelte-5-preview/src/lib/CodeMirror.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
]
};
Expand Down
Loading