diff --git a/.changeset/empty-sloths-give.md b/.changeset/empty-sloths-give.md new file mode 100644 index 0000000000..163c214460 --- /dev/null +++ b/.changeset/empty-sloths-give.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: allow usage of `$props.id` everywhere if invoked within a component script diff --git a/documentation/docs/98-reference/.generated/compile-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md index a4ecbb31d5..58c73bded8 100644 --- a/documentation/docs/98-reference/.generated/compile-errors.md +++ b/documentation/docs/98-reference/.generated/compile-errors.md @@ -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 ``` diff --git a/documentation/docs/98-reference/.generated/shared-errors.md b/documentation/docs/98-reference/.generated/shared-errors.md index 0102aafcbc..454f726045 100644 --- a/documentation/docs/98-reference/.generated/shared-errors.md +++ b/documentation/docs/98-reference/.generated/shared-errors.md @@ -54,6 +54,12 @@ Certain lifecycle methods can only be used during component initialisation. To f ``` +### props_id_invalid_placement + +``` +`$props.id()` can only be used inside a component initialization phase +``` + ### store_invalid_shape ``` diff --git a/packages/svelte/messages/compile-errors/script.md b/packages/svelte/messages/compile-errors/script.md index 795c0b007d..975ac51497 100644 --- a/packages/svelte/messages/compile-errors/script.md +++ b/packages/svelte/messages/compile-errors/script.md @@ -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) diff --git a/packages/svelte/messages/shared-errors/errors.md b/packages/svelte/messages/shared-errors/errors.md index 8b4c61303a..6a0e75208a 100644 --- a/packages/svelte/messages/shared-errors/errors.md +++ b/packages/svelte/messages/shared-errors/errors.md @@ -48,6 +48,10 @@ Certain lifecycle methods can only be used during component initialisation. To f ``` +## 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 diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index 93eeee539c..9eef96feb8 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -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 diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 846abcf7df..ad9db24e1e 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -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, diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js index ce520cc980..6cf1e7b43e 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js @@ -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; } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js index 1507123e13..0e7c610b54 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js @@ -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); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index 2e6307a4b7..7c210d9723 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -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))))) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js index 7a3057451a..73ccc68fed 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js @@ -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 ( diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js index 31e712cdcc..0e512b4988 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js @@ -43,7 +43,7 @@ export function VariableDeclaration(node, context) { } if (rune === '$props.id') { - // skip + declarations.push(/** @type {VariableDeclarator} */ (context.visit(declarator))); continue; } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js index 9214a13c94..00634f229e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js @@ -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('')); } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js index df3d831d3c..89d3aad870 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js @@ -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); diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js index 386c6b6ff3..9e062aa804 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js @@ -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(); } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js index c4c31d7eb3..6ec5cbaff4 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js @@ -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; } diff --git a/packages/svelte/src/compiler/phases/types.d.ts b/packages/svelte/src/compiler/phases/types.d.ts index abe2b115de..fe32dbba3e 100644 --- a/packages/svelte/src/compiler/phases/types.d.ts +++ b/packages/svelte/src/compiler/phases/types.d.ts @@ -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` */ diff --git a/packages/svelte/src/internal/client/dom/template.js b/packages/svelte/src/internal/client/dom/template.js index 6ff3b0fa19..25a161e92f 100644 --- a/packages/svelte/src/internal/client/dom/template.js +++ b/packages/svelte/src/internal/client/dom/template.js @@ -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; } diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index d78f6d452e..92046aaf09 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -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 { diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index 728f2ebc2a..7ac7e1a249 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -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 += ''; - 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 = '' + payload.out; + } + need_props_id = old_needs_props_id; + payload.out = old_payload + payload.out; + current_id = old_id; + }; } export { attr, clsx }; diff --git a/packages/svelte/src/internal/shared/errors.js b/packages/svelte/src/internal/shared/errors.js index 26d6822cdb..7269ac6847 100644 --- a/packages/svelte/src/internal/shared/errors.js +++ b/packages/svelte/src/internal/shared/errors.js @@ -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 diff --git a/packages/svelte/tests/runtime-runes/samples/props-id-everywhere/Child.svelte b/packages/svelte/tests/runtime-runes/samples/props-id-everywhere/Child.svelte new file mode 100644 index 0000000000..6a5018981a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-id-everywhere/Child.svelte @@ -0,0 +1,6 @@ + + +
{id}
diff --git a/packages/svelte/tests/runtime-runes/samples/props-id-everywhere/_config.js b/packages/svelte/tests/runtime-runes/samples/props-id-everywhere/_config.js new file mode 100644 index 0000000000..b40d1ce299 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-id-everywhere/_config.js @@ -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']); + } + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/props-id-everywhere/get_id.svelte.js b/packages/svelte/tests/runtime-runes/samples/props-id-everywhere/get_id.svelte.js new file mode 100644 index 0000000000..d31840f685 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-id-everywhere/get_id.svelte.js @@ -0,0 +1,3 @@ +export function get_id() { + return $props.id(); +} diff --git a/packages/svelte/tests/runtime-runes/samples/props-id-everywhere/main.svelte b/packages/svelte/tests/runtime-runes/samples/props-id-everywhere/main.svelte new file mode 100644 index 0000000000..13de6e6ff1 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-id-everywhere/main.svelte @@ -0,0 +1,20 @@ + + + + +{id}
+ +