Skip to content

Commit 2754e4e

Browse files
authored
fix: handle reassignment of $$props and $$restProps (#11348)
* fix: handle reassignment of `$$props` and `$$restProps` Some libraries assign to properties of `$$props` and `$$restProps`. These were previously resulting in an error but are now handled properly #10359 (comment) * $$props is coarse grained on updates, so we can simplify this * fix * fix comment
1 parent 5e0845f commit 2754e4e

File tree

12 files changed

+177
-12
lines changed

12 files changed

+177
-12
lines changed

.changeset/lucky-teachers-exist.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+
fix: handle reassignment of `$$props` and `$$restProps`

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -449,7 +449,7 @@ export function analyze_component(root, source, options) {
449449
);
450450
}
451451
} else {
452-
instance.scope.declare(b.id('$$props'), 'bindable_prop', 'synthetic');
452+
instance.scope.declare(b.id('$$props'), 'rest_prop', 'synthetic');
453453
instance.scope.declare(b.id('$$restProps'), 'rest_prop', 'synthetic');
454454

455455
for (const { ast, scope, scopes } of [module, instance, template]) {

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -418,7 +418,7 @@ export function client_component(source, analysis, options) {
418418
b.const(
419419
'$$restProps',
420420
b.call(
421-
'$.rest_props',
421+
'$.legacy_rest_props',
422422
b.id('$$sanitized_props'),
423423
b.array(named_props.map((name) => b.literal(name)))
424424
)
@@ -431,8 +431,12 @@ export function client_component(source, analysis, options) {
431431
if (analysis.custom_element) {
432432
to_remove.push(b.literal('$$host'));
433433
}
434+
434435
component_block.body.unshift(
435-
b.const('$$sanitized_props', b.call('$.rest_props', b.id('$$props'), b.array(to_remove)))
436+
b.const(
437+
'$$sanitized_props',
438+
b.call('$.legacy_rest_props', b.id('$$props'), b.array(to_remove))
439+
)
436440
);
437441
}
438442

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

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,11 @@ export function serialize_get_binding(node, state) {
7474
return node;
7575
}
7676

77+
if (binding.node.name === '$$props') {
78+
// Special case for $$props which only exists in the old world
79+
return b.id('$$sanitized_props');
80+
}
81+
7782
if (binding.kind === 'store_sub') {
7883
return b.call(node);
7984
}
@@ -83,12 +88,6 @@ export function serialize_get_binding(node, state) {
8388
}
8489

8590
if (binding.kind === 'prop' || binding.kind === 'bindable_prop') {
86-
if (binding.node.name === '$$props') {
87-
// Special case for $$props which only exists in the old world
88-
// TODO this probably shouldn't have a 'prop' binding kind
89-
return node;
90-
}
91-
9291
if (
9392
state.analysis.accessors ||
9493
(state.analysis.immutable ? binding.reassigned : binding.mutated) ||

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ export { mutable_source, mutate, source, set } from './reactivity/sources.js';
8888
export {
8989
prop,
9090
rest_props,
91+
legacy_rest_props,
9192
spread_props,
9293
update_pre_prop,
9394
update_prop

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

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import {
66
PROPS_IS_UPDATED
77
} from '../../../constants.js';
88
import { get_descriptor, is_function } from '../utils.js';
9-
import { mutable_source, set } from './sources.js';
9+
import { mutable_source, set, source } from './sources.js';
1010
import { derived } from './deriveds.js';
11-
import { get, is_signals_recorded, untrack } from '../runtime.js';
11+
import { get, is_signals_recorded, untrack, update } from '../runtime.js';
1212
import { safe_equals } from './equality.js';
1313
import { inspect_fn } from '../dev/inspect.js';
1414
import * as e from '../errors.js';
@@ -79,7 +79,67 @@ const rest_props_handler = {
7979
* @returns {Record<string, unknown>}
8080
*/
8181
export function rest_props(props, exclude, name) {
82-
return new Proxy(DEV ? { props, exclude, name } : { props, exclude }, rest_props_handler);
82+
return new Proxy(
83+
DEV ? { props, exclude, name, other: {}, to_proxy: [] } : { props, exclude },
84+
rest_props_handler
85+
);
86+
}
87+
88+
/**
89+
* The proxy handler for legacy $$restProps and $$props
90+
* @type {ProxyHandler<{ props: Record<string | symbol, unknown>, exclude: Array<string | symbol>, special: Record<string | symbol, (v?: unknown) => unknown>, version: import('./types.js').Source<number> }>}}
91+
*/
92+
const legacy_rest_props_handler = {
93+
get(target, key) {
94+
if (target.exclude.includes(key)) return;
95+
get(target.version);
96+
return key in target.special ? target.special[key]() : target.props[key];
97+
},
98+
set(target, key, value) {
99+
if (!(key in target.special)) {
100+
// Handle props that can temporarily get out of sync with the parent
101+
/** @type {Record<string, (v?: unknown) => unknown>} */
102+
target.special[key] = prop(
103+
{
104+
get [key]() {
105+
return target.props[key];
106+
}
107+
},
108+
/** @type {string} */ (key),
109+
PROPS_IS_UPDATED
110+
);
111+
}
112+
113+
target.special[key](value);
114+
update(target.version); // $$props is coarse-grained: when $$props.x is updated, usages of $$props.y etc are also rerun
115+
return true;
116+
},
117+
getOwnPropertyDescriptor(target, key) {
118+
if (target.exclude.includes(key)) return;
119+
if (key in target.props) {
120+
return {
121+
enumerable: true,
122+
configurable: true,
123+
value: target.props[key]
124+
};
125+
}
126+
},
127+
has(target, key) {
128+
if (target.exclude.includes(key)) return false;
129+
return key in target.props;
130+
},
131+
ownKeys(target) {
132+
return Reflect.ownKeys(target.props).filter((key) => !target.exclude.includes(key));
133+
}
134+
};
135+
136+
/**
137+
* @param {Record<string, unknown>} props
138+
* @param {string[]} exclude
139+
* @returns {Record<string, unknown>}
140+
*/
141+
export function legacy_rest_props(props, exclude) {
142+
return new Proxy({ props, exclude, special: {}, version: source(0) }, legacy_rest_props_handler);
83143
}
84144

85145
/**
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<script>
2+
$: $$props.a = $$props.a * 2;
3+
</script>
4+
5+
<p>{$$props.a} {$$props.b}</p>
6+
<button on:click={() => $$props.b = 'b'}>update</button>
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
html: `
5+
<button>increment</button>
6+
<p>0 </p>
7+
<button>update</button>
8+
`,
9+
10+
async test({ assert, target }) {
11+
const [btn1, btn2] = target.querySelectorAll('button');
12+
13+
await btn1.click();
14+
15+
assert.htmlEqual(
16+
target.innerHTML,
17+
`
18+
<button>increment</button>
19+
<p>2 </p>
20+
<button>update</button>
21+
`
22+
);
23+
24+
await btn2.click();
25+
26+
assert.htmlEqual(
27+
target.innerHTML,
28+
`
29+
<button>increment</button>
30+
<p>4 b</p>
31+
<button>update</button>
32+
`
33+
);
34+
}
35+
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<script>
2+
import App from './App.svelte';
3+
let a = 0;
4+
</script>
5+
6+
<button on:click={() => a++}>increment</button>
7+
<App {a} />
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<script>
2+
$: $$restProps.c = $$restProps.c ?? 'c';
3+
</script>
4+
5+
<p>{$$restProps.a} {$$restProps.b} {$$restProps.c}</p>
6+
<button on:click={() => $$restProps.b = 'b'}>update</button>
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
html: `
5+
<button>increment</button>
6+
<p>0 c</p>
7+
<button>update</button>
8+
`,
9+
10+
async test({ assert, target }) {
11+
const [btn1, btn2] = target.querySelectorAll('button');
12+
13+
await btn1.click();
14+
15+
assert.htmlEqual(
16+
target.innerHTML,
17+
`
18+
<button>increment</button>
19+
<p>1 c</p>
20+
<button>update</button>
21+
`
22+
);
23+
24+
await btn2.click();
25+
26+
assert.htmlEqual(
27+
target.innerHTML,
28+
`
29+
<button>increment</button>
30+
<p>1 b c</p>
31+
<button>update</button>
32+
`
33+
);
34+
}
35+
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<script>
2+
import App from './App.svelte';
3+
let a = 0;
4+
</script>
5+
6+
<button on:click={() => a++}>increment</button>
7+
<App {a} />

0 commit comments

Comments
 (0)