diff --git a/documentation/docs/98-reference/.generated/compile-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md index 2fef3bd45d..369901fe66 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 0aa6fbed90..ebc53b4ba2 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 4e5b51b7af..7919a61b17 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 53a6ac6849..25528d30cc 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 624c3a540e..4894d0a45f 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 0a6b3f3ee5..38b6cdfd0e 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 498a63232a..79dccd5a7c 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 2a4878c056..bb1d49080d 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 afb90bbec7..31e712cdcc 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 00634f229e..9214a13c94 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 8511aa03ad..ca965245d0 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 31de811ac7..c4c31d7eb3 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 d4c10c9940..abe2b115de 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 e8e1bc224c..d4d106d56d 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 5356468c24..60e76d27c2 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