fix hydrating <head> (#4082)

pull/4266/head
Tan Li Hau 5 years ago committed by Conduitry
parent d7d7ce1e6c
commit b3582c7fb2

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

@ -5,6 +5,7 @@ import Element from '../nodes/Element';
import { Ast, TemplateNode } from '../../interfaces';
import Component from '../Component';
import { CssNode } from './interfaces';
import hash from "../utils/hash";
function remove_css_prefix(name: string): string {
return name.replace(/^-((webkit)|(moz)|(o)|(ms))-/, '');
@ -37,15 +38,6 @@ function minify_declarations(
return c;
}
// https://github.com/darkskyapp/string-hash/blob/master/index.js
function hash(str: string): string {
let hash = 5381;
let i = str.length;
while (i--) hash = ((hash << 5) - hash) ^ str.charCodeAt(i);
return (hash >>> 0).toString(36);
}
class Rule {
selectors: Selector[];
declarations: Declaration[];

@ -1,9 +1,11 @@
import Node from './shared/Node';
import map_children from './shared/map_children';
import hash from '../utils/hash';
export default class Head extends Node {
type: 'Head';
children: any[]; // TODO
id: string;
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
@ -18,5 +20,9 @@ export default class Head extends Node {
this.children = map_children(component, parent, scope, info.children.filter(child => {
return (child.type !== 'Text' || /\S/.test(child.data));
}));
if (this.children.length > 0) {
this.id = `svelte-${hash(this.component.source.slice(this.start, this.end))}`;
}
}
}

@ -3,11 +3,12 @@ import Renderer from '../Renderer';
import Block from '../Block';
import Head from '../../nodes/Head';
import FragmentWrapper from './Fragment';
import { x } from 'code-red';
import { x, b } from 'code-red';
import { Identifier } from 'estree';
export default class HeadWrapper extends Wrapper {
fragment: FragmentWrapper;
node: Head;
constructor(
renderer: Renderer,
@ -32,6 +33,18 @@ export default class HeadWrapper extends Wrapper {
}
render(block: Block, _parent_node: Identifier, _parent_nodes: Identifier) {
this.fragment.render(block, x`@_document.head` as unknown as Identifier, x`#nodes` as unknown as Identifier);
let nodes;
if (this.renderer.options.hydratable && this.fragment.nodes.length) {
nodes = block.get_unique_name('head_nodes');
block.chunks.claim.push(b`const ${nodes} = @query_selector_all('[data-svelte="${this.node.id}"]', @_document.head);`);
}
this.fragment.render(block, x`@_document.head` as unknown as Identifier, nodes);
if (nodes && this.renderer.options.hydratable) {
block.chunks.claim.push(
b`${nodes}.forEach(@detach);`
);
}
}
}

@ -41,6 +41,7 @@ const handlers: Record<string, Handler> = {
export interface RenderOptions extends CompileOptions{
locate: (c: number) => { line: number; column: number };
head_id?: string;
}
export default class Renderer {

@ -124,6 +124,10 @@ export default function(node: Element, renderer: Renderer, options: RenderOption
}
});
if (options.head_id) {
renderer.add_string(` data-svelte="${options.head_id}"`);
}
renderer.add_string('>');
if (node_contents !== undefined) {

@ -3,8 +3,13 @@ import Head from '../../nodes/Head';
import { x } from 'code-red';
export default function(node: Head, renderer: Renderer, options: RenderOptions) {
const head_options = {
...options,
head_id: node.id
};
renderer.push();
renderer.render(node.children, options);
renderer.render(node.children, head_options);
const result = renderer.pop();
renderer.add_expression(x`($$result.head += ${result}, "")`);

@ -5,7 +5,7 @@ import { x } from 'code-red';
export default function(node: Title, renderer: Renderer, options: RenderOptions) {
renderer.push();
renderer.add_string(`<title>`);
renderer.add_string(`<title data-svelte="${options.head_id}">`);
renderer.render(node.children, options);

@ -0,0 +1,8 @@
// https://github.com/darkskyapp/string-hash/blob/master/index.js
export default function hash(str: string): string {
let hash = 5381;
let i = str.length;
while (i--) hash = ((hash << 5) - hash) ^ str.charCodeAt(i);
return (hash >>> 0).toString(36);
}

@ -273,6 +273,10 @@ export function custom_event<T=any>(type: string, detail?: T) {
return e;
}
export function query_selector_all(selector: string, parent: HTMLElement = document.body) {
return Array.from(parent.querySelectorAll(selector));
}
export class HtmlTag {
e: HTMLElement;
n: ChildNode[];

@ -68,6 +68,7 @@ window.scrollTo = function(pageXOffset, pageYOffset) {
export function env() {
window.document.title = '';
window.document.head.innerHTML = '';
window.document.body.innerHTML = '<main></main>';
return window;

@ -67,8 +67,16 @@ describe('hydration', () => {
}
const target = window.document.body;
const head = window.document.head;
target.innerHTML = fs.readFileSync(`${cwd}/_before.html`, 'utf-8');
let before_head;
try {
before_head = fs.readFileSync(`${cwd}/_before_head.html`, 'utf-8');
head.innerHTML = before_head;
} catch (err) {}
const snapshot = config.snapshot ? config.snapshot(target) : {};
const component = new SvelteComponent({
@ -88,6 +96,19 @@ describe('hydration', () => {
}
}
if (before_head) {
try {
assert.htmlEqual(head.innerHTML, fs.readFileSync(`${cwd}/_after_head.html`, 'utf-8'));
} catch (error) {
if (shouldUpdateExpected()) {
fs.writeFileSync(`${cwd}/_after_head.html`, head.innerHTML);
console.log(`Updated ${cwd}/_after_head.html.`);
} else {
throw error;
}
}
}
if (config.test) {
config.test(assert, target, snapshot, component, window);
} else {

@ -0,0 +1,4 @@
<title>Some Title</title>
<link href="/" rel="canonical">
<meta content="some description" name="description">
<meta content="some keywords" name="keywords">

@ -0,0 +1,4 @@
<title data-svelte="svelte-1s8aodm">Some Title</title>
<link rel="canonical" href="/" data-svelte="svelte-1s8aodm">
<meta name="description" content="some description" data-svelte="svelte-1s8aodm">
<meta name="keywords" content="some keywords" data-svelte="svelte-1s8aodm">

@ -0,0 +1,5 @@
export default {
test(assert, target, snapshot, component, window) {
assert.equal(window.document.querySelectorAll('meta').length, 2);
}
};

@ -0,0 +1,8 @@
<svelte:head>
<title>Some Title</title>
<link rel="canonical" href="/">
<meta name="description" content="some description">
<meta name="keywords" content="some keywords">
</svelte:head>
<div>Just a dummy page.</div>

@ -0,0 +1,4 @@
<title data-svelte="svelte-1s8aodm">Some Title</title>
<link rel="canonical" href="/" data-svelte="svelte-1s8aodm">
<meta name="description" content="some description" data-svelte="svelte-1s8aodm">
<meta name="keywords" content="some keywords" data-svelte="svelte-1s8aodm">

@ -0,0 +1,8 @@
<svelte:head>
<title>Some Title</title>
<link rel="canonical" href="/">
<meta name="description" content="some description">
<meta name="keywords" content="some keywords">
</svelte:head>
<div>Just a dummy page.</div>

@ -1 +1 @@
<title>B</title>
<title data-svelte="svelte-1csszk6">B</title>

@ -1 +1 @@
<title>a custom title</title>
<title data-svelte="svelte-135agoq">a custom title</title>
Loading…
Cancel
Save