Skip to content

Commit 48adf81

Browse files
committed
fix: add test and change strategy
1 parent a73a619 commit 48adf81

File tree

9 files changed

+194
-79
lines changed

9 files changed

+194
-79
lines changed

packages/svelte/src/compiler/phases/3-transform/client/types.d.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@ export interface ClientTransformState extends TransformState {
2323
* us to rewrite `this.foo` as `this.#foo.value`
2424
*/
2525
readonly in_constructor: boolean;
26-
readonly safe_props_ids?: Map<string, Expression>;
27-
readonly safe_props_name?: string;
2826

2927
readonly transform: Record<
3028
string,
@@ -47,6 +45,7 @@ export interface ComponentClientTransformState extends ClientTransformState {
4745
readonly hoisted: Array<Statement | ModuleDeclaration>;
4846
readonly events: Set<string>;
4947
readonly is_instance: boolean;
48+
readonly needs_safe_props: boolean;
5049
readonly store_to_invalidate?: string;
5150

5251
/** Stuff that happens before the render effect(s) */

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

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,6 @@ import { build_component } from './shared/component.js';
1010
*/
1111
export function Component(node, context) {
1212
if (node.metadata.dynamic) {
13-
let safe_props_ids = new Map();
14-
15-
const safe_props_name = context.state.scope.generate('$$safe_props');
16-
1713
// Handle dynamic references to what seems like static inline components
1814
const component = build_component(
1915
node,
@@ -22,8 +18,7 @@ export function Component(node, context) {
2218
...context,
2319
state: {
2420
...context.state,
25-
safe_props_ids,
26-
safe_props_name
21+
needs_safe_props: true
2722
}
2823
},
2924
b.id('$$anchor')
@@ -36,19 +31,7 @@ export function Component(node, context) {
3631
// TODO use untrack here to not update when binding changes?
3732
// Would align with Svelte 4 behavior, but it's arguably nicer/expected to update this
3833
b.thunk(/** @type {Expression} */ (context.visit(b.member_id(node.name)))),
39-
b.arrow(
40-
[b.id('$$anchor'), b.id('$$component')],
41-
b.block([
42-
b.const(
43-
safe_props_name,
44-
b.call(
45-
'$.safe_props',
46-
b.object([...safe_props_ids].map(([name, id]) => b.get(name, [b.return(id)])))
47-
)
48-
),
49-
component
50-
])
51-
)
34+
b.arrow([b.id('$$anchor'), b.id('$$component')], b.block([component]))
5235
)
5336
)
5437
);

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

Lines changed: 3 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ import { build_getter } from '../utils.js';
99
* @param {Context} context
1010
*/
1111
export function Identifier(node, context) {
12-
let parent = context.path.at(-1);
12+
const parent = /** @type {Node} */ (context.path.at(-1));
1313

14-
if (is_reference(node, /** @type {Node} */ (parent))) {
14+
if (is_reference(node, parent)) {
1515
if (node.name === '$$props') {
1616
return b.id('$$sanitized_props');
1717
}
@@ -36,36 +36,6 @@ export function Identifier(node, context) {
3636
}
3737
}
3838

39-
const getter = build_getter(node, context.state);
40-
41-
if (
42-
// this means we are inside an if or as an attribute of a dynamic component
43-
// and we want to access `$$safe_props` to allow for the component to access them
44-
// after destructuring
45-
context.state.safe_props_name != null &&
46-
context.state.safe_props_ids != null &&
47-
// the parent can either be a component/svelte component in that case we
48-
// check if this identifier is one of the attributes
49-
(((parent?.type === 'Component' || parent?.type === 'SvelteComponent') &&
50-
parent.attributes.some(
51-
(el) =>
52-
(el.type === 'Attribute' &&
53-
typeof el.value !== 'boolean' &&
54-
!Array.isArray(el.value) &&
55-
el.value.expression === node) ||
56-
(el.type === 'BindDirective' && el.expression === node)
57-
)) ||
58-
// or a spread and we check the expression
59-
(parent?.type === 'SpreadAttribute' && parent.expression === node)) &&
60-
// we also don't want to transform bindings that are defined withing the if block
61-
// itself (for example an each local variable)
62-
!binding?.references[0].path.some((node) => node.type === 'IfBlock')
63-
) {
64-
// we store the getter in the safe props id and return an access to `$$safe_props.name`
65-
context.state.safe_props_ids.set(node.name, getter);
66-
return b.member(b.id(context.state.safe_props_name), b.id(node.name));
67-
}
68-
69-
return getter;
39+
return build_getter(node, context.state);
7040
}
7141
}

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

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,37 +11,26 @@ export function IfBlock(node, context) {
1111
context.state.template.push('<!>');
1212
const statements = [];
1313

14-
let safe_props_ids = new Map();
15-
16-
const safe_props_id = context.state.scope.generate('$$safe_props');
17-
1814
const consequent = /** @type {BlockStatement} */ (
1915
context.visit(node.consequent, {
2016
...context.state,
21-
safe_props_ids,
22-
safe_props_name: safe_props_id
17+
needs_safe_props: true
2318
})
2419
);
2520

26-
if (consequent.body.length > 0 && safe_props_ids) {
27-
consequent.body.unshift(
28-
b.const(
29-
safe_props_id,
30-
b.call(
31-
'$.safe_props',
32-
b.object([...safe_props_ids].map(([name, id]) => b.get(name, [b.return(id)])))
33-
)
34-
)
35-
);
36-
}
3721
const consequent_id = context.state.scope.generate('consequent');
3822

3923
statements.push(b.var(b.id(consequent_id), b.arrow([b.id('$$anchor')], consequent)));
4024

4125
let alternate_id;
4226

4327
if (node.alternate) {
44-
const alternate = /** @type {BlockStatement} */ (context.visit(node.alternate));
28+
const alternate = /** @type {BlockStatement} */ (
29+
context.visit(node.alternate, {
30+
...context.state,
31+
needs_safe_props: true
32+
})
33+
);
4534
alternate_id = context.state.scope.generate('alternate');
4635
statements.push(b.var(b.id(alternate_id), b.arrow([b.id('$$anchor')], alternate)));
4736
}

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

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,21 @@ export function build_component(node, component_name, context, anchor = context.
9292
}
9393
}
9494

95+
let safe_props_ids = new Map();
96+
let safe_props_name = context.state.scope.generate('$$safe_props');
97+
98+
/**
99+
* @param {string} name
100+
* @param {Expression} expression
101+
*/
102+
function safe_propify(name, expression) {
103+
if (context.state.needs_safe_props) {
104+
safe_props_ids.set(name, expression);
105+
return b.member(b.id(safe_props_name), b.id(name));
106+
}
107+
return expression;
108+
}
109+
95110
for (const attribute of node.attributes) {
96111
if (attribute.type === 'LetDirective') {
97112
if (!slot_scope_applies_to_itself) {
@@ -118,13 +133,14 @@ export function build_component(node, component_name, context, anchor = context.
118133
if (attribute.metadata.expression.has_state) {
119134
let value = expression;
120135

136+
const name = context.state.scope.generate('spread_element');
121137
if (attribute.metadata.expression.has_call) {
122-
const id = b.id(context.state.scope.generate('spread_element'));
138+
const id = b.id(name);
123139
context.state.init.push(b.var(id, b.call('$.derived', b.thunk(value))));
124140
value = b.call('$.get', id);
125141
}
126142

127-
props_and_spreads.push(b.thunk(value));
143+
props_and_spreads.push(b.thunk(safe_propify(name, value)));
128144
} else {
129145
props_and_spreads.push(expression);
130146
}
@@ -172,7 +188,7 @@ export function build_component(node, component_name, context, anchor = context.
172188
);
173189

174190
if (has_state) {
175-
push_prop(b.get(attribute.name, [b.return(value)]));
191+
push_prop(b.get(attribute.name, [b.return(safe_propify(attribute.name, value))]));
176192
} else {
177193
push_prop(b.init(attribute.name, value));
178194
}
@@ -205,7 +221,9 @@ export function build_component(node, component_name, context, anchor = context.
205221
context.state.init.push(b.var(get_id, get));
206222
context.state.init.push(b.var(set_id, set));
207223

208-
push_prop(b.get(attribute.name, [b.return(b.call(get_id))]));
224+
push_prop(
225+
b.get(attribute.name, [b.return(safe_propify(attribute.name, b.call(get_id)))])
226+
);
209227
push_prop(b.set(attribute.name, [b.stmt(b.call(set_id, b.id('$$value')))]));
210228
}
211229
} else {
@@ -228,11 +246,17 @@ export function build_component(node, component_name, context, anchor = context.
228246
// Delay prop pushes so bindings come at the end, to avoid spreads overwriting them
229247
if (is_store_sub) {
230248
push_prop(
231-
b.get(attribute.name, [b.stmt(b.call('$.mark_store_binding')), b.return(expression)]),
249+
b.get(attribute.name, [
250+
b.stmt(b.call('$.mark_store_binding')),
251+
b.return(safe_propify(attribute.name, expression))
252+
]),
232253
true
233254
);
234255
} else {
235-
push_prop(b.get(attribute.name, [b.return(expression)]), true);
256+
push_prop(
257+
b.get(attribute.name, [b.return(safe_propify(attribute.name, expression))]),
258+
true
259+
);
236260
}
237261

238262
const assignment = b.assignment(
@@ -403,6 +427,32 @@ export function build_component(node, component_name, context, anchor = context.
403427

404428
const statements = [...snippet_declarations];
405429

430+
if (safe_props_ids.size > 0) {
431+
// if it is a dynamic component we need to include the safe props call inside the component
432+
// function otherwise in the init (which in case of the if will be in the consequent/alternate function)
433+
if (component_name === '$$component') {
434+
statements.push(
435+
b.const(
436+
safe_props_name,
437+
b.call(
438+
'$.safe_props',
439+
b.object([...safe_props_ids].map(([name, id]) => b.get(name, [b.return(id)])))
440+
)
441+
)
442+
);
443+
} else {
444+
context.state.init.push(
445+
b.const(
446+
safe_props_name,
447+
b.call(
448+
'$.safe_props',
449+
b.object([...safe_props_ids].map(([name, id]) => b.get(name, [b.return(id)])))
450+
)
451+
)
452+
);
453+
}
454+
}
455+
406456
if (node.type === 'SvelteComponent') {
407457
const prev = fn;
408458

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ export function build_attribute_value(value, context, memoize = (value) => value
185185
return { value: b.literal(chunk.data), has_state: false };
186186
}
187187

188-
let expression = /** @type {Expression} */ (context.visit(chunk.expression, context.state));
188+
let expression = /** @type {Expression} */ (context.visit(chunk.expression));
189189

190190
return {
191191
value: memoize(expression, chunk.metadata.expression),
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<script>
2+
let { checked = $bindable(), count = $bindable() } = $props();
3+
4+
$effect(() => ()=>{
5+
console.log(count, checked);
6+
});
7+
</script>
8+
9+
<p>{count}</p>
10+
11+
<button onclick={()=> count-- }></button>
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { ok, test } from '../../test';
2+
import { flushSync } from 'svelte';
3+
4+
export default test({
5+
async test({ assert, target, logs }) {
6+
const [btn1, btn2, btn3] = target.querySelectorAll('button');
7+
let ps = [...target.querySelectorAll('p')];
8+
9+
for (const p of ps) {
10+
assert.equal(p.innerHTML, '0');
11+
}
12+
13+
flushSync(() => {
14+
btn1.click();
15+
});
16+
17+
// prop update normally if we are not unmounting
18+
for (const p of ps) {
19+
assert.equal(p.innerHTML, '1');
20+
}
21+
22+
flushSync(() => {
23+
btn3.click();
24+
});
25+
26+
// binding still works and update the value correctly
27+
for (const p of ps) {
28+
assert.equal(p.innerHTML, '0');
29+
}
30+
31+
flushSync(() => {
32+
btn1.click();
33+
});
34+
35+
flushSync(() => {
36+
btn1.click();
37+
});
38+
39+
console.warn(logs);
40+
41+
// the five components guarded by `count < 2` unmount and log
42+
assert.deepEqual(logs, [1, true, 1, true, 1, true, 1, true, 1, true]);
43+
44+
flushSync(() => {
45+
btn2.click();
46+
});
47+
48+
// the three components guarded by `show` unmount and log
49+
assert.deepEqual(logs, [
50+
1,
51+
true,
52+
1,
53+
true,
54+
1,
55+
true,
56+
1,
57+
true,
58+
1,
59+
true,
60+
2,
61+
true,
62+
2,
63+
true,
64+
2,
65+
true
66+
]);
67+
}
68+
});

0 commit comments

Comments
 (0)