From 08e2cf25b03707f4ca6e44a1e6ca0480190c14a1 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 6 Dec 2024 16:08:58 -0500 Subject: [PATCH 01/33] chore: fix sandbox (#14596) --- playgrounds/sandbox/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playgrounds/sandbox/package.json b/playgrounds/sandbox/package.json index 944e79621e0d..654a517c9fb6 100644 --- a/playgrounds/sandbox/package.json +++ b/playgrounds/sandbox/package.json @@ -6,7 +6,7 @@ "scripts": { "prepare": "node scripts/create-app-svelte.js", "dev": "vite --host", - "ssr": "node ./ssr-dev.js", + "ssr": "node --conditions=development ./ssr-dev.js", "build": "vite build --outDir dist/client && vite build --outDir dist/server --ssr ssr-prod.js", "prod": "npm run build && node dist/server/ssr-prod", "preview": "vite preview" From 1a0b822f4826b82454962a94c072519530b7c126 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 6 Dec 2024 17:41:41 -0500 Subject: [PATCH 02/33] fix: always run `if` block code the first time (#14597) * fix: always run `if` block code the first time * fix --- .changeset/rare-cheetahs-laugh.md | 5 +++++ .../svelte/src/internal/client/dom/blocks/if.js | 8 ++++---- .../samples/if-block-mismatch-2/_config.js | 15 +++++++++++++++ .../samples/if-block-mismatch-2/_expected.html | 1 + .../samples/if-block-mismatch-2/main.svelte | 13 +++++++++++++ .../samples/if-block-mismatch/_expected.html | 1 + .../samples/if-block-mismatch/main.svelte | 2 +- 7 files changed, 40 insertions(+), 5 deletions(-) create mode 100644 .changeset/rare-cheetahs-laugh.md create mode 100644 packages/svelte/tests/hydration/samples/if-block-mismatch-2/_config.js create mode 100644 packages/svelte/tests/hydration/samples/if-block-mismatch-2/_expected.html create mode 100644 packages/svelte/tests/hydration/samples/if-block-mismatch-2/main.svelte create mode 100644 packages/svelte/tests/hydration/samples/if-block-mismatch/_expected.html diff --git a/.changeset/rare-cheetahs-laugh.md b/.changeset/rare-cheetahs-laugh.md new file mode 100644 index 000000000000..2637b50b3c38 --- /dev/null +++ b/.changeset/rare-cheetahs-laugh.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: always run `if` block code the first time diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js index 6a880f28bc98..36790c05c135 100644 --- a/packages/svelte/src/internal/client/dom/blocks/if.js +++ b/packages/svelte/src/internal/client/dom/blocks/if.js @@ -9,7 +9,7 @@ import { set_hydrating } from '../hydration.js'; import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js'; -import { HYDRATION_START_ELSE } from '../../../../constants.js'; +import { HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js'; /** * @param {TemplateNode} node @@ -30,8 +30,8 @@ export function if_block(node, fn, elseif = false) { /** @type {Effect | null} */ var alternate_effect = null; - /** @type {boolean | null} */ - var condition = null; + /** @type {UNINITIALIZED | boolean | null} */ + var condition = UNINITIALIZED; var flags = elseif ? EFFECT_TRANSPARENT : 0; @@ -54,7 +54,7 @@ export function if_block(node, fn, elseif = false) { if (hydrating) { const is_else = /** @type {Comment} */ (anchor).data === HYDRATION_START_ELSE; - if (condition === is_else) { + if (!!condition === is_else) { // Hydration mismatch: remove everything inside the anchor and start fresh. // This could happen with `{#if browser}...{/if}`, for example anchor = remove_nodes(); diff --git a/packages/svelte/tests/hydration/samples/if-block-mismatch-2/_config.js b/packages/svelte/tests/hydration/samples/if-block-mismatch-2/_config.js new file mode 100644 index 000000000000..ffde9ee303b5 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/if-block-mismatch-2/_config.js @@ -0,0 +1,15 @@ +import { test } from '../../test'; + +// even {#if true} or {#if false} should be kept as an if block, because it could be {#if browser} originally, +// which is then different between client and server. +export default test({ + server_props: { + condition: true + }, + + props: { + condition: false + }, + + trim_whitespace: false +}); diff --git a/packages/svelte/tests/hydration/samples/if-block-mismatch-2/_expected.html b/packages/svelte/tests/hydration/samples/if-block-mismatch-2/_expected.html new file mode 100644 index 000000000000..08a3809de9cc --- /dev/null +++ b/packages/svelte/tests/hydration/samples/if-block-mismatch-2/_expected.html @@ -0,0 +1 @@ +
hello diff --git a/packages/svelte/tests/hydration/samples/if-block-mismatch-2/main.svelte b/packages/svelte/tests/hydration/samples/if-block-mismatch-2/main.svelte new file mode 100644 index 000000000000..3136406698b3 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/if-block-mismatch-2/main.svelte @@ -0,0 +1,13 @@ + + +{#if condition} + +{/if} + +
+ +
+ +hello diff --git a/packages/svelte/tests/hydration/samples/if-block-mismatch/_expected.html b/packages/svelte/tests/hydration/samples/if-block-mismatch/_expected.html new file mode 100644 index 000000000000..79cf2cf35f0f --- /dev/null +++ b/packages/svelte/tests/hydration/samples/if-block-mismatch/_expected.html @@ -0,0 +1 @@ +

foo

diff --git a/packages/svelte/tests/hydration/samples/if-block-mismatch/main.svelte b/packages/svelte/tests/hydration/samples/if-block-mismatch/main.svelte index c6799c5f95fc..552c43410162 100644 --- a/packages/svelte/tests/hydration/samples/if-block-mismatch/main.svelte +++ b/packages/svelte/tests/hydration/samples/if-block-mismatch/main.svelte @@ -2,7 +2,7 @@ let { condition } = $props(); -{#if true} +{#if condition}

foo

{:else}

bar

From 5771b455c0caf860bb063499feb2100acad4fbd5 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Sun, 8 Dec 2024 12:28:37 +0000 Subject: [PATCH 03/33] feat: add support for bind getter/setters (#14307) * feat: add support for bind getters/setters * different direction * oops * oops * build * add changeset and tests * move validation * add comment * build * bind:group error * simpler to just keep it as a SequenceExpression * fix * lint * fix * move validation to visitor * fix * no longer needed * fix * parser changes are no longer needed * simplify * simplify * update messages * docs --------- Co-authored-by: Rich Harris Co-authored-by: Simon Holthausen --- .changeset/slimy-donkeys-hang.md | 5 + .../docs/03-template-syntax/11-bind.md | 24 ++ .../98-reference/.generated/compile-errors.md | 14 +- .../messages/compile-errors/template.md | 10 +- packages/svelte/src/compiler/errors.js | 23 +- .../2-analyze/visitors/BindDirective.js | 214 ++++++++++-------- .../client/visitors/BindDirective.js | 74 +++--- .../client/visitors/RegularElement.js | 10 +- .../client/visitors/shared/component.js | 113 +++++---- .../client/visitors/shared/utils.js | 12 +- .../server/visitors/shared/component.js | 52 +++-- .../server/visitors/shared/element.js | 18 +- .../src/compiler/types/legacy-nodes.d.ts | 5 +- .../svelte/src/compiler/types/template.d.ts | 5 +- .../samples/bind-getter-setter-2/Child.svelte | 11 + .../samples/bind-getter-setter-2/_config.js | 9 + .../samples/bind-getter-setter-2/main.svelte | 11 + .../samples/bind-getter-setter/Child.svelte | 12 + .../samples/bind-getter-setter/_config.js | 20 ++ .../samples/bind-getter-setter/main.svelte | 16 ++ .../bind_group_invalid_expression/errors.json | 14 ++ .../input.svelte | 12 + packages/svelte/types/index.d.ts | 4 +- 23 files changed, 471 insertions(+), 217 deletions(-) create mode 100644 .changeset/slimy-donkeys-hang.md create mode 100644 packages/svelte/tests/runtime-runes/samples/bind-getter-setter-2/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/bind-getter-setter-2/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/bind-getter-setter-2/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/bind-getter-setter/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/bind-getter-setter/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/bind-getter-setter/main.svelte create mode 100644 packages/svelte/tests/validator/samples/bind_group_invalid_expression/errors.json create mode 100644 packages/svelte/tests/validator/samples/bind_group_invalid_expression/input.svelte diff --git a/.changeset/slimy-donkeys-hang.md b/.changeset/slimy-donkeys-hang.md new file mode 100644 index 000000000000..d63141660eed --- /dev/null +++ b/.changeset/slimy-donkeys-hang.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +feat: add support for bind getters/setters diff --git a/documentation/docs/03-template-syntax/11-bind.md b/documentation/docs/03-template-syntax/11-bind.md index fe3cf727e285..90046c8c456d 100644 --- a/documentation/docs/03-template-syntax/11-bind.md +++ b/documentation/docs/03-template-syntax/11-bind.md @@ -12,10 +12,34 @@ The general syntax is `bind:property={expression}`, where `expression` is an _lv ``` + Svelte creates an event listener that updates the bound value. If an element already has a listener for the same event, that listener will be fired before the bound value is updated. Most bindings are _two-way_, meaning that changes to the value will affect the element and vice versa. A few bindings are _readonly_, meaning that changing their value will have no effect on the element. +## Function bindings + +You can also use `bind:property={get, set}`, where `get` and `set` are functions, allowing you to perform validation and transformation: + +```svelte + value, + (v) => value = v.toLowerCase()} +/> +``` + +In the case of readonly bindings like [dimension bindings](#Dimensions), the `get` value should be `null`: + +```svelte +
...
+``` + +> [!NOTE] +> Function bindings are available in Svelte 5.9.0 and newer. + ## `` A `bind:value` directive on an `` element binds the input's `value` property: diff --git a/documentation/docs/98-reference/.generated/compile-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md index 3bd162d8d74d..d726d25fa188 100644 --- a/documentation/docs/98-reference/.generated/compile-errors.md +++ b/documentation/docs/98-reference/.generated/compile-errors.md @@ -78,10 +78,16 @@ Sequence expressions are not allowed as attribute/directive values in runes mode Attribute values containing `{...}` must be enclosed in quote marks, unless the value only contains the expression ``` +### bind_group_invalid_expression + +``` +`bind:group` can only bind to an Identifier or MemberExpression +``` + ### bind_invalid_expression ``` -Can only bind to an Identifier or MemberExpression +Can only bind to an Identifier or MemberExpression or a `{get, set}` pair ``` ### bind_invalid_name @@ -94,6 +100,12 @@ Can only bind to an Identifier or MemberExpression `bind:%name%` is not a valid binding. %explanation% ``` +### bind_invalid_parens + +``` +`bind:%name%={get, set}` must not have surrounding parentheses +``` + ### bind_invalid_target ``` diff --git a/packages/svelte/messages/compile-errors/template.md b/packages/svelte/messages/compile-errors/template.md index 9621a6457ba9..02961b61fccc 100644 --- a/packages/svelte/messages/compile-errors/template.md +++ b/packages/svelte/messages/compile-errors/template.md @@ -50,9 +50,13 @@ > Attribute values containing `{...}` must be enclosed in quote marks, unless the value only contains the expression +## bind_group_invalid_expression + +> `bind:group` can only bind to an Identifier or MemberExpression + ## bind_invalid_expression -> Can only bind to an Identifier or MemberExpression +> Can only bind to an Identifier or MemberExpression or a `{get, set}` pair ## bind_invalid_name @@ -60,6 +64,10 @@ > `bind:%name%` is not a valid binding. %explanation% +## bind_invalid_parens + +> `bind:%name%={get, set}` must not have surrounding parentheses + ## bind_invalid_target > `bind:%name%` can only be used with %elements% diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index 901ea1983ea7..1a4525ef5cb9 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -716,12 +716,21 @@ export function attribute_unquoted_sequence(node) { } /** - * Can only bind to an Identifier or MemberExpression + * `bind:group` can only bind to an Identifier or MemberExpression + * @param {null | number | NodeLike} node + * @returns {never} + */ +export function bind_group_invalid_expression(node) { + e(node, "bind_group_invalid_expression", "`bind:group` can only bind to an Identifier or MemberExpression"); +} + +/** + * Can only bind to an Identifier or MemberExpression or a `{get, set}` pair * @param {null | number | NodeLike} node * @returns {never} */ export function bind_invalid_expression(node) { - e(node, "bind_invalid_expression", "Can only bind to an Identifier or MemberExpression"); + e(node, "bind_invalid_expression", "Can only bind to an Identifier or MemberExpression or a `{get, set}` pair"); } /** @@ -735,6 +744,16 @@ export function bind_invalid_name(node, name, explanation) { e(node, "bind_invalid_name", explanation ? `\`bind:${name}\` is not a valid binding. ${explanation}` : `\`bind:${name}\` is not a valid binding`); } +/** + * `bind:%name%={get, set}` must not have surrounding parentheses + * @param {null | number | NodeLike} node + * @param {string} name + * @returns {never} + */ +export function bind_invalid_parens(node, name) { + e(node, "bind_invalid_parens", `\`bind:${name}={get, set}\` must not have surrounding parentheses`); +} + /** * `bind:%name%` can only be used with %elements% * @param {null | number | NodeLike} node diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js index 5b56d9ddac38..b06236538008 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js @@ -17,102 +17,6 @@ import { is_content_editable_binding, is_svg } from '../../../../utils.js'; * @param {Context} context */ export function BindDirective(node, context) { - validate_no_const_assignment(node, node.expression, context.state.scope, true); - - const assignee = node.expression; - const left = object(assignee); - - if (left === null) { - e.bind_invalid_expression(node); - } - - const binding = context.state.scope.get(left.name); - - if (assignee.type === 'Identifier') { - // reassignment - if ( - node.name !== 'this' && // bind:this also works for regular variables - (!binding || - (binding.kind !== 'state' && - binding.kind !== 'raw_state' && - binding.kind !== 'prop' && - binding.kind !== 'bindable_prop' && - binding.kind !== 'each' && - binding.kind !== 'store_sub' && - !binding.updated)) // TODO wut? - ) { - e.bind_invalid_value(node.expression); - } - - if (context.state.analysis.runes && binding?.kind === 'each') { - e.each_item_invalid_assignment(node); - } - - if (binding?.kind === 'snippet') { - e.snippet_parameter_assignment(node); - } - } - - if (node.name === 'group') { - if (!binding) { - throw new Error('Cannot find declaration for bind:group'); - } - - // Traverse the path upwards and find all EachBlocks who are (indirectly) contributing to bind:group, - // i.e. one of their declarations is referenced in the binding. This allows group bindings to work - // correctly when referencing a variable declared in an EachBlock by using the index of the each block - // entries as keys. - const each_blocks = []; - const [keypath, expression_ids] = extract_all_identifiers_from_expression(node.expression); - let ids = expression_ids; - - let i = context.path.length; - while (i--) { - const parent = context.path[i]; - - if (parent.type === 'EachBlock') { - const references = ids.filter((id) => parent.metadata.declarations.has(id.name)); - - if (references.length > 0) { - parent.metadata.contains_group_binding = true; - - each_blocks.push(parent); - ids = ids.filter((id) => !references.includes(id)); - ids.push(...extract_all_identifiers_from_expression(parent.expression)[1]); - } - } - } - - // The identifiers that make up the binding expression form they key for the binding group. - // If the same identifiers in the same order are used in another bind:group, they will be in the same group. - // (there's an edge case where `bind:group={a[i]}` will be in a different group than `bind:group={a[j]}` even when i == j, - // but this is a limitation of the current static analysis we do; it also never worked in Svelte 4) - const bindings = expression_ids.map((id) => context.state.scope.get(id.name)); - let group_name; - - outer: for (const [[key, b], group] of context.state.analysis.binding_groups) { - if (b.length !== bindings.length || key !== keypath) continue; - for (let i = 0; i < bindings.length; i++) { - if (bindings[i] !== b[i]) continue outer; - } - group_name = group; - } - - if (!group_name) { - group_name = context.state.scope.root.unique('binding_group'); - context.state.analysis.binding_groups.set([keypath, bindings], group_name); - } - - node.metadata = { - binding_group_name: group_name, - parent_each_blocks: each_blocks - }; - } - - if (binding?.kind === 'each' && binding.metadata?.inside_rest) { - w.bind_invalid_each_rest(binding.node, binding.node.name); - } - const parent = context.path.at(-1); if ( @@ -218,5 +122,123 @@ export function BindDirective(node, context) { } } + // When dealing with bind getters/setters skip the specific binding validation + // Group bindings aren't supported for getter/setters so we don't need to handle + // the metadata + if (node.expression.type === 'SequenceExpression') { + if (node.name === 'group') { + e.bind_group_invalid_expression(node); + } + + let i = /** @type {number} */ (node.expression.start); + while (context.state.analysis.source[--i] !== '{') { + if (context.state.analysis.source[i] === '(') { + e.bind_invalid_parens(node, node.name); + } + } + + if (node.expression.expressions.length !== 2) { + e.bind_invalid_expression(node); + } + + return; + } + + validate_no_const_assignment(node, node.expression, context.state.scope, true); + + const assignee = node.expression; + const left = object(assignee); + + if (left === null) { + e.bind_invalid_expression(node); + } + + const binding = context.state.scope.get(left.name); + + if (assignee.type === 'Identifier') { + // reassignment + if ( + node.name !== 'this' && // bind:this also works for regular variables + (!binding || + (binding.kind !== 'state' && + binding.kind !== 'raw_state' && + binding.kind !== 'prop' && + binding.kind !== 'bindable_prop' && + binding.kind !== 'each' && + binding.kind !== 'store_sub' && + !binding.updated)) // TODO wut? + ) { + e.bind_invalid_value(node.expression); + } + + if (context.state.analysis.runes && binding?.kind === 'each') { + e.each_item_invalid_assignment(node); + } + + if (binding?.kind === 'snippet') { + e.snippet_parameter_assignment(node); + } + } + + if (node.name === 'group') { + if (!binding) { + throw new Error('Cannot find declaration for bind:group'); + } + + // Traverse the path upwards and find all EachBlocks who are (indirectly) contributing to bind:group, + // i.e. one of their declarations is referenced in the binding. This allows group bindings to work + // correctly when referencing a variable declared in an EachBlock by using the index of the each block + // entries as keys. + const each_blocks = []; + const [keypath, expression_ids] = extract_all_identifiers_from_expression(node.expression); + let ids = expression_ids; + + let i = context.path.length; + while (i--) { + const parent = context.path[i]; + + if (parent.type === 'EachBlock') { + const references = ids.filter((id) => parent.metadata.declarations.has(id.name)); + + if (references.length > 0) { + parent.metadata.contains_group_binding = true; + + each_blocks.push(parent); + ids = ids.filter((id) => !references.includes(id)); + ids.push(...extract_all_identifiers_from_expression(parent.expression)[1]); + } + } + } + + // The identifiers that make up the binding expression form they key for the binding group. + // If the same identifiers in the same order are used in another bind:group, they will be in the same group. + // (there's an edge case where `bind:group={a[i]}` will be in a different group than `bind:group={a[j]}` even when i == j, + // but this is a limitation of the current static analysis we do; it also never worked in Svelte 4) + const bindings = expression_ids.map((id) => context.state.scope.get(id.name)); + let group_name; + + outer: for (const [[key, b], group] of context.state.analysis.binding_groups) { + if (b.length !== bindings.length || key !== keypath) continue; + for (let i = 0; i < bindings.length; i++) { + if (bindings[i] !== b[i]) continue outer; + } + group_name = group; + } + + if (!group_name) { + group_name = context.state.scope.root.unique('binding_group'); + context.state.analysis.binding_groups.set([keypath, bindings], group_name); + } + + node.metadata = { + binding_group_name: group_name, + parent_each_blocks: each_blocks + }; + } + + if (binding?.kind === 'each' && binding.metadata?.inside_rest) { + w.bind_invalid_each_rest(binding.node, binding.node.name); + } + context.next(); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/BindDirective.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/BindDirective.js index 79969240c771..f129e059f2a7 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/BindDirective.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/BindDirective.js @@ -1,4 +1,4 @@ -/** @import { CallExpression, Expression, MemberExpression } from 'estree' */ +/** @import { CallExpression, Expression, MemberExpression, Pattern } from 'estree' */ /** @import { AST, SvelteNode } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import { dev, is_ignored } from '../../../../state.js'; @@ -13,41 +13,50 @@ import { build_bind_this, validate_binding } from './shared/utils.js'; * @param {ComponentContext} context */ export function BindDirective(node, context) { - const expression = node.expression; + const expression = /** @type {Expression} */ (context.visit(node.expression)); const property = binding_properties[node.name]; const parent = /** @type {SvelteNode} */ (context.path.at(-1)); - if ( - dev && - context.state.analysis.runes && - expression.type === 'MemberExpression' && - (node.name !== 'this' || - context.path.some( - ({ type }) => - type === 'IfBlock' || type === 'EachBlock' || type === 'AwaitBlock' || type === 'KeyBlock' - )) && - !is_ignored(node, 'binding_property_non_reactive') - ) { - validate_binding( - context.state, - node, - /**@type {MemberExpression} */ (context.visit(expression)) - ); - } + let get, set; - const get = b.thunk(/** @type {Expression} */ (context.visit(expression))); + if (expression.type === 'SequenceExpression') { + [get, set] = expression.expressions; + } else { + if ( + dev && + context.state.analysis.runes && + expression.type === 'MemberExpression' && + (node.name !== 'this' || + context.path.some( + ({ type }) => + type === 'IfBlock' || + type === 'EachBlock' || + type === 'AwaitBlock' || + type === 'KeyBlock' + )) && + !is_ignored(node, 'binding_property_non_reactive') + ) { + validate_binding(context.state, node, expression); + } - /** @type {Expression | undefined} */ - let set = b.unthunk( - b.arrow( - [b.id('$$value')], - /** @type {Expression} */ (context.visit(b.assignment('=', expression, b.id('$$value')))) - ) - ); + get = b.thunk(expression); - if (get === set) { - set = undefined; + /** @type {Expression | undefined} */ + set = b.unthunk( + b.arrow( + [b.id('$$value')], + /** @type {Expression} */ ( + context.visit( + b.assignment('=', /** @type {Pattern} */ (node.expression), b.id('$$value')) + ) + ) + ) + ); + + if (get === set) { + set = undefined; + } } /** @type {CallExpression} */ @@ -162,7 +171,7 @@ export function BindDirective(node, context) { break; case 'this': - call = build_bind_this(expression, context.state.node, context); + call = build_bind_this(node.expression, context.state.node, context); break; case 'textContent': @@ -213,10 +222,7 @@ export function BindDirective(node, context) { if (value !== undefined) { group_getter = b.thunk( - b.block([ - b.stmt(build_attribute_value(value, context).value), - b.return(/** @type {Expression} */ (context.visit(expression))) - ]) + b.block([b.stmt(build_attribute_value(value, context).value), b.return(expression)]) ); } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js index 3c0be589c363..2c2c287f1275 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js @@ -450,6 +450,11 @@ function setup_select_synchronization(value_binding, context) { if (context.state.analysis.runes) return; let bound = value_binding.expression; + + if (bound.type === 'SequenceExpression') { + return; + } + while (bound.type === 'MemberExpression') { bound = /** @type {Identifier | MemberExpression} */ (bound.object); } @@ -484,10 +489,7 @@ function setup_select_synchronization(value_binding, context) { b.call( '$.template_effect', b.thunk( - b.block([ - b.stmt(/** @type {Expression} */ (context.visit(value_binding.expression))), - b.stmt(invalidator) - ]) + b.block([b.stmt(/** @type {Expression} */ (context.visit(bound))), b.stmt(invalidator)]) ) ) ) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js index aa7be93cb57e..c94c1e1b0ec8 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js @@ -1,4 +1,4 @@ -/** @import { BlockStatement, Expression, ExpressionStatement, Identifier, MemberExpression, Property, Statement } from 'estree' */ +/** @import { BlockStatement, Expression, ExpressionStatement, Identifier, MemberExpression, Pattern, Property, SequenceExpression, Statement } from 'estree' */ /** @import { AST, TemplateNode } from '#compiler' */ /** @import { ComponentContext } from '../../types.js' */ import { dev, is_ignored } from '../../../../../state.js'; @@ -44,7 +44,7 @@ export function build_component(node, component_name, context, anchor = context. /** @type {Property[]} */ const custom_css_props = []; - /** @type {Identifier | MemberExpression | null} */ + /** @type {Identifier | MemberExpression | SequenceExpression | null} */ let bind_this = null; /** @type {ExpressionStatement[]} */ @@ -174,60 +174,83 @@ export function build_component(node, component_name, context, anchor = context. } else if (attribute.type === 'BindDirective') { const expression = /** @type {Expression} */ (context.visit(attribute.expression)); - if ( - dev && - expression.type === 'MemberExpression' && - context.state.analysis.runes && - !is_ignored(node, 'binding_property_non_reactive') - ) { - validate_binding(context.state, attribute, expression); + if (dev && attribute.name !== 'this' && attribute.expression.type !== 'SequenceExpression') { + const left = object(attribute.expression); + let binding; + + if (left?.type === 'Identifier') { + binding = context.state.scope.get(left.name); + } + + // Only run ownership addition on $state fields. + // Theoretically someone could create a `$state` while creating `$state.raw` or inside a `$derived.by`, + // but that feels so much of an edge case that it doesn't warrant a perf hit for the common case. + if (binding?.kind !== 'derived' && binding?.kind !== 'raw_state') { + binding_initializers.push( + b.stmt( + b.call( + b.id('$.add_owner_effect'), + b.thunk(expression), + b.id(component_name), + is_ignored(node, 'ownership_invalid_binding') && b.true + ) + ) + ); + } } - if (attribute.name === 'this') { - bind_this = attribute.expression; + if (expression.type === 'SequenceExpression') { + if (attribute.name === 'this') { + bind_this = attribute.expression; + } else { + const [get, set] = expression.expressions; + const get_id = b.id(context.state.scope.generate('bind_get')); + const set_id = b.id(context.state.scope.generate('bind_set')); + + context.state.init.push(b.var(get_id, get)); + context.state.init.push(b.var(set_id, set)); + + push_prop(b.get(attribute.name, [b.return(b.call(get_id))])); + push_prop(b.set(attribute.name, [b.stmt(b.call(set_id, b.id('$$value')))])); + } } else { - if (dev) { - const left = object(attribute.expression); - let binding; - if (left?.type === 'Identifier') { - binding = context.state.scope.get(left.name); - } - // Only run ownership addition on $state fields. - // Theoretically someone could create a `$state` while creating `$state.raw` or inside a `$derived.by`, - // but that feels so much of an edge case that it doesn't warrant a perf hit for the common case. - if (binding?.kind !== 'derived' && binding?.kind !== 'raw_state') { - binding_initializers.push( - b.stmt( - b.call( - b.id('$.add_owner_effect'), - b.thunk(expression), - b.id(component_name), - is_ignored(node, 'ownership_invalid_binding') && b.true - ) - ) + if ( + dev && + expression.type === 'MemberExpression' && + context.state.analysis.runes && + !is_ignored(node, 'binding_property_non_reactive') + ) { + validate_binding(context.state, attribute, expression); + } + + if (attribute.name === 'this') { + bind_this = attribute.expression; + } else { + const is_store_sub = + attribute.expression.type === 'Identifier' && + context.state.scope.get(attribute.expression.name)?.kind === 'store_sub'; + + // Delay prop pushes so bindings come at the end, to avoid spreads overwriting them + if (is_store_sub) { + push_prop( + b.get(attribute.name, [b.stmt(b.call('$.mark_store_binding')), b.return(expression)]), + true ); + } else { + push_prop(b.get(attribute.name, [b.return(expression)]), true); } - } - const is_store_sub = - attribute.expression.type === 'Identifier' && - context.state.scope.get(attribute.expression.name)?.kind === 'store_sub'; + const assignment = b.assignment( + '=', + /** @type {Pattern} */ (attribute.expression), + b.id('$$value') + ); - // Delay prop pushes so bindings come at the end, to avoid spreads overwriting them - if (is_store_sub) { push_prop( - b.get(attribute.name, [b.stmt(b.call('$.mark_store_binding')), b.return(expression)]), + b.set(attribute.name, [b.stmt(/** @type {Expression} */ (context.visit(assignment)))]), true ); - } else { - push_prop(b.get(attribute.name, [b.return(expression)]), true); } - - const assignment = b.assignment('=', attribute.expression, b.id('$$value')); - push_prop( - b.set(attribute.name, [b.stmt(/** @type {Expression} */ (context.visit(assignment)))]), - true - ); } } } 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 59beacbb0c30..11f76aa025e7 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 @@ -1,4 +1,4 @@ -/** @import { Expression, ExpressionStatement, Identifier, MemberExpression, Statement, Super } from 'estree' */ +/** @import { Expression, ExpressionStatement, Identifier, MemberExpression, SequenceExpression, Statement, Super } from 'estree' */ /** @import { AST, SvelteNode } from '#compiler' */ /** @import { ComponentClientTransformState } from '../../types' */ import { walk } from 'zimmerframe'; @@ -143,11 +143,16 @@ export function build_update_assignment(state, id, init, value, update) { /** * Serializes `bind:this` for components and elements. - * @param {Identifier | MemberExpression} expression + * @param {Identifier | MemberExpression | SequenceExpression} expression * @param {Expression} value * @param {import('zimmerframe').Context} context */ export function build_bind_this(expression, value, { state, visit }) { + if (expression.type === 'SequenceExpression') { + const [get, set] = /** @type {SequenceExpression} */ (visit(expression)).expressions; + return b.call('$.bind_this', value, set, get); + } + /** @type {Identifier[]} */ const ids = []; @@ -224,6 +229,9 @@ export function build_bind_this(expression, value, { state, visit }) { * @param {MemberExpression} expression */ export function validate_binding(state, binding, expression) { + if (binding.expression.type === 'SequenceExpression') { + return; + } // If we are referencing a $store.foo then we don't need to add validation const left = object(binding.expression); const left_binding = left && state.scope.get(left.name); diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js index 7cabfb06c527..0d0444433564 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js @@ -1,4 +1,4 @@ -/** @import { BlockStatement, Expression, Pattern, Property, Statement } from 'estree' */ +/** @import { BlockStatement, Expression, Pattern, Property, SequenceExpression, Statement } from 'estree' */ /** @import { AST, TemplateNode } from '#compiler' */ /** @import { ComponentContext } from '../../types.js' */ import { empty_comment, build_attribute_value } from './utils.js'; @@ -92,24 +92,38 @@ export function build_inline_component(node, expression, context) { const value = build_attribute_value(attribute.value, context, false, true); push_prop(b.prop('init', b.key(attribute.name), value)); } else if (attribute.type === 'BindDirective' && attribute.name !== 'this') { - // Delay prop pushes so bindings come at the end, to avoid spreads overwriting them - push_prop( - b.get(attribute.name, [ - b.return(/** @type {Expression} */ (context.visit(attribute.expression))) - ]), - true - ); - push_prop( - b.set(attribute.name, [ - b.stmt( - /** @type {Expression} */ ( - context.visit(b.assignment('=', attribute.expression, b.id('$$value'))) - ) - ), - b.stmt(b.assignment('=', b.id('$$settled'), b.false)) - ]), - true - ); + if (attribute.expression.type === 'SequenceExpression') { + const [get, set] = /** @type {SequenceExpression} */ (context.visit(attribute.expression)) + .expressions; + const get_id = b.id(context.state.scope.generate('bind_get')); + const set_id = b.id(context.state.scope.generate('bind_set')); + + context.state.init.push(b.var(get_id, get)); + context.state.init.push(b.var(set_id, set)); + + push_prop(b.get(attribute.name, [b.return(b.call(get_id))])); + push_prop(b.set(attribute.name, [b.stmt(b.call(set_id, b.id('$$value')))])); + } else { + // Delay prop pushes so bindings come at the end, to avoid spreads overwriting them + push_prop( + b.get(attribute.name, [ + b.return(/** @type {Expression} */ (context.visit(attribute.expression))) + ]), + true + ); + + push_prop( + b.set(attribute.name, [ + b.stmt( + /** @type {Expression} */ ( + context.visit(b.assignment('=', attribute.expression, b.id('$$value'))) + ) + ), + b.stmt(b.assignment('=', b.id('$$settled'), b.false)) + ]), + true + ); + } } } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js index 434447727b33..d626bb08db30 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js @@ -110,14 +110,17 @@ export function build_element_attributes(node, context) { const binding = binding_properties[attribute.name]; if (binding?.omit_in_ssr) continue; + let expression = /** @type {Expression} */ (context.visit(attribute.expression)); + + if (expression.type === 'SequenceExpression') { + expression = b.call(expression.expressions[0]); + } + if (is_content_editable_binding(attribute.name)) { - content = /** @type {Expression} */ (context.visit(attribute.expression)); + content = expression; } else if (attribute.name === 'value' && node.name === 'textarea') { - content = b.call( - '$.escape', - /** @type {Expression} */ (context.visit(attribute.expression)) - ); - } else if (attribute.name === 'group') { + content = b.call('$.escape', expression); + } else if (attribute.name === 'group' && attribute.expression.type !== 'SequenceExpression') { const value_attribute = /** @type {AST.Attribute | undefined} */ ( node.attributes.find((attr) => attr.type === 'Attribute' && attr.name === 'value') ); @@ -130,6 +133,7 @@ export function build_element_attributes(node, context) { is_text_attribute(attr) && attr.value[0].data === 'checkbox' ); + attributes.push( create_attribute('checked', -1, -1, [ { @@ -159,7 +163,7 @@ export function build_element_attributes(node, context) { type: 'ExpressionTag', start: -1, end: -1, - expression: attribute.expression, + expression, metadata: { expression: create_expression_metadata() } diff --git a/packages/svelte/src/compiler/types/legacy-nodes.d.ts b/packages/svelte/src/compiler/types/legacy-nodes.d.ts index 2bd5fbbfa6d2..0013f5c17a60 100644 --- a/packages/svelte/src/compiler/types/legacy-nodes.d.ts +++ b/packages/svelte/src/compiler/types/legacy-nodes.d.ts @@ -6,7 +6,8 @@ import type { Identifier, MemberExpression, ObjectExpression, - Pattern + Pattern, + SequenceExpression } from 'estree'; interface BaseNode { @@ -49,7 +50,7 @@ export interface LegacyBinding extends BaseNode { /** The 'x' in `bind:x` */ name: string; /** The y in `bind:x={y}` */ - expression: Identifier | MemberExpression; + expression: Identifier | MemberExpression | SequenceExpression; } export interface LegacyBody extends BaseElement { diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index a409cf570489..b8724f28dc94 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -14,7 +14,8 @@ import type { Pattern, Program, ChainExpression, - SimpleCallExpression + SimpleCallExpression, + SequenceExpression } from 'estree'; import type { Scope } from '../phases/scope'; @@ -187,7 +188,7 @@ export namespace AST { /** The 'x' in `bind:x` */ name: string; /** The y in `bind:x={y}` */ - expression: Identifier | MemberExpression; + expression: Identifier | MemberExpression | SequenceExpression; /** @internal */ metadata: { binding_group_name: Identifier; diff --git a/packages/svelte/tests/runtime-runes/samples/bind-getter-setter-2/Child.svelte b/packages/svelte/tests/runtime-runes/samples/bind-getter-setter-2/Child.svelte new file mode 100644 index 000000000000..0026309d449f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-getter-setter-2/Child.svelte @@ -0,0 +1,11 @@ + + +
div, v => div = v}>123
diff --git a/packages/svelte/tests/runtime-runes/samples/bind-getter-setter-2/_config.js b/packages/svelte/tests/runtime-runes/samples/bind-getter-setter-2/_config.js new file mode 100644 index 000000000000..1d51c8eead1e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-getter-setter-2/_config.js @@ -0,0 +1,9 @@ +import { test } from '../../test'; + +export default test({ + async test({ assert, target, logs }) { + assert.htmlEqual(target.innerHTML, `
123
`); + + assert.deepEqual(logs, ['123', '123']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/bind-getter-setter-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/bind-getter-setter-2/main.svelte new file mode 100644 index 000000000000..21646e745a4e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-getter-setter-2/main.svelte @@ -0,0 +1,11 @@ + + + child, v => child = v} /> diff --git a/packages/svelte/tests/runtime-runes/samples/bind-getter-setter/Child.svelte b/packages/svelte/tests/runtime-runes/samples/bind-getter-setter/Child.svelte new file mode 100644 index 000000000000..bea5849ec76b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-getter-setter/Child.svelte @@ -0,0 +1,12 @@ + + + a, + (v) => { + console.log('b', v); + a = v; + }} +/> diff --git a/packages/svelte/tests/runtime-runes/samples/bind-getter-setter/_config.js b/packages/svelte/tests/runtime-runes/samples/bind-getter-setter/_config.js new file mode 100644 index 000000000000..158d1e6f63e0 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-getter-setter/_config.js @@ -0,0 +1,20 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; +import { assert_ok } from '../../../suite'; + +export default test({ + async test({ assert, target, logs }) { + const input = target.querySelector('input'); + + assert_ok(input); + + input.value = '2'; + input.dispatchEvent(new window.Event('input')); + + flushSync(); + + assert.htmlEqual(target.innerHTML, ``); + + assert.deepEqual(logs, ['b', '2', 'a', '2']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/bind-getter-setter/main.svelte b/packages/svelte/tests/runtime-runes/samples/bind-getter-setter/main.svelte new file mode 100644 index 000000000000..191a4234764d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-getter-setter/main.svelte @@ -0,0 +1,16 @@ + + + + + a, + (v) => { + console.log('a', v); + a = v; + }} +/> + diff --git a/packages/svelte/tests/validator/samples/bind_group_invalid_expression/errors.json b/packages/svelte/tests/validator/samples/bind_group_invalid_expression/errors.json new file mode 100644 index 000000000000..f85363106b01 --- /dev/null +++ b/packages/svelte/tests/validator/samples/bind_group_invalid_expression/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "bind_group_invalid_expression", + "message": "`bind:group` can only bind to an Identifier or MemberExpression", + "start": { + "line": 8, + "column": 38 + }, + "end": { + "line": 8, + "column": 84 + } + } +] diff --git a/packages/svelte/tests/validator/samples/bind_group_invalid_expression/input.svelte b/packages/svelte/tests/validator/samples/bind_group_invalid_expression/input.svelte new file mode 100644 index 000000000000..3f8afe76550f --- /dev/null +++ b/packages/svelte/tests/validator/samples/bind_group_invalid_expression/input.svelte @@ -0,0 +1,12 @@ + + +{#each values as value} + +{/each} + +

{selected.name}

diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 0d761919a8e0..61a34dcb8e93 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -606,7 +606,7 @@ declare module 'svelte/animate' { } declare module 'svelte/compiler' { - import type { Expression, Identifier, ArrayExpression, ArrowFunctionExpression, VariableDeclaration, VariableDeclarator, MemberExpression, ObjectExpression, Pattern, Program, ChainExpression, SimpleCallExpression } from 'estree'; + import type { Expression, Identifier, ArrayExpression, ArrowFunctionExpression, VariableDeclaration, VariableDeclarator, MemberExpression, ObjectExpression, Pattern, Program, ChainExpression, SimpleCallExpression, SequenceExpression } from 'estree'; import type { SourceMap } from 'magic-string'; import type { Location } from 'locate-character'; /** @@ -1047,7 +1047,7 @@ declare module 'svelte/compiler' { /** The 'x' in `bind:x` */ name: string; /** The y in `bind:x={y}` */ - expression: Identifier | MemberExpression; + expression: Identifier | MemberExpression | SequenceExpression; } /** A `class:` directive */ From 57f8ca6e3c13d680d642bf7938d2a869e04043dc Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 8 Dec 2024 07:31:14 -0500 Subject: [PATCH 04/33] oops --- .changeset/slimy-donkeys-hang.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/slimy-donkeys-hang.md b/.changeset/slimy-donkeys-hang.md index d63141660eed..b491d78b4c9e 100644 --- a/.changeset/slimy-donkeys-hang.md +++ b/.changeset/slimy-donkeys-hang.md @@ -1,5 +1,5 @@ --- -'svelte': patch +'svelte': minor --- feat: add support for bind getters/setters From 301744f1f7f45946f799f82e63f072303e740071 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 8 Dec 2024 07:32:51 -0500 Subject: [PATCH 05/33] Version Packages (#14598) Co-authored-by: github-actions[bot] --- .changeset/rare-cheetahs-laugh.md | 5 ----- .changeset/slimy-donkeys-hang.md | 5 ----- packages/svelte/CHANGELOG.md | 10 ++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) delete mode 100644 .changeset/rare-cheetahs-laugh.md delete mode 100644 .changeset/slimy-donkeys-hang.md diff --git a/.changeset/rare-cheetahs-laugh.md b/.changeset/rare-cheetahs-laugh.md deleted file mode 100644 index 2637b50b3c38..000000000000 --- a/.changeset/rare-cheetahs-laugh.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: always run `if` block code the first time diff --git a/.changeset/slimy-donkeys-hang.md b/.changeset/slimy-donkeys-hang.md deleted file mode 100644 index b491d78b4c9e..000000000000 --- a/.changeset/slimy-donkeys-hang.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': minor ---- - -feat: add support for bind getters/setters diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 4031110fa799..fea90ca0eab6 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,15 @@ # svelte +## 5.9.0 + +### Minor Changes + +- feat: add support for bind getters/setters ([#14307](https://github.com/sveltejs/svelte/pull/14307)) + +### Patch Changes + +- fix: always run `if` block code the first time ([#14597](https://github.com/sveltejs/svelte/pull/14597)) + ## 5.8.1 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index c751a598db53..e5afd8e13065 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.8.1", + "version": "5.9.0", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 3061318cb034..f5369fe169e8 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -6,5 +6,5 @@ * https://svelte.dev/docs/svelte-compiler#svelte-version * @type {string} */ -export const VERSION = '5.8.1'; +export const VERSION = '5.9.0'; export const PUBLIC_VERSION = '5'; From c1c59e77a54109e6dd868e8ee7884caf9a275f5a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 8 Dec 2024 07:38:01 -0500 Subject: [PATCH 06/33] docs: where the hell did this come from? (#14613) --- documentation/docs/03-template-syntax/03-each.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/documentation/docs/03-template-syntax/03-each.md b/documentation/docs/03-template-syntax/03-each.md index df0ba4d8f59c..70666f6a5798 100644 --- a/documentation/docs/03-template-syntax/03-each.md +++ b/documentation/docs/03-template-syntax/03-each.md @@ -23,8 +23,6 @@ Iterating over values can be done with an each block. The values in question can ``` -You can use each blocks to iterate over any array or array-like value — that is, any object with a `length` property. - An each block can also specify an _index_, equivalent to the second argument in an `array.map(...)` callback: ```svelte From c66bf178aae18c2b4d8ac189a48cf10c47e4d417 Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Mon, 9 Dec 2024 13:39:31 +0100 Subject: [PATCH 07/33] fix: mark subtree dynamic for bind with sequence expressions (#14626) --- .changeset/green-pandas-study.md | 5 +++++ .../phases/2-analyze/visitors/BindDirective.js | 3 +++ .../samples/bind-getter-setter/_config.js | 14 ++++++++++---- .../samples/bind-getter-setter/main.svelte | 9 +++++++++ 4 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 .changeset/green-pandas-study.md diff --git a/.changeset/green-pandas-study.md b/.changeset/green-pandas-study.md new file mode 100644 index 000000000000..869599055cb0 --- /dev/null +++ b/.changeset/green-pandas-study.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: mark subtree dynamic for bind with sequence expressions diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js index b06236538008..b4de1925df24 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js @@ -11,6 +11,7 @@ import * as w from '../../../warnings.js'; import { binding_properties } from '../../bindings.js'; import fuzzymatch from '../../1-parse/utils/fuzzymatch.js'; import { is_content_editable_binding, is_svg } from '../../../../utils.js'; +import { mark_subtree_dynamic } from './shared/fragment.js'; /** * @param {AST.BindDirective} node @@ -141,6 +142,8 @@ export function BindDirective(node, context) { e.bind_invalid_expression(node); } + mark_subtree_dynamic(context.path); + return; } diff --git a/packages/svelte/tests/runtime-runes/samples/bind-getter-setter/_config.js b/packages/svelte/tests/runtime-runes/samples/bind-getter-setter/_config.js index 158d1e6f63e0..dd5c387405e0 100644 --- a/packages/svelte/tests/runtime-runes/samples/bind-getter-setter/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/bind-getter-setter/_config.js @@ -4,17 +4,23 @@ import { assert_ok } from '../../../suite'; export default test({ async test({ assert, target, logs }) { - const input = target.querySelector('input'); - - assert_ok(input); + const [input, checkbox] = target.querySelectorAll('input'); input.value = '2'; input.dispatchEvent(new window.Event('input')); flushSync(); - assert.htmlEqual(target.innerHTML, ``); + assert.htmlEqual( + target.innerHTML, + `
` + ); assert.deepEqual(logs, ['b', '2', 'a', '2']); + + flushSync(() => { + checkbox.click(); + }); + assert.deepEqual(logs, ['b', '2', 'a', '2', 'check', false]); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/bind-getter-setter/main.svelte b/packages/svelte/tests/runtime-runes/samples/bind-getter-setter/main.svelte index 191a4234764d..f6d908fba196 100644 --- a/packages/svelte/tests/runtime-runes/samples/bind-getter-setter/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/bind-getter-setter/main.svelte @@ -2,6 +2,7 @@ import Child from './Child.svelte'; let a = $state(0); + let check = $state(true); @@ -14,3 +15,11 @@ }} /> +
+ check, + (v)=>{ + console.log('check', v); + check = v; + }} /> +
\ No newline at end of file From 0a10c59517e77a0ed0b9fb51fab44a450a3710e7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2024 09:29:51 -0500 Subject: [PATCH 08/33] Version Packages (#14628) Co-authored-by: github-actions[bot] --- .changeset/green-pandas-study.md | 5 ----- packages/svelte/CHANGELOG.md | 6 ++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .changeset/green-pandas-study.md diff --git a/.changeset/green-pandas-study.md b/.changeset/green-pandas-study.md deleted file mode 100644 index 869599055cb0..000000000000 --- a/.changeset/green-pandas-study.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: mark subtree dynamic for bind with sequence expressions diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index fea90ca0eab6..00ef29347533 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.9.1 + +### Patch Changes + +- fix: mark subtree dynamic for bind with sequence expressions ([#14626](https://github.com/sveltejs/svelte/pull/14626)) + ## 5.9.0 ### Minor Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index e5afd8e13065..5d662af00c3f 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.9.0", + "version": "5.9.1", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index f5369fe169e8..20ff578fbeac 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -6,5 +6,5 @@ * https://svelte.dev/docs/svelte-compiler#svelte-version * @type {string} */ -export const VERSION = '5.9.0'; +export const VERSION = '5.9.1'; export const PUBLIC_VERSION = '5'; From 38171f60ead8d702f50f6b5c23633d2ae4d85be6 Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Mon, 9 Dec 2024 16:48:34 +0100 Subject: [PATCH 09/33] fix: allow exports with source from script module even if no bind is present (#14620) * fix: allow exports with source from script module even if no bind is present * chore: move test to validator --- .changeset/four-carrots-burn.md | 5 +++++ packages/svelte/src/compiler/phases/2-analyze/index.js | 2 +- .../export-not-defined-module-with-source/errors.json | 1 + .../export-not-defined-module-with-source/input.svelte | 3 +++ 4 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 .changeset/four-carrots-burn.md create mode 100644 packages/svelte/tests/validator/samples/export-not-defined-module-with-source/errors.json create mode 100644 packages/svelte/tests/validator/samples/export-not-defined-module-with-source/input.svelte diff --git a/.changeset/four-carrots-burn.md b/.changeset/four-carrots-burn.md new file mode 100644 index 000000000000..39cefcc4b76e --- /dev/null +++ b/.changeset/four-carrots-burn.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: allow exports with source from script module even if no bind is present diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 8f1efd7f635f..9e29813ee336 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -698,7 +698,7 @@ export function analyze_component(root, source, options) { } for (const node of analysis.module.ast.body) { - if (node.type === 'ExportNamedDeclaration' && node.specifiers !== null) { + if (node.type === 'ExportNamedDeclaration' && node.specifiers !== null && node.source == null) { for (const specifier of node.specifiers) { if (specifier.local.type !== 'Identifier') continue; diff --git a/packages/svelte/tests/validator/samples/export-not-defined-module-with-source/errors.json b/packages/svelte/tests/validator/samples/export-not-defined-module-with-source/errors.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/packages/svelte/tests/validator/samples/export-not-defined-module-with-source/errors.json @@ -0,0 +1 @@ +[] diff --git a/packages/svelte/tests/validator/samples/export-not-defined-module-with-source/input.svelte b/packages/svelte/tests/validator/samples/export-not-defined-module-with-source/input.svelte new file mode 100644 index 000000000000..df50ebc1fa66 --- /dev/null +++ b/packages/svelte/tests/validator/samples/export-not-defined-module-with-source/input.svelte @@ -0,0 +1,3 @@ + From 11764632b9d64621bfbf86cd1d3a65adda1dfd09 Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Mon, 9 Dec 2024 17:22:42 +0100 Subject: [PATCH 10/33] fix: deconflict `get_name` for literal class properties (#14607) --- .changeset/stupid-buckets-drum.md | 5 ++++ .../3-transform/client/visitors/ClassBody.js | 24 +++++++++++++++---- .../_config.js | 3 +++ .../main.svelte | 6 +++++ 4 files changed, 34 insertions(+), 4 deletions(-) create mode 100644 .changeset/stupid-buckets-drum.md create mode 100644 packages/svelte/tests/runtime-runes/samples/class-state-conflicting-get-name/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/class-state-conflicting-get-name/main.svelte diff --git a/.changeset/stupid-buckets-drum.md b/.changeset/stupid-buckets-drum.md new file mode 100644 index 000000000000..57d6f015f786 --- /dev/null +++ b/.changeset/stupid-buckets-drum.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: deconflict `get_name` for literal class properties diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js index 5e842a82febf..7b3a9a4d0e29 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js @@ -23,6 +23,9 @@ export function ClassBody(node, context) { /** @type {Map} */ const private_state = new Map(); + /** @type {Map<(MethodDefinition|PropertyDefinition)["key"], string>} */ + const definition_names = new Map(); + /** @type {string[]} */ const private_ids = []; @@ -34,9 +37,12 @@ export function ClassBody(node, context) { definition.key.type === 'Literal') ) { const type = definition.key.type; - const name = get_name(definition.key); + const name = get_name(definition.key, public_state); if (!name) continue; + // we store the deconflicted name in the map so that we can access it later + definition_names.set(definition.key, name); + const is_private = type === 'PrivateIdentifier'; if (is_private) private_ids.push(name); @@ -96,7 +102,7 @@ export function ClassBody(node, context) { definition.key.type === 'PrivateIdentifier' || definition.key.type === 'Literal') ) { - const name = get_name(definition.key); + const name = definition_names.get(definition.key); if (!name) continue; const is_private = definition.key.type === 'PrivateIdentifier'; @@ -210,10 +216,20 @@ export function ClassBody(node, context) { /** * @param {Identifier | PrivateIdentifier | Literal} node + * @param {Map} public_state */ -function get_name(node) { +function get_name(node, public_state) { if (node.type === 'Literal') { - return node.value?.toString().replace(regex_invalid_identifier_chars, '_'); + let name = node.value?.toString().replace(regex_invalid_identifier_chars, '_'); + + // the above could generate conflicts because it has to generate a valid identifier + // so stuff like `0` and `1` or `state%` and `state^` will result in the same string + // so we have to de-conflict. We can only check `public_state` because private state + // can't have literal keys + while (name && public_state.has(name)) { + name = '_' + name; + } + return name; } else { return node.name; } diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-conflicting-get-name/_config.js b/packages/svelte/tests/runtime-runes/samples/class-state-conflicting-get-name/_config.js new file mode 100644 index 000000000000..f47bee71df87 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-state-conflicting-get-name/_config.js @@ -0,0 +1,3 @@ +import { test } from '../../test'; + +export default test({}); diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-conflicting-get-name/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-state-conflicting-get-name/main.svelte new file mode 100644 index 000000000000..aec1e67cc675 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-state-conflicting-get-name/main.svelte @@ -0,0 +1,6 @@ + \ No newline at end of file From c6fca0200981a8be0a73f9602803b37f8ff1c45b Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Mon, 9 Dec 2024 18:39:13 +0100 Subject: [PATCH 11/33] docs: more details for errors/warnings on the site (#14632) * docs: more details for errors/warnings on the site Related to #11305 * Apply suggestions from code review Co-authored-by: Rich Harris * fix in correct place * tab not spaces * tweaks * fix * Apply suggestions from code review * regenerate --------- Co-authored-by: Rich Harris --- .../.generated/client-warnings.md | 84 +++++++++++++ .../.generated/compile-warnings.md | 117 ++++++++++++++++++ .../98-reference/.generated/server-errors.md | 2 + .../98-reference/.generated/shared-errors.md | 42 +++++++ .../.generated/shared-warnings.md | 9 ++ .../messages/client-warnings/warnings.md | 84 +++++++++++++ .../messages/compile-warnings/script.md | 91 ++++++++++++++ .../svelte/messages/compile-warnings/style.md | 14 +++ .../messages/compile-warnings/template.md | 12 ++ .../messages/server-errors/lifecycle.md | 2 + .../svelte/messages/shared-errors/errors.md | 42 +++++++ .../messages/shared-warnings/warnings.md | 9 ++ 12 files changed, 508 insertions(+) diff --git a/documentation/docs/98-reference/.generated/client-warnings.md b/documentation/docs/98-reference/.generated/client-warnings.md index ef19a28994bd..b0490b84ffd9 100644 --- a/documentation/docs/98-reference/.generated/client-warnings.md +++ b/documentation/docs/98-reference/.generated/client-warnings.md @@ -66,6 +66,31 @@ The easiest way to log a value as it changes over time is to use the [`$inspect` The `%attribute%` attribute on `%html%` changed its value between server and client renders. The client value, `%value%`, will be ignored in favour of the server value ``` +Certain attributes like `src` on an `` element will not be repaired during hydration, i.e. the server value will be kept. That's because updating these attributes can cause the image to be refetched (or in the case of an ` - - - diff --git a/sites/svelte-5-preview/src/lib/Output/PaneWithPanel.svelte b/sites/svelte-5-preview/src/lib/Output/PaneWithPanel.svelte deleted file mode 100644 index 9018a50bee48..000000000000 --- a/sites/svelte-5-preview/src/lib/Output/PaneWithPanel.svelte +++ /dev/null @@ -1,83 +0,0 @@ - - - -
- -
- -
-
- - -
- -
- -
-
-
- - diff --git a/sites/svelte-5-preview/src/lib/Output/ReplProxy.js b/sites/svelte-5-preview/src/lib/Output/ReplProxy.js deleted file mode 100644 index 0e45887bd727..000000000000 --- a/sites/svelte-5-preview/src/lib/Output/ReplProxy.js +++ /dev/null @@ -1,96 +0,0 @@ -let uid = 1; - -export default class ReplProxy { - /** @type {HTMLIFrameElement} */ - iframe; - - /** @type {import("./proxy").Handlers} */ - handlers; - - /** @type {Map void, reject: (value: any) => void }>} */ - pending_cmds = new Map(); - - /** @param {MessageEvent} event */ - handle_event = (event) => { - if (event.source !== this.iframe.contentWindow) return; - - const { action, args } = event.data; - - switch (action) { - case 'cmd_error': - case 'cmd_ok': - return this.handle_command_message(event.data); - case 'fetch_progress': - return this.handlers.on_fetch_progress(args.remaining); - case 'error': - return this.handlers.on_error(event.data); - case 'unhandledrejection': - return this.handlers.on_unhandled_rejection(event.data); - case 'console': - return this.handlers.on_console(event.data); - } - }; - - /** - * @param {HTMLIFrameElement} iframe - * @param {import("./proxy").Handlers} handlers - */ - constructor(iframe, handlers) { - this.iframe = iframe; - this.handlers = handlers; - - window.addEventListener('message', this.handle_event, false); - } - - destroy() { - window.removeEventListener('message', this.handle_event); - } - - /** - * @param {string} action - * @param {any} args - */ - iframe_command(action, args) { - return new Promise((resolve, reject) => { - const cmd_id = uid++; - - this.pending_cmds.set(cmd_id, { resolve, reject }); - - this.iframe.contentWindow?.postMessage({ action, cmd_id, args }, '*'); - }); - } - - /** - * @param {{ action: string; cmd_id: number; message: string; stack: any; args: any; }} cmd_data - */ - handle_command_message(cmd_data) { - let action = cmd_data.action; - let id = cmd_data.cmd_id; - let handler = this.pending_cmds.get(id); - - if (handler) { - this.pending_cmds.delete(id); - if (action === 'cmd_error') { - let { message, stack } = cmd_data; - let e = new Error(message); - e.stack = stack; - handler.reject(e); - } - - if (action === 'cmd_ok') { - handler.resolve(cmd_data.args); - } - } else { - console.error('command not found', id, cmd_data, [...this.pending_cmds.keys()]); - } - } - - /** @param {string} script */ - eval(script) { - return this.iframe_command('eval', { script }); - } - - handle_links() { - return this.iframe_command('catch_clicks', {}); - } -} diff --git a/sites/svelte-5-preview/src/lib/Output/Viewer.svelte b/sites/svelte-5-preview/src/lib/Output/Viewer.svelte deleted file mode 100644 index db506ada3bfd..000000000000 --- a/sites/svelte-5-preview/src/lib/Output/Viewer.svelte +++ /dev/null @@ -1,337 +0,0 @@ - - -
- -
- - - {#if $bundle?.error} - - {/if} -
- -
- -
- -
- -
-
- -
- {#if error} - - {:else if status || !$bundle} - {status || 'loading Svelte compiler...'} - {/if} -
-
- - diff --git a/sites/svelte-5-preview/src/lib/Output/console/Console.svelte b/sites/svelte-5-preview/src/lib/Output/console/Console.svelte deleted file mode 100644 index e2a880aff8bb..000000000000 --- a/sites/svelte-5-preview/src/lib/Output/console/Console.svelte +++ /dev/null @@ -1,46 +0,0 @@ - - -
- {#each logs as log} - - {/each} -
- - diff --git a/sites/svelte-5-preview/src/lib/Output/console/ConsoleLine.svelte b/sites/svelte-5-preview/src/lib/Output/console/ConsoleLine.svelte deleted file mode 100644 index 36635c713049..000000000000 --- a/sites/svelte-5-preview/src/lib/Output/console/ConsoleLine.svelte +++ /dev/null @@ -1,292 +0,0 @@ - - -{#if log.command === 'table'} - -{/if} - -
- -
- {#if log.count && log.count > 1} - {log.count} - {/if} - - {#if log.stack || log.command === 'group'} - {'\u25B6'} - {/if} - - {#if log.command === 'clear'} - Console was cleared - {:else if log.command === 'unclonable'} - Message could not be cloned. Open devtools to see it - {:else if log.command === 'table'} - - {:else} - - {#each format_args(log.args) as part} - - {#if !part.formatted} - {' '} - {/if}{#if part.type === 'value'} - - {:else} - {part.value} - {/if} - {/each} - - {/if} -
- - {#if log.stack && !log.collapsed} -
- {#each log.stack as line} - {line.label} - {line.location} - {/each} -
- {/if} - - {#each new Array(depth) as _, idx} -
- {/each} -
- -{#if log.command === 'group' && !log.collapsed} - {#each log.logs ?? [] as childLog} - - {/each} -{/if} - - diff --git a/sites/svelte-5-preview/src/lib/Output/console/ConsoleTable.svelte b/sites/svelte-5-preview/src/lib/Output/console/ConsoleTable.svelte deleted file mode 100644 index cba9721807c7..000000000000 --- a/sites/svelte-5-preview/src/lib/Output/console/ConsoleTable.svelte +++ /dev/null @@ -1,145 +0,0 @@ - - -
- - - - - - {#each table.columns as column} - - {/each} - - - - - {#each table.rows as row} - - - - {#each row.values as value} - - {/each} - - {/each} - -
(index){column}
- {#if typeof row.key === 'string'} - {row.key} - {:else} - - {/if} - - {#if typeof value === 'string'} - {value} - {:else} - - {/if} -
-
- - diff --git a/sites/svelte-5-preview/src/lib/Output/console/console.d.ts b/sites/svelte-5-preview/src/lib/Output/console/console.d.ts deleted file mode 100644 index 540e0b3b020a..000000000000 --- a/sites/svelte-5-preview/src/lib/Output/console/console.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -export type Log = { - command: 'info' | 'warn' | 'error' | 'table' | 'group' | 'clear' | 'unclonable'; - action?: 'console'; - args?: any[]; - collapsed?: boolean; - expanded?: boolean; - count?: number; - logs?: Log[]; - stack?: Array<{ - label?: string; - location?: string; - }>; - data?: any; - columns?: string[]; -}; diff --git a/sites/svelte-5-preview/src/lib/Output/get-location-from-stack.js b/sites/svelte-5-preview/src/lib/Output/get-location-from-stack.js deleted file mode 100644 index 3ac3e457d6b3..000000000000 --- a/sites/svelte-5-preview/src/lib/Output/get-location-from-stack.js +++ /dev/null @@ -1,42 +0,0 @@ -import { decode } from '@jridgewell/sourcemap-codec'; - -/** - * @param {string} stack - * @param {import('@jridgewell/sourcemap-codec').SourceMapMappings} map - * @returns - */ -export default function getLocationFromStack(stack, map) { - if (!stack) return; - const last = stack.split('\n')[1]; - const match = /:(\d+):(\d+)\)$/.exec(last); - - if (!match) return null; - - const line = +match[1]; - const column = +match[2]; - - return trace({ line, column }, map); -} - -/** - * - * @param {Omit} loc - * @param {*} map - * @returns - */ -function trace(loc, map) { - const mappings = decode(map.mappings); - const segments = mappings[loc.line - 1]; - - for (let i = 0; i < segments.length; i += 1) { - const segment = segments[i]; - if (segment[0] === loc.column) { - const [, sourceIndex, line, column] = segment; - const source = map.sources[sourceIndex ?? 0].slice(2); - - return { source, line: (line ?? 0) + 1, column }; - } - } - - return null; -} diff --git a/sites/svelte-5-preview/src/lib/Output/proxy.d.ts b/sites/svelte-5-preview/src/lib/Output/proxy.d.ts deleted file mode 100644 index b3f9fa8d1ae4..000000000000 --- a/sites/svelte-5-preview/src/lib/Output/proxy.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type Handlers = Record< - 'on_fetch_progress' | 'on_error' | 'on_unhandled_rejection' | 'on_console', - (data: any) => void ->; diff --git a/sites/svelte-5-preview/src/lib/Output/srcdoc/index.html b/sites/svelte-5-preview/src/lib/Output/srcdoc/index.html deleted file mode 100644 index 202a5f973a04..000000000000 --- a/sites/svelte-5-preview/src/lib/Output/srcdoc/index.html +++ /dev/null @@ -1,287 +0,0 @@ - - - - - - - - - diff --git a/sites/svelte-5-preview/src/lib/Repl.svelte b/sites/svelte-5-preview/src/lib/Repl.svelte deleted file mode 100644 index f43f5f899b84..000000000000 --- a/sites/svelte-5-preview/src/lib/Repl.svelte +++ /dev/null @@ -1,438 +0,0 @@ - - - - -
-
- -
- - -
- -
- -
-
-
- - {#if $toggleable} - - {/if} -
- - diff --git a/sites/svelte-5-preview/src/lib/autocomplete.js b/sites/svelte-5-preview/src/lib/autocomplete.js deleted file mode 100644 index b535c7ff0416..000000000000 --- a/sites/svelte-5-preview/src/lib/autocomplete.js +++ /dev/null @@ -1,209 +0,0 @@ -import { snippetCompletion } from '@codemirror/autocomplete'; -import { syntaxTree } from '@codemirror/language'; - -/** @typedef {(node: import('@lezer/common').SyntaxNode, context: import('@codemirror/autocomplete').CompletionContext, selected: import('./types').File) => boolean} Test */ - -/** - * Returns `true` if `$bindable()` is valid - * @type {Test} - */ -function is_bindable(node, context) { - // disallow outside `let { x = $bindable }` - if (node.parent?.name !== 'PatternProperty') return false; - if (node.parent.parent?.name !== 'ObjectPattern') return false; - if (node.parent.parent.parent?.name !== 'VariableDeclaration') return false; - - let last = node.parent.parent.parent.lastChild; - if (!last) return true; - - // if the declaration is incomplete, assume the best - if (last.name === 'ObjectPattern' || last.name === 'Equals' || last.name === '⚠') { - return true; - } - - if (last.name === ';') { - last = last.prevSibling; - if (!last || last.name === '⚠') return true; - } - - // if the declaration is complete, only return true if it is a `$props()` declaration - return ( - last.name === 'CallExpression' && - last.firstChild?.name === 'VariableName' && - context.state.sliceDoc(last.firstChild.from, last.firstChild.to) === '$props' - ); -} - -/** - * Returns `true` if `$props()` is valid - * TODO only allow in `.svelte` files, and only at the top level - * @type {Test} - */ -function is_props(node, _, selected) { - if (selected.type !== 'svelte') return false; - - return ( - node.name === 'VariableName' && - node.parent?.name === 'VariableDeclaration' && - node.parent.parent?.name === 'Script' - ); -} - -/** - * Returns `true` is this is a valid place to declare state - * @type {Test} - */ -function is_state(node) { - let parent = node.parent; - - if (node.name === '.' || node.name === 'PropertyName') { - if (parent?.name !== 'MemberExpression') return false; - parent = parent.parent; - } - - if (!parent) return false; - - return parent.name === 'VariableDeclaration' || parent.name === 'PropertyDeclaration'; -} - -/** - * Returns `true` if we're already in a valid call expression, e.g. - * changing an existing `$state()` to `$state.raw()` - * @type {Test} - */ -function is_state_call(node) { - let parent = node.parent; - - if (node.name === '.' || node.name === 'PropertyName') { - if (parent?.name !== 'MemberExpression') return false; - parent = parent.parent; - } - - if (parent?.name !== 'CallExpression') { - return false; - } - - parent = parent.parent; - if (!parent) return false; - - return parent.name === 'VariableDeclaration' || parent.name === 'PropertyDeclaration'; -} - -/** @type {Test} */ -function is_statement(node) { - if (node.name === 'VariableName') { - return node.parent?.name === 'ExpressionStatement'; - } - - if (node.name === '.' || node.name === 'PropertyName') { - return node.parent?.parent?.name === 'ExpressionStatement'; - } - - return false; -} - -/** @type {Array<{ snippet: string, test?: Test }>} */ -const runes = [ - { snippet: '$state(${})', test: is_state }, - { snippet: '$state', test: is_state_call }, - { snippet: '$props()', test: is_props }, - { snippet: '$derived(${});', test: is_state }, - { snippet: '$derived', test: is_state_call }, - { snippet: '$derived.by(() => {\n\t${}\n});', test: is_state }, - { snippet: '$derived.by', test: is_state_call }, - { snippet: '$effect(() => {\n\t${}\n});', test: is_statement }, - { snippet: '$effect.pre(() => {\n\t${}\n});', test: is_statement }, - { snippet: '$state.raw(${});', test: is_state }, - { snippet: '$state.raw', test: is_state_call }, - { snippet: '$bindable()', test: is_bindable }, - { snippet: '$effect.root(() => {\n\t${}\n})' }, - { snippet: '$state.snapshot(${})' }, - { snippet: '$effect.tracking()' }, - { snippet: '$inspect(${});', test: is_statement } -]; - -const options = runes.map(({ snippet, test }, i) => ({ - option: snippetCompletion(snippet, { - type: 'keyword', - boost: runes.length - i, - label: snippet.includes('(') ? snippet.slice(0, snippet.indexOf('(')) : snippet - }), - test -})); - -/** - * @param {import('@codemirror/autocomplete').CompletionContext} context - * @param {import('./types.js').File} selected - * @param {import('./types.js').File[]} files - */ -export function autocomplete(context, selected, files) { - let node = syntaxTree(context.state).resolveInner(context.pos, -1); - - if (node.name === 'String' && node.parent?.name === 'ImportDeclaration') { - const modules = [ - 'svelte', - 'svelte/animate', - 'svelte/easing', - 'svelte/events', - 'svelte/legacy', - 'svelte/motion', - 'svelte/reactivity', - 'svelte/store', - 'svelte/transition' - ]; - - for (const file of files) { - if (file === selected) continue; - modules.push(`./${file.name}.${file.type}`); - } - - return { - from: node.from + 1, - options: modules.map((label) => ({ - label, - type: 'string' - })) - }; - } - - if ( - selected.type !== 'svelte' && - (selected.type !== 'js' || !selected.name.endsWith('.svelte')) - ) { - return false; - } - - if (node.name === 'VariableName' || node.name === 'PropertyName' || node.name === '.') { - // special case — `$inspect(...).with(...)` is the only rune that 'returns' - // an 'object' with a 'method' - if (node.name === 'PropertyName' || node.name === '.') { - if ( - node.parent?.name === 'MemberExpression' && - node.parent.firstChild?.name === 'CallExpression' && - node.parent.firstChild.firstChild?.name === 'VariableName' && - context.state.sliceDoc( - node.parent.firstChild.firstChild.from, - node.parent.firstChild.firstChild.to - ) === '$inspect' - ) { - const open = context.matchBefore(/\.\w*/); - if (!open) return null; - - return { - from: open.from, - options: [snippetCompletion('.with(${})', { type: 'keyword', label: '.with' })] - }; - } - } - - const open = context.matchBefore(/\$[\w\.]*/); - if (!open) return null; - - return { - from: open.from, - options: options - .filter((option) => (option.test ? option.test(node, context, selected) : true)) - .map((option) => option.option) - }; - } -} diff --git a/sites/svelte-5-preview/src/lib/context.js b/sites/svelte-5-preview/src/lib/context.js deleted file mode 100644 index e983e6d147af..000000000000 --- a/sites/svelte-5-preview/src/lib/context.js +++ /dev/null @@ -1,13 +0,0 @@ -import { getContext, setContext } from 'svelte'; - -const key = Symbol('repl'); - -/** @returns {import("./types").ReplContext} */ -export function get_repl_context() { - return getContext(key); -} - -/** @param {import("./types").ReplContext} value */ -export function set_repl_context(value) { - setContext(key, value); -} diff --git a/sites/svelte-5-preview/src/lib/index.js b/sites/svelte-5-preview/src/lib/index.js deleted file mode 100644 index 969b64140852..000000000000 --- a/sites/svelte-5-preview/src/lib/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './Repl.svelte'; diff --git a/sites/svelte-5-preview/src/lib/theme.js b/sites/svelte-5-preview/src/lib/theme.js deleted file mode 100644 index 867e144acc50..000000000000 --- a/sites/svelte-5-preview/src/lib/theme.js +++ /dev/null @@ -1,153 +0,0 @@ -import { HighlightStyle, syntaxHighlighting } from '@codemirror/language'; -import { EditorView } from '@codemirror/view'; -import { tags as t } from '@lezer/highlight'; - -const ERROR_HUE = 0; -const WARNING_HUE = 40; - -const WARNING_FG = `hsl(${WARNING_HUE} 100% 60%)`; -const WARNING_BG = `hsl(${WARNING_HUE} 100% 40% / 0.5)`; - -const ERROR_FG = `hsl(${ERROR_HUE} 100% 40%)`; -const ERROR_BG = `hsl(${ERROR_HUE} 100% 40% / 0.5)`; - -/** - * @param {string} content - * @param {string} attrs - */ -function svg(content, attrs = `viewBox="0 0 40 40"`) { - return `url('data:image/svg+xml,${encodeURIComponent( - content - )}')`; -} - -/** - * @param {string} color - */ -function underline(color) { - return svg( - ``, - `width="6" height="4"` - ); -} - -const svelteThemeStyles = EditorView.theme( - { - '&': { - color: 'var(--sk-code-base)', - backgroundColor: 'transparent' - }, - - '.cm-content': { - caretColor: 'var(--sk-theme-3)' - }, - - '.cm-cursor, .cm-dropCursor': { borderLeftColor: 'var(--sk-theme-3)' }, - '&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': - { backgroundColor: 'var(--sk-selection-color)' }, - - '.cm-panels': { backgroundColor: 'var(--sk-back-2)', color: 'var(--sk-text-2)' }, - '.cm-panels.cm-panels-top': { borderBottom: '2px solid black' }, - '.cm-panels.cm-panels-bottom': { borderTop: '2px solid black' }, - - '.cm-searchMatch': { - backgroundColor: 'var(--sk-theme-2)' - // outline: '1px solid #457dff', - }, - '.cm-searchMatch.cm-searchMatch-selected': { - backgroundColor: '#6199ff2f' - }, - - '.cm-activeLine': { backgroundColor: '#6699ff0b' }, - '.cm-selectionMatch': { backgroundColor: '#aafe661a' }, - - '&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': { - backgroundColor: '#bad0f847' - }, - - '.cm-gutters': { - backgroundColor: 'var(--sk-back-3)', - border: 'none' - }, - - '.cm-activeLineGutter': { - backgroundColor: 'var(--sk-back-4)' - }, - - '.cm-foldPlaceholder': { - backgroundColor: 'transparent', - border: 'none', - color: '#ddd' - }, - - // https://github.com/codemirror/lint/blob/271b35f5d31a7e3645eaccbfec608474022098e1/src/lint.ts#L620 - '.cm-lintRange': { - backgroundPosition: 'left bottom', - backgroundRepeat: 'repeat-x', - paddingBottom: '4px' - }, - '.cm-lintRange-error': { - backgroundImage: underline(ERROR_FG) - }, - '.cm-lintRange-warning': { - backgroundImage: underline(WARNING_FG) - }, - '.cm-tooltip .cm-tooltip-arrow:before': { - borderTopColor: 'transparent', - borderBottomColor: 'transparent' - }, - '.cm-tooltip .cm-tooltip-arrow:after': { - borderTopColor: 'var(--sk-back-3)', - borderBottomColor: 'var(--sk-back-3)' - }, - '.cm-tooltip-autocomplete': { - color: 'var(--sk-text-2) !important', - perspective: '1px', - '& > ul > li[aria-selected]': { - backgroundColor: 'var(--sk-back-4)', - color: 'var(--sk-text-1) !important' - } - } - }, - { dark: true } -); - -/// The highlighting style for code in the One Dark theme. -const svelteHighlightStyle = HighlightStyle.define([ - { tag: t.keyword, color: 'var(--sk-code-keyword)' }, - { - tag: [t.name, t.deleted, t.character, t.propertyName, t.macroName], - color: 'var(--sk-code-base)' - }, - { tag: [t.function(t.variableName), t.labelName], color: 'var(--sk-code-tags)' }, - { tag: [t.color, t.constant(t.name), t.standard(t.name)], color: 'var(--sk-code-base)' }, - { tag: [t.definition(t.name), t.separator], color: 'var(--sk-code-base)' }, - { - tag: [ - t.typeName, - t.className, - t.number, - t.changed, - t.annotation, - t.modifier, - t.self, - t.namespace - ], - color: 'var(--sk-code-tags)' - }, - { - tag: [t.operator, t.operatorKeyword, t.url, t.escape, t.regexp, t.link, t.special(t.string)], - color: 'var(--sk-code-base)' - }, - { tag: [t.meta, t.comment], color: 'var(--sk-code-comment)' }, - { tag: t.strong, fontWeight: 'bold' }, - { tag: t.emphasis, fontStyle: 'italic' }, - { tag: t.strikethrough, textDecoration: 'line-through' }, - { tag: t.link, color: 'var(--sk-code-base)', textDecoration: 'underline' }, - { tag: t.heading, fontWeight: 'bold', color: 'var(--sk-text-1)' }, - { tag: [t.atom, t.bool], color: 'var(--sk-code-atom)' }, - { tag: [t.processingInstruction, t.string, t.inserted], color: 'var(--sk-code-string)' }, - { tag: t.invalid, color: '#ff008c' } -]); - -export const svelteTheme = [svelteThemeStyles, syntaxHighlighting(svelteHighlightStyle)]; diff --git a/sites/svelte-5-preview/src/lib/types.d.ts b/sites/svelte-5-preview/src/lib/types.d.ts deleted file mode 100644 index a758846d293b..000000000000 --- a/sites/svelte-5-preview/src/lib/types.d.ts +++ /dev/null @@ -1,77 +0,0 @@ -import type { EditorState } from '@codemirror/state'; -import { OutputChunk, RollupError } from '@rollup/browser'; -import type { Readable, Writable } from 'svelte/store'; -import type { CompileOptions, CompileError } from 'svelte/compiler'; - -export type Lang = 'js' | 'svelte' | 'json' | 'md' | 'css' | (string & Record); - -type StartOrEnd = { - line: number; - column: number; - character: number; -}; - -export type MessageDetails = { - start: StartOrEnd; - end: StartOrEnd; - filename: string; - message: string; -}; - -export type Warning = MessageDetails; - -export type Bundle = { - uid: number; - client: OutputChunk | null; - error: (RollupError & CompileError) | null; - server: OutputChunk | null; - imports: string[]; - warnings: Warning[]; -}; - -export type File = { - name: string; - source: string; - type: Lang; - modified?: boolean; -}; - -export type ReplState = { - files: File[]; - selected_name: string; - selected: File | null; - bundle: Bundle | null; - bundling: Promise; - bundler: import('./Bundler').default | null; - compile_options: CompileOptions; - cursor_pos: number; - toggleable: boolean; - module_editor: import('./CodeMirror.svelte').default | null; -}; - -export type ReplContext = { - files: Writable; - selected_name: Writable; - selected: Readable; - bundle: Writable; - bundling: Writable; - bundler: Writable; - compile_options: Writable; - cursor_pos: Writable; - toggleable: Writable; - module_editor: Writable; - - EDITOR_STATE_MAP: Map; - - // Methods - rebundle(): Promise; - migrate(): Promise; - handle_select(filename: string): Promise; - handle_change( - event: CustomEvent<{ - value: string; - }> - ): Promise; - go_to_warning_pos(item?: MessageDetails): Promise; - clear_state(): void; -}; diff --git a/sites/svelte-5-preview/src/lib/utils.js b/sites/svelte-5-preview/src/lib/utils.js deleted file mode 100644 index d378e1d51744..000000000000 --- a/sites/svelte-5-preview/src/lib/utils.js +++ /dev/null @@ -1,16 +0,0 @@ -/** - * @param {number} min - * @param {number} max - * @param {number} value - */ -export const clamp = (min, max, value) => Math.max(min, Math.min(max, value)); - -/** - * @param {number} ms - */ -export const sleep = (ms) => new Promise((f) => setTimeout(f, ms)); - -/** @param {import('./types').File} file */ -export function get_full_filename(file) { - return `${file.name}.${file.type}`; -} diff --git a/sites/svelte-5-preview/src/lib/workers/bundler/index.js b/sites/svelte-5-preview/src/lib/workers/bundler/index.js deleted file mode 100644 index 5a289fff7dcf..000000000000 --- a/sites/svelte-5-preview/src/lib/workers/bundler/index.js +++ /dev/null @@ -1,576 +0,0 @@ -/// - -import '../patch_window.js'; -import { sleep } from '$lib/utils.js'; -import { rollup } from '@rollup/browser'; -import { DEV } from 'esm-env'; -import * as resolve from 'resolve.exports'; -import commonjs from './plugins/commonjs.js'; -import glsl from './plugins/glsl.js'; -import json from './plugins/json.js'; -import replace from './plugins/replace.js'; -import loop_protect from './plugins/loop-protect.js'; - -/** @type {string} */ -var pkg_name; - -/** @type {string} */ -let packages_url; - -/** @type {string} */ -let svelte_url; - -/** @type {number} */ -let current_id; - -/** @type {(arg?: never) => void} */ -let fulfil_ready; -const ready = new Promise((f) => { - fulfil_ready = f; -}); - -/** - * @type {{ - * compile: typeof import('svelte/compiler').compile; - * compileModule: typeof import('svelte/compiler').compileModule; - * VERSION: string; - * }} - */ -let svelte; - -self.addEventListener( - 'message', - /** @param {MessageEvent} event */ async (event) => { - switch (event.data.type) { - case 'init': { - ({ packages_url, svelte_url } = event.data); - - const { version } = await fetch(`${svelte_url}/package.json`).then((r) => r.json()); - console.log(`Using Svelte compiler version ${version}`); - - const compiler = await fetch(`${svelte_url}/compiler/index.js`).then((r) => r.text()); - (0, eval)(compiler + '\n//# sourceURL=compiler/index.js@' + version); - - svelte = globalThis.svelte; - - fulfil_ready(); - break; - } - - case 'bundle': { - await ready; - const { uid, files } = event.data; - - if (files.length === 0) return; - - current_id = uid; - - setTimeout(async () => { - if (current_id !== uid) return; - - const result = await bundle({ uid, files }); - - if (JSON.stringify(result.error) === JSON.stringify(ABORT)) return; - if (result && uid === current_id) postMessage(result); - }); - - break; - } - } - } -); - -/** @type {Record<'client' | 'server', Map }>>} */ -let cached = { - client: new Map(), - server: new Map() -}; - -const ABORT = { aborted: true }; - -/** @type {Map>} */ -const FETCH_CACHE = new Map(); - -/** - * @param {string} url - * @param {number} uid - */ -async function fetch_if_uncached(url, uid) { - if (FETCH_CACHE.has(url)) { - return FETCH_CACHE.get(url); - } - - // TODO: investigate whether this is necessary - await sleep(50); - if (uid !== current_id) throw ABORT; - - const promise = fetch(url) - .then(async (r) => { - if (!r.ok) throw new Error(await r.text()); - - return { - url: r.url, - body: await r.text() - }; - }) - .catch((err) => { - FETCH_CACHE.delete(url); - throw err; - }); - - FETCH_CACHE.set(url, promise); - return promise; -} - -/** - * @param {string} url - * @param {number} uid - */ -async function follow_redirects(url, uid) { - const res = await fetch_if_uncached(url, uid); - return res?.url; -} - -/** - * - * @param {number} major - * @param {number} minor - * @param {number} patch - * @returns {number} - */ -function compare_to_version(major, minor, patch) { - const v = svelte.VERSION.match(/^(\d+)\.(\d+)\.(\d+)/); - - // @ts-ignore - return +v[1] - major || +v[2] - minor || +v[3] - patch; -} - -function is_v4() { - return compare_to_version(4, 0, 0) >= 0; -} - -function is_v5() { - return compare_to_version(5, 0, 0) >= 0; -} - -function is_legacy_package_structure() { - return compare_to_version(3, 4, 4) <= 0; -} - -function has_loopGuardTimeout_feature() { - return compare_to_version(3, 14, 0) >= 0; -} - -/** - * - * @param {Record} pkg - * @param {string} subpath - * @param {number} uid - * @param {string} pkg_url_base - */ -async function resolve_from_pkg(pkg, subpath, uid, pkg_url_base) { - // match legacy Rollup logic — pkg.svelte takes priority over pkg.exports - if (typeof pkg.svelte === 'string' && subpath === '.') { - return pkg.svelte; - } - - // modern - if (pkg.exports) { - try { - const [resolved] = - resolve.exports(pkg, subpath, { - browser: true, - conditions: ['svelte', 'development'] - }) ?? []; - - return resolved; - } catch { - throw `no matched export path was found in "${pkg_name}/package.json"`; - } - } - - // legacy - if (subpath === '.') { - let resolved_id = resolve.legacy(pkg, { - fields: ['browser', 'module', 'main'] - }); - - if (typeof resolved_id === 'object' && !Array.isArray(resolved_id)) { - const subpath = resolved_id['.']; - if (subpath === false) return 'data:text/javascript,export {}'; - - resolved_id = - subpath ?? - resolve.legacy(pkg, { - fields: ['module', 'main'] - }); - } - - if (!resolved_id) { - // last ditch — try to match index.js/index.mjs - for (const index_file of ['index.mjs', 'index.js']) { - try { - const indexUrl = new URL(index_file, `${pkg_url_base}/`).href; - return (await follow_redirects(indexUrl, uid)) ?? ''; - } catch { - // maybe the next option will be successful - } - } - - throw `could not find entry point in "${pkg_name}/package.json"`; - } - - return resolved_id; - } - - if (typeof pkg.browser === 'object') { - // this will either return `pkg.browser[subpath]` or `subpath` - return resolve.legacy(pkg, { - browser: subpath - }); - } - - return subpath; -} - -/** - * @param {number} uid - * @param {'client' | 'server'} mode - * @param {typeof cached['client']} cache - * @param {Map} local_files_lookup - */ -async function get_bundle(uid, mode, cache, local_files_lookup) { - let bundle; - - /** A set of package names (without subpaths) to include in pkg.devDependencies when downloading an app */ - /** @type {Set} */ - const imports = new Set(); - - /** @type {import('$lib/types.js').Warning[]} */ - const warnings = []; - - /** @type {{ message: string }[]} */ - const all_warnings = []; - - /** @type {typeof cache} */ - const new_cache = new Map(); - - /** @type {import('@rollup/browser').Plugin} */ - const repl_plugin = { - name: 'svelte-repl', - async resolveId(importee, importer) { - if (uid !== current_id) throw ABORT; - - if (importee === 'esm-env') return importee; - - const v5 = is_v5(); - const v4 = !v5 && is_v4(); - - if (!v5) { - // importing from Svelte - if (importee === `svelte`) - return v4 ? `${svelte_url}/src/runtime/index.js` : `${svelte_url}/index.mjs`; - - if (importee.startsWith(`svelte/`)) { - const sub_path = importee.slice(7); - if (v4) { - return `${svelte_url}/src/runtime/${sub_path}/index.js`; - } - - return is_legacy_package_structure() - ? `${svelte_url}/${sub_path}.mjs` - : `${svelte_url}/${sub_path}/index.mjs`; - } - } - - // importing from another file in REPL - if (local_files_lookup.has(importee) && (!importer || local_files_lookup.has(importer))) - return importee; - if (local_files_lookup.has(importee + '.js')) return importee + '.js'; - if (local_files_lookup.has(importee + '.json')) return importee + '.json'; - - // remove trailing slash - if (importee.endsWith('/')) importee = importee.slice(0, -1); - - // importing from a URL - if (/^https?:/.test(importee)) return importee; - - if (importee.startsWith('.')) { - if (importer && local_files_lookup.has(importer)) { - // relative import in a REPL file - // should've matched above otherwise importee doesn't exist - console.error(`Cannot find file "${importee}" imported by "${importer}" in the REPL`); - return; - } else { - // relative import in an external file - const url = new URL(importee, importer).href; - self.postMessage({ type: 'status', uid, message: `resolving ${url}` }); - - return await follow_redirects(url, uid); - } - } else { - // fetch from unpkg - self.postMessage({ type: 'status', uid, message: `resolving ${importee}` }); - - const match = /^((?:@[^/]+\/)?[^/]+)(\/.+)?$/.exec(importee); - if (!match) { - return console.error(`Invalid import "${importee}"`); - } - - const pkg_name = match[1]; - const subpath = `.${match[2] ?? ''}`; - - // if this was imported by one of our files, add it to the `imports` set - if (importer && local_files_lookup.has(importer)) { - imports.add(pkg_name); - } - - const fetch_package_info = async () => { - try { - const pkg_url = await follow_redirects( - `${pkg_name === 'svelte' ? '' : packages_url}/${pkg_name}/package.json`, - uid - ); - - if (!pkg_url) throw new Error(); - - const pkg_json = (await fetch_if_uncached(pkg_url, uid))?.body; - const pkg = JSON.parse(pkg_json ?? '""'); - - const pkg_url_base = pkg_url.replace(/\/package\.json$/, ''); - - return { - pkg, - pkg_url_base - }; - } catch (_e) { - throw new Error(`Error fetching "${pkg_name}" from unpkg. Does the package exist?`); - } - }; - - const { pkg, pkg_url_base } = await fetch_package_info(); - - try { - const resolved_id = await resolve_from_pkg(pkg, subpath, uid, pkg_url_base); - return new URL(resolved_id + '', `${pkg_url_base}/`).href; - } catch (reason) { - throw new Error(`Cannot import "${importee}": ${reason}.`); - } - } - }, - async load(resolved) { - if (uid !== current_id) throw ABORT; - - if (resolved === 'esm-env') { - return `export const BROWSER = true; export const DEV = true`; - } - - const cached_file = local_files_lookup.get(resolved); - if (cached_file) return cached_file.source; - - if (!FETCH_CACHE.has(resolved)) { - self.postMessage({ type: 'status', uid, message: `fetching ${resolved}` }); - } - - const res = await fetch_if_uncached(resolved, uid); - return res?.body; - }, - transform(code, id) { - if (uid !== current_id) throw ABORT; - - self.postMessage({ type: 'status', uid, message: `bundling ${id}` }); - - if (!/\.(svelte|js)$/.test(id)) return null; - - const name = id.split('/').pop()?.split('.')[0]; - - const cached_id = cache.get(id); - let result; - - if (cached_id && cached_id.code === code) { - result = cached_id.result; - } else if (id.endsWith('.svelte')) { - result = svelte.compile(code, { - filename: name + '.svelte', - generate: 'client', - dev: true - }); - - if (result.css) { - result.js.code += - '\n\n' + - ` - const $$__style = document.createElement('style'); - $$__style.textContent = ${JSON.stringify(result.css.code)}; - document.head.append($$__style); - `.replace(/\t/g, ''); - } - } else if (id.endsWith('.svelte.js')) { - result = svelte.compileModule(code, { - filename: name + '.js', - generate: 'client', - dev: true - }); - if (!result) { - return null; - } - } else { - return null; - } - - new_cache.set(id, { code, result }); - - // @ts-expect-error - (result.warnings || result.stats?.warnings)?.forEach((warning) => { - // This is required, otherwise postMessage won't work - // @ts-ignore - delete warning.toString; - // TODO remove stats post-launch - // @ts-ignore - warnings.push(warning); - }); - - /** @type {import('@rollup/browser').TransformResult} */ - const transform_result = { - code: result.js.code, - map: result.js.map - }; - - return transform_result; - } - }; - - try { - bundle = await rollup({ - input: './__entry.js', - plugins: [ - repl_plugin, - commonjs, - json, - glsl, - loop_protect, - replace({ - 'process.env.NODE_ENV': JSON.stringify('production') - }) - ], - inlineDynamicImports: true, - onwarn(warning) { - all_warnings.push({ - message: warning.message - }); - } - }); - - return { - bundle, - imports: Array.from(imports), - cache: new_cache, - error: null, - warnings, - all_warnings - }; - } catch (error) { - return { error, imports: null, bundle: null, cache: new_cache, warnings, all_warnings }; - } -} - -/** - * @param {{ uid: number; files: import('$lib/types.js').File[] }} param0 - * @returns - */ -async function bundle({ uid, files }) { - if (!DEV) { - console.clear(); - console.log(`running Svelte compiler version %c${svelte.VERSION}`, 'font-weight: bold'); - } - - /** @type {Map} */ - const lookup = new Map(); - - lookup.set('./__entry.js', { - name: '__entry', - source: ` - export { mount, unmount, untrack } from 'svelte'; - export {default as App} from './App.svelte'; - `, - type: 'js', - modified: false - }); - - files.forEach((file) => { - const path = `./${file.name}.${file.type}`; - lookup.set(path, file); - }); - - /** @type {Awaited>} */ - let client = await get_bundle(uid, 'client', cached.client, lookup); - let error; - - try { - if (client.error) { - throw client.error; - } - - cached.client = client.cache; - - const client_result = ( - await client.bundle?.generate({ - format: 'iife', - exports: 'named' - // sourcemap: 'inline' - }) - )?.output[0]; - - const server = false // TODO how can we do SSR? - ? await get_bundle(uid, 'server', cached.server, lookup) - : null; - - if (server) { - cached.server = server.cache; - if (server.error) { - throw server.error; - } - } - - const server_result = server - ? ( - await server.bundle?.generate({ - format: 'iife', - name: 'SvelteComponent', - exports: 'named' - // sourcemap: 'inline' - }) - )?.output?.[0] - : null; - - return { - uid, - client: client_result, - server: server_result, - imports: client.imports, - warnings: client.warnings, - error: null - }; - } catch (err) { - console.error(err); - - /** @type {Error} */ - // @ts-ignore - const e = error || err; - - // @ts-ignore - delete e.toString; - - return { - uid, - client: null, - server: null, - imports: null, - warnings: client.warnings, - error: Object.assign({}, e, { - message: e.message, - stack: e.stack - }) - }; - } -} diff --git a/sites/svelte-5-preview/src/lib/workers/bundler/plugins/commonjs.js b/sites/svelte-5-preview/src/lib/workers/bundler/plugins/commonjs.js deleted file mode 100644 index 9e0a92dbddc5..000000000000 --- a/sites/svelte-5-preview/src/lib/workers/bundler/plugins/commonjs.js +++ /dev/null @@ -1,58 +0,0 @@ -import { parse } from 'acorn'; -import { walk } from 'zimmerframe'; - -const require = `function require(id) { - if (id in __repl_lookup) return __repl_lookup[id]; - throw new Error(\`Cannot require modules dynamically (\${id})\`); -}`; - -/** @type {import('@rollup/browser').Plugin} */ -export default { - name: 'commonjs', - - transform: (code, id) => { - if (!/\b(require|module|exports)\b/.test(code)) return; - - try { - const ast = parse(code, { - ecmaVersion: 'latest' - }); - - /** @type {string[]} */ - const requires = []; - - walk(/** @type {import('estree').Node} */ (ast), null, { - CallExpression: (node) => { - if (node.callee.type === 'Identifier' && node.callee.name === 'require') { - if (node.arguments.length !== 1) return; - const arg = node.arguments[0]; - if (arg.type !== 'Literal' || typeof arg.value !== 'string') return; - - requires.push(arg.value); - } - } - }); - - const imports = requires.map((id, i) => `import __repl_${i} from '${id}';`).join('\n'); - const lookup = `const __repl_lookup = { ${requires - .map((id, i) => `'${id}': __repl_${i}`) - .join(', ')} };`; - - const transformed = [ - imports, - lookup, - require, - `const exports = {}; const module = { exports };`, - code, - `export default module.exports;` - ].join('\n\n'); - - return { - code: transformed, - map: null - }; - } catch (err) { - return null; - } - } -}; diff --git a/sites/svelte-5-preview/src/lib/workers/bundler/plugins/glsl.js b/sites/svelte-5-preview/src/lib/workers/bundler/plugins/glsl.js deleted file mode 100644 index 51e7e062a4d9..000000000000 --- a/sites/svelte-5-preview/src/lib/workers/bundler/plugins/glsl.js +++ /dev/null @@ -1,12 +0,0 @@ -/** @type {import('@rollup/browser').Plugin} */ -export default { - name: 'glsl', - transform: (code, id) => { - if (!id.endsWith('.glsl')) return; - - return { - code: `export default ${JSON.stringify(code)};`, - map: null - }; - } -}; diff --git a/sites/svelte-5-preview/src/lib/workers/bundler/plugins/json.js b/sites/svelte-5-preview/src/lib/workers/bundler/plugins/json.js deleted file mode 100644 index 2f79b289e4e5..000000000000 --- a/sites/svelte-5-preview/src/lib/workers/bundler/plugins/json.js +++ /dev/null @@ -1,12 +0,0 @@ -/** @type {import('@rollup/browser').Plugin} */ -export default { - name: 'json', - transform: (code, id) => { - if (!id.endsWith('.json')) return; - - return { - code: `export default ${code};`, - map: null - }; - } -}; diff --git a/sites/svelte-5-preview/src/lib/workers/bundler/plugins/loop-protect.js b/sites/svelte-5-preview/src/lib/workers/bundler/plugins/loop-protect.js deleted file mode 100644 index 9cb4a8e25e6a..000000000000 --- a/sites/svelte-5-preview/src/lib/workers/bundler/plugins/loop-protect.js +++ /dev/null @@ -1,111 +0,0 @@ -import { parse } from 'acorn'; -import { print } from 'esrap'; -import { walk } from 'zimmerframe'; - -const TIMEOUT = 100; - -const regex = /\b(for|while)\b/; - -/** - * - * @param {string} code - * @returns {import('estree').Statement} - */ -function parse_statement(code) { - return /** @type {import('estree').Statement} */ (parse(code, { ecmaVersion: 'latest' }).body[0]); -} - -const declaration = parse_statement(` - const __start = Date.now(); -`); - -const check = parse_statement(` - if (Date.now() > __start + ${TIMEOUT}) { - throw new Error('Infinite loop detected'); - } -`); - -/** - * - * @param {import('estree').Node[]} path - * @returns {null | import('estree').FunctionExpression | import('estree').FunctionDeclaration | import('estree').ArrowFunctionExpression} - */ -export function get_current_function(path) { - for (let i = path.length - 1; i >= 0; i--) { - const node = path[i]; - if ( - node.type === 'FunctionDeclaration' || - node.type === 'FunctionExpression' || - node.type === 'ArrowFunctionExpression' - ) { - return node; - } - } - return null; -} - -/** - * @template {import('estree').DoWhileStatement | import('estree').ForStatement | import('estree').WhileStatement} Statement - * @param {Statement} node - * @param {import('zimmerframe').Context} context - * @returns {import('estree').Node | void} - */ -function loop_protect(node, context) { - const current_function = get_current_function(context.path); - - if (current_function === null || (!current_function.async && !current_function.generator)) { - const body = /** @type {import('estree').Statement} */ (context.visit(node.body)); - - const statements = body.type === 'BlockStatement' ? [...body.body] : [body]; - - /** @type {import('estree').BlockStatement} */ - const replacement = { - type: 'BlockStatement', - body: [ - declaration, - { - .../** @type {Statement} */ (context.next() ?? node), - body: { - type: 'BlockStatement', - body: [...statements, check] - } - } - ] - }; - - return replacement; - } - - context.next(); -} - -/** @type {import('@rollup/browser').Plugin} */ -export default { - name: 'loop-protect', - transform: (code, id) => { - // only applies to local files, not imports - if (!id.startsWith('./')) return; - - // only applies to JS and Svelte files - if (!id.endsWith('.js') && !id.endsWith('.svelte')) return; - - // fast path - if (!regex.test(code)) return; - - const ast = parse(code, { - ecmaVersion: 'latest', - sourceType: 'module' - }); - - const transformed = walk(/** @type {import('estree').Node} */ (ast), null, { - WhileStatement: loop_protect, - DoWhileStatement: loop_protect, - ForStatement: loop_protect - }); - - // nothing changed - if (ast === transformed) return null; - - return print(transformed); - } -}; diff --git a/sites/svelte-5-preview/src/lib/workers/bundler/plugins/replace.js b/sites/svelte-5-preview/src/lib/workers/bundler/plugins/replace.js deleted file mode 100644 index 6ccdeffed827..000000000000 --- a/sites/svelte-5-preview/src/lib/workers/bundler/plugins/replace.js +++ /dev/null @@ -1,72 +0,0 @@ -/** @param {string} str */ -function escape(str) { - return str.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&'); -} - -/** @param {unknown} functionOrValue */ -function ensureFunction(functionOrValue) { - if (typeof functionOrValue === 'function') { - return functionOrValue; - } - return function () { - return functionOrValue; - }; -} - -/** - * @param {string} a - * @param {string} b - */ -function longest(a, b) { - return b.length - a.length; -} - -/** @param {Record} object */ -function mapToFunctions(object) { - return Object.keys(object).reduce( - /** @param {Record} functions */ function (functions, key) { - functions[key] = ensureFunction(object[key]); - return functions; - }, - {} - ); -} - -/** - * @param {Record} options - * @returns {import('@rollup/browser').Plugin} - */ -function replace(options) { - const functionValues = mapToFunctions(options); - const keys = Object.keys(functionValues).sort(longest).map(escape); - - const pattern = new RegExp('\\b(' + keys.join('|') + ')\\b', 'g'); - - return { - name: 'replace', - - transform: function transform(code, id) { - let hasReplacements = false; - let match; - let start; - let end; - let replacement; - - code = code.replace(pattern, (_, key) => { - hasReplacements = true; - return String(functionValues[key](id)); - }); - - if (!hasReplacements) { - return null; - } - - return { - code, - map: null - }; - } - }; -} - -export default replace; diff --git a/sites/svelte-5-preview/src/lib/workers/compiler/index.js b/sites/svelte-5-preview/src/lib/workers/compiler/index.js deleted file mode 100644 index 9247894dd6e3..000000000000 --- a/sites/svelte-5-preview/src/lib/workers/compiler/index.js +++ /dev/null @@ -1,154 +0,0 @@ -/// -self.window = self; //TODO: still need?: egregious hack to get magic-string to work in a worker - -/** - * @type {{ - * parse: typeof import('svelte/compiler').parse; - * compile: typeof import('svelte/compiler').compile; - * compileModule: typeof import('svelte/compiler').compileModule; - * VERSION: string; - * }} - */ -let svelte; - -/** @type {(arg?: never) => void} */ -let fulfil_ready; -const ready = new Promise((f) => { - fulfil_ready = f; -}); - -self.addEventListener( - 'message', - /** @param {MessageEvent} event */ - async (event) => { - switch (event.data.type) { - case 'init': - const { svelte_url } = event.data; - - const { version } = await fetch(`${svelte_url}/package.json`) - .then((r) => r.json()) - .catch(() => ({ version: 'experimental' })); - - const compiler = await fetch(`${svelte_url}/compiler/index.js`).then((r) => r.text()); - (0, eval)(compiler + '\n//# sourceURL=compiler/index.js@' + version); - - svelte = globalThis.svelte; - - fulfil_ready(); - break; - - case 'compile': - await ready; - postMessage(compile(event.data)); - break; - - case 'migrate': - await ready; - postMessage(migrate(event.data)); - break; - } - } -); - -const common_options = { - dev: false, - css: false -}; - -/** @param {import("../workers").CompileMessageData} param0 */ -function compile({ id, source, options, return_ast }) { - try { - const css = `/* Select a component to see compiled CSS */`; - - if (options.filename.endsWith('.svelte')) { - const compiled = svelte.compile(source, { - ...options, - discloseVersion: false // less visual noise in the output tab - }); - - const { js, css, warnings, metadata } = compiled; - - const ast = return_ast ? svelte.parse(source, { modern: true }) : undefined; - - return { - id, - result: { - js: js.code, - css: css?.code || `/* Add a tag to see compiled CSS */`, - error: null, - warnings: warnings.map((warning) => warning.toJSON()), - metadata, - ast - } - }; - } else if (options.filename.endsWith('.svelte.js')) { - const compiled = svelte.compileModule(source, { - filename: options.filename, - generate: options.generate, - dev: options.dev - }); - - if (compiled) { - return { - id, - result: { - js: compiled.js.code, - css, - error: null, - warnings: compiled.warnings.map((warning) => warning.toJSON()), - metadata: compiled.metadata - } - }; - } - } - - return { - id, - result: { - js: `// Select a component, or a '.svelte.js' module that uses runes, to see compiled output`, - css, - error: null, - warnings: [], - metadata: null - } - }; - } catch (err) { - // @ts-ignore - let message = `/*\nError compiling ${err.filename ?? 'component'}:\n${err.message}\n*/`; - - return { - id, - result: { - js: message, - css: message, - error: { - message: err.message, - position: err.position - }, - warnings: [], - metadata: null - } - }; - } -} - -/** @param {import("../workers").MigrateMessageData} param0 */ -function migrate({ id, source, filename }) { - try { - const result = svelte.migrate(source, { filename }); - - return { - id, - result - }; - } catch (err) { - // @ts-ignore - let message = `/*\nError migrating ${err.filename ?? 'component'}:\n${err.message}\n*/`; - - return { - id, - result: { code: source }, - error: message - }; - } -} diff --git a/sites/svelte-5-preview/src/lib/workers/jsconfig.json b/sites/svelte-5-preview/src/lib/workers/jsconfig.json deleted file mode 100644 index 60351b754815..000000000000 --- a/sites/svelte-5-preview/src/lib/workers/jsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "include": ["./**/*"], - "compilerOptions": { - "paths": { - "svelte": ["../../../static/svelte/main"], - "svelte/*": ["../../../static/svelte/*"] - } - } -} diff --git a/sites/svelte-5-preview/src/lib/workers/patch_window.js b/sites/svelte-5-preview/src/lib/workers/patch_window.js deleted file mode 100644 index ff7057c9c28f..000000000000 --- a/sites/svelte-5-preview/src/lib/workers/patch_window.js +++ /dev/null @@ -1 +0,0 @@ -self.window = self; // hack for magic-sring and rollup inline sourcemaps diff --git a/sites/svelte-5-preview/src/lib/workers/workers.d.ts b/sites/svelte-5-preview/src/lib/workers/workers.d.ts deleted file mode 100644 index e66e075c14f1..000000000000 --- a/sites/svelte-5-preview/src/lib/workers/workers.d.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { CompileOptions, File } from '../types'; - -export type CompileMessageData = { - id: number; - type: 'compile' | 'init'; - source: string; - options: CompileOptions; - is_entry: boolean; - return_ast: boolean; - svelte_url?: string; - result: { - js: string; - css: string; - ast?: import('svelte/types/compiler/interfaces').Ast; - metadata?: { - runes: boolean; - }; - }; -}; - -export type BundleMessageData = { - uid: number; - type: 'init' | 'bundle' | 'status'; - message: string; - packages_url: string; - svelte_url: string; - files: File[]; -}; - -export type MigrateMessageData = { - id: number; - result: { code: string }; - error?: string; -}; diff --git a/sites/svelte-5-preview/src/routes/+error.svelte b/sites/svelte-5-preview/src/routes/+error.svelte deleted file mode 100644 index 6d6d8a7d7cb5..000000000000 --- a/sites/svelte-5-preview/src/routes/+error.svelte +++ /dev/null @@ -1,73 +0,0 @@ - - - - {$page.status} - - -
- {#if online} - {#if $page.status === 404} -

Not found!

-

- If you were expecting to find something here, please drop by the - Discord chatroom - and let us know, or raise an issue on - GitHub. Thanks! -

- {:else} -

Yikes!

-

Something went wrong when we tried to render this page.

- {#if $page.error.message} -

{$page.status}: {$page.error.message}

- {:else} -

Encountered a {$page.status} error.

- {/if} -

Please try reloading the page.

-

- If the error persists, please drop by the - Discord chatroom - and let us know, or raise an issue on - GitHub. Thanks! -

- {/if} - {:else} -

It looks like you're offline

-

Reload the page once you've found the internet.

- {/if} -
- - diff --git a/sites/svelte-5-preview/src/routes/+layout.server.js b/sites/svelte-5-preview/src/routes/+layout.server.js deleted file mode 100644 index 640c4c57df10..000000000000 --- a/sites/svelte-5-preview/src/routes/+layout.server.js +++ /dev/null @@ -1,12 +0,0 @@ -export const prerender = true; - -/** @type {import('@sveltejs/adapter-vercel').EdgeConfig} */ -export const config = { - runtime: 'edge' -}; - -export const load = async ({ fetch }) => { - const nav_data = await fetch('/nav.json').then((r) => r.json()); - - return { nav_links: nav_data }; -}; diff --git a/sites/svelte-5-preview/src/routes/+layout.svelte b/sites/svelte-5-preview/src/routes/+layout.svelte deleted file mode 100644 index 99b72d0fade5..000000000000 --- a/sites/svelte-5-preview/src/routes/+layout.svelte +++ /dev/null @@ -1,101 +0,0 @@ - - - - Svelte 5 preview - - - - - - - - - - - - - - diff --git a/sites/svelte-5-preview/src/routes/+page.svelte b/sites/svelte-5-preview/src/routes/+page.svelte deleted file mode 100644 index b54c36ee5b58..000000000000 --- a/sites/svelte-5-preview/src/routes/+page.svelte +++ /dev/null @@ -1,98 +0,0 @@ - - - { - if (!setting_hash) { - change_from_hash(); - } - - setting_hash = false; - }} -/> - - diff --git a/sites/svelte-5-preview/src/routes/defaults.js b/sites/svelte-5-preview/src/routes/defaults.js deleted file mode 100644 index f1bdbbbb3548..000000000000 --- a/sites/svelte-5-preview/src/routes/defaults.js +++ /dev/null @@ -1,21 +0,0 @@ -export const default_files = () => [ - { - name: 'App', - type: 'svelte', - source: ` - - - - ` - .replace(/^\t{3}/gm, '') - .trim() - } -]; diff --git a/sites/svelte-5-preview/src/routes/docs/+layout.server.js b/sites/svelte-5-preview/src/routes/docs/+layout.server.js deleted file mode 100644 index cbb6433bf9be..000000000000 --- a/sites/svelte-5-preview/src/routes/docs/+layout.server.js +++ /dev/null @@ -1,13 +0,0 @@ -export async function load({ url }) { - if (url.pathname === '/docs') { - return { - sections: [] - }; - } - - const { get_docs_data, get_docs_list } = await import('./render.js'); - - return { - sections: get_docs_list(await get_docs_data()) - }; -} diff --git a/sites/svelte-5-preview/src/routes/docs/+layout.svelte b/sites/svelte-5-preview/src/routes/docs/+layout.svelte deleted file mode 100644 index 097fde98ff0e..000000000000 --- a/sites/svelte-5-preview/src/routes/docs/+layout.svelte +++ /dev/null @@ -1,110 +0,0 @@ - - -
-
- -
- -
- {#if category} -

{category}

- {/if} - {#if title} -

{title}

- {/if} - - -
-
- - diff --git a/sites/svelte-5-preview/src/routes/docs/+page.js b/sites/svelte-5-preview/src/routes/docs/+page.js deleted file mode 100644 index fba7f30e4b6b..000000000000 --- a/sites/svelte-5-preview/src/routes/docs/+page.js +++ /dev/null @@ -1,5 +0,0 @@ -import { redirect } from '@sveltejs/kit'; - -export function load() { - redirect(307, '/docs/introduction'); -} diff --git a/sites/svelte-5-preview/src/routes/docs/[slug]/+page.server.js b/sites/svelte-5-preview/src/routes/docs/[slug]/+page.server.js deleted file mode 100644 index 25c78ba28132..000000000000 --- a/sites/svelte-5-preview/src/routes/docs/[slug]/+page.server.js +++ /dev/null @@ -1,19 +0,0 @@ -import { error } from '@sveltejs/kit'; - -export async function entries() { - const { get_docs_data } = await import('../render.js'); - - const data = await get_docs_data(); - return data[0].pages.map((page) => ({ slug: page.slug })); -} - -export async function load({ params }) { - const { get_docs_data, get_parsed_docs } = await import('../render.js'); - - const data = await get_docs_data(); - const processed_page = await get_parsed_docs(data, params.slug); - - if (!processed_page) error(404); - - return { page: processed_page }; -} diff --git a/sites/svelte-5-preview/src/routes/docs/[slug]/+page.svelte b/sites/svelte-5-preview/src/routes/docs/[slug]/+page.svelte deleted file mode 100644 index bb5c166711f7..000000000000 --- a/sites/svelte-5-preview/src/routes/docs/[slug]/+page.svelte +++ /dev/null @@ -1,76 +0,0 @@ - - - - {data.page?.title} • Docs • Svelte 5 preview - - - - - - -
- - - {@html data.page.content} -
- -
-
- previous - - {#if prev} - {prev.title} - {/if} -
- -
- next - {#if next} - {next.title} - {/if} -
-
- - diff --git a/sites/svelte-5-preview/src/routes/docs/content/01-api/01-introduction.md b/sites/svelte-5-preview/src/routes/docs/content/01-api/01-introduction.md deleted file mode 100644 index 2e3cb987c050..000000000000 --- a/sites/svelte-5-preview/src/routes/docs/content/01-api/01-introduction.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: Introduction ---- - -Welcome to the Svelte 5 preview documentation! This is intended as a resource for people who already have some familiarity with Svelte and want to learn about the new runes API, which you can learn about in the [Introducing runes](https://svelte.dev/blog/runes) blog post. - -You can try runes for yourself in the [playground](/), or learn more about our plans via the [FAQ](/docs/faq). diff --git a/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md b/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md deleted file mode 100644 index 84062a1cfaee..000000000000 --- a/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md +++ /dev/null @@ -1,700 +0,0 @@ ---- -title: Runes ---- - -Svelte 5 introduces _runes_, a powerful set of primitives for controlling reactivity inside your Svelte components and — for the first time — inside `.svelte.js` and `.svelte.ts` modules. - -Runes are function-like symbols that provide instructions to the Svelte compiler. You don't need to import them from anywhere — when you use Svelte, they're part of the language. - -When you [opt in to runes mode](#how-to-opt-in), the non-runes features listed in the 'What this replaces' sections are no longer available. - -> Check out the [Introducing runes](https://svelte.dev/blog/runes) blog post before diving into the docs! - -## `$state` - -Reactive state is declared with the `$state` rune: - -```svelte - - - -``` - -You can also use `$state` in class fields (whether public or private): - -```js -// @errors: 7006 2554 -class Todo { - done = $state(false); - text = $state(); - - constructor(text) { - this.text = text; - } -} -``` - -> In this example, the compiler transforms `done` and `text` into `get`/`set` methods on the class prototype referencing private fields - -Only plain objects and arrays [are made deeply reactive](/#H4sIAAAAAAAAE42QwWrDMBBEf2URhUhUNEl7c21DviPOwZY3jVpZEtIqUBz9e-UUt9BTj7M784bdmZ21wciq48xsPyGr2MF7Jhl9-kXEKxrCoqNLQS2TOqqgPbWd7cgggU3TgCFCAw-RekJ-3Et4lvByEq-drbe_dlsPichZcFYZrT6amQto2pXw5FO88FUYtG90gUfYi3zvWrYL75vxL57zfA07_zfr23k1vjtt-aZ0bQTcbrDL5ZifZcAxKeS8lzDc8X0xDhJ2ItdbX1jlOZMb9VnjyCoKCfMpfwG975NFVwEAAA==) by wrapping them with [`Proxies`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy): - -```svelte - - - - - - -

- {numbers.join(' + ') || 0} - = - {numbers.reduce((a, b) => a + b, 0)} -

-``` - -### What this replaces - -In non-runes mode, a `let` declaration is treated as reactive state if it is updated at some point. Unlike `$state(...)`, which works anywhere in your app, `let` only behaves this way at the top level of a component. - -## `$state.raw` - -State declared with `$state.raw` cannot be mutated; it can only be _reassigned_. In other words, rather than assigning to a property of an object, or using an array method like `push`, replace the object or array altogether if you'd like to update it: - -```diff - - -- - -- -+ - -

- {numbers.join(' + ') || 0} - = - {numbers.reduce((a, b) => a + b, 0)} -

-``` - -This can improve performance with large arrays and objects that you weren't planning to mutate anyway, since it avoids the cost of making them reactive. Note that raw state can _contain_ reactive state (for example, a raw array of reactive objects). - -## `$state.snapshot` - -To take a static snapshot of a deeply reactive `$state` proxy, use `$state.snapshot`: - -```svelte - -``` - -This is handy when you want to pass some state to an external library or API that doesn't expect a proxy, such as `structuredClone`. - -## `$derived` - -Derived state is declared with the `$derived` rune: - -```diff - - - - -+

{count} doubled is {doubled}

-``` - -The expression inside `$derived(...)` should be free of side-effects. Svelte will disallow state changes (e.g. `count++`) inside derived expressions. - -As with `$state`, you can mark class fields as `$derived`. - -### What this replaces - -If the value of a reactive variable is being computed it should be replaced with `$derived` whether it previously took the form of `$: double = count * 2` or `$: { double = count * 2; }` There are some important differences to be aware of: - -- With the `$derived` rune, the value of `double` is always current (for example if you update `count` then immediately `console.log(double)`). With `$:` declarations, values are not updated until right before Svelte updates the DOM -- In non-runes mode, Svelte determines the dependencies of `double` by statically analysing the `count * 2` expression. If you refactor it... - ```js - // @errors: 2304 - const doubleCount = () => count * 2; - $: double = doubleCount(); - ``` - ...that dependency information is lost, and `double` will no longer update when `count` changes. With runes, dependencies are instead tracked at runtime. -- In non-runes mode, reactive statements are ordered _topologically_, meaning that in a case like this... - ```js - // @errors: 2304 - $: triple = double + count; - $: double = count * 2; - ``` - ...`double` will be calculated first despite the source order. In runes mode, `triple` cannot reference `double` before it has been declared. - -## `$derived.by` - -Sometimes you need to create complex derivations that don't fit inside a short expression. In these cases, you can use `$derived.by` which accepts a function as its argument. - -```svelte - - - -``` - -In essence, `$derived(expression)` is equivalent to `$derived.by(() => expression)`. - -## `$effect` - -To run _side-effects_ when the component is mounted to the DOM, and when values change, we can use the `$effect` rune ([demo](/#H4sIAAAAAAAAE31T24rbMBD9lUG7kAQ2sbdlX7xOYNk_aB_rQhRpbAsU2UiTW0P-vbrYubSlYGzmzMzROTPymdVKo2PFjzMzfIusYB99z14YnfoQuD1qQh-7bmdFQEonrOppVZmKNBI49QthCc-OOOH0LZ-9jxnR6c7eUpOnuv6KeT5JFdcqbvbcBcgDz1jXKGg6ncFyBedYR6IzLrAZwiN5vtSxaJA-EzadfJEjKw11C6GR22-BLH8B_wxdByWpvUYtqqal2XB6RVkG1CoHB6U1WJzbnYFDiwb3aGEdDa3Bm1oH12sQLTcNPp7r56m_00mHocSG97_zd7ICUXonA5fwKbPbkE2ZtMJGGVkEdctzQi4QzSwr9prnFYNk5hpmqVuqPQjNnfOJoMF22lUsrq_UfIN6lfSVyvQ7grB3X2mjMZYO3XO9w-U5iLx42qg29md3BP_ni5P4gy9ikTBlHxjLzAtPDlyYZmRdjAbGq7HprEQ7p64v4LU_guu0kvAkhBim3nMplWl8FreQD-CW20aZR0wq12t-KqDWeBywhvexKC3memmDwlHAv9q4Vo2ZK8KtK0CgX7u9J8wXbzdKv-nRnfF_2baTqlYoWUF2h5efl9-n0O6koAMAAA==)): - -```svelte - - - -``` - -The function passed to `$effect` will run when the component mounts, and will re-run after any changes to the values it reads that were declared with `$state` or `$derived` (including those passed in with `$props`). Re-runs are batched (i.e. changing `color` and `size` in the same moment won't cause two separate runs), and happen after any DOM updates have been applied. - -Values that are read asynchronously — after an `await` or inside a `setTimeout`, for example — will _not_ be tracked. Here, the canvas will be repainted when `color` changes, but not when `size` changes ([demo](/#H4sIAAAAAAAAE31T24rbMBD9lUG7kCxsbG_LvrhOoPQP2r7VhSjy2BbIspHGuTT436tLnMtSCiaOzpw5M2dGPrNaKrQs_3VmmnfIcvZ1GNgro9PgD3aPitCdbT8a4ZHCCiMH2pS6JIUEVv5BWMOzJU64fM9evswR0ave3EKLp7r-jFm2iIwri-s9tx5ywDPWNQpaLl9gvYFz4JHotfVqmvBITi9mJA3St4gtF5-qWZUuvEQo5Oa7F8tewT2XrIOsqL2eWpRNS7eGSkpToFZaOEilwODKjBoOLWrco4FtsLQF0XLdoE2S5LGmm6X6QSflBxKod8IW6afssB8_uAslndJuJNA9hWKw9VO91pmJ92XunHlu_J1nMDk8_p_8q0hvO9NFtA47qavcW12fIzJBmM26ZG9ZVjKIs7ke05hdyT0Ixa11Ad-P6ZUtWbgNheI7VJvYQiH14Bz5a-SYxvtwIqHonqsR12ff8ORkQ-chP70T-L9eGO4HvYAFwRh9UCxS13h0YP2CgmoyG5h3setNhWZF_ZDD23AE2ytZwZMQ4jLYgVeV1I2LYgfZBey4aaR-xCppB8VPOdQKjxes4UMgxcVcvwHf4dzAv9K4ko1eScLO5iDQXQFzL5gl7zdJt-nZnXYfbddXspZYsZzMiNPv6S8Bl41G7wMAAA==)): - -```ts -// @filename: index.ts -declare let canvas: { - width: number; - height: number; - getContext( - type: '2d', - options?: CanvasRenderingContext2DSettings - ): CanvasRenderingContext2D; -}; -declare let color: string; -declare let size: number; - -// ---cut--- -$effect(() => { - const context = canvas.getContext('2d'); - context.clearRect(0, 0, canvas.width, canvas.height); - - // this will re-run whenever `color` changes... - context.fillStyle = color; - - setTimeout(() => { - // ...but not when `size` changes - context.fillRect(0, 0, size, size); - }, 0); -}); -``` - -An effect only reruns when the object it reads changes, not when a property inside it changes. (If you want to observe changes _inside_ an object at dev time, you can use [`$inspect`](#$inspect).) - -```svelte - - - - -

{state.value} doubled is {derived.value}

-``` - -An effect only depends on the values that it read the last time it ran. If `a` is true, changes to `b` will [not cause this effect to rerun](/#H4sIAAAAAAAAE3WQ0WrDMAxFf0U1hTow1vcsMfQ7lj3YjlxEXTvEymC4_vfFC6Ewtidxde8RkrJw5DGJ9j2LoO8oWnGZJvEi-GuqIn2iZ1x1istsa6dLdqaJ1RAG9sigoYdjYs0onfYJm7fdMX85q3dE59CylA30CnJtDWxjSNHjq49XeZqXEChcT9usLUAOpIbHA0yzM78oColGhDVofLS3neZSS6mqOz-XD51ZmGOAGKwne-vztk-956CL0kAJsi7decupf4l658EUZX4I8yTWt93jSI5wFC3PC5aP8g0Aje5DcQEAAA==): - -```ts -let a = false; -let b = false; -// ---cut--- -$effect(() => { - console.log('running'); - - if (a || b) { - console.log('inside if block'); - } -}); -``` - -You can return a function from `$effect`, which will run immediately before the effect re-runs, and before it is destroyed ([demo](/#H4sIAAAAAAAAE42SzW6DMBCEX2Vl5RDaVCQ9JoDUY--9lUox9lKsGBvZC1GEePcaKPnpqSe86_m0M2t6ViqNnu0_e2Z4jWzP3pqGbRhdmrHwHWrCUHvbOjF2Ei-caijLTU4aCYRtDUEKK0-ccL2NDstNrbRWHoU10t8Eu-121gTVCssSBa3XEaQZ9GMrpziGj0p5OAccCgSHwmEgJZwrNNihg6MyhK7j-gii4uYb_YyGUZ5guQwzPdL7b_U4ZNSOvp9T2B3m1rB5cLx4zMkhtc7AHz7YVCVwEFzrgosTBMuNs52SKDegaPbvWnMH8AhUXaNUIY6-hHCldQhUIcyLCFlfAuHvkCKaYk8iYevGGgy2wyyJnpy9oLwG0sjdNe2yhGhJN32HsUzi2xOapNpl_bSLIYnDeeoVLZE1YI3QSpzSfo7-8J5PKbwOmdf2jC6JZyD7HxpPaMk93aHhF6utVKVCyfbkWhy-hh9Z3o_2nQIAAA==)). - -```svelte - - -

{count}

- - - -``` - -### When not to use `$effect` - -In general, `$effect` is best considered something of an escape hatch — useful for things like analytics and direct DOM manipulation — rather than a tool you should use frequently. In particular, avoid using it to synchronise state. Instead of this... - -```svelte - -``` - -...do this: - -```svelte - -``` - -> For things that are more complicated than a simple expression like `count * 2`, you can also use [`$derived.by`](#$derived-by). - -You might be tempted to do something convoluted with effects to link one value to another. The following example shows two inputs for "money spent" and "money left" that are connected to each other. If you update one, the other should update accordingly. Don't use effects for this ([demo](/#H4sIAAAAAAAACpVRQWrDMBD8ihA5ONDG7qEXxQ70HXUPir0KgrUsrHWIMf57pXWdlFIKPe6MZmZnNUtjEYJU77N0ugOp5Jv38knS5NMQroAEcQ79ODQJKUMzWE-n2tWEQIJ60igq8VIUxw0LHhxFbBdIE2TF_s4gmG8Ea5mM9A6MgYaybC-qk5gTlDT8fg15Xo3ZbPlTti2w6ZLNQ1bmjw6uRH0G5DqldX6MjWL1qpaDdheopThb16qrxhGqmX0X0elbNbP3InKWfjH5hvKYku7u_wtKC_-aw8Q9Jk0_UgJNCOvvJHC7SGuDRz0pYRBuxxW7aK9EcXiFbr0NX4bl8cO7vrXGQisVDSMsH8sniirsuSsCAAA=)): - -```svelte - - - - - -``` - -Instead, use callbacks where possible ([demo](/#H4sIAAAAAAAACo1SMW6EMBD8imWluFNyQIo0HERKf13KkMKB5WTJGAsvp0OIv8deMEEJRcqdmZ1ZjzzyWiqwPP0YuRYN8JS_GcOfOA7GD_YGCsHNtu270iOZLTtp8LXQBSpAhi0KxXL2nCTngFkDGh32YFEgHJLjyiioNwTtEunoutclylaz3lSOfPceBziy0ZMFBs9HiFB0V8DoJlQP55ldfOdjTvMBRE275hcn33gv2_vWITh4e3GwzuKfNnSmxBcoKiaT2vSuG1diXvBO6CsUnJFrPpLhxFpNonzcvHdijbjnI0VNLCavRR8HlEYfvcb9O9mf_if4QuBOLqnXWD_9SrU4KJg_ggdDm5W0RokhZbWC-1LiVZiUJdELNJvqaN39raatZC2h4il2PUyf0zcIbC-7lgIAAA==)): - -```svelte - - - - - -``` - -If you need to use bindings, for whatever reason (for example when you want some kind of "writable `$derived`"), consider using getters and setters to synchronise state ([demo](/#H4sIAAAAAAAACpVRQW7DIBD8CkI9JFIau4deiB2p7yg9kHhtIWGMYG3Fsvh7ARs3qnrpCWZGM8MuC22lAkfZ50K16IEy-mEMPVGcTQRuAoUQsBtGe49M5e5WGrxyzVEBEhxQKFKTt7K8ZM4Z0Bi4F4cC4VAeo7JpCtooLRFz7AIzCTXC4ZgpjhZwtHpLfl3TLqvoT-vpdt_0ZMy92TllVzx8AFXx83pdKXEDlQappDZjmCUMXXNqhe6AU3KTumGppV5StCe9eNRLivekSNZNKTKbYGza0_9XFPdzTvc_257kvTJyvxodzgrWP4pkXlEjnVFiZqRV8NiW0wnDSHl-hz4RPm0p2cO390MjWwkNZWhD5Zf_BkCCa6AxAgAA)): - -```svelte - - - - - -``` - -If you absolutely have to update `$state` within an effect and run into an infinite loop because you read and write to the same `$state`, use [untrack](functions#untrack). - -### What this replaces - -The portions of `$: {}` that are triggering side-effects can be replaced with `$effect` while being careful to migrate updates of reactive variables to use `$derived`. There are some important differences: - -- Effects only run in the browser, not during server-side rendering -- They run after the DOM has been updated, whereas `$:` statements run immediately _before_ -- You can return a cleanup function that will be called whenever the effect refires - -Additionally, you may prefer to use effects in some places where you previously used `onMount` and `afterUpdate` (the latter of which will be deprecated in Svelte 5). There are some differences between these APIs as `$effect` should not be used to compute reactive values and will be triggered each time a referenced reactive variable changes (unless using `untrack`). - -## `$effect.pre` - -In rare cases, you may need to run code _before_ the DOM updates. For this we can use the `$effect.pre` rune: - -```svelte - - -
- {#each messages as message} -

{message}

- {/each} -
-``` - -Apart from the timing, `$effect.pre` works exactly like [`$effect`](#$effect) — refer to its documentation for more info. - -### What this replaces - -Previously, you would have used `beforeUpdate`, which — like `afterUpdate` — is deprecated in Svelte 5. - -## `$effect.tracking` - -The `$effect.tracking` rune is an advanced feature that tells you whether or not the code is running inside a tracking context, such as an effect or inside your template ([demo](/#H4sIAAAAAAAACn3PQWrDMBAF0KtMRSA2xPFeUQU5R92FUUZBVB4N1rgQjO9eKSlkEcjyfz6PmVX5EDEr_bUqGidUWp2Z1UHJjWvIvxgFS85pmV1tTHZzYLEDDeIS5RTxGNO12QcClyZOhCSQURbW-wPs0Ht0cpR5dD-Brk3bnqDvwY8xYzGK8j9pmhY-Lay1eqUfm3eizEsFZWtPA5n-eSYZtkUQnDiOghrWV2IzPVswH113d6DrbHl6SpfgA16UruX2vf0BWo7W2y8BAAA=)): - -```svelte - - -

in template: {$effect.tracking()}

-``` - -This allows you to (for example) add things like subscriptions without causing memory leaks, by putting them in child effects. - -## `$effect.root` - -The `$effect.root` rune is an advanced feature that creates a non-tracked scope that doesn't auto-cleanup. This is useful for -nested effects that you want to manually control. This rune also allows for creation of effects outside of the component initialisation phase. - -```svelte - -``` - -## `$props` - -To declare component props, use the `$props` rune: - -```js -let { optionalProp = 42, requiredProp } = $props(); -``` - -You can use familiar destructuring syntax to rename props, in cases where you need to (for example) use a reserved word like `catch` in ``: - -```js -let { catch: theCatch } = $props(); -``` - -To get all properties, use rest syntax: - -```js -let { a, b, c, ...everythingElse } = $props(); -``` - -You can also use an identifier: - -```js -let props = $props(); -``` - -If you're using TypeScript, you can declare the prop types: - - -```ts -interface MyProps { - required: string; - optional?: number; - partOfEverythingElse?: boolean; -}; - -let { required, optional, ...everythingElse }: MyProps = $props(); -``` - -> In an earlier preview, `$props()` took a type argument. This caused bugs, since in a case like this... -> -> ```ts -> // @errors: 2558 -> let { x = 42 } = $props<{ x?: string }>(); -> ``` -> -> ...TypeScript [widens the type](https://www.typescriptlang.org/play?#code/CYUwxgNghgTiAEAzArgOzAFwJYHtXwBIAHGHIgZwB4AVeAXnilQE8A+ACgEoAueagbgBQgiCAzwA3vAAe9eABYATPAC+c4qQqUp03uQwwsqAOaqOnIfCsB6a-AB6AfiA) of `x` to be `string | number`, instead of erroring. - -If you're using JavaScript, you can declare the prop types using JSDoc: - -```js -/** @type {{ x: string }} */ -let { x } = $props(); - -// or use @typedef if you want to document the properties: - -/** - * @typedef {Object} MyProps - * @property {string} y Some documentation - */ - -/** @type {MyProps} */ -let { y } = $props(); -``` - -By default props are treated as readonly, meaning reassignments will not propagate upwards and mutations will result in a warning at runtime in development mode. You will also get a runtime error when trying to `bind:` to a readonly prop in a parent component. To declare props as bindable, use [`$bindable()`](#$bindable). - -### What this replaces - -`$props` replaces the `export let` and `export { x as y }` syntax for declaring props. It also replaces `$$props` and `$$restProps`, and the little-known `interface $$Props {...}` construct. - -Note that you can still use `export const` and `export function` to expose things to users of your component (if they're using `bind:this`, for example). - -## `$bindable` - -To declare props as bindable, use `$bindable()`. Besides using them as regular props, the parent can (_can_, not _must_) then also `bind:` to them. - -```svelte - -``` - -You can pass an argument to `$bindable()`. This argument is used as a fallback value when the property is `undefined`. - -```svelte - -``` - -Note that the parent is not allowed to pass `undefined` to a property with a fallback if it `bind:`s to that property. - -## `$inspect` - -The `$inspect` rune is roughly equivalent to `console.log`, with the exception that it will re-run whenever its -argument changes. `$inspect` tracks reactive state deeply, meaning that updating something inside an object -or array using [fine-grained reactivity](/docs/fine-grained-reactivity) will cause it to re-fire. ([Demo:](/#H4sIAAAAAAAACkWQ0YqDQAxFfyUMhSotdZ-tCvu431AXtGOqQ2NmmMm0LOK_r7Utfby5JzeXTOpiCIPKT5PidkSVq2_n1F7Jn3uIcEMSXHSw0evHpAjaGydVzbUQCmgbWaCETZBWMPlKj29nxBDaHj_edkAiu12JhdkYDg61JGvE_s2nR8gyuBuiJZuDJTyQ7eE-IEOzog1YD80Lb0APLfdYc5F9qnFxjiKWwbImo6_llKRQVs-2u91c_bD2OCJLkT3JZasw7KLA2XCX31qKWE6vIzNk1fKE0XbmYrBTufiI8-_8D2cUWBA_AQAA)) - -```svelte - - - - -``` - -`$inspect` returns a property `with`, which you can invoke with a callback, which will then be invoked instead of `console.log`. The first argument to the callback is either `"init"` or `"update"`, all following arguments are the values passed to `$inspect`. [Demo:](/#H4sIAAAAAAAACkVQ24qDMBD9lSEUqlTqPlsj7ON-w7pQG8c2VCchmVSK-O-bKMs-DefKYRYx6BG9qL4XQd2EohKf1opC8Nsm4F84MkbsTXAqMbVXTltuWmp5RAZlAjFIOHjuGLOP_BKVqB00eYuKs82Qn2fNjyxLtcWeyUE2sCRry3qATQIpJRyD7WPVMf9TW-7xFu53dBcoSzAOrsqQNyOe2XUKr0Xi5kcMvdDB2wSYO-I9vKazplV1-T-d6ltgNgSG1KjVUy7ZtmdbdjqtzRcphxMS1-XubOITJtPrQWMvKnYB15_1F7KKadA_AQAA) - -```svelte - - - -``` - -A convenient way to find the origin of some change is to pass `console.trace` to `with`: - -```js -// @errors: 2304 -$inspect(stuff).with(console.trace); -``` - -> `$inspect` only works during development. - -## `$host` - -Retrieves the `this` reference of the custom element that contains this component. Example: - -```svelte - - - - - -``` - -> Only available inside custom element components, and only on the client-side - -## How to opt in - -Current Svelte code will continue to work without any adjustments. Components using the Svelte 4 syntax can use components using runes and vice versa. - -The easiest way to opt in to runes mode is to just start using them in your code. Alternatively, you can force the compiler into runes or non-runes mode either on a per-component basis... - - -```svelte - - - -``` - -...or for your entire app: - -```js -/// file: svelte.config.js -export default { - compilerOptions: { - runes: true - } -}; -``` diff --git a/sites/svelte-5-preview/src/routes/docs/content/01-api/03-snippets.md b/sites/svelte-5-preview/src/routes/docs/content/01-api/03-snippets.md deleted file mode 100644 index b3fe34d21a3b..000000000000 --- a/sites/svelte-5-preview/src/routes/docs/content/01-api/03-snippets.md +++ /dev/null @@ -1,269 +0,0 @@ ---- -title: Snippets ---- - -Snippets, and _render tags_, are a way to create reusable chunks of markup inside your components. Instead of writing duplicative code like [this](/#H4sIAAAAAAAAE5VUYW-kIBD9K8Tmsm2yXXRzvQ-s3eR-R-0HqqOQKhAZb9sz_vdDkV1t000vRmHewMx7w2AflbIGG7GnPlK8gYhFv42JthG-m9Gwf6BGcLbVXZuPSGrzVho8ZirDGpDIhldgySN5GpEMez9kaNuckY1ANJZRamRuu2ZnhEZt6a84pvs43mzD4pMsUDDi8DMkQFYCGdkvsJwblFq5uCik9bmJ4JZwUkv1eoknWigX2eGNN6aGXa6bjV8ybP-X7sM36T58SVcrIIV2xVIaA41xeD5kKqWXuqpUJEefOqVuOkL9DfBchGrzWfu0vb-RpTd3o-zBR045Ga3HfuE5BmJpKauuhbPtENlUF2sqR9jqpsPSxWsMrlngyj3VJiyYjJXb1-lMa7IWC-iSk2M5Zzh-SJjShe-siq5kpZRPs55BbSGU5YPyte4vVV_VfFXxVb10dSLf17pS2lM5HnpPxw4Zpv6x-F57p0jI3OKlVnhv5V9wPQrNYQQ9D_f6aGHlC89fq1Z3qmDkJCTCweOGF4VUFSPJvD_DhreVdA0eu8ehJJ5x91dBaBkpWm3ureCFPt3uzRv56d4kdp-2euG38XZ6dsnd3ZmPG9yRBCrzRUvi-MccOdwz3qE-fOZ7AwAhlrtTUx3c76vRhSwlFBHDtoPhefgHX3dM0PkEAAA=)... - -```svelte -{#each images as image} - {#if image.href} - -
- {image.caption} -
{image.caption}
-
-
- {:else} -
- {image.caption} -
{image.caption}
-
- {/if} -{/each} -``` - -...you can write [this](/#H4sIAAAAAAAAE5VUYW-bMBD9KxbRlERKY4jWfSA02n5H6QcXDmwVbMs-lnaI_z6D7TTt1moTAnPvzvfenQ_GpBEd2CS_HxPJekjy5IfWyS7BFz0b9id0CM62ajDVjBS2MkLjqZQldoBE9KwFS-7I_YyUOPqlRGuqnKw5orY5pVpUduj3mitUln5LU3pI0_UuBp9FjTwnDr9AHETLMSeHK6xiGoWSLi9yYT034cwSRjohn17zcQPNFTs8s153sK9Uv_Yh0-5_5d7-o9zbD-UqCaRWrllSYZQxLw_HUhb0ta-y4NnJUxfUvc7QuLJSaO0a3oh2MLBZat8u-wsPnXzKQvTtVVF34xK5d69ThFmHEQ4SpzeVRediTG8rjD5vBSeN3E5JyHh6R1DQK9-iml5kjzQUN_lSgVU8DhYLx7wwjSvRkMDvTjiwF4zM1kXZ7DlF1eN3A7IG85e-zRrYEjjm0FkI4Cc7Ripm0pHOChexhcWXzreeZyRMU6Mk3ljxC9w4QH-cQZ_b3T5pjHxk1VNr1CDrnJy5QDh6XLO6FrLNSRb2l9gz0wo3S6m7HErSgLsPGMHkpDZK31jOanXeHPQz-eruLHUP0z6yTbpbrn223V70uMXNSpQSZjpL0y8hcxxpNqA6_ql3BQAxlxvfpQ_uT9GrWjQC6iRHM8D0MP0GQsIi92QEAAA=): - -```diff -+{#snippet figure(image)} -
- {image.caption} -
{image.caption}
-
-+{/snippet} - -{#each images as image} - {#if image.href} - -+ {@render figure(image)} - - {:else} -+ {@render figure(image)} - {/if} -{/each} -``` - -Snippet parameters can be destructured ([demo](/#H4sIAAAAAAAAE5VTYW-bMBD9KyeiKYlEY4jWfSAk2n5H6QcXDmwVbMs2SzuL_z6DTRqp2rQJ2Ycfd_ced2eXtLxHkxRPLhF0wKRIfiiVpIl9V_PB_MTeoj8bOep6RkpTa67spRKV7dECH2iHBs7wNCOVdcFU1ui6gC2zVpmCEMVrMw4HxaSVhnzLMnLMsm26Ol95Y1kBHr9BDHnHbAHHO6ymynIpfF7LuAncwKgBCj0Xrx_5mMb2jh3f6KB6PNRy2AaXKf1fuY__KPfxj3KlQGikL5aQdpUxm-dTJUryUVdRsvwSqEviX2fIbYzgSvmCt7wbNe4ceMUpRIoUFkkpBBkw7ZfMZXC-BLKSDx3Q3p5djJrA-SR-X4K9DdHT6u-jo-flFlKSO3ThIDcSR6LIKUhGWrN1QGhs16LLbXgbjoe5U1PkozCfzu7uy2WtpfuuUTSo1_9ffPZrJKGLoyuwNxjBv0Q4wmdSR2aFi9jS2Pc-FIrlEKeilcI-GP4LfVtxOM1gyO1XSLp6vtD6tdNyFE0BV8YtngKuaNNw0RWQx_jKDlR33M9E5h-PQhZxfxEt6gIaLdWDYbSR191RvcFXv_LMb7p7obssXZ5Dvt_f9HgzdzZKibOZZ9mXmHkdTTpaefqsd4OIay4_hksd_I0fZMNbjk1SWD3i9Dz9BpdEPu8sBAAA)): - -```svelte -{#snippet figure({ src, caption, width, height })} -
- {caption} -
{caption}
-
-{/snippet} -``` - -Like function declarations, snippets can have an arbitrary number of parameters, which can have default values. You cannot use rest parameters however. - -## Snippet scope - -Snippets can be declared anywhere inside your component. They can reference values declared outside themselves, for example in the ` - -{#snippet hello(name)} -

hello {name}! {message}!

-{/snippet} - -{@render hello('alice')} -{@render hello('bob')} -``` - -...and they are 'visible' to everything in the same lexical scope (i.e. siblings, and children of those siblings): - -```svelte -
- {#snippet x()} - {#snippet y()}...{/snippet} - - - {@render y()} - {/snippet} - - - {@render y()} -
- - -{@render x()} -``` - -Snippets can reference themselves and each other ([demo](/#H4sIAAAAAAAAE2WPTQqDMBCFrxLiRqH1Zysi7TlqF1YnENBJSGJLCYGeo5tesUeosfYH3c2bee_jjaWMd6BpfrAU6x5oTvdS0g01V-mFPkNnYNRaDKrxGxto5FKCIaeu1kYwFkauwsoUWtZYPh_3W5FMY4U2mb3egL9kIwY0rbhgiO-sDTgjSEqSTvIDs-jiOP7i_MHuFGAL6p9BtiSbOTl0GtzCuihqE87cqtyam6WRGz_vRcsZh5bmRg3gju4Fptq_kzQBAAA=)): - -```svelte -{#snippet blastoff()} - 🚀 -{/snippet} - -{#snippet countdown(n)} - {#if n > 0} - {n}... - {@render countdown(n - 1)} - {:else} - {@render blastoff()} - {/if} -{/snippet} - -{@render countdown(10)} -``` - -## Passing snippets to components - -Within the template, snippets are values just like any other. As such, they can be passed to components as props ([demo](/#H4sIAAAAAAAAE41SwY6bMBD9lRGplKQlYRMpF5ZF7T_0ttmDwSZYJbZrT9pGlv-9g4Fkk-xhxYV5vHlvhjc-aWQnXJK_-kSxo0jy5IcxSZrg2fSF-yM6FFQ7fbJ1jxSuttJguVd7lEejLcJPVnUCGquPMF9nsVoPjfNnohGx1sohMU4SHbzAa4_t0UNvmcOcGUNDzFP4jeccdikYK2v6sIWQ3lErpui5cDdPF_LmkVy3wlp5Vd5e2U_rHYSe_kYjFtl1KeVnTkljBEIrGBd2sYy8AtsyLlBk9DYhJHtTR_UbBDWybkR8NkqHWyOr_y74ZMNLz9f9AoG6ePkOJLMHLBp-xISvcPf11r0YUuMM2Ysfkgngh5XphUYKkJWU_FFz2UjBkxztSYT0cihR4LOn0tGaPrql439N-7Uh0Dl8MVYbt1jeJ1Fg7xDb_Uw2Y18YQqZ_S2U5FH1pS__dCkWMa3C0uR0pfQRTg89kE4bLLLDS_Dxy_Eywuo1TAnPAw4fqY1rvtH3W9w35ZZMgvU3jq8LhedwkguCHRhT_cMU6eVA5dKLB5wGutCWjlTOslupAxxrxceKoD2hzhe2qbmXHF1v1bbOcNCtW_zpYfVI8h5kQ4qY3mueHTlesW2C7TOEO4hcdwzgf3Nc7cZxUKKC4yuNhvIX_MlV_Xk0EAAA=)): - -```svelte - - -{#snippet header()} - fruit - qty - price - total -{/snippet} - -{#snippet row(d)} - {d.name} - {d.qty} - {d.price} - {d.qty * d.price} -{/snippet} - - -``` - -As an authoring convenience, snippets declared directly _inside_ a component implicitly become props _on_ the component ([demo](/#H4sIAAAAAAAAE41Sy27bMBD8lYVcwHYrW4kBXxRFaP-htzgHSqQsojLJkuu2BqF_74qUrfhxCHQRh7MzO9z1SSM74ZL8zSeKHUSSJz-MSdIET2Y4uD-iQ0Fnp4-2HpDC1VYaLHdqh_JgtEX4yapOQGP1AebrLJzWsXD-QjQi1lo5JMZRooNXeBuwHXoYLHOYM2OoiXkKv_GUwzYFY2VNFxvo0xtqxRR9F-7z04X8fE-uW2GtnJQ3E_tpvYV-oL9Ti0U2hVJFjMMZslcfW-5DWj9zShojEFrBuLCLZR_9CmzLQCwy-psw8rxBgvkNhhpZd8F8NppE7Stbq_8u-GTKS8_XQ9Keqnl5BZP1AzTYP2bDV7i7_9hLEeda0iocNJeNFDzJ0R5Fn142JzA-uzsdBfLhldPxPdMhIPS0H1-M1cYtlnejwdBDfBXZjHXTFOg4BhuOtvTfrVDEmAZG2ew5ezYV-Ew2fVzVAivNTyPHzwSr29AlMAe8f6g-zuWDts-GusAmdBSkv3P7qnB4GpMEEHwsRPEPV6yTe5VDJxp8iXClLRmtnGG1VHva3oCPHQd9QJsrbFd1Kzu-2Khvz8uzZsXqX3urj4rnMBNCXNUG83zf6Yp1C2yXKdxA_KJjGOfRfb0Vh7MKDShEuV-M9_4_nq6svF4EAAA=)): - -```svelte - -
- {#snippet header()} - - - - - {/snippet} - - {#snippet row(d)} - - - - - {/snippet} -
fruitqtypricetotal{d.name}{d.qty}{d.price}{d.qty * d.price}
-``` - -Any content inside the component tags that is _not_ a snippet declaration implicitly becomes part of the `children` snippet ([demo](/#H4sIAAAAAAAAE41S247aMBD9lVFYCegGsiDxks1G7T_0bdkHJ3aI1cR27aEtsvzvtZ0LZeGhiiJ5js-cmTMemzS8YybJ320iSM-SPPmmVJImeFEhML9Yh8zHRp51HZDC1JorLI_iiLxXUiN8J1XHoNGyh-U2i9F2SFy-epon1lIY9IwzRwNv8B6wI1oIJXNYEqV8E8sUfuIlh0MKSvPaX-zBpZ-oFRH-m7m7l5m8uyfXLdOaX5X3V_bL9gAu0D98i0V2NSWKwQ4lSN7s0LKLbgtsyxgXmT9NiBe-iaP-DYISSTcj4bcLI7hSDEHL3yu6dkPfBdLS0m1o3nk-LW9gX-gBGss9ZsMXuLu32VjZBdfRaelft5eUN5zRJEd9Zi6dlyEy_ncdOm_IxsGlULe8o5qJNFgE5x_9SWmpzGp9N2-MXQxz4c2cOQ-lZWQyF0Jd2q_-mjI9U1fr4FBPE8iuKTbjjRt2sMBK0svIsQtG6jb2CsQAdQ_1x9f5R9tmIS-yPToK-tNkQRQGL6ObCIIdEpH9wQ3p-Enk0LEGXwe4ktoX2hhFai5Ofi0jPnYc9QF1LrDdRK-rvXjerSfNitQ_TlqeBc1hwRi7yY3F81MnK9KtsF2n8Amis44ilA7VtwfWTyr-kaKV-_X4cH8BTOhfRzcEAAA=)): - -```diff - -- {#snippet header()} -- -- -- -- -- {/snippet} -+ -+ -+ -+ - - -
fruitqtypricetotalfruitqtypricetotal
-``` - -```diff - - - -- {#if header} -+ {#if children} - -- {@render header()} -+ {@render children()} - - {/if} - - -
-``` - -> Note that you cannot have a prop called `children` if you also have content inside the component — for this reason, you should avoid having props with that name - -## Typing snippets - -Snippets implement the `Snippet` interface imported from `'svelte'`: - -```diff -- -``` - -With this change, red squigglies will appear if you try and use the component without providing a `data` prop and a `row` snippet. Notice that the type argument provided to `Snippet` is a tuple, since snippets can have multiple parameters. - -We can tighten things up further by declaring a generic, so that `data` and `row` refer to the same type: - -```diff -- -``` - -## Creating snippets programmatically - -In advanced scenarios, you may need to create a snippet programmatically. For this, you can use [`createRawSnippet`](/docs/imports#svelte-createrawsnippet) - -## Snippets and slots - -In Svelte 4, content can be passed to components using [slots](https://svelte.dev/docs/special-elements#slot). Snippets are more powerful and flexible, and as such slots are deprecated in Svelte 5. - -They continue to work, however, and you can mix and match snippets and slots in your components. - -When using custom elements, you should still use `` like before. In a future version, when Svelte removes its internal version of slots, it will leave those slots as-is, i.e. output a regular DOM tag instead of transforming it. diff --git a/sites/svelte-5-preview/src/routes/docs/content/01-api/04-event-handlers.md b/sites/svelte-5-preview/src/routes/docs/content/01-api/04-event-handlers.md deleted file mode 100644 index 5124ae291d3e..000000000000 --- a/sites/svelte-5-preview/src/routes/docs/content/01-api/04-event-handlers.md +++ /dev/null @@ -1,205 +0,0 @@ ---- -title: Event handlers ---- - -Event handlers have been given a facelift in Svelte 5. Whereas in Svelte 4 we use the `on:` directive to attach an event listener to an element, in Svelte 5 they are properties like any other: - -```diff - - -- -``` - -Since they're just properties, you can use the normal shorthand syntax... - -```svelte - - - -``` - -...though when using a named event handler function it's usually better to use a more descriptive name. - -Traditional `on:` event handlers will continue to work, but are deprecated in Svelte 5. - -## Component events - -In Svelte 4, components could emit events by creating a dispatcher with [`createEventDispatcher`](https://svelte.dev/docs/svelte#createeventdispatcher). - -This function is deprecated in Svelte 5. Instead, components should accept _callback props_ - which means you then pass functions as properties to these components ([demo](/#H4sIAAAAAAAACo1US27bMBC9yoBtELu2ZDmAG0CRhPYG3VddyPIwIUKRgjiOkwrcd9VFL5BV75cjFKQo2e5_IQnzeW-GM3zqGRcSDUs_9kxVDbKUvW9btmT01DrDPKAkZEtm9L6rnSczdSdaKkpVkmha3RF82Dct8E43cBmvnBEPsMsbl-QeiQRGfEbI4bWhinC23sxvxsh23xk6hnglDfqoKonvVU1CK-jQIM3m0HtOCmzrzVCDRg4P9j5bqmx1bFZlrjPfteKyIsz7WasP2M0hL85YFzn4QGAWHGbeX8D1Zj41S90-1LHuvcM_kp4QJPNhDNFpCUew8i32rwQfCnjObLsn0gq0qqWo7_Pez8AWCg-wraTUWmWrIcevIzNtpaCWlTF5ybZaNyUrXp6_fc9WLlKUqk9RGrS_SR7oSgaGniTmJTN1JTGFPomTNbzxbduSFcORXp6_fvEkE_FKcOun7PE-zRcIM2i1EW6NKXDxiLswWomcUkiCRbo9Ggexo7sU1klyETx3KG7v6MzFtaLIdea9D4eRCB8pqqS4VSnUqGhapRQKo4nnZmxNuJQIH1CRSUFpNV0g94nDbMajUFep8TB-SJDEV-YcoXUzpldKNNWQ7d1JvDHAdXeout0Z6t09PvGuatDAKT65gB7CMpL4LdjBfbU5819vxoAbz0lkcA9aCJthS9boneACdyx119guJ_E7jfyv-p10ewhqWkJQAFin5LbTrZkdJe5v-1HiXvzn6vz5rs-8hAJ7EJUtgn1y7f8ADN1MwGD_G-gBUWSLaModfnA-kELvvxb-Bl8sbLGY4L_O-5P9ATwVcA54BQAA)): - -```svelte - - - { - size += power; - if (size > 75) burst = true; - }} - deflate={(power) => { - if (size > 0) size -= power; - }} -/> - -{#if burst} - - 💥 -{:else} - - 🎈 - -{/if} -``` - -```svelte - - - - - -Pump power: {power} - -``` - -## Bubbling events - -Instead of doing ` -``` - -Note that this also means you can 'spread' event handlers onto the element along with other props: - -```svelte - - - -``` - -## Event modifiers - -In Svelte 4, you can add event modifiers to handlers: - -```svelte - -``` - -Modifiers are specific to `on:` and as such do not work with modern event handlers. Adding things like `event.preventDefault()` inside the handler itself is preferable, since all the logic lives in one place rather than being split between handler and modifiers. - -Since event handlers are just functions, you can create your own wrappers as necessary: - -```svelte - - - -``` - -There are three modifiers — `capture`, `passive` and `nonpassive` — that can't be expressed as wrapper functions, since they need to be applied when the event handler is bound rather than when it runs. - -For `capture`, we add the modifier to the event name: - -```svelte - -``` - -Changing the [`passive`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#using_passive_listeners) option of an event handler, meanwhile, is not something to be done lightly. If you have a use case for it — and you probably don't! - then you will need to use an action to apply the event handler yourself. - -## Multiple event handlers - -In Svelte 4, this is possible: - -```svelte - -``` - -This is something of an anti-pattern, since it impedes readability (if there are many attributes, it becomes harder to spot that there are two handlers unless they are right next to each other) and implies that the two handlers are independent, when in fact something like `event.stopImmediatePropagation()` inside `one` would prevent `two` from being called. - -Duplicate attributes/properties on elements — which now includes event handlers — are not allowed. Instead, do this: - -```svelte - -``` - -When spreading props, local event handlers must go _after_ the spread, or they risk being overwritten: - -```svelte - -``` - -## Why the change? - -By deprecating `createEventDispatcher` and the `on:` directive in favour of callback props and normal element properties, we: - -- reduce Svelte's learning curve -- remove boilerplate, particularly around `createEventDispatcher` -- remove the overhead of creating `CustomEvent` objects for events that may not even have listeners -- add the ability to spread event handlers -- add the ability to know which event handlers were provided to a component -- add the ability to express whether a given event handler is required or optional -- increase type safety (previously, it was effectively impossible for Svelte to guarantee that a component didn't emit a particular event) diff --git a/sites/svelte-5-preview/src/routes/docs/content/01-api/05-imports.md b/sites/svelte-5-preview/src/routes/docs/content/01-api/05-imports.md deleted file mode 100644 index 7cbec56e17ac..000000000000 --- a/sites/svelte-5-preview/src/routes/docs/content/01-api/05-imports.md +++ /dev/null @@ -1,277 +0,0 @@ ---- -title: Imports ---- - -As well as runes, Svelte 5 introduces a handful of new things you can import, alongside existing ones like `getContext`, `setContext` and `tick`. - -## `svelte` - -### `flushSync` - -Forces any pending effects (including DOM updates) to be applied immediately, rather than in the future. This is mainly useful in a testing context — you'll rarely need it in application code. - -```svelte - - -{count} - -``` - -### `mount` - -Instantiates a component and mounts it to the given target: - -```js -// @errors: 2322 -import { mount } from 'svelte'; -import App from './App.svelte'; - -const app = mount(App, { - target: document.querySelector('#app'), - props: { some: 'property' } -}); -``` - -Note that unlike calling `new App(...)` in Svelte 4, things like effects (including `onMount` callbacks, and action functions) will not run during `mount`. If you need to force pending effects to run (in the context of a test, for example) you can do so with `flushSync()`. - -### `hydrate` - -Like `mount`, but will reuse up any HTML rendered by Svelte's SSR output (from the [`render`](#svelte-server-render) function) inside the target and make it interactive: - -```js -// @errors: 2322 -import { hydrate } from 'svelte'; -import App from './App.svelte'; - -const app = hydrate(App, { - target: document.querySelector('#app'), - props: { some: 'property' } -}); -``` - -As with `mount`, effects will not run during `hydrate` — use `flushSync()` immediately afterwards if you need them to. - -### `unmount` - -Unmounts a component created with [`mount`](#svelte-mount) or [`hydrate`](#svelte-hydrate): - -```js -// @errors: 1109 -import { mount, unmount } from 'svelte'; -import App from './App.svelte'; - -const app = mount(App, {...}); - -// later -unmount(app); -``` - -### `untrack` - -To prevent something from being treated as an `$effect`/`$derived` dependency, use `untrack`: - -```svelte - -``` - -### `createRawSnippet` - -An advanced API designed for people building frameworks that integrate with Svelte, `createRawSnippet` allows you to create [snippets](/docs/snippets) programmatically for use with `{@render ...}` tags: - -```js -import { createRawSnippet } from 'svelte'; - -const greet = createRawSnippet((name) => { - return { - render: () => ` -

Hello ${name()}!

- `, - setup: (node) => { - $effect(() => { - node.textContent = `Hello ${name()}!`; - }); - } - }; -}); -``` - -The `render` function is called during server-side rendering, or during `mount` (but not during `hydrate`, because it already ran on the server), and must return HTML representing a single element. - -The `setup` function is called during `mount` or `hydrate` with that same element as its sole argument. It is responsible for ensuring that the DOM is updated when the arguments change their value — in this example, when `name` changes: - -```svelte -{@render greet(name)} -``` - -If `setup` returns a function, it will be called when the snippet is unmounted. If the snippet is fully static, you can omit the `setup` function altogether. - -## `svelte/reactivity` - -Svelte provides reactive `SvelteMap`, `SvelteSet`, `SvelteDate` and `SvelteURL` classes. These can be imported from `svelte/reactivity` and used just like their native counterparts. [Demo:](https://svelte-5-preview.vercel.app/#H4sIAAAAAAAAE32QwUrEMBBAf2XMpQrb9t7tFrx7UjxZYWM6NYFkEpJJ16X03yWK9OQeZ3iPecwqZmMxie5tFSQdik48hiAOgq-hDGlByygOIvkcVdn0SUUTeBhpZOOCjwwrvPxgr89PsMEcvYPqV2wjSsVmMXytjiMVR3lKDDlaOAHhZVfvK80cUte2-CVdsNgo79ogWVcPx5H6dj9M_V1dg9KSPjEBe2CNCZumgboeRuoNhczwYWjqFmkzntYcbROiZ6-83f5HtE9c3nADKUF_yEi9jnvQxVgLOUySEc464nwGSRMsRiEsGJO8mVeEbRAH4fxkZoOT6Dhm3N63b9_bGfOlAQAA) - -```svelte - - - - - - - -
- - - -``` - -## `svelte/events` - -Where possible, event handlers added with [attributes like `onclick`](/docs/event-handlers) use a technique called _event delegation_. It works by creating a single handler for each event type on the root DOM element, rather than creating a handler for each element, resulting in better performance and memory usage. - -Delegated event handlers run after other event handlers. In other words, a handler added programmatically with [`addEventListener`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener) will run _before_ a handler added declaratively with `onclick`, regardless of their relative position in the DOM ([demo](/#H4sIAAAAAAAAE41Sy2rDMBD8lUUXJxDiu-sYeugt_YK6h8RaN6LyykgrQzH6965shxJooQc_RhrNzA6aVW8sBlW9zYouA6pKPY-jOij-GjMIE1pGwcFF3-WVOnTejNy01LIZRucZZnD06iIxJOi9G6BYjxVPmZQfiwzaTBkL2ti73R5ODcwLiftIHRtHcLuQtuhlc9tpuSyBbyZAuLloNfhIELBzpO8E-Q_O4tG6j13hIqO_y0BvPOpiv0bhtJ1Y3pLoeNH6ZULiswmMJLZFZ033WRzuAvstdMseOXqCh9SriMfBTfgPnZxg-aYM6_KnS6pFCK6GdJVHPc0C01JyfY0slUnHi-JpfgjwSzUycdgmfOjFEP3RS1qdhJ8dYMDFt1yNmxxU0jRyCwanTW9Qq4p9xPSevgHI3m43QAIAAA==)). It also means that calling `event.stopPropagation()` inside a declarative handler _won't_ prevent the programmatic handler (created inside an action, for example) from running. - -To preserve the relative order, use `on` rather than `addEventListener` ([demo](/#H4sIAAAAAAAAE3VRy26DMBD8lZUvECkqdwpI_YB-QdJDgpfGqlkjex2pQv73rnmoStQeMB52dnZmmdVgLAZVn2ZFlxFVrd6mSR0Vf08ZhDtaRsHBRd_nL03ovZm4O9OZzTg5zzCDo3cXiSHB4N0IxdpWvD6RnuoV3pE4rLT8WGTQ5p6xoE20LA_QdjAvJB4i9WxE6nYhbdFLcaucuaqAbyZAuLloNfhIELB3pHeC3IOz-GLdZ1m4yOh3GRiMR10cViucto7l9MjRk9gvxdsRit6a_qs47q1rT8qvpvpdDjXChqshXWdT7SwwLVtrrpElnAguSu38EPCPEOItbF4eEhiifxKkdZLw8wQYcZlbrYO7bFTcdPJbR6fNYFCrmn3E9JF-AJZOg9MRAgAA)): - -```js -// @filename: index.ts -const element: Element = null as any; -// ---cut--- -import { on } from 'svelte/events'; - -const off = on(element, 'click', () => { - console.log('element was clicked'); -}); - -// later, if we need to remove the event listener: -off(); -``` - -`on` also accepts an optional fourth argument which matches the options argument for `addEventListener`. - -## `svelte/server` - -### `render` - -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: - -```js -// @errors: 2724 2305 2307 -import { render } from 'svelte/server'; -import App from './App.svelte'; - -const result = render(App, { - props: { some: 'property' } -}); -``` - -If the `css` compiler option was set to `'injected'`, ` diff --git a/sites/svelte-5-preview/src/routes/status/data.json/+server.js b/sites/svelte-5-preview/src/routes/status/data.json/+server.js deleted file mode 100644 index cfa65b0065f0..000000000000 --- a/sites/svelte-5-preview/src/routes/status/data.json/+server.js +++ /dev/null @@ -1,6 +0,0 @@ -import { json } from '@sveltejs/kit'; -import results from '../results.json'; - -export function GET() { - return json(results); -} diff --git a/sites/svelte-5-preview/src/routes/svelte/[...path]/+server.js b/sites/svelte-5-preview/src/routes/svelte/[...path]/+server.js deleted file mode 100644 index 4e2254243526..000000000000 --- a/sites/svelte-5-preview/src/routes/svelte/[...path]/+server.js +++ /dev/null @@ -1,37 +0,0 @@ -import compiler_js from '../../../../../../packages/svelte/compiler/index.js?url'; -import package_json from '../../../../../../packages/svelte/package.json?url'; -import { read } from '$app/server'; - -const files = import.meta.glob('../../../../../../packages/svelte/src/**/*.js', { - eager: true, - query: '?url', - import: 'default' -}); - -const prefix = '../../../../../../packages/svelte/'; - -export const prerender = true; - -export function entries() { - const entries = Object.keys(files).map((path) => ({ path: path.replace(prefix, '') })); - entries.push({ path: 'compiler/index.js' }, { path: 'package.json' }); - return entries; -} - -// service worker requests files under this path to load the compiler and runtime -export async function GET({ params }) { - let file = ''; - - if (params.path === 'compiler/index.js') { - file = compiler_js; - } else if (params.path === 'package.json') { - file = package_json; - } else { - file = /** @type {string} */ (files[prefix + params.path]); - - // remove query string added by Vite when changing source code locally - file = file.split('?')[0]; - } - - return read(file); -} diff --git a/sites/svelte-5-preview/static/favicon.png b/sites/svelte-5-preview/static/favicon.png deleted file mode 100644 index 825b9e65af7c104cfb07089bb28659393b4f2097..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1571 zcmV+;2Hg3HP)Px)-AP12RCwC$UE6KzI1p6{F2N z1VK2vi|pOpn{~#djwYcWXTI_im_u^TJgMZ4JMOsSj!0ma>B?-(Hr@X&W@|R-$}W@Z zgj#$x=!~7LGqHW?IO8+*oE1MyDp!G=L0#^lUx?;!fXv@l^6SvTnf^ac{5OurzC#ZMYc20lI%HhX816AYVs1T3heS1*WaWH z%;x>)-J}YB5#CLzU@GBR6sXYrD>Vw(Fmt#|JP;+}<#6b63Ike{Fuo!?M{yEffez;| zp!PfsuaC)>h>-AdbnwN13g*1LowNjT5?+lFVd#9$!8Z9HA|$*6dQ8EHLu}U|obW6f z2%uGv?vr=KNq7YYa2Roj;|zooo<)lf=&2yxM@e`kM$CmCR#x>gI>I|*Ubr({5Y^rb zghxQU22N}F51}^yfDSt786oMTc!W&V;d?76)9KXX1 z+6Okem(d}YXmmOiZq$!IPk5t8nnS{%?+vDFz3BevmFNgpIod~R{>@#@5x9zJKEHLHv!gHeK~n)Ld!M8DB|Kfe%~123&Hz1Z(86nU7*G5chmyDe ziV7$pB7pJ=96hpxHv9rCR29%bLOXlKU<_13_M8x)6;P8E1Kz6G<&P?$P^%c!M5`2` zfY2zg;VK5~^>TJGQzc+33-n~gKt{{of8GzUkWmU110IgI0DLxRIM>0US|TsM=L|@F z0Bun8U!cRB7-2apz=y-7*UxOxz@Z0)@QM)9wSGki1AZ38ceG7Q72z5`i;i=J`ILzL z@iUO?SBBG-0cQuo+an4TsLy-g-x;8P4UVwk|D8{W@U1Zi z!M)+jqy@nQ$p?5tsHp-6J304Q={v-B>66$P0IDx&YT(`IcZ~bZfmn11#rXd7<5s}y zBi9eim&zQc0Dk|2>$bs0PnLmDfMP5lcXRY&cvJ=zKxI^f0%-d$tD!`LBf9^jMSYUA zI8U?CWdY@}cRq6{5~y+)#h1!*-HcGW@+gZ4B};0OnC~`xQOyH19z*TA!!BJ%9s0V3F?CAJ{hTd#*tf+ur-W9MOURF-@B77_-OshsY}6 zOXRY=5%C^*26z?l)1=$bz30!so5tfABdSYzO+H=CpV~aaUefmjvfZ3Ttu9W&W3Iu6 zROlh0MFA5h;my}8lB0tAV-Rvc2Zs_CCSJnx@d`**$idgy-iMob4dJWWw|21b4NB=LfsYp0Aeh{Ov)yztQi;eL4y5 zMi>8^SzKqk8~k?UiQK^^-5d8c%bV?$F8%X~czyiaKCI2=UH Date: Thu, 12 Dec 2024 00:22:30 +0100 Subject: [PATCH 31/33] fix: correctly handle ssr for `reactivity/window` (#14681) --- .changeset/khaki-guests-switch.md | 5 +++++ packages/svelte/src/reactivity/window/index.js | 8 +++++--- .../samples/reactivity-window/_expected.html | 1 + .../samples/reactivity-window/main.svelte | 14 ++++++++++++++ packages/svelte/types/index.d.ts | 2 +- 5 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 .changeset/khaki-guests-switch.md create mode 100644 packages/svelte/tests/server-side-rendering/samples/reactivity-window/_expected.html create mode 100644 packages/svelte/tests/server-side-rendering/samples/reactivity-window/main.svelte diff --git a/.changeset/khaki-guests-switch.md b/.changeset/khaki-guests-switch.md new file mode 100644 index 000000000000..f32e71084bef --- /dev/null +++ b/.changeset/khaki-guests-switch.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: correctly handle ssr for `reactivity/window` diff --git a/packages/svelte/src/reactivity/window/index.js b/packages/svelte/src/reactivity/window/index.js index 16e8b7b87b9a..8c50a5c440df 100644 --- a/packages/svelte/src/reactivity/window/index.js +++ b/packages/svelte/src/reactivity/window/index.js @@ -124,7 +124,7 @@ export const online = new ReactiveValue( * `devicePixelRatio.current` is a reactive view of `window.devicePixelRatio`. On the server it is `undefined`. * Note that behaviour differs between browsers — on Chrome it will respond to the current zoom level, * on Firefox and Safari it won't. - * @type {{ get current(): number }} + * @type {{ get current(): number | undefined }} * @since 5.11.0 */ export const devicePixelRatio = /* @__PURE__ */ new (class DevicePixelRatio { @@ -144,11 +144,13 @@ export const devicePixelRatio = /* @__PURE__ */ new (class DevicePixelRatio { } constructor() { - this.#update(); + if (BROWSER) { + this.#update(); + } } get current() { get(this.#dpr); - return window.devicePixelRatio; + return BROWSER ? window.devicePixelRatio : undefined; } })(); diff --git a/packages/svelte/tests/server-side-rendering/samples/reactivity-window/_expected.html b/packages/svelte/tests/server-side-rendering/samples/reactivity-window/_expected.html new file mode 100644 index 000000000000..ee65cb76c7d7 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/reactivity-window/_expected.html @@ -0,0 +1 @@ +

devicePixelRatio:

innerHeight:

innerWidth:

online:

outerHeight:

outerWidth:

screenLeft:

screenTop:

scrollX:

scrollY:

\ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/reactivity-window/main.svelte b/packages/svelte/tests/server-side-rendering/samples/reactivity-window/main.svelte new file mode 100644 index 000000000000..e84e41bf637b --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/reactivity-window/main.svelte @@ -0,0 +1,14 @@ + + +

devicePixelRatio: {devicePixelRatio.current}

+

innerHeight: {innerHeight.current}

+

innerWidth: {innerWidth.current}

+

online: {online.current}

+

outerHeight: {outerHeight.current}

+

outerWidth: {outerWidth.current}

+

screenLeft: {screenLeft.current}

+

screenTop: {screenTop.current}

+

scrollX: {scrollX.current}

+

scrollY: {scrollY.current}

\ No newline at end of file diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 206f9931f50c..435476d7033a 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -2015,7 +2015,7 @@ declare module 'svelte/reactivity/window' { * @since 5.11.0 */ export const devicePixelRatio: { - get current(): number; + get current(): number | undefined; }; class ReactiveValue { From 7aa80fc2a7ae1e622ec2c2b49d4654fa8aef257a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 11 Dec 2024 18:29:35 -0500 Subject: [PATCH 32/33] Version Packages (#14682) Co-authored-by: github-actions[bot] --- .changeset/khaki-guests-switch.md | 5 ----- packages/svelte/CHANGELOG.md | 6 ++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .changeset/khaki-guests-switch.md diff --git a/.changeset/khaki-guests-switch.md b/.changeset/khaki-guests-switch.md deleted file mode 100644 index f32e71084bef..000000000000 --- a/.changeset/khaki-guests-switch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: correctly handle ssr for `reactivity/window` diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index f4b7c18d7dc5..978e841bf83b 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.11.2 + +### Patch Changes + +- fix: correctly handle ssr for `reactivity/window` ([#14681](https://github.com/sveltejs/svelte/pull/14681)) + ## 5.11.1 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index f461a4b4c3a2..e95341a0bdef 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.11.1", + "version": "5.11.2", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 1cedefa3149b..e264eace2c12 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -6,5 +6,5 @@ * https://svelte.dev/docs/svelte-compiler#svelte-version * @type {string} */ -export const VERSION = '5.11.1'; +export const VERSION = '5.11.2'; export const PUBLIC_VERSION = '5'; From 8ba1b9ddd0bc65b9a790030aa8b7c73ae2990543 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Thu, 12 Dec 2024 10:42:10 +0000 Subject: [PATCH 33/33] fix: avoid mutation validation for invalidate_inner_signals (#14688) * fix: avoid mutation validation for invalidate_inner_signals * add test * Update packages/svelte/src/internal/client/runtime.js --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- .changeset/strong-pandas-provide.md | 5 +++++ .../svelte/src/internal/client/runtime.js | 7 ++++--- .../binding-interop-derived/Comp.svelte | 12 ++++++++++++ .../binding-interop-derived/_config.js | 5 +++++ .../binding-interop-derived/main.svelte | 19 +++++++++++++++++++ 5 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 .changeset/strong-pandas-provide.md create mode 100644 packages/svelte/tests/runtime-runes/samples/binding-interop-derived/Comp.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/binding-interop-derived/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/binding-interop-derived/main.svelte diff --git a/.changeset/strong-pandas-provide.md b/.changeset/strong-pandas-provide.md new file mode 100644 index 000000000000..0fe7e70c6d6a --- /dev/null +++ b/.changeset/strong-pandas-provide.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: avoid mutation validation for invalidate_inner_signals diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 4928419d16af..5d53ca336079 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -29,7 +29,7 @@ import { } from './constants.js'; import { flush_tasks } from './dom/task.js'; import { add_owner } from './dev/ownership.js'; -import { mutate, set, source } from './reactivity/sources.js'; +import { internal_set, set, source } from './reactivity/sources.js'; import { destroy_derived, execute_derived, update_derived } from './reactivity/deriveds.js'; import * as e from './errors.js'; import { lifecycle_outside_component } from '../shared/errors.js'; @@ -960,11 +960,12 @@ export function invalidate_inner_signals(fn) { if ((signal.f & LEGACY_DERIVED_PROP) !== 0) { for (const dep of /** @type {Derived} */ (signal).deps || []) { if ((dep.f & DERIVED) === 0) { - mutate(dep, null /* doesnt matter */); + // Use internal_set instead of set here and below to avoid mutation validation + internal_set(dep, dep.v); } } } else { - mutate(signal, null /* doesnt matter */); + internal_set(signal, signal.v); } } } diff --git a/packages/svelte/tests/runtime-runes/samples/binding-interop-derived/Comp.svelte b/packages/svelte/tests/runtime-runes/samples/binding-interop-derived/Comp.svelte new file mode 100644 index 000000000000..c3092997481b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/binding-interop-derived/Comp.svelte @@ -0,0 +1,12 @@ + + +{@render children({ props: snippetProps })} diff --git a/packages/svelte/tests/runtime-runes/samples/binding-interop-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/binding-interop-derived/_config.js new file mode 100644 index 000000000000..e52264c793c7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/binding-interop-derived/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + html: '' +}); diff --git a/packages/svelte/tests/runtime-runes/samples/binding-interop-derived/main.svelte b/packages/svelte/tests/runtime-runes/samples/binding-interop-derived/main.svelte new file mode 100644 index 000000000000..5900ddc84645 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/binding-interop-derived/main.svelte @@ -0,0 +1,19 @@ + + + + + + {#snippet children({ props })} + + {/snippet} +