From 7f473f618d76eb634845c807664442fc65032662 Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Thu, 17 Apr 2025 23:51:46 +0200 Subject: [PATCH 001/259] fix: allow accessing snippet in script tag (#15789) * fix: allow accessing snippet in script tag * branches are identical * update test * rename test * fix validation (we were mutating the wrong array) * tidy up * remove submodule * changeset --------- Co-authored-by: Rich Harris --- .changeset/few-horses-wink.md | 5 ++++ .../server/visitors/SnippetBlock.js | 30 ++++++++----------- .../snippet-access-in-script/_config.js | 7 +++++ .../samples/snippet-access-in-script/fn.js | 3 ++ .../snippet-access-in-script/main.svelte | 10 +++++++ .../samples/snippet-invalid-call/_config.js | 9 ++++++ .../samples/snippet-invalid-call/main.svelte | 7 +++++ .../_expected/server/index.svelte.js | 4 +-- 8 files changed, 55 insertions(+), 20 deletions(-) create mode 100644 .changeset/few-horses-wink.md create mode 100644 packages/svelte/tests/runtime-runes/samples/snippet-access-in-script/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/snippet-access-in-script/fn.js create mode 100644 packages/svelte/tests/runtime-runes/samples/snippet-access-in-script/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/snippet-invalid-call/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/snippet-invalid-call/main.svelte diff --git a/.changeset/few-horses-wink.md b/.changeset/few-horses-wink.md new file mode 100644 index 0000000000..8ffa3640c0 --- /dev/null +++ b/.changeset/few-horses-wink.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: use function declaration for snippets in server output to avoid TDZ violation diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SnippetBlock.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SnippetBlock.js index 5118679b34..238485e665 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SnippetBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SnippetBlock.js @@ -1,4 +1,4 @@ -/** @import { ArrowFunctionExpression, BlockStatement, CallExpression } from 'estree' */ +/** @import { BlockStatement } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types.js' */ import { dev } from '../../../../state.js'; @@ -9,27 +9,21 @@ import * as b from '#compiler/builders'; * @param {ComponentContext} context */ export function SnippetBlock(node, context) { - const body = /** @type {BlockStatement} */ (context.visit(node.body)); + let fn = b.function_declaration( + node.expression, + [b.id('$$payload'), ...node.parameters], + /** @type {BlockStatement} */ (context.visit(node.body)) + ); - if (dev) { - body.body.unshift(b.stmt(b.call('$.validate_snippet_args', b.id('$$payload')))); - } + // @ts-expect-error - TODO remove this hack once $$render_inner for legacy bindings is gone + fn.___snippet = true; - /** @type {ArrowFunctionExpression | CallExpression} */ - let fn = b.arrow([b.id('$$payload'), ...node.parameters], body); + const statements = node.metadata.can_hoist ? context.state.hoisted : context.state.init; if (dev) { - fn = b.call('$.prevent_snippet_stringification', fn); + fn.body.body.unshift(b.stmt(b.call('$.validate_snippet_args', b.id('$$payload')))); + statements.push(b.stmt(b.call('$.prevent_snippet_stringification', fn.id))); } - const declaration = b.declaration('const', [b.declarator(node.expression, fn)]); - - // @ts-expect-error - TODO remove this hack once $$render_inner for legacy bindings is gone - fn.___snippet = true; - - if (node.metadata.can_hoist) { - context.state.hoisted.push(declaration); - } else { - context.state.init.push(declaration); - } + statements.push(fn); } diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-access-in-script/_config.js b/packages/svelte/tests/runtime-runes/samples/snippet-access-in-script/_config.js new file mode 100644 index 0000000000..ed0ead960b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-access-in-script/_config.js @@ -0,0 +1,7 @@ +import { test } from '../../test'; + +export default test({ + compileOptions: { + dev: true + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-access-in-script/fn.js b/packages/svelte/tests/runtime-runes/samples/snippet-access-in-script/fn.js new file mode 100644 index 0000000000..9e9a48c60c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-access-in-script/fn.js @@ -0,0 +1,3 @@ +export function fn(snippet) { + return snippet; +} diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-access-in-script/main.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-access-in-script/main.svelte new file mode 100644 index 0000000000..cc73aa31db --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-access-in-script/main.svelte @@ -0,0 +1,10 @@ + + +{#snippet test()} + {variable} +{/snippet} \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-invalid-call/_config.js b/packages/svelte/tests/runtime-runes/samples/snippet-invalid-call/_config.js new file mode 100644 index 0000000000..7e72fd751a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-invalid-call/_config.js @@ -0,0 +1,9 @@ +import { test } from '../../test'; + +export default test({ + compileOptions: { + dev: true + }, + + error: 'invalid_snippet_arguments' +}); diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-invalid-call/main.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-invalid-call/main.svelte new file mode 100644 index 0000000000..3c47db3f96 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-invalid-call/main.svelte @@ -0,0 +1,7 @@ + + +{#snippet test()} +

hello

+{/snippet} diff --git a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/server/index.svelte.js index 04bfbf6ae4..cadae2cf15 100644 --- a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/server/index.svelte.js @@ -1,9 +1,9 @@ import * as $ from 'svelte/internal/server'; import TextInput from './Child.svelte'; -const snippet = ($$payload) => { +function snippet($$payload) { $$payload.out += `Something`; -}; +} export default function Bind_component_snippet($$payload) { let value = ''; From 5b7af5e28979d50f610cfd000fc2e67a9d8a5101 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 17 Apr 2025 21:11:04 -0400 Subject: [PATCH 002/259] Version Packages (#15790) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/few-horses-wink.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/few-horses-wink.md diff --git a/.changeset/few-horses-wink.md b/.changeset/few-horses-wink.md deleted file mode 100644 index 8ffa3640c0..0000000000 --- a/.changeset/few-horses-wink.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: use function declaration for snippets in server output to avoid TDZ violation diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index e5681a3ceb..709b829fcd 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.27.3 + +### Patch Changes + +- fix: use function declaration for snippets in server output to avoid TDZ violation ([#15789](https://github.com/sveltejs/svelte/pull/15789)) + ## 5.27.2 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index dc6c57b1a2..efead5604c 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.27.2", + "version": "5.27.3", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 62624e866c..c64eded9c4 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.27.2'; +export const VERSION = '5.27.3'; export const PUBLIC_VERSION = '5'; From d0dcc0b79534f377349e7f6aa4a97c7b55f51864 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Fri, 18 Apr 2025 08:19:37 -0700 Subject: [PATCH 003/259] fix: improve partial evaluation (#15781) * init * remove console log * Update packages/svelte/src/compiler/phases/scope.js * fix each indices * dedupe * Update packages/svelte/src/compiler/phases/scope.js * always break * fix formatting * Apply suggestions from code review * compactify * compactify * reuse values * add more globals, template literals, try functions and (some) member expressions * remove console logs * remove function handling, tweak failing test * changeset * try putting static stuff in the template * nevermind * unused * simplify * Update packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js * YAGNI * simplify and fix (should use cooked, not raw) * unused * changeset --------- Co-authored-by: Rich Harris Co-authored-by: Rich Harris --- .changeset/serious-adults-sit.md | 5 + .../client/visitors/shared/utils.js | 10 +- packages/svelte/src/compiler/phases/scope.js | 210 ++++++++++++++++-- .../samples/each-index-non-null/_config.js | 3 + .../_expected/client/index.svelte.js | 19 ++ .../_expected/server/index.svelte.js | 13 ++ .../samples/each-index-non-null/index.svelte | 3 + .../purity/_expected/client/index.svelte.js | 2 +- .../purity/_expected/server/index.svelte.js | 2 +- 9 files changed, 243 insertions(+), 24 deletions(-) create mode 100644 .changeset/serious-adults-sit.md create mode 100644 packages/svelte/tests/snapshot/samples/each-index-non-null/_config.js create mode 100644 packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/client/index.svelte.js create mode 100644 packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/server/index.svelte.js create mode 100644 packages/svelte/tests/snapshot/samples/each-index-non-null/index.svelte diff --git a/.changeset/serious-adults-sit.md b/.changeset/serious-adults-sit.md new file mode 100644 index 0000000000..8c98a7c366 --- /dev/null +++ b/.changeset/serious-adults-sit.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: partially evaluate more expressions 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 380cf6cd02..bc79b76043 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 @@ -69,11 +69,17 @@ export function build_template_chunk( node.metadata.expression ); - has_state ||= node.metadata.expression.has_state; + const evaluated = state.scope.evaluate(value); + + has_state ||= node.metadata.expression.has_state && !evaluated.is_known; if (values.length === 1) { // If we have a single expression, then pass that in directly to possibly avoid doing // extra work in the template_effect (instead we do the work in set_text). + if (evaluated.is_known) { + value = b.literal(evaluated.value); + } + return { value, has_state }; } @@ -89,8 +95,6 @@ export function build_template_chunk( } } - const evaluated = state.scope.evaluate(value); - if (evaluated.is_known) { quasi.value.cooked += evaluated.value + ''; } else { diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 570d5e22d9..8297f174d3 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -1,4 +1,4 @@ -/** @import { ArrowFunctionExpression, BinaryOperator, ClassDeclaration, Expression, FunctionDeclaration, FunctionExpression, Identifier, ImportDeclaration, MemberExpression, LogicalOperator, Node, Pattern, UnaryOperator, VariableDeclarator } from 'estree' */ +/** @import { ArrowFunctionExpression, BinaryOperator, ClassDeclaration, Expression, FunctionDeclaration, FunctionExpression, Identifier, ImportDeclaration, MemberExpression, LogicalOperator, Node, Pattern, UnaryOperator, VariableDeclarator, Super } from 'estree' */ /** @import { Context, Visitor } from 'zimmerframe' */ /** @import { AST, BindingKind, DeclarationKind } from '#compiler' */ import is_reference from 'is-reference'; @@ -18,8 +18,71 @@ import { validate_identifier_name } from './2-analyze/visitors/shared/utils.js'; const UNKNOWN = Symbol('unknown'); /** Includes `BigInt` */ -const NUMBER = Symbol('number'); -const STRING = Symbol('string'); +export const NUMBER = Symbol('number'); +export const STRING = Symbol('string'); + +/** @type {Record} */ +const globals = { + BigInt: [NUMBER, BigInt], + 'Math.min': [NUMBER, Math.min], + 'Math.max': [NUMBER, Math.max], + 'Math.random': [NUMBER], + 'Math.floor': [NUMBER, Math.floor], + // @ts-expect-error + 'Math.f16round': [NUMBER, Math.f16round], + 'Math.round': [NUMBER, Math.round], + 'Math.abs': [NUMBER, Math.abs], + 'Math.acos': [NUMBER, Math.acos], + 'Math.asin': [NUMBER, Math.asin], + 'Math.atan': [NUMBER, Math.atan], + 'Math.atan2': [NUMBER, Math.atan2], + 'Math.ceil': [NUMBER, Math.ceil], + 'Math.cos': [NUMBER, Math.cos], + 'Math.sin': [NUMBER, Math.sin], + 'Math.tan': [NUMBER, Math.tan], + 'Math.exp': [NUMBER, Math.exp], + 'Math.log': [NUMBER, Math.log], + 'Math.pow': [NUMBER, Math.pow], + 'Math.sqrt': [NUMBER, Math.sqrt], + 'Math.clz32': [NUMBER, Math.clz32], + 'Math.imul': [NUMBER, Math.imul], + 'Math.sign': [NUMBER, Math.sign], + 'Math.log10': [NUMBER, Math.log10], + 'Math.log2': [NUMBER, Math.log2], + 'Math.log1p': [NUMBER, Math.log1p], + 'Math.expm1': [NUMBER, Math.expm1], + 'Math.cosh': [NUMBER, Math.cosh], + 'Math.sinh': [NUMBER, Math.sinh], + 'Math.tanh': [NUMBER, Math.tanh], + 'Math.acosh': [NUMBER, Math.acosh], + 'Math.asinh': [NUMBER, Math.asinh], + 'Math.atanh': [NUMBER, Math.atanh], + 'Math.trunc': [NUMBER, Math.trunc], + 'Math.fround': [NUMBER, Math.fround], + 'Math.cbrt': [NUMBER, Math.cbrt], + Number: [NUMBER, Number], + 'Number.isInteger': [NUMBER, Number.isInteger], + 'Number.isFinite': [NUMBER, Number.isFinite], + 'Number.isNaN': [NUMBER, Number.isNaN], + 'Number.isSafeInteger': [NUMBER, Number.isSafeInteger], + 'Number.parseFloat': [NUMBER, Number.parseFloat], + 'Number.parseInt': [NUMBER, Number.parseInt], + String: [STRING, String], + 'String.fromCharCode': [STRING, String.fromCharCode], + 'String.fromCodePoint': [STRING, String.fromCodePoint] +}; + +/** @type {Record} */ +const global_constants = { + 'Math.PI': Math.PI, + 'Math.E': Math.E, + 'Math.LN10': Math.LN10, + 'Math.LN2': Math.LN2, + 'Math.LOG10E': Math.LOG10E, + 'Math.LOG2E': Math.LOG2E, + 'Math.SQRT2': Math.SQRT2, + 'Math.SQRT1_2': Math.SQRT1_2 +}; export class Binding { /** @type {Scope} */ @@ -107,7 +170,7 @@ export class Binding { class Evaluation { /** @type {Set} */ - values = new Set(); + values; /** * True if there is exactly one possible value @@ -147,8 +210,11 @@ class Evaluation { * * @param {Scope} scope * @param {Expression} expression + * @param {Set} values */ - constructor(scope, expression) { + constructor(scope, expression, values) { + this.values = values; + switch (expression.type) { case 'Literal': { this.values.add(expression.value); @@ -172,15 +238,18 @@ class Evaluation { binding.kind === 'rest_prop' || binding.kind === 'bindable_prop'; - if (!binding.updated && binding.initial !== null && !is_prop) { - const evaluation = binding.scope.evaluate(/** @type {Expression} */ (binding.initial)); - for (const value of evaluation.values) { - this.values.add(value); - } + if (binding.initial?.type === 'EachBlock' && binding.initial.index === expression.name) { + this.values.add(NUMBER); break; } - // TODO each index is always defined + if (!binding.updated && binding.initial !== null && !is_prop) { + binding.scope.evaluate(/** @type {Expression} */ (binding.initial), this.values); + break; + } + } else if (expression.name === 'undefined') { + this.values.add(undefined); + break; } // TODO glean what we can from reassignments @@ -336,6 +405,101 @@ class Evaluation { break; } + case 'CallExpression': { + const keypath = get_global_keypath(expression.callee, scope); + + if (keypath) { + if (is_rune(keypath)) { + const arg = /** @type {Expression | undefined} */ (expression.arguments[0]); + + switch (keypath) { + case '$state': + case '$state.raw': + case '$derived': + if (arg) { + scope.evaluate(arg, this.values); + } else { + this.values.add(undefined); + } + break; + + case '$props.id': + this.values.add(STRING); + break; + + case '$effect.tracking': + this.values.add(false); + this.values.add(true); + break; + + case '$derived.by': + if (arg?.type === 'ArrowFunctionExpression' && arg.body.type !== 'BlockStatement') { + scope.evaluate(arg.body, this.values); + break; + } + + this.values.add(UNKNOWN); + break; + + default: { + this.values.add(UNKNOWN); + } + } + + break; + } + + if ( + Object.hasOwn(globals, keypath) && + expression.arguments.every((arg) => arg.type !== 'SpreadElement') + ) { + const [type, fn] = globals[keypath]; + const values = expression.arguments.map((arg) => scope.evaluate(arg)); + + if (fn && values.every((e) => e.is_known)) { + this.values.add(fn(...values.map((e) => e.value))); + } else { + this.values.add(type); + } + + break; + } + } + + this.values.add(UNKNOWN); + break; + } + + case 'TemplateLiteral': { + let result = expression.quasis[0].value.cooked; + + for (let i = 0; i < expression.expressions.length; i += 1) { + const e = scope.evaluate(expression.expressions[i]); + + if (e.is_known) { + result += e.value + expression.quasis[i + 1].value.cooked; + } else { + this.values.add(STRING); + break; + } + } + + this.values.add(result); + break; + } + + case 'MemberExpression': { + const keypath = get_global_keypath(expression, scope); + + if (keypath && Object.hasOwn(global_constants, keypath)) { + this.values.add(global_constants[keypath]); + break; + } + + this.values.add(UNKNOWN); + break; + } + default: { this.values.add(UNKNOWN); } @@ -548,10 +712,10 @@ export class Scope { * Only call this once scope has been fully generated in a first pass, * else this evaluates on incomplete data and may yield wrong results. * @param {Expression} expression - * @param {Set} values + * @param {Set} [values] */ evaluate(expression, values = new Set()) { - return new Evaluation(this, expression); + return new Evaluation(this, expression, values); } } @@ -1115,7 +1279,19 @@ export function get_rune(node, scope) { if (!node) return null; if (node.type !== 'CallExpression') return null; - let n = node.callee; + const keypath = get_global_keypath(node.callee, scope); + + if (!keypath || !is_rune(keypath)) return null; + return keypath; +} + +/** + * Returns the name of the rune if the given expression is a `CallExpression` using a rune. + * @param {Expression | Super} node + * @param {Scope} scope + */ +function get_global_keypath(node, scope) { + let n = node; let joined = ''; @@ -1133,12 +1309,8 @@ export function get_rune(node, scope) { if (n.type !== 'Identifier') return null; - joined = n.name + joined; - - if (!is_rune(joined)) return null; - const binding = scope.get(n.name); if (binding !== null) return null; // rune name, but references a variable or store - return joined; + return n.name + joined; } diff --git a/packages/svelte/tests/snapshot/samples/each-index-non-null/_config.js b/packages/svelte/tests/snapshot/samples/each-index-non-null/_config.js new file mode 100644 index 0000000000..f47bee71df --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/each-index-non-null/_config.js @@ -0,0 +1,3 @@ +import { test } from '../../test'; + +export default test({}); diff --git a/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/client/index.svelte.js new file mode 100644 index 0000000000..3d46a679b8 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/client/index.svelte.js @@ -0,0 +1,19 @@ +import 'svelte/internal/disclose-version'; +import 'svelte/internal/flags/legacy'; +import * as $ from 'svelte/internal/client'; + +var root_1 = $.template(`

`); + +export default function Each_index_non_null($$anchor) { + var fragment = $.comment(); + var node = $.first_child(fragment); + + $.each(node, 0, () => Array(10), $.index, ($$anchor, $$item, i) => { + var p = root_1(); + + p.textContent = `index: ${i}`; + $.append($$anchor, p); + }); + + $.append($$anchor, fragment); +} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/server/index.svelte.js new file mode 100644 index 0000000000..3431e36833 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/server/index.svelte.js @@ -0,0 +1,13 @@ +import * as $ from 'svelte/internal/server'; + +export default function Each_index_non_null($$payload) { + const each_array = $.ensure_array_like(Array(10)); + + $$payload.out += ``; + + for (let i = 0, $$length = each_array.length; i < $$length; i++) { + $$payload.out += `

index: ${$.escape(i)}

`; + } + + $$payload.out += ``; +} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/each-index-non-null/index.svelte b/packages/svelte/tests/snapshot/samples/each-index-non-null/index.svelte new file mode 100644 index 0000000000..03bfc9e372 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/each-index-non-null/index.svelte @@ -0,0 +1,3 @@ +{#each Array(10), i} +

index: {i}

+{/each} diff --git a/packages/svelte/tests/snapshot/samples/purity/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/purity/_expected/client/index.svelte.js index 940ed8f9e8..5bc9766acf 100644 --- a/packages/svelte/tests/snapshot/samples/purity/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/purity/_expected/client/index.svelte.js @@ -8,7 +8,7 @@ export default function Purity($$anchor) { var fragment = root(); var p = $.first_child(fragment); - p.textContent = Math.max(0, Math.min(0, 100)); + p.textContent = 0; var p_1 = $.sibling(p, 2); diff --git a/packages/svelte/tests/snapshot/samples/purity/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/purity/_expected/server/index.svelte.js index 588332407a..9457378c0d 100644 --- a/packages/svelte/tests/snapshot/samples/purity/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/purity/_expected/server/index.svelte.js @@ -1,7 +1,7 @@ import * as $ from 'svelte/internal/server'; export default function Purity($$payload) { - $$payload.out += `

${$.escape(Math.max(0, Math.min(0, 100)))}

${$.escape(location.href)}

`; + $$payload.out += `

0

${$.escape(location.href)}

`; Child($$payload, { prop: encodeURIComponent('hello') }); $$payload.out += ``; } \ No newline at end of file From 6fe5977b3d0bfd86d7b8e47f9e5374ffa31fba56 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 18 Apr 2025 11:27:19 -0400 Subject: [PATCH 004/259] Version Packages (#15798) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/serious-adults-sit.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/serious-adults-sit.md diff --git a/.changeset/serious-adults-sit.md b/.changeset/serious-adults-sit.md deleted file mode 100644 index 8c98a7c366..0000000000 --- a/.changeset/serious-adults-sit.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': minor ---- - -feat: partially evaluate more expressions diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 709b829fcd..8c01c71099 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.28.0 + +### Minor Changes + +- feat: partially evaluate more expressions ([#15781](https://github.com/sveltejs/svelte/pull/15781)) + ## 5.27.3 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index efead5604c..784b14ea26 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.27.3", + "version": "5.28.0", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index c64eded9c4..4418204eca 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.27.3'; +export const VERSION = '5.28.0'; export const PUBLIC_VERSION = '5'; From d8c6afde944a61c648c6b6f416f5ccafde272d44 Mon Sep 17 00:00:00 2001 From: 7nik Date: Fri, 18 Apr 2025 18:48:27 +0300 Subject: [PATCH 005/259] fix: emit error on wrong placement of the `:global` block selector (#15794) Co-authored-by: 7nik --- .changeset/pretty-beers-care.md | 5 +++++ .../docs/98-reference/.generated/compile-errors.md | 6 ++++++ packages/svelte/messages/compile-errors/style.md | 4 ++++ packages/svelte/src/compiler/errors.js | 9 +++++++++ .../src/compiler/phases/2-analyze/css/css-analyze.js | 6 +++++- .../samples/css-global-block-in-pseudoclass/_config.js | 9 +++++++++ .../samples/css-global-block-in-pseudoclass/main.svelte | 4 ++++ 7 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 .changeset/pretty-beers-care.md create mode 100644 packages/svelte/tests/compiler-errors/samples/css-global-block-in-pseudoclass/_config.js create mode 100644 packages/svelte/tests/compiler-errors/samples/css-global-block-in-pseudoclass/main.svelte diff --git a/.changeset/pretty-beers-care.md b/.changeset/pretty-beers-care.md new file mode 100644 index 0000000000..821bad41c4 --- /dev/null +++ b/.changeset/pretty-beers-care.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: emit error on wrong placement of the `:global` block selector diff --git a/documentation/docs/98-reference/.generated/compile-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md index 6196a85ade..e8669ead53 100644 --- a/documentation/docs/98-reference/.generated/compile-errors.md +++ b/documentation/docs/98-reference/.generated/compile-errors.md @@ -274,6 +274,12 @@ A `:global` selector cannot modify an existing selector A `:global` selector can only be modified if it is a descendant of other selectors ``` +### css_global_block_invalid_placement + +``` +A `:global` selector cannot be inside a pseudoclass +``` + ### css_global_invalid_placement ``` diff --git a/packages/svelte/messages/compile-errors/style.md b/packages/svelte/messages/compile-errors/style.md index f08a2156a3..272efbccce 100644 --- a/packages/svelte/messages/compile-errors/style.md +++ b/packages/svelte/messages/compile-errors/style.md @@ -50,6 +50,10 @@ x y { > A `:global` selector can only be modified if it is a descendant of other selectors +## css_global_block_invalid_placement + +> A `:global` selector cannot be inside a pseudoclass + ## css_global_invalid_placement > `:global(...)` can be at the start or end of a selector sequence, but not in the middle diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index aa328764e1..c99f597468 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -581,6 +581,15 @@ export function css_global_block_invalid_modifier_start(node) { e(node, 'css_global_block_invalid_modifier_start', `A \`:global\` selector can only be modified if it is a descendant of other selectors\nhttps://svelte.dev/e/css_global_block_invalid_modifier_start`); } +/** + * A `:global` selector cannot be inside a pseudoclass + * @param {null | number | NodeLike} node + * @returns {never} + */ +export function css_global_block_invalid_placement(node) { + e(node, 'css_global_block_invalid_placement', `A \`:global\` selector cannot be inside a pseudoclass\nhttps://svelte.dev/e/css_global_block_invalid_placement`); +} + /** * `:global(...)` can be at the start or end of a selector sequence, but not in the middle * @param {null | number | NodeLike} node diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js index 2dc3435648..d6052c9c3e 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js @@ -68,8 +68,12 @@ const css_visitors = { const global = node.children.find(is_global); if (global) { - const idx = node.children.indexOf(global); + const is_nested = context.path.at(-2)?.type === 'PseudoClassSelector'; + if (is_nested && !global.selectors[0].args) { + e.css_global_block_invalid_placement(global.selectors[0]); + } + const idx = node.children.indexOf(global); if (global.selectors[0].args !== null && idx !== 0 && idx !== node.children.length - 1) { // ensure `:global(...)` is not used in the middle of a selector (but multiple `global(...)` in sequence are ok) for (let i = idx + 1; i < node.children.length; i++) { diff --git a/packages/svelte/tests/compiler-errors/samples/css-global-block-in-pseudoclass/_config.js b/packages/svelte/tests/compiler-errors/samples/css-global-block-in-pseudoclass/_config.js new file mode 100644 index 0000000000..c987725d74 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/css-global-block-in-pseudoclass/_config.js @@ -0,0 +1,9 @@ +import { test } from '../../test'; + +export default test({ + error: { + code: 'css_global_block_invalid_placement', + message: 'A `:global` selector cannot be inside a pseudoclass', + position: [28, 35] + } +}); diff --git a/packages/svelte/tests/compiler-errors/samples/css-global-block-in-pseudoclass/main.svelte b/packages/svelte/tests/compiler-errors/samples/css-global-block-in-pseudoclass/main.svelte new file mode 100644 index 0000000000..53060df97d --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/css-global-block-in-pseudoclass/main.svelte @@ -0,0 +1,4 @@ + From 3d808840298dcd7fbb40dfa90700082911e514e7 Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Fri, 18 Apr 2025 17:50:38 +0200 Subject: [PATCH 006/259] fix: `update_version` after `delete` if `source` is `undefined` and `prop` in `target` (#15796) * fix: `update_version` after `delete` if `source` is `undefined` and `prop` in `target` * chore: remove submodule * tweak test --------- Co-authored-by: Rich Harris --- .changeset/great-colts-film.md | 5 +++++ packages/svelte/src/internal/client/proxy.js | 1 + .../samples/delete-proxy-key/_config.js | 13 +++++++++++++ .../samples/delete-proxy-key/main.svelte | 13 +++++++++++++ 4 files changed, 32 insertions(+) create mode 100644 .changeset/great-colts-film.md create mode 100644 packages/svelte/tests/runtime-runes/samples/delete-proxy-key/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/delete-proxy-key/main.svelte diff --git a/.changeset/great-colts-film.md b/.changeset/great-colts-film.md new file mode 100644 index 0000000000..273509c107 --- /dev/null +++ b/.changeset/great-colts-film.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: `update_version` after `delete` if `source` is `undefined` and `prop` in `target` diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js index d690790e3a..fd5706eaf2 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -100,6 +100,7 @@ export function proxy(value) { prop, with_parent(() => source(UNINITIALIZED, stack)) ); + update_version(version); } } else { // When working with arrays, we need to also ensure we update the length when removing diff --git a/packages/svelte/tests/runtime-runes/samples/delete-proxy-key/_config.js b/packages/svelte/tests/runtime-runes/samples/delete-proxy-key/_config.js new file mode 100644 index 0000000000..23141a01cd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/delete-proxy-key/_config.js @@ -0,0 +1,13 @@ +import { test } from '../../test'; +import { flushSync } from 'svelte'; + +export default test({ + html: `

test

`, + + async test({ assert, target }) { + const btn = target.querySelector('button'); + + flushSync(() => btn?.click()); + assert.htmlEqual(target.innerHTML, ''); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/delete-proxy-key/main.svelte b/packages/svelte/tests/runtime-runes/samples/delete-proxy-key/main.svelte new file mode 100644 index 0000000000..d3c519c401 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/delete-proxy-key/main.svelte @@ -0,0 +1,13 @@ + + + + +{#each keys as key} +

{key}

+{/each} From b2d5787a09bffefb716d72706460d8b66e96c862 Mon Sep 17 00:00:00 2001 From: zeroberry <98147817+zeroberry@users.noreply.github.com> Date: Sat, 19 Apr 2025 01:16:25 +0900 Subject: [PATCH 007/259] fix: ensure `` properly removes error content in production mode (#15793) * fix: ensure properly removes error content in production mode * add changeset --- .changeset/eleven-roses-speak.md | 5 ++ .../svelte/src/internal/client/runtime.js | 79 ++++++++----------- .../samples/error-boundary-22/Child.svelte | 3 + .../samples/error-boundary-22/_config.js | 11 +++ .../samples/error-boundary-22/main.svelte | 15 ++++ 5 files changed, 69 insertions(+), 44 deletions(-) create mode 100644 .changeset/eleven-roses-speak.md create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-22/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-22/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-22/main.svelte diff --git a/.changeset/eleven-roses-speak.md b/.changeset/eleven-roses-speak.md new file mode 100644 index 0000000000..0d906f3b5f --- /dev/null +++ b/.changeset/eleven-roses-speak.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: ensure properly removes error content in production mode diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 7595aa4a19..fccea3e856 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -291,31 +291,21 @@ export function handle_error(error, effect, previous_effect, component_context) is_throwing_error = true; } - if ( - !DEV || - component_context === null || - !(error instanceof Error) || - handled_errors.has(error) - ) { - propagate_error(error, effect); - return; - } - - handled_errors.add(error); + if (DEV && component_context !== null && error instanceof Error && !handled_errors.has(error)) { + handled_errors.add(error); - const component_stack = []; + const component_stack = []; - const effect_name = effect.fn?.name; + const effect_name = effect.fn?.name; - if (effect_name) { - component_stack.push(effect_name); - } + if (effect_name) { + component_stack.push(effect_name); + } - /** @type {ComponentContext | null} */ - let current_context = component_context; + /** @type {ComponentContext | null} */ + let current_context = component_context; - while (current_context !== null) { - if (DEV) { + while (current_context !== null) { /** @type {string} */ var filename = current_context.function?.[FILENAME]; @@ -323,35 +313,36 @@ export function handle_error(error, effect, previous_effect, component_context) const file = filename.split('/').pop(); component_stack.push(file); } + + current_context = current_context.p; } - current_context = current_context.p; - } + const indent = is_firefox ? ' ' : '\t'; + define_property(error, 'message', { + value: + error.message + `\n${component_stack.map((name) => `\n${indent}in ${name}`).join('')}\n` + }); + define_property(error, 'component_stack', { + value: component_stack + }); - const indent = is_firefox ? ' ' : '\t'; - define_property(error, 'message', { - value: error.message + `\n${component_stack.map((name) => `\n${indent}in ${name}`).join('')}\n` - }); - define_property(error, 'component_stack', { - value: component_stack - }); - - const stack = error.stack; - - // Filter out internal files from callstack - if (stack) { - const lines = stack.split('\n'); - const new_lines = []; - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - if (line.includes('svelte/src/internal')) { - continue; + const stack = error.stack; + + // Filter out internal files from callstack + if (stack) { + const lines = stack.split('\n'); + const new_lines = []; + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (line.includes('svelte/src/internal')) { + continue; + } + new_lines.push(line); } - new_lines.push(line); + define_property(error, 'stack', { + value: new_lines.join('\n') + }); } - define_property(error, 'stack', { - value: new_lines.join('\n') - }); } propagate_error(error, effect); diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-22/Child.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-22/Child.svelte new file mode 100644 index 0000000000..ea60542af9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-22/Child.svelte @@ -0,0 +1,3 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-22/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary-22/_config.js new file mode 100644 index 0000000000..c6be4a8cfd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-22/_config.js @@ -0,0 +1,11 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['client'], + test({ assert, target }) { + flushSync(); + + assert.htmlEqual(target.innerHTML, '

error occurred

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-22/main.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-22/main.svelte new file mode 100644 index 0000000000..39b2fb2eb2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-22/main.svelte @@ -0,0 +1,15 @@ + + + +

This should be removed

+ + {#if true} + + {/if} + + {#snippet failed()} +

error occurred

+ {/snippet} +
From e40e9eb1a444426eb5ab27bf39cbda6225283574 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 18 Apr 2025 12:19:20 -0400 Subject: [PATCH 008/259] Version Packages (#15800) * Version Packages * Update packages/svelte/CHANGELOG.md --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Rich Harris --- .changeset/eleven-roses-speak.md | 5 ----- .changeset/great-colts-film.md | 5 ----- .changeset/pretty-beers-care.md | 5 ----- packages/svelte/CHANGELOG.md | 10 ++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 6 files changed, 12 insertions(+), 17 deletions(-) delete mode 100644 .changeset/eleven-roses-speak.md delete mode 100644 .changeset/great-colts-film.md delete mode 100644 .changeset/pretty-beers-care.md diff --git a/.changeset/eleven-roses-speak.md b/.changeset/eleven-roses-speak.md deleted file mode 100644 index 0d906f3b5f..0000000000 --- a/.changeset/eleven-roses-speak.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: ensure properly removes error content in production mode diff --git a/.changeset/great-colts-film.md b/.changeset/great-colts-film.md deleted file mode 100644 index 273509c107..0000000000 --- a/.changeset/great-colts-film.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: `update_version` after `delete` if `source` is `undefined` and `prop` in `target` diff --git a/.changeset/pretty-beers-care.md b/.changeset/pretty-beers-care.md deleted file mode 100644 index 821bad41c4..0000000000 --- a/.changeset/pretty-beers-care.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: emit error on wrong placement of the `:global` block selector diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 8c01c71099..cb3b356cc7 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,15 @@ # svelte +## 5.28.1 + +### Patch Changes + +- fix: ensure `` properly removes error content in production mode ([#15793](https://github.com/sveltejs/svelte/pull/15793)) + +- fix: `update_version` after `delete` if `source` is `undefined` and `prop` in `target` ([#15796](https://github.com/sveltejs/svelte/pull/15796)) + +- fix: emit error on wrong placement of the `:global` block selector ([#15794](https://github.com/sveltejs/svelte/pull/15794)) + ## 5.28.0 ### Minor Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 784b14ea26..c91fbdd7d3 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.28.0", + "version": "5.28.1", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 4418204eca..a19a773cd1 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.28.0'; +export const VERSION = '5.28.1'; export const PUBLIC_VERSION = '5'; From a1adf2be6bae6032ab475b0efa76a720643583a2 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 19 Apr 2025 17:14:28 -0400 Subject: [PATCH 009/259] chore: squelch test console output (#15807) * squelch some console spam * another * more * more * more * last one --- .../samples/ondestroy-prop-access/_config.js | 2 -- .../svelte/tests/runtime-legacy/shared.ts | 10 ++++----- .../_config.js | 10 ++------- .../event-handler-invalid-values/_config.js | 18 ++++----------- .../event-handler-invalid-warning/_config.js | 10 ++------- .../samples/inspect-exception/_config.js | 3 ++- .../samples/inspect-trace-null/_config.js | 6 ++++- .../main.svelte | 4 ++-- packages/svelte/tests/signals/test.ts | 22 ++++++++++++++++--- 9 files changed, 41 insertions(+), 44 deletions(-) diff --git a/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/_config.js b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/_config.js index 2ffb7e653f..4f75e82aae 100644 --- a/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/_config.js @@ -36,8 +36,6 @@ export default test({ btn1.click(); }); - console.warn(logs); - // the five components guarded by `count < 2` unmount and log assert.deepEqual(logs, [1, true, 1, true, 1, true, 1, true, 1, true]); diff --git a/packages/svelte/tests/runtime-legacy/shared.ts b/packages/svelte/tests/runtime-legacy/shared.ts index fc748ce6b2..14b6cff841 100644 --- a/packages/svelte/tests/runtime-legacy/shared.ts +++ b/packages/svelte/tests/runtime-legacy/shared.ts @@ -351,7 +351,7 @@ async function run_test_variant( // @ts-expect-error globalThis.__svelte.uid = 1; - if (manual_hydrate) { + if (manual_hydrate && variant === 'hydrate') { hydrate_fn = () => { instance = hydrate(mod.default, { target, @@ -469,10 +469,6 @@ async function run_test_variant( throw err; } } finally { - console.log = console_log; - console.warn = console_warn; - console.error = console_error; - config.after_test?.(); // Free up the microtask queue @@ -486,6 +482,10 @@ async function run_test_variant( process.on('unhandledRejection', listener); }); } + + console.log = console_log; + console.warn = console_warn; + console.error = console_error; } } diff --git a/packages/svelte/tests/runtime-runes/samples/event-handler-component-invalid-warning/_config.js b/packages/svelte/tests/runtime-runes/samples/event-handler-component-invalid-warning/_config.js index 01ac3c9ad0..e771010669 100644 --- a/packages/svelte/tests/runtime-runes/samples/event-handler-component-invalid-warning/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/event-handler-component-invalid-warning/_config.js @@ -7,12 +7,8 @@ export default test({ dev: true }, - test({ assert, target, warnings }) { - /** @type {any} */ - let error; - + test({ assert, target, warnings, errors }) { const handler = (/** @type {any}} */ e) => { - error = e.error; e.stopImmediatePropagation(); }; @@ -20,9 +16,7 @@ export default test({ target.querySelector('button')?.click(); - assert.throws(() => { - throw error; - }, /state_unsafe_mutation/); + assert.include(errors[0], 'state_unsafe_mutation'); window.removeEventListener('error', handler, true); diff --git a/packages/svelte/tests/runtime-runes/samples/event-handler-invalid-values/_config.js b/packages/svelte/tests/runtime-runes/samples/event-handler-invalid-values/_config.js index d53812d4c3..7ca12af774 100644 --- a/packages/svelte/tests/runtime-runes/samples/event-handler-invalid-values/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/event-handler-invalid-values/_config.js @@ -1,4 +1,3 @@ -import { assertType } from 'vitest'; import { test } from '../../test'; export default test({ @@ -8,12 +7,8 @@ export default test({ dev: true }, - test({ assert, target, warnings, logs }) { - /** @type {any} */ - let error = null; - + test({ assert, target, warnings, logs, errors }) { const handler = (/** @type {any} */ e) => { - error = e.error; e.stopImmediatePropagation(); }; @@ -23,16 +18,12 @@ export default test({ b1.click(); assert.deepEqual(logs, []); - assert.equal(error, null); - - error = null; - logs.length = 0; + assert.deepEqual(errors, []); b2.click(); assert.deepEqual(logs, ['clicked']); - assert.equal(error, null); + assert.deepEqual(errors, []); - error = null; logs.length = 0; b3.click(); @@ -40,8 +31,7 @@ export default test({ assert.deepEqual(warnings, [ '`click` handler at main.svelte:10:17 should be a function. Did you mean to add a leading `() =>`?' ]); - assert.isNotNull(error); - assert.match(error.message, /is not a function/); + assert.include(errors[0], 'is not a function'); window.removeEventListener('error', handler, true); } diff --git a/packages/svelte/tests/runtime-runes/samples/event-handler-invalid-warning/_config.js b/packages/svelte/tests/runtime-runes/samples/event-handler-invalid-warning/_config.js index 423351a4c7..a0c792360e 100644 --- a/packages/svelte/tests/runtime-runes/samples/event-handler-invalid-warning/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/event-handler-invalid-warning/_config.js @@ -7,12 +7,8 @@ export default test({ dev: true }, - test({ assert, target, warnings }) { - /** @type {any} */ - let error; - + test({ assert, target, warnings, errors }) { const handler = (/** @type {any} */ e) => { - error = e.error; e.stopImmediatePropagation(); }; @@ -20,9 +16,7 @@ export default test({ target.querySelector('button')?.click(); - assert.throws(() => { - throw error; - }, /state_unsafe_mutation/); + assert.include(errors[0], 'state_unsafe_mutation'); window.removeEventListener('error', handler, true); diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-exception/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-exception/_config.js index 57caf6e08d..e155ff236a 100644 --- a/packages/svelte/tests/runtime-runes/samples/inspect-exception/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/inspect-exception/_config.js @@ -6,11 +6,12 @@ export default test({ dev: true }, - async test({ assert, target, logs }) { + async test({ assert, target, logs, errors }) { const b1 = target.querySelector('button'); b1?.click(); flushSync(); + assert.ok(errors.length > 0); assert.deepEqual(logs, ['init', 'a', 'init', 'b']); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-trace-null/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-trace-null/_config.js index e779a835c2..7982e8c1c6 100644 --- a/packages/svelte/tests/runtime-runes/samples/inspect-trace-null/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/inspect-trace-null/_config.js @@ -1,8 +1,12 @@ +import { assert } from 'vitest'; import { test } from '../../test'; export default test({ compileOptions: { dev: true }, - test() {} + + test({ logs }) { + assert.ok(logs.length > 0); + } }); diff --git a/packages/svelte/tests/runtime-runes/samples/unhoist-function-accessing-snippet/main.svelte b/packages/svelte/tests/runtime-runes/samples/unhoist-function-accessing-snippet/main.svelte index e909d77fd6..9f3a56a9ed 100644 --- a/packages/svelte/tests/runtime-runes/samples/unhoist-function-accessing-snippet/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/unhoist-function-accessing-snippet/main.svelte @@ -1,6 +1,6 @@ @@ -9,4 +9,4 @@ {#snippet snip()} snippet {x} -{/snippet} \ No newline at end of file +{/snippet} diff --git a/packages/svelte/tests/signals/test.ts b/packages/svelte/tests/signals/test.ts index 3a427e9392..8421ae4a7c 100644 --- a/packages/svelte/tests/signals/test.ts +++ b/packages/svelte/tests/signals/test.ts @@ -15,6 +15,7 @@ import { derived } from '../../src/internal/client/reactivity/deriveds'; import { snapshot } from '../../src/internal/shared/clone.js'; import { SvelteSet } from '../../src/reactivity/set'; import { DESTROYED } from '../../src/internal/client/constants'; +import { noop } from 'svelte/internal/client'; /** * @param runes runes mode @@ -469,6 +470,9 @@ describe('signals', () => { test('schedules rerun when writing to signal before reading it', (runes) => { if (!runes) return () => {}; + const error = console.error; + console.error = noop; + const value = state({ count: 0 }); user_effect(() => { set(value, { count: 0 }); @@ -482,14 +486,19 @@ describe('signals', () => { } catch (e: any) { assert.include(e.message, 'effect_update_depth_exceeded'); errored = true; + } finally { + assert.equal(errored, true); + console.error = error; } - assert.equal(errored, true); }; }); test('schedules rerun when updating deeply nested value', (runes) => { if (!runes) return () => {}; + const error = console.error; + console.error = noop; + const value = proxy({ a: { b: { c: 0 } } }); user_effect(() => { value.a.b.c += 1; @@ -502,14 +511,19 @@ describe('signals', () => { } catch (e: any) { assert.include(e.message, 'effect_update_depth_exceeded'); errored = true; + } finally { + assert.equal(errored, true); + console.error = error; } - assert.equal(errored, true); }; }); test('schedules rerun when writing to signal before reading it', (runes) => { if (!runes) return () => {}; + const error = console.error; + console.error = noop; + const value = proxy({ arr: [] }); user_effect(() => { value.arr = []; @@ -523,8 +537,10 @@ describe('signals', () => { } catch (e: any) { assert.include(e.message, 'effect_update_depth_exceeded'); errored = true; + } finally { + assert.equal(errored, true); + console.error = error; } - assert.equal(errored, true); }; }); From bfb969a6cccb92180180416641e48889eab730a6 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 21 Apr 2025 05:56:42 -0400 Subject: [PATCH 010/259] chore: simplify `process_effects` (#15809) --- packages/svelte/src/internal/client/runtime.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index fccea3e856..7a926bf624 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -780,19 +780,12 @@ function process_effects(root) { } else if (is_branch) { effect.f ^= CLEAN; } else { - // Ensure we set the effect to be the active reaction - // to ensure that unowned deriveds are correctly tracked - // because we're flushing the current effect - var previous_active_reaction = active_reaction; try { - active_reaction = effect; if (check_dirtiness(effect)) { update_effect(effect); } } catch (error) { handle_error(error, effect, null, effect.ctx); - } finally { - active_reaction = previous_active_reaction; } } From dfd742d532f578f47bc3e414e18e61af030381f7 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Tue, 22 Apr 2025 12:03:02 +0200 Subject: [PATCH 011/259] fix: don't mark selector lists inside `:global` with multiple items as unused (#15817) Regression from #15762 Fixes #15816 --- .changeset/wild-actors-retire.md | 5 +++++ .../src/compiler/phases/3-transform/css/index.js | 7 ++++--- .../tests/css/samples/global-block/_config.js | 16 ++++++++-------- .../tests/css/samples/global-block/expected.css | 4 ++++ .../tests/css/samples/global-block/input.svelte | 4 ++++ 5 files changed, 25 insertions(+), 11 deletions(-) create mode 100644 .changeset/wild-actors-retire.md diff --git a/.changeset/wild-actors-retire.md b/.changeset/wild-actors-retire.md new file mode 100644 index 0000000000..d01a1c169b --- /dev/null +++ b/.changeset/wild-actors-retire.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't mark selector lists inside `:global` with multiple items as unused diff --git a/packages/svelte/src/compiler/phases/3-transform/css/index.js b/packages/svelte/src/compiler/phases/3-transform/css/index.js index 9f1142cce9..cee7ab2791 100644 --- a/packages/svelte/src/compiler/phases/3-transform/css/index.js +++ b/packages/svelte/src/compiler/phases/3-transform/css/index.js @@ -196,9 +196,12 @@ const visitors = { next(); }, SelectorList(node, { state, next, path }) { + const parent = path.at(-1); + // Only add comments if we're not inside a complex selector that itself is unused or a global block if ( - (!is_in_global_block(path) || node.children.length > 1) && + (!is_in_global_block(path) || + (node.children.length > 1 && parent?.type === 'Rule' && parent.metadata.is_global_block)) && !path.find((n) => n.type === 'ComplexSelector' && !n.metadata.used) ) { const children = node.children; @@ -260,7 +263,6 @@ const visitors = { // if this selector list belongs to a rule, require a specificity bump for the // first scoped selector but only if we're at the top level - let parent = path.at(-1); if (parent?.type === 'Rule') { specificity = { bumped: false }; @@ -376,7 +378,6 @@ const visitors = { }; /** - * * @param {Array} path */ function is_in_global_block(path) { diff --git a/packages/svelte/tests/css/samples/global-block/_config.js b/packages/svelte/tests/css/samples/global-block/_config.js index a8b11a73ec..18a56e9a97 100644 --- a/packages/svelte/tests/css/samples/global-block/_config.js +++ b/packages/svelte/tests/css/samples/global-block/_config.js @@ -7,28 +7,28 @@ export default test({ code: 'css_unused_selector', message: 'Unused CSS selector ".unused :global"', start: { - line: 69, + line: 73, column: 1, - character: 917 + character: 964 }, end: { - line: 69, + line: 73, column: 16, - character: 932 + character: 979 } }, { code: 'css_unused_selector', message: 'Unused CSS selector "unused :global"', start: { - line: 100, + line: 104, column: 29, - character: 1223 + character: 1270 }, end: { - line: 100, + line: 104, column: 43, - character: 1237 + character: 1284 } } ] diff --git a/packages/svelte/tests/css/samples/global-block/expected.css b/packages/svelte/tests/css/samples/global-block/expected.css index 12f9a75032..be1838fd98 100644 --- a/packages/svelte/tests/css/samples/global-block/expected.css +++ b/packages/svelte/tests/css/samples/global-block/expected.css @@ -3,6 +3,10 @@ .x { color: green; } + + .a, .selector, .list { + color: green; + } /*}*/ div.svelte-xyz { diff --git a/packages/svelte/tests/css/samples/global-block/input.svelte b/packages/svelte/tests/css/samples/global-block/input.svelte index ee05205d67..86d438031a 100644 --- a/packages/svelte/tests/css/samples/global-block/input.svelte +++ b/packages/svelte/tests/css/samples/global-block/input.svelte @@ -5,6 +5,10 @@ .x { color: green; } + + .a, .selector, .list { + color: green; + } } div :global { From 018996c1677e17857c404df33c8035a1a0c8b42e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 22 Apr 2025 12:17:51 +0200 Subject: [PATCH 012/259] Version Packages (#15818) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/wild-actors-retire.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/wild-actors-retire.md diff --git a/.changeset/wild-actors-retire.md b/.changeset/wild-actors-retire.md deleted file mode 100644 index d01a1c169b..0000000000 --- a/.changeset/wild-actors-retire.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: don't mark selector lists inside `:global` with multiple items as unused diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index cb3b356cc7..72cd00bc6a 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.28.2 + +### Patch Changes + +- fix: don't mark selector lists inside `:global` with multiple items as unused ([#15817](https://github.com/sveltejs/svelte/pull/15817)) + ## 5.28.1 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index c91fbdd7d3..ff71429d2f 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.28.1", + "version": "5.28.2", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index a19a773cd1..a3a9979d65 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.28.1'; +export const VERSION = '5.28.2'; export const PUBLIC_VERSION = '5'; From 1c2fc210232c16d2eb0fdb20d9bac3cef2b87897 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Wed, 23 Apr 2025 16:49:11 +0200 Subject: [PATCH 013/259] fix: allow characters in the supplementary special-purpose plane (#15823) fixes #15821 Source: https://en.wikipedia.org/wiki/Plane_(Unicode)#Supplementary_Special-purpose_Plane --- .changeset/wild-bulldogs-move.md | 5 +++++ packages/svelte/src/compiler/phases/1-parse/utils/html.js | 7 +++++++ 2 files changed, 12 insertions(+) create mode 100644 .changeset/wild-bulldogs-move.md diff --git a/.changeset/wild-bulldogs-move.md b/.changeset/wild-bulldogs-move.md new file mode 100644 index 0000000000..c3c5580f77 --- /dev/null +++ b/.changeset/wild-bulldogs-move.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: allow characters in the supplementary special-purpose plane diff --git a/packages/svelte/src/compiler/phases/1-parse/utils/html.js b/packages/svelte/src/compiler/phases/1-parse/utils/html.js index a68acb996f..a0c2a5b06f 100644 --- a/packages/svelte/src/compiler/phases/1-parse/utils/html.js +++ b/packages/svelte/src/compiler/phases/1-parse/utils/html.js @@ -72,6 +72,8 @@ const NUL = 0; // to replace them ourselves // // Source: http://en.wikipedia.org/wiki/Character_encodings_in_HTML#Illegal_characters +// Also see: https://en.wikipedia.org/wiki/Plane_(Unicode) +// Also see: https://html.spec.whatwg.org/multipage/parsing.html#preprocessing-the-input-stream /** @param {number} code */ function validate_code(code) { @@ -116,5 +118,10 @@ function validate_code(code) { return code; } + // supplementary special-purpose plane 0xe0000 - 0xe07f and 0xe0100 - 0xe01ef + if ((code >= 917504 && code <= 917631) || (code >= 917760 && code <= 917999)) { + return code; + } + return NUL; } From c051a6eb1e61bf6c72842ba3c04900b07c54a5fe Mon Sep 17 00:00:00 2001 From: Lars Francke Date: Wed, 30 Apr 2025 08:53:22 +0200 Subject: [PATCH 014/259] fix: improve error message for migration errors when slot would be renamed (#15841) Co-authored-by: Paolo Ricciuti --- .changeset/strong-coins-peel.md | 5 +++++ packages/svelte/src/compiler/migrate/index.js | 4 ++-- .../impossible-migrate-$derived-derived-var-3/output.svelte | 4 ++-- .../impossible-migrate-slot-change-name/output.svelte | 4 ++-- .../impossible-migrate-slot-non-identifier/output.svelte | 4 ++-- 5 files changed, 13 insertions(+), 8 deletions(-) create mode 100644 .changeset/strong-coins-peel.md diff --git a/.changeset/strong-coins-peel.md b/.changeset/strong-coins-peel.md new file mode 100644 index 0000000000..013e8e44a1 --- /dev/null +++ b/.changeset/strong-coins-peel.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: improve error message for migration errors when slot would be renamed diff --git a/packages/svelte/src/compiler/migrate/index.js b/packages/svelte/src/compiler/migrate/index.js index 75a9a64905..2d5a4dcd9e 100644 --- a/packages/svelte/src/compiler/migrate/index.js +++ b/packages/svelte/src/compiler/migrate/index.js @@ -1307,7 +1307,7 @@ const template = { name = state.scope.generate(slot_name); if (name !== slot_name) { throw new MigrationError( - 'This migration would change the name of a slot making the component unusable' + `This migration would change the name of a slot (${slot_name} to ${name}) making the component unusable` ); } } @@ -1880,7 +1880,7 @@ function handle_identifier(node, state, path) { let new_name = state.scope.generate(name); if (new_name !== name) { throw new MigrationError( - 'This migration would change the name of a slot making the component unusable' + `This migration would change the name of a slot (${name} to ${new_name}) making the component unusable` ); } } diff --git a/packages/svelte/tests/migrate/samples/impossible-migrate-$derived-derived-var-3/output.svelte b/packages/svelte/tests/migrate/samples/impossible-migrate-$derived-derived-var-3/output.svelte index 9e4f086aed..26012e1115 100644 --- a/packages/svelte/tests/migrate/samples/impossible-migrate-$derived-derived-var-3/output.svelte +++ b/packages/svelte/tests/migrate/samples/impossible-migrate-$derived-derived-var-3/output.svelte @@ -1,7 +1,7 @@ - + - \ No newline at end of file + diff --git a/packages/svelte/tests/migrate/samples/impossible-migrate-slot-change-name/output.svelte b/packages/svelte/tests/migrate/samples/impossible-migrate-slot-change-name/output.svelte index 2b6838a1d6..328966b63b 100644 --- a/packages/svelte/tests/migrate/samples/impossible-migrate-slot-change-name/output.svelte +++ b/packages/svelte/tests/migrate/samples/impossible-migrate-slot-change-name/output.svelte @@ -1,6 +1,6 @@ - + - \ No newline at end of file + diff --git a/packages/svelte/tests/migrate/samples/impossible-migrate-slot-non-identifier/output.svelte b/packages/svelte/tests/migrate/samples/impossible-migrate-slot-non-identifier/output.svelte index 6e5ab10310..1e763577df 100644 --- a/packages/svelte/tests/migrate/samples/impossible-migrate-slot-non-identifier/output.svelte +++ b/packages/svelte/tests/migrate/samples/impossible-migrate-slot-non-identifier/output.svelte @@ -1,2 +1,2 @@ - - \ No newline at end of file + + From 0ace76d8f1ea5402087e202152630cd50a5d757a Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Sat, 3 May 2025 05:43:27 -0700 Subject: [PATCH 015/259] docs: clarify `$inspect.trace` docs (#15858) * docs: clarify `$inspect.trace` docs * Update 07-$inspect.md * Update documentation/docs/02-runes/07-$inspect.md --------- Co-authored-by: Rich Harris --- documentation/docs/02-runes/07-$inspect.md | 1 + 1 file changed, 1 insertion(+) diff --git a/documentation/docs/02-runes/07-$inspect.md b/documentation/docs/02-runes/07-$inspect.md index ff3d64757b..13ac8b79a3 100644 --- a/documentation/docs/02-runes/07-$inspect.md +++ b/documentation/docs/02-runes/07-$inspect.md @@ -52,6 +52,7 @@ This rune, added in 5.14, causes the surrounding function to be _traced_ in deve import { doSomeWork } from './elsewhere'; $effect(() => { + +++// $inspect.trace must be the first statement of a function body+++ +++$inspect.trace();+++ doSomeWork(); }); From 8b7bea162449877be637dea44caa256928be0b19 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 12 May 2025 14:19:47 -0400 Subject: [PATCH 016/259] chore: better `bench:compare` script (#15894) * chore: make bench:compare results more legible * fix * fix * fix --- benchmarking/compare/index.js | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/benchmarking/compare/index.js b/benchmarking/compare/index.js index 9d8d279c35..8f38686a29 100644 --- a/benchmarking/compare/index.js +++ b/benchmarking/compare/index.js @@ -67,19 +67,29 @@ for (let i = 0; i < results[0].length; i += 1) { for (const metric of ['time', 'gc_time']) { const times = results.map((result) => +result[i][metric]); let min = Infinity; + let max = -Infinity; let min_index = -1; for (let b = 0; b < times.length; b += 1) { - if (times[b] < min) { - min = times[b]; + const time = times[b]; + + if (time < min) { + min = time; min_index = b; } + + if (time > max) { + max = time; + } } if (min !== 0) { - console.group(`${metric}: fastest is ${branches[min_index]}`); + console.group(`${metric}: fastest is ${char(min_index)} (${branches[min_index]})`); times.forEach((time, b) => { - console.log(`${branches[b]}: ${time.toFixed(2)}ms (${((time / min) * 100).toFixed(2)}%)`); + const SIZE = 20; + const n = Math.round(SIZE * (time / max)); + + console.log(`${char(b)}: ${'◼'.repeat(n)}${' '.repeat(SIZE - n)} ${time.toFixed(2)}ms`); }); console.groupEnd(); } @@ -87,3 +97,7 @@ for (let i = 0; i < results[0].length; i += 1) { console.groupEnd(); } + +function char(i) { + return String.fromCharCode(97 + i); +} From b6164128b3ea823876fc9d14e8251befac6ff7bd Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 12 May 2025 15:51:05 -0400 Subject: [PATCH 017/259] chore: avoid microtasks when flushing sync (alternative) (#15895) * chore: avoid microtasks when flushing sync * chore: avoid microtasks when flushing sync * chore: avoid microtasks when flushing sync * tweak * WIP * another * more * tweak * fix * fix * belt and braces * Revert "belt and braces" This reverts commit f8de1cf47ade629347ae7087cbb7e5f5099d0758. --------- Co-authored-by: Dominic Gannaway --- .changeset/polite-melons-tickle.md | 5 +++++ packages/svelte/src/internal/client/runtime.js | 2 ++ 2 files changed, 7 insertions(+) create mode 100644 .changeset/polite-melons-tickle.md diff --git a/.changeset/polite-melons-tickle.md b/.changeset/polite-melons-tickle.md new file mode 100644 index 0000000000..42967ea936 --- /dev/null +++ b/.changeset/polite-melons-tickle.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: avoid microtasks when flushing sync diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 7a926bf624..2375dc0a63 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -823,6 +823,8 @@ export function flushSync(fn) { if (fn) { is_flushing = true; flush_queued_root_effects(); + + is_flushing = true; result = fn(); } From 9e49be4d307dd179eab6312a80f0c0f7bc0040b4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 12 May 2025 16:03:49 -0400 Subject: [PATCH 018/259] Version Packages (#15825) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/polite-melons-tickle.md | 5 ----- .changeset/strong-coins-peel.md | 5 ----- .changeset/wild-bulldogs-move.md | 5 ----- packages/svelte/CHANGELOG.md | 10 ++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 6 files changed, 12 insertions(+), 17 deletions(-) delete mode 100644 .changeset/polite-melons-tickle.md delete mode 100644 .changeset/strong-coins-peel.md delete mode 100644 .changeset/wild-bulldogs-move.md diff --git a/.changeset/polite-melons-tickle.md b/.changeset/polite-melons-tickle.md deleted file mode 100644 index 42967ea936..0000000000 --- a/.changeset/polite-melons-tickle.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -chore: avoid microtasks when flushing sync diff --git a/.changeset/strong-coins-peel.md b/.changeset/strong-coins-peel.md deleted file mode 100644 index 013e8e44a1..0000000000 --- a/.changeset/strong-coins-peel.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: improve error message for migration errors when slot would be renamed diff --git a/.changeset/wild-bulldogs-move.md b/.changeset/wild-bulldogs-move.md deleted file mode 100644 index c3c5580f77..0000000000 --- a/.changeset/wild-bulldogs-move.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: allow characters in the supplementary special-purpose plane diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 72cd00bc6a..8650ba0e5b 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,15 @@ # svelte +## 5.28.3 + +### Patch Changes + +- chore: avoid microtasks when flushing sync ([#15895](https://github.com/sveltejs/svelte/pull/15895)) + +- fix: improve error message for migration errors when slot would be renamed ([#15841](https://github.com/sveltejs/svelte/pull/15841)) + +- fix: allow characters in the supplementary special-purpose plane ([#15823](https://github.com/sveltejs/svelte/pull/15823)) + ## 5.28.2 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index ff71429d2f..85eef9c2eb 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.28.2", + "version": "5.28.3", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index a3a9979d65..b072c6432c 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.28.2'; +export const VERSION = '5.28.3'; export const PUBLIC_VERSION = '5'; From 1cab761110b7268bd4ad98c86db419f594a0ac2e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 May 2025 16:10:30 -0400 Subject: [PATCH 019/259] chore(deps-dev): bump vite from 5.4.18 to 5.4.19 (#15897) Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.4.18 to 5.4.19. - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/v5.4.19/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v5.4.19/packages/vite) --- updated-dependencies: - dependency-name: vite dependency-version: 5.4.19 dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- playgrounds/sandbox/package.json | 2 +- pnpm-lock.yaml | 289 ++++++++++++++++--------------- 2 files changed, 149 insertions(+), 142 deletions(-) diff --git a/playgrounds/sandbox/package.json b/playgrounds/sandbox/package.json index 5aee92ab17..3ab65ac4b5 100644 --- a/playgrounds/sandbox/package.json +++ b/playgrounds/sandbox/package.json @@ -18,7 +18,7 @@ "polka": "^1.0.0-next.25", "svelte": "workspace:*", "tinyglobby": "^0.2.12", - "vite": "^5.4.18", + "vite": "^5.4.19", "vite-plugin-inspect": "^0.8.4" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3518b0e57e..3d5db0fd8e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -152,7 +152,7 @@ importers: devDependencies: '@sveltejs/vite-plugin-svelte': specifier: ^4.0.0-next.6 - version: 4.0.0-next.6(svelte@packages+svelte)(vite@5.4.18(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) + version: 4.0.0-next.6(svelte@packages+svelte)(vite@5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) polka: specifier: ^1.0.0-next.25 version: 1.0.0-next.25 @@ -163,11 +163,11 @@ importers: specifier: ^0.2.12 version: 0.2.12 vite: - specifier: ^5.4.18 - version: 5.4.18(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) + specifier: ^5.4.19 + version: 5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) vite-plugin-inspect: specifier: ^0.8.4 - version: 0.8.4(rollup@4.39.0)(vite@5.4.18(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) + version: 0.8.4(rollup@4.40.2)(vite@5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) packages: @@ -408,8 +408,8 @@ packages: peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - '@eslint-community/eslint-utils@4.5.1': - resolution: {integrity: sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w==} + '@eslint-community/eslint-utils@4.7.0': + resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 @@ -551,8 +551,8 @@ packages: cpu: [arm] os: [android] - '@rollup/rollup-android-arm-eabi@4.39.0': - resolution: {integrity: sha512-lGVys55Qb00Wvh8DMAocp5kIcaNzEFTmGhfFd88LfaogYTRKrdxgtlO5H6S49v2Nd8R2C6wLOal0qv6/kCkOwA==} + '@rollup/rollup-android-arm-eabi@4.40.2': + resolution: {integrity: sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==} cpu: [arm] os: [android] @@ -561,8 +561,8 @@ packages: cpu: [arm64] os: [android] - '@rollup/rollup-android-arm64@4.39.0': - resolution: {integrity: sha512-It9+M1zE31KWfqh/0cJLrrsCPiF72PoJjIChLX+rEcujVRCb4NLQ5QzFkzIZW8Kn8FTbvGQBY5TkKBau3S8cCQ==} + '@rollup/rollup-android-arm64@4.40.2': + resolution: {integrity: sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw==} cpu: [arm64] os: [android] @@ -571,8 +571,8 @@ packages: cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-arm64@4.39.0': - resolution: {integrity: sha512-lXQnhpFDOKDXiGxsU9/l8UEGGM65comrQuZ+lDcGUx+9YQ9dKpF3rSEGepyeR5AHZ0b5RgiligsBhWZfSSQh8Q==} + '@rollup/rollup-darwin-arm64@4.40.2': + resolution: {integrity: sha512-Gzf1Hn2Aoe8VZzevHostPX23U7N5+4D36WJNHK88NZHCJr7aVMG4fadqkIf72eqVPGjGc0HJHNuUaUcxiR+N/w==} cpu: [arm64] os: [darwin] @@ -581,18 +581,18 @@ packages: cpu: [x64] os: [darwin] - '@rollup/rollup-darwin-x64@4.39.0': - resolution: {integrity: sha512-mKXpNZLvtEbgu6WCkNij7CGycdw9cJi2k9v0noMb++Vab12GZjFgUXD69ilAbBh034Zwn95c2PNSz9xM7KYEAQ==} + '@rollup/rollup-darwin-x64@4.40.2': + resolution: {integrity: sha512-47N4hxa01a4x6XnJoskMKTS8XZ0CZMd8YTbINbi+w03A2w4j1RTlnGHOz/P0+Bg1LaVL6ufZyNprSg+fW5nYQQ==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.39.0': - resolution: {integrity: sha512-jivRRlh2Lod/KvDZx2zUR+I4iBfHcu2V/BA2vasUtdtTN2Uk3jfcZczLa81ESHZHPHy4ih3T/W5rPFZ/hX7RtQ==} + '@rollup/rollup-freebsd-arm64@4.40.2': + resolution: {integrity: sha512-8t6aL4MD+rXSHHZUR1z19+9OFJ2rl1wGKvckN47XFRVO+QL/dUSpKA2SLRo4vMg7ELA8pzGpC+W9OEd1Z/ZqoQ==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.39.0': - resolution: {integrity: sha512-8RXIWvYIRK9nO+bhVz8DwLBepcptw633gv/QT4015CpJ0Ht8punmoHU/DuEd3iw9Hr8UwUV+t+VNNuZIWYeY7Q==} + '@rollup/rollup-freebsd-x64@4.40.2': + resolution: {integrity: sha512-C+AyHBzfpsOEYRFjztcYUFsH4S7UsE9cDtHCtma5BK8+ydOZYgMmWg1d/4KBytQspJCld8ZIujFMAdKG1xyr4Q==} cpu: [x64] os: [freebsd] @@ -601,8 +601,8 @@ packages: cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-gnueabihf@4.39.0': - resolution: {integrity: sha512-mz5POx5Zu58f2xAG5RaRRhp3IZDK7zXGk5sdEDj4o96HeaXhlUwmLFzNlc4hCQi5sGdR12VDgEUqVSHer0lI9g==} + '@rollup/rollup-linux-arm-gnueabihf@4.40.2': + resolution: {integrity: sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==} cpu: [arm] os: [linux] @@ -611,8 +611,8 @@ packages: cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.39.0': - resolution: {integrity: sha512-+YDwhM6gUAyakl0CD+bMFpdmwIoRDzZYaTWV3SDRBGkMU/VpIBYXXEvkEcTagw/7VVkL2vA29zU4UVy1mP0/Yw==} + '@rollup/rollup-linux-arm-musleabihf@4.40.2': + resolution: {integrity: sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==} cpu: [arm] os: [linux] @@ -621,8 +621,8 @@ packages: cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.39.0': - resolution: {integrity: sha512-EKf7iF7aK36eEChvlgxGnk7pdJfzfQbNvGV/+l98iiMwU23MwvmV0Ty3pJ0p5WQfm3JRHOytSIqD9LB7Bq7xdQ==} + '@rollup/rollup-linux-arm64-gnu@4.40.2': + resolution: {integrity: sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==} cpu: [arm64] os: [linux] @@ -631,13 +631,13 @@ packages: cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.39.0': - resolution: {integrity: sha512-vYanR6MtqC7Z2SNr8gzVnzUul09Wi1kZqJaek3KcIlI/wq5Xtq4ZPIZ0Mr/st/sv/NnaPwy/D4yXg5x0B3aUUA==} + '@rollup/rollup-linux-arm64-musl@4.40.2': + resolution: {integrity: sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-loongarch64-gnu@4.39.0': - resolution: {integrity: sha512-NMRUT40+h0FBa5fb+cpxtZoGAggRem16ocVKIv5gDB5uLDgBIwrIsXlGqYbLwW8YyO3WVTk1FkFDjMETYlDqiw==} + '@rollup/rollup-linux-loongarch64-gnu@4.40.2': + resolution: {integrity: sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==} cpu: [loong64] os: [linux] @@ -646,8 +646,8 @@ packages: cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-powerpc64le-gnu@4.39.0': - resolution: {integrity: sha512-0pCNnmxgduJ3YRt+D+kJ6Ai/r+TaePu9ZLENl+ZDV/CdVczXl95CbIiwwswu4L+K7uOIGf6tMo2vm8uadRaICQ==} + '@rollup/rollup-linux-powerpc64le-gnu@4.40.2': + resolution: {integrity: sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==} cpu: [ppc64] os: [linux] @@ -656,13 +656,13 @@ packages: cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.39.0': - resolution: {integrity: sha512-t7j5Zhr7S4bBtksT73bO6c3Qa2AV/HqiGlj9+KB3gNF5upcVkx+HLgxTm8DK4OkzsOYqbdqbLKwvGMhylJCPhQ==} + '@rollup/rollup-linux-riscv64-gnu@4.40.2': + resolution: {integrity: sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-riscv64-musl@4.39.0': - resolution: {integrity: sha512-m6cwI86IvQ7M93MQ2RF5SP8tUjD39Y7rjb1qjHgYh28uAPVU8+k/xYWvxRO3/tBN2pZkSMa5RjnPuUIbrwVxeA==} + '@rollup/rollup-linux-riscv64-musl@4.40.2': + resolution: {integrity: sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==} cpu: [riscv64] os: [linux] @@ -671,8 +671,8 @@ packages: cpu: [s390x] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.39.0': - resolution: {integrity: sha512-iRDJd2ebMunnk2rsSBYlsptCyuINvxUfGwOUldjv5M4tpa93K8tFMeYGpNk2+Nxl+OBJnBzy2/JCscGeO507kA==} + '@rollup/rollup-linux-s390x-gnu@4.40.2': + resolution: {integrity: sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==} cpu: [s390x] os: [linux] @@ -681,8 +681,8 @@ packages: cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.39.0': - resolution: {integrity: sha512-t9jqYw27R6Lx0XKfEFe5vUeEJ5pF3SGIM6gTfONSMb7DuG6z6wfj2yjcoZxHg129veTqU7+wOhY6GX8wmf90dA==} + '@rollup/rollup-linux-x64-gnu@4.40.2': + resolution: {integrity: sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==} cpu: [x64] os: [linux] @@ -691,8 +691,8 @@ packages: cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.39.0': - resolution: {integrity: sha512-ThFdkrFDP55AIsIZDKSBWEt/JcWlCzydbZHinZ0F/r1h83qbGeenCt/G/wG2O0reuENDD2tawfAj2s8VK7Bugg==} + '@rollup/rollup-linux-x64-musl@4.40.2': + resolution: {integrity: sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==} cpu: [x64] os: [linux] @@ -701,8 +701,8 @@ packages: cpu: [arm64] os: [win32] - '@rollup/rollup-win32-arm64-msvc@4.39.0': - resolution: {integrity: sha512-jDrLm6yUtbOg2TYB3sBF3acUnAwsIksEYjLeHL+TJv9jg+TmTwdyjnDex27jqEMakNKf3RwwPahDIt7QXCSqRQ==} + '@rollup/rollup-win32-arm64-msvc@4.40.2': + resolution: {integrity: sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==} cpu: [arm64] os: [win32] @@ -711,8 +711,8 @@ packages: cpu: [ia32] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.39.0': - resolution: {integrity: sha512-6w9uMuza+LbLCVoNKL5FSLE7yvYkq9laSd09bwS0tMjkwXrmib/4KmoJcrKhLWHvw19mwU+33ndC69T7weNNjQ==} + '@rollup/rollup-win32-ia32-msvc@4.40.2': + resolution: {integrity: sha512-dt1llVSGEsGKvzeIO76HToiYPNPYPkmjhMHhP00T9S4rDern8P2ZWvWAQUEJ+R1UdMWJ/42i/QqJ2WV765GZcA==} cpu: [ia32] os: [win32] @@ -721,8 +721,8 @@ packages: cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.39.0': - resolution: {integrity: sha512-yAkUOkIKZlK5dl7u6dg897doBgLXmUHhIINM2c+sND3DZwnrdQkkSiDh7N75Ll4mM4dxSkYfXqU9fW3lLkMFug==} + '@rollup/rollup-win32-x64-msvc@4.40.2': + resolution: {integrity: sha512-bwspbWB04XJpeElvsp+DCylKfF4trJDa2Y9Go8O6A7YLX2LIKGcNK/CYImJN6ZP4DcuOHB4Utl3iCbnR62DudA==} cpu: [x64] os: [win32] @@ -816,8 +816,8 @@ packages: resolution: {integrity: sha512-E0ntLvsfPqnPwng8b8y4OGuzh/iIOm2z8U3S9zic2TeMLW61u5IH2Q1wu0oSTkfrSzwbDJIB/Lm8O3//8BWMPA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/scope-manager@8.29.1': - resolution: {integrity: sha512-2nggXGX5F3YrsGN08pw4XpMLO1Rgtnn4AzTegC2MDesv6q3QaTU5yU7IbS1tf1IwCR0Hv/1EFygLn9ms6LIpDA==} + '@typescript-eslint/scope-manager@8.32.1': + resolution: {integrity: sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@typescript-eslint/type-utils@8.26.0': @@ -831,8 +831,8 @@ packages: resolution: {integrity: sha512-89B1eP3tnpr9A8L6PZlSjBvnJhWXtYfZhECqlBl1D9Lme9mHO6iWlsprBtVenQvY1HMhax1mWOjhtL3fh/u+pA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/types@8.29.1': - resolution: {integrity: sha512-VT7T1PuJF1hpYC3AGm2rCgJBjHL3nc+A/bhOp9sGMKfi5v0WufsX/sHCFBfNTx2F+zA6qBc/PD0/kLRLjdt8mQ==} + '@typescript-eslint/types@8.32.1': + resolution: {integrity: sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@typescript-eslint/typescript-estree@8.26.0': @@ -841,8 +841,8 @@ packages: peerDependencies: typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/typescript-estree@8.29.1': - resolution: {integrity: sha512-l1enRoSaUkQxOQnbi0KPUtqeZkSiFlqrx9/3ns2rEDhGKfTa+88RmXqedC1zmVTOWrLc2e6DEJrTA51C9iLH5g==} + '@typescript-eslint/typescript-estree@8.32.1': + resolution: {integrity: sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <5.9.0' @@ -854,8 +854,8 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/utils@8.29.1': - resolution: {integrity: sha512-QAkFEbytSaB8wnmB+DflhUPz6CLbFWE2SnSCrRMEa+KnXIzDYbpsn++1HGvnfAsUY44doDXmvRkO5shlM/3UfA==} + '@typescript-eslint/utils@8.32.1': + resolution: {integrity: sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -865,8 +865,8 @@ packages: resolution: {integrity: sha512-2z8JQJWAzPdDd51dRQ/oqIJxe99/hoLIqmf8RMCAJQtYDc535W/Jt2+RTP4bP0aKeBG1F65yjIZuczOXCmbWwg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/visitor-keys@8.29.1': - resolution: {integrity: sha512-RGLh5CRaUEf02viP5c1Vh1cMGffQscyHe7HPAzGpfmfflFg1wUz2rYxd+OZqwpeypYvZ8UxSxuIpF++fmOzEcg==} + '@typescript-eslint/visitor-keys@8.32.1': + resolution: {integrity: sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@vitest/coverage-v8@2.0.5': @@ -1974,8 +1974,8 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true - rollup@4.39.0: - resolution: {integrity: sha512-thI8kNc02yNvnmJp8dr3fNWJ9tCONDhp6TV35X6HkKGGs9E6q7YWCHbe5vKiTa7TAiNcFEmXKj3X/pG2b3ci0g==} + rollup@4.40.2: + resolution: {integrity: sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -2013,6 +2013,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} @@ -2310,8 +2315,8 @@ packages: terser: optional: true - vite@5.4.18: - resolution: {integrity: sha512-1oDcnEp3lVyHCuQ2YFelM4Alm2o91xNoMncRm1U7S+JdYfYOvbiGZ3/CxGttrOu2M/KcGz7cRC2DoNUA6urmMA==} + vite@5.4.19: + resolution: {integrity: sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: @@ -2711,7 +2716,7 @@ snapshots: eslint: 9.9.1 eslint-visitor-keys: 3.4.3 - '@eslint-community/eslint-utils@4.5.1(eslint@9.9.1)': + '@eslint-community/eslint-utils@4.7.0(eslint@9.9.1)': dependencies: eslint: 9.9.1 eslint-visitor-keys: 3.4.3 @@ -2860,120 +2865,120 @@ snapshots: optionalDependencies: rollup: 4.22.4 - '@rollup/pluginutils@5.1.0(rollup@4.39.0)': + '@rollup/pluginutils@5.1.0(rollup@4.40.2)': dependencies: '@types/estree': 1.0.6 estree-walker: 2.0.2 picomatch: 2.3.1 optionalDependencies: - rollup: 4.39.0 + rollup: 4.40.2 '@rollup/rollup-android-arm-eabi@4.22.4': optional: true - '@rollup/rollup-android-arm-eabi@4.39.0': + '@rollup/rollup-android-arm-eabi@4.40.2': optional: true '@rollup/rollup-android-arm64@4.22.4': optional: true - '@rollup/rollup-android-arm64@4.39.0': + '@rollup/rollup-android-arm64@4.40.2': optional: true '@rollup/rollup-darwin-arm64@4.22.4': optional: true - '@rollup/rollup-darwin-arm64@4.39.0': + '@rollup/rollup-darwin-arm64@4.40.2': optional: true '@rollup/rollup-darwin-x64@4.22.4': optional: true - '@rollup/rollup-darwin-x64@4.39.0': + '@rollup/rollup-darwin-x64@4.40.2': optional: true - '@rollup/rollup-freebsd-arm64@4.39.0': + '@rollup/rollup-freebsd-arm64@4.40.2': optional: true - '@rollup/rollup-freebsd-x64@4.39.0': + '@rollup/rollup-freebsd-x64@4.40.2': optional: true '@rollup/rollup-linux-arm-gnueabihf@4.22.4': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.39.0': + '@rollup/rollup-linux-arm-gnueabihf@4.40.2': optional: true '@rollup/rollup-linux-arm-musleabihf@4.22.4': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.39.0': + '@rollup/rollup-linux-arm-musleabihf@4.40.2': optional: true '@rollup/rollup-linux-arm64-gnu@4.22.4': optional: true - '@rollup/rollup-linux-arm64-gnu@4.39.0': + '@rollup/rollup-linux-arm64-gnu@4.40.2': optional: true '@rollup/rollup-linux-arm64-musl@4.22.4': optional: true - '@rollup/rollup-linux-arm64-musl@4.39.0': + '@rollup/rollup-linux-arm64-musl@4.40.2': optional: true - '@rollup/rollup-linux-loongarch64-gnu@4.39.0': + '@rollup/rollup-linux-loongarch64-gnu@4.40.2': optional: true '@rollup/rollup-linux-powerpc64le-gnu@4.22.4': optional: true - '@rollup/rollup-linux-powerpc64le-gnu@4.39.0': + '@rollup/rollup-linux-powerpc64le-gnu@4.40.2': optional: true '@rollup/rollup-linux-riscv64-gnu@4.22.4': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.39.0': + '@rollup/rollup-linux-riscv64-gnu@4.40.2': optional: true - '@rollup/rollup-linux-riscv64-musl@4.39.0': + '@rollup/rollup-linux-riscv64-musl@4.40.2': optional: true '@rollup/rollup-linux-s390x-gnu@4.22.4': optional: true - '@rollup/rollup-linux-s390x-gnu@4.39.0': + '@rollup/rollup-linux-s390x-gnu@4.40.2': optional: true '@rollup/rollup-linux-x64-gnu@4.22.4': optional: true - '@rollup/rollup-linux-x64-gnu@4.39.0': + '@rollup/rollup-linux-x64-gnu@4.40.2': optional: true '@rollup/rollup-linux-x64-musl@4.22.4': optional: true - '@rollup/rollup-linux-x64-musl@4.39.0': + '@rollup/rollup-linux-x64-musl@4.40.2': optional: true '@rollup/rollup-win32-arm64-msvc@4.22.4': optional: true - '@rollup/rollup-win32-arm64-msvc@4.39.0': + '@rollup/rollup-win32-arm64-msvc@4.40.2': optional: true '@rollup/rollup-win32-ia32-msvc@4.22.4': optional: true - '@rollup/rollup-win32-ia32-msvc@4.39.0': + '@rollup/rollup-win32-ia32-msvc@4.40.2': optional: true '@rollup/rollup-win32-x64-msvc@4.22.4': optional: true - '@rollup/rollup-win32-x64-msvc@4.39.0': + '@rollup/rollup-win32-x64-msvc@4.40.2': optional: true '@stylistic/eslint-plugin-js@1.8.0(eslint@9.9.1)': @@ -3000,25 +3005,25 @@ snapshots: typescript: 5.5.4 typescript-eslint: 8.26.0(eslint@9.9.1)(typescript@5.5.4) - '@sveltejs/vite-plugin-svelte-inspector@3.0.0-next.2(@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@packages+svelte)(vite@5.4.18(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)))(svelte@packages+svelte)(vite@5.4.18(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))': + '@sveltejs/vite-plugin-svelte-inspector@3.0.0-next.2(@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@packages+svelte)(vite@5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)))(svelte@packages+svelte)(vite@5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))': dependencies: - '@sveltejs/vite-plugin-svelte': 4.0.0-next.6(svelte@packages+svelte)(vite@5.4.18(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) + '@sveltejs/vite-plugin-svelte': 4.0.0-next.6(svelte@packages+svelte)(vite@5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) debug: 4.4.0 svelte: link:packages/svelte - vite: 5.4.18(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) + vite: 5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@packages+svelte)(vite@5.4.18(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))': + '@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@packages+svelte)(vite@5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 3.0.0-next.2(@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@packages+svelte)(vite@5.4.18(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)))(svelte@packages+svelte)(vite@5.4.18(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) + '@sveltejs/vite-plugin-svelte-inspector': 3.0.0-next.2(@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@packages+svelte)(vite@5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)))(svelte@packages+svelte)(vite@5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) debug: 4.4.0 deepmerge: 4.3.1 kleur: 4.1.5 magic-string: 0.30.17 svelte: link:packages/svelte - vite: 5.4.18(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) - vitefu: 0.2.5(vite@5.4.18(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) + vite: 5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) + vitefu: 0.2.5(vite@5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) transitivePeerDependencies: - supports-color @@ -3088,10 +3093,10 @@ snapshots: '@typescript-eslint/types': 8.26.0 '@typescript-eslint/visitor-keys': 8.26.0 - '@typescript-eslint/scope-manager@8.29.1': + '@typescript-eslint/scope-manager@8.32.1': dependencies: - '@typescript-eslint/types': 8.29.1 - '@typescript-eslint/visitor-keys': 8.29.1 + '@typescript-eslint/types': 8.32.1 + '@typescript-eslint/visitor-keys': 8.32.1 '@typescript-eslint/type-utils@8.26.0(eslint@9.9.1)(typescript@5.5.4)': dependencies: @@ -3106,7 +3111,7 @@ snapshots: '@typescript-eslint/types@8.26.0': {} - '@typescript-eslint/types@8.29.1': {} + '@typescript-eslint/types@8.32.1': {} '@typescript-eslint/typescript-estree@8.26.0(typescript@5.5.4)': dependencies: @@ -3122,15 +3127,15 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@8.29.1(typescript@5.5.4)': + '@typescript-eslint/typescript-estree@8.32.1(typescript@5.5.4)': dependencies: - '@typescript-eslint/types': 8.29.1 - '@typescript-eslint/visitor-keys': 8.29.1 + '@typescript-eslint/types': 8.32.1 + '@typescript-eslint/visitor-keys': 8.32.1 debug: 4.4.0 fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 - semver: 7.7.1 + semver: 7.7.2 ts-api-utils: 2.1.0(typescript@5.5.4) typescript: 5.5.4 transitivePeerDependencies: @@ -3147,12 +3152,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.29.1(eslint@9.9.1)(typescript@5.5.4)': + '@typescript-eslint/utils@8.32.1(eslint@9.9.1)(typescript@5.5.4)': dependencies: - '@eslint-community/eslint-utils': 4.5.1(eslint@9.9.1) - '@typescript-eslint/scope-manager': 8.29.1 - '@typescript-eslint/types': 8.29.1 - '@typescript-eslint/typescript-estree': 8.29.1(typescript@5.5.4) + '@eslint-community/eslint-utils': 4.7.0(eslint@9.9.1) + '@typescript-eslint/scope-manager': 8.32.1 + '@typescript-eslint/types': 8.32.1 + '@typescript-eslint/typescript-estree': 8.32.1(typescript@5.5.4) eslint: 9.9.1 typescript: 5.5.4 transitivePeerDependencies: @@ -3163,9 +3168,9 @@ snapshots: '@typescript-eslint/types': 8.26.0 eslint-visitor-keys: 4.2.0 - '@typescript-eslint/visitor-keys@8.29.1': + '@typescript-eslint/visitor-keys@8.32.1': dependencies: - '@typescript-eslint/types': 8.29.1 + '@typescript-eslint/types': 8.32.1 eslint-visitor-keys: 4.2.0 '@vitest/coverage-v8@2.0.5(vitest@2.1.9(@types/node@20.12.7)(jsdom@25.0.1)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))': @@ -3491,7 +3496,7 @@ snapshots: eslint-compat-utils@0.5.1(eslint@9.9.1): dependencies: eslint: 9.9.1 - semver: 7.7.1 + semver: 7.7.2 eslint-config-prettier@9.1.0(eslint@9.9.1): dependencies: @@ -3499,7 +3504,7 @@ snapshots: eslint-plugin-es-x@7.8.0(eslint@9.9.1): dependencies: - '@eslint-community/eslint-utils': 4.5.1(eslint@9.9.1) + '@eslint-community/eslint-utils': 4.7.0(eslint@9.9.1) '@eslint-community/regexpp': 4.12.1 eslint: 9.9.1 eslint-compat-utils: 0.5.1(eslint@9.9.1) @@ -3508,8 +3513,8 @@ snapshots: eslint-plugin-n@17.16.1(eslint@9.9.1)(typescript@5.5.4): dependencies: - '@eslint-community/eslint-utils': 4.5.1(eslint@9.9.1) - '@typescript-eslint/utils': 8.29.1(eslint@9.9.1)(typescript@5.5.4) + '@eslint-community/eslint-utils': 4.7.0(eslint@9.9.1) + '@typescript-eslint/utils': 8.32.1(eslint@9.9.1)(typescript@5.5.4) enhanced-resolve: 5.18.1 eslint: 9.9.1 eslint-plugin-es-x: 7.8.0(eslint@9.9.1) @@ -3517,7 +3522,7 @@ snapshots: globals: 15.15.0 ignore: 5.3.2 minimatch: 9.0.5 - semver: 7.7.1 + semver: 7.7.2 ts-declaration-location: 1.0.7(typescript@5.5.4) transitivePeerDependencies: - supports-color @@ -3525,7 +3530,7 @@ snapshots: eslint-plugin-svelte@2.38.0(eslint@9.9.1)(svelte@packages+svelte): dependencies: - '@eslint-community/eslint-utils': 4.5.1(eslint@9.9.1) + '@eslint-community/eslint-utils': 4.7.0(eslint@9.9.1) '@jridgewell/sourcemap-codec': 1.5.0 debug: 4.4.0 eslint: 9.9.1 @@ -3536,7 +3541,7 @@ snapshots: postcss-load-config: 3.1.4(postcss@8.5.3) postcss-safe-parser: 6.0.0(postcss@8.5.3) postcss-selector-parser: 6.1.2 - semver: 7.7.1 + semver: 7.7.2 svelte-eslint-parser: 0.43.0(svelte@packages+svelte) optionalDependencies: svelte: link:packages/svelte @@ -4292,30 +4297,30 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.22.4 fsevents: 2.3.3 - rollup@4.39.0: + rollup@4.40.2: dependencies: '@types/estree': 1.0.7 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.39.0 - '@rollup/rollup-android-arm64': 4.39.0 - '@rollup/rollup-darwin-arm64': 4.39.0 - '@rollup/rollup-darwin-x64': 4.39.0 - '@rollup/rollup-freebsd-arm64': 4.39.0 - '@rollup/rollup-freebsd-x64': 4.39.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.39.0 - '@rollup/rollup-linux-arm-musleabihf': 4.39.0 - '@rollup/rollup-linux-arm64-gnu': 4.39.0 - '@rollup/rollup-linux-arm64-musl': 4.39.0 - '@rollup/rollup-linux-loongarch64-gnu': 4.39.0 - '@rollup/rollup-linux-powerpc64le-gnu': 4.39.0 - '@rollup/rollup-linux-riscv64-gnu': 4.39.0 - '@rollup/rollup-linux-riscv64-musl': 4.39.0 - '@rollup/rollup-linux-s390x-gnu': 4.39.0 - '@rollup/rollup-linux-x64-gnu': 4.39.0 - '@rollup/rollup-linux-x64-musl': 4.39.0 - '@rollup/rollup-win32-arm64-msvc': 4.39.0 - '@rollup/rollup-win32-ia32-msvc': 4.39.0 - '@rollup/rollup-win32-x64-msvc': 4.39.0 + '@rollup/rollup-android-arm-eabi': 4.40.2 + '@rollup/rollup-android-arm64': 4.40.2 + '@rollup/rollup-darwin-arm64': 4.40.2 + '@rollup/rollup-darwin-x64': 4.40.2 + '@rollup/rollup-freebsd-arm64': 4.40.2 + '@rollup/rollup-freebsd-x64': 4.40.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.40.2 + '@rollup/rollup-linux-arm-musleabihf': 4.40.2 + '@rollup/rollup-linux-arm64-gnu': 4.40.2 + '@rollup/rollup-linux-arm64-musl': 4.40.2 + '@rollup/rollup-linux-loongarch64-gnu': 4.40.2 + '@rollup/rollup-linux-powerpc64le-gnu': 4.40.2 + '@rollup/rollup-linux-riscv64-gnu': 4.40.2 + '@rollup/rollup-linux-riscv64-musl': 4.40.2 + '@rollup/rollup-linux-s390x-gnu': 4.40.2 + '@rollup/rollup-linux-x64-gnu': 4.40.2 + '@rollup/rollup-linux-x64-musl': 4.40.2 + '@rollup/rollup-win32-arm64-msvc': 4.40.2 + '@rollup/rollup-win32-ia32-msvc': 4.40.2 + '@rollup/rollup-win32-x64-msvc': 4.40.2 fsevents: 2.3.3 rrweb-cssom@0.7.1: {} @@ -4347,6 +4352,8 @@ snapshots: semver@7.7.1: {} + semver@7.7.2: {} + serialize-javascript@6.0.2: dependencies: randombytes: 2.1.0 @@ -4562,7 +4569,7 @@ snapshots: debug: 4.4.0 es-module-lexer: 1.6.0 pathe: 1.1.2 - vite: 5.4.18(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) + vite: 5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) transitivePeerDependencies: - '@types/node' - less @@ -4574,10 +4581,10 @@ snapshots: - supports-color - terser - vite-plugin-inspect@0.8.4(rollup@4.39.0)(vite@5.4.18(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)): + vite-plugin-inspect@0.8.4(rollup@4.40.2)(vite@5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)): dependencies: '@antfu/utils': 0.7.8 - '@rollup/pluginutils': 5.1.0(rollup@4.39.0) + '@rollup/pluginutils': 5.1.0(rollup@4.40.2) debug: 4.4.0 error-stack-parser-es: 0.1.1 fs-extra: 11.2.0 @@ -4585,7 +4592,7 @@ snapshots: perfect-debounce: 1.0.0 picocolors: 1.1.1 sirv: 2.0.4 - vite: 5.4.18(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) + vite: 5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) transitivePeerDependencies: - rollup - supports-color @@ -4602,11 +4609,11 @@ snapshots: sass: 1.70.0 terser: 5.27.0 - vite@5.4.18(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0): + vite@5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0): dependencies: esbuild: 0.21.5 postcss: 8.5.3 - rollup: 4.39.0 + rollup: 4.40.2 optionalDependencies: '@types/node': 20.12.7 fsevents: 2.3.3 @@ -4614,9 +4621,9 @@ snapshots: sass: 1.70.0 terser: 5.27.0 - vitefu@0.2.5(vite@5.4.18(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)): + vitefu@0.2.5(vite@5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)): optionalDependencies: - vite: 5.4.18(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) + vite: 5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) vitest@2.1.9(@types/node@20.12.7)(jsdom@25.0.1)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0): dependencies: From df03af2e9d6e53be6cfb72257c42b0c9e5e2c050 Mon Sep 17 00:00:00 2001 From: 7nik Date: Mon, 12 May 2025 23:15:03 +0300 Subject: [PATCH 020/259] fix: emit right error for a shadowed invalid rune (#15892) Co-authored-by: 7nik --- .changeset/tall-cherries-fix.md | 5 +++++ .../src/compiler/phases/2-analyze/visitors/Identifier.js | 2 +- .../samples/invalid-rune-name-shadowed/_config.js | 8 ++++++++ .../samples/invalid-rune-name-shadowed/main.svelte.js | 5 +++++ 4 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 .changeset/tall-cherries-fix.md create mode 100644 packages/svelte/tests/compiler-errors/samples/invalid-rune-name-shadowed/_config.js create mode 100644 packages/svelte/tests/compiler-errors/samples/invalid-rune-name-shadowed/main.svelte.js diff --git a/.changeset/tall-cherries-fix.md b/.changeset/tall-cherries-fix.md new file mode 100644 index 0000000000..df25e02da9 --- /dev/null +++ b/.changeset/tall-cherries-fix.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: emit right error for a shadowed invalid rune diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js index efbbe6cfa2..89a81127c1 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js @@ -39,7 +39,7 @@ export function Identifier(node, context) { if ( is_rune(node.name) && context.state.scope.get(node.name) === null && - context.state.scope.get(node.name.slice(1)) === null + context.state.scope.get(node.name.slice(1))?.kind !== 'store_sub' ) { /** @type {Expression} */ let current = node; diff --git a/packages/svelte/tests/compiler-errors/samples/invalid-rune-name-shadowed/_config.js b/packages/svelte/tests/compiler-errors/samples/invalid-rune-name-shadowed/_config.js new file mode 100644 index 0000000000..a24996677c --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/invalid-rune-name-shadowed/_config.js @@ -0,0 +1,8 @@ +import { test } from '../../test'; + +export default test({ + error: { + code: 'rune_invalid_name', + message: '`$state.foo` is not a valid rune' + } +}); diff --git a/packages/svelte/tests/compiler-errors/samples/invalid-rune-name-shadowed/main.svelte.js b/packages/svelte/tests/compiler-errors/samples/invalid-rune-name-shadowed/main.svelte.js new file mode 100644 index 0000000000..966f7bc63b --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/invalid-rune-name-shadowed/main.svelte.js @@ -0,0 +1,5 @@ +class State { + value = $state.foo(); +} + +export const state = new State(); From eebb940b31285f300edd8a4a8c05aeab6a7b3d6e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 12 May 2025 17:07:06 -0400 Subject: [PATCH 021/259] fix: prevent invalid BigInt calls from blowing up at compile time (#15900) --- .changeset/little-worms-wonder.md | 5 +++++ packages/svelte/src/compiler/phases/scope.js | 2 +- .../tests/runtime-runes/samples/bigint-invalid/_config.js | 7 +++++++ .../tests/runtime-runes/samples/bigint-invalid/main.svelte | 5 +++++ 4 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 .changeset/little-worms-wonder.md create mode 100644 packages/svelte/tests/runtime-runes/samples/bigint-invalid/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/bigint-invalid/main.svelte diff --git a/.changeset/little-worms-wonder.md b/.changeset/little-worms-wonder.md new file mode 100644 index 0000000000..5482d7d5ce --- /dev/null +++ b/.changeset/little-worms-wonder.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: prevent invalid BigInt calls from blowing up at compile time diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 8297f174d3..752bdac9d8 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -23,7 +23,7 @@ export const STRING = Symbol('string'); /** @type {Record} */ const globals = { - BigInt: [NUMBER, BigInt], + BigInt: [NUMBER], 'Math.min': [NUMBER, Math.min], 'Math.max': [NUMBER, Math.max], 'Math.random': [NUMBER], diff --git a/packages/svelte/tests/runtime-runes/samples/bigint-invalid/_config.js b/packages/svelte/tests/runtime-runes/samples/bigint-invalid/_config.js new file mode 100644 index 0000000000..810ac338a5 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bigint-invalid/_config.js @@ -0,0 +1,7 @@ +import { test } from '../../test'; + +export default test({ + // check that this is a runtime error, not a compile time error + // caused by over-eager partial-evaluation + error: 'Cannot convert invalid to a BigInt' +}); diff --git a/packages/svelte/tests/runtime-runes/samples/bigint-invalid/main.svelte b/packages/svelte/tests/runtime-runes/samples/bigint-invalid/main.svelte new file mode 100644 index 0000000000..126528bad2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bigint-invalid/main.svelte @@ -0,0 +1,5 @@ + + +

{invalid}

From 8a351944a3445a10ea4a035edf5255b3701e2c30 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 12 May 2025 17:07:46 -0400 Subject: [PATCH 022/259] chore: tweak flushSync implementation (#15898) --- packages/svelte/src/internal/client/runtime.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 2375dc0a63..d790c0ad14 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -828,15 +828,16 @@ export function flushSync(fn) { result = fn(); } - flush_tasks(); + while (true) { + flush_tasks(); + + if (queued_root_effects.length === 0) { + return /** @type {T} */ (result); + } - while (queued_root_effects.length > 0) { is_flushing = true; flush_queued_root_effects(); - flush_tasks(); } - - return /** @type {T} */ (result); } /** From 895337cf4786fea4131d6fd2fdc79c6369a2a088 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 12 May 2025 17:12:48 -0400 Subject: [PATCH 023/259] fix: treat nullish expression as empty string (#15901) * fix: treat nullish expression as empty string * fix --- .changeset/blue-badgers-play.md | 5 +++++ .../phases/3-transform/client/visitors/shared/utils.js | 4 ++-- .../runtime-runes/samples/nullish-empty-string/_config.js | 5 +++++ .../runtime-runes/samples/nullish-empty-string/main.svelte | 1 + .../snapshot/samples/purity/_expected/client/index.svelte.js | 2 +- 5 files changed, 14 insertions(+), 3 deletions(-) create mode 100644 .changeset/blue-badgers-play.md create mode 100644 packages/svelte/tests/runtime-runes/samples/nullish-empty-string/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/nullish-empty-string/main.svelte diff --git a/.changeset/blue-badgers-play.md b/.changeset/blue-badgers-play.md new file mode 100644 index 0000000000..e07d393568 --- /dev/null +++ b/.changeset/blue-badgers-play.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: treat nullish expression as empty string 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 bc79b76043..fb3f8df74b 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 @@ -77,7 +77,7 @@ export function build_template_chunk( // If we have a single expression, then pass that in directly to possibly avoid doing // extra work in the template_effect (instead we do the work in set_text). if (evaluated.is_known) { - value = b.literal(evaluated.value); + value = b.literal((evaluated.value ?? '') + ''); } return { value, has_state }; @@ -96,7 +96,7 @@ export function build_template_chunk( } if (evaluated.is_known) { - quasi.value.cooked += evaluated.value + ''; + quasi.value.cooked += (evaluated.value ?? '') + ''; } else { if (!evaluated.is_defined) { // add `?? ''` where necessary diff --git a/packages/svelte/tests/runtime-runes/samples/nullish-empty-string/_config.js b/packages/svelte/tests/runtime-runes/samples/nullish-empty-string/_config.js new file mode 100644 index 0000000000..84e97e9735 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/nullish-empty-string/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + html: '[]' +}); diff --git a/packages/svelte/tests/runtime-runes/samples/nullish-empty-string/main.svelte b/packages/svelte/tests/runtime-runes/samples/nullish-empty-string/main.svelte new file mode 100644 index 0000000000..efe39b91fe --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/nullish-empty-string/main.svelte @@ -0,0 +1 @@ +[{undefined ?? null}] diff --git a/packages/svelte/tests/snapshot/samples/purity/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/purity/_expected/client/index.svelte.js index 5bc9766acf..f661dbc01d 100644 --- a/packages/svelte/tests/snapshot/samples/purity/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/purity/_expected/client/index.svelte.js @@ -8,7 +8,7 @@ export default function Purity($$anchor) { var fragment = root(); var p = $.first_child(fragment); - p.textContent = 0; + p.textContent = '0'; var p_1 = $.sibling(p, 2); From 92cdeadadfa85ac4d70f8649440655e6bb902dbd Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 12 May 2025 18:52:51 -0400 Subject: [PATCH 024/259] chore: ts-ignore Math.f16round (#15902) --- packages/svelte/src/compiler/phases/scope.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 752bdac9d8..75a26d487b 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -28,7 +28,7 @@ const globals = { 'Math.max': [NUMBER, Math.max], 'Math.random': [NUMBER], 'Math.floor': [NUMBER, Math.floor], - // @ts-expect-error + // @ts-ignore 'Math.f16round': [NUMBER, Math.f16round], 'Math.round': [NUMBER, Math.round], 'Math.abs': [NUMBER, Math.abs], From aa041a9e65cffc551f70a1499f51c223ef4c2289 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Mon, 12 May 2025 16:27:55 -0700 Subject: [PATCH 025/259] fix: warn on bidirectional control characters, fix various issues with template expressions (#15893) * fix: warn on bidirectional control characters * check evaluated values as well, fix minor issue * fix failing tests * lint * fix * shrink warning code * use validator test suite rather than snapshot (which should be used sparingly as it creates more git noise) * show ranges during parsing, and warn on all occurrences rather than just the first * fix lint * move check into Text visitor so it happens in expected order * unused * add svelte-ignore test * ignore control characters following a svelte-ignore comment * tweak message * no need to test evaluations, since we are already testing the literals that they are composed of --------- Co-authored-by: Rich Harris --- .changeset/rare-crews-collect.md | 5 ++ .../.generated/compile-warnings.md | 8 +++ .../svelte/messages/compile-warnings/misc.md | 6 +++ .../src/compiler/phases/2-analyze/index.js | 4 ++ .../phases/2-analyze/visitors/Literal.js | 14 ++++++ .../2-analyze/visitors/TemplateElement.js | 12 +++++ .../phases/2-analyze/visitors/Text.js | 38 ++++++++++++-- .../svelte/src/compiler/phases/patterns.js | 2 + packages/svelte/src/compiler/warnings.js | 9 ++++ .../input.svelte | 8 +++ .../warnings.json | 50 +++++++++++++++++++ 11 files changed, 153 insertions(+), 3 deletions(-) create mode 100644 .changeset/rare-crews-collect.md create mode 100644 packages/svelte/src/compiler/phases/2-analyze/visitors/Literal.js create mode 100644 packages/svelte/src/compiler/phases/2-analyze/visitors/TemplateElement.js create mode 100644 packages/svelte/tests/validator/samples/bidirectional-control-characters/input.svelte create mode 100644 packages/svelte/tests/validator/samples/bidirectional-control-characters/warnings.json diff --git a/.changeset/rare-crews-collect.md b/.changeset/rare-crews-collect.md new file mode 100644 index 0000000000..a4ffe09324 --- /dev/null +++ b/.changeset/rare-crews-collect.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: warn on bidirectional control characters diff --git a/documentation/docs/98-reference/.generated/compile-warnings.md b/documentation/docs/98-reference/.generated/compile-warnings.md index 0e94cbadb2..7069f90206 100644 --- a/documentation/docs/98-reference/.generated/compile-warnings.md +++ b/documentation/docs/98-reference/.generated/compile-warnings.md @@ -586,6 +586,14 @@ Attributes should not contain ':' characters to prevent ambiguity with Svelte di Quoted attributes on components and custom elements will be stringified in a future version of Svelte. If this isn't what you want, remove the quotes ``` +### bidirectional_control_characters + +``` +A bidirectional control character was detected in your code. These characters can be used to alter the visual direction of your code and could have unintended consequences +``` + +Bidirectional control characters can alter the direction in which text appears to be in. For example, via control characters, you can make `defabc` look like `abcdef`. As a result, if you were to unknowingly copy and paste some code that has these control characters, they may alter the behavior of your code in ways you did not intend. See [trojansource.codes](https://trojansource.codes/) for more information. + ### bind_invalid_each_rest ``` diff --git a/packages/svelte/messages/compile-warnings/misc.md b/packages/svelte/messages/compile-warnings/misc.md index 3b977db1be..29343dd28a 100644 --- a/packages/svelte/messages/compile-warnings/misc.md +++ b/packages/svelte/messages/compile-warnings/misc.md @@ -1,3 +1,9 @@ +## bidirectional_control_characters + +> A bidirectional control character was detected in your code. These characters can be used to alter the visual direction of your code and could have unintended consequences + +Bidirectional control characters can alter the direction in which text appears to be in. For example, via control characters, you can make `defabc` look like `abcdef`. As a result, if you were to unknowingly copy and paste some code that has these control characters, they may alter the behavior of your code in ways you did not intend. See [trojansource.codes](https://trojansource.codes/) for more information. + ## legacy_code > `%code%` is no longer valid — please use `%suggestion%` instead diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 2e36a89649..766b317d06 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -43,6 +43,7 @@ import { ImportDeclaration } from './visitors/ImportDeclaration.js'; import { KeyBlock } from './visitors/KeyBlock.js'; import { LabeledStatement } from './visitors/LabeledStatement.js'; import { LetDirective } from './visitors/LetDirective.js'; +import { Literal } from './visitors/Literal.js'; import { MemberExpression } from './visitors/MemberExpression.js'; import { NewExpression } from './visitors/NewExpression.js'; import { OnDirective } from './visitors/OnDirective.js'; @@ -63,6 +64,7 @@ import { SvelteSelf } from './visitors/SvelteSelf.js'; import { SvelteWindow } from './visitors/SvelteWindow.js'; import { SvelteBoundary } from './visitors/SvelteBoundary.js'; import { TaggedTemplateExpression } from './visitors/TaggedTemplateExpression.js'; +import { TemplateElement } from './visitors/TemplateElement.js'; import { Text } from './visitors/Text.js'; import { TitleElement } from './visitors/TitleElement.js'; import { TransitionDirective } from './visitors/TransitionDirective.js'; @@ -156,6 +158,7 @@ const visitors = { KeyBlock, LabeledStatement, LetDirective, + Literal, MemberExpression, NewExpression, OnDirective, @@ -176,6 +179,7 @@ const visitors = { SvelteWindow, SvelteBoundary, TaggedTemplateExpression, + TemplateElement, Text, TransitionDirective, TitleElement, diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Literal.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Literal.js new file mode 100644 index 0000000000..58684ba71c --- /dev/null +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Literal.js @@ -0,0 +1,14 @@ +/** @import { Literal } from 'estree' */ +import * as w from '../../../warnings.js'; +import { regex_bidirectional_control_characters } from '../../patterns.js'; + +/** + * @param {Literal} node + */ +export function Literal(node) { + if (typeof node.value === 'string') { + if (regex_bidirectional_control_characters.test(node.value)) { + w.bidirectional_control_characters(node); + } + } +} diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/TemplateElement.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/TemplateElement.js new file mode 100644 index 0000000000..978042bbc5 --- /dev/null +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/TemplateElement.js @@ -0,0 +1,12 @@ +/** @import { TemplateElement } from 'estree' */ +import * as w from '../../../warnings.js'; +import { regex_bidirectional_control_characters } from '../../patterns.js'; + +/** + * @param {TemplateElement} node + */ +export function TemplateElement(node) { + if (regex_bidirectional_control_characters.test(node.value.cooked ?? '')) { + w.bidirectional_control_characters(node); + } +} diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Text.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Text.js index 363a111b7d..a03421e8dd 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Text.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Text.js @@ -1,20 +1,52 @@ /** @import { AST } from '#compiler' */ /** @import { Context } from '../types' */ import { is_tag_valid_with_parent } from '../../../../html-tree-validation.js'; -import { regex_not_whitespace } from '../../patterns.js'; +import { regex_bidirectional_control_characters, regex_not_whitespace } from '../../patterns.js'; import * as e from '../../../errors.js'; +import * as w from '../../../warnings.js'; +import { extract_svelte_ignore } from '../../../utils/extract_svelte_ignore.js'; /** * @param {AST.Text} node * @param {Context} context */ export function Text(node, context) { - const in_template = context.path.at(-1)?.type === 'Fragment'; + const parent = /** @type {AST.SvelteNode} */ (context.path.at(-1)); - if (in_template && context.state.parent_element && regex_not_whitespace.test(node.data)) { + if ( + parent.type === 'Fragment' && + context.state.parent_element && + regex_not_whitespace.test(node.data) + ) { const message = is_tag_valid_with_parent('#text', context.state.parent_element); if (message) { e.node_invalid_placement(node, message); } } + + regex_bidirectional_control_characters.lastIndex = 0; + for (const match of node.data.matchAll(regex_bidirectional_control_characters)) { + let is_ignored = false; + + // if we have a svelte-ignore comment earlier in the text, bail + // (otherwise we can only use svelte-ignore on parent elements/blocks) + if (parent.type === 'Fragment') { + for (const child of parent.nodes) { + if (child === node) break; + + if (child.type === 'Comment') { + is_ignored ||= extract_svelte_ignore( + child.start + 4, + child.data, + context.state.analysis.runes + ).includes('bidirectional_control_characters'); + } + } + } + + if (!is_ignored) { + let start = match.index + node.start; + w.bidirectional_control_characters({ start, end: start + match[0].length }); + } + } } diff --git a/packages/svelte/src/compiler/phases/patterns.js b/packages/svelte/src/compiler/phases/patterns.js index bda299de9e..2bee717131 100644 --- a/packages/svelte/src/compiler/phases/patterns.js +++ b/packages/svelte/src/compiler/phases/patterns.js @@ -21,3 +21,5 @@ export const regex_invalid_identifier_chars = /(^[^a-zA-Z_$]|[^a-zA-Z0-9_$])/g; export const regex_starts_with_vowel = /^[aeiou]/; export const regex_heading_tags = /^h[1-6]$/; export const regex_illegal_attribute_character = /(^[0-9-.])|[\^$@%&#?!|()[\]{}^*+~;]/; +export const regex_bidirectional_control_characters = + /[\u202a\u202b\u202c\u202d\u202e\u2066\u2067\u2068\u2069]+/g; diff --git a/packages/svelte/src/compiler/warnings.js b/packages/svelte/src/compiler/warnings.js index e6fc8caba5..c281433213 100644 --- a/packages/svelte/src/compiler/warnings.js +++ b/packages/svelte/src/compiler/warnings.js @@ -86,6 +86,7 @@ export const codes = [ 'a11y_role_supports_aria_props_implicit', 'a11y_unknown_aria_attribute', 'a11y_unknown_role', + 'bidirectional_control_characters', 'legacy_code', 'unknown_code', 'options_deprecated_accessors', @@ -506,6 +507,14 @@ export function a11y_unknown_role(node, role, suggestion) { w(node, 'a11y_unknown_role', `${suggestion ? `Unknown role '${role}'. Did you mean '${suggestion}'?` : `Unknown role '${role}'`}\nhttps://svelte.dev/e/a11y_unknown_role`); } +/** + * A bidirectional control character was detected in your code. These characters can be used to alter the visual direction of your code and could have unintended consequences + * @param {null | NodeLike} node + */ +export function bidirectional_control_characters(node) { + w(node, 'bidirectional_control_characters', `A bidirectional control character was detected in your code. These characters can be used to alter the visual direction of your code and could have unintended consequences\nhttps://svelte.dev/e/bidirectional_control_characters`); +} + /** * `%code%` is no longer valid — please use `%suggestion%` instead * @param {null | NodeLike} node diff --git a/packages/svelte/tests/validator/samples/bidirectional-control-characters/input.svelte b/packages/svelte/tests/validator/samples/bidirectional-control-characters/input.svelte new file mode 100644 index 0000000000..21587e5f4f --- /dev/null +++ b/packages/svelte/tests/validator/samples/bidirectional-control-characters/input.svelte @@ -0,0 +1,8 @@ + +⁧⁦def⁩⁦abc⁩⁩ +

Hello, {name}!

+ + +⁧⁦def⁩⁦abc⁩⁩ diff --git a/packages/svelte/tests/validator/samples/bidirectional-control-characters/warnings.json b/packages/svelte/tests/validator/samples/bidirectional-control-characters/warnings.json new file mode 100644 index 0000000000..6e70193c6c --- /dev/null +++ b/packages/svelte/tests/validator/samples/bidirectional-control-characters/warnings.json @@ -0,0 +1,50 @@ +[ + { + "code": "bidirectional_control_characters", + "message": "A bidirectional control character was detected in your code. These characters can be used to alter the visual direction of your code and could have unintended consequences", + "start": { + "line": 2, + "column": 15 + }, + "end": { + "line": 2, + "column": 58 + } + }, + { + "code": "bidirectional_control_characters", + "message": "A bidirectional control character was detected in your code. These characters can be used to alter the visual direction of your code and could have unintended consequences", + "start": { + "line": 4, + "column": 0 + }, + "end": { + "line": 4, + "column": 2 + } + }, + { + "code": "bidirectional_control_characters", + "message": "A bidirectional control character was detected in your code. These characters can be used to alter the visual direction of your code and could have unintended consequences", + "start": { + "line": 4, + "column": 5 + }, + "end": { + "line": 4, + "column": 7 + } + }, + { + "code": "bidirectional_control_characters", + "message": "A bidirectional control character was detected in your code. These characters can be used to alter the visual direction of your code and could have unintended consequences", + "start": { + "line": 4, + "column": 10 + }, + "end": { + "line": 4, + "column": 12 + } + } +] From f2796dda0e4dc1e3677bc87cf434ac8c2163c4c1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 12 May 2025 19:51:16 -0400 Subject: [PATCH 026/259] Version Packages (#15899) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/blue-badgers-play.md | 5 ----- .changeset/little-worms-wonder.md | 5 ----- .changeset/rare-crews-collect.md | 5 ----- .changeset/tall-cherries-fix.md | 5 ----- packages/svelte/CHANGELOG.md | 12 ++++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 7 files changed, 14 insertions(+), 22 deletions(-) delete mode 100644 .changeset/blue-badgers-play.md delete mode 100644 .changeset/little-worms-wonder.md delete mode 100644 .changeset/rare-crews-collect.md delete mode 100644 .changeset/tall-cherries-fix.md diff --git a/.changeset/blue-badgers-play.md b/.changeset/blue-badgers-play.md deleted file mode 100644 index e07d393568..0000000000 --- a/.changeset/blue-badgers-play.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: treat nullish expression as empty string diff --git a/.changeset/little-worms-wonder.md b/.changeset/little-worms-wonder.md deleted file mode 100644 index 5482d7d5ce..0000000000 --- a/.changeset/little-worms-wonder.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: prevent invalid BigInt calls from blowing up at compile time diff --git a/.changeset/rare-crews-collect.md b/.changeset/rare-crews-collect.md deleted file mode 100644 index a4ffe09324..0000000000 --- a/.changeset/rare-crews-collect.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: warn on bidirectional control characters diff --git a/.changeset/tall-cherries-fix.md b/.changeset/tall-cherries-fix.md deleted file mode 100644 index df25e02da9..0000000000 --- a/.changeset/tall-cherries-fix.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: emit right error for a shadowed invalid rune diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 8650ba0e5b..95378823f5 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,17 @@ # svelte +## 5.28.4 + +### Patch Changes + +- fix: treat nullish expression as empty string ([#15901](https://github.com/sveltejs/svelte/pull/15901)) + +- fix: prevent invalid BigInt calls from blowing up at compile time ([#15900](https://github.com/sveltejs/svelte/pull/15900)) + +- fix: warn on bidirectional control characters ([#15893](https://github.com/sveltejs/svelte/pull/15893)) + +- fix: emit right error for a shadowed invalid rune ([#15892](https://github.com/sveltejs/svelte/pull/15892)) + ## 5.28.3 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 85eef9c2eb..92d44ec155 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.28.3", + "version": "5.28.4", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index b072c6432c..d933675d36 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.28.3'; +export const VERSION = '5.28.4'; export const PUBLIC_VERSION = '5'; From 326b32932cf81ba01005b2757268321460f477cb Mon Sep 17 00:00:00 2001 From: 7nik Date: Tue, 13 May 2025 03:04:48 +0300 Subject: [PATCH 027/259] fix: more frequently update `bind:buffered` to actual value (#15874) * fix: more frequently update `bind:buffered` to actual value * small tweak --------- Co-authored-by: 7nik Co-authored-by: Rich Harris --- .changeset/short-impalas-exist.md | 5 +++++ .../client/dom/elements/bindings/media.js | 18 +++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 .changeset/short-impalas-exist.md diff --git a/.changeset/short-impalas-exist.md b/.changeset/short-impalas-exist.md new file mode 100644 index 0000000000..a1f9120c8f --- /dev/null +++ b/.changeset/short-impalas-exist.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: more frequently update `bind:buffered` to actual value diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/media.js b/packages/svelte/src/internal/client/dom/elements/bindings/media.js index 30a8dac1af..f96ee05989 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/media.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/media.js @@ -62,7 +62,23 @@ export function bind_current_time(media, get, set = get) { * @param {(array: Array<{ start: number; end: number }>) => void} set */ export function bind_buffered(media, set) { - listen(media, ['loadedmetadata', 'progress'], () => set(time_ranges_to_array(media.buffered))); + /** @type {{ start: number; end: number; }[]} */ + var current; + + // `buffered` can update without emitting any event, so we check it on various events. + // By specs, `buffered` always returns a new object, so we have to compare deeply. + listen(media, ['loadedmetadata', 'progress', 'timeupdate', 'seeking'], () => { + var ranges = media.buffered; + + if ( + !current || + current.length !== ranges.length || + current.some((range, i) => ranges.start(i) !== range.start || ranges.end(i) !== range.end) + ) { + current = time_ranges_to_array(ranges); + set(current); + } + }); } /** From 3bb1af9677ad145a5a048311090b3bada54aef12 Mon Sep 17 00:00:00 2001 From: 7nik Date: Tue, 13 May 2025 03:05:15 +0300 Subject: [PATCH 028/259] fix: proxify the value in assignment shorthands to the private field (#15862) Co-authored-by: 7nik --- .changeset/shaggy-toys-arrive.md | 5 ++++ packages/svelte/src/compiler/utils/ast.js | 4 ++- .../_config.js | 20 +++++++++++++ .../main.svelte | 28 +++++++++++++++++++ 4 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 .changeset/shaggy-toys-arrive.md create mode 100644 packages/svelte/tests/runtime-runes/samples/class-private-fields-assignment-shorthand/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/class-private-fields-assignment-shorthand/main.svelte diff --git a/.changeset/shaggy-toys-arrive.md b/.changeset/shaggy-toys-arrive.md new file mode 100644 index 0000000000..440fa042c9 --- /dev/null +++ b/.changeset/shaggy-toys-arrive.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: proxify the value in assignment shorthands to the private field diff --git a/packages/svelte/src/compiler/utils/ast.js b/packages/svelte/src/compiler/utils/ast.js index 108f4eff64..23a95a1026 100644 --- a/packages/svelte/src/compiler/utils/ast.js +++ b/packages/svelte/src/compiler/utils/ast.js @@ -580,5 +580,7 @@ export function build_assignment_value(operator, left, right) { return operator === '=' ? right : // turn something like x += 1 into x = x + 1 - b.binary(/** @type {ESTree.BinaryOperator} */ (operator.slice(0, -1)), left, right); + ['||=', '&&=', '??='].includes(operator) + ? b.logical(/** @type {ESTree.LogicalOperator} */ (operator.slice(0, -1)), left, right) + : b.binary(/** @type {ESTree.BinaryOperator} */ (operator.slice(0, -1)), left, right); } diff --git a/packages/svelte/tests/runtime-runes/samples/class-private-fields-assignment-shorthand/_config.js b/packages/svelte/tests/runtime-runes/samples/class-private-fields-assignment-shorthand/_config.js new file mode 100644 index 0000000000..0fdeabfe0b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-private-fields-assignment-shorthand/_config.js @@ -0,0 +1,20 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const btn = target.querySelector('button'); + + btn?.click(); + flushSync(); + assert.htmlEqual( + target.innerHTML, + ` + +

a:1

+

b:2

+

c:3

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/class-private-fields-assignment-shorthand/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-private-fields-assignment-shorthand/main.svelte new file mode 100644 index 0000000000..746f22b1e6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-private-fields-assignment-shorthand/main.svelte @@ -0,0 +1,28 @@ + + + + +{#key 1}

a:{counter.a}

{/key} +{#key 2}

b:{counter.b}

{/key} +{#key 3}

c:{counter.c}

{/key} From 77bdf249ffcd6b08765d3bf05da577d1223f0366 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 12 May 2025 20:29:01 -0400 Subject: [PATCH 029/259] Version Packages (#15903) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/shaggy-toys-arrive.md | 5 ----- .changeset/short-impalas-exist.md | 5 ----- packages/svelte/CHANGELOG.md | 8 ++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 5 files changed, 10 insertions(+), 12 deletions(-) delete mode 100644 .changeset/shaggy-toys-arrive.md delete mode 100644 .changeset/short-impalas-exist.md diff --git a/.changeset/shaggy-toys-arrive.md b/.changeset/shaggy-toys-arrive.md deleted file mode 100644 index 440fa042c9..0000000000 --- a/.changeset/shaggy-toys-arrive.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: proxify the value in assignment shorthands to the private field diff --git a/.changeset/short-impalas-exist.md b/.changeset/short-impalas-exist.md deleted file mode 100644 index a1f9120c8f..0000000000 --- a/.changeset/short-impalas-exist.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: more frequently update `bind:buffered` to actual value diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 95378823f5..4ea42a3da2 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,13 @@ # svelte +## 5.28.5 + +### Patch Changes + +- fix: proxify the value in assignment shorthands to the private field ([#15862](https://github.com/sveltejs/svelte/pull/15862)) + +- fix: more frequently update `bind:buffered` to actual value ([#15874](https://github.com/sveltejs/svelte/pull/15874)) + ## 5.28.4 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 92d44ec155..c3255e30cd 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.28.4", + "version": "5.28.5", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index d933675d36..3ba616e962 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.28.4'; +export const VERSION = '5.28.5'; export const PUBLIC_VERSION = '5'; From 2bc2ae0dddab74eecff606790e1ef01ea281d6ea Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Tue, 13 May 2025 02:57:23 +0200 Subject: [PATCH 030/259] fix: use `transform.read` for `ownership_validator.mutation` array (#15848) --- .changeset/gorgeous-experts-perform.md | 5 +++++ .../phases/3-transform/client/visitors/shared/utils.js | 7 +++++-- .../_config.js | 10 ++++++++++ .../main.svelte | 5 +++++ 4 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 .changeset/gorgeous-experts-perform.md create mode 100644 packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-use-transform/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-use-transform/main.svelte diff --git a/.changeset/gorgeous-experts-perform.md b/.changeset/gorgeous-experts-perform.md new file mode 100644 index 0000000000..9c4673a341 --- /dev/null +++ b/.changeset/gorgeous-experts-perform.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: use `transform.read` for `ownership_validator.mutation` array 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 fb3f8df74b..80b472ac37 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 @@ -324,15 +324,18 @@ export function validate_mutation(node, context, expression) { const state = /** @type {ComponentClientTransformState} */ (context.state); state.analysis.needs_mutation_validation = true; - /** @type {Array} */ + /** @type {Array} */ const path = []; while (left.type === 'MemberExpression') { if (left.property.type === 'Literal') { path.unshift(left.property); } else if (left.property.type === 'Identifier') { + const transform = Object.hasOwn(context.state.transform, left.property.name) + ? context.state.transform[left.property.name] + : null; if (left.computed) { - path.unshift(left.property); + path.unshift(transform?.read ? transform.read(left.property) : left.property); } else { path.unshift(b.literal(left.property.name)); } diff --git a/packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-use-transform/_config.js b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-use-transform/_config.js new file mode 100644 index 0000000000..95556f4737 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-use-transform/_config.js @@ -0,0 +1,10 @@ +import { test } from '../../test'; + +export default test({ + compileOptions: { + dev: true + }, + async test({ assert, errors }) { + assert.deepEqual(errors, []); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-use-transform/main.svelte b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-use-transform/main.svelte new file mode 100644 index 0000000000..d834551221 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-use-transform/main.svelte @@ -0,0 +1,5 @@ + + From 944ef1dbcecf6277e42d3da9ca60dcbe2929aa38 Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Tue, 13 May 2025 03:00:11 +0200 Subject: [PATCH 031/259] fix: don't spread away `$$slots` from `$props` is it's used (#15849) * fix: don't spread away `$$slots` from `$props` is it's used * beef up test * fix * Update .changeset/ten-plants-carry.md --------- Co-authored-by: Rich Harris --- .changeset/ten-plants-carry.md | 5 +++++ .../server/visitors/VariableDeclaration.js | 11 +++++++++-- .../samples/props-and-slots/Child.svelte | 9 +++++++++ .../runtime-runes/samples/props-and-slots/_config.js | 8 ++++++++ .../runtime-runes/samples/props-and-slots/main.svelte | 7 +++++++ 5 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 .changeset/ten-plants-carry.md create mode 100644 packages/svelte/tests/runtime-runes/samples/props-and-slots/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/props-and-slots/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/props-and-slots/main.svelte diff --git a/.changeset/ten-plants-carry.md b/.changeset/ten-plants-carry.md new file mode 100644 index 0000000000..91ae5889cf --- /dev/null +++ b/.changeset/ten-plants-carry.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't redeclare `$$slots` diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js index 1f2bd3e2b1..b76455b5c1 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js @@ -1,6 +1,7 @@ /** @import { VariableDeclaration, VariableDeclarator, Expression, CallExpression, Pattern, Identifier } from 'estree' */ /** @import { Binding } from '#compiler' */ /** @import { Context } from '../types.js' */ +/** @import { ComponentAnalysis } from '../../../types.js' */ /** @import { Scope } from '../../../scope.js' */ import { build_fallback, extract_paths } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; @@ -50,20 +51,26 @@ export function VariableDeclaration(node, context) { } } }); + + // if `$$slots` is declared separately, deconflict + const slots_name = /** @type {ComponentAnalysis} */ (context.state.analysis).uses_slots + ? b.id('$$slots_') + : b.id('$$slots'); + if (id.type === 'ObjectPattern' && has_rest) { // If a rest pattern is used within an object pattern, we need to ensure we don't expose $$slots or $$events id.properties.splice( id.properties.length - 1, 0, // @ts-ignore - b.prop('init', b.id('$$slots'), b.id('$$slots')), + b.prop('init', b.id('$$slots'), slots_name), b.prop('init', b.id('$$events'), b.id('$$events')) ); } else if (id.type === 'Identifier') { // If $props is referenced as an identifier, we need to ensure we don't expose $$slots or $$events as properties // on the identifier reference id = b.object_pattern([ - b.prop('init', b.id('$$slots'), b.id('$$slots')), + b.prop('init', b.id('$$slots'), slots_name), b.prop('init', b.id('$$events'), b.id('$$events')), b.rest(b.id(id.name)) ]); diff --git a/packages/svelte/tests/runtime-runes/samples/props-and-slots/Child.svelte b/packages/svelte/tests/runtime-runes/samples/props-and-slots/Child.svelte new file mode 100644 index 0000000000..a2e7d6d8a4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-and-slots/Child.svelte @@ -0,0 +1,9 @@ + + +

{Object.keys(props)}

+ +{#if $$slots.foo} +

foo exists

+{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/props-and-slots/_config.js b/packages/svelte/tests/runtime-runes/samples/props-and-slots/_config.js new file mode 100644 index 0000000000..3f6fb4143c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-and-slots/_config.js @@ -0,0 +1,8 @@ +import { test } from '../../test'; + +export default test({ + html: ` +

a

+

foo exists

+ ` +}); diff --git a/packages/svelte/tests/runtime-runes/samples/props-and-slots/main.svelte b/packages/svelte/tests/runtime-runes/samples/props-and-slots/main.svelte new file mode 100644 index 0000000000..3535da7132 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-and-slots/main.svelte @@ -0,0 +1,7 @@ + + + +
foo
+
From fbb1aecc2d617c6f0cf47240ac457b50baf490ef Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 12 May 2025 21:23:34 -0400 Subject: [PATCH 032/259] Version Packages (#15904) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/gorgeous-experts-perform.md | 5 ----- .changeset/ten-plants-carry.md | 5 ----- packages/svelte/CHANGELOG.md | 8 ++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 5 files changed, 10 insertions(+), 12 deletions(-) delete mode 100644 .changeset/gorgeous-experts-perform.md delete mode 100644 .changeset/ten-plants-carry.md diff --git a/.changeset/gorgeous-experts-perform.md b/.changeset/gorgeous-experts-perform.md deleted file mode 100644 index 9c4673a341..0000000000 --- a/.changeset/gorgeous-experts-perform.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: use `transform.read` for `ownership_validator.mutation` array diff --git a/.changeset/ten-plants-carry.md b/.changeset/ten-plants-carry.md deleted file mode 100644 index 91ae5889cf..0000000000 --- a/.changeset/ten-plants-carry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: don't redeclare `$$slots` diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 4ea42a3da2..8df324d649 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,13 @@ # svelte +## 5.28.6 + +### Patch Changes + +- fix: use `transform.read` for `ownership_validator.mutation` array ([#15848](https://github.com/sveltejs/svelte/pull/15848)) + +- fix: don't redeclare `$slots` ([#15849](https://github.com/sveltejs/svelte/pull/15849)) + ## 5.28.5 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index c3255e30cd..dadba9d50c 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.28.5", + "version": "5.28.6", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 3ba616e962..a77849804c 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.28.5'; +export const VERSION = '5.28.6'; export const PUBLIC_VERSION = '5'; From d4fb9f4b100683cafc1f8d558c059c8691aab250 Mon Sep 17 00:00:00 2001 From: Ryan Atkinson Date: Mon, 12 May 2025 20:00:13 -0600 Subject: [PATCH 033/259] fix: remove unncessary guards that require CSP privilege when removing event attributes (#15846) * fix: remove unncessary guards that cause CSP violations when removing event attributes * add changeset --- .changeset/hot-seals-hang.md | 5 +++++ .../svelte/src/internal/client/dom/elements/events.js | 8 ++------ 2 files changed, 7 insertions(+), 6 deletions(-) create mode 100644 .changeset/hot-seals-hang.md diff --git a/.changeset/hot-seals-hang.md b/.changeset/hot-seals-hang.md new file mode 100644 index 0000000000..184b89d506 --- /dev/null +++ b/.changeset/hot-seals-hang.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: remove unncessary guards that require CSP privilege when removing event attributes diff --git a/packages/svelte/src/internal/client/dom/elements/events.js b/packages/svelte/src/internal/client/dom/elements/events.js index 3374fe713f..c2b7fc7d83 100644 --- a/packages/svelte/src/internal/client/dom/elements/events.js +++ b/packages/svelte/src/internal/client/dom/elements/events.js @@ -26,12 +26,8 @@ export const root_event_handles = new Set(); export function replay_events(dom) { if (!hydrating) return; - if (dom.onload) { - dom.removeAttribute('onload'); - } - if (dom.onerror) { - dom.removeAttribute('onerror'); - } + dom.removeAttribute('onload'); + dom.removeAttribute('onerror'); // @ts-expect-error const event = dom.__e; if (event !== undefined) { From 8866851e2c3254d650732ee04c9cceb33f5a552c Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Mon, 12 May 2025 19:42:13 -0700 Subject: [PATCH 034/259] docs: add links to new CLI add-on documentation (#15835) Co-authored-by: Rich Harris --- documentation/docs/07-misc/02-testing.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/documentation/docs/07-misc/02-testing.md b/documentation/docs/07-misc/02-testing.md index 0842019039..64bf49d77a 100644 --- a/documentation/docs/07-misc/02-testing.md +++ b/documentation/docs/07-misc/02-testing.md @@ -6,9 +6,9 @@ Testing helps you write and maintain your code and guard against regressions. Te ## Unit and integration testing using Vitest -Unit tests allow you to test small isolated parts of your code. Integration tests allow you to test parts of your application to see if they work together. If you're using Vite (including via SvelteKit), we recommend using [Vitest](https://vitest.dev/). +Unit tests allow you to test small isolated parts of your code. Integration tests allow you to test parts of your application to see if they work together. If you're using Vite (including via SvelteKit), we recommend using [Vitest](https://vitest.dev/). You can use the Svelte CLI to [setup Vitest](/docs/cli/vitest) either during project creation or later on. -To get started, install Vitest: +To setup Vitest manually, first install it: ```bash npm install -D vitest @@ -254,9 +254,9 @@ When writing component tests that involve two-way bindings, context or snippet p E2E (short for 'end to end') tests allow you to test your full application through the eyes of the user. This section uses [Playwright](https://playwright.dev/) as an example, but you can also use other solutions like [Cypress](https://www.cypress.io/) or [NightwatchJS](https://nightwatchjs.org/). -To get started with Playwright, either install it via [the VS Code extension](https://playwright.dev/docs/getting-started-vscode), or install it from the command line using `npm init playwright`. It is also part of the setup CLI when you run `npx sv create`. +You can use the Svelte CLI to [setup Playwright](/docs/cli/playwright) either during project creation or later on. You can also [set it up with `npm init playwright`](https://playwright.dev/docs/intro). Additionally, you may also want to install an IDE plugin such as [the VS Code extension](https://playwright.dev/docs/getting-started-vscode) to be able to execute tests from inside your IDE. -After you've done that, you should have a `tests` folder and a Playwright config. You may need to adjust that config to tell Playwright what to do before running the tests - mainly starting your application at a certain port: +If you've run `npm init playwright` or are not using Vite, you may need to adjust the Playwright config to tell Playwright what to do before running the tests - mainly starting your application at a certain port. For example: ```js /// file: playwright.config.js From b6fe1cca3ce6170ce92c6e09d78c43f6be17f9fb Mon Sep 17 00:00:00 2001 From: Tristan <62366897+TitanCmd@users.noreply.github.com> Date: Tue, 13 May 2025 22:51:57 +1000 Subject: [PATCH 035/259] docs: remove dead link in 07-misc/99-faq.md (#15906) remove dead link from the docs in favor of pointing to our own docs --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- documentation/docs/07-misc/99-faq.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/documentation/docs/07-misc/99-faq.md b/documentation/docs/07-misc/99-faq.md index ed5c6277c0..cf98cdd3c3 100644 --- a/documentation/docs/07-misc/99-faq.md +++ b/documentation/docs/07-misc/99-faq.md @@ -81,9 +81,10 @@ _End-to-End Tests_: To ensure your users are able to interact with your applicat Some resources for getting started with testing: +- [Svelte docs on testing](/docs/svelte/testing) +- [Setup Vitest using the Svelte CLI](/docs/cli/vitest) - [Svelte Testing Library](https://testing-library.com/docs/svelte-testing-library/example/) - [Svelte Component Testing in Cypress](https://docs.cypress.io/guides/component-testing/svelte/overview) -- [Example using vitest](https://github.com/vitest-dev/vitest/tree/main/examples/sveltekit) - [Example using uvu test runner with JSDOM](https://github.com/lukeed/uvu/tree/master/examples/svelte) - [Test Svelte components using Vitest & Playwright](https://davipon.hashnode.dev/test-svelte-component-using-vitest-playwright) - [Component testing with WebdriverIO](https://webdriver.io/docs/component-testing/svelte) From 8fc8bc79d1bd0b1c44b40135faf4211a5e648e5d Mon Sep 17 00:00:00 2001 From: 7nik Date: Tue, 13 May 2025 17:16:37 +0300 Subject: [PATCH 036/259] docs: add missing bindings (#15834) * add missing bindings docs * tweak * tweak --------- Co-authored-by: 7nik Co-authored-by: Rich Harris --- .../docs/03-template-syntax/11-bind.md | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/documentation/docs/03-template-syntax/11-bind.md b/documentation/docs/03-template-syntax/11-bind.md index c23f3b5232..0970ee384e 100644 --- a/documentation/docs/03-template-syntax/11-bind.md +++ b/documentation/docs/03-template-syntax/11-bind.md @@ -117,6 +117,29 @@ Since 5.6.0, if an `` has a `defaultChecked` attribute and is part of a f ``` +## `` + +Checkboxes can be in an [indeterminate](https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/indeterminate) state, independently of whether they are checked or unchecked: + +```svelte + + +
+ + + {#if indeterminate} + waiting... + {:else if checked} + checked + {:else} + unchecked + {/if} +
+``` + ## `` Inputs that work together can use `bind:group`. @@ -227,6 +250,7 @@ You can give the `` a default value by adding a `selected` attribute to ``` +## `window` and `document` + +To bind to properties of `window` and `document`, see [``](svelte-window) and [``](svelte-document). + ## Contenteditable bindings Elements with the `contenteditable` attribute support the following bindings: @@ -278,6 +306,10 @@ All visible elements have the following readonly bindings, measured with a `Resi - [`clientHeight`](https://developer.mozilla.org/en-US/docs/Web/API/Element/clientHeight) - [`offsetWidth`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetWidth) - [`offsetHeight`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetHeight) +- [`contentRect`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry/contentRect) +- [`contentBoxSize`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry/contentBoxSize) +- [`borderBoxSize`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry/borderBoxSize) +- [`devicePixelContentBoxSize`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry/devicePixelContentBoxSize) ```svelte
@@ -285,7 +317,7 @@ All visible elements have the following readonly bindings, measured with a `Resi
``` -> [!NOTE] `display: inline` elements do not have a width or height (except for elements with 'intrinsic' dimensions, like `` and ``), and cannot be observed with a `ResizeObserver`. You will need to change the `display` style of these elements to something else, such as `inline-block`. +> [!NOTE] `display: inline` elements do not have a width or height (except for elements with 'intrinsic' dimensions, like `` and ``), and cannot be observed with a `ResizeObserver`. You will need to change the `display` style of these elements to something else, such as `inline-block`. Note that CSS transformations do not trigger `ResizeObserver` callbacks. ## bind:this From 7826ebaccceeee5edda77b5d900d2c204590c6ff Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Tue, 13 May 2025 09:06:12 -0700 Subject: [PATCH 037/259] fix: rewrite destructuring logic to handle iterators (#15813) * fix: wrap array destructuring in spread to avoid iterator edge case * spread at `tmp` declaration * completely rewrite destructuring handling * only wrap in iife if necessary * oops * minor style tweaks * separate visitors * tweak --------- Co-authored-by: Rich Harris --- .changeset/serious-moles-yell.md | 5 ++ .../client/visitors/VariableDeclaration.js | 69 ++++++++++--------- .../server/visitors/VariableDeclaration.js | 13 ++-- .../phases/3-transform/shared/assignments.js | 67 ++++++++++-------- packages/svelte/src/compiler/phases/scope.js | 5 +- packages/svelte/src/compiler/utils/ast.js | 46 ++++++++++++- .../_expected/client/index.svelte.js | 10 +-- 7 files changed, 141 insertions(+), 74 deletions(-) create mode 100644 .changeset/serious-moles-yell.md diff --git a/.changeset/serious-moles-yell.md b/.changeset/serious-moles-yell.md new file mode 100644 index 0000000000..08f33dc059 --- /dev/null +++ b/.changeset/serious-moles-yell.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: rewrite destructuring logic to handle iterators diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js index 84044e4ded..84c205d163 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js @@ -2,7 +2,7 @@ /** @import { Binding } from '#compiler' */ /** @import { ComponentClientTransformState, ComponentContext } from '../types' */ import { dev } from '../../../../state.js'; -import { extract_paths } from '../../../../utils/ast.js'; +import { build_pattern, extract_paths } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; import * as assert from '../../../../utils/assert.js'; import { get_rune } from '../../../scope.js'; @@ -141,20 +141,20 @@ export function VariableDeclaration(node, context) { b.declarator(declarator.id, create_state_declarator(declarator.id, value)) ); } else { - const tmp = context.state.scope.generate('tmp'); - const paths = extract_paths(declarator.id); + const [pattern, replacements] = build_pattern(declarator.id, context.state.scope); declarations.push( - b.declarator(b.id(tmp), value), - ...paths.map((path) => { - const value = path.expression?.(b.id(tmp)); - const binding = context.state.scope.get(/** @type {Identifier} */ (path.node).name); - return b.declarator( - path.node, - binding?.kind === 'state' || binding?.kind === 'raw_state' - ? create_state_declarator(binding.node, value) - : value - ); - }) + b.declarator(pattern, value), + .../** @type {[Identifier, Identifier][]} */ ([...replacements]).map( + ([original, replacement]) => { + const binding = context.state.scope.get(original.name); + return b.declarator( + original, + binding?.kind === 'state' || binding?.kind === 'raw_state' + ? create_state_declarator(binding.node, replacement) + : replacement + ); + } + ) ); } @@ -170,8 +170,7 @@ export function VariableDeclaration(node, context) { ) ); } else { - const bindings = extract_paths(declarator.id); - + const [pattern, replacements] = build_pattern(declarator.id, context.state.scope); const init = /** @type {CallExpression} */ (declarator.init); /** @type {Identifier} */ @@ -189,10 +188,16 @@ export function VariableDeclaration(node, context) { ); } - for (let i = 0; i < bindings.length; i++) { - const binding = bindings[i]; + for (let i = 0; i < replacements.size; i++) { + const [original, replacement] = [...replacements][i]; declarations.push( - b.declarator(binding.node, b.call('$.derived', b.thunk(binding.expression(rhs)))) + b.declarator( + original, + b.call( + '$.derived', + b.arrow([], b.block([b.let(pattern, rhs), b.return(replacement)])) + ) + ) ); } } @@ -304,19 +309,19 @@ function create_state_declarators(declarator, { scope, analysis }, value) { ]; } - const tmp = scope.generate('tmp'); - const paths = extract_paths(declarator.id); + const [pattern, replacements] = build_pattern(declarator.id, scope); return [ - b.declarator(b.id(tmp), value), - ...paths.map((path) => { - const value = path.expression?.(b.id(tmp)); - const binding = scope.get(/** @type {Identifier} */ (path.node).name); - return b.declarator( - path.node, - binding?.kind === 'state' - ? b.call('$.mutable_source', value, analysis.immutable ? b.true : undefined) - : value - ); - }) + b.declarator(pattern, value), + .../** @type {[Identifier, Identifier][]} */ ([...replacements]).map( + ([original, replacement]) => { + const binding = scope.get(original.name); + return b.declarator( + original, + binding?.kind === 'state' + ? b.call('$.mutable_source', replacement, analysis.immutable ? b.true : undefined) + : replacement + ); + } + ) ]; } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js index b76455b5c1..17bf516a22 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js @@ -3,7 +3,7 @@ /** @import { Context } from '../types.js' */ /** @import { ComponentAnalysis } from '../../../types.js' */ /** @import { Scope } from '../../../scope.js' */ -import { build_fallback, extract_paths } from '../../../../utils/ast.js'; +import { build_pattern, build_fallback, extract_paths } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; import { get_rune } from '../../../scope.js'; import { walk } from 'zimmerframe'; @@ -188,13 +188,10 @@ function create_state_declarators(declarator, scope, value) { return [b.declarator(declarator.id, value)]; } - const tmp = scope.generate('tmp'); - const paths = extract_paths(declarator.id); + const [pattern, replacements] = build_pattern(declarator.id, scope); return [ - b.declarator(b.id(tmp), value), // TODO inject declarator for opts, so we can use it below - ...paths.map((path) => { - const value = path.expression?.(b.id(tmp)); - return b.declarator(path.node, value); - }) + b.declarator(pattern, value), + // TODO inject declarator for opts, so we can use it below + ...[...replacements].map(([original, replacement]) => b.declarator(original, replacement)) ]; } diff --git a/packages/svelte/src/compiler/phases/3-transform/shared/assignments.js b/packages/svelte/src/compiler/phases/3-transform/shared/assignments.js index 3e6bb0c4c6..85b0869a15 100644 --- a/packages/svelte/src/compiler/phases/3-transform/shared/assignments.js +++ b/packages/svelte/src/compiler/phases/3-transform/shared/assignments.js @@ -1,7 +1,7 @@ -/** @import { AssignmentExpression, AssignmentOperator, Expression, Node, Pattern } from 'estree' */ +/** @import { AssignmentExpression, AssignmentOperator, Expression, Identifier, Node, Pattern } from 'estree' */ /** @import { Context as ClientContext } from '../client/types.js' */ /** @import { Context as ServerContext } from '../server/types.js' */ -import { extract_paths, is_expression_async } from '../../../utils/ast.js'; +import { build_pattern, is_expression_async } from '../../../utils/ast.js'; import * as b from '#compiler/builders'; /** @@ -23,21 +23,23 @@ export function visit_assignment_expression(node, context, build_assignment) { let changed = false; - const assignments = extract_paths(node.left).map((path) => { - const value = path.expression?.(rhs); + const [pattern, replacements] = build_pattern(node.left, context.state.scope); - let assignment = build_assignment('=', path.node, value, context); - if (assignment !== null) changed = true; - - return ( - assignment ?? - b.assignment( - '=', - /** @type {Pattern} */ (context.visit(path.node)), - /** @type {Expression} */ (context.visit(value)) - ) - ); - }); + const assignments = [ + b.let(pattern, rhs), + ...[...replacements].map(([original, replacement]) => { + let assignment = build_assignment(node.operator, original, replacement, context); + if (assignment !== null) changed = true; + return b.stmt( + assignment ?? + b.assignment( + node.operator, + /** @type {Identifier} */ (context.visit(original)), + /** @type {Expression} */ (context.visit(replacement)) + ) + ); + }) + ]; if (!changed) { // No change to output -> nothing to transform -> we can keep the original assignment @@ -45,25 +47,36 @@ export function visit_assignment_expression(node, context, build_assignment) { } const is_standalone = /** @type {Node} */ (context.path.at(-1)).type.endsWith('Statement'); - const sequence = b.sequence(assignments); + const block = b.block(assignments); if (!is_standalone) { // this is part of an expression, we need the sequence to end with the value - sequence.expressions.push(rhs); + block.body.push(b.return(rhs)); } - if (should_cache) { - // the right hand side is a complex expression, wrap in an IIFE to cache it - const iife = b.arrow([rhs], sequence); + if (is_standalone && !should_cache) { + return block; + } - const iife_is_async = - is_expression_async(value) || - assignments.some((assignment) => is_expression_async(assignment)); + const iife = b.arrow(should_cache ? [rhs] : [], block); - return iife_is_async ? b.await(b.call(b.async(iife), value)) : b.call(iife, value); - } + const iife_is_async = + is_expression_async(value) || + assignments.some( + (assignment) => + (assignment.type === 'ExpressionStatement' && + is_expression_async(assignment.expression)) || + (assignment.type === 'VariableDeclaration' && + assignment.declarations.some( + (declaration) => + is_expression_async(declaration.id) || + (declaration.init && is_expression_async(declaration.init)) + )) + ); - return sequence; + return iife_is_async + ? b.await(b.call(b.async(iife), should_cache ? value : undefined)) + : b.call(iife, should_cache ? value : undefined); } if (node.left.type !== 'Identifier' && node.left.type !== 'MemberExpression') { diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 75a26d487b..f37dfab8d1 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -630,9 +630,10 @@ export class Scope { /** * @param {string} preferred_name + * @param {(name: string, counter: number) => string} [generator] * @returns {string} */ - generate(preferred_name) { + generate(preferred_name, generator = (name, counter) => `${name}_${counter}`) { if (this.#porous) { return /** @type {Scope} */ (this.parent).generate(preferred_name); } @@ -647,7 +648,7 @@ export class Scope { this.root.conflicts.has(name) || is_reserved(name) ) { - name = `${preferred_name}_${n++}`; + name = generator(preferred_name, n++); } this.references.set(name, []); diff --git a/packages/svelte/src/compiler/utils/ast.js b/packages/svelte/src/compiler/utils/ast.js index 23a95a1026..32ff5a37b3 100644 --- a/packages/svelte/src/compiler/utils/ast.js +++ b/packages/svelte/src/compiler/utils/ast.js @@ -1,7 +1,8 @@ -/** @import { AST } from '#compiler' */ +/** @import { AST, Scope } from '#compiler' */ /** @import * as ESTree from 'estree' */ import { walk } from 'zimmerframe'; import * as b from '#compiler/builders'; +import is_reference from 'is-reference'; /** * Gets the left-most identifier of a member expression or identifier. @@ -128,6 +129,49 @@ export function unwrap_pattern(pattern, nodes = []) { return nodes; } +/** + * @param {ESTree.Pattern} id + * @param {Scope} scope + * @returns {[ESTree.Pattern, Map]} + */ +export function build_pattern(id, scope) { + /** @type {Map} */ + const map = new Map(); + + /** @type {Map} */ + const names = new Map(); + + let counter = 0; + + for (const node of unwrap_pattern(id)) { + const name = scope.generate(`$$${++counter}`, (_, counter) => `$$${counter}`); + + map.set(node, b.id(name)); + + if (node.type === 'Identifier') { + names.set(node.name, name); + } + } + + const pattern = walk(id, null, { + Identifier(node, context) { + if (is_reference(node, /** @type {ESTree.Pattern} */ (context.path.at(-1)))) { + const name = names.get(node.name); + if (name) return b.id(name); + } + }, + + MemberExpression(node, context) { + const n = map.get(node); + if (n) return n; + + context.next(); + } + }); + + return [pattern, map]; +} + /** * Extracts all identifiers from a pattern. * @param {ESTree.Pattern} pattern diff --git a/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/client/index.svelte.js index 47f297bce9..b2ef29ccaf 100644 --- a/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/client/index.svelte.js @@ -7,10 +7,12 @@ let c = 3; let d = 4; export function update(array) { - ( - $.set(a, array[0], true), - $.set(b, array[1], true) - ); + { + let [$$1, $$2] = array; + + $.set(a, $$1, true); + $.set(b, $$2, true); + }; [c, d] = array; } \ No newline at end of file From 7636031b0d73ec350e0addb2dbfa7a1e3ee413df Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 14 May 2025 13:22:08 -0400 Subject: [PATCH 038/259] Version Packages (#15905) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/hot-seals-hang.md | 5 ----- .changeset/serious-moles-yell.md | 5 ----- packages/svelte/CHANGELOG.md | 8 ++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 5 files changed, 10 insertions(+), 12 deletions(-) delete mode 100644 .changeset/hot-seals-hang.md delete mode 100644 .changeset/serious-moles-yell.md diff --git a/.changeset/hot-seals-hang.md b/.changeset/hot-seals-hang.md deleted file mode 100644 index 184b89d506..0000000000 --- a/.changeset/hot-seals-hang.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: remove unncessary guards that require CSP privilege when removing event attributes diff --git a/.changeset/serious-moles-yell.md b/.changeset/serious-moles-yell.md deleted file mode 100644 index 08f33dc059..0000000000 --- a/.changeset/serious-moles-yell.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: rewrite destructuring logic to handle iterators diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 8df324d649..935d2b7cc6 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,13 @@ # svelte +## 5.28.7 + +### Patch Changes + +- fix: remove unncessary guards that require CSP privilege when removing event attributes ([#15846](https://github.com/sveltejs/svelte/pull/15846)) + +- fix: rewrite destructuring logic to handle iterators ([#15813](https://github.com/sveltejs/svelte/pull/15813)) + ## 5.28.6 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index dadba9d50c..ce257c687a 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.28.6", + "version": "5.28.7", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index a77849804c..d7c041762f 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.28.6'; +export const VERSION = '5.28.7'; export const PUBLIC_VERSION = '5'; From b93a617aa814a0e9154ae84b6767cd8ca02459a2 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 14 May 2025 13:33:23 -0400 Subject: [PATCH 039/259] feat: attachments (#15000) * parse attachments * basic attachments working * working * rename to attach * fix * restrict which symbols are recognised as attachment keys * allow cleanup to be returned directly * changeset * fix * lint * remove createAttachmentKey/isAttachmentKey * fix spreading of symbol properties onto component * types * fix * update name * reserve ability to use sequence expressions in future * Update packages/svelte/src/internal/client/dom/elements/attachments.js Co-authored-by: Leonidaz * actually let's do this instead * expose createAttachmentKey * make room for `@attach` docs * add docs * failing test * fix * lock down * add missing reference docs * prevent conflicts * update docs * regenerate * fix link * add Attachment interface * beef up test * regenerate * tweak types * fix --------- Co-authored-by: Leonidaz --- .changeset/poor-days-pay.md | 5 + .../docs/03-template-syntax/09-@attach.md | 138 +++++++++++++++++ .../{09-@const.md => 10-@const.md} | 0 .../{10-@debug.md => 11-@debug.md} | 0 .../{11-bind.md => 12-bind.md} | 0 .../{12-use.md => 13-use.md} | 3 + .../{13-transition.md => 14-transition.md} | 0 .../{14-in-and-out.md => 15-in-and-out.md} | 0 .../{15-animate.md => 16-animate.md} | 0 .../98-reference/21-svelte-attachments.md | 5 + packages/svelte/elements.d.ts | 5 + packages/svelte/package.json | 4 + packages/svelte/scripts/generate-types.js | 1 + packages/svelte/src/attachments/index.js | 27 ++++ packages/svelte/src/attachments/public.d.ts | 12 ++ .../compiler/phases/1-parse/read/script.js | 2 +- .../src/compiler/phases/1-parse/read/style.js | 2 +- .../compiler/phases/1-parse/state/element.js | 23 ++- .../src/compiler/phases/2-analyze/index.js | 2 + .../phases/2-analyze/visitors/AttachTag.js | 13 ++ .../2-analyze/visitors/shared/component.js | 36 +++-- .../3-transform/client/transform-client.js | 2 + .../3-transform/client/visitors/AttachTag.js | 21 +++ .../client/visitors/RegularElement.js | 6 +- .../client/visitors/shared/component.js | 8 + .../svelte/src/compiler/types/template.d.ts | 13 +- packages/svelte/src/constants.js | 2 + .../client/dom/elements/attachments.js | 15 ++ .../client/dom/elements/attributes.js | 9 +- packages/svelte/src/internal/client/index.js | 2 + .../src/internal/client/reactivity/props.js | 6 + .../samples/attachments/input.svelte | 1 + .../samples/attachments/output.json | 141 ++++++++++++++++++ .../samples/attachment-basic/_config.js | 6 + .../samples/attachment-basic/main.svelte | 1 + .../attachment-component-spread/Child.svelte | 5 + .../attachment-component-spread/_config.js | 15 ++ .../attachment-component-spread/main.svelte | 30 ++++ .../samples/attachment-component/Child.svelte | 5 + .../samples/attachment-component/_config.js | 14 ++ .../samples/attachment-component/main.svelte | 15 ++ .../samples/attachment-reactive/_config.js | 14 ++ .../samples/attachment-reactive/main.svelte | 6 + .../samples/attachment-spread/_config.js | 8 + .../samples/attachment-spread/main.svelte | 9 ++ .../attachment-svelte-element/_config.js | 6 + .../attachment-svelte-element/main.svelte | 1 + packages/svelte/types/index.d.ts | 47 +++++- 48 files changed, 669 insertions(+), 17 deletions(-) create mode 100644 .changeset/poor-days-pay.md create mode 100644 documentation/docs/03-template-syntax/09-@attach.md rename documentation/docs/03-template-syntax/{09-@const.md => 10-@const.md} (100%) rename documentation/docs/03-template-syntax/{10-@debug.md => 11-@debug.md} (100%) rename documentation/docs/03-template-syntax/{11-bind.md => 12-bind.md} (100%) rename documentation/docs/03-template-syntax/{12-use.md => 13-use.md} (93%) rename documentation/docs/03-template-syntax/{13-transition.md => 14-transition.md} (100%) rename documentation/docs/03-template-syntax/{14-in-and-out.md => 15-in-and-out.md} (100%) rename documentation/docs/03-template-syntax/{15-animate.md => 16-animate.md} (100%) create mode 100644 documentation/docs/98-reference/21-svelte-attachments.md create mode 100644 packages/svelte/src/attachments/index.js create mode 100644 packages/svelte/src/attachments/public.d.ts create mode 100644 packages/svelte/src/compiler/phases/2-analyze/visitors/AttachTag.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/AttachTag.js create mode 100644 packages/svelte/src/internal/client/dom/elements/attachments.js create mode 100644 packages/svelte/tests/parser-modern/samples/attachments/input.svelte create mode 100644 packages/svelte/tests/parser-modern/samples/attachments/output.json create mode 100644 packages/svelte/tests/runtime-runes/samples/attachment-basic/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/attachment-basic/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/attachment-component-spread/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/attachment-component-spread/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/attachment-component-spread/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/attachment-component/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/attachment-component/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/attachment-component/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/attachment-reactive/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/attachment-reactive/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/attachment-spread/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/attachment-spread/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/attachment-svelte-element/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/attachment-svelte-element/main.svelte diff --git a/.changeset/poor-days-pay.md b/.changeset/poor-days-pay.md new file mode 100644 index 0000000000..8fbff10586 --- /dev/null +++ b/.changeset/poor-days-pay.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: attachments diff --git a/documentation/docs/03-template-syntax/09-@attach.md b/documentation/docs/03-template-syntax/09-@attach.md new file mode 100644 index 0000000000..c988386933 --- /dev/null +++ b/documentation/docs/03-template-syntax/09-@attach.md @@ -0,0 +1,138 @@ +--- +title: {@attach ...} +--- + +Attachments are functions that run when an element is mounted to the DOM. Optionally, they can return a function that is called when the element is later removed from the DOM. + +> [!NOTE] +> Attachments are available in Svelte 5.29 and newer. + +```svelte + + + +
...
+``` + +An element can have any number of attachments. + +## Attachment factories + +A useful pattern is for a function, such as `tooltip` in this example, to _return_ an attachment ([demo](/playground/untitled#H4sIAAAAAAAAE3VT0XLaMBD8lavbDiaNCUlbHhTItG_5h5AH2T5ArdBppDOEMv73SkbGJGnH47F9t3un3TsfMyO3mInsh2SW1Sa7zlZKo8_E0zHjg42pGAjxBPxp7cTvUHOMldLjv-IVGUbDoUw295VTlh-WZslqa8kxsLL2ACtHWxh175NffnQfAAGikSGxYQGfPEvGfPSIWtOH0TiBVo2pWJEBJtKhQp4YYzjG9JIdcuMM5IZqHMPioY8vOSA997zQoevf4a7heO7cdp34olRiTGr07OhwH1IdoO2A7dLMbwahZq6MbRhKZWqxk7rBxTGVbuHmhCgb5qDgmIx_J6XtHHukHTrYYqx_YpzYng8aO4RYayql7hU-1ZJl0akqHBE_D9KLolwL-Dibzc7iSln9XjtqTF1UpMkJ2EmXR-BgQErsN4pxIJKr0RVO1qrxAqaTO4fbc9bKulZm3cfDY3aZDgvFGErWjmzhN7KmfX5rXyDeX8Pt1mU-hXjdBOrtuB97vK4GPUtmJ41XcRMEGDLD8do0nJ73zhUhSlyRw0t3vPqD8cjfLs-axiFgNBrkUd9Ulp50c-GLxlXAVlJX-ffpZyiSn7H0eLCUySZQcQdXlxj4El0Yv_FZvIKElqqGTruVLhzu7VRKCh22_5toOyxsWqLwwzK-cCbYNdg-hy-p9D7sbiZWUnts_wLUOF3CJgQAAA==)): + +```svelte + + + + + + +``` + +Since the `tooltip(content)` expression runs inside an [effect]($effect), the attachment will be destroyed and recreated whenever `content` changes. + +## Inline attachments + +Attachments can also be created inline ([demo](/playground/untitled#H4sIAAAAAAAAE71WbW_aSBD-Kyt0VaBJyGKbqoUkOhdI68qGUkh6pPSDMY6xYwyH12Ab8d9vZtYE6DX38aQQe3fennlm1jvbUmTP3VKj9KcthO3MShelJz9041Ljx7YksiWKcAP2C0V9uazGazcUuDexY_d3-84iEm4kwE3pOnZW_lLcjqOx8OfLxUqwLVvafiTYjj2tFnN2Vr3yVvbUB4NqEJ81x9H11cEounbsaG3HaL_xp2J2s1WVHa5mru_NxMtyW6TAytKgwm5u2RYlYwF4YsEIVSrYDZMaVc8VLblXPlOmZ5UmxkP9P9ynJ9cR5fKxk7EIXQGQIV9wsXL_TtxY6JE_t4W_iO5wv_yURA6uWLhYLMuicrAdi_-2RAMCUGgTReUC8gUTB9mueC2WK1ckq4j9AhVytiPHDX_Fh_-PXBVvhcsdEHl7fSXZkeTHIgtdKp7c3UegUjRYjfM3hQ9ZjpOty407efbF5dyOnxssWYXlcWlqC7sBmDz3Kl575-k8bGIXvdMuvn7uKo_Zx3Ayv_Mnn-7FaH4X2Mo0m6gPyWObR5P5g2q0dc9q6fVeS8uMdifttRxvOg_DKf-ydkEHZBuj_ayZgeFZw472JfuoTb6niZPzyP78jTvtxdpUp-o0q6tWVl87c2dtBfrGan3Ip3Mn-hqkm9Ff3xbGp_6HLwqvWwOtDnFqZvAYmMPOxgyehTW8T7oZzy1fU8yhAXuW6La9xPJ5arW0fNLiWTfTamCnmsED2DlJd6BzM3DA1gBbPQVbDv444Qw6iTXgKfjxwC43B5QbyDzPgrXRNlAm0MZoW0nX5_B06Ak-Mc-k10L7kQe81M3gHvYAz0CvkTwAvC2IOdDT7kADDq0MdSHvxMp0XnAJeXyLrQCx8hTxj3J6L2Igbp5KDIRbSNw6YSPcuDfsI5ac8KI80yFWX0AeitHuox4-pa-BpoEvzKMOOSMfWDeBGIFXwP4gzOE9cu71kF_FEpgf8AF-eYq4wQZ5z8A_2HtUF_LRwjXEaYFvrBnkA7rg00L9pCfjJYjHxNzmG8qbeBlgjndBwT1ypyCG7gtPngcY-aTd8TBPM-h41vfiiX6hjsAT9g3yw4t9ReLGdR_rSjUEOfBDtQRcyKUhSI4cwG_SNlTiD3vou5XiO2IB_zniBhusJeanORnHPpLcU92oZ9F3RjUiTizkDnx2BPUv4KK6Qc9RHIwbTGPZ632vCzqjDHlxEFOK9l3C-Yx1UiQ_XDtgkjUkf0MjR59QJ5XiEqZ-geMZasBzmds9YIK-xadPfIkenTsPsWPP_YYHB2OkxXlIqT6DopYDXaOa-1i_jvwW0JkiPHhG8AwUsfpYV6gF4tFzeXYQD9ZDo76kHoV1l3r5MYa9WtG3VA-sPfYKxW5xhbiRvYm9IqhX8HwO8Ix0UL8471hLOtd16mPip4N5UR6AgRdnJ8dvCMip1vCjbw3khfFS6h9lI-jswjnHnpY16yPHWdGPGeHzMcdZTj1J_d3B_JVRjvnopCv5wD7RVdLDPqG4kscTTpQNfvPgbI3g_f-pS4--a3TGUynH_hvJb9QpDzXJg3fo3eyld1Xq3YHjmbn23lTh7sm1m3Gpwur8Df2umMq16vtlyqLF5cpdurb4zb12Gfu522Dv-HruR_IWpQGmuDdhGMILvNQQq8TdXbwyVB3NP6dT1angaKxyUxqlXuaNf40L8qKWg8-W0XV9weQdDYPXzX4YqsprvXlQpru5Dbf0kRIMSsZ-u8wvGPydeNxPTk-LFSvjlLQEY96Ex_XBXxWv_mroRp6Yoej8hmmV0wnNB7MlEK81j3dT2PXZGxnyRJKBpOyDAYkq7Pb2FsLupzips3KnoPVOY-esXFPes7csrewtYA8Eb5lli1k19qOyAAkMMLxyEsZbuW70i5MMnRR8HntxFvErXiZhguMfmL8gPOXmB3DC-E8aEafNVzVqqEGQXtdRUAcDvq6ioopSr-97tugAqvcyOar3iy3VnZLanbb1T1jZfrjxo2mp8WSHsbv7Bx1mHBBZDAAA)): + +```svelte + + + + { + const context = canvas.getContext('2d'); + + $effect(() => { + let frame = requestAnimationFrame(function loop(t) { + frame = requestAnimationFrame(loop); + paint(context, t); + }); + + return () => { + cancelAnimationFrame(frame); + }; + }); + }} +> +``` + +## Passing attachments to components + +When used on a component, `{@attach ...}` will create a prop whose key is a [`Symbol`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol). If the component then [spreads](/tutorial/svelte/spread-props) props onto an element, the element will receive those attachments. + +This allows you to create _wrapper components_ that augment elements ([demo](/playground/untitled#H4sIAAAAAAAAE3VUS3ObMBD-KxvajnFqsJM2PhA7TXrKob31FjITAbKtRkiMtDhJPfz3LiAMdpxhGJvdb1_fPnaeYjn3Iu-WIbJ04028lZDcetHDzsO3olbVApI74F1RhHbLJdayhFl-Sp5qhVwhufEWNjWiwJtYxSjyQhsEFEXxBiujcxg1_8O_dnQ9APwsEbVyiHDafjrvDZCgkiO4MLCEzxYZcn90z6XUZ6OxA61KlaIgV6i1pFC-sxjDrlbHaDiWRoGvdMbHsLzp5DES0mJnRxGaRBvcBHb7yFUTCQeunEWYcYtGv12TqgFUDbCK1WLaM6IWQhUlQiJUFm2ZLPly51xXMG0Rjoyd69C7UqqG2nu95QZyXvtvLVpri2-SN4hoLXXCZFfhQ8aQBU1VgdEaH_vSgyBZR_BpPp_vi0tY-rw2ulRZkGqpTQRbZvwa2BPgFC8bgbw31CbjJjAsE6WNYBZeGp7vtQXLMqHWnZx-5kM1TR5ycpkZXQR2wzL94l8Ur1C_3-g168SfQf1MyfRi3LW9fs77emJEw5QV9SREoLTq06tcczq7d6xEUcJX2vAhO1b843XK34e5unZEMBr15ekuKEusluWAF8lXhE2ZTP2r2RcIHJ-163FPKerCgYJLOB9i4GvNwviI5-gAQiFFBk3tBTOU3HFXEk0R8o86WvUD64aINhv5K3oRmpJXkw8uxMG6Hh6JY9X7OwGSqfUy9tDG3sHNoEi0d_d_fv9qndxRU0VClFqo3KVo3U655Hnt1PXB3Qra2Y2QGdEwgTAMCxopsoxOe6SD0gD8movDhT0LAnhqlE8gVCpLWnRoV7OJCkFAwEXitrYL1W7p7pbiE_P7XH6E_rihODm5s52XtiH9Ekaw0VgI9exadWL1uoEYjPtg2672k5szsxbKyWB2fdT0w5Y_0hcT8oXOlRetmLS8-g-6TLXXQgYAAA==)): + +```svelte + + + + + +``` + +```svelte + + + + + + +``` + +## Creating attachments programmatically + +To add attachments to an object that will be spread onto a component or element, use [`createAttachmentKey`](svelte-attachments#createAttachmentKey). diff --git a/documentation/docs/03-template-syntax/09-@const.md b/documentation/docs/03-template-syntax/10-@const.md similarity index 100% rename from documentation/docs/03-template-syntax/09-@const.md rename to documentation/docs/03-template-syntax/10-@const.md diff --git a/documentation/docs/03-template-syntax/10-@debug.md b/documentation/docs/03-template-syntax/11-@debug.md similarity index 100% rename from documentation/docs/03-template-syntax/10-@debug.md rename to documentation/docs/03-template-syntax/11-@debug.md diff --git a/documentation/docs/03-template-syntax/11-bind.md b/documentation/docs/03-template-syntax/12-bind.md similarity index 100% rename from documentation/docs/03-template-syntax/11-bind.md rename to documentation/docs/03-template-syntax/12-bind.md diff --git a/documentation/docs/03-template-syntax/12-use.md b/documentation/docs/03-template-syntax/13-use.md similarity index 93% rename from documentation/docs/03-template-syntax/12-use.md rename to documentation/docs/03-template-syntax/13-use.md index 45de023578..5f5321a1c0 100644 --- a/documentation/docs/03-template-syntax/12-use.md +++ b/documentation/docs/03-template-syntax/13-use.md @@ -2,6 +2,9 @@ title: use: --- +> [!NOTE] +> In Svelte 5.29 and newer, consider using [attachments](@attach) instead, as they are more flexible and composable. + Actions are functions that are called when an element is mounted. They are added with the `use:` directive, and will typically use an `$effect` so that they can reset any state when the element is unmounted: ```svelte diff --git a/documentation/docs/03-template-syntax/13-transition.md b/documentation/docs/03-template-syntax/14-transition.md similarity index 100% rename from documentation/docs/03-template-syntax/13-transition.md rename to documentation/docs/03-template-syntax/14-transition.md diff --git a/documentation/docs/03-template-syntax/14-in-and-out.md b/documentation/docs/03-template-syntax/15-in-and-out.md similarity index 100% rename from documentation/docs/03-template-syntax/14-in-and-out.md rename to documentation/docs/03-template-syntax/15-in-and-out.md diff --git a/documentation/docs/03-template-syntax/15-animate.md b/documentation/docs/03-template-syntax/16-animate.md similarity index 100% rename from documentation/docs/03-template-syntax/15-animate.md rename to documentation/docs/03-template-syntax/16-animate.md diff --git a/documentation/docs/98-reference/21-svelte-attachments.md b/documentation/docs/98-reference/21-svelte-attachments.md new file mode 100644 index 0000000000..e446bdddc2 --- /dev/null +++ b/documentation/docs/98-reference/21-svelte-attachments.md @@ -0,0 +1,5 @@ +--- +title: svelte/attachments +--- + +> MODULE: svelte/attachments diff --git a/packages/svelte/elements.d.ts b/packages/svelte/elements.d.ts index 99d87b4c09..c637137365 100644 --- a/packages/svelte/elements.d.ts +++ b/packages/svelte/elements.d.ts @@ -31,6 +31,8 @@ // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped // TypeScript Version: 2.8 +import type { Attachment } from 'svelte/attachments'; + // Note: We also allow `null` as a valid value because Svelte treats this the same as `undefined` type Booleanish = boolean | 'true' | 'false'; @@ -860,6 +862,9 @@ export interface HTMLAttributes extends AriaAttributes, D // allow any data- attribute [key: `data-${string}`]: any; + + // allow any attachment + [key: symbol]: Attachment; } export type HTMLAttributeAnchorTarget = '_self' | '_blank' | '_parent' | '_top' | (string & {}); diff --git a/packages/svelte/package.json b/packages/svelte/package.json index ce257c687a..0ec1f9204d 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -34,6 +34,10 @@ "types": "./types/index.d.ts", "default": "./src/animate/index.js" }, + "./attachments": { + "types": "./types/index.d.ts", + "default": "./src/attachments/index.js" + }, "./compiler": { "types": "./types/index.d.ts", "require": "./compiler/index.js", diff --git a/packages/svelte/scripts/generate-types.js b/packages/svelte/scripts/generate-types.js index 377fca4343..c558a2bbf7 100644 --- a/packages/svelte/scripts/generate-types.js +++ b/packages/svelte/scripts/generate-types.js @@ -35,6 +35,7 @@ await createBundle({ [pkg.name]: `${dir}/src/index.d.ts`, [`${pkg.name}/action`]: `${dir}/src/action/public.d.ts`, [`${pkg.name}/animate`]: `${dir}/src/animate/public.d.ts`, + [`${pkg.name}/attachments`]: `${dir}/src/attachments/public.d.ts`, [`${pkg.name}/compiler`]: `${dir}/src/compiler/public.d.ts`, [`${pkg.name}/easing`]: `${dir}/src/easing/index.js`, [`${pkg.name}/legacy`]: `${dir}/src/legacy/legacy-client.js`, diff --git a/packages/svelte/src/attachments/index.js b/packages/svelte/src/attachments/index.js new file mode 100644 index 0000000000..948a19f4dd --- /dev/null +++ b/packages/svelte/src/attachments/index.js @@ -0,0 +1,27 @@ +import { ATTACHMENT_KEY } from '../constants.js'; + +/** + * Creates an object key that will be recognised as an attachment when the object is spread onto an element, + * as a programmatic alternative to using `{@attach ...}`. This can be useful for library authors, though + * is generally not needed when building an app. + * + * ```svelte + * + * + * + * ``` + * @since 5.29 + */ +export function createAttachmentKey() { + return Symbol(ATTACHMENT_KEY); +} diff --git a/packages/svelte/src/attachments/public.d.ts b/packages/svelte/src/attachments/public.d.ts new file mode 100644 index 0000000000..caf1342d0a --- /dev/null +++ b/packages/svelte/src/attachments/public.d.ts @@ -0,0 +1,12 @@ +/** + * An [attachment](https://svelte.dev/docs/svelte/@attach) is a function that runs when an element is mounted + * to the DOM, and optionally returns a function that is called when the element is later removed. + * + * It can be attached to an element with an `{@attach ...}` tag, or by spreading an object containing + * a property created with [`createAttachmentKey`](https://svelte.dev/docs/svelte/svelte-attachments#createAttachmentKey). + */ +export interface Attachment { + (element: T): void | (() => void); +} + +export * from './index.js'; diff --git a/packages/svelte/src/compiler/phases/1-parse/read/script.js b/packages/svelte/src/compiler/phases/1-parse/read/script.js index 9d9ed3a1ef..6290127811 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/script.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/script.js @@ -16,7 +16,7 @@ const ALLOWED_ATTRIBUTES = ['context', 'generics', 'lang', 'module']; /** * @param {Parser} parser * @param {number} start - * @param {Array} attributes + * @param {Array} attributes * @returns {AST.Script} */ export function read_script(parser, start, attributes) { diff --git a/packages/svelte/src/compiler/phases/1-parse/read/style.js b/packages/svelte/src/compiler/phases/1-parse/read/style.js index 56dbe124b7..e15a47e6d5 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/style.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/style.js @@ -18,7 +18,7 @@ const REGEX_HTML_COMMENT_CLOSE = /-->/; /** * @param {Parser} parser * @param {number} start - * @param {Array} attributes + * @param {Array} attributes * @returns {AST.CSS.StyleSheet} */ export default function read_style(parser, start, attributes) { diff --git a/packages/svelte/src/compiler/phases/1-parse/state/element.js b/packages/svelte/src/compiler/phases/1-parse/state/element.js index 66946a8f8d..d20369b0d9 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/element.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/element.js @@ -480,7 +480,7 @@ function read_static_attribute(parser) { /** * @param {Parser} parser - * @returns {AST.Attribute | AST.SpreadAttribute | AST.Directive | null} + * @returns {AST.Attribute | AST.SpreadAttribute | AST.Directive | AST.AttachTag | null} */ function read_attribute(parser) { const start = parser.index; @@ -488,6 +488,27 @@ function read_attribute(parser) { if (parser.eat('{')) { parser.allow_whitespace(); + if (parser.eat('@attach')) { + parser.require_whitespace(); + + const expression = read_expression(parser); + parser.allow_whitespace(); + parser.eat('}', true); + + /** @type {AST.AttachTag} */ + const attachment = { + type: 'AttachTag', + start, + end: parser.index, + expression, + metadata: { + expression: create_expression_metadata() + } + }; + + return attachment; + } + if (parser.eat('...')) { const expression = read_expression(parser); diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 766b317d06..09cb41b23f 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -18,6 +18,7 @@ import { extract_svelte_ignore } from '../../utils/extract_svelte_ignore.js'; import { ignore_map, ignore_stack, pop_ignore, push_ignore } from '../../state.js'; import { ArrowFunctionExpression } from './visitors/ArrowFunctionExpression.js'; import { AssignmentExpression } from './visitors/AssignmentExpression.js'; +import { AttachTag } from './visitors/AttachTag.js'; import { Attribute } from './visitors/Attribute.js'; import { AwaitBlock } from './visitors/AwaitBlock.js'; import { BindDirective } from './visitors/BindDirective.js'; @@ -133,6 +134,7 @@ const visitors = { }, ArrowFunctionExpression, AssignmentExpression, + AttachTag, Attribute, AwaitBlock, BindDirective, diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/AttachTag.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/AttachTag.js new file mode 100644 index 0000000000..1e318f228d --- /dev/null +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AttachTag.js @@ -0,0 +1,13 @@ +/** @import { AST } from '#compiler' */ +/** @import { Context } from '../types' */ + +import { mark_subtree_dynamic } from './shared/fragment.js'; + +/** + * @param {AST.AttachTag} node + * @param {Context} context + */ +export function AttachTag(node, context) { + mark_subtree_dynamic(context.path); + context.next({ ...context.state, expression: node.metadata.expression }); +} diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js index 04bf3d2ff3..aca87fab81 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js @@ -1,3 +1,4 @@ +/** @import { Expression } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { AnalysisState, Context } from '../../types' */ import * as e from '../../../../errors.js'; @@ -74,7 +75,8 @@ export function visit_component(node, context) { attribute.type !== 'SpreadAttribute' && attribute.type !== 'LetDirective' && attribute.type !== 'OnDirective' && - attribute.type !== 'BindDirective' + attribute.type !== 'BindDirective' && + attribute.type !== 'AttachTag' ) { e.component_invalid_directive(attribute); } @@ -91,15 +93,10 @@ export function visit_component(node, context) { validate_attribute(attribute, node); if (is_expression_attribute(attribute)) { - const expression = get_attribute_expression(attribute); - if (expression.type === 'SequenceExpression') { - let i = /** @type {number} */ (expression.start); - while (--i > 0) { - const char = context.state.analysis.source[i]; - if (char === '(') break; // parenthesized sequence expressions are ok - if (char === '{') e.attribute_invalid_sequence_expression(expression); - } - } + disallow_unparenthesized_sequences( + get_attribute_expression(attribute), + context.state.analysis.source + ); } } @@ -113,6 +110,10 @@ export function visit_component(node, context) { if (attribute.type === 'BindDirective' && attribute.name !== 'this') { context.state.analysis.uses_component_bindings = true; } + + if (attribute.type === 'AttachTag') { + disallow_unparenthesized_sequences(attribute.expression, context.state.analysis.source); + } } // If the component has a slot attribute — `` — @@ -158,3 +159,18 @@ export function visit_component(node, context) { context.visit({ ...node.fragment, nodes: nodes[slot_name] }, state); } } + +/** + * @param {Expression} expression + * @param {string} source + */ +function disallow_unparenthesized_sequences(expression, source) { + if (expression.type === 'SequenceExpression') { + let i = /** @type {number} */ (expression.start); + while (--i > 0) { + const char = source[i]; + if (char === '(') break; // parenthesized sequence expressions are ok + if (char === '{') e.attribute_invalid_sequence_expression(expression); + } + } +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index f0da5a4918..9a2f4dd34c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -56,6 +56,7 @@ import { TitleElement } from './visitors/TitleElement.js'; import { TransitionDirective } from './visitors/TransitionDirective.js'; import { UpdateExpression } from './visitors/UpdateExpression.js'; import { UseDirective } from './visitors/UseDirective.js'; +import { AttachTag } from './visitors/AttachTag.js'; import { VariableDeclaration } from './visitors/VariableDeclaration.js'; /** @type {Visitors} */ @@ -131,6 +132,7 @@ const visitors = { TransitionDirective, UpdateExpression, UseDirective, + AttachTag, VariableDeclaration }; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AttachTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AttachTag.js new file mode 100644 index 0000000000..062604cacc --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AttachTag.js @@ -0,0 +1,21 @@ +/** @import { Expression } from 'estree' */ +/** @import { AST } from '#compiler' */ +/** @import { ComponentContext } from '../types' */ +import * as b from '../../../../utils/builders.js'; + +/** + * @param {AST.AttachTag} node + * @param {ComponentContext} context + */ +export function AttachTag(node, context) { + context.state.init.push( + b.stmt( + b.call( + '$.attach', + context.state.node, + b.thunk(/** @type {Expression} */ (context.visit(node.expression))) + ) + ) + ); + context.next(); +} 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 7468fcbbc7..ab981878ad 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 @@ -83,7 +83,7 @@ export function RegularElement(node, context) { /** @type {AST.StyleDirective[]} */ const style_directives = []; - /** @type {Array} */ + /** @type {Array} */ const other_directives = []; /** @type {ExpressionStatement[]} */ @@ -153,6 +153,10 @@ export function RegularElement(node, context) { has_use = true; other_directives.push(attribute); break; + + case 'AttachTag': + other_directives.push(attribute); + break; } } 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 c4071c67fe..6d3d8a68e6 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 @@ -257,6 +257,14 @@ export function build_component(node, component_name, context, anchor = context. ); } } + } else if (attribute.type === 'AttachTag') { + let expression = /** @type {Expression} */ (context.visit(attribute.expression)); + + if (attribute.metadata.expression.has_state) { + expression = b.arrow([b.id('$$node')], b.call(expression, b.id('$$node'))); + } + + push_prop(b.prop('get', b.call('$.attachment'), expression, true)); } } diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index a544cd1dec..54fdda92b8 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -174,6 +174,16 @@ export namespace AST { }; } + /** A `{@attach foo(...)} tag */ + export interface AttachTag extends BaseNode { + type: 'AttachTag'; + expression: Expression; + /** @internal */ + metadata: { + expression: ExpressionMetadata; + }; + } + /** An `animate:` directive */ export interface AnimateDirective extends BaseNode { type: 'AnimateDirective'; @@ -273,7 +283,7 @@ export namespace AST { interface BaseElement extends BaseNode { name: string; - attributes: Array; + attributes: Array; fragment: Fragment; } @@ -546,6 +556,7 @@ export namespace AST { | AST.Attribute | AST.SpreadAttribute | Directive + | AST.AttachTag | AST.Comment | Block; diff --git a/packages/svelte/src/constants.js b/packages/svelte/src/constants.js index 8861e440fc..2ecd4afee2 100644 --- a/packages/svelte/src/constants.js +++ b/packages/svelte/src/constants.js @@ -55,3 +55,5 @@ export const IGNORABLE_RUNTIME_WARNINGS = /** @type {const} */ ([ * TODO this is currently unused */ export const ELEMENTS_WITHOUT_TEXT = ['audio', 'datalist', 'dl', 'optgroup', 'select', 'video']; + +export const ATTACHMENT_KEY = '@attach'; diff --git a/packages/svelte/src/internal/client/dom/elements/attachments.js b/packages/svelte/src/internal/client/dom/elements/attachments.js new file mode 100644 index 0000000000..6e3089a384 --- /dev/null +++ b/packages/svelte/src/internal/client/dom/elements/attachments.js @@ -0,0 +1,15 @@ +import { effect } from '../../reactivity/effects.js'; + +/** + * @param {Element} node + * @param {() => (node: Element) => void} get_fn + */ +export function attach(node, get_fn) { + effect(() => { + const fn = get_fn(); + + // we use `&&` rather than `?.` so that things like + // `{@attach DEV && something_dev_only()}` work + return fn && fn(node); + }); +} diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index f63f55cc6e..3d1acbd31c 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -13,10 +13,11 @@ import { set_active_effect, set_active_reaction } from '../../runtime.js'; +import { attach } from './attachments.js'; import { clsx } from '../../../shared/attributes.js'; import { set_class } from './class.js'; import { set_style } from './style.js'; -import { NAMESPACE_HTML } from '../../../../constants.js'; +import { ATTACHMENT_KEY, NAMESPACE_HTML } from '../../../../constants.js'; export const CLASS = Symbol('class'); export const STYLE = Symbol('style'); @@ -446,6 +447,12 @@ export function set_attributes(element, prev, next, css_hash, skip_warning = fal set_hydrating(true); } + for (let symbol of Object.getOwnPropertySymbols(next)) { + if (symbol.description === ATTACHMENT_KEY) { + attach(element, () => next[symbol]); + } + } + return current; } diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 14d6e29f5b..b5f746b276 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -1,3 +1,4 @@ +export { createAttachmentKey as attachment } from '../../attachments/index.js'; export { FILENAME, HMR, NAMESPACE_SVG } from '../../constants.js'; export { push, pop } from './context.js'; export { assign, assign_and, assign_or, assign_nullish } from './dev/assign.js'; @@ -22,6 +23,7 @@ export { element } from './dom/blocks/svelte-element.js'; export { head } from './dom/blocks/svelte-head.js'; export { append_styles } from './dom/css.js'; export { action } from './dom/elements/actions.js'; +export { attach } from './dom/elements/attachments.js'; export { remove_input_defaults, set_attribute, diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js index 8bfd8f9e25..77c58720e1 100644 --- a/packages/svelte/src/internal/client/reactivity/props.js +++ b/packages/svelte/src/internal/client/reactivity/props.js @@ -218,9 +218,15 @@ const spread_props_handler = { for (let p of target.props) { if (is_function(p)) p = p(); + if (!p) continue; + for (const key in p) { if (!keys.includes(key)) keys.push(key); } + + for (const key of Object.getOwnPropertySymbols(p)) { + if (!keys.includes(key)) keys.push(key); + } } return keys; diff --git a/packages/svelte/tests/parser-modern/samples/attachments/input.svelte b/packages/svelte/tests/parser-modern/samples/attachments/input.svelte new file mode 100644 index 0000000000..9faae8d1bf --- /dev/null +++ b/packages/svelte/tests/parser-modern/samples/attachments/input.svelte @@ -0,0 +1 @@ +
{}} {@attach (node) => {}}>
diff --git a/packages/svelte/tests/parser-modern/samples/attachments/output.json b/packages/svelte/tests/parser-modern/samples/attachments/output.json new file mode 100644 index 0000000000..42e9880fcc --- /dev/null +++ b/packages/svelte/tests/parser-modern/samples/attachments/output.json @@ -0,0 +1,141 @@ +{ + "css": null, + "js": [], + "start": 0, + "end": 57, + "type": "Root", + "fragment": { + "type": "Fragment", + "nodes": [ + { + "type": "RegularElement", + "start": 0, + "end": 57, + "name": "div", + "attributes": [ + { + "type": "AttachTag", + "start": 5, + "end": 27, + "expression": { + "type": "ArrowFunctionExpression", + "start": 14, + "end": 26, + "loc": { + "start": { + "line": 1, + "column": 14 + }, + "end": { + "line": 1, + "column": 26 + } + }, + "id": null, + "expression": false, + "generator": false, + "async": false, + "params": [ + { + "type": "Identifier", + "start": 15, + "end": 19, + "loc": { + "start": { + "line": 1, + "column": 15 + }, + "end": { + "line": 1, + "column": 19 + } + }, + "name": "node" + } + ], + "body": { + "type": "BlockStatement", + "start": 24, + "end": 26, + "loc": { + "start": { + "line": 1, + "column": 24 + }, + "end": { + "line": 1, + "column": 26 + } + }, + "body": [] + } + } + }, + { + "type": "AttachTag", + "start": 28, + "end": 50, + "expression": { + "type": "ArrowFunctionExpression", + "start": 37, + "end": 49, + "loc": { + "start": { + "line": 1, + "column": 37 + }, + "end": { + "line": 1, + "column": 49 + } + }, + "id": null, + "expression": false, + "generator": false, + "async": false, + "params": [ + { + "type": "Identifier", + "start": 38, + "end": 42, + "loc": { + "start": { + "line": 1, + "column": 38 + }, + "end": { + "line": 1, + "column": 42 + } + }, + "name": "node" + } + ], + "body": { + "type": "BlockStatement", + "start": 47, + "end": 49, + "loc": { + "start": { + "line": 1, + "column": 47 + }, + "end": { + "line": 1, + "column": 49 + } + }, + "body": [] + } + } + } + ], + "fragment": { + "type": "Fragment", + "nodes": [] + } + } + ] + }, + "options": null +} diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-basic/_config.js b/packages/svelte/tests/runtime-runes/samples/attachment-basic/_config.js new file mode 100644 index 0000000000..1be4737069 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attachment-basic/_config.js @@ -0,0 +1,6 @@ +import { test } from '../../test'; + +export default test({ + ssrHtml: `
`, + html: `
DIV
` +}); diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-basic/main.svelte b/packages/svelte/tests/runtime-runes/samples/attachment-basic/main.svelte new file mode 100644 index 0000000000..1a1f74e4a9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attachment-basic/main.svelte @@ -0,0 +1 @@ +
node.textContent = node.nodeName}>
diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-component-spread/Child.svelte b/packages/svelte/tests/runtime-runes/samples/attachment-component-spread/Child.svelte new file mode 100644 index 0000000000..6760da61fa --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attachment-component-spread/Child.svelte @@ -0,0 +1,5 @@ + + +
diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-component-spread/_config.js b/packages/svelte/tests/runtime-runes/samples/attachment-component-spread/_config.js new file mode 100644 index 0000000000..23907c62d2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attachment-component-spread/_config.js @@ -0,0 +1,15 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `
`, + + test({ target, assert, logs }) { + const button = target.querySelector('button'); + + assert.deepEqual(logs, ['one DIV']); + + flushSync(() => button?.click()); + assert.deepEqual(logs, ['one DIV', 'cleanup one', 'two DIV']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-component-spread/main.svelte b/packages/svelte/tests/runtime-runes/samples/attachment-component-spread/main.svelte new file mode 100644 index 0000000000..d1d7e65126 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attachment-component-spread/main.svelte @@ -0,0 +1,30 @@ + + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-component/Child.svelte b/packages/svelte/tests/runtime-runes/samples/attachment-component/Child.svelte new file mode 100644 index 0000000000..6760da61fa --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attachment-component/Child.svelte @@ -0,0 +1,5 @@ + + +
diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-component/_config.js b/packages/svelte/tests/runtime-runes/samples/attachment-component/_config.js new file mode 100644 index 0000000000..b6ef016be5 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attachment-component/_config.js @@ -0,0 +1,14 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + ssrHtml: `
`, + html: `
one
`, + + test({ target, assert }) { + const button = target.querySelector('button'); + + flushSync(() => button?.click()); + assert.htmlEqual(target.innerHTML, '
two
'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-component/main.svelte b/packages/svelte/tests/runtime-runes/samples/attachment-component/main.svelte new file mode 100644 index 0000000000..29e26689db --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attachment-component/main.svelte @@ -0,0 +1,15 @@ + + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-reactive/_config.js b/packages/svelte/tests/runtime-runes/samples/attachment-reactive/_config.js new file mode 100644 index 0000000000..7d0502590b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attachment-reactive/_config.js @@ -0,0 +1,14 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + ssrHtml: `
`, + html: `
1
`, + + test: ({ assert, target }) => { + const btn = target.querySelector('button'); + + flushSync(() => btn?.click()); + assert.htmlEqual(target.innerHTML, `
2
`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-reactive/main.svelte b/packages/svelte/tests/runtime-runes/samples/attachment-reactive/main.svelte new file mode 100644 index 0000000000..9fa3cfdb67 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attachment-reactive/main.svelte @@ -0,0 +1,6 @@ + + +
node.textContent = value}>
+ diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-spread/_config.js b/packages/svelte/tests/runtime-runes/samples/attachment-spread/_config.js new file mode 100644 index 0000000000..96fc207450 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attachment-spread/_config.js @@ -0,0 +1,8 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test({ assert, logs, target }) { + assert.deepEqual(logs, ['hello']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-spread/main.svelte b/packages/svelte/tests/runtime-runes/samples/attachment-spread/main.svelte new file mode 100644 index 0000000000..dbd8c47ada --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attachment-spread/main.svelte @@ -0,0 +1,9 @@ + + +
diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-svelte-element/_config.js b/packages/svelte/tests/runtime-runes/samples/attachment-svelte-element/_config.js new file mode 100644 index 0000000000..1be4737069 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attachment-svelte-element/_config.js @@ -0,0 +1,6 @@ +import { test } from '../../test'; + +export default test({ + ssrHtml: `
`, + html: `
DIV
` +}); diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-svelte-element/main.svelte b/packages/svelte/tests/runtime-runes/samples/attachment-svelte-element/main.svelte new file mode 100644 index 0000000000..bd4b52342f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attachment-svelte-element/main.svelte @@ -0,0 +1 @@ + node.textContent = node.nodeName}> diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index b233cfcc0b..ff9764b88b 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -624,6 +624,44 @@ declare module 'svelte/animate' { export {}; } +declare module 'svelte/attachments' { + /** + * An [attachment](https://svelte.dev/docs/svelte/@attach) is a function that runs when an element is mounted + * to the DOM, and optionally returns a function that is called when the element is later removed. + * + * It can be attached to an element with an `{@attach ...}` tag, or by spreading an object containing + * a property created with [`createAttachmentKey`](https://svelte.dev/docs/svelte/svelte-attachments#createAttachmentKey). + */ + export interface Attachment { + (element: T): void | (() => void); + } + /** + * Creates an object key that will be recognised as an attachment when the object is spread onto an element, + * as a programmatic alternative to using `{@attach ...}`. This can be useful for library authors, though + * is generally not needed when building an app. + * + * ```svelte + * + * + * + * ``` + * @since 5.29 + */ + export function createAttachmentKey(): symbol; + + export {}; +} + declare module 'svelte/compiler' { import type { SourceMap } from 'magic-string'; import type { ArrayExpression, ArrowFunctionExpression, VariableDeclaration, VariableDeclarator, Expression, Identifier, MemberExpression, Node, ObjectExpression, Pattern, Program, ChainExpression, SimpleCallExpression, SequenceExpression } from 'estree'; @@ -1055,6 +1093,12 @@ declare module 'svelte/compiler' { expression: SimpleCallExpression | (ChainExpression & { expression: SimpleCallExpression }); } + /** A `{@attach foo(...)} tag */ + export interface AttachTag extends BaseNode { + type: 'AttachTag'; + expression: Expression; + } + /** An `animate:` directive */ export interface AnimateDirective extends BaseNode { type: 'AnimateDirective'; @@ -1137,7 +1181,7 @@ declare module 'svelte/compiler' { interface BaseElement extends BaseNode { name: string; - attributes: Array; + attributes: Array; fragment: Fragment; } @@ -1327,6 +1371,7 @@ declare module 'svelte/compiler' { | AST.Attribute | AST.SpreadAttribute | Directive + | AST.AttachTag | AST.Comment | Block; From 51b858dfd1e38f92c0086146e5fa7f55ed7a8069 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 14 May 2025 13:36:51 -0400 Subject: [PATCH 040/259] Version Packages (#15914) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/poor-days-pay.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/poor-days-pay.md diff --git a/.changeset/poor-days-pay.md b/.changeset/poor-days-pay.md deleted file mode 100644 index 8fbff10586..0000000000 --- a/.changeset/poor-days-pay.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': minor ---- - -feat: attachments diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 935d2b7cc6..136219af42 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.29.0 + +### Minor Changes + +- feat: attachments ([#15000](https://github.com/sveltejs/svelte/pull/15000)) + ## 5.28.7 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 0ec1f9204d..312aa50c25 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.28.7", + "version": "5.29.0", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index d7c041762f..4499374896 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.28.7'; +export const VERSION = '5.29.0'; export const PUBLIC_VERSION = '5'; From 66a21552f64eea77a46e22a46eb46d4ff010d949 Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Wed, 14 May 2025 21:58:58 +0200 Subject: [PATCH 041/259] feat: allow generics on snippets (#15915) * feat: allow generics on snippets * chore: fix lint * reuse bracket matching logic * remove some unused stuff * chore: update name, test and types * chore: fix lint --------- Co-authored-by: Rich Harris --- .changeset/ten-colts-grab.md | 5 + .../compiler/phases/1-parse/read/context.js | 75 +---- .../src/compiler/phases/1-parse/state/tag.js | 20 ++ .../compiler/phases/1-parse/utils/bracket.js | 113 +++++-- .../svelte/src/compiler/types/template.d.ts | 1 + .../samples/generic-snippets/input.svelte | 10 + .../samples/generic-snippets/output.json | 299 ++++++++++++++++++ packages/svelte/types/index.d.ts | 1 + 8 files changed, 421 insertions(+), 103 deletions(-) create mode 100644 .changeset/ten-colts-grab.md create mode 100644 packages/svelte/tests/parser-modern/samples/generic-snippets/input.svelte create mode 100644 packages/svelte/tests/parser-modern/samples/generic-snippets/output.json diff --git a/.changeset/ten-colts-grab.md b/.changeset/ten-colts-grab.md new file mode 100644 index 0000000000..6e0e20bc84 --- /dev/null +++ b/.changeset/ten-colts-grab.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: allow generics on snippets diff --git a/packages/svelte/src/compiler/phases/1-parse/read/context.js b/packages/svelte/src/compiler/phases/1-parse/read/context.js index f4c73dcf40..b118901830 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/context.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/context.js @@ -1,7 +1,7 @@ /** @import { Location } from 'locate-character' */ /** @import { Pattern } from 'estree' */ /** @import { Parser } from '../index.js' */ -import { is_bracket_open, is_bracket_close, get_bracket_close } from '../utils/bracket.js'; +import { match_bracket } from '../utils/bracket.js'; import { parse_expression_at } from '../acorn.js'; import { regex_not_newline_characters } from '../../patterns.js'; import * as e from '../../../errors.js'; @@ -33,7 +33,9 @@ export default function read_pattern(parser) { }; } - if (!is_bracket_open(parser.template[i])) { + const char = parser.template[i]; + + if (char !== '{' && char !== '[') { e.expected_pattern(i); } @@ -71,75 +73,6 @@ export default function read_pattern(parser) { } } -/** - * @param {Parser} parser - * @param {number} start - */ -function match_bracket(parser, start) { - const bracket_stack = []; - - let i = start; - - while (i < parser.template.length) { - let char = parser.template[i++]; - - if (char === "'" || char === '"' || char === '`') { - i = match_quote(parser, i, char); - continue; - } - - if (is_bracket_open(char)) { - bracket_stack.push(char); - } else if (is_bracket_close(char)) { - const popped = /** @type {string} */ (bracket_stack.pop()); - const expected = /** @type {string} */ (get_bracket_close(popped)); - - if (char !== expected) { - e.expected_token(i - 1, expected); - } - - if (bracket_stack.length === 0) { - return i; - } - } - } - - e.unexpected_eof(parser.template.length); -} - -/** - * @param {Parser} parser - * @param {number} start - * @param {string} quote - */ -function match_quote(parser, start, quote) { - let is_escaped = false; - let i = start; - - while (i < parser.template.length) { - const char = parser.template[i++]; - - if (is_escaped) { - is_escaped = false; - continue; - } - - if (char === quote) { - return i; - } - - if (char === '\\') { - is_escaped = true; - } - - if (quote === '`' && char === '$' && parser.template[i] === '{') { - i = match_bracket(parser, i); - } - } - - e.unterminated_string_constant(start); -} - /** * @param {Parser} parser * @returns {any} diff --git a/packages/svelte/src/compiler/phases/1-parse/state/tag.js b/packages/svelte/src/compiler/phases/1-parse/state/tag.js index 0eb98c27e8..4153463c83 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/tag.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/tag.js @@ -8,9 +8,12 @@ import { parse_expression_at } from '../acorn.js'; import read_pattern from '../read/context.js'; import read_expression, { get_loose_identifier } from '../read/expression.js'; import { create_fragment } from '../utils/create.js'; +import { match_bracket } from '../utils/bracket.js'; const regex_whitespace_with_closing_curly_brace = /^\s*}/; +const pointy_bois = { '<': '>' }; + /** @param {Parser} parser */ export default function tag(parser) { const start = parser.index; @@ -351,6 +354,22 @@ function open(parser) { const params_start = parser.index; + // snippets could have a generic signature, e.g. `#snippet foo(...)` + /** @type {string | undefined} */ + let type_params; + + // if we match a generic opening + if (parser.ts && parser.match('<')) { + const start = parser.index; + const end = match_bracket(parser, start, pointy_bois); + + type_params = parser.template.slice(start + 1, end - 1); + + parser.index = end; + } + + parser.allow_whitespace(); + const matched = parser.eat('(', true, false); if (matched) { @@ -388,6 +407,7 @@ function open(parser) { end: name_end, name }, + typeParams: type_params, parameters: function_expression.params, body: create_fragment(), metadata: { diff --git a/packages/svelte/src/compiler/phases/1-parse/utils/bracket.js b/packages/svelte/src/compiler/phases/1-parse/utils/bracket.js index b7c8cb43cd..8c69a58c99 100644 --- a/packages/svelte/src/compiler/phases/1-parse/utils/bracket.js +++ b/packages/svelte/src/compiler/phases/1-parse/utils/bracket.js @@ -1,34 +1,5 @@ -const SQUARE_BRACKET_OPEN = '['; -const SQUARE_BRACKET_CLOSE = ']'; -const CURLY_BRACKET_OPEN = '{'; -const CURLY_BRACKET_CLOSE = '}'; -const PARENTHESES_OPEN = '('; -const PARENTHESES_CLOSE = ')'; - -/** @param {string} char */ -export function is_bracket_open(char) { - return char === SQUARE_BRACKET_OPEN || char === CURLY_BRACKET_OPEN; -} - -/** @param {string} char */ -export function is_bracket_close(char) { - return char === SQUARE_BRACKET_CLOSE || char === CURLY_BRACKET_CLOSE; -} - -/** @param {string} open */ -export function get_bracket_close(open) { - if (open === SQUARE_BRACKET_OPEN) { - return SQUARE_BRACKET_CLOSE; - } - - if (open === CURLY_BRACKET_OPEN) { - return CURLY_BRACKET_CLOSE; - } - - if (open === PARENTHESES_OPEN) { - return PARENTHESES_CLOSE; - } -} +/** @import { Parser } from '../index.js' */ +import * as e from '../../../errors.js'; /** * @param {number} num @@ -121,7 +92,7 @@ function count_leading_backslashes(string, search_start_index) { * @returns {number | undefined} The index of the closing bracket, or undefined if not found. */ export function find_matching_bracket(template, index, open) { - const close = get_bracket_close(open); + const close = default_brackets[open]; let brackets = 1; let i = index; while (brackets > 0 && i < template.length) { @@ -162,3 +133,81 @@ export function find_matching_bracket(template, index, open) { } return undefined; } + +/** @type {Record} */ +const default_brackets = { + '{': '}', + '(': ')', + '[': ']' +}; + +/** + * @param {Parser} parser + * @param {number} start + * @param {Record} brackets + */ +export function match_bracket(parser, start, brackets = default_brackets) { + const close = Object.values(brackets); + const bracket_stack = []; + + let i = start; + + while (i < parser.template.length) { + let char = parser.template[i++]; + + if (char === "'" || char === '"' || char === '`') { + i = match_quote(parser, i, char); + continue; + } + + if (char in brackets) { + bracket_stack.push(char); + } else if (close.includes(char)) { + const popped = /** @type {string} */ (bracket_stack.pop()); + const expected = /** @type {string} */ (brackets[popped]); + + if (char !== expected) { + e.expected_token(i - 1, expected); + } + + if (bracket_stack.length === 0) { + return i; + } + } + } + + e.unexpected_eof(parser.template.length); +} + +/** + * @param {Parser} parser + * @param {number} start + * @param {string} quote + */ +function match_quote(parser, start, quote) { + let is_escaped = false; + let i = start; + + while (i < parser.template.length) { + const char = parser.template[i++]; + + if (is_escaped) { + is_escaped = false; + continue; + } + + if (char === quote) { + return i; + } + + if (char === '\\') { + is_escaped = true; + } + + if (quote === '`' && char === '$' && parser.template[i] === '{') { + i = match_bracket(parser, i); + } + } + + e.unterminated_string_constant(start); +} diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index 54fdda92b8..6dec1f2dbe 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -468,6 +468,7 @@ export namespace AST { type: 'SnippetBlock'; expression: Identifier; parameters: Pattern[]; + typeParams?: string; body: Fragment; /** @internal */ metadata: { diff --git a/packages/svelte/tests/parser-modern/samples/generic-snippets/input.svelte b/packages/svelte/tests/parser-modern/samples/generic-snippets/input.svelte new file mode 100644 index 0000000000..4ee619728d --- /dev/null +++ b/packages/svelte/tests/parser-modern/samples/generic-snippets/input.svelte @@ -0,0 +1,10 @@ + + +{#snippet generic(val: T)} + {val} +{/snippet} + +{#snippet complex_generic">>(val: T)} + {val} +{/snippet} \ No newline at end of file diff --git a/packages/svelte/tests/parser-modern/samples/generic-snippets/output.json b/packages/svelte/tests/parser-modern/samples/generic-snippets/output.json new file mode 100644 index 0000000000..b66ee7288f --- /dev/null +++ b/packages/svelte/tests/parser-modern/samples/generic-snippets/output.json @@ -0,0 +1,299 @@ +{ + "css": null, + "js": [], + "start": 30, + "end": 192, + "type": "Root", + "fragment": { + "type": "Fragment", + "nodes": [ + { + "type": "Text", + "start": 28, + "end": 30, + "raw": "\n\n", + "data": "\n\n" + }, + { + "type": "SnippetBlock", + "start": 30, + "end": 92, + "expression": { + "type": "Identifier", + "start": 40, + "end": 47, + "name": "generic" + }, + "typeParams": "T extends string", + "parameters": [ + { + "type": "Identifier", + "start": 66, + "end": 72, + "loc": { + "start": { + "line": 4, + "column": 36 + }, + "end": { + "line": 4, + "column": 42 + } + }, + "name": "val", + "typeAnnotation": { + "type": "TSTypeAnnotation", + "start": 69, + "end": 72, + "loc": { + "start": { + "line": 4, + "column": 39 + }, + "end": { + "line": 4, + "column": 42 + } + }, + "typeAnnotation": { + "type": "TSTypeReference", + "start": 71, + "end": 72, + "loc": { + "start": { + "line": 4, + "column": 41 + }, + "end": { + "line": 4, + "column": 42 + } + }, + "typeName": { + "type": "Identifier", + "start": 71, + "end": 72, + "loc": { + "start": { + "line": 4, + "column": 41 + }, + "end": { + "line": 4, + "column": 42 + } + }, + "name": "T" + } + } + } + } + ], + "body": { + "type": "Fragment", + "nodes": [ + { + "type": "Text", + "start": 74, + "end": 76, + "raw": "\n\t", + "data": "\n\t" + }, + { + "type": "ExpressionTag", + "start": 76, + "end": 81, + "expression": { + "type": "Identifier", + "start": 77, + "end": 80, + "loc": { + "start": { + "line": 5, + "column": 2 + }, + "end": { + "line": 5, + "column": 5 + } + }, + "name": "val" + } + }, + { + "type": "Text", + "start": 81, + "end": 82, + "raw": "\n", + "data": "\n" + } + ] + } + }, + { + "type": "Text", + "start": 92, + "end": 94, + "raw": "\n\n", + "data": "\n\n" + }, + { + "type": "SnippetBlock", + "start": 94, + "end": 192, + "expression": { + "type": "Identifier", + "start": 104, + "end": 119, + "name": "complex_generic" + }, + "typeParams": "T extends { bracket: \"<\" } | \"<\" | Set<\"<>\">", + "parameters": [ + { + "type": "Identifier", + "start": 166, + "end": 172, + "loc": { + "start": { + "line": 8, + "column": 72 + }, + "end": { + "line": 8, + "column": 78 + } + }, + "name": "val", + "typeAnnotation": { + "type": "TSTypeAnnotation", + "start": 169, + "end": 172, + "loc": { + "start": { + "line": 8, + "column": 75 + }, + "end": { + "line": 8, + "column": 78 + } + }, + "typeAnnotation": { + "type": "TSTypeReference", + "start": 171, + "end": 172, + "loc": { + "start": { + "line": 8, + "column": 77 + }, + "end": { + "line": 8, + "column": 78 + } + }, + "typeName": { + "type": "Identifier", + "start": 171, + "end": 172, + "loc": { + "start": { + "line": 8, + "column": 77 + }, + "end": { + "line": 8, + "column": 78 + } + }, + "name": "T" + } + } + } + } + ], + "body": { + "type": "Fragment", + "nodes": [ + { + "type": "Text", + "start": 174, + "end": 176, + "raw": "\n\t", + "data": "\n\t" + }, + { + "type": "ExpressionTag", + "start": 176, + "end": 181, + "expression": { + "type": "Identifier", + "start": 177, + "end": 180, + "loc": { + "start": { + "line": 9, + "column": 2 + }, + "end": { + "line": 9, + "column": 5 + } + }, + "name": "val" + } + }, + { + "type": "Text", + "start": 181, + "end": 182, + "raw": "\n", + "data": "\n" + } + ] + } + } + ] + }, + "options": null, + "instance": { + "type": "Script", + "start": 0, + "end": 28, + "context": "default", + "content": { + "type": "Program", + "start": 18, + "end": 19, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 2, + "column": 0 + } + }, + "body": [], + "sourceType": "module" + }, + "attributes": [ + { + "type": "Attribute", + "start": 8, + "end": 17, + "name": "lang", + "value": [ + { + "start": 14, + "end": 16, + "type": "Text", + "raw": "ts", + "data": "ts" + } + ] + } + ] + } +} diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index ff9764b88b..bb958c5108 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -1301,6 +1301,7 @@ declare module 'svelte/compiler' { type: 'SnippetBlock'; expression: Identifier; parameters: Pattern[]; + typeParams?: string; body: Fragment; } From e2a13a3beb246fe3f3e471353921a2db5ca1c57d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 14 May 2025 16:03:05 -0400 Subject: [PATCH 042/259] Version Packages (#15916) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/ten-colts-grab.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/ten-colts-grab.md diff --git a/.changeset/ten-colts-grab.md b/.changeset/ten-colts-grab.md deleted file mode 100644 index 6e0e20bc84..0000000000 --- a/.changeset/ten-colts-grab.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': minor ---- - -feat: allow generics on snippets diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 136219af42..b25235276b 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.30.0 + +### Minor Changes + +- feat: allow generics on snippets ([#15915](https://github.com/sveltejs/svelte/pull/15915)) + ## 5.29.0 ### Minor Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 312aa50c25..5c27d88952 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.29.0", + "version": "5.30.0", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 4499374896..69492b1bf2 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.29.0'; +export const VERSION = '5.30.0'; export const PUBLIC_VERSION = '5'; From 43e32ba2ce7bf30a4854af9318c8388a4d1265d6 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 14 May 2025 17:37:25 -0400 Subject: [PATCH 043/259] docs: update attach example (#15920) --- .../docs/03-template-syntax/09-@attach.md | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/documentation/docs/03-template-syntax/09-@attach.md b/documentation/docs/03-template-syntax/09-@attach.md index c988386933..2df0882e34 100644 --- a/documentation/docs/03-template-syntax/09-@attach.md +++ b/documentation/docs/03-template-syntax/09-@attach.md @@ -59,14 +59,10 @@ Since the `tooltip(content)` expression runs inside an [effect]($effect), the at ## Inline attachments -Attachments can also be created inline ([demo](/playground/untitled#H4sIAAAAAAAAE71WbW_aSBD-Kyt0VaBJyGKbqoUkOhdI68qGUkh6pPSDMY6xYwyH12Ab8d9vZtYE6DX38aQQe3fennlm1jvbUmTP3VKj9KcthO3MShelJz9041Ljx7YksiWKcAP2C0V9uazGazcUuDexY_d3-84iEm4kwE3pOnZW_lLcjqOx8OfLxUqwLVvafiTYjj2tFnN2Vr3yVvbUB4NqEJ81x9H11cEounbsaG3HaL_xp2J2s1WVHa5mru_NxMtyW6TAytKgwm5u2RYlYwF4YsEIVSrYDZMaVc8VLblXPlOmZ5UmxkP9P9ynJ9cR5fKxk7EIXQGQIV9wsXL_TtxY6JE_t4W_iO5wv_yURA6uWLhYLMuicrAdi_-2RAMCUGgTReUC8gUTB9mueC2WK1ckq4j9AhVytiPHDX_Fh_-PXBVvhcsdEHl7fSXZkeTHIgtdKp7c3UegUjRYjfM3hQ9ZjpOty407efbF5dyOnxssWYXlcWlqC7sBmDz3Kl575-k8bGIXvdMuvn7uKo_Zx3Ayv_Mnn-7FaH4X2Mo0m6gPyWObR5P5g2q0dc9q6fVeS8uMdifttRxvOg_DKf-ydkEHZBuj_ayZgeFZw472JfuoTb6niZPzyP78jTvtxdpUp-o0q6tWVl87c2dtBfrGan3Ip3Mn-hqkm9Ff3xbGp_6HLwqvWwOtDnFqZvAYmMPOxgyehTW8T7oZzy1fU8yhAXuW6La9xPJ5arW0fNLiWTfTamCnmsED2DlJd6BzM3DA1gBbPQVbDv444Qw6iTXgKfjxwC43B5QbyDzPgrXRNlAm0MZoW0nX5_B06Ak-Mc-k10L7kQe81M3gHvYAz0CvkTwAvC2IOdDT7kADDq0MdSHvxMp0XnAJeXyLrQCx8hTxj3J6L2Igbp5KDIRbSNw6YSPcuDfsI5ac8KI80yFWX0AeitHuox4-pa-BpoEvzKMOOSMfWDeBGIFXwP4gzOE9cu71kF_FEpgf8AF-eYq4wQZ5z8A_2HtUF_LRwjXEaYFvrBnkA7rg00L9pCfjJYjHxNzmG8qbeBlgjndBwT1ypyCG7gtPngcY-aTd8TBPM-h41vfiiX6hjsAT9g3yw4t9ReLGdR_rSjUEOfBDtQRcyKUhSI4cwG_SNlTiD3vou5XiO2IB_zniBhusJeanORnHPpLcU92oZ9F3RjUiTizkDnx2BPUv4KK6Qc9RHIwbTGPZ632vCzqjDHlxEFOK9l3C-Yx1UiQ_XDtgkjUkf0MjR59QJ5XiEqZ-geMZasBzmds9YIK-xadPfIkenTsPsWPP_YYHB2OkxXlIqT6DopYDXaOa-1i_jvwW0JkiPHhG8AwUsfpYV6gF4tFzeXYQD9ZDo76kHoV1l3r5MYa9WtG3VA-sPfYKxW5xhbiRvYm9IqhX8HwO8Ix0UL8471hLOtd16mPip4N5UR6AgRdnJ8dvCMip1vCjbw3khfFS6h9lI-jswjnHnpY16yPHWdGPGeHzMcdZTj1J_d3B_JVRjvnopCv5wD7RVdLDPqG4kscTTpQNfvPgbI3g_f-pS4--a3TGUynH_hvJb9QpDzXJg3fo3eyld1Xq3YHjmbn23lTh7sm1m3Gpwur8Df2umMq16vtlyqLF5cpdurb4zb12Gfu522Dv-HruR_IWpQGmuDdhGMILvNQQq8TdXbwyVB3NP6dT1angaKxyUxqlXuaNf40L8qKWg8-W0XV9weQdDYPXzX4YqsprvXlQpru5Dbf0kRIMSsZ-u8wvGPydeNxPTk-LFSvjlLQEY96Ex_XBXxWv_mroRp6Yoej8hmmV0wnNB7MlEK81j3dT2PXZGxnyRJKBpOyDAYkq7Pb2FsLupzips3KnoPVOY-esXFPes7csrewtYA8Eb5lli1k19qOyAAkMMLxyEsZbuW70i5MMnRR8HntxFvErXiZhguMfmL8gPOXmB3DC-E8aEafNVzVqqEGQXtdRUAcDvq6ioopSr-97tugAqvcyOar3iy3VnZLanbb1T1jZfrjxo2mp8WSHsbv7Bx1mHBBZDAAA)): +Attachments can also be created inline ([demo](/playground/untitled#H4sIAAAAAAAAE71Wf3OaWBT9KoyTTnW3MS-I3dYmnWXVtnRAazRJzbozRSQEApiRhwKO333vuY8m225m_9yZGOT9OPfcc84D943UTfxGr_G7K6Xr3TVeNW7D2M8avT_3DVk-YAoDNF4vNB8e2tnWjyXGlm7mPzfurVPpp5JgGmeZtwkf5PtFupCxLzVvHa832rl2lElX-s2Xm2DZFNqp_hs-rZetd4v07ORpT3qmQHu7MF2td0BZp8k6z_xkvfXP902_pZ2_1_aYWEiqm0kN8I4r79qbdZ6umnq3q_2iNf22F4dE6qt2oimwdpim_uY6XMm7Fuo-IQT_iTD_CeGTHwZ38ieIJUFQRxirR1Xf39Dw0X5z0I72Af4tD61vvPNwWKQnqmfPTbduhsEd2J3vO_oBd3dc6fF2X7umNdWGf0vBRhSS6qoV7cCXfTXWfKmvWG61_si_vfU92Wz-E4RhsLhNIYinsox9QKGVd8-tuACCeKXRX12P-T_eKf7fhTq0Hvt-f3ailtSeoxJHRo1-58NoPe1UiBc1hkL8Yeh45y_vQ3mcuNl9T8s3cXPRWLnS7YWJG_gn2Tb4tUjid8jua-PVl08j_ab8I14mH8Llx0s5Tz5Err4ql52r_GYg0mVy1bEGZuD0ze64b5TWYFiM-16wSuJ4JT5vfVpDcztrcG_YkRU4s6HxufzDWF4XuVeJ1P10IbzBemt3Vp1V2e04ZXfrJd7Wicyd039brRIv_RIVu_nXi7X1cfL2sy66ztToUp1TO7qJ7NlwZ0f30pld5qNSVE5o6PbMojFHjgZB7oSicPpGteyLclQap7SvY0dXtM_LR1NT2JFHey3aaxa0VxCeYJ7RMHemoiCcgPZV9pR7o7kgcOjeGliYk9hjDZx8FAq6enwlTPSZj_vYPw9Il64dXdIY8ZmapzwfEd8-1ZyaxWhqkIZOibXUd-6Upqi1pD4uMicCV1GA_7zi73UN8BaF4sC8peJtMjfmjbHZBFwq5ov50qRaE0l96NZggnW4KqypYRAW-uhSz9ADvklwJF2J-5W0Z5fQPBhDX92R6I_0IFxRgDftge4l4dP-gH1hjD7uqU6fsOEZ9UNrCdPB-nys6uXgY6O3ZMd9sy5T9PghqrWHdjo4jB51CgLiKJaDYYA-7WgYONf1FbjkI-mE3EAfUY_rijfuJ_CVPaR50oe9JF7Q0pI8Dw3osxxYHdYPGbp2CnwHF8KvwJv2wEv0Z3ilQI6U9uwbZxbYJXvEmjjQjjCHkvNLvNg3yhzXQd1olamsT4IRrZmX0MUDpwL7R8zzHj7pSh9hPHFSHjLezKqAST51uC5zmtQ87skDUaneLokT5RbXkPWSYz53Abgjc8_o4KFGUZ-Hgv2Z1l5OTYM9D-HfUD0L-EwxH5wRnIG61gS-khfgY1bq7IAP_DA4l5xRuh9xlm8yGjutc8t-wHtkhWv3hc7aqGwiK5KzgvM5xRkZYn193uEln-su55j1GaIv7oM4iPrsVHiG0Dx7TR9-1lBfqFdwfvSd5LNL5xyZVp5NoHFZ57FkfiF6vKs4k5zvIfrX5xX6MXmt0gM5MTu8DjnhukrHHzTRd3jm0dma0_f_x5cxP9f4jBdqHvmbq2fUjzqcKh2Cp-yWj9ntcHanXmBXxhu7Q--eyjhfNFpaV7zgz4nWEUb7zUOhpevjjf_gu_KZ99pxFlZ-T3sttkmYqrco_26q35v0Ewzv5EZPbnL_8BfduWGMnyyN3q0bZ_7hb_7KG_L4CQAA)): ```svelte - - { - let frame = requestAnimationFrame(function loop(t) { - frame = requestAnimationFrame(loop); - paint(context, t); - }); - - return () => { - cancelAnimationFrame(frame); - }; + context.fillStyle = color; + context.fillRect(0, 0, canvas.width, canvas.height); }); }} > ``` +> [!NOTE] +> The nested effect runs whenever `color` changes, while the outer effect (where `canvas.getContext(...)` is called) only runs once, since it doesn't read any reactive state. + ## Passing attachments to components When used on a component, `{@attach ...}` will create a prop whose key is a [`Symbol`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol). If the component then [spreads](/tutorial/svelte/spread-props) props onto an element, the element will receive those attachments. From 623cfb06531873c9b765c77572dc2a2f2abece36 Mon Sep 17 00:00:00 2001 From: Scott Wu Date: Thu, 15 May 2025 06:01:24 +0800 Subject: [PATCH 044/259] docs: Clarify when is a variable proxied (#15804) * Update 02-$state.md * Update 02-$state.md * Update 02-$state.md * Update documentation/docs/02-runes/02-$state.md --------- Co-authored-by: Rich Harris --- documentation/docs/02-runes/02-$state.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/docs/02-runes/02-$state.md b/documentation/docs/02-runes/02-$state.md index 16630a977b..aac006d2f5 100644 --- a/documentation/docs/02-runes/02-$state.md +++ b/documentation/docs/02-runes/02-$state.md @@ -20,7 +20,7 @@ Unlike other frameworks you may have encountered, there is no API for interactin If `$state` is used with an array or a simple object, the result is a deeply reactive _state proxy_. [Proxies](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) allow Svelte to run code when you read or write properties, including via methods like `array.push(...)`, triggering granular updates. -> [!NOTE] Classes like `Set` and `Map` will not be proxied, but Svelte provides reactive implementations for various built-ins like these that can be imported from [`svelte/reactivity`](./svelte-reactivity). +> [!NOTE] Class instances are not proxied. You can create [reactive state fields](#Classes) on classes that you define. Svelte provides reactive implementations of built-ins like `Set` and `Map` that can be imported from [`svelte/reactivity`](svelte-reactivity). State is proxified recursively until Svelte finds something other than an array or simple object. In a case like this... From 0e22b541104783d5ac5079272cefe20d79de5fe7 Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Thu, 15 May 2025 00:08:36 +0200 Subject: [PATCH 045/259] fix: add `typeParams` to `SnippetBlock` for legacy parser (#15921) --- .changeset/polite-tips-enjoy.md | 5 + packages/svelte/src/compiler/legacy.js | 3 +- .../samples/generic-snippets/input.svelte | 10 + .../samples/generic-snippets/output.json | 244 ++++++++++++++++++ 4 files changed, 261 insertions(+), 1 deletion(-) create mode 100644 .changeset/polite-tips-enjoy.md create mode 100644 packages/svelte/tests/parser-legacy/samples/generic-snippets/input.svelte create mode 100644 packages/svelte/tests/parser-legacy/samples/generic-snippets/output.json diff --git a/.changeset/polite-tips-enjoy.md b/.changeset/polite-tips-enjoy.md new file mode 100644 index 0000000000..ab95388f7f --- /dev/null +++ b/.changeset/polite-tips-enjoy.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: add `typeParams` to `SnippetBlock` for legacy parser diff --git a/packages/svelte/src/compiler/legacy.js b/packages/svelte/src/compiler/legacy.js index e3f88c8f1d..f6b7e4b054 100644 --- a/packages/svelte/src/compiler/legacy.js +++ b/packages/svelte/src/compiler/legacy.js @@ -378,7 +378,8 @@ export function convert(source, ast) { end: node.end, expression: node.expression, parameters: node.parameters, - children: node.body.nodes.map((child) => visit(child)) + children: node.body.nodes.map((child) => visit(child)), + typeParams: node.typeParams }; }, // @ts-expect-error diff --git a/packages/svelte/tests/parser-legacy/samples/generic-snippets/input.svelte b/packages/svelte/tests/parser-legacy/samples/generic-snippets/input.svelte new file mode 100644 index 0000000000..4ee619728d --- /dev/null +++ b/packages/svelte/tests/parser-legacy/samples/generic-snippets/input.svelte @@ -0,0 +1,10 @@ + + +{#snippet generic(val: T)} + {val} +{/snippet} + +{#snippet complex_generic">>(val: T)} + {val} +{/snippet} \ No newline at end of file diff --git a/packages/svelte/tests/parser-legacy/samples/generic-snippets/output.json b/packages/svelte/tests/parser-legacy/samples/generic-snippets/output.json new file mode 100644 index 0000000000..37fb499e7b --- /dev/null +++ b/packages/svelte/tests/parser-legacy/samples/generic-snippets/output.json @@ -0,0 +1,244 @@ +{ + "html": { + "type": "Fragment", + "start": 30, + "end": 192, + "children": [ + { + "type": "Text", + "start": 28, + "end": 30, + "raw": "\n\n", + "data": "\n\n" + }, + { + "type": "SnippetBlock", + "start": 30, + "end": 92, + "expression": { + "type": "Identifier", + "start": 40, + "end": 47, + "name": "generic" + }, + "parameters": [ + { + "type": "Identifier", + "start": 66, + "end": 72, + "loc": { + "start": { + "line": 4, + "column": 36 + }, + "end": { + "line": 4, + "column": 42 + } + }, + "name": "val", + "typeAnnotation": { + "type": "TSTypeAnnotation", + "start": 69, + "end": 72, + "loc": { + "start": { + "line": 4, + "column": 39 + }, + "end": { + "line": 4, + "column": 42 + } + }, + "typeAnnotation": { + "type": "TSTypeReference", + "start": 71, + "end": 72, + "loc": { + "start": { + "line": 4, + "column": 41 + }, + "end": { + "line": 4, + "column": 42 + } + }, + "typeName": { + "type": "Identifier", + "start": 71, + "end": 72, + "loc": { + "start": { + "line": 4, + "column": 41 + }, + "end": { + "line": 4, + "column": 42 + } + }, + "name": "T" + } + } + } + } + ], + "children": [ + { + "type": "MustacheTag", + "start": 76, + "end": 81, + "expression": { + "type": "Identifier", + "start": 77, + "end": 80, + "loc": { + "start": { + "line": 5, + "column": 2 + }, + "end": { + "line": 5, + "column": 5 + } + }, + "name": "val" + } + } + ], + "typeParams": "T extends string" + }, + { + "type": "Text", + "start": 92, + "end": 94, + "raw": "\n\n", + "data": "\n\n" + }, + { + "type": "SnippetBlock", + "start": 94, + "end": 192, + "expression": { + "type": "Identifier", + "start": 104, + "end": 119, + "name": "complex_generic" + }, + "parameters": [ + { + "type": "Identifier", + "start": 166, + "end": 172, + "loc": { + "start": { + "line": 8, + "column": 72 + }, + "end": { + "line": 8, + "column": 78 + } + }, + "name": "val", + "typeAnnotation": { + "type": "TSTypeAnnotation", + "start": 169, + "end": 172, + "loc": { + "start": { + "line": 8, + "column": 75 + }, + "end": { + "line": 8, + "column": 78 + } + }, + "typeAnnotation": { + "type": "TSTypeReference", + "start": 171, + "end": 172, + "loc": { + "start": { + "line": 8, + "column": 77 + }, + "end": { + "line": 8, + "column": 78 + } + }, + "typeName": { + "type": "Identifier", + "start": 171, + "end": 172, + "loc": { + "start": { + "line": 8, + "column": 77 + }, + "end": { + "line": 8, + "column": 78 + } + }, + "name": "T" + } + } + } + } + ], + "children": [ + { + "type": "MustacheTag", + "start": 176, + "end": 181, + "expression": { + "type": "Identifier", + "start": 177, + "end": 180, + "loc": { + "start": { + "line": 9, + "column": 2 + }, + "end": { + "line": 9, + "column": 5 + } + }, + "name": "val" + } + } + ], + "typeParams": "T extends { bracket: \"<\" } | \"<\" | Set<\"<>\">" + } + ] + }, + "instance": { + "type": "Script", + "start": 0, + "end": 28, + "context": "default", + "content": { + "type": "Program", + "start": 18, + "end": 19, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 2, + "column": 0 + } + }, + "body": [], + "sourceType": "module" + } + } +} From 271fd336340acaa28007f426fa76d90a1fc904c7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 14 May 2025 18:27:24 -0400 Subject: [PATCH 046/259] Version Packages (#15922) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/polite-tips-enjoy.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/polite-tips-enjoy.md diff --git a/.changeset/polite-tips-enjoy.md b/.changeset/polite-tips-enjoy.md deleted file mode 100644 index ab95388f7f..0000000000 --- a/.changeset/polite-tips-enjoy.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: add `typeParams` to `SnippetBlock` for legacy parser diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index b25235276b..2b65a1889f 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.30.1 + +### Patch Changes + +- fix: add `typeParams` to `SnippetBlock` for legacy parser ([#15921](https://github.com/sveltejs/svelte/pull/15921)) + ## 5.30.0 ### Minor Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 5c27d88952..b6e8d06c63 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.30.0", + "version": "5.30.1", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 69492b1bf2..b849318036 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.30.0'; +export const VERSION = '5.30.1'; export const PUBLIC_VERSION = '5'; From ae71152013ef47cb917c371156a69224248d174c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 15 May 2025 11:18:55 -0400 Subject: [PATCH 047/259] docs: clarify keyed each blocks (#15923) --- documentation/docs/03-template-syntax/03-each.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/documentation/docs/03-template-syntax/03-each.md b/documentation/docs/03-template-syntax/03-each.md index 70666f6a57..006cadd152 100644 --- a/documentation/docs/03-template-syntax/03-each.md +++ b/documentation/docs/03-template-syntax/03-each.md @@ -43,7 +43,9 @@ An each block can also specify an _index_, equivalent to the second argument in {#each expression as name, index (key)}...{/each} ``` -If a _key_ expression is provided — which must uniquely identify each list item — Svelte will use it to diff the list when data changes, rather than adding or removing items at the end. The key can be any object, but strings and numbers are recommended since they allow identity to persist when the objects themselves change. +If a _key_ expression is provided — which must uniquely identify each list item — Svelte will use it to intelligently update the list when data changes by inserting, moving and deleting items, rather than adding or removing items at the end and updating the state in the middle. + +The key can be any object, but strings and numbers are recommended since they allow identity to persist when the objects themselves change. ```svelte {#each items as item (item.id)} From 17a7cf51e44dfb7704e6b2559e14e63f22972cd3 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 15 May 2025 11:19:09 -0400 Subject: [PATCH 048/259] docs: clarify ordering of attributes with spread (#15917) --- documentation/docs/03-template-syntax/01-basic-markup.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/documentation/docs/03-template-syntax/01-basic-markup.md b/documentation/docs/03-template-syntax/01-basic-markup.md index fe5f8b02aa..feecfe033e 100644 --- a/documentation/docs/03-template-syntax/01-basic-markup.md +++ b/documentation/docs/03-template-syntax/01-basic-markup.md @@ -82,12 +82,14 @@ As with elements, `name={name}` can be replaced with the `{name}` shorthand. ``` +## Spread attributes + _Spread attributes_ allow many attributes or properties to be passed to an element or component at once. -An element or component can have multiple spread attributes, interspersed with regular ones. +An element or component can have multiple spread attributes, interspersed with regular ones. Order matters — if `things.a` exists it will take precedence over `a="b"`, while `c="d"` would take precedence over `things.c`: ```svelte - + ``` ## Events From a5a0b49003d9cb544ed7919a4320870bda237833 Mon Sep 17 00:00:00 2001 From: Matteo Battista Date: Thu, 15 May 2025 18:14:18 +0200 Subject: [PATCH 049/259] fix: update_branch with (anchor).data possible undefined on ios devices (#15851) * fix: update_branch with (anchor).data possible undefined * error sooner * add tests * changeset --------- Co-authored-by: Rich Harris --- .changeset/silly-apples-remain.md | 5 +++++ .../svelte/src/internal/client/dom/blocks/each.js | 3 ++- .../svelte/src/internal/client/dom/blocks/if.js | 4 +++- .../svelte/src/internal/client/dom/hydration.js | 13 +++++++++++++ .../samples/cloudflare-mirage-borking-2/_config.js | 6 ++++++ .../cloudflare-mirage-borking-2/_expected.html | 1 + .../cloudflare-mirage-borking-2/_override.html | 1 + .../samples/cloudflare-mirage-borking-2/main.svelte | 5 +++++ .../samples/cloudflare-mirage-borking/_config.js | 6 ++++++ .../cloudflare-mirage-borking/_expected.html | 1 + .../cloudflare-mirage-borking/_override.html | 1 + .../samples/cloudflare-mirage-borking/main.svelte | 9 +++++++++ 12 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 .changeset/silly-apples-remain.md create mode 100644 packages/svelte/tests/hydration/samples/cloudflare-mirage-borking-2/_config.js create mode 100644 packages/svelte/tests/hydration/samples/cloudflare-mirage-borking-2/_expected.html create mode 100644 packages/svelte/tests/hydration/samples/cloudflare-mirage-borking-2/_override.html create mode 100644 packages/svelte/tests/hydration/samples/cloudflare-mirage-borking-2/main.svelte create mode 100644 packages/svelte/tests/hydration/samples/cloudflare-mirage-borking/_config.js create mode 100644 packages/svelte/tests/hydration/samples/cloudflare-mirage-borking/_expected.html create mode 100644 packages/svelte/tests/hydration/samples/cloudflare-mirage-borking/_override.html create mode 100644 packages/svelte/tests/hydration/samples/cloudflare-mirage-borking/main.svelte diff --git a/.changeset/silly-apples-remain.md b/.changeset/silly-apples-remain.md new file mode 100644 index 0000000000..10d43db550 --- /dev/null +++ b/.changeset/silly-apples-remain.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: handle more hydration mismatches diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index 92c953b541..2997664fa2 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -12,6 +12,7 @@ import { hydrate_next, hydrate_node, hydrating, + read_hydration_instruction, remove_nodes, set_hydrate_node, set_hydrating @@ -160,7 +161,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f let mismatch = false; if (hydrating) { - var is_else = /** @type {Comment} */ (anchor).data === HYDRATION_START_ELSE; + var is_else = read_hydration_instruction(anchor) === HYDRATION_START_ELSE; if (is_else !== (length === 0)) { // hydration mismatch — remove the server-rendered DOM and start over diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js index 925abb9d9d..bf1098c3f4 100644 --- a/packages/svelte/src/internal/client/dom/blocks/if.js +++ b/packages/svelte/src/internal/client/dom/blocks/if.js @@ -4,6 +4,7 @@ import { hydrate_next, hydrate_node, hydrating, + read_hydration_instruction, remove_nodes, set_hydrate_node, set_hydrating @@ -56,7 +57,8 @@ export function if_block(node, fn, [root_index, hydrate_index] = [0, 0]) { if (hydrating && hydrate_index !== -1) { if (root_index === 0) { - const data = /** @type {Comment} */ (anchor).data; + const data = read_hydration_instruction(anchor); + if (data === HYDRATION_START) { hydrate_index = 0; } else if (data === HYDRATION_START_ELSE) { diff --git a/packages/svelte/src/internal/client/dom/hydration.js b/packages/svelte/src/internal/client/dom/hydration.js index 8523ff97d5..ab3256da82 100644 --- a/packages/svelte/src/internal/client/dom/hydration.js +++ b/packages/svelte/src/internal/client/dom/hydration.js @@ -103,3 +103,16 @@ export function remove_nodes() { node = next; } } + +/** + * + * @param {TemplateNode} node + */ +export function read_hydration_instruction(node) { + if (!node || node.nodeType !== 8) { + w.hydration_mismatch(); + throw HYDRATION_ERROR; + } + + return /** @type {Comment} */ (node).data; +} diff --git a/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking-2/_config.js b/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking-2/_config.js new file mode 100644 index 0000000000..56ba73b064 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking-2/_config.js @@ -0,0 +1,6 @@ +import { test } from '../../test'; + +// https://github.com/sveltejs/svelte/issues/15819 +export default test({ + expect_hydration_error: true +}); diff --git a/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking-2/_expected.html b/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking-2/_expected.html new file mode 100644 index 0000000000..5179fb04a5 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking-2/_expected.html @@ -0,0 +1 @@ +

start

cond

diff --git a/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking-2/_override.html b/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking-2/_override.html new file mode 100644 index 0000000000..2a1c323288 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking-2/_override.html @@ -0,0 +1 @@ +

start

cond

diff --git a/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking-2/main.svelte b/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking-2/main.svelte new file mode 100644 index 0000000000..bfb4f2cdb8 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking-2/main.svelte @@ -0,0 +1,5 @@ + + +

start

{#if cond}

cond

{/if} diff --git a/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking/_config.js b/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking/_config.js new file mode 100644 index 0000000000..56ba73b064 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking/_config.js @@ -0,0 +1,6 @@ +import { test } from '../../test'; + +// https://github.com/sveltejs/svelte/issues/15819 +export default test({ + expect_hydration_error: true +}); diff --git a/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking/_expected.html b/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking/_expected.html new file mode 100644 index 0000000000..f6c03b87c1 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking/_expected.html @@ -0,0 +1 @@ +

start

pre123 mid diff --git a/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking/_override.html b/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking/_override.html new file mode 100644 index 0000000000..c84efbb00b --- /dev/null +++ b/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking/_override.html @@ -0,0 +1 @@ +

start

pre123 mid diff --git a/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking/main.svelte b/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking/main.svelte new file mode 100644 index 0000000000..2c9a94686e --- /dev/null +++ b/packages/svelte/tests/hydration/samples/cloudflare-mirage-borking/main.svelte @@ -0,0 +1,9 @@ + + +

start

+pre123 +{#if cond} +mid +{/if} From 60b22ab933c45ce329f4e7919be0b0b73a65ecbb Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Sat, 17 May 2025 10:07:21 +0200 Subject: [PATCH 050/259] fix: falsy attachments types (#15939) --- .changeset/chilled-otters-hear.md | 5 +++++ packages/svelte/elements.d.ts | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 .changeset/chilled-otters-hear.md diff --git a/.changeset/chilled-otters-hear.md b/.changeset/chilled-otters-hear.md new file mode 100644 index 0000000000..53a9872d96 --- /dev/null +++ b/.changeset/chilled-otters-hear.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: falsy attachments types diff --git a/packages/svelte/elements.d.ts b/packages/svelte/elements.d.ts index c637137365..237e96c699 100644 --- a/packages/svelte/elements.d.ts +++ b/packages/svelte/elements.d.ts @@ -863,8 +863,8 @@ export interface HTMLAttributes extends AriaAttributes, D // allow any data- attribute [key: `data-${string}`]: any; - // allow any attachment - [key: symbol]: Attachment; + // allow any attachment and falsy values (by using false we prevent the usage of booleans values by themselves) + [key: symbol]: Attachment | false | undefined | null; } export type HTMLAttributeAnchorTarget = '_self' | '_blank' | '_parent' | '_top' | (string & {}); From c7e4b8e765ee3e5f184d736ed401e63124065753 Mon Sep 17 00:00:00 2001 From: pengqiseven <134899215+pengqiseven@users.noreply.github.com> Date: Sat, 17 May 2025 16:10:54 +0800 Subject: [PATCH 051/259] chore: remove redundant word in comment (#15942) Signed-off-by: pengqiseven <912170095@qq.com> --- .../svelte/src/compiler/phases/2-analyze/visitors/Attribute.js | 2 +- .../phases/3-transform/client/visitors/RegularElement.js | 2 +- packages/svelte/src/internal/client/dom/elements/class.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js index 3ba81767cc..773aa59744 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js @@ -211,7 +211,7 @@ function get_delegated_event(event_name, handler, context) { if ( binding !== null && - // Bail out if the the binding is a rest param + // Bail out if the binding is a rest param (binding.declaration_kind === 'rest_param' || // Bail out if we reference anything from the EachBlock (for now) that mutates in non-runes mode, (((!context.state.analysis.runes && binding.kind === 'each') || 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 ab981878ad..1d39289f3d 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 @@ -564,7 +564,7 @@ export function build_style_directives_object(style_directives, context) { /** * Serializes an assignment to an element property by adding relevant statements to either only - * the init or the the init and update arrays, depending on whether or not the value is dynamic. + * the init or the init and update arrays, depending on whether or not the value is dynamic. * Resulting code for static looks something like this: * ```js * element.property = value; diff --git a/packages/svelte/src/internal/client/dom/elements/class.js b/packages/svelte/src/internal/client/dom/elements/class.js index fc081b8956..038ce33f3e 100644 --- a/packages/svelte/src/internal/client/dom/elements/class.js +++ b/packages/svelte/src/internal/client/dom/elements/class.js @@ -24,7 +24,7 @@ export function set_class(dom, is_html, value, hash, prev_classes, next_classes) if (!hydrating || next_class_name !== dom.getAttribute('class')) { // Removing the attribute when the value is only an empty string causes // performance issues vs simply making the className an empty string. So - // we should only remove the class if the the value is nullish + // we should only remove the class if the value is nullish // and there no hash/directives : if (next_class_name == null) { dom.removeAttribute('class'); From c365634ace068698e29c0331399f3a8c03ad5467 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 17 May 2025 05:04:39 -0400 Subject: [PATCH 052/259] Version Packages (#15931) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/chilled-otters-hear.md | 5 ----- .changeset/silly-apples-remain.md | 5 ----- packages/svelte/CHANGELOG.md | 8 ++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 5 files changed, 10 insertions(+), 12 deletions(-) delete mode 100644 .changeset/chilled-otters-hear.md delete mode 100644 .changeset/silly-apples-remain.md diff --git a/.changeset/chilled-otters-hear.md b/.changeset/chilled-otters-hear.md deleted file mode 100644 index 53a9872d96..0000000000 --- a/.changeset/chilled-otters-hear.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: falsy attachments types diff --git a/.changeset/silly-apples-remain.md b/.changeset/silly-apples-remain.md deleted file mode 100644 index 10d43db550..0000000000 --- a/.changeset/silly-apples-remain.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: handle more hydration mismatches diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 2b65a1889f..9d5cfe1334 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,13 @@ # svelte +## 5.30.2 + +### Patch Changes + +- fix: falsy attachments types ([#15939](https://github.com/sveltejs/svelte/pull/15939)) + +- fix: handle more hydration mismatches ([#15851](https://github.com/sveltejs/svelte/pull/15851)) + ## 5.30.1 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index b6e8d06c63..4ec70a88b5 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.30.1", + "version": "5.30.2", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index b849318036..a5cc8b7191 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.30.1'; +export const VERSION = '5.30.2'; export const PUBLIC_VERSION = '5'; From b2ce3fa85ed77ba173da9ab27d7bc36f17b7e9c7 Mon Sep 17 00:00:00 2001 From: Mateusz Kadlubowski Date: Sun, 18 May 2025 16:25:11 +0800 Subject: [PATCH 053/259] (fix/ast types) fix: Add missing `AttachTag` in `Tag` union type inside the `AST` namespace from `"svelte/compiler"` (#15946) --- .changeset/funny-carrots-teach.md | 5 +++++ packages/svelte/src/compiler/types/template.d.ts | 8 +++++++- packages/svelte/types/index.d.ts | 8 +++++++- 3 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 .changeset/funny-carrots-teach.md diff --git a/.changeset/funny-carrots-teach.md b/.changeset/funny-carrots-teach.md new file mode 100644 index 0000000000..53ff135e84 --- /dev/null +++ b/.changeset/funny-carrots-teach.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: Add missing `AttachTag` in `Tag` union type inside the `AST` namespace from `"svelte/compiler"` diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index 6dec1f2dbe..b51c9e9a8d 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -547,7 +547,13 @@ export namespace AST { | AST.SvelteWindow | AST.SvelteBoundary; - export type Tag = AST.ExpressionTag | AST.HtmlTag | AST.ConstTag | AST.DebugTag | AST.RenderTag; + export type Tag = + | AST.AttachTag + | AST.ConstTag + | AST.DebugTag + | AST.ExpressionTag + | AST.HtmlTag + | AST.RenderTag; export type TemplateNode = | AST.Root diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index bb958c5108..1fda9a36b8 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -1362,7 +1362,13 @@ declare module 'svelte/compiler' { | AST.SvelteWindow | AST.SvelteBoundary; - export type Tag = AST.ExpressionTag | AST.HtmlTag | AST.ConstTag | AST.DebugTag | AST.RenderTag; + export type Tag = + | AST.AttachTag + | AST.ConstTag + | AST.DebugTag + | AST.ExpressionTag + | AST.HtmlTag + | AST.RenderTag; export type TemplateNode = | AST.Root From 42e7e8168d01f2510c73bf23bec1e0a6d9e44a66 Mon Sep 17 00:00:00 2001 From: Christian Decker <50999401+cmdecker95@users.noreply.github.com> Date: Sun, 18 May 2025 04:50:12 -0400 Subject: [PATCH 054/259] chore: fix docs typo in 02-context.md (#15944) --- documentation/docs/06-runtime/02-context.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/docs/06-runtime/02-context.md b/documentation/docs/06-runtime/02-context.md index 4204bcfe6d..f395de421c 100644 --- a/documentation/docs/06-runtime/02-context.md +++ b/documentation/docs/06-runtime/02-context.md @@ -125,7 +125,7 @@ In many cases this is perfectly fine, but there is a risk: if you mutate the sta ```svelte + + diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-3/_config.js b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-3/_config.js new file mode 100644 index 0000000000..dd847ce2f2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-3/_config.js @@ -0,0 +1,13 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ``, + ssrHtml: ``, + + async test({ assert, target }) { + flushSync(); + + assert.htmlEqual(target.innerHTML, ``); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-3/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-3/main.svelte new file mode 100644 index 0000000000..47b8c901eb --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-closure-private-3/main.svelte @@ -0,0 +1,12 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-conflicting-get-name/_config.js b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-conflicting-get-name/_config.js new file mode 100644 index 0000000000..f47bee71df --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-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-constructor-conflicting-get-name/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-conflicting-get-name/main.svelte new file mode 100644 index 0000000000..e2c4f302b3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-conflicting-get-name/main.svelte @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-derived-unowned/_config.js b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-derived-unowned/_config.js new file mode 100644 index 0000000000..4cf1aea213 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-derived-unowned/_config.js @@ -0,0 +1,45 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + // The component context class instance gets shared between tests, strangely, causing hydration to fail? + mode: ['client', 'server'], + + async test({ assert, target, logs }) { + const btn = target.querySelector('button'); + + flushSync(() => { + btn?.click(); + }); + + assert.deepEqual(logs, [0, 'class trigger false', 'local trigger false', 1]); + + flushSync(() => { + btn?.click(); + }); + + assert.deepEqual(logs, [0, 'class trigger false', 'local trigger false', 1, 2]); + + flushSync(() => { + btn?.click(); + }); + + assert.deepEqual(logs, [0, 'class trigger false', 'local trigger false', 1, 2, 3]); + + flushSync(() => { + btn?.click(); + }); + + assert.deepEqual(logs, [ + 0, + 'class trigger false', + 'local trigger false', + 1, + 2, + 3, + 4, + 'class trigger true', + 'local trigger true' + ]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-derived-unowned/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-derived-unowned/main.svelte new file mode 100644 index 0000000000..03687d01bb --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-derived-unowned/main.svelte @@ -0,0 +1,37 @@ + + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-predeclared-field/_config.js b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-predeclared-field/_config.js new file mode 100644 index 0000000000..02cf36d900 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-predeclared-field/_config.js @@ -0,0 +1,20 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ``, + + test({ assert, target }) { + const btn = target.querySelector('button'); + + flushSync(() => { + btn?.click(); + }); + assert.htmlEqual(target.innerHTML, ``); + + flushSync(() => { + btn?.click(); + }); + assert.htmlEqual(target.innerHTML, ``); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-predeclared-field/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-predeclared-field/main.svelte new file mode 100644 index 0000000000..5dbbb10afd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-predeclared-field/main.svelte @@ -0,0 +1,12 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-subclass/_config.js b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-subclass/_config.js new file mode 100644 index 0000000000..32cca6c693 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-subclass/_config.js @@ -0,0 +1,20 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ``, + + test({ assert, target }) { + const btn = target.querySelector('button'); + + flushSync(() => { + btn?.click(); + }); + assert.htmlEqual(target.innerHTML, ``); + + flushSync(() => { + btn?.click(); + }); + assert.htmlEqual(target.innerHTML, ``); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor-subclass/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-subclass/main.svelte new file mode 100644 index 0000000000..d8feb554cd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor-subclass/main.svelte @@ -0,0 +1,22 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor/_config.js b/packages/svelte/tests/runtime-runes/samples/class-state-constructor/_config.js new file mode 100644 index 0000000000..f35dc57228 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor/_config.js @@ -0,0 +1,20 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ``, + + test({ assert, target }) { + const btn = target.querySelector('button'); + + flushSync(() => { + btn?.click(); + }); + assert.htmlEqual(target.innerHTML, ``); + + flushSync(() => { + btn?.click(); + }); + assert.htmlEqual(target.innerHTML, ``); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-constructor/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-state-constructor/main.svelte new file mode 100644 index 0000000000..aa8ba1658b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-state-constructor/main.svelte @@ -0,0 +1,18 @@ + + + diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-1/errors.json b/packages/svelte/tests/validator/samples/class-state-constructor-1/errors.json new file mode 100644 index 0000000000..82765c51c1 --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-1/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "state_field_duplicate", + "message": "`count` has already been declared on this class", + "start": { + "line": 5, + "column": 2 + }, + "end": { + "line": 5, + "column": 24 + } + } +] diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-1/input.svelte.js b/packages/svelte/tests/validator/samples/class-state-constructor-1/input.svelte.js new file mode 100644 index 0000000000..05cd4d9d9d --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-1/input.svelte.js @@ -0,0 +1,7 @@ +export class Counter { + count = $state(0); + + constructor() { + this.count = $state(0); + } +} diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-10/errors.json b/packages/svelte/tests/validator/samples/class-state-constructor-10/errors.json new file mode 100644 index 0000000000..c4cb0991d0 --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-10/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "state_field_invalid_assignment", + "message": "Cannot assign to a state field before its declaration", + "start": { + "line": 4, + "column": 3 + }, + "end": { + "line": 4, + "column": 18 + } + } +] diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-10/input.svelte.js b/packages/svelte/tests/validator/samples/class-state-constructor-10/input.svelte.js new file mode 100644 index 0000000000..e5ad562727 --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-10/input.svelte.js @@ -0,0 +1,9 @@ +export class Counter { + constructor() { + if (true) { + this.count = -1; + } + + this.count = $state(0); + } +} diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-2/errors.json b/packages/svelte/tests/validator/samples/class-state-constructor-2/errors.json new file mode 100644 index 0000000000..82765c51c1 --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-2/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "state_field_duplicate", + "message": "`count` has already been declared on this class", + "start": { + "line": 5, + "column": 2 + }, + "end": { + "line": 5, + "column": 24 + } + } +] diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-2/input.svelte.js b/packages/svelte/tests/validator/samples/class-state-constructor-2/input.svelte.js new file mode 100644 index 0000000000..e37be4b3e6 --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-2/input.svelte.js @@ -0,0 +1,7 @@ +export class Counter { + constructor() { + this.count = $state(0); + this.count = 1; + this.count = $state(0); + } +} diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-3/errors.json b/packages/svelte/tests/validator/samples/class-state-constructor-3/errors.json new file mode 100644 index 0000000000..175c41f98c --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-3/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "state_field_duplicate", + "message": "`count` has already been declared on this class", + "start": { + "line": 5, + "column": 2 + }, + "end": { + "line": 5, + "column": 28 + } + } +] diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-3/input.svelte.js b/packages/svelte/tests/validator/samples/class-state-constructor-3/input.svelte.js new file mode 100644 index 0000000000..f9196ff3cd --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-3/input.svelte.js @@ -0,0 +1,7 @@ +export class Counter { + constructor() { + this.count = $state(0); + this.count = 1; + this.count = $state.raw(0); + } +} diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-4/errors.json b/packages/svelte/tests/validator/samples/class-state-constructor-4/errors.json new file mode 100644 index 0000000000..9f959874c8 --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-4/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "state_invalid_placement", + "message": "`$state(...)` can only be used as a variable declaration initializer, a class field declaration, or the first assignment to a class field at the top level of the constructor.", + "start": { + "line": 4, + "column": 16 + }, + "end": { + "line": 4, + "column": 25 + } + } +] diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-4/input.svelte.js b/packages/svelte/tests/validator/samples/class-state-constructor-4/input.svelte.js new file mode 100644 index 0000000000..bf1aada1b5 --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-4/input.svelte.js @@ -0,0 +1,7 @@ +export class Counter { + constructor() { + if (true) { + this.count = $state(0); + } + } +} diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-5/errors.json b/packages/svelte/tests/validator/samples/class-state-constructor-5/errors.json new file mode 100644 index 0000000000..af2f30dade --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-5/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "state_field_duplicate", + "message": "`count` has already been declared on this class", + "start": { + "line": 5, + "column": 2 + }, + "end": { + "line": 5, + "column": 27 + } + } +] diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-5/input.svelte.js b/packages/svelte/tests/validator/samples/class-state-constructor-5/input.svelte.js new file mode 100644 index 0000000000..bc3d19a14f --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-5/input.svelte.js @@ -0,0 +1,7 @@ +export class Counter { + // prettier-ignore + 'count' = $state(0); + constructor() { + this['count'] = $state(0); + } +} diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-6/errors.json b/packages/svelte/tests/validator/samples/class-state-constructor-6/errors.json new file mode 100644 index 0000000000..ae7a47f31b --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-6/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "state_field_duplicate", + "message": "`count` has already been declared on this class", + "start": { + "line": 4, + "column": 2 + }, + "end": { + "line": 4, + "column": 27 + } + } +] diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-6/input.svelte.js b/packages/svelte/tests/validator/samples/class-state-constructor-6/input.svelte.js new file mode 100644 index 0000000000..2ebe52e685 --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-6/input.svelte.js @@ -0,0 +1,6 @@ +export class Counter { + count = $state(0); + constructor() { + this['count'] = $state(0); + } +} diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-7/errors.json b/packages/svelte/tests/validator/samples/class-state-constructor-7/errors.json new file mode 100644 index 0000000000..64e56f8d5c --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-7/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "state_invalid_placement", + "message": "`$state(...)` can only be used as a variable declaration initializer, a class field declaration, or the first assignment to a class field at the top level of the constructor.", + "start": { + "line": 5, + "column": 16 + }, + "end": { + "line": 5, + "column": 25 + } + } +] diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-7/input.svelte.js b/packages/svelte/tests/validator/samples/class-state-constructor-7/input.svelte.js new file mode 100644 index 0000000000..50c8559837 --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-7/input.svelte.js @@ -0,0 +1,7 @@ +const count = 'count'; + +export class Counter { + constructor() { + this[count] = $state(0); + } +} diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-8/errors.json b/packages/svelte/tests/validator/samples/class-state-constructor-8/errors.json new file mode 100644 index 0000000000..2e0bd10ff8 --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-8/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "state_field_invalid_assignment", + "message": "Cannot assign to a state field before its declaration", + "start": { + "line": 3, + "column": 2 + }, + "end": { + "line": 3, + "column": 17 + } + } +] diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-8/input.svelte.js b/packages/svelte/tests/validator/samples/class-state-constructor-8/input.svelte.js new file mode 100644 index 0000000000..0a76c6fec9 --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-8/input.svelte.js @@ -0,0 +1,6 @@ +export class Counter { + constructor() { + this.count = -1; + this.count = $state(0); + } +} diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-9/errors.json b/packages/svelte/tests/validator/samples/class-state-constructor-9/errors.json new file mode 100644 index 0000000000..b7dd4c8ed4 --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-9/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "state_field_invalid_assignment", + "message": "Cannot assign to a state field before its declaration", + "start": { + "line": 2, + "column": 1 + }, + "end": { + "line": 2, + "column": 12 + } + } +] diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-9/input.svelte.js b/packages/svelte/tests/validator/samples/class-state-constructor-9/input.svelte.js new file mode 100644 index 0000000000..a8469e13af --- /dev/null +++ b/packages/svelte/tests/validator/samples/class-state-constructor-9/input.svelte.js @@ -0,0 +1,7 @@ +export class Counter { + count = -1; + + constructor() { + this.count = $state(0); + } +} diff --git a/packages/svelte/tests/validator/samples/const-tag-invalid-rune-usage/errors.json b/packages/svelte/tests/validator/samples/const-tag-invalid-rune-usage/errors.json index 32594e4268..e1906b181a 100644 --- a/packages/svelte/tests/validator/samples/const-tag-invalid-rune-usage/errors.json +++ b/packages/svelte/tests/validator/samples/const-tag-invalid-rune-usage/errors.json @@ -1,7 +1,7 @@ [ { "code": "state_invalid_placement", - "message": "`$derived(...)` can only be used as a variable declaration initializer or a class field", + "message": "`$derived(...)` can only be used as a variable declaration initializer, a class field declaration, or the first assignment to a class field at the top level of the constructor.", "start": { "line": 2, "column": 15 From 21bf947ca8269ada01a2ec7962aeedc841026ac5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 19 May 2025 11:56:41 -0400 Subject: [PATCH 056/259] Version Packages (#15947) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/funny-carrots-teach.md | 5 ----- .changeset/mean-squids-scream.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/funny-carrots-teach.md delete mode 100644 .changeset/mean-squids-scream.md diff --git a/.changeset/funny-carrots-teach.md b/.changeset/funny-carrots-teach.md deleted file mode 100644 index 53ff135e84..0000000000 --- a/.changeset/funny-carrots-teach.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: Add missing `AttachTag` in `Tag` union type inside the `AST` namespace from `"svelte/compiler"` diff --git a/.changeset/mean-squids-scream.md b/.changeset/mean-squids-scream.md deleted file mode 100644 index 2157ea85a6..0000000000 --- a/.changeset/mean-squids-scream.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': minor ---- - -feat: allow state fields to be declared inside class constructors diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 9d5cfe1334..d40ff09317 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,15 @@ # svelte +## 5.31.0 + +### Minor Changes + +- feat: allow state fields to be declared inside class constructors ([#15820](https://github.com/sveltejs/svelte/pull/15820)) + +### Patch Changes + +- fix: Add missing `AttachTag` in `Tag` union type inside the `AST` namespace from `"svelte/compiler"` ([#15946](https://github.com/sveltejs/svelte/pull/15946)) + ## 5.30.2 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 4ec70a88b5..ee35cda2bd 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.30.2", + "version": "5.31.0", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index a5cc8b7191..ed99ef6795 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.30.2'; +export const VERSION = '5.31.0'; export const PUBLIC_VERSION = '5'; From b7b393d50fab48d4f38d0f72c5c177de5c236024 Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Mon, 19 May 2025 18:26:10 +0200 Subject: [PATCH 057/259] chore: watch for messages changes in dev generate script (#15950) * chore: watch for messages changes in dev generate script * no need for this to be async * tweak * guard * only create one timeout --------- Co-authored-by: Rich Harris --- packages/svelte/package.json | 2 +- .../svelte/scripts/process-messages/index.js | 680 +++++++++--------- 2 files changed, 357 insertions(+), 325 deletions(-) diff --git a/packages/svelte/package.json b/packages/svelte/package.json index ee35cda2bd..e0f6afe6fc 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -136,7 +136,7 @@ ], "scripts": { "build": "node scripts/process-messages && rollup -c && pnpm generate:types && node scripts/check-treeshakeability.js", - "dev": "node scripts/process-messages && rollup -cw", + "dev": "node scripts/process-messages -w & rollup -cw", "check": "tsc --project tsconfig.runtime.json && tsc && cd ./tests/types && tsc", "check:watch": "tsc --watch", "generate:version": "node ./scripts/generate-version.js", diff --git a/packages/svelte/scripts/process-messages/index.js b/packages/svelte/scripts/process-messages/index.js index 80619acfa7..81c59271de 100644 --- a/packages/svelte/scripts/process-messages/index.js +++ b/packages/svelte/scripts/process-messages/index.js @@ -1,409 +1,441 @@ // @ts-check +import process from 'node:process'; import fs from 'node:fs'; import * as acorn from 'acorn'; import { walk } from 'zimmerframe'; import * as esrap from 'esrap'; -/** @type {Record>} */ -const messages = {}; -const seen = new Set(); - const DIR = '../../documentation/docs/98-reference/.generated'; -fs.rmSync(DIR, { force: true, recursive: true }); -fs.mkdirSync(DIR); -for (const category of fs.readdirSync('messages')) { - if (category.startsWith('.')) continue; +const watch = process.argv.includes('-w'); - messages[category] = {}; +function run() { + /** @type {Record>} */ + const messages = {}; + const seen = new Set(); - for (const file of fs.readdirSync(`messages/${category}`)) { - if (!file.endsWith('.md')) continue; + fs.rmSync(DIR, { force: true, recursive: true }); + fs.mkdirSync(DIR); - const markdown = fs - .readFileSync(`messages/${category}/${file}`, 'utf-8') - .replace(/\r\n/g, '\n'); + for (const category of fs.readdirSync('messages')) { + if (category.startsWith('.')) continue; - const sorted = []; + messages[category] = {}; - for (const match of markdown.matchAll(/## ([\w]+)\n\n([^]+?)(?=$|\n\n## )/g)) { - const [_, code, text] = match; + for (const file of fs.readdirSync(`messages/${category}`)) { + if (!file.endsWith('.md')) continue; - if (seen.has(code)) { - throw new Error(`Duplicate message code ${category}/${code}`); - } + const markdown = fs + .readFileSync(`messages/${category}/${file}`, 'utf-8') + .replace(/\r\n/g, '\n'); - sorted.push({ code, _ }); + const sorted = []; - const sections = text.trim().split('\n\n'); - const details = []; + for (const match of markdown.matchAll(/## ([\w]+)\n\n([^]+?)(?=$|\n\n## )/g)) { + const [_, code, text] = match; - while (!sections[sections.length - 1].startsWith('> ')) { - details.unshift(/** @type {string} */ (sections.pop())); - } + if (seen.has(code)) { + throw new Error(`Duplicate message code ${category}/${code}`); + } + + sorted.push({ code, _ }); - if (sections.length === 0) { - throw new Error('No message text'); + const sections = text.trim().split('\n\n'); + const details = []; + + while (!sections[sections.length - 1].startsWith('> ')) { + details.unshift(/** @type {string} */ (sections.pop())); + } + + if (sections.length === 0) { + throw new Error('No message text'); + } + + seen.add(code); + messages[category][code] = { + messages: sections.map((section) => section.replace(/^> /gm, '').replace(/^>\n/gm, '\n')), + details: details.join('\n\n') + }; } - seen.add(code); - messages[category][code] = { - messages: sections.map((section) => section.replace(/^> /gm, '').replace(/^>\n/gm, '\n')), - details: details.join('\n\n') - }; + sorted.sort((a, b) => (a.code < b.code ? -1 : 1)); + + fs.writeFileSync( + `messages/${category}/${file}`, + sorted.map((x) => x._.trim()).join('\n\n') + '\n' + ); } - sorted.sort((a, b) => (a.code < b.code ? -1 : 1)); fs.writeFileSync( - `messages/${category}/${file}`, - sorted.map((x) => x._.trim()).join('\n\n') + '\n' + `${DIR}/${category}.md`, + '\n\n' + + Object.entries(messages[category]) + .map(([code, { messages, details }]) => { + const chunks = [ + `### ${code}`, + ...messages.map((message) => '```\n' + message + '\n```') + ]; + + if (details) { + chunks.push(details); + } + + return chunks.join('\n\n'); + }) + .sort() + .join('\n\n') + + '\n' ); } - fs.writeFileSync( - `${DIR}/${category}.md`, - '\n\n' + - Object.entries(messages[category]) - .map(([code, { messages, details }]) => { - const chunks = [`### ${code}`, ...messages.map((message) => '```\n' + message + '\n```')]; - - if (details) { - chunks.push(details); - } - - return chunks.join('\n\n'); - }) - .sort() - .join('\n\n') + - '\n' - ); -} - -/** - * @param {string} name - * @param {string} dest - */ -function transform(name, dest) { - const source = fs - .readFileSync(new URL(`./templates/${name}.js`, import.meta.url), 'utf-8') - .replace(/\r\n/g, '\n'); - /** - * @type {Array<{ - * type: string; - * value: string; - * start: number; - * end: number - * }>} + * @param {string} name + * @param {string} dest */ - const comments = []; - - let ast = acorn.parse(source, { - ecmaVersion: 'latest', - sourceType: 'module', - onComment: (block, value, start, end) => { - if (block && /\n/.test(value)) { - let a = start; - while (a > 0 && source[a - 1] !== '\n') a -= 1; - - let b = a; - while (/[ \t]/.test(source[b])) b += 1; - - const indentation = source.slice(a, b); - value = value.replace(new RegExp(`^${indentation}`, 'gm'), ''); - } - - comments.push({ type: block ? 'Block' : 'Line', value, start, end }); - } - }); + function transform(name, dest) { + const source = fs + .readFileSync(new URL(`./templates/${name}.js`, import.meta.url), 'utf-8') + .replace(/\r\n/g, '\n'); - ast = walk(ast, null, { - _(node, { next }) { - let comment; + /** + * @type {Array<{ + * type: string; + * value: string; + * start: number; + * end: number + * }>} + */ + const comments = []; + + let ast = acorn.parse(source, { + ecmaVersion: 'latest', + sourceType: 'module', + onComment: (block, value, start, end) => { + if (block && /\n/.test(value)) { + let a = start; + while (a > 0 && source[a - 1] !== '\n') a -= 1; + + let b = a; + while (/[ \t]/.test(source[b])) b += 1; + + const indentation = source.slice(a, b); + value = value.replace(new RegExp(`^${indentation}`, 'gm'), ''); + } - while (comments[0] && comments[0].start < node.start) { - comment = comments.shift(); - // @ts-expect-error - (node.leadingComments ||= []).push(comment); + comments.push({ type: block ? 'Block' : 'Line', value, start, end }); } + }); - next(); - - if (comments[0]) { - const slice = source.slice(node.end, comments[0].start); + ast = walk(ast, null, { + _(node, { next }) { + let comment; - if (/^[,) \t]*$/.test(slice)) { + while (comments[0] && comments[0].start < node.start) { + comment = comments.shift(); // @ts-expect-error - node.trailingComments = [comments.shift()]; + (node.leadingComments ||= []).push(comment); } - } - }, - // @ts-expect-error - Identifier(node, context) { - if (node.name === 'CODES') { - return { - type: 'ArrayExpression', - elements: Object.keys(messages[name]).map((code) => ({ - type: 'Literal', - value: code - })) - }; - } - } - }); - - if (comments.length > 0) { - // @ts-expect-error - (ast.trailingComments ||= []).push(...comments); - } - - const category = messages[name]; - - // find the `export function CODE` node - const index = ast.body.findIndex((node) => { - if ( - node.type === 'ExportNamedDeclaration' && - node.declaration && - node.declaration.type === 'FunctionDeclaration' - ) { - return node.declaration.id.name === 'CODE'; - } - }); - - if (index === -1) throw new Error(`missing export function CODE in ${name}.js`); - const template_node = ast.body[index]; - ast.body.splice(index, 1); + next(); - for (const code in category) { - const { messages } = category[code]; - /** @type {string[]} */ - const vars = []; + if (comments[0]) { + const slice = source.slice(node.end, comments[0].start); - const group = messages.map((text, i) => { - for (const match of text.matchAll(/%(\w+)%/g)) { - const name = match[1]; - if (!vars.includes(name)) { - vars.push(match[1]); + if (/^[,) \t]*$/.test(slice)) { + // @ts-expect-error + node.trailingComments = [comments.shift()]; + } + } + }, + // @ts-expect-error + Identifier(node, context) { + if (node.name === 'CODES') { + return { + type: 'ArrayExpression', + elements: Object.keys(messages[name]).map((code) => ({ + type: 'Literal', + value: code + })) + }; } } - - return { - text, - vars: vars.slice() - }; }); - /** @type {import('estree').Expression} */ - let message = { type: 'Literal', value: '' }; - let prev_vars; + if (comments.length > 0) { + // @ts-expect-error + (ast.trailingComments ||= []).push(...comments); + } - for (let i = 0; i < group.length; i += 1) { - const { text, vars } = group[i]; + const category = messages[name]; - if (vars.length === 0) { - message = { - type: 'Literal', - value: text - }; - prev_vars = vars; - continue; + // find the `export function CODE` node + const index = ast.body.findIndex((node) => { + if ( + node.type === 'ExportNamedDeclaration' && + node.declaration && + node.declaration.type === 'FunctionDeclaration' + ) { + return node.declaration.id.name === 'CODE'; } + }); - const parts = text.split(/(%\w+%)/); + if (index === -1) throw new Error(`missing export function CODE in ${name}.js`); - /** @type {import('estree').Expression[]} */ - const expressions = []; + const template_node = ast.body[index]; + ast.body.splice(index, 1); - /** @type {import('estree').TemplateElement[]} */ - const quasis = []; + for (const code in category) { + const { messages } = category[code]; + /** @type {string[]} */ + const vars = []; - for (let i = 0; i < parts.length; i += 1) { - const part = parts[i]; - if (i % 2 === 0) { - const str = part.replace(/(`|\${)/g, '\\$1'); - quasis.push({ - type: 'TemplateElement', - value: { raw: str, cooked: str }, - tail: i === parts.length - 1 - }); - } else { - expressions.push({ - type: 'Identifier', - name: part.slice(1, -1) - }); + const group = messages.map((text, i) => { + for (const match of text.matchAll(/%(\w+)%/g)) { + const name = match[1]; + if (!vars.includes(name)) { + vars.push(match[1]); + } } - } + + return { + text, + vars: vars.slice() + }; + }); /** @type {import('estree').Expression} */ - const expression = { - type: 'TemplateLiteral', - expressions, - quasis - }; - - if (prev_vars) { - if (vars.length === prev_vars.length) { - throw new Error('Message overloads must have new parameters'); + let message = { type: 'Literal', value: '' }; + let prev_vars; + + for (let i = 0; i < group.length; i += 1) { + const { text, vars } = group[i]; + + if (vars.length === 0) { + message = { + type: 'Literal', + value: text + }; + prev_vars = vars; + continue; } - message = { - type: 'ConditionalExpression', - test: { - type: 'Identifier', - name: vars[prev_vars.length] - }, - consequent: expression, - alternate: message - }; - } else { - message = expression; - } + const parts = text.split(/(%\w+%)/); - prev_vars = vars; - } + /** @type {import('estree').Expression[]} */ + const expressions = []; - const clone = walk(/** @type {import('estree').Node} */ (template_node), null, { - // @ts-expect-error Block is a block comment, which is not recognised - Block(node, context) { - if (!node.value.includes('PARAMETER')) return; - - const value = /** @type {string} */ (node.value) - .split('\n') - .map((line) => { - if (line === ' * MESSAGE') { - return messages[messages.length - 1] - .split('\n') - .map((line) => ` * ${line}`) - .join('\n'); - } + /** @type {import('estree').TemplateElement[]} */ + const quasis = []; - if (line.includes('PARAMETER')) { - return vars - .map((name, i) => { - const optional = i >= group[0].vars.length; + for (let i = 0; i < parts.length; i += 1) { + const part = parts[i]; + if (i % 2 === 0) { + const str = part.replace(/(`|\${)/g, '\\$1'); + quasis.push({ + type: 'TemplateElement', + value: { raw: str, cooked: str }, + tail: i === parts.length - 1 + }); + } else { + expressions.push({ + type: 'Identifier', + name: part.slice(1, -1) + }); + } + } - return optional - ? ` * @param {string | undefined | null} [${name}]` - : ` * @param {string} ${name}`; - }) - .join('\n'); - } + /** @type {import('estree').Expression} */ + const expression = { + type: 'TemplateLiteral', + expressions, + quasis + }; - return line; - }) - .filter((x) => x !== '') - .join('\n'); + if (prev_vars) { + if (vars.length === prev_vars.length) { + throw new Error('Message overloads must have new parameters'); + } - if (value !== node.value) { - return { ...node, value }; + message = { + type: 'ConditionalExpression', + test: { + type: 'Identifier', + name: vars[prev_vars.length] + }, + consequent: expression, + alternate: message + }; + } else { + message = expression; } - }, - FunctionDeclaration(node, context) { - if (node.id.name !== 'CODE') return; - const params = []; + prev_vars = vars; + } - for (const param of node.params) { - if (param.type === 'Identifier' && param.name === 'PARAMETER') { - params.push(...vars.map((name) => ({ type: 'Identifier', name }))); - } else { - params.push(param); + const clone = walk(/** @type {import('estree').Node} */ (template_node), null, { + // @ts-expect-error Block is a block comment, which is not recognised + Block(node, context) { + if (!node.value.includes('PARAMETER')) return; + + const value = /** @type {string} */ (node.value) + .split('\n') + .map((line) => { + if (line === ' * MESSAGE') { + return messages[messages.length - 1] + .split('\n') + .map((line) => ` * ${line}`) + .join('\n'); + } + + if (line.includes('PARAMETER')) { + return vars + .map((name, i) => { + const optional = i >= group[0].vars.length; + + return optional + ? ` * @param {string | undefined | null} [${name}]` + : ` * @param {string} ${name}`; + }) + .join('\n'); + } + + return line; + }) + .filter((x) => x !== '') + .join('\n'); + + if (value !== node.value) { + return { ...node, value }; } - } + }, + FunctionDeclaration(node, context) { + if (node.id.name !== 'CODE') return; + + const params = []; - return /** @type {import('estree').FunctionDeclaration} */ ({ - .../** @type {import('estree').FunctionDeclaration} */ (context.next()), - params, - id: { - ...node.id, - name: code + for (const param of node.params) { + if (param.type === 'Identifier' && param.name === 'PARAMETER') { + params.push(...vars.map((name) => ({ type: 'Identifier', name }))); + } else { + params.push(param); + } } - }); - }, - TemplateLiteral(node, context) { - /** @type {import('estree').TemplateElement} */ - let quasi = { - type: 'TemplateElement', - value: { - ...node.quasis[0].value - }, - tail: node.quasis[0].tail - }; - /** @type {import('estree').TemplateLiteral} */ - let out = { - type: 'TemplateLiteral', - quasis: [quasi], - expressions: [] - }; + return /** @type {import('estree').FunctionDeclaration} */ ({ + .../** @type {import('estree').FunctionDeclaration} */ (context.next()), + params, + id: { + ...node.id, + name: code + } + }); + }, + TemplateLiteral(node, context) { + /** @type {import('estree').TemplateElement} */ + let quasi = { + type: 'TemplateElement', + value: { + ...node.quasis[0].value + }, + tail: node.quasis[0].tail + }; - for (let i = 0; i < node.expressions.length; i += 1) { - const q = structuredClone(node.quasis[i + 1]); - const e = node.expressions[i]; + /** @type {import('estree').TemplateLiteral} */ + let out = { + type: 'TemplateLiteral', + quasis: [quasi], + expressions: [] + }; - if (e.type === 'Literal' && e.value === 'CODE') { - quasi.value.raw += code + q.value.raw; - continue; - } + for (let i = 0; i < node.expressions.length; i += 1) { + const q = structuredClone(node.quasis[i + 1]); + const e = node.expressions[i]; - if (e.type === 'Identifier' && e.name === 'MESSAGE') { - if (message.type === 'Literal') { - const str = /** @type {string} */ (message.value).replace(/(`|\${)/g, '\\$1'); - quasi.value.raw += str + q.value.raw; + if (e.type === 'Literal' && e.value === 'CODE') { + quasi.value.raw += code + q.value.raw; continue; } - if (message.type === 'TemplateLiteral') { - const m = structuredClone(message); - quasi.value.raw += m.quasis[0].value.raw; - out.quasis.push(...m.quasis.slice(1)); - out.expressions.push(...m.expressions); - quasi = m.quasis[m.quasis.length - 1]; - quasi.value.raw += q.value.raw; - continue; + if (e.type === 'Identifier' && e.name === 'MESSAGE') { + if (message.type === 'Literal') { + const str = /** @type {string} */ (message.value).replace(/(`|\${)/g, '\\$1'); + quasi.value.raw += str + q.value.raw; + continue; + } + + if (message.type === 'TemplateLiteral') { + const m = structuredClone(message); + quasi.value.raw += m.quasis[0].value.raw; + out.quasis.push(...m.quasis.slice(1)); + out.expressions.push(...m.expressions); + quasi = m.quasis[m.quasis.length - 1]; + quasi.value.raw += q.value.raw; + continue; + } } + + out.quasis.push((quasi = q)); + out.expressions.push(/** @type {import('estree').Expression} */ (context.visit(e))); } - out.quasis.push((quasi = q)); - out.expressions.push(/** @type {import('estree').Expression} */ (context.visit(e))); + return out; + }, + Literal(node) { + if (node.value === 'CODE') { + return { + type: 'Literal', + value: code + }; + } + }, + Identifier(node) { + if (node.name !== 'MESSAGE') return; + return message; } + }); - return out; - }, - Literal(node) { - if (node.value === 'CODE') { - return { - type: 'Literal', - value: code - }; - } - }, - Identifier(node) { - if (node.name !== 'MESSAGE') return; - return message; - } - }); + // @ts-expect-error + ast.body.push(clone); + } + + const module = esrap.print(ast); - // @ts-expect-error - ast.body.push(clone); + fs.writeFileSync( + dest, + `/* This file is generated by scripts/process-messages/index.js. Do not edit! */\n\n` + + module.code, + 'utf-8' + ); } - const module = esrap.print(ast); + transform('compile-errors', 'src/compiler/errors.js'); + transform('compile-warnings', 'src/compiler/warnings.js'); - fs.writeFileSync( - dest, - `/* This file is generated by scripts/process-messages/index.js. Do not edit! */\n\n` + - module.code, - 'utf-8' - ); + transform('client-warnings', 'src/internal/client/warnings.js'); + transform('client-errors', 'src/internal/client/errors.js'); + transform('server-errors', 'src/internal/server/errors.js'); + transform('shared-errors', 'src/internal/shared/errors.js'); + transform('shared-warnings', 'src/internal/shared/warnings.js'); } -transform('compile-errors', 'src/compiler/errors.js'); -transform('compile-warnings', 'src/compiler/warnings.js'); +if (watch) { + let running = false; + let timeout; + + fs.watch('messages', { recursive: true }, (type, file) => { + if (running) { + timeout ??= setTimeout(() => { + running = false; + timeout = null; + }); + } else { + running = true; + + // eslint-disable-next-line no-console + console.log('Regenerating messages...'); + run(); + } + }); +} -transform('client-warnings', 'src/internal/client/warnings.js'); -transform('client-errors', 'src/internal/client/errors.js'); -transform('server-errors', 'src/internal/server/errors.js'); -transform('shared-errors', 'src/internal/shared/errors.js'); -transform('shared-warnings', 'src/internal/shared/warnings.js'); +run(); From a2ddca1aa10362c7e6962e04ec60e55ec758218d Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Mon, 19 May 2025 20:25:07 +0200 Subject: [PATCH 058/259] fix: avoid auto-parenthesis for special-keywords-only `MediaQuery` (#15937) * fix: avoid auto-parenthesis for special keywords only `MediaQuery` * chore: update changeset * chore: i has english knowledge * fix: fix media queries with commas --- .changeset/sweet-islands-sell.md | 5 +++++ packages/svelte/src/reactivity/media-query.js | 20 ++++++++++++++++++- .../samples/media-query/_config.js | 5 +++++ .../samples/media-query/main.svelte | 5 +++++ 4 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 .changeset/sweet-islands-sell.md diff --git a/.changeset/sweet-islands-sell.md b/.changeset/sweet-islands-sell.md new file mode 100644 index 0000000000..b4fbbf182c --- /dev/null +++ b/.changeset/sweet-islands-sell.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: avoid auto-parenthesis for special-keywords-only `MediaQuery` diff --git a/packages/svelte/src/reactivity/media-query.js b/packages/svelte/src/reactivity/media-query.js index 22310df18d..d286709719 100644 --- a/packages/svelte/src/reactivity/media-query.js +++ b/packages/svelte/src/reactivity/media-query.js @@ -3,6 +3,19 @@ import { ReactiveValue } from './reactive-value.js'; const parenthesis_regex = /\(.+\)/; +// these keywords are valid media queries but they need to be without parenthesis +// +// eg: new MediaQuery('screen') +// +// however because of the auto-parenthesis logic in the constructor since there's no parentehesis +// in the media query they'll be surrounded by parenthesis +// +// however we can check if the media query is only composed of these keywords +// and skip the auto-parenthesis +// +// https://github.com/sveltejs/svelte/issues/15930 +const non_parenthesized_keywords = new Set(['all', 'print', 'screen', 'and', 'or', 'not', 'only']); + /** * Creates a media query and provides a `current` property that reflects whether or not it matches. * @@ -27,7 +40,12 @@ export class MediaQuery extends ReactiveValue { * @param {boolean} [fallback] Fallback value for the server */ constructor(query, fallback) { - let final_query = parenthesis_regex.test(query) ? query : `(${query})`; + let final_query = + parenthesis_regex.test(query) || + // we need to use `some` here because technically this `window.matchMedia('random,screen')` still returns true + query.split(/[\s,]+/).some((keyword) => non_parenthesized_keywords.has(keyword.trim())) + ? query + : `(${query})`; const q = window.matchMedia(final_query); super( () => q.matches, diff --git a/packages/svelte/tests/runtime-runes/samples/media-query/_config.js b/packages/svelte/tests/runtime-runes/samples/media-query/_config.js index f7a4ca05f5..d8b202955a 100644 --- a/packages/svelte/tests/runtime-runes/samples/media-query/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/media-query/_config.js @@ -5,5 +5,10 @@ export default test({ async test({ window }) { expect(window.matchMedia).toHaveBeenCalledWith('(max-width: 599px), (min-width: 900px)'); expect(window.matchMedia).toHaveBeenCalledWith('(min-width: 900px)'); + expect(window.matchMedia).toHaveBeenCalledWith('screen'); + expect(window.matchMedia).toHaveBeenCalledWith('not print'); + expect(window.matchMedia).toHaveBeenCalledWith('screen,print'); + expect(window.matchMedia).toHaveBeenCalledWith('screen, print'); + expect(window.matchMedia).toHaveBeenCalledWith('screen, random'); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/media-query/main.svelte b/packages/svelte/tests/runtime-runes/samples/media-query/main.svelte index 446a9213dd..fe07ed8ab0 100644 --- a/packages/svelte/tests/runtime-runes/samples/media-query/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/media-query/main.svelte @@ -3,4 +3,9 @@ const mq = new MediaQuery("(max-width: 599px), (min-width: 900px)"); const mq2 = new MediaQuery("min-width: 900px"); + const mq3 = new MediaQuery("screen"); + const mq4 = new MediaQuery("not print"); + const mq5 = new MediaQuery("screen,print"); + const mq6 = new MediaQuery("screen, print"); + const mq7 = new MediaQuery("screen, random"); From a96d01b9187a223861c51833046703fe521c9235 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 19 May 2025 15:39:25 -0400 Subject: [PATCH 059/259] Version Packages (#15956) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/sweet-islands-sell.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/sweet-islands-sell.md diff --git a/.changeset/sweet-islands-sell.md b/.changeset/sweet-islands-sell.md deleted file mode 100644 index b4fbbf182c..0000000000 --- a/.changeset/sweet-islands-sell.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: avoid auto-parenthesis for special-keywords-only `MediaQuery` diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index d40ff09317..4508a9b054 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.31.1 + +### Patch Changes + +- fix: avoid auto-parenthesis for special-keywords-only `MediaQuery` ([#15937](https://github.com/sveltejs/svelte/pull/15937)) + ## 5.31.0 ### Minor Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index e0f6afe6fc..45a875d19b 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.31.0", + "version": "5.31.1", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index ed99ef6795..e42e6741bd 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.31.0'; +export const VERSION = '5.31.1'; export const PUBLIC_VERSION = '5'; From 22a0211cbb1fed32cfdc6d496e8f8fe6bdee086c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 19 May 2025 16:32:23 -0400 Subject: [PATCH 060/259] docs: clarify when attachments re-run (#15927) * clarify when attachments re-run * Update documentation/docs/03-template-syntax/09-@attach.md * Update documentation/docs/03-template-syntax/09-@attach.md * update docs * fix --- .../docs/03-template-syntax/09-@attach.md | 35 +++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/documentation/docs/03-template-syntax/09-@attach.md b/documentation/docs/03-template-syntax/09-@attach.md index 2df0882e34..3644cbc8e8 100644 --- a/documentation/docs/03-template-syntax/09-@attach.md +++ b/documentation/docs/03-template-syntax/09-@attach.md @@ -2,7 +2,9 @@ title: {@attach ...} --- -Attachments are functions that run when an element is mounted to the DOM. Optionally, they can return a function that is called when the element is later removed from the DOM. +Attachments are functions that run in an [effect]($effect) when an element is mounted to the DOM or when [state]($state) read inside the function updates. + +Optionally, they can return a function that is called before the attachment re-runs, or after the element is later removed from the DOM. > [!NOTE] > Attachments are available in Svelte 5.29 and newer. @@ -55,7 +57,7 @@ A useful pattern is for a function, such as `tooltip` in this example, to _retur ``` -Since the `tooltip(content)` expression runs inside an [effect]($effect), the attachment will be destroyed and recreated whenever `content` changes. +Since the `tooltip(content)` expression runs inside an [effect]($effect), the attachment will be destroyed and recreated whenever `content` changes. The same thing would happen for any state read _inside_ the attachment function when it first runs. (If this isn't what you want, see [Controlling when attachments re-run](#Controlling-when-attachments-re-run).) ## Inline attachments @@ -126,6 +128,35 @@ This allows you to create _wrapper components_ that augment elements ([demo](/pl ``` +## Controlling when attachments re-run + +Attachments, unlike [actions](use), are fully reactive: `{@attach foo(bar)}` will re-run on changes to `foo` _or_ `bar` (or any state read inside `foo`): + +```js +// @errors: 7006 2304 2552 +function foo(bar) { + return (node) => { + veryExpensiveSetupWork(node); + update(node, bar); + }; +} +``` + +In the rare case that this is a problem (for example, if `foo` does expensive and unavoidable setup work) consider passing the data inside a function and reading it in a child effect: + +```js +// @errors: 7006 2304 2552 +function foo(+++getBar+++) { + return (node) => { + veryExpensiveSetupWork(node); + ++++ $effect(() => { + update(node, getBar()); + });+++ + } +} +``` + ## Creating attachments programmatically To add attachments to an object that will be spread onto a component or element, use [`createAttachmentKey`](svelte-attachments#createAttachmentKey). From 7183886a73208d4ed34d990319ce2c0f455cad24 Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Tue, 20 May 2025 00:02:08 +0200 Subject: [PATCH 061/259] feat: attachments `fromAction` utility (#15933) * feat: attachments `fromAction` utility * fix: typing of the utility Co-authored-by: Aidan Bleser <117548273+ieedan@users.noreply.github.com> * simplify implementation - create action first, then only create update/destroy effects if necessary * add since * regenerate * remove FromAction interface * overload * fix: use typedef instead of exported interface * get rid of the arg0, arg1 stuff * oops * god i hate overloads * defer to the reference documentation * damn ur weird typescript * gah --------- Co-authored-by: Aidan Bleser <117548273+ieedan@users.noreply.github.com> Co-authored-by: Rich Harris --- .changeset/wise-tigers-happen.md | 5 + .../docs/03-template-syntax/09-@attach.md | 4 + packages/svelte/src/attachments/index.js | 86 +++++++++++++ .../samples/attachment-from-action/_config.js | 116 ++++++++++++++++++ .../attachment-from-action/main.svelte | 37 ++++++ packages/svelte/types/index.d.ts | 101 +++++++++++++++ 6 files changed, 349 insertions(+) create mode 100644 .changeset/wise-tigers-happen.md create mode 100644 packages/svelte/tests/runtime-runes/samples/attachment-from-action/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/attachment-from-action/main.svelte diff --git a/.changeset/wise-tigers-happen.md b/.changeset/wise-tigers-happen.md new file mode 100644 index 0000000000..53cc671859 --- /dev/null +++ b/.changeset/wise-tigers-happen.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: attachments `fromAction` utility diff --git a/documentation/docs/03-template-syntax/09-@attach.md b/documentation/docs/03-template-syntax/09-@attach.md index 3644cbc8e8..b25fbb32a6 100644 --- a/documentation/docs/03-template-syntax/09-@attach.md +++ b/documentation/docs/03-template-syntax/09-@attach.md @@ -160,3 +160,7 @@ function foo(+++getBar+++) { ## Creating attachments programmatically To add attachments to an object that will be spread onto a component or element, use [`createAttachmentKey`](svelte-attachments#createAttachmentKey). + +## Converting actions to attachments + +If you're using a library that only provides actions, you can convert them to attachments with [`fromAction`](svelte-attachments#fromAction), allowing you to (for example) use them with components. diff --git a/packages/svelte/src/attachments/index.js b/packages/svelte/src/attachments/index.js index 948a19f4dd..dc2f3e93d7 100644 --- a/packages/svelte/src/attachments/index.js +++ b/packages/svelte/src/attachments/index.js @@ -1,4 +1,9 @@ +/** @import { Action, ActionReturn } from '../action/public' */ +/** @import { Attachment } from './public' */ +import { noop, render_effect } from 'svelte/internal/client'; import { ATTACHMENT_KEY } from '../constants.js'; +import { untrack } from 'svelte'; +import { teardown } from '../internal/client/reactivity/effects.js'; /** * Creates an object key that will be recognised as an attachment when the object is spread onto an element, @@ -25,3 +30,84 @@ import { ATTACHMENT_KEY } from '../constants.js'; export function createAttachmentKey() { return Symbol(ATTACHMENT_KEY); } + +/** + * Converts an [action](https://svelte.dev/docs/svelte/use) into an [attachment](https://svelte.dev/docs/svelte/@attach) keeping the same behavior. + * It's useful if you want to start using attachments on components but you have actions provided by a library. + * + * Note that the second argument, if provided, must be a function that _returns_ the argument to the + * action function, not the argument itself. + * + * ```svelte + * + *
...
+ * + * + *
bar)}>...
+ * ``` + * @template {EventTarget} E + * @template {unknown} T + * @overload + * @param {Action | ((element: E, arg: T) => void | ActionReturn)} action The action function + * @param {() => T} fn A function that returns the argument for the action + * @returns {Attachment} + */ +/** + * Converts an [action](https://svelte.dev/docs/svelte/use) into an [attachment](https://svelte.dev/docs/svelte/@attach) keeping the same behavior. + * It's useful if you want to start using attachments on components but you have actions provided by a library. + * + * Note that the second argument, if provided, must be a function that _returns_ the argument to the + * action function, not the argument itself. + * + * ```svelte + * + *
...
+ * + * + *
bar)}>...
+ * ``` + * @template {EventTarget} E + * @overload + * @param {Action | ((element: E) => void | ActionReturn)} action The action function + * @returns {Attachment} + */ +/** + * Converts an [action](https://svelte.dev/docs/svelte/use) into an [attachment](https://svelte.dev/docs/svelte/@attach) keeping the same behavior. + * It's useful if you want to start using attachments on components but you have actions provided by a library. + * + * Note that the second argument, if provided, must be a function that _returns_ the argument to the + * action function, not the argument itself. + * + * ```svelte + * + *
...
+ * + * + *
bar)}>...
+ * ``` + * + * @template {EventTarget} E + * @template {unknown} T + * @param {Action | ((element: E, arg: T) => void | ActionReturn)} action The action function + * @param {() => T} fn A function that returns the argument for the action + * @returns {Attachment} + * @since 5.32 + */ +export function fromAction(action, fn = /** @type {() => T} */ (noop)) { + return (element) => { + const { update, destroy } = untrack(() => action(element, fn()) ?? {}); + + if (update) { + var ran = false; + render_effect(() => { + const arg = fn(); + if (ran) update(arg); + }); + ran = true; + } + + if (destroy) { + teardown(destroy); + } + }; +} diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-from-action/_config.js b/packages/svelte/tests/runtime-runes/samples/attachment-from-action/_config.js new file mode 100644 index 0000000000..2e53f0d29d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attachment-from-action/_config.js @@ -0,0 +1,116 @@ +import { ok, test } from '../../test'; +import { flushSync } from 'svelte'; + +export default test({ + async test({ assert, target, logs }) { + const [btn, btn2, btn3] = target.querySelectorAll('button'); + + // both logs on creation it will not log on change + assert.deepEqual(logs, ['create', 0, 'action', 'create', 0, 'attachment']); + + // clicking the first button logs the right value + flushSync(() => { + btn?.click(); + }); + assert.deepEqual(logs, ['create', 0, 'action', 'create', 0, 'attachment', 0]); + + // clicking the second button logs the right value + flushSync(() => { + btn2?.click(); + }); + assert.deepEqual(logs, ['create', 0, 'action', 'create', 0, 'attachment', 0, 0]); + + // updating the arguments logs the update function for both + flushSync(() => { + btn3?.click(); + }); + assert.deepEqual(logs, [ + 'create', + 0, + 'action', + 'create', + 0, + 'attachment', + 0, + 0, + 'update', + 1, + 'action', + 'update', + 1, + 'attachment' + ]); + + // clicking the first button again shows the right value + flushSync(() => { + btn?.click(); + }); + assert.deepEqual(logs, [ + 'create', + 0, + 'action', + 'create', + 0, + 'attachment', + 0, + 0, + 'update', + 1, + 'action', + 'update', + 1, + 'attachment', + 1 + ]); + + // clicking the second button again shows the right value + flushSync(() => { + btn2?.click(); + }); + assert.deepEqual(logs, [ + 'create', + 0, + 'action', + 'create', + 0, + 'attachment', + 0, + 0, + 'update', + 1, + 'action', + 'update', + 1, + 'attachment', + 1, + 1 + ]); + + // unmounting logs the destroy function for both + flushSync(() => { + btn3?.click(); + }); + assert.deepEqual(logs, [ + 'create', + 0, + 'action', + 'create', + 0, + 'attachment', + 0, + 0, + 'update', + 1, + 'action', + 'update', + 1, + 'attachment', + 1, + 1, + 'destroy', + 'action', + 'destroy', + 'attachment' + ]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-from-action/main.svelte b/packages/svelte/tests/runtime-runes/samples/attachment-from-action/main.svelte new file mode 100644 index 0000000000..35079aa15e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attachment-from-action/main.svelte @@ -0,0 +1,37 @@ + + +{#if count < 2} + + +{/if} + + \ No newline at end of file diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 1fda9a36b8..fcc1ec315c 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -658,6 +658,107 @@ declare module 'svelte/attachments' { * @since 5.29 */ export function createAttachmentKey(): symbol; + /** + * Converts an [action](https://svelte.dev/docs/svelte/use) into an [attachment](https://svelte.dev/docs/svelte/@attach) keeping the same behavior. + * It's useful if you want to start using attachments on components but you have actions provided by a library. + * + * Note that the second argument, if provided, must be a function that _returns_ the argument to the + * action function, not the argument itself. + * + * ```svelte + * + *
...
+ * + * + *
bar)}>...
+ * ``` + * */ + export function fromAction(action: Action | ((element: E, arg: T) => void | ActionReturn), fn: () => T): Attachment; + /** + * Converts an [action](https://svelte.dev/docs/svelte/use) into an [attachment](https://svelte.dev/docs/svelte/@attach) keeping the same behavior. + * It's useful if you want to start using attachments on components but you have actions provided by a library. + * + * Note that the second argument, if provided, must be a function that _returns_ the argument to the + * action function, not the argument itself. + * + * ```svelte + * + *
...
+ * + * + *
bar)}>...
+ * ``` + * */ + export function fromAction(action: Action | ((element: E) => void | ActionReturn)): Attachment; + /** + * Actions can return an object containing the two properties defined in this interface. Both are optional. + * - update: An action can have a parameter. This method will be called whenever that parameter changes, + * immediately after Svelte has applied updates to the markup. `ActionReturn` and `ActionReturn` both + * mean that the action accepts no parameters. + * - destroy: Method that is called after the element is unmounted + * + * Additionally, you can specify which additional attributes and events the action enables on the applied element. + * This applies to TypeScript typings only and has no effect at runtime. + * + * Example usage: + * ```ts + * interface Attributes { + * newprop?: string; + * 'on:event': (e: CustomEvent) => void; + * } + * + * export function myAction(node: HTMLElement, parameter: Parameter): ActionReturn { + * // ... + * return { + * update: (updatedParameter) => {...}, + * destroy: () => {...} + * }; + * } + * ``` + */ + interface ActionReturn< + Parameter = undefined, + Attributes extends Record = Record + > { + update?: (parameter: Parameter) => void; + destroy?: () => void; + /** + * ### DO NOT USE THIS + * This exists solely for type-checking and has no effect at runtime. + * Set this through the `Attributes` generic instead. + */ + $$_attributes?: Attributes; + } + + /** + * Actions are functions that are called when an element is created. + * You can use this interface to type such actions. + * The following example defines an action that only works on `
` elements + * and optionally accepts a parameter which it has a default value for: + * ```ts + * export const myAction: Action = (node, param = { someProperty: true }) => { + * // ... + * } + * ``` + * `Action` and `Action` both signal that the action accepts no parameters. + * + * You can return an object with methods `update` and `destroy` from the function and type which additional attributes and events it has. + * See interface `ActionReturn` for more details. + */ + interface Action< + Element = HTMLElement, + Parameter = undefined, + Attributes extends Record = Record + > { + ( + ...args: undefined extends Parameter + ? [node: Node, parameter?: Parameter] + : [node: Node, parameter: Parameter] + ): void | ActionReturn; + } + + // Implementation notes: + // - undefined extends X instead of X extends undefined makes this work better with both strict and nonstrict mode export {}; } From c66a43940aee4c24f4c003a50b433e64d316e40a Mon Sep 17 00:00:00 2001 From: 7nik Date: Tue, 20 May 2025 17:50:54 +0300 Subject: [PATCH 062/259] feat: warn on implicitly closed tags (#15932) * warn on implicitly closed tags * lint * changeset * separate tests for parent/sibling case * account for attributes in opening tag * tweak * update message --------- Co-authored-by: 7nik Co-authored-by: Rich Harris --- .changeset/blue-experts-tell.md | 5 ++++ .../.generated/compile-warnings.md | 19 ++++++++++++++ .../messages/compile-warnings/template.md | 17 ++++++++++++ .../compiler/phases/1-parse/state/element.js | 13 +++++++++- packages/svelte/src/compiler/warnings.js | 11 ++++++++ .../implicitly-closed-by-parent/input.svelte | 6 +++++ .../implicitly-closed-by-parent/warnings.json | 26 +++++++++++++++++++ .../implicitly-closed-by-sibling/input.svelte | 9 +++++++ .../warnings.json | 26 +++++++++++++++++++ 9 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 .changeset/blue-experts-tell.md create mode 100644 packages/svelte/tests/validator/samples/implicitly-closed-by-parent/input.svelte create mode 100644 packages/svelte/tests/validator/samples/implicitly-closed-by-parent/warnings.json create mode 100644 packages/svelte/tests/validator/samples/implicitly-closed-by-sibling/input.svelte create mode 100644 packages/svelte/tests/validator/samples/implicitly-closed-by-sibling/warnings.json diff --git a/.changeset/blue-experts-tell.md b/.changeset/blue-experts-tell.md new file mode 100644 index 0000000000..a12fb126d1 --- /dev/null +++ b/.changeset/blue-experts-tell.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: warn on implicitly closed tags diff --git a/documentation/docs/98-reference/.generated/compile-warnings.md b/documentation/docs/98-reference/.generated/compile-warnings.md index 7069f90206..b579d38602 100644 --- a/documentation/docs/98-reference/.generated/compile-warnings.md +++ b/documentation/docs/98-reference/.generated/compile-warnings.md @@ -632,6 +632,25 @@ In some situations a selector may target an element that is not 'visible' to the ``` +### element_implicitly_closed + +``` +This element is implicitly closed by the following `%tag%`, which can cause an unexpected DOM structure. Add an explicit `%closing%` to avoid surprises. +``` + +In HTML, some elements are implicitly closed by another element. For example, you cannot nest a `

` inside another `

`: + +```html + +

hello

+ + +

+

hello

+``` + +Similarly, a parent element's closing tag will implicitly close all child elements, even if the ` `<%name%>` will be treated as an HTML element unless it begins with a capital letter +## element_implicitly_closed + +> This element is implicitly closed by the following `%tag%`, which can cause an unexpected DOM structure. Add an explicit `%closing%` to avoid surprises. + +In HTML, some elements are implicitly closed by another element. For example, you cannot nest a `

` inside another `

`: + +```html + +

hello

+ + +

+

hello

+``` + +Similarly, a parent element's closing tag will implicitly close all child elements, even if the ` Self-closing HTML tags for non-void elements are ambiguous — use `<%name% ...>` rather than `<%name% ... />` diff --git a/packages/svelte/src/compiler/phases/1-parse/state/element.js b/packages/svelte/src/compiler/phases/1-parse/state/element.js index d20369b0d9..6b6c9160d8 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/element.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/element.js @@ -93,7 +93,16 @@ export default function element(parser) { } } - if (parent.type !== 'RegularElement' && !parser.loose) { + if (parent.type === 'RegularElement') { + if (!parser.last_auto_closed_tag || parser.last_auto_closed_tag.tag !== name) { + const end = parent.fragment.nodes[0]?.start ?? start; + w.element_implicitly_closed( + { start: parent.start, end }, + ``, + `` + ); + } + } else if (!parser.loose) { if (parser.last_auto_closed_tag && parser.last_auto_closed_tag.tag === name) { e.element_invalid_closing_tag_autoclosed(start, name, parser.last_auto_closed_tag.reason); } else { @@ -186,6 +195,8 @@ export default function element(parser) { parser.allow_whitespace(); if (parent.type === 'RegularElement' && closing_tag_omitted(parent.name, name)) { + const end = parent.fragment.nodes[0]?.start ?? start; + w.element_implicitly_closed({ start: parent.start, end }, `<${name}>`, ``); parent.end = start; parser.pop(); parser.last_auto_closed_tag = { diff --git a/packages/svelte/src/compiler/warnings.js b/packages/svelte/src/compiler/warnings.js index c281433213..2a8316308c 100644 --- a/packages/svelte/src/compiler/warnings.js +++ b/packages/svelte/src/compiler/warnings.js @@ -114,6 +114,7 @@ export const codes = [ 'bind_invalid_each_rest', 'block_empty', 'component_name_lowercase', + 'element_implicitly_closed', 'element_invalid_self_closing_tag', 'event_directive_deprecated', 'node_invalid_placement_ssr', @@ -746,6 +747,16 @@ export function component_name_lowercase(node, name) { w(node, 'component_name_lowercase', `\`<${name}>\` will be treated as an HTML element unless it begins with a capital letter\nhttps://svelte.dev/e/component_name_lowercase`); } +/** + * This element is implicitly closed by the following `%tag%`, which can cause an unexpected DOM structure. Add an explicit `%closing%` to avoid surprises. + * @param {null | NodeLike} node + * @param {string} tag + * @param {string} closing + */ +export function element_implicitly_closed(node, tag, closing) { + w(node, 'element_implicitly_closed', `This element is implicitly closed by the following \`${tag}\`, which can cause an unexpected DOM structure. Add an explicit \`${closing}\` to avoid surprises.\nhttps://svelte.dev/e/element_implicitly_closed`); +} + /** * Self-closing HTML tags for non-void elements are ambiguous — use `<%name% ...>` rather than `<%name% ... />` * @param {null | NodeLike} node diff --git a/packages/svelte/tests/validator/samples/implicitly-closed-by-parent/input.svelte b/packages/svelte/tests/validator/samples/implicitly-closed-by-parent/input.svelte new file mode 100644 index 0000000000..f67eba18b8 --- /dev/null +++ b/packages/svelte/tests/validator/samples/implicitly-closed-by-parent/input.svelte @@ -0,0 +1,6 @@ +
+ +
+
+

hello

+
diff --git a/packages/svelte/tests/validator/samples/implicitly-closed-by-parent/warnings.json b/packages/svelte/tests/validator/samples/implicitly-closed-by-parent/warnings.json new file mode 100644 index 0000000000..1316a2b65b --- /dev/null +++ b/packages/svelte/tests/validator/samples/implicitly-closed-by-parent/warnings.json @@ -0,0 +1,26 @@ +[ + { + "code": "element_implicitly_closed", + "message": "This element is implicitly closed by the following ``, which can cause an unexpected DOM structure. Add an explicit `
` to avoid surprises.", + "start": { + "line": 1, + "column": 6 + }, + "end": { + "line": 1, + "column": 25 + } + }, + { + "code": "element_implicitly_closed", + "message": "This element is implicitly closed by the following ``, which can cause an unexpected DOM structure. Add an explicit `` to avoid surprises.", + "start": { + "line": 4, + "column": 1 + }, + "end": { + "line": 4, + "column": 20 + } + } +] diff --git a/packages/svelte/tests/validator/samples/implicitly-closed-by-sibling/input.svelte b/packages/svelte/tests/validator/samples/implicitly-closed-by-sibling/input.svelte new file mode 100644 index 0000000000..7721f2f380 --- /dev/null +++ b/packages/svelte/tests/validator/samples/implicitly-closed-by-sibling/input.svelte @@ -0,0 +1,9 @@ +
+

+ +

+
+ +
+

+
diff --git a/packages/svelte/tests/validator/samples/implicitly-closed-by-sibling/warnings.json b/packages/svelte/tests/validator/samples/implicitly-closed-by-sibling/warnings.json new file mode 100644 index 0000000000..6ea36c5a50 --- /dev/null +++ b/packages/svelte/tests/validator/samples/implicitly-closed-by-sibling/warnings.json @@ -0,0 +1,26 @@ +[ + { + "code": "element_implicitly_closed", + "message": "This element is implicitly closed by the following `

`, which can cause an unexpected DOM structure. Add an explicit `

` to avoid surprises.", + "start": { + "line": 2, + "column": 1 + }, + "end": { + "line": 2, + "column": 18 + } + }, + { + "code": "element_implicitly_closed", + "message": "This element is implicitly closed by the following `

`, which can cause an unexpected DOM structure. Add an explicit `

` to avoid surprises.", + "start": { + "line": 8, + "column": 1 + }, + "end": { + "line": 8, + "column": 18 + } + } +] From 93443f93160792d49a85b6b8ac7571abd3fccede Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 20 May 2025 11:13:55 -0400 Subject: [PATCH 063/259] fix: only re-run directly applied attachment if it changed (#15962) * fix: only re-run directly applied attachment if it changed * add note * one down * fix * Revert "one down" This reverts commit 9f6c4cf84838de8d8cb93e7217de37871839ac8b. --- .changeset/slimy-drinks-divide.md | 5 ++++ .../client/dom/elements/attachments.js | 30 +++++++++++++++---- .../attachment-in-mutated-state/_config.js | 16 ++++++++++ .../attachment-in-mutated-state/main.svelte | 15 ++++++++++ 4 files changed, 60 insertions(+), 6 deletions(-) create mode 100644 .changeset/slimy-drinks-divide.md create mode 100644 packages/svelte/tests/runtime-legacy/samples/attachment-in-mutated-state/_config.js create mode 100644 packages/svelte/tests/runtime-legacy/samples/attachment-in-mutated-state/main.svelte diff --git a/.changeset/slimy-drinks-divide.md b/.changeset/slimy-drinks-divide.md new file mode 100644 index 0000000000..420d0bed56 --- /dev/null +++ b/.changeset/slimy-drinks-divide.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: only re-run directly applied attachment if it changed diff --git a/packages/svelte/src/internal/client/dom/elements/attachments.js b/packages/svelte/src/internal/client/dom/elements/attachments.js index 6e3089a384..4fc1280138 100644 --- a/packages/svelte/src/internal/client/dom/elements/attachments.js +++ b/packages/svelte/src/internal/client/dom/elements/attachments.js @@ -1,15 +1,33 @@ -import { effect } from '../../reactivity/effects.js'; +/** @import { Effect } from '#client' */ +import { block, branch, effect, destroy_effect } from '../../reactivity/effects.js'; + +// TODO in 6.0 or 7.0, when we remove legacy mode, we can simplify this by +// getting rid of the block/branch stuff and just letting the effect rip. +// see https://github.com/sveltejs/svelte/pull/15962 /** * @param {Element} node * @param {() => (node: Element) => void} get_fn */ export function attach(node, get_fn) { - effect(() => { - const fn = get_fn(); + /** @type {false | undefined | ((node: Element) => void)} */ + var fn = undefined; + + /** @type {Effect | null} */ + var e; + + block(() => { + if (fn !== (fn = get_fn())) { + if (e) { + destroy_effect(e); + e = null; + } - // we use `&&` rather than `?.` so that things like - // `{@attach DEV && something_dev_only()}` work - return fn && fn(node); + if (fn) { + e = branch(() => { + effect(() => /** @type {(node: Element) => void} */ (fn)(node)); + }); + } + } }); } diff --git a/packages/svelte/tests/runtime-legacy/samples/attachment-in-mutated-state/_config.js b/packages/svelte/tests/runtime-legacy/samples/attachment-in-mutated-state/_config.js new file mode 100644 index 0000000000..5d37252358 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/attachment-in-mutated-state/_config.js @@ -0,0 +1,16 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test({ assert, target, logs }) { + assert.deepEqual(logs, ['up']); + + const button = target.querySelector('button'); + + flushSync(() => button?.click()); + assert.deepEqual(logs, ['up']); + + flushSync(() => button?.click()); + assert.deepEqual(logs, ['up', 'down']); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/attachment-in-mutated-state/main.svelte b/packages/svelte/tests/runtime-legacy/samples/attachment-in-mutated-state/main.svelte new file mode 100644 index 0000000000..fbec108c7a --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/attachment-in-mutated-state/main.svelte @@ -0,0 +1,15 @@ + + + + +{#if state.count < 2} +
+{/if} From 21d5448ab72c1e0cf5dfd6ec989489d4f57516f7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 20 May 2025 11:28:43 -0400 Subject: [PATCH 064/259] Version Packages (#15959) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/blue-experts-tell.md | 5 ----- .changeset/slimy-drinks-divide.md | 5 ----- .changeset/wise-tigers-happen.md | 5 ----- packages/svelte/CHANGELOG.md | 12 ++++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 6 files changed, 14 insertions(+), 17 deletions(-) delete mode 100644 .changeset/blue-experts-tell.md delete mode 100644 .changeset/slimy-drinks-divide.md delete mode 100644 .changeset/wise-tigers-happen.md diff --git a/.changeset/blue-experts-tell.md b/.changeset/blue-experts-tell.md deleted file mode 100644 index a12fb126d1..0000000000 --- a/.changeset/blue-experts-tell.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': minor ---- - -feat: warn on implicitly closed tags diff --git a/.changeset/slimy-drinks-divide.md b/.changeset/slimy-drinks-divide.md deleted file mode 100644 index 420d0bed56..0000000000 --- a/.changeset/slimy-drinks-divide.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: only re-run directly applied attachment if it changed diff --git a/.changeset/wise-tigers-happen.md b/.changeset/wise-tigers-happen.md deleted file mode 100644 index 53cc671859..0000000000 --- a/.changeset/wise-tigers-happen.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': minor ---- - -feat: attachments `fromAction` utility diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 4508a9b054..270460e5ed 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,17 @@ # svelte +## 5.32.0 + +### Minor Changes + +- feat: warn on implicitly closed tags ([#15932](https://github.com/sveltejs/svelte/pull/15932)) + +- feat: attachments `fromAction` utility ([#15933](https://github.com/sveltejs/svelte/pull/15933)) + +### Patch Changes + +- fix: only re-run directly applied attachment if it changed ([#15962](https://github.com/sveltejs/svelte/pull/15962)) + ## 5.31.1 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 45a875d19b..d374880887 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.31.1", + "version": "5.32.0", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index e42e6741bd..9a198d097d 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.31.1'; +export const VERSION = '5.32.0'; export const PUBLIC_VERSION = '5'; From de8094910de7e2d491c2f26b891844c2f311f78f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gautier=20Ben=20A=C3=AFm?= <48261497+GauBen@users.noreply.github.com> Date: Tue, 20 May 2025 19:32:47 +0200 Subject: [PATCH 065/259] fix(14815): warn when an invalid value is given * Create nervous-stingrays-travel.md --- .changeset/nervous-stingrays-travel.md | 5 +++++ .../.generated/client-warnings.md | 13 +++++++++++++ .../messages/client-warnings/warnings.md | 11 +++++++++++ .../client/dom/elements/bindings/select.js | 19 +++++++++++++++---- .../svelte/src/internal/client/warnings.js | 11 +++++++++++ .../select-multiple-invalid-value/_config.js | 7 +++++++ .../select-multiple-invalid-value/main.svelte | 9 +++++++++ 7 files changed, 71 insertions(+), 4 deletions(-) create mode 100644 .changeset/nervous-stingrays-travel.md create mode 100644 packages/svelte/tests/runtime-runes/samples/select-multiple-invalid-value/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/select-multiple-invalid-value/main.svelte diff --git a/.changeset/nervous-stingrays-travel.md b/.changeset/nervous-stingrays-travel.md new file mode 100644 index 0000000000..9ccc3e6c65 --- /dev/null +++ b/.changeset/nervous-stingrays-travel.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +Warn when an invalid `` element should be an array, but it received a non-array value. The selection will be kept as is. +``` + +When using `` element should be an array, but it received a non-array value. The selection will be kept as is. + +When using `` element should be an array, but it received a non-array value. The selection will be kept as is. + */ +export function select_multiple_invalid_value() { + if (DEV) { + console.warn(`%c[svelte] select_multiple_invalid_value\n%cThe \`value\` property of a \`` element should be an array, but it received a non-array value. The selection will be kept as is.' + ] +}); diff --git a/packages/svelte/tests/runtime-runes/samples/select-multiple-invalid-value/main.svelte b/packages/svelte/tests/runtime-runes/samples/select-multiple-invalid-value/main.svelte new file mode 100644 index 0000000000..f3dfedf0e9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/select-multiple-invalid-value/main.svelte @@ -0,0 +1,9 @@ + + + From ef48d35a15fc104efd6ee5cf13eceea3ace4ae0d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 20 May 2025 14:13:31 -0400 Subject: [PATCH 066/259] Version Packages (#15967) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/nervous-stingrays-travel.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/nervous-stingrays-travel.md diff --git a/.changeset/nervous-stingrays-travel.md b/.changeset/nervous-stingrays-travel.md deleted file mode 100644 index 9ccc3e6c65..0000000000 --- a/.changeset/nervous-stingrays-travel.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"svelte": patch ---- - -Warn when an invalid `` value is given ([#14816](https://github.com/sveltejs/svelte/pull/14816)) + ## 5.32.0 ### Minor Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index d374880887..64fa308000 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.32.0", + "version": "5.32.1", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 9a198d097d..c6748487fb 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.32.0'; +export const VERSION = '5.32.1'; export const PUBLIC_VERSION = '5'; From 4e72b7d28b5d8f469ee7ffaee02f94a111823bd4 Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Wed, 21 May 2025 15:18:33 +0200 Subject: [PATCH 067/259] fix: import `untrack` directly from client in `svelte/attachments` (#15974) --- .changeset/short-ads-rhyme.md | 5 +++++ packages/svelte/src/attachments/index.js | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/short-ads-rhyme.md diff --git a/.changeset/short-ads-rhyme.md b/.changeset/short-ads-rhyme.md new file mode 100644 index 0000000000..31434530ab --- /dev/null +++ b/.changeset/short-ads-rhyme.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: import untrack directly from client in `svelte/attachments` diff --git a/packages/svelte/src/attachments/index.js b/packages/svelte/src/attachments/index.js index dc2f3e93d7..b9fde9b6d9 100644 --- a/packages/svelte/src/attachments/index.js +++ b/packages/svelte/src/attachments/index.js @@ -2,7 +2,7 @@ /** @import { Attachment } from './public' */ import { noop, render_effect } from 'svelte/internal/client'; import { ATTACHMENT_KEY } from '../constants.js'; -import { untrack } from 'svelte'; +import { untrack } from '../index-client.js'; import { teardown } from '../internal/client/reactivity/effects.js'; /** From c03ea47e4ef88782a0f43b3157d7712a03c44698 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 21 May 2025 15:09:21 -0400 Subject: [PATCH 068/259] chore: simplify `
` cleaning (#15980)

---
 .changeset/fifty-llamas-know.md                 |  5 +++++
 .../src/compiler/phases/3-transform/utils.js    | 17 +++++------------
 2 files changed, 10 insertions(+), 12 deletions(-)
 create mode 100644 .changeset/fifty-llamas-know.md

diff --git a/.changeset/fifty-llamas-know.md b/.changeset/fifty-llamas-know.md
new file mode 100644
index 0000000000..f11c975f3d
--- /dev/null
+++ b/.changeset/fifty-llamas-know.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+chore: simplify `
` cleaning
diff --git a/packages/svelte/src/compiler/phases/3-transform/utils.js b/packages/svelte/src/compiler/phases/3-transform/utils.js
index 5aa40c8abb..5803c04947 100644
--- a/packages/svelte/src/compiler/phases/3-transform/utils.js
+++ b/packages/svelte/src/compiler/phases/3-transform/utils.js
@@ -271,19 +271,12 @@ export function clean_nodes(
 
 	var first = trimmed[0];
 
-	// initial newline inside a `
` is disregarded, if not followed by another newline
+	// if first text node inside a 
 is a single newline, discard it, because otherwise
+	// the browser will do it for us which could break hydration
 	if (parent.type === 'RegularElement' && parent.name === 'pre' && first?.type === 'Text') {
-		const text = first.data.replace(regex_starts_with_newline, '');
-		if (text !== first.data) {
-			const tmp = text.replace(regex_starts_with_newline, '');
-			if (text === tmp) {
-				first.data = text;
-				first.raw = first.raw.replace(regex_starts_with_newline, '');
-				if (first.data === '') {
-					trimmed.shift();
-					first = trimmed[0];
-				}
-			}
+		if (first.data === '\n' || first.data === '\r\n') {
+			trimmed.shift();
+			first = trimmed[0];
 		}
 	}
 

From 50de8c53178d61f675c5fa1d2d1fc88ae94bb0cd Mon Sep 17 00:00:00 2001
From: Rich Harris 
Date: Wed, 21 May 2025 19:45:07 -0400
Subject: [PATCH 069/259] fix: attach __svelte_meta correctly to elements
 following a CSS wrapper (#15982)

* fix: attach __svelte_meta correctly to elements following a CSS wrapper

* Update .changeset/nervous-hotels-clean.md
---
 .changeset/nervous-hotels-clean.md            |  5 +++
 .../client/visitors/shared/component.js       |  9 +++-
 .../svelte-meta-css-wrapper/Component.svelte  |  7 ++++
 .../svelte-meta-css-wrapper/_config.js        | 42 +++++++++++++++++++
 .../svelte-meta-css-wrapper/main.svelte       |  7 ++++
 5 files changed, 69 insertions(+), 1 deletion(-)
 create mode 100644 .changeset/nervous-hotels-clean.md
 create mode 100644 packages/svelte/tests/runtime-runes/samples/svelte-meta-css-wrapper/Component.svelte
 create mode 100644 packages/svelte/tests/runtime-runes/samples/svelte-meta-css-wrapper/_config.js
 create mode 100644 packages/svelte/tests/runtime-runes/samples/svelte-meta-css-wrapper/main.svelte

diff --git a/.changeset/nervous-hotels-clean.md b/.changeset/nervous-hotels-clean.md
new file mode 100644
index 0000000000..77d5c186c7
--- /dev/null
+++ b/.changeset/nervous-hotels-clean.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: attach `__svelte_meta` correctly to elements following a CSS wrapper
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 6d3d8a68e6..99ed294d26 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,7 +1,7 @@
 /** @import { BlockStatement, Expression, ExpressionStatement, Identifier, MemberExpression, Pattern, Property, SequenceExpression, Statement } from 'estree' */
 /** @import { AST } from '#compiler' */
 /** @import { ComponentContext } from '../../types.js' */
-import { dev, is_ignored } from '../../../../../state.js';
+import { dev, is_ignored, locator } from '../../../../../state.js';
 import { get_attribute_chunks, object } from '../../../../../utils/ast.js';
 import * as b from '#compiler/builders';
 import { build_bind_this, memoize_expression, validate_binding } from '../shared/utils.js';
@@ -440,6 +440,13 @@ export function build_component(node, component_name, context, anchor = context.
 	}
 
 	if (Object.keys(custom_css_props).length > 0) {
+		if (dev) {
+			const loc = locator(node.start);
+			if (loc) {
+				context.state.locations.push([loc.line, loc.column]);
+			}
+		}
+
 		context.state.template.push(
 			context.state.metadata.namespace === 'svg'
 				? ''
diff --git a/packages/svelte/tests/runtime-runes/samples/svelte-meta-css-wrapper/Component.svelte b/packages/svelte/tests/runtime-runes/samples/svelte-meta-css-wrapper/Component.svelte
new file mode 100644
index 0000000000..5668311b0e
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/svelte-meta-css-wrapper/Component.svelte
@@ -0,0 +1,7 @@
+

hello from component

+ + diff --git a/packages/svelte/tests/runtime-runes/samples/svelte-meta-css-wrapper/_config.js b/packages/svelte/tests/runtime-runes/samples/svelte-meta-css-wrapper/_config.js new file mode 100644 index 0000000000..1d76b0dd00 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/svelte-meta-css-wrapper/_config.js @@ -0,0 +1,42 @@ +import { test } from '../../test'; + +export default test({ + compileOptions: { + dev: true + }, + + html: ` +

hello

+ +

hello from component

+
+

goodbye

+ `, + + async test({ target, assert }) { + const h1 = target.querySelector('h1'); + const h2 = target.querySelector('h2'); + const p = target.querySelector('p'); + + // @ts-expect-error + assert.deepEqual(h1.__svelte_meta.loc, { + file: 'main.svelte', + line: 5, + column: 0 + }); + + // @ts-expect-error + assert.deepEqual(h2.__svelte_meta.loc, { + file: 'Component.svelte', + line: 1, + column: 0 + }); + + // @ts-expect-error + assert.deepEqual(p.__svelte_meta.loc, { + file: 'main.svelte', + line: 7, + column: 0 + }); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/svelte-meta-css-wrapper/main.svelte b/packages/svelte/tests/runtime-runes/samples/svelte-meta-css-wrapper/main.svelte new file mode 100644 index 0000000000..f49a48fb52 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/svelte-meta-css-wrapper/main.svelte @@ -0,0 +1,7 @@ + + +

hello

+ +

goodbye

From 5a840982a7d06d1aaea59db8352fb2bf0188c0ec Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 22 May 2025 11:18:39 -0400 Subject: [PATCH 070/259] Version Packages (#15978) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/fifty-llamas-know.md | 5 ----- .changeset/nervous-hotels-clean.md | 5 ----- .changeset/short-ads-rhyme.md | 5 ----- packages/svelte/CHANGELOG.md | 10 ++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 6 files changed, 12 insertions(+), 17 deletions(-) delete mode 100644 .changeset/fifty-llamas-know.md delete mode 100644 .changeset/nervous-hotels-clean.md delete mode 100644 .changeset/short-ads-rhyme.md diff --git a/.changeset/fifty-llamas-know.md b/.changeset/fifty-llamas-know.md deleted file mode 100644 index f11c975f3d..0000000000 --- a/.changeset/fifty-llamas-know.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -chore: simplify `
` cleaning
diff --git a/.changeset/nervous-hotels-clean.md b/.changeset/nervous-hotels-clean.md
deleted file mode 100644
index 77d5c186c7..0000000000
--- a/.changeset/nervous-hotels-clean.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'svelte': patch
----
-
-fix: attach `__svelte_meta` correctly to elements following a CSS wrapper
diff --git a/.changeset/short-ads-rhyme.md b/.changeset/short-ads-rhyme.md
deleted file mode 100644
index 31434530ab..0000000000
--- a/.changeset/short-ads-rhyme.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'svelte': patch
----
-
-fix: import untrack directly from client in `svelte/attachments`
diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md
index b65a941bd9..4c65954c9f 100644
--- a/packages/svelte/CHANGELOG.md
+++ b/packages/svelte/CHANGELOG.md
@@ -1,5 +1,15 @@
 # svelte
 
+## 5.32.2
+
+### Patch Changes
+
+- chore: simplify `
` cleaning ([#15980](https://github.com/sveltejs/svelte/pull/15980))
+
+- fix: attach `__svelte_meta` correctly to elements following a CSS wrapper ([#15982](https://github.com/sveltejs/svelte/pull/15982))
+
+- fix: import untrack directly from client in `svelte/attachments` ([#15974](https://github.com/sveltejs/svelte/pull/15974))
+
 ## 5.32.1
 
 ### Patch Changes
diff --git a/packages/svelte/package.json b/packages/svelte/package.json
index 64fa308000..168e7abe37 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.32.1",
+  "version": "5.32.2",
   "type": "module",
   "types": "./types/index.d.ts",
   "engines": {
diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js
index c6748487fb..c93d7e2e25 100644
--- a/packages/svelte/src/version.js
+++ b/packages/svelte/src/version.js
@@ -4,5 +4,5 @@
  * The current version, as set in package.json.
  * @type {string}
  */
-export const VERSION = '5.32.1';
+export const VERSION = '5.32.2';
 export const PUBLIC_VERSION = '5';

From d1141a19b5eaa156d3cd39cb756f0a69dd3c3b8e Mon Sep 17 00:00:00 2001
From: Paolo Ricciuti 
Date: Thu, 22 May 2025 17:21:50 +0200
Subject: [PATCH 071/259] feat: functional template generation (#15538)

Co-authored-by: Rich Harris 
---
 .changeset/forty-llamas-unite.md              |   5 +
 .changeset/smart-boats-accept.md              |   5 +
 .../3-transform/client/transform-client.js    |   7 +-
 .../fix-attribute-casing.js                   |  18 ++
 .../client/transform-template/index.js        |  68 ++++++++
 .../client/transform-template/template.js     | 162 ++++++++++++++++++
 .../client/transform-template/types.d.ts      |  22 +++
 .../phases/3-transform/client/types.d.ts      |  23 +--
 .../3-transform/client/visitors/AwaitBlock.js |   2 +-
 .../3-transform/client/visitors/Comment.js    |   2 +-
 .../3-transform/client/visitors/EachBlock.js  |   2 +-
 .../3-transform/client/visitors/Fragment.js   | 136 ++-------------
 .../3-transform/client/visitors/HtmlTag.js    |   2 +-
 .../3-transform/client/visitors/IfBlock.js    |   2 +-
 .../3-transform/client/visitors/KeyBlock.js   |   2 +-
 .../client/visitors/RegularElement.js         |  66 ++-----
 .../3-transform/client/visitors/RenderTag.js  |   2 +-
 .../client/visitors/SlotElement.js            |   2 +-
 .../client/visitors/SvelteBoundary.js         |   2 +-
 .../client/visitors/SvelteElement.js          |   2 +-
 .../client/visitors/shared/component.js       |  23 ++-
 .../client/visitors/shared/fragment.js        |   4 +-
 .../server/visitors/RegularElement.js         |   6 +-
 .../src/compiler/phases/3-transform/utils.js  |   2 +-
 packages/svelte/src/compiler/types/index.d.ts |   9 +
 .../svelte/src/compiler/validate-options.js   |   2 +
 packages/svelte/src/constants.js              |   2 +
 .../src/internal/client/dev/elements.js       |   2 +-
 .../src/internal/client/dom/operations.js     |  41 +++++
 .../src/internal/client/dom/reconciler.js     |   2 +-
 .../src/internal/client/dom/template.js       | 145 +++++++++++++---
 .../svelte/src/internal/client/dom/types.d.ts |   4 +
 packages/svelte/src/internal/client/index.js  |  12 +-
 .../svelte/src/internal/client/types.d.ts     |   4 +
 .../svelte/src/internal/shared/types.d.ts     |   4 -
 packages/svelte/tests/helpers.js              |   2 +
 packages/svelte/tests/hydration/test.ts       |  11 +-
 packages/svelte/tests/runtime-browser/test.ts |   3 +-
 .../svelte/tests/runtime-legacy/shared.ts     |   3 +-
 .../custom-element-attributes/main.svelte     |  26 +--
 .../samples/functional-templating/_config.js  |   9 +
 .../samples/functional-templating/main.svelte |   1 +
 .../_expected/client/index.svelte.js          |   2 +-
 .../_expected/client/index.svelte.js          |   2 +-
 .../_expected/client/main.svelte.js           |   2 +-
 .../_expected/client/index.svelte.js          |   2 +-
 .../samples/functional-templating/_config.js  |   7 +
 .../_expected/client/index.svelte.js          |  25 +++
 .../_expected/server/index.svelte.js          |   5 +
 .../functional-templating/index.svelte        |   6 +
 .../_expected/client/index.svelte.js          |   2 +-
 .../hmr/_expected/client/index.svelte.js      |   2 +-
 .../_expected/client/index.svelte.js          |   2 +-
 .../purity/_expected/client/index.svelte.js   |   2 +-
 .../_expected/client/index.svelte.js          |   2 +-
 .../_expected/server/index.svelte.js          |   2 +-
 .../_expected/client/index.svelte.js          |   2 +-
 .../_expected/server/index.svelte.js          |   2 +-
 .../_expected/client/index.svelte.js          |   2 +-
 packages/svelte/types/index.d.ts              |  18 ++
 playgrounds/sandbox/run.js                    |  18 +-
 61 files changed, 666 insertions(+), 288 deletions(-)
 create mode 100644 .changeset/forty-llamas-unite.md
 create mode 100644 .changeset/smart-boats-accept.md
 create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/transform-template/fix-attribute-casing.js
 create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/transform-template/index.js
 create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/transform-template/template.js
 create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/transform-template/types.d.ts
 create mode 100644 packages/svelte/src/internal/client/dom/types.d.ts
 create mode 100644 packages/svelte/tests/runtime-runes/samples/functional-templating/_config.js
 create mode 100644 packages/svelte/tests/runtime-runes/samples/functional-templating/main.svelte
 create mode 100644 packages/svelte/tests/snapshot/samples/functional-templating/_config.js
 create mode 100644 packages/svelte/tests/snapshot/samples/functional-templating/_expected/client/index.svelte.js
 create mode 100644 packages/svelte/tests/snapshot/samples/functional-templating/_expected/server/index.svelte.js
 create mode 100644 packages/svelte/tests/snapshot/samples/functional-templating/index.svelte

diff --git a/.changeset/forty-llamas-unite.md b/.changeset/forty-llamas-unite.md
new file mode 100644
index 0000000000..933cece1c6
--- /dev/null
+++ b/.changeset/forty-llamas-unite.md
@@ -0,0 +1,5 @@
+---
+'svelte': minor
+---
+
+feat: XHTML compliance
diff --git a/.changeset/smart-boats-accept.md b/.changeset/smart-boats-accept.md
new file mode 100644
index 0000000000..4cdfeb30d0
--- /dev/null
+++ b/.changeset/smart-boats-accept.md
@@ -0,0 +1,5 @@
+---
+'svelte': minor
+---
+
+feat: add `fragments: 'html' | 'tree'` option for wider CSP compliance
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js
index f2eda3a7d2..e2e006c14b 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js
@@ -154,10 +154,6 @@ export function client_component(analysis, options) {
 		legacy_reactive_imports: [],
 		legacy_reactive_statements: new Map(),
 		metadata: {
-			context: {
-				template_needs_import_node: false,
-				template_contains_script_tag: false
-			},
 			namespace: options.namespace,
 			bound_contenteditable: false
 		},
@@ -174,8 +170,7 @@ export function client_component(analysis, options) {
 		update: /** @type {any} */ (null),
 		expressions: /** @type {any} */ (null),
 		after_update: /** @type {any} */ (null),
-		template: /** @type {any} */ (null),
-		locations: /** @type {any} */ (null)
+		template: /** @type {any} */ (null)
 	};
 
 	const module = /** @type {ESTree.Program} */ (
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-template/fix-attribute-casing.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/fix-attribute-casing.js
new file mode 100644
index 0000000000..ce56c43d7c
--- /dev/null
+++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/fix-attribute-casing.js
@@ -0,0 +1,18 @@
+const svg_attributes =
+	'accent-height accumulate additive alignment-baseline allowReorder alphabetic amplitude arabic-form ascent attributeName attributeType autoReverse azimuth baseFrequency baseline-shift baseProfile bbox begin bias by calcMode cap-height class clip clipPathUnits clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering contentScriptType contentStyleType cursor cx cy d decelerate descent diffuseConstant direction display divisor dominant-baseline dur dx dy edgeMode elevation enable-background end exponent externalResourcesRequired fill fill-opacity fill-rule filter filterRes filterUnits flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight format from fr fx fy g1 g2 glyph-name glyph-orientation-horizontal glyph-orientation-vertical glyphRef gradientTransform gradientUnits hanging height href horiz-adv-x horiz-origin-x id ideographic image-rendering in in2 intercept k k1 k2 k3 k4 kernelMatrix kernelUnitLength kerning keyPoints keySplines keyTimes lang lengthAdjust letter-spacing lighting-color limitingConeAngle local marker-end marker-mid marker-start markerHeight markerUnits markerWidth mask maskContentUnits maskUnits mathematical max media method min mode name numOctaves offset onabort onactivate onbegin onclick onend onerror onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup onrepeat onresize onscroll onunload opacity operator order orient orientation origin overflow overline-position overline-thickness panose-1 paint-order pathLength patternContentUnits patternTransform patternUnits pointer-events points pointsAtX pointsAtY pointsAtZ preserveAlpha preserveAspectRatio primitiveUnits r radius refX refY rendering-intent repeatCount repeatDur requiredExtensions requiredFeatures restart result rotate rx ry scale seed shape-rendering slope spacing specularConstant specularExponent speed spreadMethod startOffset stdDeviation stemh stemv stitchTiles stop-color stop-opacity strikethrough-position strikethrough-thickness string stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style surfaceScale systemLanguage tabindex tableValues target targetX targetY text-anchor text-decoration text-rendering textLength to transform type u1 u2 underline-position underline-thickness unicode unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical values version vert-adv-y vert-origin-x vert-origin-y viewBox viewTarget visibility width widths word-spacing writing-mode x x-height x1 x2 xChannelSelector xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y y1 y2 yChannelSelector z zoomAndPan'.split(
+		' '
+	);
+
+const svg_attribute_lookup = new Map();
+
+svg_attributes.forEach((name) => {
+	svg_attribute_lookup.set(name.toLowerCase(), name);
+});
+
+/**
+ * @param {string} name
+ */
+export default function fix_attribute_casing(name) {
+	name = name.toLowerCase();
+	return svg_attribute_lookup.get(name) || name;
+}
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-template/index.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/index.js
new file mode 100644
index 0000000000..d0327e702a
--- /dev/null
+++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/index.js
@@ -0,0 +1,68 @@
+/** @import { Location } from 'locate-character' */
+/** @import { Namespace } from '#compiler' */
+/** @import { ComponentClientTransformState } from '../types.js' */
+/** @import { Node } from './types.js' */
+import { TEMPLATE_USE_MATHML, TEMPLATE_USE_SVG } from '../../../../../constants.js';
+import { dev, locator } from '../../../../state.js';
+import * as b from '../../../../utils/builders.js';
+
+/**
+ * @param {Node[]} nodes
+ */
+function build_locations(nodes) {
+	const array = b.array([]);
+
+	for (const node of nodes) {
+		if (node.type !== 'element') continue;
+
+		const { line, column } = /** @type {Location} */ (locator(node.start));
+
+		const expression = b.array([b.literal(line), b.literal(column)]);
+		const children = build_locations(node.children);
+
+		if (children.elements.length > 0) {
+			expression.elements.push(children);
+		}
+
+		array.elements.push(expression);
+	}
+
+	return array;
+}
+
+/**
+ * @param {ComponentClientTransformState} state
+ * @param {Namespace} namespace
+ * @param {number} [flags]
+ */
+export function transform_template(state, namespace, flags = 0) {
+	const tree = state.options.fragments === 'tree';
+
+	const expression = tree ? state.template.as_tree() : state.template.as_html();
+
+	if (tree) {
+		if (namespace === 'svg') flags |= TEMPLATE_USE_SVG;
+		if (namespace === 'mathml') flags |= TEMPLATE_USE_MATHML;
+	}
+
+	let call = b.call(
+		tree ? `$.from_tree` : `$.from_${namespace}`,
+		expression,
+		flags ? b.literal(flags) : undefined
+	);
+
+	if (state.template.contains_script_tag) {
+		call = b.call(`$.with_script`, call);
+	}
+
+	if (dev) {
+		call = b.call(
+			'$.add_locations',
+			call,
+			b.member(b.id(state.analysis.name), '$.FILENAME', true),
+			build_locations(state.template.nodes)
+		);
+	}
+
+	return call;
+}
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-template/template.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/template.js
new file mode 100644
index 0000000000..8f7f8a1f43
--- /dev/null
+++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/template.js
@@ -0,0 +1,162 @@
+/** @import { AST } from '#compiler' */
+/** @import { Node, Element } from './types'; */
+import { escape_html } from '../../../../../escaping.js';
+import { is_void } from '../../../../../utils.js';
+import * as b from '#compiler/builders';
+import fix_attribute_casing from './fix-attribute-casing.js';
+import { regex_starts_with_newline } from '../../../patterns.js';
+
+export class Template {
+	/**
+	 * `true` if HTML template contains a `
 
 
diff --git a/packages/svelte/tests/runtime-runes/samples/functional-templating/_config.js b/packages/svelte/tests/runtime-runes/samples/functional-templating/_config.js
new file mode 100644
index 0000000000..5d36f2e969
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/functional-templating/_config.js
@@ -0,0 +1,9 @@
+import { test } from '../../test';
+
+export default test({
+	compileOptions: {
+		fragments: 'tree'
+	},
+
+	html: `

hello

` +}); diff --git a/packages/svelte/tests/runtime-runes/samples/functional-templating/main.svelte b/packages/svelte/tests/runtime-runes/samples/functional-templating/main.svelte new file mode 100644 index 0000000000..302a01f335 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/functional-templating/main.svelte @@ -0,0 +1 @@ +

hello

diff --git a/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/client/index.svelte.js index 3e5a12ed9d..9bb45ebf78 100644 --- a/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/client/index.svelte.js @@ -5,7 +5,7 @@ function increment(_, counter) { counter.count += 1; } -var root = $.template(` `, 1); +var root = $.from_html(` `, 1); export default function Await_block_scope($$anchor) { let counter = $.proxy({ count: 0 }); diff --git a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js index 390e86a351..ba3f4b155a 100644 --- a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js @@ -10,7 +10,7 @@ const snippet = ($$anchor) => { $.append($$anchor, text); }; -var root = $.template(` `, 1); +var root = $.from_html(` `, 1); export default function Bind_component_snippet($$anchor) { let value = $.state(''); diff --git a/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js b/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js index 219db6ffd5..3a13fa7e15 100644 --- a/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js +++ b/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js @@ -1,7 +1,7 @@ import 'svelte/internal/disclose-version'; import * as $ from 'svelte/internal/client'; -var root = $.template(`
`, 3); +var root = $.from_html(`
`, 3); export default function Main($$anchor) { // needs to be a snapshot test because jsdom does auto-correct the attribute casing diff --git a/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/client/index.svelte.js index 3d46a679b8..804a7c26f1 100644 --- a/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/client/index.svelte.js @@ -2,7 +2,7 @@ import 'svelte/internal/disclose-version'; import 'svelte/internal/flags/legacy'; import * as $ from 'svelte/internal/client'; -var root_1 = $.template(`

`); +var root_1 = $.from_html(`

`); export default function Each_index_non_null($$anchor) { var fragment = $.comment(); diff --git a/packages/svelte/tests/snapshot/samples/functional-templating/_config.js b/packages/svelte/tests/snapshot/samples/functional-templating/_config.js new file mode 100644 index 0000000000..23231c969b --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/functional-templating/_config.js @@ -0,0 +1,7 @@ +import { test } from '../../test'; + +export default test({ + compileOptions: { + fragments: 'tree' + } +}); diff --git a/packages/svelte/tests/snapshot/samples/functional-templating/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/functional-templating/_expected/client/index.svelte.js new file mode 100644 index 0000000000..792d5421e1 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/functional-templating/_expected/client/index.svelte.js @@ -0,0 +1,25 @@ +import 'svelte/internal/disclose-version'; +import 'svelte/internal/flags/legacy'; +import * as $ from 'svelte/internal/client'; + +var root = $.from_tree( + [ + ['h1', null, 'hello'], + ' ', + [ + 'div', + { class: 'potato' }, + ['p', null, 'child element'], + ' ', + ['p', null, 'another child element'] + ] + ], + 1 +); + +export default function Functional_templating($$anchor) { + var fragment = root(); + + $.next(2); + $.append($$anchor, fragment); +} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/functional-templating/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/functional-templating/_expected/server/index.svelte.js new file mode 100644 index 0000000000..dc49c0c213 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/functional-templating/_expected/server/index.svelte.js @@ -0,0 +1,5 @@ +import * as $ from 'svelte/internal/server'; + +export default function Functional_templating($$payload) { + $$payload.out += `

hello

child element

another child element

`; +} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/functional-templating/index.svelte b/packages/svelte/tests/snapshot/samples/functional-templating/index.svelte new file mode 100644 index 0000000000..c0fe8965b8 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/functional-templating/index.svelte @@ -0,0 +1,6 @@ +

hello

+ +
+

child element

+

another child element

+
diff --git a/packages/svelte/tests/snapshot/samples/hello-world/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/hello-world/_expected/client/index.svelte.js index 899c126001..68fdaa4570 100644 --- a/packages/svelte/tests/snapshot/samples/hello-world/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/hello-world/_expected/client/index.svelte.js @@ -2,7 +2,7 @@ import 'svelte/internal/disclose-version'; import 'svelte/internal/flags/legacy'; import * as $ from 'svelte/internal/client'; -var root = $.template(`

hello world

`); +var root = $.from_html(`

hello world

`); export default function Hello_world($$anchor) { var h1 = root(); diff --git a/packages/svelte/tests/snapshot/samples/hmr/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/hmr/_expected/client/index.svelte.js index 3c8322500b..1fac1338c5 100644 --- a/packages/svelte/tests/snapshot/samples/hmr/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/hmr/_expected/client/index.svelte.js @@ -2,7 +2,7 @@ import 'svelte/internal/disclose-version'; import 'svelte/internal/flags/legacy'; import * as $ from 'svelte/internal/client'; -var root = $.template(`

hello world

`); +var root = $.from_html(`

hello world

`); function Hmr($$anchor) { var h1 = root(); diff --git a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js index 21f6ed9680..b46acee82e 100644 --- a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js @@ -2,7 +2,7 @@ import 'svelte/internal/disclose-version'; import * as $ from 'svelte/internal/client'; var on_click = (_, count) => $.update(count); -var root = $.template(`

`, 1); +var root = $.from_html(`

`, 1); export default function Nullish_coallescence_omittance($$anchor) { let name = 'world'; diff --git a/packages/svelte/tests/snapshot/samples/purity/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/purity/_expected/client/index.svelte.js index f661dbc01d..a351851875 100644 --- a/packages/svelte/tests/snapshot/samples/purity/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/purity/_expected/client/index.svelte.js @@ -2,7 +2,7 @@ import 'svelte/internal/disclose-version'; import 'svelte/internal/flags/legacy'; import * as $ from 'svelte/internal/client'; -var root = $.template(`

`, 1); +var root = $.from_html(`

`, 1); export default function Purity($$anchor) { var fragment = root(); diff --git a/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/client/index.svelte.js index 541b56a407..78147659ff 100644 --- a/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/client/index.svelte.js @@ -1,7 +1,7 @@ import 'svelte/internal/disclose-version'; import * as $ from 'svelte/internal/client'; -var root = $.template(`

we don't need to traverse these nodes

or

these

ones

these

trailing

nodes

can

be

completely

ignored

`, 3); +var root = $.from_html(`

we don't need to traverse these nodes

or

these

ones

these

trailing

nodes

can

be

completely

ignored

`, 3); export default function Skip_static_subtree($$anchor, $$props) { var fragment = root(); diff --git a/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/server/index.svelte.js index e694c12647..a587c05e2c 100644 --- a/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/server/index.svelte.js @@ -3,5 +3,5 @@ import * as $ from 'svelte/internal/server'; export default function Skip_static_subtree($$payload, $$props) { let { title, content } = $$props; - $$payload.out += `

${$.escape(title)}

we don't need to traverse these nodes

or

these

ones

${$.html(content)}

these

trailing

nodes

can

be

completely

ignored

`; + $$payload.out += `

${$.escape(title)}

we don't need to traverse these nodes

or

these

ones

${$.html(content)}

these

trailing

nodes

can

be

completely

ignored

`; } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/client/index.svelte.js index a67210e541..c446b3d3ef 100644 --- a/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/client/index.svelte.js @@ -8,7 +8,7 @@ function reset(_, str, tpl) { $.set(tpl, ``); } -var root = $.template(` `, 1); +var root = $.from_html(` `, 1); export default function State_proxy_literal($$anchor) { let str = $.state(''); diff --git a/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/server/index.svelte.js index 7b2a884d70..f814dd4f84 100644 --- a/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/server/index.svelte.js @@ -11,5 +11,5 @@ export default function State_proxy_literal($$payload) { tpl = ``; } - $$payload.out += ` `; + $$payload.out += ` `; } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/text-nodes-deriveds/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/text-nodes-deriveds/_expected/client/index.svelte.js index d520d1ef24..464435cb0a 100644 --- a/packages/svelte/tests/snapshot/samples/text-nodes-deriveds/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/text-nodes-deriveds/_expected/client/index.svelte.js @@ -1,7 +1,7 @@ import 'svelte/internal/disclose-version'; import * as $ from 'svelte/internal/client'; -var root = $.template(`

`); +var root = $.from_html(`

`); export default function Text_nodes_deriveds($$anchor) { let count1 = 0; diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index fcc1ec315c..201dbed9aa 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -985,6 +985,15 @@ declare module 'svelte/compiler' { * @default false */ preserveWhitespace?: boolean; + /** + * Which strategy to use when cloning DOM fragments: + * + * - `html` populates a `