From e5d0cd2eb47834bf7584a250e749a564c73a8f1d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 31 May 2025 15:38:49 -0400 Subject: [PATCH 1/5] Version Packages (#16045) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/orange-tips-pull.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/orange-tips-pull.md diff --git a/.changeset/orange-tips-pull.md b/.changeset/orange-tips-pull.md deleted file mode 100644 index 4d474dafaf..0000000000 --- a/.changeset/orange-tips-pull.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: treat transitive dependencies of each blocks as mutable in legacy mode if item is mutated diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index da35e7cd68..4623f64766 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.33.11 + +### Patch Changes + +- fix: treat transitive dependencies of each blocks as mutable in legacy mode if item is mutated ([#16038](https://github.com/sveltejs/svelte/pull/16038)) + ## 5.33.10 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index d1cf6c428b..2ee065730c 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.33.10", + "version": "5.33.11", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index eef48b0cbb..4cbf73e7f9 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.33.10'; +export const VERSION = '5.33.11'; export const PUBLIC_VERSION = '5'; From b5fcd112c60012276e3416e65feae3ac16bfdaf1 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sun, 1 Jun 2025 00:53:48 -0700 Subject: [PATCH 2/5] fix: correctly transform reassignments to class fields in SSR mode (#16051) * fix: correctly transform reassignments to class fields in SSR mode * add test, fix more stuff * fix --- .changeset/rude-drinks-relate.md | 5 ++++ .../server/visitors/AssignmentExpression.js | 13 +++++++++-- .../_expected/client/index.svelte.js | 23 ++++++++++++++++++- .../_expected/server/index.svelte.js | 23 ++++++++++++++++++- .../index.svelte | 7 ++++-- 5 files changed, 65 insertions(+), 6 deletions(-) create mode 100644 .changeset/rude-drinks-relate.md diff --git a/.changeset/rude-drinks-relate.md b/.changeset/rude-drinks-relate.md new file mode 100644 index 0000000000..d0eab6ba11 --- /dev/null +++ b/.changeset/rude-drinks-relate.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: correctly transform reassignments to class fields in SSR mode diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js index 466682fb82..49a284d12b 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js @@ -24,7 +24,12 @@ export function AssignmentExpression(node, context) { * @returns {Expression | null} */ function build_assignment(operator, left, right, context) { - if (context.state.analysis.runes && left.type === 'MemberExpression') { + if ( + context.state.analysis.runes && + left.type === 'MemberExpression' && + left.object.type === 'ThisExpression' && + !left.computed + ) { const name = get_name(left.property); const field = name && context.state.state_fields.get(name); @@ -44,7 +49,11 @@ function build_assignment(operator, left, right, context) { /** @type {Expression} */ (context.visit(right)) ); } - } else if (field && (field.type === '$derived' || field.type === '$derived.by')) { + } else if ( + field && + (field.type === '$derived' || field.type === '$derived.by') && + left.property.type === 'PrivateIdentifier' + ) { let value = /** @type {Expression} */ ( context.visit(build_assignment_value(operator, left, right)) ); diff --git a/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/client/index.svelte.js index 2133974176..c9725d6718 100644 --- a/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/client/index.svelte.js @@ -5,7 +5,7 @@ export default function Class_state_field_constructor_assignment($$anchor, $$pro $.push($$props, true); class Foo { - #a = $.state(); + #a = $.state(0); get a() { return $.get(this.#a); @@ -16,10 +16,31 @@ export default function Class_state_field_constructor_assignment($$anchor, $$pro } #b = $.state(); + #foo = $.derived(() => ({ bar: this.a * 2 })); + + get foo() { + return $.get(this.#foo); + } + + set foo(value) { + $.set(this.#foo, value); + } + + #bar = $.derived(() => ({ baz: this.foo })); + + get bar() { + return $.get(this.#bar); + } + + set bar(value) { + $.set(this.#bar, value); + } constructor() { this.a = 1; $.set(this.#b, 2); + this.foo.bar = 3; + this.bar = 4; } } diff --git a/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/server/index.svelte.js index 2a115a4983..abfc264fea 100644 --- a/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/server/index.svelte.js @@ -4,12 +4,33 @@ export default function Class_state_field_constructor_assignment($$payload, $$pr $.push(); class Foo { - a; + a = 0; #b; + #foo = $.derived(() => ({ bar: this.a * 2 })); + + get foo() { + return this.#foo(); + } + + set foo($$value) { + return this.#foo($$value); + } + + #bar = $.derived(() => ({ baz: this.foo })); + + get bar() { + return this.#bar(); + } + + set bar($$value) { + return this.#bar($$value); + } constructor() { this.a = 1; this.#b = 2; + this.foo.bar = 3; + this.bar = 4; } } diff --git a/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/index.svelte b/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/index.svelte index a3ff5917e7..127cfd4d2a 100644 --- a/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/index.svelte +++ b/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/index.svelte @@ -1,11 +1,14 @@ From 03273b78791c3359865040081fc58b53de782db8 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 1 Jun 2025 16:13:30 -0400 Subject: [PATCH 3/5] chore: better HTML normalization test helper --- packages/svelte/tests/html_equal.js | 96 +++++++++++-------- .../samples/binding-select/_config.js | 4 +- .../samples/input-list/_config.js | 4 +- .../samples/namespace-html/_config.js | 2 +- .../samples/select-in-each/_config.js | 4 +- .../svelte/tests/runtime-legacy/shared.ts | 4 +- 6 files changed, 66 insertions(+), 48 deletions(-) diff --git a/packages/svelte/tests/html_equal.js b/packages/svelte/tests/html_equal.js index 4c9e2a7253..f3ab5c54ca 100644 --- a/packages/svelte/tests/html_equal.js +++ b/packages/svelte/tests/html_equal.js @@ -3,6 +3,15 @@ import { assert } from 'vitest'; /** @param {Element} node */ function clean_children(node) { let previous = null; + let has_element_children = false; + let template = + node.nodeName === 'TEMPLATE' ? /** @type {HTMLTemplateElement} */ (node) : undefined; + + if (template) { + const div = document.createElement('div'); + div.append(template.content); + node = div; + } // sort attributes const attributes = Array.from(node.attributes).sort((a, b) => { @@ -14,6 +23,10 @@ function clean_children(node) { }); attributes.forEach((attr) => { + if ((attr.name === 'onload' || attr.name === 'onerror') && attr.value === 'this.__e=event') { + return; + } + node.setAttribute(attr.name, attr.value); }); @@ -27,23 +40,35 @@ function clean_children(node) { node.tagName !== 'tspan' ) { node.removeChild(child); + continue; } - text.data = text.data.replace(/[ \t\n\r\f]+/g, '\n'); + text.data = text.data.replace(/[^\S]+/g, ' '); if (previous && previous.nodeType === 3) { const prev = /** @type {Text} */ (previous); prev.data += text.data; - prev.data = prev.data.replace(/[ \t\n\r\f]+/g, '\n'); - node.removeChild(text); + text = prev; + text.data = text.data.replace(/[^\S]+/g, ' '); + + continue; } } else if (child.nodeType === 8) { // comment - // do nothing - } else { + child.remove(); + continue; + } else if (child.nodeType === 1) { + if (previous?.nodeType === 3) { + const prev = /** @type {Text} */ (previous); + prev.data = prev.data.replace(/^[^\S]+$/, '\n'); + } else if (previous?.nodeType === 1) { + node.insertBefore(document.createTextNode('\n'), child); + } + + has_element_children = true; clean_children(/** @type {Element} */ (child)); } @@ -53,37 +78,35 @@ function clean_children(node) { // collapse whitespace if (node.firstChild && node.firstChild.nodeType === 3) { const text = /** @type {Text} */ (node.firstChild); - text.data = text.data.replace(/^[ \t\n\r\f]+/, ''); - if (!text.data.length) node.removeChild(text); + text.data = text.data.trimStart(); } if (node.lastChild && node.lastChild.nodeType === 3) { const text = /** @type {Text} */ (node.lastChild); - text.data = text.data.replace(/[ \t\n\r\f]+$/, ''); - if (!text.data.length) node.removeChild(text); + text.data = text.data.trimEnd(); + } + + if (has_element_children && node.parentNode) { + node.innerHTML = `\n\t${node.innerHTML.replace(/\n/g, '\n\t')}\n`; + } + + if (template) { + template.innerHTML = node.innerHTML; } } /** * @param {Window} window * @param {string} html - * @param {{ removeDataSvelte?: boolean, preserveComments?: boolean }} param2 + * @param {{ preserveComments?: boolean }} opts */ -export function normalize_html( - window, - html, - { removeDataSvelte = false, preserveComments = false } -) { +export function normalize_html(window, html, { preserveComments = false } = {}) { try { const node = window.document.createElement('div'); - node.innerHTML = html - .replace(/()/g, preserveComments ? '$1' : '') - .replace(/(data-svelte-h="[^"]+")/g, removeDataSvelte ? '' : '$1') - .replace(/>[ \t\n\r\f]+<') - // Strip out the special onload/onerror hydration events from the test output - .replace(/\s?onerror="this.__e=event"|\s?onload="this.__e=event"/g, '') - .trim(); + node.innerHTML = html.replace(/()/g, preserveComments ? '$1' : '').trim(); + clean_children(node); + return node.innerHTML; } catch (err) { throw new Error(`Failed to normalize HTML:\n${html}\nCause: ${err}`); @@ -98,10 +121,7 @@ export function normalize_new_line(html) { return html.replace(/\r\n/g, '\n'); } -/** - * @param {{ removeDataSvelte?: boolean }} options - */ -export function setup_html_equal(options = {}) { +export function setup_html_equal() { /** * @param {string} actual * @param {string} expected @@ -109,11 +129,7 @@ export function setup_html_equal(options = {}) { */ const assert_html_equal = (actual, expected, message) => { try { - assert.deepEqual( - normalize_html(window, actual, options), - normalize_html(window, expected, options), - message - ); + assert.deepEqual(normalize_html(window, actual), normalize_html(window, expected), message); } catch (e) { if (Error.captureStackTrace) Error.captureStackTrace(/** @type {Error} */ (e), assert_html_equal); @@ -137,15 +153,17 @@ export function setup_html_equal(options = {}) { try { assert.deepEqual( withoutNormalizeHtml - ? normalize_new_line(actual.trim()) - .replace(/(\sdata-svelte-h="[^"]+")/g, options.removeDataSvelte ? '' : '$1') - .replace(/()/g, preserveComments !== false ? '$1' : '') - : normalize_html(window, actual.trim(), { ...options, preserveComments }), + ? normalize_new_line(actual.trim()).replace( + /()/g, + preserveComments !== false ? '$1' : '' + ) + : normalize_html(window, actual.trim(), { preserveComments }), withoutNormalizeHtml - ? normalize_new_line(expected.trim()) - .replace(/(\sdata-svelte-h="[^"]+")/g, options.removeDataSvelte ? '' : '$1') - .replace(/()/g, preserveComments !== false ? '$1' : '') - : normalize_html(window, expected.trim(), { ...options, preserveComments }), + ? normalize_new_line(expected.trim()).replace( + /()/g, + preserveComments !== false ? '$1' : '' + ) + : normalize_html(window, expected.trim(), { preserveComments }), message ); } catch (e) { diff --git a/packages/svelte/tests/runtime-legacy/samples/binding-select/_config.js b/packages/svelte/tests/runtime-legacy/samples/binding-select/_config.js index 2507f5fc83..996f68e39f 100644 --- a/packages/svelte/tests/runtime-legacy/samples/binding-select/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/binding-select/_config.js @@ -25,7 +25,7 @@ export default test({

selected: one

@@ -54,7 +54,7 @@ export default test({

selected: two

diff --git a/packages/svelte/tests/runtime-legacy/samples/input-list/_config.js b/packages/svelte/tests/runtime-legacy/samples/input-list/_config.js index fe6a29207d..1e95aaafa6 100644 --- a/packages/svelte/tests/runtime-legacy/samples/input-list/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/input-list/_config.js @@ -4,7 +4,9 @@ export default test({ html: ` - + + ` }); diff --git a/packages/svelte/tests/runtime-legacy/samples/namespace-html/_config.js b/packages/svelte/tests/runtime-legacy/samples/namespace-html/_config.js index 3be9f0e925..b7ecd04def 100644 --- a/packages/svelte/tests/runtime-legacy/samples/namespace-html/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/namespace-html/_config.js @@ -9,7 +9,7 @@ export default test({ - +
hi
`, diff --git a/packages/svelte/tests/runtime-legacy/samples/select-in-each/_config.js b/packages/svelte/tests/runtime-legacy/samples/select-in-each/_config.js index 4c94ea1e01..df03b7a053 100644 --- a/packages/svelte/tests/runtime-legacy/samples/select-in-each/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/select-in-each/_config.js @@ -7,7 +7,7 @@ export default test({ target.innerHTML, ` selected: a @@ -23,7 +23,7 @@ export default test({ target.innerHTML, ` selected: b diff --git a/packages/svelte/tests/runtime-legacy/shared.ts b/packages/svelte/tests/runtime-legacy/shared.ts index 690a7e3d98..c94c4ed422 100644 --- a/packages/svelte/tests/runtime-legacy/shared.ts +++ b/packages/svelte/tests/runtime-legacy/shared.ts @@ -86,9 +86,7 @@ function unhandled_rejection_handler(err: Error) { const listeners = process.rawListeners('unhandledRejection'); -const { assert_html_equal, assert_html_equal_with_options } = setup_html_equal({ - removeDataSvelte: true -}); +const { assert_html_equal, assert_html_equal_with_options } = setup_html_equal(); beforeAll(() => { // @ts-expect-error TODO huh? From 7c10b237d15c563e5a9160f19fad04be8cc9c77f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 1 Jun 2025 16:20:36 -0400 Subject: [PATCH 4/5] simplify --- packages/svelte/tests/html_equal.js | 108 ++++++++---------- .../svelte/tests/runtime-legacy/shared.ts | 4 +- 2 files changed, 50 insertions(+), 62 deletions(-) diff --git a/packages/svelte/tests/html_equal.js b/packages/svelte/tests/html_equal.js index f3ab5c54ca..22e52417a2 100644 --- a/packages/svelte/tests/html_equal.js +++ b/packages/svelte/tests/html_equal.js @@ -121,63 +121,53 @@ export function normalize_new_line(html) { return html.replace(/\r\n/g, '\n'); } -export function setup_html_equal() { - /** - * @param {string} actual - * @param {string} expected - * @param {string} [message] - */ - const assert_html_equal = (actual, expected, message) => { - try { - assert.deepEqual(normalize_html(window, actual), normalize_html(window, expected), message); - } catch (e) { - if (Error.captureStackTrace) - Error.captureStackTrace(/** @type {Error} */ (e), assert_html_equal); - throw e; - } - }; - - /** - * - * @param {string} actual - * @param {string} expected - * @param {{ preserveComments?: boolean, withoutNormalizeHtml?: boolean }} param2 - * @param {string} [message] - */ - const assert_html_equal_with_options = ( - actual, - expected, - { preserveComments, withoutNormalizeHtml }, - message - ) => { - try { - assert.deepEqual( - withoutNormalizeHtml - ? normalize_new_line(actual.trim()).replace( - /()/g, - preserveComments !== false ? '$1' : '' - ) - : normalize_html(window, actual.trim(), { preserveComments }), - withoutNormalizeHtml - ? normalize_new_line(expected.trim()).replace( - /()/g, - preserveComments !== false ? '$1' : '' - ) - : normalize_html(window, expected.trim(), { preserveComments }), - message - ); - } catch (e) { - if (Error.captureStackTrace) - Error.captureStackTrace(/** @type {Error} */ (e), assert_html_equal_with_options); - throw e; - } - }; - - return { - assert_html_equal, - assert_html_equal_with_options - }; -} +/** + * @param {string} actual + * @param {string} expected + * @param {string} [message] + */ +export const assert_html_equal = (actual, expected, message) => { + try { + assert.deepEqual(normalize_html(window, actual), normalize_html(window, expected), message); + } catch (e) { + if (Error.captureStackTrace) + Error.captureStackTrace(/** @type {Error} */ (e), assert_html_equal); + throw e; + } +}; -// Common case without options -export const { assert_html_equal, assert_html_equal_with_options } = setup_html_equal(); +/** + * + * @param {string} actual + * @param {string} expected + * @param {{ preserveComments?: boolean, withoutNormalizeHtml?: boolean }} param2 + * @param {string} [message] + */ +export const assert_html_equal_with_options = ( + actual, + expected, + { preserveComments, withoutNormalizeHtml }, + message +) => { + try { + assert.deepEqual( + withoutNormalizeHtml + ? normalize_new_line(actual.trim()).replace( + /()/g, + preserveComments !== false ? '$1' : '' + ) + : normalize_html(window, actual.trim(), { preserveComments }), + withoutNormalizeHtml + ? normalize_new_line(expected.trim()).replace( + /()/g, + preserveComments !== false ? '$1' : '' + ) + : normalize_html(window, expected.trim(), { preserveComments }), + message + ); + } catch (e) { + if (Error.captureStackTrace) + Error.captureStackTrace(/** @type {Error} */ (e), assert_html_equal_with_options); + throw e; + } +}; diff --git a/packages/svelte/tests/runtime-legacy/shared.ts b/packages/svelte/tests/runtime-legacy/shared.ts index c94c4ed422..c0d1177a82 100644 --- a/packages/svelte/tests/runtime-legacy/shared.ts +++ b/packages/svelte/tests/runtime-legacy/shared.ts @@ -7,7 +7,7 @@ import { flushSync, hydrate, mount, unmount } from 'svelte'; import { render } from 'svelte/server'; import { afterAll, assert, beforeAll } from 'vitest'; import { compile_directory, fragments } from '../helpers.js'; -import { setup_html_equal } from '../html_equal.js'; +import { assert_html_equal, assert_html_equal_with_options } from '../html_equal.js'; import { raf } from '../animation-helpers.js'; import type { CompileOptions } from '#compiler'; import { suite_with_variants, type BaseTest } from '../suite.js'; @@ -86,8 +86,6 @@ function unhandled_rejection_handler(err: Error) { const listeners = process.rawListeners('unhandledRejection'); -const { assert_html_equal, assert_html_equal_with_options } = setup_html_equal(); - beforeAll(() => { // @ts-expect-error TODO huh? process.prependListener('unhandledRejection', unhandled_rejection_handler); From 73796c49b05b2d2eb8c5120a78f599ebaaade377 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 1 Jun 2025 16:45:38 -0400 Subject: [PATCH 5/5] simplify/robustify --- packages/svelte/tests/html_equal.js | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/packages/svelte/tests/html_equal.js b/packages/svelte/tests/html_equal.js index 22e52417a2..b5e18fdd42 100644 --- a/packages/svelte/tests/html_equal.js +++ b/packages/svelte/tests/html_equal.js @@ -1,7 +1,10 @@ import { assert } from 'vitest'; -/** @param {Element} node */ -function clean_children(node) { +/** + * @param {Element} node + * @param {{ preserveComments: boolean }} opts + */ +function clean_children(node, opts) { let previous = null; let has_element_children = false; let template = @@ -56,20 +59,26 @@ function clean_children(node) { continue; } - } else if (child.nodeType === 8) { + } + + if (child.nodeType === 8 && !opts.preserveComments) { // comment child.remove(); continue; - } else if (child.nodeType === 1) { + } + + if (child.nodeType === 1 || child.nodeType === 8) { if (previous?.nodeType === 3) { const prev = /** @type {Text} */ (previous); prev.data = prev.data.replace(/^[^\S]+$/, '\n'); - } else if (previous?.nodeType === 1) { + } else if (previous?.nodeType === 1 || previous?.nodeType === 8) { node.insertBefore(document.createTextNode('\n'), child); } - has_element_children = true; - clean_children(/** @type {Element} */ (child)); + if (child.nodeType === 1) { + has_element_children = true; + clean_children(/** @type {Element} */ (child), opts); + } } previous = child; @@ -103,9 +112,9 @@ function clean_children(node) { export function normalize_html(window, html, { preserveComments = false } = {}) { try { const node = window.document.createElement('div'); - node.innerHTML = html.replace(/()/g, preserveComments ? '$1' : '').trim(); - clean_children(node); + node.innerHTML = html.trim(); + clean_children(node, { preserveComments }); return node.innerHTML; } catch (err) {