Skip to content

Commit 33e44ea

Browse files
authored
feat: allow let props = $props(), optimize prop read access (#12201)
- allow to write `let props = $props()` - optimize read access of props.x to use `$$props` argument directly; closes #11055
1 parent d959d4a commit 33e44ea

File tree

10 files changed

+174
-75
lines changed

10 files changed

+174
-75
lines changed

.changeset/six-gorillas-obey.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
feat: allow `let props = $props()` and optimize prop read access

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

+37-28
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { hash } from './utils.js';
3131
import { warn_unused } from './css/css-warn.js';
3232
import { extract_svelte_ignore } from '../../utils/extract_svelte_ignore.js';
3333
import { ignore_map, ignore_stack, pop_ignore, push_ignore } from '../../state.js';
34+
import { equal } from '../../utils/assert.js';
3435

3536
/**
3637
* @param {import('#compiler').Script | null} script
@@ -969,34 +970,42 @@ const runes_scope_tweaker = {
969970
if (rune === '$props') {
970971
state.analysis.needs_props = true;
971972

972-
for (const property of /** @type {import('estree').ObjectPattern} */ (node.id).properties) {
973-
if (property.type !== 'Property') continue;
974-
975-
const name =
976-
property.value.type === 'AssignmentPattern'
977-
? /** @type {import('estree').Identifier} */ (property.value.left).name
978-
: /** @type {import('estree').Identifier} */ (property.value).name;
979-
const alias =
980-
property.key.type === 'Identifier'
981-
? property.key.name
982-
: String(/** @type {import('estree').Literal} */ (property.key).value);
983-
let initial = property.value.type === 'AssignmentPattern' ? property.value.right : null;
984-
985-
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(name));
986-
binding.prop_alias = alias;
987-
988-
// rewire initial from $props() to the actual initial value, stripping $bindable() if necessary
989-
if (
990-
initial?.type === 'CallExpression' &&
991-
initial.callee.type === 'Identifier' &&
992-
initial.callee.name === '$bindable'
993-
) {
994-
binding.initial = /** @type {import('estree').Expression | null} */ (
995-
initial.arguments[0] ?? null
996-
);
997-
binding.kind = 'bindable_prop';
998-
} else {
999-
binding.initial = initial;
973+
if (node.id.type === 'Identifier') {
974+
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(node.id.name));
975+
binding.initial = null; // else would be $props()
976+
binding.kind = 'rest_prop';
977+
} else {
978+
equal(node.id.type, 'ObjectPattern');
979+
980+
for (const property of node.id.properties) {
981+
if (property.type !== 'Property') continue;
982+
983+
const name =
984+
property.value.type === 'AssignmentPattern'
985+
? /** @type {import('estree').Identifier} */ (property.value.left).name
986+
: /** @type {import('estree').Identifier} */ (property.value).name;
987+
const alias =
988+
property.key.type === 'Identifier'
989+
? property.key.name
990+
: String(/** @type {import('estree').Literal} */ (property.key).value);
991+
let initial = property.value.type === 'AssignmentPattern' ? property.value.right : null;
992+
993+
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(name));
994+
binding.prop_alias = alias;
995+
996+
// rewire initial from $props() to the actual initial value, stripping $bindable() if necessary
997+
if (
998+
initial?.type === 'CallExpression' &&
999+
initial.callee.type === 'Identifier' &&
1000+
initial.callee.name === '$bindable'
1001+
) {
1002+
binding.initial = /** @type {import('estree').Expression | null} */ (
1003+
initial.arguments[0] ?? null
1004+
);
1005+
binding.kind = 'bindable_prop';
1006+
} else {
1007+
binding.initial = initial;
1008+
}
10001009
}
10011010
}
10021011
}

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

