Repository: sveltejs/kit (js)

Issue:
`browser.hydrate: false` breaks client-side router
### Describe the bug

Setting `browser.hydrate` to `false` renders the client-side router unusable whilst not disabling it. This results in the following behaviour:

1. Link gets clicked
2. URL in address bar changes
3. Content of page stays unchanged

Setting `export const hydrate = false` on a page does interestingly not break the router.

### Reproduction

[🚩 Link to a demo](https://stackblitz.com/edit/sveltejs-kit-template-default-q36fqu?file=svelte.config.js)

1. Start the dev server and click on the links to observe the broken router
2. Comment the `browser: { hydrate: false },` line in `svelte.config.js` and restart the dev server to see the router working again.

### Logs

When clicking on link (browser console):
```
[HMR][Svelte] Unrecoverable HMR error in <Root>: next update will trigger a full reload
Uncaught (in promise) Error: options.hydrate only works if the component was compiled with the `hydratable: true` option
    at Object.claim [as l] (root.svelte? [sm]:50:13)
    at init (index.mjs:1883:40)
    at new Root (root.svelte? [sm]:16:25)
    at createProxiedComponent (svelte-hooks.js:341:9)
    at new ProxyComponent (proxy.js:242:7)
    at new Proxy<Root> (proxy.js:349:11)
    at initialize (start.js:702:10)
    at update (start.js:631:4)
    at async navigate (start.js:1241:3)
```
![image](https://user-images.githubusercontent.com/79395582/158990748-6225bfe9-563e-4e79-a6a6-9ce04d743802.png)

### System Info

```shell
System:
    OS: Windows 10 10.0.19044
    CPU: (4) x64 Intel(R) Core(TM) i5-7500 CPU @ 3.40GHz
    Memory: 10.11 GB / 15.93 GB
  Binaries:
    Node: 16.13.2 - C:\Program Files\nodejs\node.EXE
    npm: 8.3.2 - C:\Program Files\nodejs\npm.CMD
  Browsers:
    Edge: Spartan (44.19041.1266.0), Chromium (99.0.1150.36)
  npmPackages:
    @sveltejs/adapter-netlify: ^1.0.0-next.51 => 1.0.0-next.51
    @sveltejs/kit: next => 1.0.0-next.299
    svelte: ^3.46.4 => 3.46.4
```


### Severity

serious, but I can work around it

### Additional Information

_No response_

Pull request description:
[fix] Throw on invalid browser options
Throw error if browser.hydrate is false and brower.router is true. The router needs at least the router component to be hydrated, and we currently can't dynamically compile Svelte components as hydratable or not on case-by-case basis
Closes #4382

### Please don't delete this checklist! Before submitting the PR, please make sure you do the following:
- [x] It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs
- [x] This message body should clearly illustrate what problems it solves.
- [x] Ideally, include a test that fails without this PR but passes with it.

### Tests
- [x] Run the tests with `pnpm test` and lint the project with `pnpm lint` and `pnpm check`

### Changesets
- [x] If your PR makes a change that should be noted in one or more packages' changelogs, generate a changeset by running `pnpm changeset` and following the prompts. All changesets should be `patch` until SvelteKit 1.0


Patch:
diff --git a/.changeset/ten-gorillas-itch.md b/.changeset/ten-gorillas-itch.md
new file mode 100644
index 000000000..15790d6cb
--- /dev/null
+++ b/.changeset/ten-gorillas-itch.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+Throw error if browser.hydrate is false and browser.router is true
diff --git a/packages/kit/src/core/config/index.spec.js b/packages/kit/src/core/config/index.spec.js
index 3cd626d49..e9d6e0e66 100644
--- a/packages/kit/src/core/config/index.spec.js
+++ b/packages/kit/src/core/config/index.spec.js
@@ -246,6 +246,19 @@ test('fails if kit.appDir ends with slash', () => {
 	}, /^config\.kit\.appDir cannot start or end with '\/'. See https:\/\/kit\.svelte\.dev\/docs\/configuration$/);
 });
 
