Skip to content

Commit a339c28

Browse files
breaking: apply fallback value every time in runes mode (#10797)
* breaking: apply fallback value every time in runes mode closes #9948 * apply fallback value in setter * encapsulate fallback logic * we should keep this logic out of b.set, since it's very specific to accessors * oops --------- Co-authored-by: Rich Harris <[email protected]>
1 parent e8ce418 commit a339c28

File tree

11 files changed

+191
-73
lines changed

11 files changed

+191
-73
lines changed

.changeset/small-spiders-fail.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"svelte": patch
3+
---
4+
5+
breaking: apply fallback value every time in runes mode

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

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -255,10 +255,23 @@ export function client_component(source, analysis, options) {
255255

256256
const key = binding.prop_alias ?? name;
257257

258-
properties.push(
259-
b.get(key, [b.return(b.call(b.id(name)))]),
260-
b.set(key, [b.stmt(b.call(b.id(name), b.id('$$value'))), b.stmt(b.call('$.flushSync'))])
261-
);
258+
const getter = b.get(key, [b.return(b.call(b.id(name)))]);
259+
260+
const setter = b.set(key, [
261+
b.stmt(b.call(b.id(name), b.id('$$value'))),
262+
b.stmt(b.call('$.flushSync'))
263+
]);
264+
265+
if (analysis.runes && binding.initial) {
266+
// turn `set foo($$value)` into `set foo($$value = expression)`
267+
setter.value.params[0] = {
268+
type: 'AssignmentPattern',
269+
left: b.id('$$value'),
270+
right: /** @type {import('estree').Expression} */ (binding.initial)
271+
};
272+
}
273+
274+
properties.push(getter, setter);
262275
}
263276
}
264277

