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
pull/12219/head
Simon H 3 months ago committed by GitHub
parent d959d4afbe
commit 33e44ea697
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: allow `let props = $props()` and optimize prop read access

@ -31,6 +31,7 @@ import { hash } from './utils.js';
import { warn_unused } from './css/css-warn.js'; import { warn_unused } from './css/css-warn.js';
import { extract_svelte_ignore } from '../../utils/extract_svelte_ignore.js'; import { extract_svelte_ignore } from '../../utils/extract_svelte_ignore.js';
import { ignore_map, ignore_stack, pop_ignore, push_ignore } from '../../state.js'; import { ignore_map, ignore_stack, pop_ignore, push_ignore } from '../../state.js';
import { equal } from '../../utils/assert.js';
/** /**
* @param {import('#compiler').Script | null} script * @param {import('#compiler').Script | null} script
@ -969,34 +970,42 @@ const runes_scope_tweaker = {
if (rune === '$props') { if (rune === '$props') {
state.analysis.needs_props = true; state.analysis.needs_props = true;
for (const property of /** @type {import('estree').ObjectPattern} */ (node.id).properties) { if (node.id.type === 'Identifier') {
if (property.type !== 'Property') continue; const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(node.id.name));
binding.initial = null; // else would be $props()
const name = binding.kind = 'rest_prop';
property.value.type === 'AssignmentPattern' } else {
? /** @type {import('estree').Identifier} */ (property.value.left).name equal(node.id.type, 'ObjectPattern');
: /** @type {import('estree').Identifier} */ (property.value).name;
const alias = for (const property of node.id.properties) {
property.key.type === 'Identifier' if (property.type !== 'Property') continue;
? property.key.name
: String(/** @type {import('estree').Literal} */ (property.key).value); const name =
let initial = property.value.type === 'AssignmentPattern' ? property.value.right : null; property.value.type === 'AssignmentPattern'
? /** @type {import('estree').Identifier} */ (property.value.left).name
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(name)); : /** @type {import('estree').Identifier} */ (property.value).name;
binding.prop_alias = alias; const alias =
property.key.type === 'Identifier'
// rewire initial from $props() to the actual initial value, stripping $bindable() if necessary ? property.key.name
if ( : String(/** @type {import('estree').Literal} */ (property.key).value);
initial?.type === 'CallExpression' && let initial = property.value.type === 'AssignmentPattern' ? property.value.right : null;
initial.callee.type === 'Identifier' &&
initial.callee.name === '$bindable' const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(name));
) { binding.prop_alias = alias;
binding.initial = /** @type {import('estree').Expression | null} */ (
initial.arguments[0] ?? null // rewire initial from $props() to the actual initial value, stripping $bindable() if necessary
); if (
binding.kind = 'bindable_prop'; initial?.type === 'CallExpression' &&
} else { initial.callee.type === 'Identifier' &&
binding.initial = initial; initial.callee.name === '$bindable'
) {
binding.initial = /** @type {import('estree').Expression | null} */ (
initial.arguments[0] ?? null
);
binding.kind = 'bindable_prop';
} else {
binding.initial = initial;
}
} }
} }
} }

@ -1240,7 +1240,7 @@ export const validation_runes = merge(validation, a11y_validators, {
e.rune_invalid_arguments(node, rune); e.rune_invalid_arguments(node, rune);
} }
if (node.id.type !== 'ObjectPattern') { if (node.id.type !== 'ObjectPattern' && node.id.type !== 'Identifier') {
e.props_invalid_identifier(node); e.props_invalid_identifier(node);
} }
@ -1248,17 +1248,19 @@ export const validation_runes = merge(validation, a11y_validators, {
e.props_invalid_placement(node); e.props_invalid_placement(node);
} }
for (const property of node.id.properties) { if (node.id.type === 'ObjectPattern') {
if (property.type === 'Property') { for (const property of node.id.properties) {
if (property.computed) { if (property.type === 'Property') {
e.props_invalid_pattern(property); if (property.computed) {
} e.props_invalid_pattern(property);
}
const value = const value =
property.value.type === 'AssignmentPattern' ? property.value.left : property.value; property.value.type === 'AssignmentPattern' ? property.value.left : property.value;
if (value.type !== 'Identifier') { if (value.type !== 'Identifier') {
e.props_invalid_pattern(property); e.props_invalid_pattern(property);
}
} }
} }
} }

@ -9,6 +9,27 @@ export const global_visitors = {
if (node.name === '$$props') { if (node.name === '$$props') {
return b.id('$$sanitized_props'); return b.id('$$sanitized_props');
} }
// Optimize prop access: If it's a member read access, we can use the $$props object directly
const binding = state.scope.get(node.name);
if (
state.analysis.runes && // can't do this in legacy mode because the proxy does more than just read/write
binding !== null &&
node !== binding.node &&
binding.kind === 'rest_prop'
) {
const parent = path.at(-1);
const grand_parent = path.at(-2);
if (
parent?.type === 'MemberExpression' &&
!parent.computed &&
grand_parent?.type !== 'AssignmentExpression' &&
grand_parent?.type !== 'UpdateExpression'
) {
return b.id('$$props');
}
}
return serialize_get_binding(node, state); return serialize_get_binding(node, state);
} }
}, },

