Repository: sveltejs/kit
Pull request #8384 (2023-01-07 18:18:08)

add assetFileNames to worker rollupOptions
fixes https://github.com/sveltejs/kit/issues/8381

### 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
- [ ] This message body should clearly illustrate what problems it solves.
- [ ] 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


changeset-bot[bot]: ###  ⚠️  No Changeset found

Latest commit: d60c95487b1a3c801c632dff8c3c19064f6528f6

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/master/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/TimoWilhelm/svelte-kit/new/master?filename=.changeset/blue-starfishes-wink.md&value=---%0A%22%40sveltejs%2Fkit%22%3A%20patch%0A---%0A%0Aadd%20assetFileNames%20to%20worker%20rollupOptions%0A)



Patch:
diff --git a/.changeset/rare-nails-guess.md b/.changeset/rare-nails-guess.md
new file mode 100644
index 000000000..9b2df8985
--- /dev/null
+++ b/.changeset/rare-nails-guess.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+Add `assetFileNames` to worker rollup options
diff --git a/documentation/docs/10-getting-started/40-web-standards.md b/documentation/docs/10-getting-started/40-web-standards.md
index 58fd9fbaf..a12f534cf 100644
--- a/documentation/docs/10-getting-started/40-web-standards.md
+++ b/documentation/docs/10-getting-started/40-web-standards.md
@@ -12,7 +12,7 @@ In particular, you'll get comfortable with the following:
 
 SvelteKit uses [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/fetch) for getting data from the network. It's available in [hooks](/docs/hooks) and [server routes](/docs/routing#server) as well as in the browser.
 
-> A special version of `fetch` is available in [`load`](/docs/load) functions for invoking endpoints directly during server-side rendering, without making an HTTP call, while preserving credentials. (To make credentialled fetches in server-side code outside `load`, you must explicitly pass `cookie` and/or `authorization` headers.) It also allows you to make relative requests, whereas server-side `fetch` normally requires a fully qualified URL.
+> A special version of `fetch` is available in [`load`](/docs/load) functions, [server hooks](/docs/hooks#server-hooks) and [API routes](/docs/routing#server) for invoking endpoints directly during server-side rendering, without making an HTTP call, while preserving credentials. (To make credentialled fetches in server-side code outside `load`, you must explicitly pass `cookie` and/or `authorization` headers.) It also allows you to make relative requests, whereas server-side `fetch` normally requires a fully qualified URL.
 
 Besides `fetch` itself, the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) includes the following interfaces:
 