+12-10
Original file line numberDiff line numberDiff line change
@@ -1240,25 +1240,27 @@ export const validation_runes = merge(validation, a11y_validators, {
12401240
e.rune_invalid_arguments(node, rune);
12411241
}
12421242

1243-
if (node.id.type !== 'ObjectPattern') {
1243+
if (node.id.type !== 'ObjectPattern' && node.id.type !== 'Identifier') {
12441244
e.props_invalid_identifier(node);
12451245
}
12461246

12471247
if (state.scope !== state.analysis.instance.scope) {
12481248
e.props_invalid_placement(node);
12491249
}
12501250

1251-
for (const property of node.id.properties) {
1252-
if (property.type === 'Property') {
1253-
if (property.computed) {
1254-
e.props_invalid_pattern(property);
1255-
}
1251+
if (node.id.type === 'ObjectPattern') {
1252+
for (const property of node.id.properties) {
1253+
if (property.type === 'Property') {
1254+
if (property.computed) {
1255+
e.props_invalid_pattern(property);
1256+
}
12561257

1257-
const value =
1258-
property.value.type === 'AssignmentPattern' ? property.value.left : property.value;
1258+
const value =
1259+
property.value.type === 'AssignmentPattern' ? property.value.left : property.value;
12591260

1260-
if (value.type !== 'Identifier') {
1261-
e.props_invalid_pattern(property);
1261+
if (value.type !== 'Identifier') {
1262+
e.props_invalid_pattern(property);
1263+
}
12621264
}
12631265
}
12641266
}

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

+21
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,27 @@ export const global_visitors = {
99
if (node.name === '$$props') {
1010
return b.id('$$sanitized_props');
1111
}
12+
13+
// Optimize prop access: If it's a member read access, we can use the $$props object directly
14+
const binding = state.scope.get(node.name);
15+
if (
16+
state.analysis.runes && // can't do this in legacy mode because the proxy does more than just read/write
17+
binding !== null &&
18+
node !== binding.node &&
19+
binding.kind === 'rest_prop'
20+
) {
21+
const parent = path.at(-1);
22+
const grand_parent = path.at(-2);
23+
if (
24+
parent?.type === 'MemberExpression' &&
25+
!parent.computed &&
26+
grand_parent?.type !== 'AssignmentExpression' &&
27+
grand_parent?.type !== 'UpdateExpression'
28+
) {
29+
return b.id('$$props');
30+
}
31+
}
32+
1233
return serialize_get_binding(node, state);
1334
}
1435
},

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

+49-37
Original file line numberDiff line numberDiff line change
@@ -238,53 +238,65 @@ export const javascript_visitors_runes = {
238238
}
239239

240240
if (rune === '$props') {
241-
assert.equal(declarator.id.type, 'ObjectPattern');
242-
243241
/** @type {string[]} */
244242
const seen = ['$$slots', '$$events', '$$legacy'];
245243

246244
if (state.analysis.custom_element) {
247245
seen.push('$$host');
248246
}
249247

250-
for (const property of declarator.id.properties) {
251-
if (property.type === 'Property') {
252-
const key = /** @type {import('estree').Identifier | import('estree').Literal} */ (
253-
property.key
254-
);
255-
const name = key.type === 'Identifier' ? key.name : /** @type {string} */ (key.value);
256-
257-
seen.push(name);
258-
259-
let id =
260-
property.value.type === 'AssignmentPattern' ? property.value.left : property.value;
261-
assert.equal(id.type, 'Identifier');
262-
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(id.name));
263-
let initial =
264-
binding.initial &&
265-
/** @type {import('estree').Expression} */ (visit(binding.initial));
266-
// We're adding proxy here on demand and not within the prop runtime function so that
267-
// people not using proxied state anywhere in their code don't have to pay the additional bundle size cost
268-
if (initial && binding.mutated && should_proxy_or_freeze(initial, state.scope)) {
269-
initial = b.call('$.proxy', initial);
270-
}
248+
if (declarator.id.type === 'Identifier') {
249+
/** @type {import('estree').Expression[]} */
250+
const args = [b.id('$$props'), b.array(seen.map((name) => b.literal(name)))];
271251

272-
if (is_prop_source(binding, state)) {
273-
declarations.push(b.declarator(id, get_prop_source(binding, state, name, initial)));
274-
}
275-
} else {
276-
// RestElement
277-
/** @type {import('estree').Expression[]} */
278-
const args = [b.id('$$props'), b.array(seen.map((name) => b.literal(name)))];
279-
280-
if (state.options.dev) {
281-
// include rest name, so we can provide informative error messages
282-
args.push(
283-
b.literal(/** @type {import('estree').Identifier} */ (property.argument).name)
252+
if (state.options.dev) {
253+
// include rest name, so we can provide informative error messages
254+
args.push(b.literal(declarator.id.name));
255+
}
256+
257+
declarations.push(b.declarator(declarator.id, b.call('$.rest_props', ...args)));
258+
} else {
259+
assert.equal(declarator.id.type, 'ObjectPattern');
260+
261+
for (const property of declarator.id.properties) {
262+
if (property.type === 'Property') {
263+
const key = /** @type {import('estree').Identifier | import('estree').Literal} */ (
264+
property.key
284265
);
266+
const name = key.type === 'Identifier' ? key.name : /** @type {string} */ (key.value);
267+
268+
seen.push(name);
269+
270+
let id =
271+
property.value.type === 'AssignmentPattern' ? property.value.left : property.value;
272+
assert.equal(id.type, 'Identifier');
273+
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(id.name));
274+
let initial =
275+
binding.initial &&
276+
/** @type {import('estree').Expression} */ (visit(binding.initial));
277+
// We're adding proxy here on demand and not within the prop runtime function so that
278+
// people not using proxied state anywhere in their code don't have to pay the additional bundle size cost
279+
if (initial && binding.mutated && should_proxy_or_freeze(initial, state.scope)) {
280+
initial = b.call('$.proxy', initial);
281+
}
282+
283+
if (is_prop_source(binding, state)) {
284+
declarations.push(b.declarator(id, get_prop_source(binding, state, name, initial)));
285+
}
286+
} else {
287+
// RestElement
288+
/** @type {import('estree').Expression[]} */
289+
const args = [b.id('$$props'), b.array(seen.map((name) => b.literal(name)))];
290+
291+
if (state.options.dev) {
292+
// include rest name, so we can provide informative error messages
293+
args.push(
294+
b.literal(/** @type {import('estree').Identifier} */ (property.argument).name)
295+
);
296+
}
297+
298+
declarations.push(b.declarator(property.argument, b.call('$.rest_props', ...args)));
285299
}
286-
287-
declarations.push(b.declarator(property.argument, b.call('$.rest_props', ...args)));
288300
}
289301
}
290302

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

+1
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ const rest_props_handler = {
7878
* @param {string} [name]
7979
* @returns {Record<string, unknown>}
8080
*/
81+
/*#__NO_SIDE_EFFECTS__*/
8182
export function rest_props(props, exclude, name) {
8283
return new Proxy(
8384
DEV ? { props, exclude, name, other: {}, to_proxy: [] } : { props, exclude },
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import "svelte/internal/disclose-version";
2+
import * as $ from "svelte/internal/client";
3+
4+
export default function Props_identifier($$anchor, $$props) {
5+
$.push($$props, true);
6+
7+
let props = $.rest_props($$props, ["$$slots", "$$events", "$$legacy"]);
8+
9+
$$props.a;
10+
props[a];
11+
$$props.a.b;
12+
$$props.a.b = true;
13+
props.a = true;
14+
props[a] = true;
15+
props;
16+
$.pop();
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import * as $ from "svelte/internal/server";
2+
3+
export default function Props_identifier($$payload, $$props) {
4+
$.push();
5+
6+
let props = $$props;
7+
8+
props.a;
9+
props[a];
10+
props.a.b;
11+
props.a.b = true;
12+
props.a = true;
13+
props[a] = true;
14+
props;
15+
$.pop();
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<script>
2+
let props = $props();
3+
props.a;
4+
props[a];
5+
props.a.b;
6+
props.a.b = true;
7+
props.a = true;
8+
props[a] = true;
9+
props;
10+
</script>

sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md

+6
Original file line numberDiff line numberDiff line change
@@ -548,6 +548,12 @@ To get all properties, use rest syntax:
548548
let { a, b, c, ...everythingElse } = $props();
549549
```
550550

551+
You can also use an identifier:
552+
553+
```js
554+
let props = $props();
555+
```
556+
551557
If you're using TypeScript, you can declare the prop types:
552558

553559
```ts

0 commit comments

Comments
 (0)