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 5 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,7 +970,14 @@ 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') {
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(node.id.name));
binding.initial = null; // else would be $props()
binding.kind = 'rest_prop';
} else {
equal(node.id.type, 'ObjectPattern');
for (const property of node.id.properties) {
if (property.type !== 'Property') continue; if (property.type !== 'Property') continue;
const name = const name =
@ -1000,6 +1008,7 @@ const runes_scope_tweaker = {
} }
} }
} }
}
}, },
ExportSpecifier(node, { state }) { ExportSpecifier(node, { state }) {
if (state.ast_type !== 'instance') return; if (state.ast_type !== 'instance') return;

@ -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,6 +1248,7 @@ export const validation_runes = merge(validation, a11y_validators, {
e.props_invalid_placement(node); e.props_invalid_placement(node);
} }
if (node.id.type === 'ObjectPattern') {
for (const property of node.id.properties) { for (const property of node.id.properties) {
if (property.type === 'Property') { if (property.type === 'Property') {
if (property.computed) { if (property.computed) {
@ -1263,6 +1264,7 @@ export const validation_runes = merge(validation, a11y_validators, {
} }
} }
} }
}
if (rune === '$derived') { if (rune === '$derived') {
const arg = args[0]; const arg = args[0];

@ -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,6 +245,19 @@ export const javascript_visitors_runes = {
seen.push('$$host'); seen.push('$$host');
} }
if (declarator.id.type === 'Identifier') {
/** @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(declarator.id.name));
}
declarations.push(b.declarator(declarator.id, b.call('$.rest_props', ...args)));
} else {
assert.equal(declarator.id.type, 'ObjectPattern');
for (const property of declarator.id.properties) { for (const property of declarator.id.properties) {
if (property.type === 'Property') { if (property.type === 'Property') {
const key = /** @type {import('estree').Identifier | import('estree').Literal} */ ( const key = /** @type {import('estree').Identifier | import('estree').Literal} */ (
@ -287,6 +298,7 @@ export const javascript_visitors_runes = {
declarations.push(b.declarator(property.argument, b.call('$.rest_props', ...args))); declarations.push(b.declarator(property.argument, b.call('$.rest_props', ...args)));
} }
} }
}
// TODO // TODO
continue; continue;

@ -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