Repository: sveltejs/svelte (JavaScript)
Date: 2020-01-14 03:05:56+00:00
Task type: Feature

Issue:
Provide way to opt out of attributes on SSR'd `<head>` elements used for hydration
**Is your feature request related to a problem? Please describe.**
#4082 was a good solution for fixing `<head>` hydration, but unfortunately it adds unneeded attributes if one is using Svelte solely as a static site templating engine.

**Describe the solution you'd like**
I'm not sure. Maybe we only add the attributes if the user has specified `generate: 'ssr'` _and_ `hydratable: true`? (Would this be considered a breaking change? Maybe technically?) As far as I know, `hydratable: true` has no effect currently when compiling in SSR mode. To make Sapper also take advantage of this would involving updating the template so that the SSR compilation is also called with `hydratable: true`.

**Describe alternatives you've considered**
Doing nothing and living with these would certainly work. Some sort of post-processing of the generated HTML would also work, but would be hacky.

**How important is this feature to you?**
Eh, it would be nice. I would imagine that purely static HTML generation (with no hydration) represents a small fraction of Svelte's usage.

**Additional context**
Not much. My personal websites, which use Svelte as a static site generator, now have unnecessary attributes on their head elements.


Issue discussion:
Only add attribute when `ssr:true` and `hydratable:true` sounds great. Anyway before #4082 hydration in head tags doesn't work anyway.
Let's make this as a necessary upgrade step, to make hydrating head tags work 🙈

Solution patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
 # Svelte changelog
 
+## Unreleased
+
+* Only attach SSR mode markers to a component's `<head>` elements when compiling with `hydratable: true` ([#4258](https://github.com/sveltejs/svelte/issues/4258))
+
 ## 3.17.0
 
 * Remove old `<head>` elements during hydration so they aren't duplicated ([#1607](https://github.com/sveltejs/svelte/issues/1607))
diff --git a/site/content/docs/03-run-time.md b/site/content/docs/03-run-time.md
--- a/site/content/docs/03-run-time.md
+++ b/site/content/docs/03-run-time.md
@@ -887,7 +887,7 @@ Existing children of `target` are left where they are.
 
 ---
 
-The `hydrate` option instructs Svelte to upgrade existing DOM (usually from server-side rendering) rather than creating new elements. It will only work if the component was compiled with the [`hydratable: true` option](docs#svelte_compile).
+The `hydrate` option instructs Svelte to upgrade existing DOM (usually from server-side rendering) rather than creating new elements. It will only work if the component was compiled with the [`hydratable: true` option](docs#svelte_compile). Hydration of `<head>` elements only works properly if the server-side rendering code was also compiled with `hydratable: true`, which adds a marker to each element in the `<head>` so that the component knows which elements it's responsible for removing during hydration.
 
 Whereas children of `target` are normally left alone, `hydrate: true` will cause any children to be removed. For that reason, the `anchor` option cannot be used alongside `hydrate: true`.
 
diff --git a/site/content/docs/04-compile-time.md b/site/content/docs/04-compile-time.md
--- a/site/content/docs/04-compile-time.md
+++ b/site/content/docs/04-compile-time.md
@@ -68,7 +68,7 @@ The following options can be passed to the compiler. None are required:
 | `generate` | `"dom"` | If `"dom"`, Svelte emits a JavaScript class for mounting to the DOM. If `"ssr"`, Svelte emits an object with a `render` method suitable for server-side rendering. If `false`, no JavaScript or CSS is returned; just metadata.
 | `dev` | `false` | If `true`, causes extra code to be added to components that will perform runtime checks and provide debugging information during development.
 | `immutable` | `false` | If `true`, tells the compiler that you promise not to mutate any objects. This allows it to be less conservative about checking whether values have changed.
-| `hydratable` | `false` | If `true`, enables the `hydrate: true` runtime option, which allows a component to upgrade existing DOM rather than creating new DOM from scratch.
+| `hydratable` | `false` | If `true` when generating DOM code, enables the `hydrate: true` runtime option, which allows a component to upgrade existing DOM rather than creating new DOM from scratch. When generating SSR code, this adds markers to `<head>` elements so that hydration knows which to replace.
 | `legacy` | `false` | If `true`, generates code that will work in IE9 and IE10, which don't support things like `element.dataset`.
 | `accessors` | `false` | If `true`, getters and setters will be created for the component's props. If `false`, they will only be created for readonly exported values (i.e. those declared with `const`, `class` and `function`). If compiling with `customElement: true` this option defaults to `true`.
 | `customElement` | `false` | If `true`, tells the compiler to generate a custom element constructor instead of a regular Svelte component.
diff --git a/src/compiler/compile/render_ssr/handlers/Element.ts b/src/compiler/compile/render_ssr/handlers/Element.ts
--- a/src/compiler/compile/render_ssr/handlers/Element.ts
+++ b/src/compiler/compile/render_ssr/handlers/Element.ts
@@ -124,7 +124,7 @@ export default function(node: Element, renderer: Renderer, options: RenderOption
 		}
 	});
 
