feat: allow usage of `$props.id` everywhere if invoked within a component script

props-id-anywhere
paoloricciuti 7 months ago
parent 868e3fa56e
commit c60e30d7de

@ -0,0 +1,5 @@
---
'svelte': minor
---
feat: allow usage of `$props.id` everywhere if invoked within a component script

@ -576,12 +576,6 @@ Unrecognised compiler option %keypath%
Cannot use `%rune%()` more than once
```
### props_id_invalid_placement
```
`$props.id()` can only be used at the top level of components as a variable declaration initializer
```
### props_illegal_name
```

@ -54,6 +54,12 @@ Certain lifecycle methods can only be used during component initialisation. To f
<button onclick={handleClick}>click me</button>
```
### props_id_invalid_placement
```
`$props.id()` can only be used inside a component initialization phase
```
### store_invalid_shape
```

@ -122,10 +122,6 @@ This turned out to be buggy and unpredictable, particularly when working with de
> Cannot use `%rune%()` more than once
## props_id_invalid_placement
> `$props.id()` can only be used at the top level of components as a variable declaration initializer
## props_illegal_name
> Declaring or accessing a prop starting with `$$` is illegal (they are reserved for Svelte internals)

@ -48,6 +48,10 @@ Certain lifecycle methods can only be used during component initialisation. To f
<button onclick={handleClick}>click me</button>
```
## props_id_invalid_placement
> `$props.id()` can only be used inside a component initialization phase
## store_invalid_shape
> `%name%` is not a store with a `subscribe` method

@ -288,15 +288,6 @@ export function props_duplicate(node, rune) {
e(node, 'props_duplicate', `Cannot use \`${rune}()\` more than once\nhttps://svelte.dev/e/props_duplicate`);
}
/**
* `$props.id()` can only be used at the top level of components as a variable declaration initializer
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function props_id_invalid_placement(node) {
e(node, 'props_id_invalid_placement', `\`$props.id()\` can only be used at the top level of components as a variable declaration initializer\nhttps://svelte.dev/e/props_id_invalid_placement`);
}
/**
* Declaring or accessing a prop starting with `$$` is illegal (they are reserved for Svelte internals)
* @param {null | number | NodeLike} node

@ -416,7 +416,6 @@ export function analyze_component(root, source, options) {
immutable: runes || options.immutable,
exports: [],
uses_props: false,
props_id: null,
uses_rest_props: false,
uses_slots: false,
uses_component_bindings: false,

@ -75,28 +75,9 @@ export function CallExpression(node, context) {
break;
case '$props.id': {
const grand_parent = get_parent(context.path, -2);
if (context.state.analysis.props_id) {
e.props_duplicate(node, rune);
}
if (
parent.type !== 'VariableDeclarator' ||
parent.id.type !== 'Identifier' ||
context.state.ast_type !== 'instance' ||
context.state.scope !== context.state.analysis.instance.scope ||
grand_parent.type !== 'VariableDeclaration'
) {
e.props_id_invalid_placement(node);
}
if (node.arguments.length > 0) {
e.rune_invalid_arguments(node, rune);
}
context.state.analysis.props_id = parent.id;
break;
}

@ -25,10 +25,6 @@ export function validate_assignment(node, argument, state) {
e.constant_assignment(node, 'derived state');
}
if (binding?.node === state.analysis.props_id) {
e.constant_assignment(node, '$props.id()');
}
if (binding?.kind === 'each') {
e.each_item_invalid_assignment(node);
}

@ -226,7 +226,10 @@ export function client_component(analysis, options) {
if (store_setup.length === 0) {
needs_store_cleanup = true;
store_setup.push(
b.const(b.array_pattern([b.id('$$stores'), b.id('$$cleanup')]), b.call('$.setup_stores'))
b.const(
b.array_pattern([b.id('$$stores'), b.id('$$cleanup_stores')]),
b.call('$.setup_stores')
)
);
}
@ -414,11 +417,13 @@ export function client_component(analysis, options) {
}
if (needs_store_cleanup) {
component_block.body.push(b.stmt(b.call('$$cleanup')));
component_block.body.push(b.stmt(b.call('$$cleanup_stores')));
if (component_returned_object.length > 0) {
component_block.body.push(b.return(b.id('$$pop')));
}
}
component_block.body.unshift(b.const('$$cleanup', b.call('$.setup')));
component_block.body.push(b.stmt(b.call('$$cleanup')));
if (analysis.uses_rest_props) {
const named_props = analysis.exports.map(({ name, alias }) => alias ?? name);
@ -562,11 +567,6 @@ export function client_component(analysis, options) {
component_block.body.unshift(b.stmt(b.call('$.check_target', b.id('new.target'))));
}
if (analysis.props_id) {
// need to be placed on first line of the component for hydration
component_block.body.unshift(b.const(analysis.props_id, b.call('$.props_id')));
}
if (state.events.size > 0) {
body.push(
b.stmt(b.call('$.delegate', b.array(Array.from(state.events).map((name) => b.literal(name)))))

@ -33,6 +33,8 @@ export function CallExpression(node, context) {
case '$inspect':
case '$inspect().with':
return transform_inspect_rune(node, context);
case '$props.id':
return b.call('$.props_id');
}
if (

@ -43,7 +43,7 @@ export function VariableDeclaration(node, context) {
}
if (rune === '$props.id') {
// skip
declarations.push(/** @type {VariableDeclarator} */ (context.visit(declarator)));
continue;
}

