Skip to content

Commit 296ba11

Browse files
tanhauhautaylorzane
authored andcommitted
fix hydrating <head> (sveltejs#4082)
1 parent a155f51 commit 296ba11

File tree

23 files changed

+109
-15
lines changed

23 files changed

+109
-15
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## Unreleased
44

5+
* Remove old `<head>` elements during hydration so they aren't duplicated ([#1607](https://github.com/sveltejs/svelte/issues/1607))
56
* Prevent text input cursor jumping in Safari with one-way binding ([#3449](https://github.com/sveltejs/svelte/issues/3449))
67
* Expose compiler version in dev events ([#4047](https://github.com/sveltejs/svelte/issues/4047))
78
* Don't run actions before their element is in the document ([#4166](https://github.com/sveltejs/svelte/issues/4166))

src/compiler/compile/css/Stylesheet.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import Element from '../nodes/Element';
55
import { Ast, TemplateNode } from '../../interfaces';
66
import Component from '../Component';
77
import { CssNode } from './interfaces';
8+
import hash from "../utils/hash";
89

910
function remove_css_prefix(name: string): string {
1011
return name.replace(/^-((webkit)|(moz)|(o)|(ms))-/, '');
@@ -37,15 +38,6 @@ function minify_declarations(
3738
return c;
3839
}
3940

40-
// https://github.com/darkskyapp/string-hash/blob/master/index.js
41-
function hash(str: string): string {
42-
let hash = 5381;
43-
let i = str.length;
44-
45-
while (i--) hash = ((hash << 5) - hash) ^ str.charCodeAt(i);
46-
return (hash >>> 0).toString(36);
47-
}
48-
4941
class Rule {
5042
selectors: Selector[];
5143
declarations: Declaration[];

src/compiler/compile/nodes/Head.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import Node from './shared/Node';
22
import map_children from './shared/map_children';
3+
import hash from '../utils/hash';
34

45
export default class Head extends Node {
56
type: 'Head';
67
children: any[]; // TODO
8+
id: string;
79

810
constructor(component, parent, scope, info) {
911
super(component, parent, scope, info);
@@ -18,5 +20,9 @@ export default class Head extends Node {
1820
this.children = map_children(component, parent, scope, info.children.filter(child => {
1921
return (child.type !== 'Text' || /\S/.test(child.data));
2022
}));
23+
24+
if (this.children.length > 0) {
25+
this.id = `svelte-${hash(this.component.source.slice(this.start, this.end))}`;
26+
}
2127
}
2228
}

src/compiler/compile/render_dom/wrappers/Head.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ import Renderer from '../Renderer';
33
import Block from '../Block';
44
import Head from '../../nodes/Head';
55
import FragmentWrapper from './Fragment';
6-
import { x } from 'code-red';
6+
import { x, b } from 'code-red';
77
import { Identifier } from 'estree';
88

99
export default class HeadWrapper extends Wrapper {
1010
fragment: FragmentWrapper;
11+
node: Head;
1112

1213
constructor(
1314
renderer: Renderer,
@@ -32,6 +33,18 @@ export default class HeadWrapper extends Wrapper {
3233
}
3334

3435
render(block: Block, _parent_node: Identifier, _parent_nodes: Identifier) {
35-
this.fragment.render(block, x`@_document.head` as unknown as Identifier, x`#nodes` as unknown as Identifier);
36+
let nodes;
37+
if (this.renderer.options.hydratable && this.fragment.nodes.length) {
38+
nodes = block.get_unique_name('head_nodes');
39+
block.chunks.claim.push(b`const ${nodes} = @query_selector_all('[data-svelte="${this.node.id}"]', @_document.head);`);
40+
}
41+
42+
this.fragment.render(block, x`@_document.head` as unknown as Identifier, nodes);
43+
44+
if (nodes && this.renderer.options.hydratable) {
45+
block.chunks.claim.push(
46+
b`${nodes}.forEach(@detach);`
47+
);
48+
}
3649
}
3750
}

src/compiler/compile/render_ssr/Renderer.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ const handlers: Record<string, Handler> = {
4141

4242
export interface RenderOptions extends CompileOptions{
4343
locate: (c: number) => { line: number; column: number };
44+
head_id?: string;
4445
}
4546

4647
export default class Renderer {

src/compiler/compile/render_ssr/handlers/Element.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,10 @@ export default function(node: Element, renderer: Renderer, options: RenderOption
124124
}
125125
});
126126

127+
if (options.head_id) {
128+
renderer.add_string(` data-svelte="${options.head_id}"`);
129+
}
130+
127131
renderer.add_string('>');
128132

129133
if (node_contents !== undefined) {

src/compiler/compile/render_ssr/handlers/Head.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,13 @@ import Head from '../../nodes/Head';
33
import { x } from 'code-red';
44

55
export default function(node: Head, renderer: Renderer, options: RenderOptions) {
6+
const head_options = {
7+
...options,
8+
head_id: node.id
9+
};
10+
611
renderer.push();
7-
renderer.render(node.children, options);
12+
renderer.render(node.children, head_options);
813
const result = renderer.pop();
914

1015
renderer.add_expression(x`($$result.head += ${result}, "")`);

src/compiler/compile/render_ssr/handlers/Title.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { x } from 'code-red';
55
export default function(node: Title, renderer: Renderer, options: RenderOptions) {
66
renderer.push();
77

8-
renderer.add_string(`<title>`);
8+
renderer.add_string(`<title data-svelte="${options.head_id}">`);
99

1010
renderer.render(node.children, options);
1111

src/compiler/compile/utils/hash.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// https://github.com/darkskyapp/string-hash/blob/master/index.js
2+
export default function hash(str: string): string {
3+
let hash = 5381;
4+
let i = str.length;
5+
6+
while (i--) hash = ((hash << 5) - hash) ^ str.charCodeAt(i);
7+
return (hash >>> 0).toString(36);
8+
}

src/runtime/internal/dom.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,10 @@ export function custom_event<T=any>(type: string, detail?: T) {
273273
return e;
274274
}
275275

276+
export function query_selector_all(selector: string, parent: HTMLElement = document.body) {
277+
return Array.from(parent.querySelectorAll(selector));
278+
}
279+
276280
export class HtmlTag {
277281
e: HTMLElement;
278282
n: ChildNode[];

test/helpers.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ window.scrollTo = function(pageXOffset, pageYOffset) {
6868

6969
export function env() {
7070
window.document.title = '';
71+
window.document.head.innerHTML = '';
7172
window.document.body.innerHTML = '<main></main>';
7273

7374
return window;

test/hydration/index.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,16 @@ describe('hydration', () => {
6767
}
6868

6969
const target = window.document.body;
70+
const head = window.document.head;
71+
7072
target.innerHTML = fs.readFileSync(`${cwd}/_before.html`, 'utf-8');
7173

74+
let before_head;
75+
try {
76+
before_head = fs.readFileSync(`${cwd}/_before_head.html`, 'utf-8');
77+
head.innerHTML = before_head;
78+
} catch (err) {}
79+
7280
const snapshot = config.snapshot ? config.snapshot(target) : {};
7381

7482
const component = new SvelteComponent({
@@ -88,6 +96,19 @@ describe('hydration', () => {
8896
}
8997
}
9098

99+
if (before_head) {
100+
try {
101+
assert.htmlEqual(head.innerHTML, fs.readFileSync(`${cwd}/_after_head.html`, 'utf-8'));
102+
} catch (error) {
103+
if (shouldUpdateExpected()) {
104+
fs.writeFileSync(`${cwd}/_after_head.html`, head.innerHTML);
105+
console.log(`Updated ${cwd}/_after_head.html.`);
106+
} else {
107+
throw error;
108+
}
109+
}
110+
}
111+
91112
if (config.test) {
92113
config.test(assert, target, snapshot, component, window);
93114
} else {
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<div>Just a dummy page.</div>
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<title>Some Title</title>
2+
<link href="/" rel="canonical">
3+
<meta content="some description" name="description">
4+
<meta content="some keywords" name="keywords">
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<div>Just a dummy page.</div>
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<title data-svelte="svelte-1s8aodm">Some Title</title>
2+
<link rel="canonical" href="/" data-svelte="svelte-1s8aodm">
3+
<meta name="description" content="some description" data-svelte="svelte-1s8aodm">
4+
<meta name="keywords" content="some keywords" data-svelte="svelte-1s8aodm">
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export default {
2+
test(assert, target, snapshot, component, window) {
3+
assert.equal(window.document.querySelectorAll('meta').length, 2);
4+
}
5+
};
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<svelte:head>
2+
<title>Some Title</title>
3+
<link rel="canonical" href="/">
4+
<meta name="description" content="some description">
5+
<meta name="keywords" content="some keywords">
6+
</svelte:head>
7+
8+
<div>Just a dummy page.</div>
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<title data-svelte="svelte-1s8aodm">Some Title</title>
2+
<link rel="canonical" href="/" data-svelte="svelte-1s8aodm">
3+
<meta name="description" content="some description" data-svelte="svelte-1s8aodm">
4+
<meta name="keywords" content="some keywords" data-svelte="svelte-1s8aodm">
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
2+
3+
<div>Just a dummy page.</div>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<svelte:head>
2+
<title>Some Title</title>
3+
<link rel="canonical" href="/">
4+
<meta name="description" content="some description">
5+
<meta name="keywords" content="some keywords">
6+
</svelte:head>
7+
8+
<div>Just a dummy page.</div>
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
<title>B</title>
1+
<title data-svelte="svelte-1csszk6">B</title>
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
<title>a custom title</title>
1+
<title data-svelte="svelte-135agoq">a custom title</title>

0 commit comments

Comments
 (0)