Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -256,10 +256,26 @@ export function reset_props_id() {
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 @@ -268,8 +284,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