@ -238,8 +238,6 @@ export const javascript_visitors_runes = {
} }
if (rune === '$props') { if (rune === '$props') {
assert.equal(declarator.id.type, 'ObjectPattern');
/** @type {string[]} */ /** @type {string[]} */
const seen = ['$$slots', '$$events', '$$legacy']; const seen = ['$$slots', '$$events', '$$legacy'];
@ -247,44 +245,58 @@ export const javascript_visitors_runes = {
seen.push('$$host'); seen.push('$$host');
} }
for (const property of declarator.id.properties) { if (declarator.id.type === 'Identifier') {
if (property.type === 'Property') { /** @type {import('estree').Expression[]} */
const key = /** @type {import('estree').Identifier | import('estree').Literal} */ ( const args = [b.id('$$props'), b.array(seen.map((name) => b.literal(name)))];
property.key
);
const name = key.type === 'Identifier' ? key.name : /** @type {string} */ (key.value);
seen.push(name);
let id =
property.value.type === 'AssignmentPattern' ? property.value.left : property.value;
assert.equal(id.type, 'Identifier');
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(id.name));
let initial =
binding.initial &&
/** @type {import('estree').Expression} */ (visit(binding.initial));
// We're adding proxy here on demand and not within the prop runtime function so that
// people not using proxied state anywhere in their code don't have to pay the additional bundle size cost
if (initial && binding.mutated && should_proxy_or_freeze(initial, state.scope)) {
initial = b.call('$.proxy', initial);
}
if (is_prop_source(binding, state)) { if (state.options.dev) {
declarations.push(b.declarator(id, get_prop_source(binding, state, name, initial))); // include rest name, so we can provide informative error messages
} args.push(b.literal(declarator.id.name));
} else { }
// RestElement
/** @type {import('estree').Expression[]} */ declarations.push(b.declarator(declarator.id, b.call('$.rest_props', ...args)));
const args = [b.id('$$props'), b.array(seen.map((name) => b.literal(name)))]; } else {
assert.equal(declarator.id.type, 'ObjectPattern');
if (state.options.dev) {
// include rest name, so we can provide informative error messages for (const property of declarator.id.properties) {
args.push( if (property.type === 'Property') {
b.literal(/** @type {import('estree').Identifier} */ (property.argument).name) const key = /** @type {import('estree').Identifier | import('estree').Literal} */ (
property.key
); );
const name = key.type === 'Identifier' ? key.name : /** @type {string} */ (key.value);
seen.push(name);
let id =
property.value.type === 'AssignmentPattern' ? property.value.left : property.value;
assert.equal(id.type, 'Identifier');
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(id.name));
let initial =
binding.initial &&
/** @type {import('estree').Expression} */ (visit(binding.initial));
// We're adding proxy here on demand and not within the prop runtime function so that
// people not using proxied state anywhere in their code don't have to pay the additional bundle size cost
if (initial && binding.mutated && should_proxy_or_freeze(initial, state.scope)) {
initial = b.call('$.proxy', initial);
}
if (is_prop_source(binding, state)) {
declarations.push(b.declarator(id, get_prop_source(binding, state, name, initial)));
}
} else {
// RestElement
/** @type {import('estree').Expression[]} */
const args = [b.id('$$props'), b.array(seen.map((name) => b.literal(name)))];
if (state.options.dev) {
// include rest name, so we can provide informative error messages
args.push(
b.literal(/** @type {import('estree').Identifier} */ (property.argument).name)
);
}
declarations.push(b.declarator(property.argument, b.call('$.rest_props', ...args)));
} }
declarations.push(b.declarator(property.argument, b.call('$.rest_props', ...args)));
} }
} }

@ -78,6 +78,7 @@ const rest_props_handler = {
* @param {string} [name] * @param {string} [name]
* @returns {Record<string, unknown>} * @returns {Record<string, unknown>}
*/ */
/*#__NO_SIDE_EFFECTS__*/
export function rest_props(props, exclude, name) { export function rest_props(props, exclude, name) {
return new Proxy( return new Proxy(
DEV ? { props, exclude, name, other: {}, to_proxy: [] } : { props, exclude }, DEV ? { props, exclude, name, other: {}, to_proxy: [] } : { props, exclude },

@ -0,0 +1,17 @@
import "svelte/internal/disclose-version";
import * as $ from "svelte/internal/client";
export default function Props_identifier($$anchor, $$props) {
$.push($$props, true);
let props = $.rest_props($$props, ["$$slots", "$$events", "$$legacy"]);
$$props.a;
props[a];
$$props.a.b;
$$props.a.b = true;
props.a = true;
props[a] = true;
props;
$.pop();
}

@ -0,0 +1,16 @@
import * as $ from "svelte/internal/server";
export default function Props_identifier($$payload, $$props) {
$.push();
let props = $$props;
props.a;
props[a];
props.a.b;
props.a.b = true;
props.a = true;
props[a] = true;
props;
$.pop();
}

@ -0,0 +1,10 @@
<script>
let props = $props();
props.a;
props[a];
props.a.b;
props.a.b = true;
props.a = true;
props[a] = true;
props;
</script>

@ -548,6 +548,12 @@ To get all properties, use rest syntax:
let { a, b, c, ...everythingElse } = $props(); let { a, b, c, ...everythingElse } = $props();
``` ```
You can also use an identifier:
```js
let props = $props();
```
If you're using TypeScript, you can declare the prop types: If you're using TypeScript, you can declare the prop types:
```ts ```ts

Loading…
Cancel
Save