From 657a09aa4c0838d742418eb7f9d28f40312e670d Mon Sep 17 00:00:00 2001 From: adiguba Date: Sun, 2 Feb 2025 09:49:49 +0100 Subject: [PATCH 01/18] first impl of $$uid --- packages/svelte/src/ambient.d.ts | 2 ++ .../src/compiler/phases/2-analyze/index.js | 2 +- .../phases/2-analyze/visitors/Identifier.js | 4 +++ .../3-transform/client/transform-client.js | 4 +++ .../3-transform/server/transform-server.js | 4 +++ .../svelte/src/compiler/phases/types.d.ts | 2 ++ .../src/internal/client/dom/template.js | 21 +++++++++++++ packages/svelte/src/internal/client/index.js | 3 +- packages/svelte/src/internal/server/index.js | 31 ++++++++++++++++--- .../svelte/src/internal/server/types.d.ts | 2 ++ packages/svelte/types/index.d.ts | 2 ++ 11 files changed, 71 insertions(+), 6 deletions(-) diff --git a/packages/svelte/src/ambient.d.ts b/packages/svelte/src/ambient.d.ts index 9dbc61c7cbd4..d231b5ca98d4 100644 --- a/packages/svelte/src/ambient.d.ts +++ b/packages/svelte/src/ambient.d.ts @@ -500,3 +500,5 @@ declare namespace $host { /** @deprecated */ export const toString: never; } + +declare const $$uid: string; diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 76c1e94277be..85ca7f664358 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -220,7 +220,7 @@ function get_component_name(filename) { return name[0].toUpperCase() + name.slice(1); } -const RESERVED = ['$$props', '$$restProps', '$$slots']; +const RESERVED = ['$$props', '$$restProps', '$$slots', '$$uid']; /** * @param {Program} ast diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js index 79dccd5a7cf5..498a63232a8e 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js @@ -35,6 +35,10 @@ export function Identifier(node, context) { context.state.analysis.uses_slots = true; } + if (node.name === '$$uid') { + context.state.analysis.uses_uid = true; + } + if (context.state.analysis.runes) { if ( is_rune(node.name) && 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 90901d29ce7d..63b8265b594c 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 @@ -565,6 +565,10 @@ export function client_component(analysis, options) { component_block.body.unshift(b.stmt(b.call('$.check_target', b.id('new.target')))); } + if (analysis.uses_uid) { + component_block.body.unshift(b.const('$$uid', b.call('$.create_uid'))); + } + 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/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js index 982b75e12f53..8511aa03adf2 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,6 +244,10 @@ export function server_component(analysis, options) { .../** @type {Statement[]} */ (template.body) ]); + if (analysis.uses_uid) { + component_block.body.unshift(b.const('$$uid', b.call('$.create_uid', b.id('$$payload')))); + } + let should_inject_context = dev || analysis.needs_context; if (should_inject_context) { diff --git a/packages/svelte/src/compiler/phases/types.d.ts b/packages/svelte/src/compiler/phases/types.d.ts index fe32dbba3e4a..d4c10c9940aa 100644 --- a/packages/svelte/src/compiler/phases/types.d.ts +++ b/packages/svelte/src/compiler/phases/types.d.ts @@ -50,6 +50,8 @@ export interface ComponentAnalysis extends Analysis { uses_slots: boolean; uses_component_bindings: boolean; uses_render_tags: boolean; + /** Whether the component uses `$$uid` */ + uses_uid: boolean; needs_context: boolean; needs_props: boolean; /** Set to the first event directive (on:x) found on a DOM element in the code */ diff --git a/packages/svelte/src/internal/client/dom/template.js b/packages/svelte/src/internal/client/dom/template.js index bcbae393ecff..47b256c9d152 100644 --- a/packages/svelte/src/internal/client/dom/template.js +++ b/packages/svelte/src/internal/client/dom/template.js @@ -249,3 +249,24 @@ export function append(anchor, dom) { anchor.before(/** @type {Node} */ (dom)); } + +let NEXT_UID = 100; + +/** + * Create (or hydrate) an unique UID for the component instance. + */ +export function create_uid() { + let uid; + if ( + hydrating && + hydrate_node && + hydrate_node.nodeType === Node.COMMENT_NODE && + hydrate_node.textContent?.startsWith('#s') + ) { + uid = hydrate_node.textContent.substring(1); + hydrate_next(); + } else { + uid = 'c' + NEXT_UID++; + } + return uid; +} diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 3b85ae18166e..58a05f66e90d 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -95,7 +95,8 @@ export { mathml_template, template, template_with_script, - text + text, + create_uid } 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 89b3c33df887..fe9ca2b5dce8 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -28,14 +28,15 @@ const INVALID_ATTR_NAME_CHAR_REGEX = * @param {Payload} to_copy * @returns {Payload} */ -export function copy_payload({ out, css, head }) { +export function copy_payload({ out, css, head, uid }) { return { out, css: new Set(css), head: { title: head.title, out: head.out - } + }, + uid }; } @@ -48,6 +49,7 @@ export function copy_payload({ out, css, head }) { export function assign_payload(p1, p2) { p1.out = p2.out; p1.head = p2.head; + p1.uid = p2.uid; } /** @@ -83,17 +85,27 @@ export function element(payload, tag, attributes_fn = noop, children_fn = noop) */ export let on_destroy = []; +function create_uid_generator() { + let uid = 100; + return () => 's' + uid++; +} + /** * Only available on the server and when compiling with the `server` option. * Takes a component and returns an object with `body` and `head` properties on it, which you can use to populate the HTML when server-rendering your app. * @template {Record} Props * @param {import('svelte').Component | ComponentType>} component - * @param {{ props?: Omit; context?: Map }} [options] + * @param {{ props?: Omit; context?: Map, uid?: () => string }} [options] * @returns {RenderOutput} */ export function render(component, options = {}) { /** @type {Payload} */ - const payload = { out: '', css: new Set(), head: { title: '', out: '' } }; + const payload = { + out: '', + css: new Set(), + head: { title: '', out: '' }, + uid: options.uid ?? create_uid_generator() + }; const prev_on_destroy = on_destroy; on_destroy = []; @@ -526,6 +538,17 @@ export function once(get_value) { }; } +/** + * Create an unique ID + * @param {Payload} payload + * @returns {string} + */ +export function create_uid(payload) { + const UID = payload.uid(); + payload.out += ''; + return UID; +} + export { attr, clsx }; export { html } from './blocks/html.js'; diff --git a/packages/svelte/src/internal/server/types.d.ts b/packages/svelte/src/internal/server/types.d.ts index e6c235147b5f..8a241deecd18 100644 --- a/packages/svelte/src/internal/server/types.d.ts +++ b/packages/svelte/src/internal/server/types.d.ts @@ -18,6 +18,8 @@ export interface Payload { title: string; out: string; }; + /** Function that generates a unique ID */ + uid: () => string; } export interface RenderOutput { diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index d422abebbc0f..da91743dc0e5 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -3159,4 +3159,6 @@ declare namespace $host { export const toString: never; } +declare const $$uid: string; + //# sourceMappingURL=index.d.ts.map From 0580d77b79938e8752589534476e6c3085bae5c4 Mon Sep 17 00:00:00 2001 From: adiguba Date: Sun, 2 Feb 2025 10:29:49 +0100 Subject: [PATCH 02/18] fix --- packages/svelte/src/compiler/phases/2-analyze/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 85ca7f664358..4e226ad42301 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -419,6 +419,7 @@ export function analyze_component(root, source, options) { uses_slots: false, uses_component_bindings: false, uses_render_tags: false, + uses_uid: false, needs_context: false, needs_props: false, event_directive_node: null, From 74daf21e489dbfed6c3cdfc2af822bf43db01089 Mon Sep 17 00:00:00 2001 From: adiguba Date: Mon, 3 Feb 2025 21:38:33 +0100 Subject: [PATCH 03/18] $props.id() --- .../98-reference/.generated/compile-errors.md | 4 +-- .../svelte/messages/compile-errors/script.md | 4 +-- packages/svelte/src/ambient.d.ts | 4 +-- packages/svelte/src/compiler/errors.js | 14 +++++----- .../src/compiler/phases/2-analyze/index.js | 4 +-- .../2-analyze/visitors/CallExpression.js | 26 +++++++++++++++++-- .../phases/2-analyze/visitors/Identifier.js | 4 --- .../3-transform/client/transform-client.js | 5 ++-- .../client/visitors/VariableDeclaration.js | 5 ++++ .../client/visitors/shared/utils.js | 6 +++++ .../3-transform/server/transform-server.js | 7 +++-- .../server/visitors/VariableDeclaration.js | 9 +++++++ .../svelte/src/compiler/phases/types.d.ts | 4 +-- packages/svelte/src/utils.js | 1 + packages/svelte/types/index.d.ts | 4 +-- 15 files changed, 73 insertions(+), 28 deletions(-) diff --git a/documentation/docs/98-reference/.generated/compile-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md index 2fef3bd45d50..369901fe66f1 100644 --- a/documentation/docs/98-reference/.generated/compile-errors.md +++ b/documentation/docs/98-reference/.generated/compile-errors.md @@ -573,7 +573,7 @@ Unrecognised compiler option %keypath% ### props_duplicate ``` -Cannot use `$props()` more than once +Cannot use `%rune%` more than once ``` ### props_illegal_name @@ -597,7 +597,7 @@ Declaring or accessing a prop starting with `$$` is illegal (they are reserved f ### props_invalid_placement ``` -`$props()` can only be used at the top level of components as a variable declaration initializer +`%rune%` can only be used at the top level of components as a variable declaration initializer ``` ### reactive_declaration_cycle diff --git a/packages/svelte/messages/compile-errors/script.md b/packages/svelte/messages/compile-errors/script.md index 0aa6fbed90d8..ebc53b4ba244 100644 --- a/packages/svelte/messages/compile-errors/script.md +++ b/packages/svelte/messages/compile-errors/script.md @@ -120,7 +120,7 @@ This turned out to be buggy and unpredictable, particularly when working with de ## props_duplicate -> Cannot use `$props()` more than once +> Cannot use `%rune%` more than once ## props_illegal_name @@ -136,7 +136,7 @@ This turned out to be buggy and unpredictable, particularly when working with de ## props_invalid_placement -> `$props()` can only be used at the top level of components as a variable declaration initializer +> `%rune%` can only be used at the top level of components as a variable declaration initializer ## reactive_declaration_cycle diff --git a/packages/svelte/src/ambient.d.ts b/packages/svelte/src/ambient.d.ts index 4e5b51b7af6e..7919a61b1790 100644 --- a/packages/svelte/src/ambient.d.ts +++ b/packages/svelte/src/ambient.d.ts @@ -339,6 +339,8 @@ declare namespace $effect { declare function $props(): any; declare namespace $props { + export function id(): string; + // prevent intellisense from being unhelpful /** @deprecated */ export const apply: never; @@ -500,5 +502,3 @@ declare namespace $host { /** @deprecated */ export const toString: never; } - -declare const $$uid: string; diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index 53a6ac6849ec..25528d30cc9a 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -279,12 +279,13 @@ export function module_illegal_default_export(node) { } /** - * Cannot use `$props()` more than once + * Cannot use `%rune%` more than once * @param {null | number | NodeLike} node + * @param {string} rune * @returns {never} */ -export function props_duplicate(node) { - e(node, 'props_duplicate', `Cannot use \`$props()\` more than once\nhttps://svelte.dev/e/props_duplicate`); +export function props_duplicate(node, rune) { + e(node, 'props_duplicate', `Cannot use \`${rune}\` more than once\nhttps://svelte.dev/e/props_duplicate`); } /** @@ -315,12 +316,13 @@ export function props_invalid_pattern(node) { } /** - * `$props()` can only be used at the top level of components as a variable declaration initializer + * `%rune%` can only be used at the top level of components as a variable declaration initializer * @param {null | number | NodeLike} node + * @param {string} rune * @returns {never} */ -export function props_invalid_placement(node) { - e(node, 'props_invalid_placement', `\`$props()\` can only be used at the top level of components as a variable declaration initializer\nhttps://svelte.dev/e/props_invalid_placement`); +export function props_invalid_placement(node, rune) { + e(node, 'props_invalid_placement', `\`${rune}\` can only be used at the top level of components as a variable declaration initializer\nhttps://svelte.dev/e/props_invalid_placement`); } /** diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 624c3a540ebd..4894d0a45f7c 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -220,7 +220,7 @@ function get_component_name(filename) { return name[0].toUpperCase() + name.slice(1); } -const RESERVED = ['$$props', '$$restProps', '$$slots', '$$uid']; +const RESERVED = ['$$props', '$$restProps', '$$slots']; /** * @param {Program} ast @@ -416,11 +416,11 @@ 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, uses_render_tags: false, - uses_uid: false, needs_context: false, needs_props: false, event_directive_node: null, 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 0a6b3f3ee520..38b6cdfd0e32 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js @@ -55,7 +55,7 @@ export function CallExpression(node, context) { case '$props': if (context.state.has_props_rune) { - e.props_duplicate(node); + e.props_duplicate(node, rune); } context.state.has_props_rune = true; @@ -65,7 +65,7 @@ export function CallExpression(node, context) { context.state.ast_type !== 'instance' || context.state.scope !== context.state.analysis.instance.scope ) { - e.props_invalid_placement(node); + e.props_invalid_placement(node, rune); } if (node.arguments.length > 0) { @@ -74,6 +74,28 @@ export function CallExpression(node, context) { break; + case '$props.id': + 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 + ) { + e.props_invalid_placement(node, rune); + } + + if (node.arguments.length > 0) { + e.rune_invalid_arguments(node, rune); + } + + context.state.analysis.props_id = parent.id; + + break; + case '$state': case '$state.raw': case '$derived': diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js index 498a63232a8e..79dccd5a7cf5 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js @@ -35,10 +35,6 @@ export function Identifier(node, context) { context.state.analysis.uses_slots = true; } - if (node.name === '$$uid') { - context.state.analysis.uses_uid = true; - } - if (context.state.analysis.runes) { if ( is_rune(node.name) && 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 2a4878c05638..bb1d49080d8d 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 @@ -562,8 +562,9 @@ export function client_component(analysis, options) { component_block.body.unshift(b.stmt(b.call('$.check_target', b.id('new.target')))); } - if (analysis.uses_uid) { - component_block.body.unshift(b.const('$$uid', b.call('$.create_uid'))); + 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('$.create_uid'))); } if (state.events.size > 0) { 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 afb90bbec7f9..31e712cdcc4d 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 @@ -42,6 +42,11 @@ export function VariableDeclaration(node, context) { continue; } + if (rune === '$props.id') { + // skip + continue; + } + if (rune === '$props') { /** @type {string[]} */ const seen = ['$$slots', '$$events', '$$legacy']; 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 00634f229eeb..9214a13c94ca 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,6 +129,12 @@ 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 8511aa03adf2..ca965245d045 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,8 +244,11 @@ export function server_component(analysis, options) { .../** @type {Statement[]} */ (template.body) ]); - if (analysis.uses_uid) { - component_block.body.unshift(b.const('$$uid', b.call('$.create_uid', b.id('$$payload')))); + 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('$.create_uid', b.id('$$payload'))) + ); } let should_inject_context = dev || analysis.needs_context; 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 31de811ac76f..c4c31d7eb304 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 @@ -24,6 +24,11 @@ export function VariableDeclaration(node, context) { continue; } + if (rune === '$props.id') { + // skip + continue; + } + if (rune === '$props') { let has_rest = false; // remove $bindable() from props declaration @@ -156,6 +161,10 @@ export function VariableDeclaration(node, context) { } } + if (declarations.length === 0) { + return b.empty; + } + return { ...node, declarations diff --git a/packages/svelte/src/compiler/phases/types.d.ts b/packages/svelte/src/compiler/phases/types.d.ts index d4c10c9940aa..abe2b115de02 100644 --- a/packages/svelte/src/compiler/phases/types.d.ts +++ b/packages/svelte/src/compiler/phases/types.d.ts @@ -44,14 +44,14 @@ 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` */ uses_slots: boolean; uses_component_bindings: boolean; uses_render_tags: boolean; - /** Whether the component uses `$$uid` */ - uses_uid: boolean; needs_context: boolean; needs_props: boolean; /** Set to the first event directive (on:x) found on a DOM element in the code */ diff --git a/packages/svelte/src/utils.js b/packages/svelte/src/utils.js index e8e1bc224ce4..d4d106d56deb 100644 --- a/packages/svelte/src/utils.js +++ b/packages/svelte/src/utils.js @@ -433,6 +433,7 @@ const RUNES = /** @type {const} */ ([ '$state.raw', '$state.snapshot', '$props', + '$props.id', '$bindable', '$derived', '$derived.by', diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 5356468c249f..60e76d27c239 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -2995,6 +2995,8 @@ declare namespace $effect { declare function $props(): any; declare namespace $props { + export function id(): string; + // prevent intellisense from being unhelpful /** @deprecated */ export const apply: never; @@ -3157,6 +3159,4 @@ declare namespace $host { export const toString: never; } -declare const $$uid: string; - //# sourceMappingURL=index.d.ts.map From 6698a349178ec811476ed96b1292c6e15aaa4f8e Mon Sep 17 00:00:00 2001 From: adiguba Date: Mon, 3 Feb 2025 22:26:04 +0100 Subject: [PATCH 04/18] fix errors --- .../98-reference/.generated/compile-errors.md | 10 ++++++++-- .../svelte/messages/compile-errors/script.md | 8 ++++++-- packages/svelte/src/compiler/errors.js | 20 +++++++++++++------ .../2-analyze/visitors/CallExpression.js | 13 ++++++++---- 4 files changed, 37 insertions(+), 14 deletions(-) diff --git a/documentation/docs/98-reference/.generated/compile-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md index 369901fe66f1..ca2a3cff323e 100644 --- a/documentation/docs/98-reference/.generated/compile-errors.md +++ b/documentation/docs/98-reference/.generated/compile-errors.md @@ -573,7 +573,13 @@ Unrecognised compiler option %keypath% ### props_duplicate ``` -Cannot use `%rune%` more than once +Cannot use `%rune%()` more than once +``` + +### props_id_invalid_placement + +``` +`$props.id()` can only be affected at the top level of components as a const declaration initializer ``` ### props_illegal_name @@ -597,7 +603,7 @@ Declaring or accessing a prop starting with `$$` is illegal (they are reserved f ### props_invalid_placement ``` -`%rune%` can only be used at the top level of components as a variable declaration initializer +`$props()` can only be used at the top level of components as a variable declaration initializer ``` ### reactive_declaration_cycle diff --git a/packages/svelte/messages/compile-errors/script.md b/packages/svelte/messages/compile-errors/script.md index ebc53b4ba244..9837d7150759 100644 --- a/packages/svelte/messages/compile-errors/script.md +++ b/packages/svelte/messages/compile-errors/script.md @@ -120,7 +120,11 @@ This turned out to be buggy and unpredictable, particularly when working with de ## props_duplicate -> Cannot use `%rune%` more than once +> Cannot use `%rune%()` more than once + +## props_id_invalid_placement + +> `$props.id()` can only be affected at the top level of components as a const declaration initializer ## props_illegal_name @@ -136,7 +140,7 @@ This turned out to be buggy and unpredictable, particularly when working with de ## props_invalid_placement -> `%rune%` can only be used at the top level of components as a variable declaration initializer +> `$props()` can only be used at the top level of components as a variable declaration initializer ## reactive_declaration_cycle diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index 25528d30cc9a..557d1d16848d 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -279,13 +279,22 @@ export function module_illegal_default_export(node) { } /** - * Cannot use `%rune%` more than once + * Cannot use `%rune%()` more than once * @param {null | number | NodeLike} node * @param {string} rune * @returns {never} */ export function props_duplicate(node, rune) { - e(node, 'props_duplicate', `Cannot use \`${rune}\` more than once\nhttps://svelte.dev/e/props_duplicate`); + e(node, 'props_duplicate', `Cannot use \`${rune}()\` more than once\nhttps://svelte.dev/e/props_duplicate`); +} + +/** + * `$props.id()` can only be affected at the top level of components as a const 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 affected at the top level of components as a const declaration initializer\nhttps://svelte.dev/e/props_id_invalid_placement`); } /** @@ -316,13 +325,12 @@ export function props_invalid_pattern(node) { } /** - * `%rune%` can only be used at the top level of components as a variable declaration initializer + * `$props()` can only be used at the top level of components as a variable declaration initializer * @param {null | number | NodeLike} node - * @param {string} rune * @returns {never} */ -export function props_invalid_placement(node, rune) { - e(node, 'props_invalid_placement', `\`${rune}\` can only be used at the top level of components as a variable declaration initializer\nhttps://svelte.dev/e/props_invalid_placement`); +export function props_invalid_placement(node) { + e(node, 'props_invalid_placement', `\`$props()\` can only be used at the top level of components as a variable declaration initializer\nhttps://svelte.dev/e/props_invalid_placement`); } /** 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 38b6cdfd0e32..e59c1ccf1a3a 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js @@ -65,7 +65,7 @@ export function CallExpression(node, context) { context.state.ast_type !== 'instance' || context.state.scope !== context.state.analysis.instance.scope ) { - e.props_invalid_placement(node, rune); + e.props_invalid_placement(node); } if (node.arguments.length > 0) { @@ -74,7 +74,9 @@ export function CallExpression(node, context) { break; - case '$props.id': + case '$props.id': { + const grand_parent = get_parent(context.path, -2); + if (context.state.analysis.props_id) { e.props_duplicate(node, rune); } @@ -83,9 +85,11 @@ export function CallExpression(node, context) { parent.type !== 'VariableDeclarator' || parent.id.type !== 'Identifier' || context.state.ast_type !== 'instance' || - context.state.scope !== context.state.analysis.instance.scope + context.state.scope !== context.state.analysis.instance.scope || + grand_parent.type !== 'VariableDeclaration' || + grand_parent.kind !== 'const' ) { - e.props_invalid_placement(node, rune); + e.props_id_invalid_placement(node); } if (node.arguments.length > 0) { @@ -95,6 +99,7 @@ export function CallExpression(node, context) { context.state.analysis.props_id = parent.id; break; + } case '$state': case '$state.raw': From 179335c346a121d5605cbd32c26f0d2ff393756c Mon Sep 17 00:00:00 2001 From: adiguba Date: Mon, 3 Feb 2025 22:31:01 +0100 Subject: [PATCH 05/18] rename $.create_uid() into $.props_id() --- .../compiler/phases/3-transform/client/transform-client.js | 2 +- .../compiler/phases/3-transform/server/transform-server.js | 2 +- packages/svelte/src/internal/client/dom/template.js | 2 +- packages/svelte/src/internal/client/index.js | 2 +- packages/svelte/src/internal/server/index.js | 6 +++--- 5 files changed, 7 insertions(+), 7 deletions(-) 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 bb1d49080d8d..2e6307a4b7a6 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 @@ -564,7 +564,7 @@ export function client_component(analysis, options) { 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('$.create_uid'))); + component_block.body.unshift(b.const(analysis.props_id, b.call('$.props_id'))); } if (state.events.size > 0) { 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 ca965245d045..df3d831d3cc3 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 @@ -247,7 +247,7 @@ export function server_component(analysis, options) { 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('$.create_uid', b.id('$$payload'))) + b.const(analysis.props_id, b.call('$.props_id', b.id('$$payload'))) ); } diff --git a/packages/svelte/src/internal/client/dom/template.js b/packages/svelte/src/internal/client/dom/template.js index 47b256c9d152..5e3f9a960e77 100644 --- a/packages/svelte/src/internal/client/dom/template.js +++ b/packages/svelte/src/internal/client/dom/template.js @@ -255,7 +255,7 @@ let NEXT_UID = 100; /** * Create (or hydrate) an unique UID for the component instance. */ -export function create_uid() { +export function props_id() { let uid; if ( hydrating && diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 43ed85a90f3d..8eaa5d66e1cb 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -97,7 +97,7 @@ export { template, template_with_script, text, - create_uid + props_id } 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 fe9ca2b5dce8..8ad213e8135a 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -85,7 +85,7 @@ export function element(payload, tag, attributes_fn = noop, children_fn = noop) */ export let on_destroy = []; -function create_uid_generator() { +function props_id_generator() { let uid = 100; return () => 's' + uid++; } @@ -104,7 +104,7 @@ export function render(component, options = {}) { out: '', css: new Set(), head: { title: '', out: '' }, - uid: options.uid ?? create_uid_generator() + uid: options.uid ?? props_id_generator() }; const prev_on_destroy = on_destroy; @@ -543,7 +543,7 @@ export function once(get_value) { * @param {Payload} payload * @returns {string} */ -export function create_uid(payload) { +export function props_id(payload) { const UID = payload.uid(); payload.out += ''; return UID; From 7b8b0959c3161522ef6da96a929bf6075df8d5d1 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 5 Feb 2025 14:24:49 -0500 Subject: [PATCH 06/18] fix message --- documentation/docs/98-reference/.generated/compile-errors.md | 2 +- packages/svelte/messages/compile-errors/script.md | 2 +- packages/svelte/src/compiler/errors.js | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/documentation/docs/98-reference/.generated/compile-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md index ca2a3cff323e..a4ecbb31d569 100644 --- a/documentation/docs/98-reference/.generated/compile-errors.md +++ b/documentation/docs/98-reference/.generated/compile-errors.md @@ -579,7 +579,7 @@ Cannot use `%rune%()` more than once ### props_id_invalid_placement ``` -`$props.id()` can only be affected at the top level of components as a const declaration initializer +`$props.id()` can only be used at the top level of components as a variable declaration initializer ``` ### props_illegal_name diff --git a/packages/svelte/messages/compile-errors/script.md b/packages/svelte/messages/compile-errors/script.md index 9837d7150759..795c0b007dca 100644 --- a/packages/svelte/messages/compile-errors/script.md +++ b/packages/svelte/messages/compile-errors/script.md @@ -124,7 +124,7 @@ This turned out to be buggy and unpredictable, particularly when working with de ## props_id_invalid_placement -> `$props.id()` can only be affected at the top level of components as a const declaration initializer +> `$props.id()` can only be used at the top level of components as a variable declaration initializer ## props_illegal_name diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index 557d1d16848d..93eeee539cc3 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -289,12 +289,12 @@ export function props_duplicate(node, rune) { } /** - * `$props.id()` can only be affected at the top level of components as a const declaration initializer + * `$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 affected at the top level of components as a const declaration initializer\nhttps://svelte.dev/e/props_id_invalid_placement`); + 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`); } /** From e9ab93257568a4120c7c3600f49f102929cb12dc Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 5 Feb 2025 14:45:30 -0500 Subject: [PATCH 07/18] relax const requirement, validate assignments instead --- .../src/compiler/phases/2-analyze/visitors/CallExpression.js | 5 ++--- .../src/compiler/phases/2-analyze/visitors/shared/utils.js | 4 ++++ 2 files changed, 6 insertions(+), 3 deletions(-) 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 e59c1ccf1a3a..8132fe91a170 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js @@ -1,5 +1,5 @@ /** @import { ArrowFunctionExpression, CallExpression, Expression, FunctionDeclaration, FunctionExpression, Identifier, VariableDeclarator } from 'estree' */ -/** @import { AST } from '#compiler' */ +/** @import { AST, Binding } from '#compiler' */ /** @import { Context } from '../types' */ import { get_rune } from '../../scope.js'; import * as e from '../../../errors.js'; @@ -86,8 +86,7 @@ export function CallExpression(node, context) { parent.id.type !== 'Identifier' || context.state.ast_type !== 'instance' || context.state.scope !== context.state.analysis.instance.scope || - grand_parent.type !== 'VariableDeclaration' || - grand_parent.kind !== 'const' + grand_parent.type !== 'VariableDeclaration' ) { e.props_id_invalid_placement(node); } 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 e265637c404d..5a550fda4b7c 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,6 +25,10 @@ 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); } From 6415f8ff4977421099f96385ffddd81973fbd214 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 5 Feb 2025 14:48:15 -0500 Subject: [PATCH 08/18] oops --- .../src/compiler/phases/2-analyze/visitors/CallExpression.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 8132fe91a170..ce520cc98055 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js @@ -1,5 +1,5 @@ /** @import { ArrowFunctionExpression, CallExpression, Expression, FunctionDeclaration, FunctionExpression, Identifier, VariableDeclarator } from 'estree' */ -/** @import { AST, Binding } from '#compiler' */ +/** @import { AST } from '#compiler' */ /** @import { Context } from '../types' */ import { get_rune } from '../../scope.js'; import * as e from '../../../errors.js'; From fe2f521d5f9afcbe4033b792b611a4e8c6b3b822 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 5 Feb 2025 15:02:23 -0500 Subject: [PATCH 09/18] simplify --- packages/svelte/src/internal/client/dom/template.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/template.js b/packages/svelte/src/internal/client/dom/template.js index 5e3f9a960e77..bdb08c856b43 100644 --- a/packages/svelte/src/internal/client/dom/template.js +++ b/packages/svelte/src/internal/client/dom/template.js @@ -256,17 +256,16 @@ let NEXT_UID = 100; * Create (or hydrate) an unique UID for the component instance. */ export function props_id() { - let uid; if ( hydrating && hydrate_node && - hydrate_node.nodeType === Node.COMMENT_NODE && + hydrate_node.nodeType === 8 && hydrate_node.textContent?.startsWith('#s') ) { - uid = hydrate_node.textContent.substring(1); + const id = hydrate_node.textContent.substring(1); hydrate_next(); - } else { - uid = 'c' + NEXT_UID++; + return id; } - return uid; + + return 'c' + NEXT_UID++; } From 3647ce3f1866e01d3a3e91e18fde3cc2fc344e3e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 5 Feb 2025 15:02:42 -0500 Subject: [PATCH 10/18] non-constants should be lowercased --- packages/svelte/src/internal/client/dom/template.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/template.js b/packages/svelte/src/internal/client/dom/template.js index bdb08c856b43..20a38e9cb5f3 100644 --- a/packages/svelte/src/internal/client/dom/template.js +++ b/packages/svelte/src/internal/client/dom/template.js @@ -250,7 +250,7 @@ export function append(anchor, dom) { anchor.before(/** @type {Node} */ (dom)); } -let NEXT_UID = 100; +let uid = 100; /** * Create (or hydrate) an unique UID for the component instance. @@ -267,5 +267,5 @@ export function props_id() { return id; } - return 'c' + NEXT_UID++; + return 'c' + uid++; } From 64768d27e731e082d75f08a2d04e7c313650d5c1 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 5 Feb 2025 15:06:19 -0500 Subject: [PATCH 11/18] ditto --- packages/svelte/src/internal/server/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index 8ad213e8135a..ccfb8834024b 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -544,9 +544,9 @@ export function once(get_value) { * @returns {string} */ export function props_id(payload) { - const UID = payload.uid(); - payload.out += ''; - return UID; + const uid = payload.uid(); + payload.out += ''; + return uid; } export { attr, clsx }; From e487bccbc783b2144071b7e4e99701204476659e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 5 Feb 2025 15:08:44 -0500 Subject: [PATCH 12/18] start at 1 --- packages/svelte/src/internal/client/dom/template.js | 2 +- packages/svelte/src/internal/server/index.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/template.js b/packages/svelte/src/internal/client/dom/template.js index 20a38e9cb5f3..3e4f45aba862 100644 --- a/packages/svelte/src/internal/client/dom/template.js +++ b/packages/svelte/src/internal/client/dom/template.js @@ -250,7 +250,7 @@ export function append(anchor, dom) { anchor.before(/** @type {Node} */ (dom)); } -let uid = 100; +let uid = 1; /** * Create (or hydrate) an unique UID for the component instance. diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index ccfb8834024b..c4e5d318dcd0 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -86,7 +86,7 @@ export function element(payload, tag, attributes_fn = noop, children_fn = noop) export let on_destroy = []; function props_id_generator() { - let uid = 100; + let uid = 1; return () => 's' + uid++; } From 21cd0a2d4a5efe684b1cc11397c9769a554b89e1 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 5 Feb 2025 15:20:54 -0500 Subject: [PATCH 13/18] add docs --- packages/svelte/src/ambient.d.ts | 7 +++++++ packages/svelte/types/index.d.ts | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/packages/svelte/src/ambient.d.ts b/packages/svelte/src/ambient.d.ts index 7919a61b1790..a5e181bd01ba 100644 --- a/packages/svelte/src/ambient.d.ts +++ b/packages/svelte/src/ambient.d.ts @@ -339,6 +339,13 @@ declare namespace $effect { declare function $props(): any; declare namespace $props { + /** + * Generates an ID that is unique to the current component instance. When hydrating a server-rendered a component, + * the value will be consistent between server and client. + * + * This is useful for linking elements via attributes like `for` and `aria-labelledby`. + * @since 5.20.0 + */ export function id(): string; // prevent intellisense from being unhelpful diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 60e76d27c239..15710ce14660 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -2995,6 +2995,13 @@ declare namespace $effect { declare function $props(): any; declare namespace $props { + /** + * Generates an ID that is unique to the current component instance. When hydrating a server-rendered a component, + * the value will be consistent between server and client. + * + * This is useful for linking elements via attributes like `for` and `aria-labelledby`. + * @since 5.20.0 + */ export function id(): string; // prevent intellisense from being unhelpful From 9d7b9d665a69af21f7f5692d6c4ee055cb26eee2 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 5 Feb 2025 15:22:54 -0500 Subject: [PATCH 14/18] changeset --- .changeset/hip-singers-vanish.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/hip-singers-vanish.md diff --git a/.changeset/hip-singers-vanish.md b/.changeset/hip-singers-vanish.md new file mode 100644 index 000000000000..9dce4d98a8db --- /dev/null +++ b/.changeset/hip-singers-vanish.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: SSR-safe ID generation with `$props.id()` From 0934e36092c6a0e30fe78602eb79508b7a91fff4 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 5 Feb 2025 15:31:23 -0500 Subject: [PATCH 15/18] add test --- .../samples/props-id/Child.svelte | 5 ++ .../runtime-runes/samples/props-id/_config.js | 61 +++++++++++++++++++ .../samples/props-id/main.svelte | 19 ++++++ 3 files changed, 85 insertions(+) create mode 100644 packages/svelte/tests/runtime-runes/samples/props-id/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/props-id/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/props-id/main.svelte diff --git a/packages/svelte/tests/runtime-runes/samples/props-id/Child.svelte b/packages/svelte/tests/runtime-runes/samples/props-id/Child.svelte new file mode 100644 index 000000000000..ad8bbd6f01ff --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-id/Child.svelte @@ -0,0 +1,5 @@ + + +

{id}

diff --git a/packages/svelte/tests/runtime-runes/samples/props-id/_config.js b/packages/svelte/tests/runtime-runes/samples/props-id/_config.js new file mode 100644 index 000000000000..9d91b98e0fa6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-id/_config.js @@ -0,0 +1,61 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test({ assert, target, variant }) { + if (variant === 'dom') { + assert.htmlEqual( + target.innerHTML, + ` + +

c1

+

c2

+

c3

+

c4

+ ` + ); + } else { + assert.htmlEqual( + target.innerHTML, + ` + +

s1

+

s2

+

s3

+

s4

+ ` + ); + } + + let button = target.querySelector('button'); + flushSync(() => button?.click()); + + if (variant === 'dom') { + assert.htmlEqual( + target.innerHTML, + ` + +

c1

+

c2

+

c3

+

c4

+

c5

+ ` + ); + } else { + // `c6` because this runs after the `dom` tests + // (slightly brittle but good enough for now) + assert.htmlEqual( + target.innerHTML, + ` + +

s1

+

s2

+

s3

+

s4

+

c6

+ ` + ); + } + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/props-id/main.svelte b/packages/svelte/tests/runtime-runes/samples/props-id/main.svelte new file mode 100644 index 000000000000..646bb2ebdefe --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-id/main.svelte @@ -0,0 +1,19 @@ + + + + +

{id}

+ + + + + +{#if show} + +{/if} From adbcbd7903e9509f79bce9bd6003f68b0d39f86b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 5 Feb 2025 15:57:27 -0500 Subject: [PATCH 16/18] add docs --- documentation/docs/02-runes/05-$props.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/documentation/docs/02-runes/05-$props.md b/documentation/docs/02-runes/05-$props.md index 4b1775bf5a61..dbb25d19670e 100644 --- a/documentation/docs/02-runes/05-$props.md +++ b/documentation/docs/02-runes/05-$props.md @@ -199,3 +199,10 @@ You can, of course, separate the type declaration from the annotation: > [!NOTE] Interfaces for native DOM elements are provided in the `svelte/elements` module (see [Typing wrapper components](typescript#Typing-wrapper-components)) Adding types is recommended, as it ensures that people using your component can easily discover which props they should provide. + + +## `$props.id()` + +This rune, added in version 5.20.0, generates an ID that is unique to the current component instance. When hydrating a server-rendered a component, the value will be consistent between server and client. + +This is useful for linking elements via attributes like `for` and `aria-labelledby`. From 0311c3b309b7c69ca789cc6275a29d896e25ed03 Mon Sep 17 00:00:00 2001 From: adiguba Date: Thu, 6 Feb 2025 14:42:25 +0100 Subject: [PATCH 17/18] doc : add code example --- documentation/docs/02-runes/05-$props.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/documentation/docs/02-runes/05-$props.md b/documentation/docs/02-runes/05-$props.md index dbb25d19670e..3f7164c18892 100644 --- a/documentation/docs/02-runes/05-$props.md +++ b/documentation/docs/02-runes/05-$props.md @@ -206,3 +206,17 @@ Adding types is recommended, as it ensures that people using your component can This rune, added in version 5.20.0, generates an ID that is unique to the current component instance. When hydrating a server-rendered a component, the value will be consistent between server and client. This is useful for linking elements via attributes like `for` and `aria-labelledby`. + +```svelte + + +
+ + + + + +
+``` \ No newline at end of file From d92e8aefabe5402972f35b248aeb224da3745ea1 Mon Sep 17 00:00:00 2001 From: adiguba Date: Fri, 7 Feb 2025 22:04:21 +0100 Subject: [PATCH 18/18] fix type reported by bennymi --- documentation/docs/02-runes/05-$props.md | 2 +- packages/svelte/src/ambient.d.ts | 2 +- packages/svelte/types/index.d.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/documentation/docs/02-runes/05-$props.md b/documentation/docs/02-runes/05-$props.md index 3f7164c18892..f300fb239d77 100644 --- a/documentation/docs/02-runes/05-$props.md +++ b/documentation/docs/02-runes/05-$props.md @@ -203,7 +203,7 @@ Adding types is recommended, as it ensures that people using your component can ## `$props.id()` -This rune, added in version 5.20.0, generates an ID that is unique to the current component instance. When hydrating a server-rendered a component, the value will be consistent between server and client. +This rune, added in version 5.20.0, generates an ID that is unique to the current component instance. When hydrating a server-rendered component, the value will be consistent between server and client. This is useful for linking elements via attributes like `for` and `aria-labelledby`. diff --git a/packages/svelte/src/ambient.d.ts b/packages/svelte/src/ambient.d.ts index a5e181bd01ba..a1484718cc77 100644 --- a/packages/svelte/src/ambient.d.ts +++ b/packages/svelte/src/ambient.d.ts @@ -340,7 +340,7 @@ declare function $props(): any; declare namespace $props { /** - * Generates an ID that is unique to the current component instance. When hydrating a server-rendered a component, + * Generates an ID that is unique to the current component instance. When hydrating a server-rendered component, * the value will be consistent between server and client. * * This is useful for linking elements via attributes like `for` and `aria-labelledby`. diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 15710ce14660..77d78477ee93 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -2996,7 +2996,7 @@ declare function $props(): any; declare namespace $props { /** - * Generates an ID that is unique to the current component instance. When hydrating a server-rendered a component, + * Generates an ID that is unique to the current component instance. When hydrating a server-rendered component, * the value will be consistent between server and client. * * This is useful for linking elements via attributes like `for` and `aria-labelledby`.