diff --git a/CHANGELOG.md b/CHANGELOG.md index a2db4470d1..4bacc5c26b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +* Remove old `` 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)) diff --git a/src/compiler/compile/css/Stylesheet.ts b/src/compiler/compile/css/Stylesheet.ts index 998a879687..246dab0f12 100644 --- a/src/compiler/compile/css/Stylesheet.ts +++ b/src/compiler/compile/css/Stylesheet.ts @@ -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[]; diff --git a/src/compiler/compile/nodes/Head.ts b/src/compiler/compile/nodes/Head.ts index 2c08dcd595..53e76d7a4d 100644 --- a/src/compiler/compile/nodes/Head.ts +++ b/src/compiler/compile/nodes/Head.ts @@ -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))}`; + } } } diff --git a/src/compiler/compile/render_dom/wrappers/Head.ts b/src/compiler/compile/render_dom/wrappers/Head.ts index 188c26931a..e0b723d6dd 100644 --- a/src/compiler/compile/render_dom/wrappers/Head.ts +++ b/src/compiler/compile/render_dom/wrappers/Head.ts @@ -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);` + ); + } } } diff --git a/src/compiler/compile/render_ssr/Renderer.ts b/src/compiler/compile/render_ssr/Renderer.ts index 00a7ee2fb5..fb9216327c 100644 --- a/src/compiler/compile/render_ssr/Renderer.ts +++ b/src/compiler/compile/render_ssr/Renderer.ts @@ -41,6 +41,7 @@ const handlers: Record = { export interface RenderOptions extends CompileOptions{ locate: (c: number) => { line: number; column: number }; + head_id?: string; } export default class Renderer { diff --git a/src/compiler/compile/render_ssr/handlers/Element.ts b/src/compiler/compile/render_ssr/handlers/Element.ts index 81b8801686..4c1eca8a9d 100644 --- a/src/compiler/compile/render_ssr/handlers/Element.ts +++ b/src/compiler/compile/render_ssr/handlers/Element.ts @@ -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) { diff --git a/src/compiler/compile/render_ssr/handlers/Head.ts b/src/compiler/compile/render_ssr/handlers/Head.ts index d457942922..456e5c279b 100644 --- a/src/compiler/compile/render_ssr/handlers/Head.ts +++ b/src/compiler/compile/render_ssr/handlers/Head.ts @@ -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}, "")`); diff --git a/src/compiler/compile/render_ssr/handlers/Title.ts b/src/compiler/compile/render_ssr/handlers/Title.ts index f1f458ed5b..e93ae13d66 100644 --- a/src/compiler/compile/render_ssr/handlers/Title.ts +++ b/src/compiler/compile/render_ssr/handlers/Title.ts @@ -5,7 +5,7 @@ import { x } from 'code-red'; export default function(node: Title, renderer: Renderer, options: RenderOptions) { renderer.push(); - renderer.add_string(``); + renderer.add_string(`<title data-svelte="${options.head_id}">`); renderer.render(node.children, options); diff --git a/src/compiler/compile/utils/hash.ts b/src/compiler/compile/utils/hash.ts new file mode 100644 index 0000000000..7ac892611b --- /dev/null +++ b/src/compiler/compile/utils/hash.ts @@ -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); +} \ No newline at end of file diff --git a/src/runtime/internal/dom.ts b/src/runtime/internal/dom.ts index c641315bc3..f9e89f41b9 100644 --- a/src/runtime/internal/dom.ts +++ b/src/runtime/internal/dom.ts @@ -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[]; diff --git a/test/helpers.js b/test/helpers.js index 2a03e0f436..5e9428243b 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -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; diff --git a/test/hydration/index.js b/test/hydration/index.js index a0bfd6de4b..f57a0cdc1a 100644 --- a/test/hydration/index.js +++ b/test/hydration/index.js @@ -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 { diff --git a/test/hydration/samples/head-meta-hydrate-duplicate/_after.html b/test/hydration/samples/head-meta-hydrate-duplicate/_after.html new file mode 100644 index 0000000000..3e5b375f0a --- /dev/null +++ b/test/hydration/samples/head-meta-hydrate-duplicate/_after.html @@ -0,0 +1 @@ +<div>Just a dummy page.</div> \ No newline at end of file diff --git a/test/hydration/samples/head-meta-hydrate-duplicate/_after_head.html b/test/hydration/samples/head-meta-hydrate-duplicate/_after_head.html new file mode 100644 index 0000000000..10cf2c8b9a --- /dev/null +++ b/test/hydration/samples/head-meta-hydrate-duplicate/_after_head.html @@ -0,0 +1,4 @@ +<title>Some Title + + + \ No newline at end of file diff --git a/test/hydration/samples/head-meta-hydrate-duplicate/_before.html b/test/hydration/samples/head-meta-hydrate-duplicate/_before.html new file mode 100644 index 0000000000..3e5b375f0a --- /dev/null +++ b/test/hydration/samples/head-meta-hydrate-duplicate/_before.html @@ -0,0 +1 @@ +
Just a dummy page.
\ No newline at end of file diff --git a/test/hydration/samples/head-meta-hydrate-duplicate/_before_head.html b/test/hydration/samples/head-meta-hydrate-duplicate/_before_head.html new file mode 100644 index 0000000000..d2f218fb8d --- /dev/null +++ b/test/hydration/samples/head-meta-hydrate-duplicate/_before_head.html @@ -0,0 +1,4 @@ +Some Title + + + \ No newline at end of file diff --git a/test/hydration/samples/head-meta-hydrate-duplicate/_config.js b/test/hydration/samples/head-meta-hydrate-duplicate/_config.js new file mode 100644 index 0000000000..482efd564d --- /dev/null +++ b/test/hydration/samples/head-meta-hydrate-duplicate/_config.js @@ -0,0 +1,5 @@ +export default { + test(assert, target, snapshot, component, window) { + assert.equal(window.document.querySelectorAll('meta').length, 2); + } +}; diff --git a/test/hydration/samples/head-meta-hydrate-duplicate/main.svelte b/test/hydration/samples/head-meta-hydrate-duplicate/main.svelte new file mode 100644 index 0000000000..1a8b125dd2 --- /dev/null +++ b/test/hydration/samples/head-meta-hydrate-duplicate/main.svelte @@ -0,0 +1,8 @@ + + Some Title + + + + + +
Just a dummy page.
\ No newline at end of file diff --git a/test/server-side-rendering/samples/head-meta-hydrate-duplicate/_expected-head.html b/test/server-side-rendering/samples/head-meta-hydrate-duplicate/_expected-head.html new file mode 100644 index 0000000000..d2f218fb8d --- /dev/null +++ b/test/server-side-rendering/samples/head-meta-hydrate-duplicate/_expected-head.html @@ -0,0 +1,4 @@ +Some Title + + + \ No newline at end of file diff --git a/test/server-side-rendering/samples/head-meta-hydrate-duplicate/_expected.html b/test/server-side-rendering/samples/head-meta-hydrate-duplicate/_expected.html new file mode 100644 index 0000000000..a469e618fa --- /dev/null +++ b/test/server-side-rendering/samples/head-meta-hydrate-duplicate/_expected.html @@ -0,0 +1,3 @@ + + +
Just a dummy page.
\ No newline at end of file diff --git a/test/server-side-rendering/samples/head-meta-hydrate-duplicate/main.svelte b/test/server-side-rendering/samples/head-meta-hydrate-duplicate/main.svelte new file mode 100644 index 0000000000..1a8b125dd2 --- /dev/null +++ b/test/server-side-rendering/samples/head-meta-hydrate-duplicate/main.svelte @@ -0,0 +1,8 @@ + + Some Title + + + + + +
Just a dummy page.
\ No newline at end of file diff --git a/test/server-side-rendering/samples/head-multiple-title/_expected-head.html b/test/server-side-rendering/samples/head-multiple-title/_expected-head.html index af5c5feba4..7147550839 100644 --- a/test/server-side-rendering/samples/head-multiple-title/_expected-head.html +++ b/test/server-side-rendering/samples/head-multiple-title/_expected-head.html @@ -1 +1 @@ -B \ No newline at end of file +B \ No newline at end of file diff --git a/test/server-side-rendering/samples/head-title/_expected-head.html b/test/server-side-rendering/samples/head-title/_expected-head.html index 7d696352f9..6e73e671e6 100644 --- a/test/server-side-rendering/samples/head-title/_expected-head.html +++ b/test/server-side-rendering/samples/head-title/_expected-head.html @@ -1 +1 @@ -a custom title \ No newline at end of file +a custom title \ No newline at end of file