Skip to content

Commit a60ba2a

Browse files
committed
feat: add $lazy rune
1 parent 777527b commit a60ba2a

File tree

9 files changed

+195
-12
lines changed

9 files changed

+195
-12
lines changed

packages/svelte/src/ambient.d.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,3 +252,21 @@ declare function $inspect<T extends any[]>(
252252
* https://svelte-5-preview.vercel.app/docs/runes#$host
253253
*/
254254
declare function $host<El extends HTMLElement = HTMLElement>(): El;
255+
256+
/**
257+
* Creates a lazy object or array property binding, similar to that of a getter/setter. If passed
258+
* a single argument, the lazy property binding with be read-only.
259+
*
260+
* ```svelte
261+
* let count = $state(0);
262+
* let double = $derived(count * 2);
263+
*
264+
* let object = {
265+
* count: $lazy(count, value => count = value),
266+
* double: $lazy(double),
267+
* };
268+
* ```
269+
*
270+
* https://svelte-5-preview.vercel.app/docs/runes#$lazy
271+
*/
272+
declare function $lazy<V>(value: V, setter: (value: V) => unknown): V;

packages/svelte/src/compiler/errors.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,8 @@ const runes = {
189189
'invalid-effect-location': () => `$effect() can only be used as an expression statement`,
190190
'invalid-host-location': () =>
191191
`$host() can only be used inside custom element component instances`,
192+
'invalid-lazy-location': () =>
193+
`$lazy() can only be used as the property value within an object or array literal expression`,
192194
/**
193195
* @param {boolean} is_binding
194196
* @param {boolean} show_details

packages/svelte/src/compiler/phases/2-analyze/validation.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -808,6 +808,14 @@ function validate_call_expression(node, scope, path) {
808808
error(node, 'invalid-props-location');
809809
}
810810

811+
if (rune === '$lazy') {
812+
if (node.arguments.length === 0 || node.arguments.length > 2) {
813+
error(node, 'invalid-rune-args-length', rune, [1, 2]);
814+
}
815+
if (parent.type === 'Property' || parent.type === 'ArrayExpression') return;
816+
error(node, 'invalid-lazy-location');
817+
}
818+
811819
if (rune === '$bindable') {
812820
if (parent.type === 'AssignmentPattern' && path.at(-3)?.type === 'ObjectPattern') {
813821
const declarator = path.at(-4);

packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,132 @@ export const javascript_visitors_runes = {
398398

399399
context.next();
400400
},
401+
ArrayExpression(node, context) {
402+
const elements = node.elements;
403+
let new_elements = [];
404+
/** @type {import('estree').Expression[]} */
405+
const lazy_call_parts = [];
406+
let has_lazy = false;
407+
408+
for (const element of elements) {
409+
const rune = get_rune(element, context.state.scope);
410+
411+
if (rune === '$lazy' && element?.type === 'CallExpression') {
412+
const args = element.arguments;
413+
if (new_elements.length > 0) {
414+
lazy_call_parts.push(b.array(new_elements));
415+
new_elements = [];
416+
}
417+
has_lazy = true;
418+
const lazy_object_properties = [
419+
b.prop(
420+
'init',
421+
b.id('get'),
422+
b.thunk(/** @type {import('estree').Expression} */ (context.visit(args[0])))
423+
)
424+
];
425+
const second_arg = args[1];
426+
427+
if (
428+
second_arg &&
429+
(second_arg.type === 'ArrowFunctionExpression' ||
430+
second_arg.type === 'FunctionExpression')
431+
) {
432+
lazy_object_properties.push(
433+
b.prop(
434+
'init',
435+
b.id('set'),
436+
/** @type {import('estree').Expression} */ (context.visit(second_arg))
437+
)
438+
);
439+
}
440+
441+
lazy_call_parts.push(b.object(lazy_object_properties));
442+
} else if (element === null) {
443+
new_elements.push(null);
444+
} else {
445+
new_elements.push(/** @type {import('estree').Expression} */ (context.visit(element)));
446+
}
447+
}
448+
449+
if (has_lazy) {
450+
if (new_elements.length > 0) {
451+
lazy_call_parts.push(b.array(new_elements));
452+
}
453+
return b.call('$.lazy_array', ...lazy_call_parts);
454+
}
455+
456+
context.next();
457+
},
458+
ObjectExpression(node, context) {
459+
const properties = node.properties;
460+
const new_properties = [];
461+
let has_lazy = false;
462+
463+
for (const property of properties) {
464+
if (property.type === 'Property') {
465+
const value = property.value;
466+
const rune = get_rune(value, context.state.scope);
467+
468+
if (rune === '$lazy' && value.type === 'CallExpression') {
469+
const key = /** @type {import('estree').Expression} */ (property.key);
470+
const args = value.arguments;
471+
has_lazy = true;
472+
new_properties.push(
473+
b.prop(
474+
'get',
475+
key,
476+
b.function(
477+
null,
478+
[],
479+
b.block([
480+
b.return(/** @type {import('estree').Expression} */ (context.visit(args[0])))
481+
])
482+
)
483+
)
484+
);
485+
const second_arg = args[1];
486+
if (
487+
second_arg &&
488+
(second_arg.type === 'ArrowFunctionExpression' ||
489+
second_arg.type === 'FunctionExpression')
490+
) {
491+
new_properties.push(
492+
b.prop(
493+
'set',
494+
key,
495+
b.function(
496+
null,
497+
second_arg.params,
498+
second_arg.body.type === 'BlockStatement'
499+
? /** @type {import('estree').BlockStatement} */ (
500+
context.visit(second_arg.body)
501+
)
502+
: b.block([
503+
b.stmt(
504+
/** @type {import('estree').Expression} */ (
505+
context.visit(second_arg.body)
506+
)
507+
)
508+
])
509+
)
510+
)
511+
);
512+
}
513+
} else {
514+
new_properties.push(/** @type {import('estree').Property} */ (context.visit(property)));
515+
}
516+
} else {
517+
new_properties.push(/** @type {import('estree').Property} */ (context.visit(property)));
518+
}
519+
}
520+
521+
if (has_lazy) {
522+
return { ...node, properties: new_properties };
523+
}
524+
525+
context.next();
526+
},
401527
CallExpression(node, context) {
402528
const rune = get_rune(node, context.state.scope);
403529

packages/svelte/src/compiler/phases/constants.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ export const Runes = /** @type {const} */ ([
4242
'$effect.root',
4343
'$inspect',
4444
'$inspect().with',
45-
'$host'
45+
'$host',
46+
'$lazy'
4647
]);
4748

4849
/**

packages/svelte/src/internal/client/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,3 +145,4 @@ export {
145145
validate_snippet,
146146
validate_void_dynamic_element
147147
} from '../shared/validate.js';
148+
export { lazy_array } from './reactivity/lazy.js';
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { is_array } from '../utils';
2+
3+
/**
4+
* @param {Array<unknown[] | { get: () => unknown, set: (v: unknown) => unknown }>} parts
5+
* @returns {unknown[]}
6+
*/
7+
export function lazy_array(...parts) {
8+
const arr = [];
9+
10+
for (var i = 0; i < parts.length; i++) {
11+
var part = parts[i];
12+
13+
if (is_array(part)) {
14+
arr.push(...part);
15+
} else {
16+
Object.defineProperty(arr, arr.length, {
17+
enumerable: true,
18+
...part
19+
});
20+
}
21+
}
22+
23+
return arr;
24+
}

packages/svelte/types/index.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2777,4 +2777,6 @@ declare function $inspect<T extends any[]>(
27772777
*/
27782778
declare function $host<El extends HTMLElement = HTMLElement>(): El;
27792779

2780+
declare function $lazy<V>(value: V, setter: (value: V) => unknown): V;
2781+
27802782
//# sourceMappingURL=index.d.ts.map

sites/svelte-5-preview/src/lib/CodeMirror.svelte

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -205,33 +205,34 @@
205205
return {
206206
from: word.from - 1,
207207
options: [
208-
{ label: '$state', type: 'keyword', boost: 12 },
209-
{ label: '$props', type: 'keyword', boost: 11 },
210-
{ label: '$derived', type: 'keyword', boost: 10 },
208+
{ label: '$state', type: 'keyword', boost: 13 },
209+
{ label: '$props', type: 'keyword', boost: 12 },
210+
{ label: '$derived', type: 'keyword', boost: 11 },
211211
snip('$derived.by(() => {\n\t${}\n});', {
212212
label: '$derived.by',
213213
type: 'keyword',
214-
boost: 9
214+
boost: 10
215215
}),
216-
snip('$effect(() => {\n\t${}\n});', { label: '$effect', type: 'keyword', boost: 8 }),
216+
snip('$effect(() => {\n\t${}\n});', { label: '$effect', type: 'keyword', boost: 9 }),
217217
snip('$effect.pre(() => {\n\t${}\n});', {
218218
label: '$effect.pre',
219219
type: 'keyword',
220-
boost: 7
220+
boost: 8
221221
}),
222-
{ label: '$state.frozen', type: 'keyword', boost: 6 },
223-
{ label: '$bindable', type: 'keyword', boost: 5 },
222+
{ label: '$state.frozen', type: 'keyword', boost: 7 },
223+
{ label: '$bindable', type: 'keyword', boost: 6 },
224224
snip('$effect.root(() => {\n\t${}\n});', {
225225
label: '$effect.root',
226226
type: 'keyword',
227-
boost: 4
227+
boost: 5
228228
}),
229-
{ label: '$state.snapshot', type: 'keyword', boost: 3 },
229+
{ label: '$state.snapshot', type: 'keyword', boost: 4 },
230230
snip('$effect.active()', {
231231
label: '$effect.active',
232232
type: 'keyword',
233-
boost: 2
233+
boost: 3
234234
}),
235+
{ label: '$lazy', type: 'keyword', boost: 2 },
235236
{ label: '$inspect', type: 'keyword', boost: 1 }
236237
]
237238
};

0 commit comments

Comments
 (0)