+test('fails if browser.hydrate is false and browser.router is true', () => {
+	assert.throws(() => {
+		validate_config({
+			kit: {
+				browser: {
+					hydrate: false,
+					router: true
+				}
+			}
+		});
+	}, /^config\.kit\.browser\.router cannot be true if config\.kit\.browser\.hydrate is false$/);
+});
+
 test('fails if paths.base is not root-relative', () => {
 	assert.throws(() => {
 		validate_config({
diff --git a/packages/kit/src/core/config/options.js b/packages/kit/src/core/config/options.js
index 9d89fd4ee..c82f008d8 100644
--- a/packages/kit/src/core/config/options.js
+++ b/packages/kit/src/core/config/options.js
@@ -107,9 +107,14 @@ const options = object(
 				return input;
 			}),
 
-			browser: object({
-				hydrate: boolean(true),
-				router: boolean(true)
+			browser: validate({ hydrate: true, router: true }, (input, keypath) => {
+				const value = object({ hydrate: boolean(true), router: boolean(true) })(input, keypath);
+				if (!value.hydrate && value.router) {
+					throw new Error(
+						'config.kit.browser.router cannot be true if config.kit.browser.hydrate is false'
+					);
+				}
+				return value;
 			}),
 
 			csp: object({


Test patch:
diff --git a/packages/create-svelte/test/check.js b/packages/create-svelte/test/check.js
index 2fa477952..5e74a81da 100644
--- a/packages/create-svelte/test/check.js
+++ b/packages/create-svelte/test/check.js
@@ -2,6 +2,7 @@ import fs from 'fs';
 import { execSync } from 'child_process';
 import path from 'path';
 import { test } from 'uvu';
+import { Writable } from 'stream';
 import * as assert from 'uvu/assert';
 import { create } from '../index.js';
 import { fileURLToPath } from 'url';
@@ -43,7 +44,8 @@ test.before(() => {
 		fs.writeFileSync(path.join(test_workspace_dir, 'pnpm-workspace.yaml'), 'packages:\n  - ./*\n');
 
 		// force creation of pnpm-lock.yaml in test workspace
-		execSync('pnpm install --no-frozen-lockfile', { dir: test_workspace_dir, stdio: 'inherit' });
+		console.log(`running pnpm install in .test-tmp/create-svelte`);
+		execSync('pnpm install --no-frozen-lockfile', { dir: test_workspace_dir, stdio: 'ignore' });
 	} catch (e) {
 		console.error('failed to setup create-svelte test workspace', e);
 		throw e;
@@ -84,7 +86,8 @@ for (const template of fs.readdirSync('templates')) {
 			fs.writeFileSync(path.join(cwd, 'package.json'), JSON.stringify(pkg, null, '\t') + '\n');
 
 			// this pnpm install works in the test workspace, which redirects to our local packages again
-			execSync('pnpm install --no-frozen-lockfile', { cwd, stdio: 'inherit' });
+			console.log(`running pnpm install in ${cwd}`);
+			execSync('pnpm install --no-frozen-lockfile', { cwd, stdio: 'ignore' });
 
 			// run provided scripts that are non-blocking. All of them should exit with 0
 			const scripts_to_test = ['prepare', 'check', 'lint', 'build', 'sync'];
@@ -95,17 +98,20 @@ for (const template of fs.readdirSync('templates')) {
 			}
 
 			// not all templates have all scripts
+			console.group(`${template}-${types}`);
 			for (const script of Object.keys(pkg.scripts).filter((s) => scripts_to_test.includes(s))) {
-				const command = `pnpm run ${script}`;
 				try {
-					console.log(`executing ${command} in ${cwd}`);
-					execSync(command, { cwd, stdio: 'inherit' });
+					execSync(`pnpm run ${script}`, { cwd, stdio: 'pipe' });
+					console.log(`✅ ${script}`);
 				} catch (e) {
-					const msg = `${command} failed in ${cwd}`;
-					console.error(msg, e);
-					assert.unreachable(msg);
+					console.error(`❌ ${script}`);
+					console.error(`---\nstdout:\n${e.stdout}`);
+					console.error(`---\nstderr:\n${e.stderr}`);
+					console.groupEnd();
+					assert.unreachable(e.message);
 				}
 			}
+			console.groupEnd();
 		});
 	}
 }