Skip to content

Commit 7bd853b

Browse files
authored
fix: hydrate HTML with surrounding whitespace (#10996)
* fix: hydrate HTML with surrounding whitespace * add test * fix a few more short comments * tidy up * avoid magic strings * avoid magic strings * fix * oops
1 parent 3f6eff5 commit 7bd853b

File tree

19 files changed

+77
-52
lines changed

19 files changed

+77
-52
lines changed

.changeset/smart-cherries-leave.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: hydrate HTML with surrounding whitespace

.prettierignore

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,8 @@ packages/svelte/tests/**/_actual*
1010
packages/svelte/tests/**/expected*
1111
packages/svelte/tests/**/_output
1212
packages/svelte/tests/**/shards/*.test.js
13-
packages/svelte/tests/hydration/samples/*/_before.html
14-
packages/svelte/tests/hydration/samples/*/_before_head.html
15-
packages/svelte/tests/hydration/samples/*/_after.html
16-
packages/svelte/tests/hydration/samples/*/_after_head.html
13+
packages/svelte/tests/hydration/samples/*/_expected.html
14+
packages/svelte/tests/hydration/samples/*/_override.html
1715
packages/svelte/types
1816
packages/svelte/compiler.cjs
1917
playgrounds/demo/src

packages/svelte/src/compiler/phases/3-transform/server/transform-server.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@ import { create_attribute, is_custom_element_node, is_element_node } from '../..
2424
import { error } from '../../../errors.js';
2525
import { binding_properties } from '../../bindings.js';
2626
import { regex_starts_with_newline, regex_whitespaces_strict } from '../../patterns.js';
27-
import { DOMBooleanAttributes } from '../../../../constants.js';
27+
import { DOMBooleanAttributes, HYDRATION_END, HYDRATION_START } from '../../../../constants.js';
2828
import { sanitize_template_string } from '../../../utils/sanitize_template_string.js';
2929

30-
const block_open = t_string('<![>');
31-
const block_close = t_string('<!]>');
30+
export const block_open = t_string(`<!--${HYDRATION_START}-->`);
31+
export const block_close = t_string(`<!--${HYDRATION_END}-->`);
3232

3333
/**
3434
* @param {string} value

packages/svelte/src/constants.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ export const TRANSITION_GLOBAL = 1 << 2;
1919
export const TEMPLATE_FRAGMENT = 1;
2020
export const TEMPLATE_USE_IMPORT_NODE = 1 << 1;
2121

22+
export const HYDRATION_START = '[';
23+
export const HYDRATION_END = ']';
24+
2225
export const UNINITIALIZED = Symbol();
2326

2427
/** List of Element events that will be delegated */

packages/svelte/src/internal/client/dom/blocks/each.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import {
44
EACH_IS_CONTROLLED,
55
EACH_IS_STRICT_EQUALS,
66
EACH_ITEM_REACTIVE,
7-
EACH_KEYED
7+
EACH_KEYED,
8+
HYDRATION_START
89
} from '../../../../constants.js';
910
import { hydrate_anchor, hydrate_nodes, hydrating, set_hydrating } from '../hydration.js';
1011
import { empty } from '../operations.js';
@@ -117,7 +118,10 @@ function each(anchor, flags, get_collection, get_key, render_fn, fallback_fn, re
117118
var child_anchor = hydrate_nodes[0];
118119

119120
for (var i = 0; i < length; i++) {
120-
if (child_anchor.nodeType !== 8 || /** @type {Comment} */ (child_anchor).data !== '[') {
121+
if (
122+
child_anchor.nodeType !== 8 ||
123+
/** @type {Comment} */ (child_anchor).data !== HYDRATION_START
124+
) {
121125
// If `nodes` is null, then that means that the server rendered fewer items than what
122126
// expected, so break out and continue appending non-hydrated items
123127
mismatch = true;

packages/svelte/src/internal/client/dom/blocks/svelte-head.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { hydrate_anchor, hydrate_nodes, hydrating, set_hydrate_nodes } from '../hydration.js';
22
import { empty } from '../operations.js';
33
import { block } from '../../reactivity/effects.js';
4+
import { HYDRATION_START } from '../../../../constants.js';
45

56
/**
67
* @param {(anchor: Node) => import('#client').Dom | void} render_fn
@@ -19,7 +20,7 @@ export function head(render_fn) {
1920
previous_hydrate_nodes = hydrate_nodes;
2021

2122
let anchor = /** @type {import('#client').TemplateNode} */ (document.head.firstChild);
22-
while (anchor.nodeType !== 8 || /** @type {Comment} */ (anchor).data !== '[') {
23+
while (anchor.nodeType !== 8 || /** @type {Comment} */ (anchor).data !== HYDRATION_START) {
2324
anchor = /** @type {import('#client').TemplateNode} */ (anchor.nextSibling);
2425
}
2526

packages/svelte/src/internal/client/dom/hydration.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { HYDRATION_END, HYDRATION_START } from '../../../constants.js';
2+
13
/**
24
* Use this variable to guard everything related to hydration code so it can be treeshaken out
35
* if the user doesn't use the `hydrate` method and these code paths are therefore not needed.
@@ -23,7 +25,7 @@ export function set_hydrate_nodes(nodes) {
2325
}
2426

2527
/**
26-
* This function is only called when `hydrating` is true. If passed a `<![>` opening
28+
* This function is only called when `hydrating` is true. If passed a `<!--[-->` opening
2729
* hydration marker, it finds the corresponding closing marker and sets `hydrate_nodes`
2830
* to everything between the markers, before returning the closing marker.
2931
* @param {Node} node
@@ -37,7 +39,7 @@ export function hydrate_anchor(node) {
3739
var current = /** @type {Node | null} */ (node);
3840

3941
// TODO this could have false positives, if a user comment consisted of `[`. need to tighten that up
40-
if (/** @type {Comment} */ (current)?.data !== '[') {
42+
if (/** @type {Comment} */ (current)?.data !== HYDRATION_START) {
4143
return node;
4244
}
4345

@@ -49,9 +51,9 @@ export function hydrate_anchor(node) {
4951
if (current.nodeType === 8) {
5052
var data = /** @type {Comment} */ (current).data;
5153

52-
if (data === '[') {
54+
if (data === HYDRATION_START) {
5355
depth += 1;
54-
} else if (data === ']') {
56+
} else if (data === HYDRATION_END) {
5557
if (depth === 0) {
5658
hydrate_nodes = /** @type {import('#client').TemplateNode[]} */ (nodes);
5759
return current;

packages/svelte/src/internal/client/render.js

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
empty,
77
init_operations
88
} from './dom/operations.js';
9-
import { PassiveDelegatedEvents } from '../../constants.js';
9+
import { HYDRATION_START, PassiveDelegatedEvents } from '../../constants.js';
1010
import { flush_sync, push, pop, current_component_context, untrack } from './runtime.js';
1111
import { effect_root, branch } from './reactivity/effects.js';
1212
import {
@@ -121,8 +121,7 @@ export function mount(component, options) {
121121
* @returns {Exports}
122122
*/
123123
export function hydrate(component, options) {
124-
const container = options.target;
125-
const first_child = /** @type {ChildNode} */ (container.firstChild);
124+
const target = options.target;
126125
const previous_hydrate_nodes = hydrate_nodes;
127126

128127
let hydrated = false;
@@ -132,7 +131,19 @@ export function hydrate(component, options) {
132131
return flush_sync(() => {
133132
set_hydrating(true);
134133

135-
const anchor = hydrate_anchor(first_child);
134+
var node = target.firstChild;
135+
while (
136+
node &&
137+
(node.nodeType !== 8 || /** @type {Comment} */ (node).data !== HYDRATION_START)
138+
) {
139+
node = node.nextSibling;
140+
}
141+
142+
if (!node) {
143+
throw new Error('Missing hydration marker');
144+
}
145+
146+
const anchor = hydrate_anchor(node);
136147
const instance = _mount(component, { ...options, anchor });
137148

138149
// flush_sync will run this callback and then synchronously run any pending effects,
@@ -153,7 +164,7 @@ export function hydrate(component, options) {
153164
error
154165
);
155166

156-
clear_text_content(container);
167+
clear_text_content(target);
157168

158169
set_hydrating(false);
159170
return mount(component, options);
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { HYDRATION_END, HYDRATION_START } from '../../constants.js';
2+
3+
export const BLOCK_OPEN = `<!--${HYDRATION_START}-->`;
4+
export const BLOCK_CLOSE = `<!--${HYDRATION_END}-->`;

packages/svelte/src/internal/server/index.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
} from '../../constants.js';
1010
import { DEV } from 'esm-env';
1111
import { current_component, pop, push } from './context.js';
12+
import { BLOCK_CLOSE, BLOCK_OPEN } from './hydration.js';
1213

1314
/**
1415
* @typedef {{
@@ -161,11 +162,11 @@ export function element(payload, tag, attributes_fn, children_fn) {
161162

162163
if (!VoidElements.has(tag)) {
163164
if (tag !== 'textarea') {
164-
payload.out += '<!--[-->';
165+
payload.out += BLOCK_OPEN;
165166
}
166167
children_fn();
167168
if (tag !== 'textarea') {
168-
payload.out += '<!--]-->';
169+
payload.out += BLOCK_CLOSE;
169170
}
170171
payload.out += `</${tag}>`;
171172
}
@@ -187,7 +188,7 @@ export function render(component, options) {
187188

188189
const prev_on_destroy = on_destroy;
189190
on_destroy = [];
190-
payload.out += '<!--[-->';
191+
payload.out += BLOCK_OPEN;
191192

192193
if (options.context) {
193194
push();
@@ -200,14 +201,14 @@ export function render(component, options) {
200201
pop();
201202
}
202203

203-
payload.out += '<!--]-->';
204+
payload.out += BLOCK_CLOSE;
204205
for (const cleanup of on_destroy) cleanup();
205206
on_destroy = prev_on_destroy;
206207

207208
return {
208209
head:
209210
payload.head.out || payload.head.title
210-
? payload.head.title + '<!--[-->' + payload.head.out + '<!--]-->'
211+
? payload.head.title + BLOCK_OPEN + payload.head.out + BLOCK_CLOSE
211212
: '',
212213
html: payload.out
213214
};
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { test } from '../../test';
2+
3+
export default test({});
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<!-- unrelated comment -->
2+
<!--[--><!--[--><!--ssr:if:true-->hello<!--]--><!--]-->
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{#if true}hello{/if}

packages/svelte/tests/hydration/test.ts

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,11 @@ interface HydrationTest extends BaseTest {
3434
after_test?: () => void;
3535
}
3636

37-
const { test, run } = suite<HydrationTest>(async (config, cwd) => {
38-
/**
39-
* Read file and remove whitespace between ssr comments
40-
*/
41-
function read_html(path: string, fallback?: string): string {
42-
const html = fs.readFileSync(fallback && !fs.existsSync(path) ? fallback : path, 'utf-8');
43-
return config.trim_whitespace !== false
44-
? html.replace(/(<!--ssr:.?-->)[ \t\n\r\f]+(<!--ssr:.?-->)/g, '$1$2')
45-
: html;
46-
}
37+
function read(path: string): string | void {
38+
return fs.existsSync(path) ? fs.readFileSync(path, 'utf-8') : undefined;
39+
}
4740

41+
const { test, run } = suite<HydrationTest>(async (config, cwd) => {
4842
if (!config.load_compiled) {
4943
await compile_directory(cwd, 'client', { accessors: true, ...config.compileOptions });
5044
await compile_directory(cwd, 'server', config.compileOptions);
@@ -58,7 +52,7 @@ const { test, run } = suite<HydrationTest>(async (config, cwd) => {
5852
});
5953

6054
fs.writeFileSync(`${cwd}/_output/body.html`, rendered.html + '\n');
61-
target.innerHTML = rendered.html;
55+
target.innerHTML = read(`${cwd}/_override.html`) ?? rendered.html;
6256

6357
if (rendered.head) {
6458
fs.writeFileSync(`${cwd}/_output/head.html`, rendered.head + '\n');
@@ -97,15 +91,11 @@ const { test, run } = suite<HydrationTest>(async (config, cwd) => {
9791
assert.ok(!got_hydration_error, 'Unexpected hydration error');
9892
}
9993

100-
const expected = fs.existsSync(`${cwd}/_expected.html`)
101-
? read_html(`${cwd}/_expected.html`)
102-
: rendered.html;
94+
const expected = read(`${cwd}/_expected.html`) ?? rendered.html;
10395
assert_html_equal(target.innerHTML, expected);
10496

10597
if (rendered.head) {
106-
const expected = fs.existsSync(`${cwd}/_expected_head.html`)
107-
? read_html(`${cwd}/_expected_head.html`)
108-
: rendered.head;
98+
const expected = read(`${cwd}/_expected_head.html`) ?? rendered.head;
10999
assert_html_equal(head.innerHTML, expected);
110100
}
111101

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
<![><p>before</p><!-- a comment --><p>after</p><!]>
1+
<!--[--><p>before</p><!-- a comment --><p>after</p><!--]-->

packages/svelte/tests/snapshot/samples/bind-this/_expected/server/index.svelte.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import * as $ from "svelte/internal/server";
44

55
export default function Bind_this($$payload, $$props) {
66
$.push(false);
7-
$$payload.out += `<![>`;
7+
$$payload.out += `<!--[-->`;
88
Foo($$payload, {});
9-
$$payload.out += `<!]>`;
9+
$$payload.out += `<!--]-->`;
1010
$.pop();
1111
}

packages/svelte/tests/snapshot/samples/each-string-template/_expected/server/index.svelte.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,16 @@ export default function Each_string_template($$payload, $$props) {
77

88
const each_array = $.ensure_array_like(['foo', 'bar', 'baz']);
99

10-
$$payload.out += `<![>`;
10+
$$payload.out += `<!--[-->`;
1111

1212
for (let $$index = 0; $$index < each_array.length; $$index++) {
1313
const thing = each_array[$$index];
1414

15-
$$payload.out += "<![>";
15+
$$payload.out += "<!--[-->";
1616
$$payload.out += `${$.escape(thing)}, `;
17-
$$payload.out += "<!]>";
17+
$$payload.out += "<!--]-->";
1818
}
1919

20-
$$payload.out += `<!]>`;
20+
$$payload.out += `<!--]-->`;
2121
$.pop();
2222
}

packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/server/index.svelte.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export default function Function_prop_no_getter($$payload, $$props) {
1313

1414
const plusOne = (num) => num + 1;
1515

16-
$$payload.out += `<![>`;
16+
$$payload.out += `<!--[-->`;
1717

1818
Button($$payload, {
1919
onmousedown: () => count += 1,
@@ -24,6 +24,6 @@ export default function Function_prop_no_getter($$payload, $$props) {
2424
}
2525
});
2626

27-
$$payload.out += `<!]>`;
27+
$$payload.out += `<!--]-->`;
2828
$.pop();
2929
}

packages/svelte/tests/snapshot/samples/svelte-element/_expected/server/index.svelte.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ export default function Svelte_element($$payload, $$props) {
77

88
let { tag = 'hr' } = $$props;
99

10-
$$payload.out += `<![>`;
10+
$$payload.out += `<!--[-->`;
1111
if (tag) $.element($$payload, tag, () => {}, () => {});
12-
$$payload.out += `<!]>`;
12+
$$payload.out += `<!--]-->`;
1313
$.pop();
1414
}

0 commit comments

Comments
 (0)