diff --git a/documentation/docs/20-core-concepts/20-load.md b/documentation/docs/20-core-concepts/20-load.md
index a4f8d9561..834f4101c 100644
--- a/documentation/docs/20-core-concepts/20-load.md
+++ b/documentation/docs/20-core-concepts/20-load.md
@@ -521,7 +521,7 @@ export async function load({ fetch, depends }) {
 ```svelte
 /// file: src/routes/random-number/+page.svelte
 <script>
-	import { invalidateAll } from '$app/navigation';
+	import { invalidate, invalidateAll } from '$app/navigation';
 
 	/** @type {import('./$types').PageData} */
 	export let data;
diff --git a/documentation/docs/30-advanced/25-errors.md b/documentation/docs/30-advanced/25-errors.md
index 40ac2269e..249aa8dd2 100644
--- a/documentation/docs/30-advanced/25-errors.md
+++ b/documentation/docs/30-advanced/25-errors.md
@@ -106,6 +106,8 @@ export function handleError({ error, event }) {
 }
 ```
 
+> Make sure that `handleError` _never_ throws an error
+
 ## Responses
 
 If an error occurs inside `handle` or inside a [`+server.js`](/docs/routing#server) request handler, SvelteKit will respond with either a fallback error page or a JSON representation of the error object, depending on the request's `Accept` headers.
diff --git a/documentation/docs/30-advanced/50-server-only-modules.md b/documentation/docs/30-advanced/50-server-only-modules.md
index c14a4f21d..e6c424003 100644
--- a/documentation/docs/30-advanced/50-server-only-modules.md
+++ b/documentation/docs/30-advanced/50-server-only-modules.md
@@ -51,4 +51,6 @@ Cannot import $lib/server/secrets.js into public-facing code:
 
 Even though the public-facing code — `src/routes/+page.svelte` — only uses the `add` export and not the secret `atlantisCoordinates` export, the secret code could end up in JavaScript that the browser downloads, and so the import chain is considered unsafe.
 
-This feature also works with dynamic imports, even interpolated ones like ``await import(`./${foo}.js`)``, with one small caveat: during development, if there are two or more dynamic imports between the public-facing code and the server-only module, the illegal import will not be detected the first time the code is loaded.
\ No newline at end of file
+This feature also works with dynamic imports, even interpolated ones like ``await import(`./${foo}.js`)``, with one small caveat: during development, if there are two or more dynamic imports between the public-facing code and the server-only module, the illegal import will not be detected the first time the code is loaded.
+
+> Unit testing frameworks like Vitest do not distinguish between server-only and public-facing code. For this reason, illegal import detection is disabled when running tests, as determined by `process.env.TEST === 'true'`.
\ No newline at end of file
diff --git a/documentation/docs/60-appendix/10-migrating.md b/documentation/docs/60-appendix/10-migrating.md
index 759bec196..2e04f115c 100644
--- a/documentation/docs/60-appendix/10-migrating.md
+++ b/documentation/docs/60-appendix/10-migrating.md
@@ -150,7 +150,7 @@ See [the FAQ](/faq#integrations) for detailed information about integrations.
 
 ### HTML minifier
 
-Sapper includes `html-minifier` by default. SvelteKit does not include this, but it can be added as a [hook](/docs/hooks#server-hooks-handle):
+Sapper includes `html-minifier` by default. SvelteKit does not include this, but you can add it as a prod dependency and then use it through a [hook](/docs/hooks#server-hooks-handle):
 
 ```js
 // @filename: ambient.d.ts
diff --git a/package.json b/package.json
index 955239522..1b4e7d3ac 100644
--- a/package.json
+++ b/package.json
@@ -36,7 +36,7 @@
 		"turbo": "^1.6.3",
 		"typescript": "^4.9.4"
 	},
-	"packageManager": "pnpm@7.22.0",
+	"packageManager": "pnpm@7.23.0",
 	"engines": {
 		"pnpm": "^7.0.0"
 	},
diff --git a/packages/kit/CHANGELOG.md b/packages/kit/CHANGELOG.md
index 3a5baa878..6b09a5a25 100644
--- a/packages/kit/CHANGELOG.md
+++ b/packages/kit/CHANGELOG.md
@@ -1,5 +1,25 @@
 # @sveltejs/kit
 
+## 1.0.8
+
+### Patch Changes
+
+- Disable illegal import detection when running unit tests ([#8365](https://github.com/sveltejs/kit/pull/8365))
+
+- only prerender a given dependency once ([#8376](https://github.com/sveltejs/kit/pull/8376))
+
+- Error in dev mode if global `fetch` is used with relative URL ([#8370](https://github.com/sveltejs/kit/pull/8370))
+
+- `updated.check()` type changed to Promise<boolean> ([#8400](https://github.com/sveltejs/kit/pull/8400))
+
+- Don't strip body in no-cors mode on the server ([#8412](https://github.com/sveltejs/kit/pull/8412))
+
+- [fix] quote 'script' in CSP directives ([#8372](https://github.com/sveltejs/kit/pull/8372))
+
+- fix route id comparison for load change detection ([#8399](https://github.com/sveltejs/kit/pull/8399))
+
+- [fix] don't add nonce attribute to `<link>` elements ([#8369](https://github.com/sveltejs/kit/pull/8369))
+
 ## 1.0.7
 
 ### Patch Changes
diff --git a/packages/kit/package.json b/packages/kit/package.json
index 18e143eff..0f6619c54 100644
--- a/packages/kit/package.json
+++ b/packages/kit/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "@sveltejs/kit",
-	"version": "1.0.7",
+	"version": "1.0.8",
 	"repository": {
 		"type": "git",
 		"url": "https://github.com/sveltejs/kit",
diff --git a/packages/kit/src/core/prerender/prerender.js b/packages/kit/src/core/prerender/prerender.js
index aa3e732c6..0e6f86306 100644
--- a/packages/kit/src/core/prerender/prerender.js
+++ b/packages/kit/src/core/prerender/prerender.js
@@ -102,10 +102,20 @@ export async function prerender() {
 	/** @type {import('types').SSRManifest} */
 	const manifest = (await import(pathToFileURL(manifest_path).href)).manifest;
 
+	/** @type {Map<string, string>} */
+	const saved = new Map();
+
 	override({
 		building: true,
 		paths: config.paths,
-		read: (file) => readFileSync(join(config.files.assets, file))
+		read: (file) => {
+			// stuff we just wrote
+			const filepath = saved.get(file);
+			if (filepath) return readFileSync(filepath);
+
+			// stuff in `static`
+			return readFileSync(join(config.files.assets, file));
+		}
 	});
 
 	const server = new Server(manifest);
@@ -222,6 +232,7 @@ export async function prerender() {
 			}
 
 			const body = result.body ?? new Uint8Array(await result.response.arrayBuffer());
+
 			save(
 				'dependencies',
 				result.response,
@@ -351,6 +362,9 @@ export async function prerender() {
 		} else if (response_type !== OK) {
 			handle_http_error({ status: response.status, path: decoded, referrer, referenceType });
 		}
+
+		manifest.assets.add(file);
+		saved.set(file, dest);
 	}
 
 	for (const route of manifest._.routes) {
diff --git a/packages/kit/src/exports/vite/build/utils.js b/packages/kit/src/exports/vite/build/utils.js
index 1198053c5..c5bf6e448 100644
--- a/packages/kit/src/exports/vite/build/utils.js
+++ b/packages/kit/src/exports/vite/build/utils.js
@@ -189,6 +189,7 @@ export function get_build_compile_config({ config, input, ssr, outDir }) {
 				output: {
 					entryFileNames: `${prefix}/workers/[name]-[hash].js`,
 					chunkFileNames: `${prefix}/workers/chunks/[name]-[hash].js`,
+					assetFileNames: `${prefix}/workers/assets/[name]-[hash][extname]`,
 					hoistTransitiveImports: false
 				}
 			}
diff --git a/packages/kit/src/exports/vite/dev/index.js b/packages/kit/src/exports/vite/dev/index.js
index 571db3d22..f1021713c 100644
--- a/packages/kit/src/exports/vite/dev/index.js
+++ b/packages/kit/src/exports/vite/dev/index.js
@@ -29,6 +29,17 @@ export async function dev(vite, vite_config, svelte_config) {
 		installPolyfills();
 	}
 
+	const fetch = globalThis.fetch;
+	globalThis.fetch = (info, init) => {
+		if (typeof info === 'string' && !/^\w+:\/\//.test(info)) {
+			throw new Error(
+				`Cannot use relative URL (${info}) with global fetch — use \`event.fetch\` instead: https://kit.svelte.dev/docs/web-standards#fetch-apis`
+			);
+		}
+
+		return fetch(info, init);
+	};
+
 	sync.init(svelte_config, vite_config.mode);
 
 	/** @type {import('types').Respond} */
diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js
index 0acd4dabd..03956dd3b 100644
--- a/packages/kit/src/exports/vite/index.js
+++ b/packages/kit/src/exports/vite/index.js
@@ -301,7 +301,7 @@ function kit({ svelte_config }) {
 		},
 
 		async load(id, options) {
-			if (options?.ssr === false) {
+			if (options?.ssr === false && process.env.TEST !== 'true') {
 				const normalized_cwd = vite.normalizePath(cwd);
 				const normalized_lib = vite.normalizePath(svelte_config.kit.files.lib);
 				if (
diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js
index 6ef4cc0ed..69fb5e4f1 100644
--- a/packages/kit/src/runtime/client/client.js
+++ b/packages/kit/src/runtime/client/client.js
@@ -709,7 +709,7 @@ export function create_client({ target, base }) {
 		let server_data = null;
 
 		const url_changed = current.url ? id !== current.url.pathname + current.url.search : false;
-		const route_changed = current.route ? id !== current.route.id : false;
+		const route_changed = current.route ? route.id !== current.route.id : false;
 
 		const invalid_server_nodes = loaders.reduce((acc, loader, i) => {
 			const previous = current.branch[i];
diff --git a/packages/kit/src/runtime/client/utils.js b/packages/kit/src/runtime/client/utils.js
index 8e1a04764..aa14bcce7 100644
--- a/packages/kit/src/runtime/client/utils.js
+++ b/packages/kit/src/runtime/client/utils.js
@@ -211,6 +211,7 @@ export function create_updated_store() {
 	/** @type {NodeJS.Timeout} */
 	let timeout;
 
+	/** @type {() => Promise<boolean>} */
 	async function check() {
 		if (DEV || !BROWSER) return false;
 
diff --git a/packages/kit/src/runtime/server/fetch.js b/packages/kit/src/runtime/server/fetch.js
index 89f21b5db..8ff06eec8 100644
--- a/packages/kit/src/runtime/server/fetch.js
+++ b/packages/kit/src/runtime/server/fetch.js
@@ -63,17 +63,7 @@ export function create_fetch({ event, options, state, get_cookie_header }) {
 						if (cookie) request.headers.set('cookie', cookie);
 					}
 
-					let response = await fetch(request);
-
-					if (mode === 'no-cors') {
-						response = new Response('', {
-							status: response.status,
-							statusText: response.statusText,
-							headers: response.headers
-						});
-					}
-
-					return response;
+					return fetch(request);
 				}
 
 				/** @type {Response} */
diff --git a/packages/kit/src/runtime/server/page/csp.js b/packages/kit/src/runtime/server/page/csp.js
index 282369140..f7b8eedd4 100644
--- a/packages/kit/src/runtime/server/page/csp.js
+++ b/packages/kit/src/runtime/server/page/csp.js
@@ -16,7 +16,8 @@ const quoted = new Set([
 	'none',
 	'strict-dynamic',
 	'report-sample',
-	'wasm-unsafe-eval'
+	'wasm-unsafe-eval',
+	'script'
 ]);
 
 const crypto_pattern = /^(nonce|sha\d\d\d)-/;
diff --git a/packages/kit/src/runtime/server/page/load_data.js b/packages/kit/src/runtime/server/page/load_data.js
index f5f20c44f..7b12a46fe 100644
--- a/packages/kit/src/runtime/server/page/load_data.js
+++ b/packages/kit/src/runtime/server/page/load_data.js
@@ -107,129 +107,150 @@ export async function load_data({
 		params: event.params,
 		data: server_data_node?.data ?? null,
 		route: event.route,
-		fetch: async (input, init) => {
-			const cloned_body = input instanceof Request && input.body ? input.clone().body : null;
-			const response = await event.fetch(input, init);
+		fetch: create_universal_fetch(event, state, fetched, csr, resolve_opts),
+		setHeaders: event.setHeaders,
+		depends: () => {},
+		parent
+	});
 
-			const url = new URL(input instanceof Request ? input.url : input, event.url);
-			const same_origin = url.origin === event.url.origin;
+	const data = result ? await unwrap_promises(result) : null;
+	validate_load_response(data, /** @type {string} */ (event.route.id));
+	return data;
+}
+
+/**
+ * @param {Pick<import('types').RequestEvent, 'fetch' | 'url' | 'request' | 'route'>} event
+ * @param {import("types").SSRState} state
+ * @param {import("./types").Fetched[]} fetched
+ * @param {boolean} csr
+ * @param {Pick<Required<import("types").ResolveOptions>, 'filterSerializedResponseHeaders'>} resolve_opts
+ */
+export function create_universal_fetch(event, state, fetched, csr, resolve_opts) {
+	/**
+	 * @param {URL | RequestInfo} input
+	 * @param {RequestInit} [init]
+	 */
+	return async (input, init) => {
+		const cloned_body = input instanceof Request && input.body ? input.clone().body : null;
+		let response = await event.fetch(input, init);
 
-			/** @type {import('types').PrerenderDependency} */
-			let dependency;
+		const url = new URL(input instanceof Request ? input.url : input, event.url);
+		const same_origin = url.origin === event.url.origin;
 
-			if (same_origin) {
-				if (state.prerendering) {
-					dependency = { response, body: null };
-					state.prerendering.dependencies.set(url.pathname, dependency);
-				}
+		/** @type {import('types').PrerenderDependency} */
+		let dependency;
+
+		if (same_origin) {
+			if (state.prerendering) {
+				dependency = { response, body: null };
+				state.prerendering.dependencies.set(url.pathname, dependency);
+			}
+		} else {
+			// simulate CORS errors and "no access to body in no-cors mode" server-side for consistency with client-side behaviour
+			const mode = input instanceof Request ? input.mode : init?.mode ?? 'cors';
+			if (mode === 'no-cors') {
+				response = new Response('', {
+					status: response.status,
+					statusText: response.statusText,
+					headers: response.headers
+				});
 			} else {
-				// simulate CORS errors server-side for consistency with client-side behaviour
-				const mode = input instanceof Request ? input.mode : init?.mode ?? 'cors';
-				if (mode !== 'no-cors') {
-					const acao = response.headers.get('access-control-allow-origin');
-					if (!acao || (acao !== event.url.origin && acao !== '*')) {
-						throw new Error(
-							`CORS error: ${
-								acao ? 'Incorrect' : 'No'
-							} 'Access-Control-Allow-Origin' header is present on the requested resource`
-						);
-					}
+				const acao = response.headers.get('access-control-allow-origin');
+				if (!acao || (acao !== event.url.origin && acao !== '*')) {
+					throw new Error(
+						`CORS error: ${
+							acao ? 'Incorrect' : 'No'
+						} 'Access-Control-Allow-Origin' header is present on the requested resource`
+					);
 				}
 			}
+		}
 
-			const proxy = new Proxy(response, {
-				get(response, key, _receiver) {
-					async function text() {
-						const body = await response.text();
-
-						if (!body || typeof body === 'string') {
-							const status_number = Number(response.status);
-							if (isNaN(status_number)) {
-								throw new Error(
-									`response.status is not a number. value: "${
-										response.status
-									}" type: ${typeof response.status}`
-								);
-							}
-
-							fetched.push({
-								url: same_origin ? url.href.slice(event.url.origin.length) : url.href,
-								method: event.request.method,
-								request_body: /** @type {string | ArrayBufferView | undefined} */ (
-									input instanceof Request && cloned_body
-										? await stream_to_string(cloned_body)
-										: init?.body
-								),
-								response_body: body,
-								response: response
-							});
-						}
+		const proxy = new Proxy(response, {
+			get(response, key, _receiver) {
+				async function text() {
+					const body = await response.text();
 
-						if (dependency) {
-							dependency.body = body;
+					if (!body || typeof body === 'string') {
+						const status_number = Number(response.status);
+						if (isNaN(status_number)) {
+							throw new Error(
+								`response.status is not a number. value: "${
+									response.status
+								}" type: ${typeof response.status}`
+							);
 						}
 
-						return body;
+						fetched.push({
+							url: same_origin ? url.href.slice(event.url.origin.length) : url.href,
+							method: event.request.method,
+							request_body: /** @type {string | ArrayBufferView | undefined} */ (
+								input instanceof Request && cloned_body
+									? await stream_to_string(cloned_body)
+									: init?.body
+							),
+							response_body: body,
+							response: response
+						});
 					}
 
-					if (key === 'arrayBuffer') {
-						return async () => {
-							const buffer = await response.arrayBuffer();
+					if (dependency) {
+						dependency.body = body;
+					}
 
-							if (dependency) {
-								dependency.body = new Uint8Array(buffer);
-							}
+					return body;
+				}
 
-							// TODO should buffer be inlined into the page (albeit base64'd)?
-							// any conditions in which it shouldn't be?
+				if (key === 'arrayBuffer') {
+					return async () => {
+						const buffer = await response.arrayBuffer();
 
-							return buffer;
-						};
-					}
+						if (dependency) {
+							dependency.body = new Uint8Array(buffer);
+						}
 
-					if (key === 'text') {
-						return text;
-					}
+						// TODO should buffer be inlined into the page (albeit base64'd)?
+						// any conditions in which it shouldn't be?
 
-					if (key === 'json') {
-						return async () => {
-							return JSON.parse(await text());
-						};
-					}
+						return buffer;
+					};
+				}
 
-					return Reflect.get(response, key, response);
+				if (key === 'text') {
+					return text;
+				}
+
+				if (key === 'json') {
+					return async () => {
+						return JSON.parse(await text());
+					};
 				}
-			});
-
-			if (csr) {
-				// ensure that excluded headers can't be read
-				const get = response.headers.get;
-				response.headers.get = (key) => {
-					const lower = key.toLowerCase();
-					const value = get.call(response.headers, lower);
-					if (value && !lower.startsWith('x-sveltekit-')) {
-						const included = resolve_opts.filterSerializedResponseHeaders(lower, value);
-						if (!included) {
-							throw new Error(
-								`Failed to get response header "${lower}" — it must be included by the \`filterSerializedResponseHeaders\` option: https://kit.svelte.dev/docs/hooks#server-hooks-handle (at ${event.route.id})`
-							);
-						}
-					}
 
-					return value;
-				};
+				return Reflect.get(response, key, response);
 			}
+		});
 
-			return proxy;
-		},
-		setHeaders: event.setHeaders,
-		depends: () => {},
-		parent
-	});
+		if (csr) {
+			// ensure that excluded headers can't be read
+			const get = response.headers.get;
+			response.headers.get = (key) => {
+				const lower = key.toLowerCase();
+				const value = get.call(response.headers, lower);
+				if (value && !lower.startsWith('x-sveltekit-')) {
+					const included = resolve_opts.filterSerializedResponseHeaders(lower, value);
+					if (!included) {
+						throw new Error(
+							`Failed to get response header "${lower}" — it must be included by the \`filterSerializedResponseHeaders\` option: https://kit.svelte.dev/docs/hooks#server-hooks-handle (at ${event.route.id})`
+						);
+					}
+				}
 
-	const data = result ? await unwrap_promises(result) : null;
-	validate_load_response(data, /** @type {string} */ (event.route.id));
-	return data;
+				return value;
+			};
+		}
+
+		return proxy;
+	};
 }
 
 /**
diff --git a/packages/kit/src/runtime/server/page/load_data.spec.js b/packages/kit/src/runtime/server/page/load_data.spec.js
new file mode 100644
index 000000000..493cbcfc5
--- /dev/null
+++ b/packages/kit/src/runtime/server/page/load_data.spec.js
@@ -0,0 +1,73 @@
+import { test } from 'uvu';
+import * as assert from 'uvu/assert';
+import { create_universal_fetch } from './load_data.js';
+
+/**
+ * @param {Partial<Pick<import('types').RequestEvent, 'fetch' | 'url' | 'request' | 'route'>>} event
+ */
+function create_fetch(event) {
+	event.fetch = event.fetch || (async () => new Response('foo'));
+	event.request = event.request || new Request('doesnt:matter');
+	event.route = event.route || { id: 'foo' };
+	event.url = event.url || new URL('https://domain-a.com');
+	return create_universal_fetch(
+		/** @type {Pick<import('types').RequestEvent, 'fetch' | 'url' | 'request' | 'route'>} */ (
+			event
+		),
+		{ getClientAddress: () => '' },
+		[],
+		true,
+		{
+			filterSerializedResponseHeaders: () => false
+		}
+	);
+}
+
+test('sets body to empty when mode is no-cors', async () => {
+	const fetch = create_fetch({});
+	const response = await fetch('https://domain-b.com', { mode: 'no-cors' });
+	const text = await response.text();
+	assert.equal(text, '');
+});
+
+test('keeps body when mode isnt no-cors on same domain', async () => {
+	const fetch = create_fetch({});
+	const response = await fetch('https://domain-a.com');
+	const text = await response.text();
+	assert.equal(text, 'foo');
+});
+
+test('succeeds when acao header present on cors', async () => {
+	const fetch = create_fetch({
+		fetch: async () => new Response('foo', { headers: { 'access-control-allow-origin': '*' } })
+	});
+	const response = await fetch('https://domain-a.com');
+	const text = await response.text();
+	assert.equal(text, 'foo');
+});
+
+test('errors when no acao header present on cors', async () => {
+	const fetch = create_fetch({});
+	try {
+		const response = await fetch('https://domain-b.com');
+		await response.text();
+		assert.unreachable('should have thrown cors error');
+	} catch (e) {
+		assert.ok(e instanceof Error);
+		assert.match(
+			e.message,
+			/CORS error: No 'Access-Control-Allow-Origin' header is present on the requested resource/
+		);
+	}
+});
+
+test('errors when trying to access non-serialized request headers on the server', async () => {
+	const fetch = create_fetch({});
+	const response = await fetch('https://domain-a.com');
+	assert.throws(
+		() => response.headers.get('content-type'),
+		/Failed to get response header "content-type" — it must be included by the `filterSerializedResponseHeaders` option/
+	);
+});
+
+test.run();
diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js
index 91337cab0..030b43561 100644
--- a/packages/kit/src/runtime/server/page/render.js
+++ b/packages/kit/src/runtime/server/page/render.js
@@ -211,22 +211,17 @@ export async function render_response({
 		const path = prefixed(dep);
 
 		if (resolve_opts.preload({ type: 'css', path })) {
-			const attributes = [];
-
-			if (csp.style_needs_nonce) {
-				attributes.push(`nonce="${csp.nonce}"`);
-			}
+			const attributes = ['rel="stylesheet"'];
 
 			if (inline_styles.has(dep)) {
 				// don't load stylesheets that are already inlined
 				// include them in disabled state so that Vite can detect them and doesn't try to add them
 				attributes.push('disabled', 'media="(max-width: 0)"');
 			} else {
-				const preload_atts = ['rel="preload"', 'as="style"'].concat(attributes);
+				const preload_atts = ['rel="preload"', 'as="style"'];
 				link_header_preloads.add(`<${encodeURI(path)}>; ${preload_atts.join(';')}; nopush`);
 			}
 
-			attributes.unshift('rel="stylesheet"');
 			head += `\n\t\t<link href="${path}" ${attributes.join(' ')}>`;
 		}
 	}
diff --git a/packages/kit/types/ambient.d.ts b/packages/kit/types/ambient.d.ts
index efcc1904d..a8bd4cf9f 100644
--- a/packages/kit/types/ambient.d.ts
+++ b/packages/kit/types/ambient.d.ts
@@ -320,7 +320,7 @@ declare module '$app/stores' {
 	/**
 	 *  A readable store whose initial value is `false`. If [`version.pollInterval`](https://kit.svelte.dev/docs/configuration#version) is a non-zero value, SvelteKit will poll for new versions of the app and update the store value to `true` when it detects one. `updated.check()` will force an immediate check, regardless of polling.
 	 */
-	export const updated: Readable<boolean> & { check(): boolean };
+	export const updated: Readable<boolean> & { check(): Promise<boolean> };
 
 	/**
 	 * A function that returns all of the contextual stores. On the server, this must be called during component initialization.


Test patch:
diff --git a/packages/kit/test/apps/basics/src/routes/load/cors/+page.js b/packages/kit/test/apps/basics/src/routes/load/cors/+page.js
deleted file mode 100644
index 9fbd10aed..000000000
--- a/packages/kit/test/apps/basics/src/routes/load/cors/+page.js
+++ /dev/null
@@ -1,8 +0,0 @@
-/** @type {import('./$types').PageLoad} */
-export async function load({ fetch, url }) {
-	const res = await fetch(`http://localhost:${url.searchParams.get('port')}`);
-
-	return {
-		text: await res.text()
-	};
-}
diff --git a/packages/kit/test/apps/basics/src/routes/load/cors/no-cors/+page.js b/packages/kit/test/apps/basics/src/routes/load/cors/no-cors/+page.js
deleted file mode 100644
index 70614cbbc..000000000
--- a/packages/kit/test/apps/basics/src/routes/load/cors/no-cors/+page.js
+++ /dev/null
@@ -1,10 +0,0 @@
-/** @type {import('./$types').PageLoad} */
-export async function load({ fetch, url }) {
-	const res = await fetch(`http://localhost:${url.searchParams.get('port')}`, {
-		mode: 'no-cors'
-	});
-
-	return {
-		text: await res.text()
-	};
-}
diff --git a/packages/kit/test/apps/basics/src/routes/load/cors/no-cors/+page.svelte b/packages/kit/test/apps/basics/src/routes/load/cors/no-cors/+page.svelte
deleted file mode 100644
index d689a601e..000000000
--- a/packages/kit/test/apps/basics/src/routes/load/cors/no-cors/+page.svelte
+++ /dev/null
@@ -1,6 +0,0 @@
-<script>
-	/** @type {import('./$types').PageData} */
-	export let data;
-</script>
-
-<h1>result: {@html data.text}</h1>
diff --git a/packages/kit/test/apps/basics/src/routes/load/cors/server-only/+page.server.js b/packages/kit/test/apps/basics/src/routes/load/cors/server-only/+page.server.js
deleted file mode 100644
index 7a412012c..000000000
--- a/packages/kit/test/apps/basics/src/routes/load/cors/server-only/+page.server.js
+++ /dev/null
@@ -1,8 +0,0 @@
-/** @type {import('./$types').PageServerLoad} */
-export async function load({ fetch, url }) {
-	const res = await fetch(`http://localhost:${url.searchParams.get('port')}`);
-
-	return {
-		text: await res.text()
-	};
-}
diff --git a/packages/kit/test/apps/basics/src/routes/load/cors/server-only/+page.svelte b/packages/kit/test/apps/basics/src/routes/load/cors/server-only/+page.svelte
deleted file mode 100644
index a5063216b..000000000
--- a/packages/kit/test/apps/basics/src/routes/load/cors/server-only/+page.svelte
+++ /dev/null
@@ -1,6 +0,0 @@
-<script>
-	/** @type {import('./$types').PageData} */
-	export let data;
-</script>
-
-<h1>{@html data.text}</h1>
diff --git a/packages/kit/test/apps/basics/src/routes/load/fetch-request-headers-invalid-access/+page.js b/packages/kit/test/apps/basics/src/routes/load/fetch-request-headers-invalid-access/+page.js
deleted file mode 100644
index eda2a2da8..000000000
--- a/packages/kit/test/apps/basics/src/routes/load/fetch-request-headers-invalid-access/+page.js
+++ /dev/null
@@ -1,8 +0,0 @@
-/** @type {import('./$types').PageLoad} */
-export async function load({ fetch }) {
-	let res = await fetch('./irrelevant', {
-		method: 'GET'
-	});
-
-	res.headers.get('content-type');
-}
diff --git a/packages/kit/test/apps/basics/src/routes/load/fetch-request-headers-invalid-access/+page.svelte b/packages/kit/test/apps/basics/src/routes/load/fetch-request-headers-invalid-access/+page.svelte
deleted file mode 100644
index e69de29bb..000000000
diff --git a/packages/kit/test/apps/basics/src/routes/load/invalidation/route/shared/+layout.js b/packages/kit/test/apps/basics/src/routes/load/invalidation/route/shared/+layout.js
index b37c81283..a144dc362 100644
--- a/packages/kit/test/apps/basics/src/routes/load/invalidation/route/shared/+layout.js
+++ b/packages/kit/test/apps/basics/src/routes/load/invalidation/route/shared/+layout.js
@@ -1,4 +1,4 @@
 /** @type {import('./$types').LayoutLoad} */
 export function load({ route }) {
-	return { route };
+	return { route, random: Math.random() };
 }
diff --git a/packages/kit/test/apps/basics/src/routes/load/invalidation/route/shared/+layout.svelte b/packages/kit/test/apps/basics/src/routes/load/invalidation/route/shared/+layout.svelte
index 2f2c178ac..3346ea43f 100644
--- a/packages/kit/test/apps/basics/src/routes/load/invalidation/route/shared/+layout.svelte
+++ b/packages/kit/test/apps/basics/src/routes/load/invalidation/route/shared/+layout.svelte
@@ -4,7 +4,11 @@
 </script>
 
 <h1>route.id: {data.route.id}</h1>
+<h2>random id: {data.random}</h2>
 <slot />
 
 <a href="/load/invalidation/route/shared/a">/load/invalidation/route/shared/a</a>
 <a href="/load/invalidation/route/shared/b">/load/invalidation/route/shared/b</a>
+
+<a href="/load/invalidation/route/shared/unchanged-x">/load/invalidation/route/shared/unchanged-x</a>
+<a href="/load/invalidation/route/shared/unchanged-y">/load/invalidation/route/shared/unchanged-y</a>
diff --git a/packages/kit/test/apps/basics/src/routes/load/invalidation/route/shared/[x]/+page.svelte b/packages/kit/test/apps/basics/src/routes/load/invalidation/route/shared/[x]/+page.svelte
new file mode 100644
index 000000000..ae59943f0
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/load/invalidation/route/shared/[x]/+page.svelte
@@ -0,0 +1,1 @@
+<p>[x]</p>
diff --git a/packages/kit/test/apps/basics/test/client.test.js b/packages/kit/test/apps/basics/test/client.test.js
index d6bf2f9b1..0a8cc4c66 100644
--- a/packages/kit/test/apps/basics/test/client.test.js
+++ b/packages/kit/test/apps/basics/test/client.test.js
@@ -1069,6 +1069,16 @@ test.describe('Invalidation', () => {
 		expect(await page.textContent('h1')).toBe('route.id: /load/invalidation/route/shared/b');
 	});
 
+	test('route.id does not rerun layout if unchanged', async ({ page, clicknav }) => {
+		await page.goto('/load/invalidation/route/shared/unchanged-x');
+		expect(await page.textContent('h1')).toBe('route.id: /load/invalidation/route/shared/[x]');
+		const id = await page.textContent('h2');
+
+		await clicknav('[href="/load/invalidation/route/shared/unchanged-y"]');
+		expect(await page.textContent('h1')).toBe('route.id: /load/invalidation/route/shared/[x]');
+		expect(await page.textContent('h2')).toBe(id);
+	});
+
 	test('$page.url can safely be mutated', async ({ page }) => {
 		await page.goto('/load/mutated-url?q=initial');
 		await expect(page.getByText('initial')).toBeVisible();
diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js
index 0a8476d16..459828418 100644
--- a/packages/kit/test/apps/basics/test/test.js
+++ b/packages/kit/test/apps/basics/test/test.js
@@ -868,17 +868,6 @@ test.describe('Load', () => {
 		}
 	});
 
-	test('errors when trying to access non-serialized request headers on the server', async ({
-		page,
-		read_errors
-	}) => {
-		await page.goto('/load/fetch-request-headers-invalid-access');
-
-		expect(read_errors(`/load/fetch-request-headers-invalid-access`).message).toContain(
-			'Failed to get response header "content-type" — it must be included by the `filterSerializedResponseHeaders` option'
-		);
-	});
-
 	test('exposes rawBody as a DataView to endpoints', async ({ page, clicknav }) => {
 		await page.goto('/load/raw-body');
 		await clicknav('[href="/load/raw-body/dataview"]');
@@ -952,37 +941,6 @@ test.describe('Load', () => {
 
 		expect(await page.textContent('h1')).toBe('true');
 	});
-
-	test('CORS errors are simulated server-side for shared load functions', async ({
-		page,
-		read_errors
-	}) => {
-		const { port, close } = await start_server(async (req, res) => {
-			res.end('hello');
-		});
-
-		await page.goto(`/load/cors?port=${port}`);
-		expect(await page.textContent('h1')).toBe('500');
-		expect(read_errors(`/load/cors`).message).toContain(
-			`CORS error: No 'Access-Control-Allow-Origin' header is present on the requested resource`
-		);
-
-		await page.goto(`/load/cors/no-cors?port=${port}`);
-		expect(await page.textContent('h1')).toBe('result: ');
-
-		await close();
-	});
-
-	test('CORS errors are skipped for server-only load functions', async ({ page }) => {
-		const { port, close } = await start_server(async (req, res) => {
-			res.end('hello');
-		});
-
-		await page.goto(`/load/cors/server-only?port=${port}`);
-		expect(await page.textContent('h1')).toBe('hello');
-
-		await close();
-	});
 });
 
 test.describe('Nested layouts', () => {
diff --git a/packages/kit/test/apps/options/svelte.config.js b/packages/kit/test/apps/options/svelte.config.js
index bae1cda49..b41b487d6 100644
--- a/packages/kit/test/apps/options/svelte.config.js
+++ b/packages/kit/test/apps/options/svelte.config.js
@@ -5,7 +5,8 @@ const config = {
 		embedded: true,
 		csp: {
 			directives: {
-				'script-src': ['self']
+				'script-src': ['self'],
+				'require-trusted-types-for': ['script']
 			}
 		},
 		files: {
diff --git a/packages/kit/test/apps/options/test/test.js b/packages/kit/test/apps/options/test/test.js
index 588a7cbe5..97fb3bc36 100644
--- a/packages/kit/test/apps/options/test/test.js
+++ b/packages/kit/test/apps/options/test/test.js
@@ -124,6 +124,13 @@ test.describe('CSP', () => {
 
 		await close();
 	});
+
+	test("quotes 'script'", async ({ page }) => {
+		const response = await page.goto(`/path-base`);
+		expect(response.headers()['content-security-policy']).toMatch(
+			/require-trusted-types-for 'script'/
+		);
+	});
 });
 
 test.describe('Custom extensions', () => {
diff --git a/packages/kit/test/prerendering/basics/src/routes/fetch-endpoint/also-not-buffered/+page.js b/packages/kit/test/prerendering/basics/src/routes/fetch-endpoint/also-not-buffered/+page.js
new file mode 100644
index 000000000..de4123c2a
--- /dev/null
+++ b/packages/kit/test/prerendering/basics/src/routes/fetch-endpoint/also-not-buffered/+page.js
@@ -0,0 +1,9 @@
+/** @type {import('@sveltejs/kit').Load} */
+export async function load({ fetch }) {
+	const url = '/fetch-endpoint/not-buffered.json';
+	const res = await fetch(url);
+
+	return {
+		headers: res.headers
+	};
+}
diff --git a/packages/kit/test/apps/basics/src/routes/load/cors/+page.svelte b/packages/kit/test/prerendering/basics/src/routes/fetch-endpoint/also-not-buffered/+page.svelte
similarity index 58%
rename from packages/kit/test/apps/basics/src/routes/load/cors/+page.svelte
rename to packages/kit/test/prerendering/basics/src/routes/fetch-endpoint/also-not-buffered/+page.svelte
index a5063216b..bf73bfc65 100644
--- a/packages/kit/test/apps/basics/src/routes/load/cors/+page.svelte
+++ b/packages/kit/test/prerendering/basics/src/routes/fetch-endpoint/also-not-buffered/+page.svelte
@@ -3,4 +3,4 @@
 	export let data;
 </script>
 
-<h1>{@html data.text}</h1>
+<h1>content-type: {data.headers.get('content-type')}</h1>
diff --git a/packages/kit/test/prerendering/basics/src/routes/fetch-endpoint/not-buffered.json/+server.js b/packages/kit/test/prerendering/basics/src/routes/fetch-endpoint/not-buffered.json/+server.js
index 82d3cb7b8..692b07ced 100644
--- a/packages/kit/test/prerendering/basics/src/routes/fetch-endpoint/not-buffered.json/+server.js
+++ b/packages/kit/test/prerendering/basics/src/routes/fetch-endpoint/not-buffered.json/+server.js
@@ -1,5 +1,10 @@
 import { json } from '@sveltejs/kit';
 
+let called = false;
+
 export async function GET() {
+	if (called) throw new Error('should only be called once');
+	called = true;
+
 	return json({ answer: 42 });
 }