@ -129,12 +129,6 @@ export function build_template_chunk(
if (value.right.value === null) {
value = { ...value, right: b.literal('') };
}
} else if (
state.analysis.props_id &&
value.type === 'Identifier' &&
value.name === state.analysis.props_id.name
) {
// do nothing ($props.id() is never null/undefined)
} else {
value = b.logical('??', value, b.literal(''));
}

@ -244,13 +244,6 @@ export function server_component(analysis, options) {
.../** @type {Statement[]} */ (template.body)
]);
if (analysis.props_id) {
// need to be placed on first line of the component for hydration
component_block.body.unshift(
b.const(analysis.props_id, b.call('$.props_id', b.id('$$payload')))
);
}
let should_inject_context = dev || analysis.needs_context;
if (should_inject_context) {
@ -258,6 +251,9 @@ export function server_component(analysis, options) {
component_block.body.push(b.stmt(b.call('$.pop')));
}
component_block.body.unshift(b.const('$$cleanup', b.call('$.setup', b.id('$$payload'))));
component_block.body.push(b.stmt(b.call('$$cleanup', b.id('$$payload'))));
if (analysis.uses_rest_props) {
/** @type {string[]} */
const named_props = analysis.exports.map(({ name, alias }) => alias ?? name);

@ -37,5 +37,9 @@ export function CallExpression(node, context) {
return transform_inspect_rune(node, context);
}
if (rune === '$props.id') {
return b.call('$.props_id');
}
context.next();
}

@ -23,9 +23,8 @@ export function VariableDeclaration(node, context) {
declarations.push(/** @type {VariableDeclarator} */ (context.visit(declarator)));
continue;
}
if (rune === '$props.id') {
// skip
declarations.push(/** @type {VariableDeclarator} */ (context.visit(declarator)));
continue;
}

@ -44,8 +44,6 @@ export interface ComponentAnalysis extends Analysis {
exports: Array<{ name: string; alias: string | null }>;
/** Whether the component uses `$$props` */
uses_props: boolean;
/** The component ID variable name, if any */
props_id: Identifier | null;
/** Whether the component uses `$$restProps` */
uses_rest_props: boolean;
/** Whether the component uses `$$slots` */

@ -4,7 +4,7 @@ import { create_text, get_first_child, is_firefox } from './operations.js';
import { create_fragment_from_html } from './reconciler.js';
import { active_effect } from '../runtime.js';
import { TEMPLATE_FRAGMENT, TEMPLATE_USE_IMPORT_NODE } from '../../../constants.js';
import * as e from '../../shared/errors.js';
/**
* @param {TemplateNode} start
* @param {TemplateNode | null} end
@ -252,10 +252,26 @@ export function append(anchor, dom) {
let uid = 1;
/**
* @type {string | undefined}
*/
let current_uid;
/**
* Create (or hydrate) an unique UID for the component instance.
*/
export function props_id() {
if (current_uid == null) {
e.props_id_invalid_placement();
}
return current_uid;
}
export function setup() {
let old_uid = current_uid;
function reset() {
current_uid = old_uid;
}
if (
hydrating &&
hydrate_node &&
@ -264,8 +280,9 @@ export function props_id() {
) {
const id = hydrate_node.textContent.substring(1);
hydrate_next();
return id;
current_uid = id;
return reset;
}
return 'c' + uid++;
current_uid = 'c' + uid++;
return reset;
}

@ -97,7 +97,8 @@ export {
template,
template_with_script,
text,
props_id
props_id,
setup
} from './dom/template.js';
export { derived, derived_safe_equal } from './reactivity/deriveds.js';
export {

@ -10,6 +10,7 @@ import {
ELEMENT_PRESERVE_ATTRIBUTE_CASE,
ELEMENT_IS_NAMESPACED
} from '../../constants.js';
import * as e from '../shared/errors.js';
import { escape_html } from '../../escaping.js';
import { DEV } from 'esm-env';
@ -543,13 +544,41 @@ export function once(get_value) {
/**
* Create an unique ID
* @param {Payload} payload
* @returns {string}
*/
export function props_id(payload) {
const uid = payload.uid();
payload.out += '<!--#' + uid + '-->';
return uid;
export function props_id() {
if (current_id == null) {
e.props_id_invalid_placement();
}
need_props_id = true;
return current_id;
}
/**
* @type {string | undefined}
*/
let current_id;
let need_props_id = false;
/**
* @param {Payload} payload
* @returns {(payload: Payload)=>void}
*/
export function setup(payload) {
let old_payload = payload.out;
let old_needs_props_id = need_props_id;
let old_id = current_id;
current_id = payload.uid();
payload.out = '';
return (payload) => {
if (need_props_id) {
payload.out = '<!--#' + current_id + '-->' + payload.out;
}
need_props_id = old_needs_props_id;
payload.out = old_payload + payload.out;
current_id = old_id;
};
}
export { attr, clsx };

@ -33,6 +33,21 @@ export function lifecycle_outside_component(name) {
}
}
/**
* `$props.id()` can only be used inside a component initialization phase
* @returns {never}
*/
export function props_id_invalid_placement() {
if (DEV) {
const error = new Error(`props_id_invalid_placement\n\`$props.id()\` can only be used inside a component initialization phase\nhttps://svelte.dev/e/props_id_invalid_placement`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/props_id_invalid_placement`);
}
}
/**
* `%name%` is not a store with a `subscribe` method
* @param {string} name

@ -0,0 +1,6 @@
<script>
import { get_id } from "./get_id.svelte.js";
let id = get_id();
</script>
<p>{id}</p>

@ -0,0 +1,27 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
test({ assert, target, variant }) {
const ps = [...target.querySelectorAll('p')].map((p) => p.innerHTML);
const unique = new Set(ps);
assert.equal(ps.length, unique.size);
if (variant === 'hydrate') {
const start = ps.map((p) => p.substring(0, 1));
assert.deepEqual(start, ['s', 's', 's', 's']);
}
let button = target.querySelector('button');
flushSync(() => button?.click());
const ps_after = [...target.querySelectorAll('p')].map((p) => p.innerHTML);
const unique_after = new Set(ps_after);
assert.equal(ps_after.length, unique_after.size);
if (variant === 'hydrate') {
const start = ps_after.map((p) => p.substring(0, 1));
assert.deepEqual(start, ['s', 's', 's', 's', 'c']);
}
}
});

@ -0,0 +1,3 @@
export function get_id() {
return $props.id();
}

@ -0,0 +1,20 @@
<script>
import { get_id } from "./get_id.svelte.js";
import Child from './Child.svelte';
let id = get_id();
let show = $state(false);
</script>
<button onclick={() => show = !show}>toggle</button>
<p>{id}</p>
<Child />
<Child />
<Child />
{#if show}
<Child />
{/if}
Loading…
Cancel
Save