From 60b22ab933c45ce329f4e7919be0b0b73a65ecbb Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Sat, 17 May 2025 10:07:21 +0200 Subject: [PATCH 01/11] 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 02/11] 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 03/11] 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 04/11] (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 05/11] 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 07/11] 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 08/11] 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 09/11] 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 10/11] 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 11/11] 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).