Merge branch 'better-normalize-html' into async

pull/16197/head
Rich Harris 4 months ago
commit 3cf0b9e077

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: treat transitive dependencies of each blocks as mutable in legacy mode if item is mutated

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: correctly transform reassignments to class fields in SSR mode

@ -1,5 +1,11 @@
# svelte # 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 ## 5.33.10
### Patch Changes ### Patch Changes

@ -2,7 +2,7 @@
"name": "svelte", "name": "svelte",
"description": "Cybernetically enhanced web apps", "description": "Cybernetically enhanced web apps",
"license": "MIT", "license": "MIT",
"version": "5.33.10", "version": "5.33.11",
"type": "module", "type": "module",
"types": "./types/index.d.ts", "types": "./types/index.d.ts",
"engines": { "engines": {

@ -24,7 +24,12 @@ export function AssignmentExpression(node, context) {
* @returns {Expression | null} * @returns {Expression | null}
*/ */
function build_assignment(operator, left, right, context) { 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 name = get_name(left.property);
const field = name && context.state.state_fields.get(name); 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)) /** @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} */ ( let value = /** @type {Expression} */ (
context.visit(build_assignment_value(operator, left, right)) context.visit(build_assignment_value(operator, left, right))
); );

@ -4,5 +4,5 @@
* The current version, as set in package.json. * The current version, as set in package.json.
* @type {string} * @type {string}
*/ */
export const VERSION = '5.33.10'; export const VERSION = '5.33.11';
export const PUBLIC_VERSION = '5'; export const PUBLIC_VERSION = '5';

@ -1,8 +1,20 @@
import { assert } from 'vitest'; 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 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 // sort attributes
const attributes = Array.from(node.attributes).sort((a, b) => { const attributes = Array.from(node.attributes).sort((a, b) => {
@ -14,6 +26,10 @@ function clean_children(node) {
}); });
attributes.forEach((attr) => { attributes.forEach((attr) => {
if ((attr.name === 'onload' || attr.name === 'onerror') && attr.value === 'this.__e=event') {
return;
}
node.setAttribute(attr.name, attr.value); node.setAttribute(attr.name, attr.value);
}); });
@ -27,24 +43,42 @@ function clean_children(node) {
node.tagName !== 'tspan' node.tagName !== 'tspan'
) { ) {
node.removeChild(child); 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) { if (previous && previous.nodeType === 3) {
const prev = /** @type {Text} */ (previous); const prev = /** @type {Text} */ (previous);
prev.data += text.data; prev.data += text.data;
prev.data = prev.data.replace(/[ \t\n\r\f]+/g, '\n');
node.removeChild(text); node.removeChild(text);
text = prev; text = prev;
text.data = text.data.replace(/[^\S]+/g, ' ');
continue;
} }
} else if (child.nodeType === 8) { }
if (child.nodeType === 8 && !opts.preserveComments) {
// comment // comment
// do nothing child.remove();
} else { continue;
clean_children(/** @type {Element} */ (child)); }
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 || previous?.nodeType === 8) {
node.insertBefore(document.createTextNode('\n'), child);
}
if (child.nodeType === 1) {
has_element_children = true;
clean_children(/** @type {Element} */ (child), opts);
}
} }
previous = child; previous = child;
@ -53,37 +87,35 @@ function clean_children(node) {
// collapse whitespace // collapse whitespace
if (node.firstChild && node.firstChild.nodeType === 3) { if (node.firstChild && node.firstChild.nodeType === 3) {
const text = /** @type {Text} */ (node.firstChild); const text = /** @type {Text} */ (node.firstChild);
text.data = text.data.replace(/^[ \t\n\r\f]+/, ''); text.data = text.data.trimStart();
if (!text.data.length) node.removeChild(text);
} }
if (node.lastChild && node.lastChild.nodeType === 3) { if (node.lastChild && node.lastChild.nodeType === 3) {
const text = /** @type {Text} */ (node.lastChild); const text = /** @type {Text} */ (node.lastChild);
text.data = text.data.replace(/[ \t\n\r\f]+$/, ''); text.data = text.data.trimEnd();
if (!text.data.length) node.removeChild(text); }
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 {Window} window
* @param {string} html * @param {string} html
* @param {{ removeDataSvelte?: boolean, preserveComments?: boolean }} param2 * @param {{ preserveComments?: boolean }} opts
*/ */
export function normalize_html( export function normalize_html(window, html, { preserveComments = false } = {}) {
window,
html,
{ removeDataSvelte = false, preserveComments = false }
) {
try { try {
const node = window.document.createElement('div'); const node = window.document.createElement('div');
node.innerHTML = html
.replace(/(<!(--)?.*?\2>)/g, preserveComments ? '$1' : '') node.innerHTML = html.trim();
.replace(/(data-svelte-h="[^"]+")/g, removeDataSvelte ? '' : '$1') clean_children(node, { preserveComments });
.replace(/>[ \t\n\r\f]+</g, '><')
// 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();
clean_children(node);
return node.innerHTML; return node.innerHTML;
} catch (err) { } catch (err) {
throw new Error(`Failed to normalize HTML:\n${html}\nCause: ${err}`); throw new Error(`Failed to normalize HTML:\n${html}\nCause: ${err}`);
@ -99,67 +131,52 @@ export function normalize_new_line(html) {
} }
/** /**
* @param {{ removeDataSvelte?: boolean }} options * @param {string} actual
* @param {string} expected
* @param {string} [message]
*/ */
export function setup_html_equal(options = {}) { export const assert_html_equal = (actual, expected, message) => {
/** try {
* @param {string} actual assert.deepEqual(normalize_html(window, actual), normalize_html(window, expected), message);
* @param {string} expected } catch (e) {
* @param {string} [message] if (Error.captureStackTrace)
*/ Error.captureStackTrace(/** @type {Error} */ (e), assert_html_equal);
const assert_html_equal = (actual, expected, message) => { throw e;
try { }
assert.deepEqual( };
normalize_html(window, actual, options),
normalize_html(window, expected, options),
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(/(\sdata-svelte-h="[^"]+")/g, options.removeDataSvelte ? '' : '$1')
.replace(/(<!(--)?.*?\2>)/g, preserveComments !== false ? '$1' : '')
: normalize_html(window, actual.trim(), { ...options, preserveComments }),
withoutNormalizeHtml
? normalize_new_line(expected.trim())
.replace(/(\sdata-svelte-h="[^"]+")/g, options.removeDataSvelte ? '' : '$1')
.replace(/(<!(--)?.*?\2>)/g, preserveComments !== false ? '$1' : '')
: normalize_html(window, expected.trim(), { ...options, 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
};
}
// 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(
/(<!(--)?.*?\2>)/g,
preserveComments !== false ? '$1' : ''
)
: normalize_html(window, actual.trim(), { preserveComments }),
withoutNormalizeHtml
? normalize_new_line(expected.trim()).replace(
/(<!(--)?.*?\2>)/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;
}
};

@ -25,7 +25,7 @@ export default test({
<p>selected: one</p> <p>selected: one</p>
<select> <select>
<option${variant === 'hydrate' ? ' selected' : ''}>one</option$> <option${variant === 'hydrate' ? ' selected' : ''}>one</option>
<option>two</option> <option>two</option>
<option>three</option> <option>three</option>
</select> </select>
@ -54,7 +54,7 @@ export default test({
<p>selected: two</p> <p>selected: two</p>
<select> <select>
<option${variant === 'hydrate' ? ' selected' : ''}>one</option$> <option${variant === 'hydrate' ? ' selected' : ''}>one</option>
<option>two</option> <option>two</option>
<option>three</option> <option>three</option>
</select> </select>

@ -4,7 +4,9 @@ export default test({
html: ` html: `
<input list='suggestions'> <input list='suggestions'>
<datalist id='suggestions'> <datalist id='suggestions'>
<option value='foo'/><option value='bar'/><option value='baz'/> <option value='foo'></option>
<option value='bar'></option>
<option value='baz'></option>
</datalist> </datalist>
` `
}); });

@ -9,7 +9,7 @@ export default test({
<math> <math>
<mrow></mrow> <mrow></mrow>
</svg> </math>
<div class="hi">hi</div> <div class="hi">hi</div>
`, `,

@ -7,7 +7,7 @@ export default test({
target.innerHTML, target.innerHTML,
` `
<select> <select>
<option${variant === 'hydrate' ? ' selected' : ''} value="a">A</option$> <option${variant === 'hydrate' ? ' selected' : ''} value="a">A</option>
<option value="b">B</option> <option value="b">B</option>
</select> </select>
selected: a selected: a
@ -23,7 +23,7 @@ export default test({
target.innerHTML, target.innerHTML,
` `
<select> <select>
<option${variant === 'hydrate' ? ' selected' : ''} value="a">A</option$> <option${variant === 'hydrate' ? ' selected' : ''} value="a">A</option>
<option value="b">B</option> <option value="b">B</option>
</select> </select>
selected: b selected: b

@ -7,7 +7,7 @@ import { flushSync, hydrate, mount, unmount } from 'svelte';
import { render } from 'svelte/server'; import { render } from 'svelte/server';
import { afterAll, assert, beforeAll } from 'vitest'; import { afterAll, assert, beforeAll } from 'vitest';
import { compile_directory, fragments } from '../helpers.js'; 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 { raf } from '../animation-helpers.js';
import type { CompileOptions } from '#compiler'; import type { CompileOptions } from '#compiler';
import { suite_with_variants, type BaseTest } from '../suite.js'; import { suite_with_variants, type BaseTest } from '../suite.js';
@ -100,10 +100,6 @@ function unhandled_rejection_handler(err: Error) {
const listeners = process.rawListeners('unhandledRejection'); const listeners = process.rawListeners('unhandledRejection');
const { assert_html_equal, assert_html_equal_with_options } = setup_html_equal({
removeDataSvelte: true
});
beforeAll(() => { beforeAll(() => {
// @ts-expect-error TODO huh? // @ts-expect-error TODO huh?
process.prependListener('unhandledRejection', unhandled_rejection_handler); process.prependListener('unhandledRejection', unhandled_rejection_handler);

@ -5,7 +5,7 @@ export default function Class_state_field_constructor_assignment($$anchor, $$pro
$.push($$props, true); $.push($$props, true);
class Foo { class Foo {
#a = $.state(); #a = $.state(0);
get a() { get a() {
return $.get(this.#a); return $.get(this.#a);
@ -16,10 +16,31 @@ export default function Class_state_field_constructor_assignment($$anchor, $$pro
} }
#b = $.state(); #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() { constructor() {
this.a = 1; this.a = 1;
$.set(this.#b, 2); $.set(this.#b, 2);
this.foo.bar = 3;
this.bar = 4;
} }
} }

@ -4,12 +4,33 @@ export default function Class_state_field_constructor_assignment($$payload, $$pr
$.push(); $.push();
class Foo { class Foo {
a; a = 0;
#b; #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() { constructor() {
this.a = 1; this.a = 1;
this.#b = 2; this.#b = 2;
this.foo.bar = 3;
this.bar = 4;
} }
} }

@ -1,11 +1,14 @@
<script> <script>
class Foo { class Foo {
a = $state(); a = $state(0);
#b = $state(); #b = $state();
foo = $derived({ bar: this.a * 2 });
bar = $derived({ baz: this.foo });
constructor() { constructor() {
this.a = 1; this.a = 1;
this.#b = 2; this.#b = 2;
this.foo.bar = 3;
this.bar = 4;
} }
} }
</script> </script>

Loading…
Cancel
Save