packages/svelte/src/compiler/utils/builders.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ export function function_declaration(id, params, body) {
210210
/**
211211
* @param {string} name
212212
* @param {import('estree').Statement[]} body
213-
* @returns {import('estree').Property}
213+
* @returns {import('estree').Property & { value: import('estree').FunctionExpression}}}
214214
*/
215215
export function get(name, body) {
216216
return prop('get', key(name), function_builder(null, [], block(body)));
@@ -305,11 +305,12 @@ export function object_pattern(properties) {
305305
}
306306

307307
/**
308+
* @template {import('estree').Expression} Value
308309
* @param {'init' | 'get' | 'set'} kind
309310
* @param {import('estree').Expression} key
310-
* @param {import('estree').Expression} value
311+
* @param {Value} value
311312
* @param {boolean} computed
312-
* @returns {import('estree').Property}
313+
* @returns {import('estree').Property & { value: Value }}
313314
*/
314315
export function prop(kind, key, value, computed = false) {
315316
return { type: 'Property', kind, key, value, method: false, shorthand: false, computed };
@@ -355,7 +356,7 @@ export function sequence(expressions) {
355356
/**
356357
* @param {string} name
357358
* @param {import('estree').Statement[]} body
358-
* @returns {import('estree').Property}
359+
* @returns {import('estree').Property & { value: import('estree').FunctionExpression}}
359360
*/
360361
export function set(name, body) {
361362
return prop('set', key(name), function_builder(null, [id('$$value')], block(body)));

packages/svelte/src/internal/client/reactivity/props.js

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
import { get_descriptor, is_function } from '../utils.js';
99
import { mutable_source, set } from './sources.js';
1010
import { derived } from './deriveds.js';
11-
import { get, inspect_fn, is_signals_recorded } from '../runtime.js';
11+
import { get, inspect_fn, is_signals_recorded, untrack } from '../runtime.js';
1212
import { safe_equals, safe_not_equal } from './equality.js';
1313

1414
/**
@@ -133,16 +133,30 @@ export function spread_props(...props) {
133133
* @param {Record<string, unknown>} props
134134
* @param {string} key
135135
* @param {number} flags
136-
* @param {V | (() => V)} [initial]
136+
* @param {V | (() => V)} [fallback]
137137
* @returns {(() => V | ((arg: V) => V) | ((arg: V, mutation: boolean) => V))}
138138
*/
139-
export function prop(props, key, flags, initial) {
139+
export function prop(props, key, flags, fallback) {
140140
var immutable = (flags & PROPS_IS_IMMUTABLE) !== 0;
141141
var runes = (flags & PROPS_IS_RUNES) !== 0;
142+
var lazy = (flags & PROPS_IS_LAZY_INITIAL) !== 0;
143+
142144
var prop_value = /** @type {V} */ (props[key]);
143145
var setter = get_descriptor(props, key)?.set;
144146

145-
if (prop_value === undefined && initial !== undefined) {
147+
var fallback_value = /** @type {V} */ (fallback);
148+
var fallback_dirty = true;
149+
150+
var get_fallback = () => {
151+
if (lazy && fallback_dirty) {
152+
fallback_dirty = false;
153+
fallback_value = untrack(/** @type {() => V} */ (fallback));
154+
}
155+
156+
return fallback_value;
157+
};
158+
159+
if (prop_value === undefined && fallback !== undefined) {
146160
if (setter && runes) {
147161
// TODO consolidate all these random runtime errors
148162
throw new Error(
@@ -153,19 +167,22 @@ export function prop(props, key, flags, initial) {
153167
);
154168
}
155169

156-
// @ts-expect-error would need a cumbersome method overload to type this
157-
if ((flags & PROPS_IS_LAZY_INITIAL) !== 0) initial = initial();
158-
159-
prop_value = /** @type {V} */ (initial);
160-
170+
prop_value = get_fallback();
161171
if (setter) setter(prop_value);
162172
}
163173

164-
var getter = () => {
165-
var value = /** @type {V} */ (props[key]);
166-
if (value !== undefined) initial = undefined;
167-
return value === undefined ? /** @type {V} */ (initial) : value;
168-
};
174+
var getter = runes
175+
? () => {
176+
var value = /** @type {V} */ (props[key]);
177+
if (value === undefined) return get_fallback();
178+
fallback_dirty = true;
179+
return value;
180+
}
181+
: () => {
182+
var value = /** @type {V} */ (props[key]);
183+
if (value !== undefined) fallback_value = /** @type {V} */ (undefined);
184+
return value === undefined ? fallback_value : value;
185+
};
169186

170187
// easy mode — prop is never written to
171188
if ((flags & PROPS_IS_UPDATED) === 0) {

packages/svelte/tests/runtime-runes/samples/props-default-value-behavior/_config.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { test } from '../../test';
22

3-
// Tests that default values apply only initially and that they propagate back correctly for bindings
3+
// Tests that default values apply every time and that they propagate back correctly for bindings
44
export default test({
55
// set accessors to false so that $.prop() is also created
66
accessors: false,
@@ -267,7 +267,7 @@ export default test({
267267
<p>props undefined:</p>
268268
<p>
269269
readonly:
270-
readonlyWithDefault:
270+
readonlyWithDefault: 1
271271
binding:
272272
</p>
273273
<button>set bindings to 5</button>
@@ -276,7 +276,7 @@ export default test({
276276
<p>props defined:</p>
277277
<p>
278278
readonly:
279-
readonlyWithDefault:
279+
readonlyWithDefault: 1
280280
binding:
281281
</p>
282282
<button>set bindings to 5</button>
@@ -285,7 +285,7 @@ export default test({
285285
<p>bindings undefined:</p>
286286
<p>
287287
readonly:
288-
readonlyWithDefault:
288+
readonlyWithDefault: 1
289289
binding:
290290
</p>
291291
<button>set bindings to 5</button>
@@ -294,7 +294,7 @@ export default test({
294294
<p>bindings defined:</p>
295295
<p>
296296
readonly:
297-
readonlyWithDefault:
297+
readonlyWithDefault: 1
298298
binding:
299299
</p>
300300
<button>set bindings to 5</button>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { test } from '../../test';
2+
3+
// Tests that default values only fire lazily when the prop is undefined, and every time
4+
export default test({
5+
props: {
6+
p0: 0,
7+
p1: 0,
8+
p2: 0,
9+
p3: 0
10+
},
11+
html: `<p>props: 0 0 0 0 1 1 1 1</p><p>log: nested.fallback_value,fallback_fn`,
12+
async test({ assert, target, component }) {
13+
component.p0 = undefined;
14+
component.p1 = undefined;
15+
component.p2 = undefined;
16+
component.p3 = undefined;
17+
// Nuance: these are already undefined in the props, but we're setting them to undefined again,
18+
// which calls the fallback value again, even if it will result in the same value. There's no way
19+
// to prevent this, and in practise it won't matter - and you shouldn't use accessors in runes mode anyway.
20+
component.p4 = undefined;
21+
component.p5 = undefined;
22+
component.p6 = undefined;
23+
component.p7 = undefined;
24+
assert.htmlEqual(
25+
target.innerHTML,
26+
`<p>props: 1 1 1 1 1 1 1 1</p><p>log: nested.fallback_value,fallback_fn,nested.fallback_value,fallback_fn,nested.fallback_value,fallback_fn`
27+
);
28+
}
29+
});
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<script>
2+
let log = $state([]);
3+
4+
const fallback_value = 1;
5+
const nested = {
6+
get fallback_value() {
7+
log.push('nested.fallback_value');
8+
return fallback_value;
9+
}
10+
}
11+
const fallback_fn = () => {
12+
log.push('fallback_fn');
13+
return fallback_value;
14+
}
15+
16+
const {
17+
p0 = 1,
18+
p1 = fallback_value,
19+
p2 = nested.fallback_value,
20+
p3 = fallback_fn(),
21+
p4 = 1,
22+
p5 = fallback_value,
23+
p6 = nested.fallback_value,
24+
p7 = fallback_fn()
25+
} = $props();
26+
</script>
27+
28+
<p>props: {p0} {p1} {p2} {p3} {p4} {p5} {p6} {p7}</p>
29+
<p>log: {log}</p>
Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,24 @@
1+
import { tick } from 'svelte';
12
import { test } from '../../test';
23

3-
// Tests that default values only fire lazily when the prop is undefined, and only initially
4+
// Tests that default values only fire lazily when the prop is undefined, and every time
45
export default test({
5-
props: {
6-
p0: 0,
7-
p1: 0,
8-
p2: 0,
9-
p3: 0
10-
},
11-
html: `<p>props: 0 0 0 0 1 1 1 1</p><p>log: nested.fallback_value,fallback_fn`,
12-
async test({ assert, target, component }) {
13-
component.p0 = undefined;
14-
component.p1 = undefined;
15-
component.p2 = undefined;
16-
component.p3 = undefined;
17-
component.p4 = undefined;
18-
component.p5 = undefined;
19-
component.p6 = undefined;
20-
component.p7 = undefined;
21-
assert.htmlEqual(target.innerHTML, `<p>props: </p><p>log: nested.fallback_value,fallback_fn`);
6+
html: `
7+
<p>props: 0 0 0 0 1 1 1 1</p>
8+
<p>log: nested.fallback_value,fallback_fn</p>
9+
<button>Set all to undefined</button>
10+
`,
11+
async test({ assert, target }) {
12+
const btn = target.querySelector('button');
13+
btn?.click();
14+
await tick();
15+
assert.htmlEqual(
16+
target.innerHTML,
17+
`
18+
<p>props: 1 1 1 1 1 1 1 1</p>
19+
<p>log: nested.fallback_value,fallback_fn,nested.fallback_value,fallback_fn</p>
20+
<button>Set all to undefined</button>
21+
`
22+
);
2223
}
2324
});
Lines changed: 21 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,25 @@
11
<script>
2-
let log = $state([]);
2+
import Sub from "./sub.svelte";
33
4-
const fallback_value = 1;
5-
const nested = {
6-
get fallback_value() {
7-
log.push('nested.fallback_value');
8-
log = log;
9-
return fallback_value;
10-
}
11-
}
12-
const fallback_fn = () => {
13-
log.push('fallback_fn');
14-
log = log;
15-
return fallback_value;
16-
}
17-
18-
const {
19-
p0 = 1,
20-
p1 = fallback_value,
21-
p2 = nested.fallback_value,
22-
p3 = fallback_fn(),
23-
p4 = 1,
24-
p5 = fallback_value,
25-
p6 = nested.fallback_value,
26-
p7 = fallback_fn()
27-
} = $props();
4+
let p0 = $state(0);
5+
let p1 = $state(0);
6+
let p2 = $state(0);
7+
let p3 = $state(0);
8+
let p4 = $state();
9+
let p5 = $state();
10+
let p6 = $state();
11+
let p7 = $state();
2812
</script>
2913

30-
<p>props: {p0} {p1} {p2} {p3} {p4} {p5} {p6} {p7}</p>
31-
<p>log: {log}</p>
14+
<Sub {p0} {p1} {p2} {p3} {p4} {p5} {p6} {p7} />
15+
16+
<button onclick={() => {
17+
p0 = undefined;
18+
p1 = undefined;
19+
p2 = undefined;
20+
p3 = undefined;
21+
p4 = undefined;
22+
p5 = undefined;
23+
p6 = undefined;
24+
p7 = undefined;
25+
}}>Set all to undefined</button>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<script>
2+
let log = $state([]);
3+
4+
const fallback_value = 1;
5+
const nested = {
6+
get fallback_value() {
7+
log.push('nested.fallback_value');
8+
return fallback_value;
9+
}
10+
}
11+
const fallback_fn = () => {
12+
log.push('fallback_fn');
13+
return fallback_value;
14+
}
15+
16+
const {
17+
p0 = 1,
18+
p1 = fallback_value,
19+
p2 = nested.fallback_value,
20+
p3 = fallback_fn(),
21+
p4 = 1,
22+
p5 = fallback_value,
23+
p6 = nested.fallback_value,
24+
p7 = fallback_fn()
25+
} = $props();
26+
</script>
27+
28+
<p>props: {p0} {p1} {p2} {p3} {p4} {p5} {p6} {p7}</p>
29+
<p>log: {log}</p>

packages/svelte/tests/runtime-runes/samples/props-spread-fallback/_config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export default test({
4848
`
4949
<button>change propA</button>
5050
<button>change propB</button>
51-
<p>true</p>
51+
<p>true fallback</p>
5252
`
5353
);
5454
}

0 commit comments

Comments
 (0)