Repository: sveltejs/kit (js)

Issue:
Attempt to error better when wrong fetch is used on an endpoint
### Describe the problem

I've now twice gotten burned by using the "wrong" fetch in my loaders. Running

fetch("/api/my-endpoint")

with the global fetch that's in scope will puke, since "/api/my-endpoint" has no way of resolving. Obviously you have to destructure the fetch that's passed to your loader. Simple enough, but it's an easy thing to miss when you're new (at SvelteKit), and it's *maddening* to debug.

### Describe the proposed solution

Would it be possible for the global fetch to detect when an (otherwise valid) api endpoint is being requested, and error with something clear about needing to use the provided fetch to hit endpoints?

I know this woulnd't be foolproof, since shared loaders are (I assume?) using the browser's provided fetch, which would be hard for you all to wrap, but on the server you're already providing your own fetch, so could this check be added to that (presumably for non-production builds)? 

### Alternatives considered

n/a

### Importance

nice to have

### Additional Information

_No response_

Pull request description:
error in dev mode if global fetch is used with relative URL
closes #7976

### 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.
- [ ] 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/big-cobras-sip.md b/.changeset/big-cobras-sip.md
new file mode 100644
index 000000000..4436c6b4e
--- /dev/null
+++ b/.changeset/big-cobras-sip.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+Disable illegal import detection when running unit tests
diff --git a/.changeset/breezy-walls-provide.md b/.changeset/breezy-walls-provide.md
new file mode 100644
index 000000000..28b778bfa
--- /dev/null
+++ b/.changeset/breezy-walls-provide.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+only prerender a given dependency once
diff --git a/.changeset/cuddly-cows-vanish.md b/.changeset/cuddly-cows-vanish.md
new file mode 100644
index 000000000..e4b8aa589
--- /dev/null
+++ b/.changeset/cuddly-cows-vanish.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+Error in dev mode if global `fetch` is used with relative URL
diff --git a/.changeset/dry-poets-crash.md b/.changeset/dry-poets-crash.md
new file mode 100644
index 000000000..58eeacb84
--- /dev/null
+++ b/.changeset/dry-poets-crash.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+`updated.check()` type changed to Promise<boolean>
diff --git a/.changeset/stupid-actors-look.md b/.changeset/stupid-actors-look.md
new file mode 100644
index 000000000..b31e235a4
--- /dev/null
+++ b/.changeset/stupid-actors-look.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+[fix] quote 'script' in CSP directives
diff --git a/.changeset/sweet-hairs-roll.md b/.changeset/sweet-hairs-roll.md
new file mode 100644
index 000000000..fb969c331
--- /dev/null
+++ b/.changeset/sweet-hairs-roll.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+fix route id comparison for load change detection
diff --git a/.changeset/wet-feet-lie.md b/.changeset/wet-feet-lie.md
new file mode 100644
index 000000000..6cee5b7d2
--- /dev/null
+++ b/.changeset/wet-feet-lie.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+[fix] don't add nonce attribute to `<link>` elements
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/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/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/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/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/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/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/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/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/prerendering/basics/src/routes/fetch-endpoint/also-not-buffered/+page.svelte b/packages/kit/test/prerendering/basics/src/routes/fetch-endpoint/also-not-buffered/+page.svelte
new file mode 100644
index 000000000..bf73bfc65
--- /dev/null
+++ b/packages/kit/test/prerendering/basics/src/routes/fetch-endpoint/also-not-buffered/+page.svelte
@@ -0,0 +1,6 @@
+<script>
+	/** @type {import('./$types').PageData} */
+	export let data;
+</script>
+
+<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 });
 }