Skip to content

Commit

Permalink
feat: allow usage of $props.id everywhere if invoked within a compo…
Browse files Browse the repository at this point in the history
…nent script
  • Loading branch information
paoloricciuti committed Feb 14, 2025
1 parent 868e3fa commit c60e30d
Show file tree
Hide file tree
Showing 25 changed files with 161 additions and 78 deletions.
5 changes: 5 additions & 0 deletions .changeset/empty-sloths-give.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': minor
---

feat: allow usage of `$props.id` everywhere if invoked within a component script
6 changes: 0 additions & 6 deletions documentation/docs/98-reference/.generated/compile-errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

```
Expand Down
6 changes: 6 additions & 0 deletions documentation/docs/98-reference/.generated/shared-errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

```
Expand Down
4 changes: 0 additions & 4 deletions packages/svelte/messages/compile-errors/script.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions packages/svelte/messages/shared-errors/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 0 additions & 9 deletions packages/svelte/src/compiler/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion packages/svelte/src/compiler/phases/2-analyze/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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')
)
);
}

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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)))))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export function VariableDeclaration(node, context) {
}

if (rune === '$props.id') {
// skip
declarations.push(/** @type {VariableDeclarator} */ (context.visit(declarator)));
continue;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(''));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -244,20 +244,16 @@ 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) {
component_block.body.unshift(b.stmt(b.call('$.push', dev && b.id(analysis.name))));
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
2 changes: 0 additions & 2 deletions packages/svelte/src/compiler/phases/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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` */
Expand Down
25 changes: 21 additions & 4 deletions packages/svelte/src/internal/client/dom/template.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 &&
Expand All @@ -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;
}
3 changes: 2 additions & 1 deletion packages/svelte/src/internal/client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
39 changes: 34 additions & 5 deletions packages/svelte/src/internal/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 };
Expand Down
15 changes: 15 additions & 0 deletions packages/svelte/src/internal/shared/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<script>
import { get_id } from "./get_id.svelte.js";
let id = get_id();
</script>

<p>{id}</p>
Loading

0 comments on commit c60e30d

Please sign in to comment.