-	if (options.head_id) {
+	if (options.hydratable && options.head_id) {
 		renderer.add_string(` data-svelte="${options.head_id}"`);
 	}
 
diff --git a/src/compiler/compile/render_ssr/handlers/Title.ts b/src/compiler/compile/render_ssr/handlers/Title.ts
--- a/src/compiler/compile/render_ssr/handlers/Title.ts
+++ b/src/compiler/compile/render_ssr/handlers/Title.ts
@@ -5,7 +5,11 @@ import { x } from 'code-red';
 export default function(node: Title, renderer: Renderer, options: RenderOptions) {
 	renderer.push();
 
-	renderer.add_string(`<title data-svelte="${options.head_id}">`);
+	renderer.add_string('<title');
+	if (options.hydratable && options.head_id) {
+		renderer.add_string(` data-svelte="${options.head_id}"`);
+	}
+	renderer.add_string('>');
 
 	renderer.render(node.children, options);
 


Test patch:
diff --git a/test/helpers.js b/test/helpers.js
--- a/test/helpers.js
+++ b/test/helpers.js
@@ -45,6 +45,12 @@ export function tryToReadFile(file) {
 	}
 }
 
+export function cleanRequireCache() {
+	Object.keys(require.cache)
+		.filter(x => x.endsWith('.svelte'))
+		.forEach(file => delete require.cache[file]);
+}
+
 const virtualConsole = new jsdom.VirtualConsole();
 virtualConsole.sendTo(console);
 
diff --git a/test/runtime/index.js b/test/runtime/index.js
--- a/test/runtime/index.js
+++ b/test/runtime/index.js
@@ -10,6 +10,7 @@ import {
 	showOutput,
 	loadConfig,
 	loadSvelte,
+	cleanRequireCache,
 	env,
 	setupHtmlEqual,
 	mkdirp
@@ -79,11 +80,7 @@ describe("runtime", () => {
 			compileOptions.immutable = config.immutable;
 			compileOptions.accessors = 'accessors' in config ? config.accessors : true;
 
-			Object.keys(require.cache)
-				.filter(x => x.endsWith('.svelte'))
-				.forEach(file => {
-					delete require.cache[file];
-				});
+			cleanRequireCache();
 
 			let mod;
 			let SvelteComponent;
diff --git a/test/server-side-rendering/index.js b/test/server-side-rendering/index.js
--- a/test/server-side-rendering/index.js
+++ b/test/server-side-rendering/index.js
@@ -9,6 +9,7 @@ import {
 	loadSvelte,
 	setupHtmlEqual,
 	tryToLoadJson,
+	cleanRequireCache,
 	shouldUpdateExpected,
 	mkdirp
 } from "../helpers.js";
@@ -27,11 +28,6 @@ let compile = null;
 
 describe("ssr", () => {
 	before(() => {
-		require("../../register")({
-			extensions: ['.svelte', '.html'],
-			sveltePath
-		});
-
 		compile = loadSvelte(true).compile;
 
 		return setupHtmlEqual();
@@ -40,9 +36,11 @@ describe("ssr", () => {
 	fs.readdirSync(`${__dirname}/samples`).forEach(dir => {
 		if (dir[0] === ".") return;
 
+		const config = loadConfig(`${__dirname}/samples/${dir}/_config.js`);
+
 		// add .solo to a sample directory name to only run that test, or
 		// .show to always show the output. or both
-		const solo = /\.solo/.test(dir);
+		const solo = config.solo || /\.solo/.test(dir);
 		const show = /\.show/.test(dir);
 
 		if (solo && process.env.CI) {
@@ -51,6 +49,18 @@ describe("ssr", () => {
 
 		(solo ? it.only : it)(dir, () => {
 			dir = path.resolve(`${__dirname}/samples`, dir);
+
+			cleanRequireCache();
+
+			const compileOptions = {
+				sveltePath,
+				...config.compileOptions,
+				generate: 'ssr',
+				format: 'cjs'
+			};
+
+			require("../../register")(compileOptions);
+
 			try {
 				const Component = require(`${dir}/main.svelte`).default;
 
@@ -133,18 +143,16 @@ describe("ssr", () => {
 		(config.skip ? it.skip : solo ? it.only : it)(dir, () => {
 			const cwd = path.resolve("test/runtime/samples", dir);
 
-			Object.keys(require.cache)
-				.filter(x => x.endsWith('.svelte'))
-				.forEach(file => {
-					delete require.cache[file];
-				});
+			cleanRequireCache();
 
 			delete global.window;
 
-			const compileOptions = Object.assign({ sveltePath }, config.compileOptions, {
+			const compileOptions = {
+				sveltePath,
+				...config.compileOptions,
 				generate: 'ssr',
 				format: 'cjs'
-			});
+			};
 
 			require("../../register")(compileOptions);
 
diff --git a/test/server-side-rendering/samples/head-meta-hydrate-duplicate/_config.js b/test/server-side-rendering/samples/head-meta-hydrate-duplicate/_config.js
new file mode 100644
--- /dev/null
+++ b/test/server-side-rendering/samples/head-meta-hydrate-duplicate/_config.js
@@ -0,0 +1,5 @@
+export default {
+	compileOptions: {
+		hydratable: true
+	}
+};
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
--- 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 @@
-<title data-svelte="svelte-1csszk6">B</title>
\ No newline at end of file
+<title>B</title>
\ 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
--- a/test/server-side-rendering/samples/head-title/_expected-head.html
+++ b/test/server-side-rendering/samples/head-title/_expected-head.html
@@ -1 +1 @@
-<title data-svelte="svelte-135agoq">a custom title</title>
\ No newline at end of file
+<title>a custom title</title>
\ No newline at end of file