Repository: sveltejs/kit
Pull request #8874 (2023-02-03 09:26:22)

fix: pin adapter versions in adapter-auto
MUST be released before #8740 is merged

`version` is used to pin the installed adapter version and should point to the latest version of the adapter that is compatible with adapter-auto's current peerDependency version of SvelteKit. Will prevent adapter-auto from accidentally installing adapter versions that are possibly incompatible with the user's SvelteKit version.

### Please don't delete this checklist! Before submitting the PR, please make sure you do the following:
- [ ] 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
- [ ] 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. Changesets that add features should be `minor` and those that fix bugs should be `patch`. Please prefix changeset messages with `feat:`, `fix:`, or `chore:`.



Patch:
diff --git a/.changeset/breezy-cycles-thank.md b/.changeset/breezy-cycles-thank.md
new file mode 100644
index 000000000..23d8356a9
--- /dev/null
+++ b/.changeset/breezy-cycles-thank.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+fix: ensure types of all form actions are accessible even if differing
diff --git a/.changeset/cyan-ladybugs-divide.md b/.changeset/cyan-ladybugs-divide.md
new file mode 100644
index 000000000..2cd5d409b
--- /dev/null
+++ b/.changeset/cyan-ladybugs-divide.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/adapter-auto': patch
+---
+
+fix: pin adapter versions
diff --git a/.changeset/fuzzy-kangaroos-relate.md b/.changeset/fuzzy-kangaroos-relate.md
new file mode 100644
index 000000000..0c27b68dd
--- /dev/null
+++ b/.changeset/fuzzy-kangaroos-relate.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+fix: correctly handle HttpErrors on the client side
diff --git a/.changeset/loud-pans-fly.md b/.changeset/loud-pans-fly.md
new file mode 100644
index 000000000..1e595deb3
--- /dev/null
+++ b/.changeset/loud-pans-fly.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+docs: discourage use of `goto` with external URLs
diff --git a/.changeset/lucky-tables-jam.md b/.changeset/lucky-tables-jam.md
new file mode 100644
index 000000000..2b334b36f
--- /dev/null
+++ b/.changeset/lucky-tables-jam.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+fix: prevent crawling empty urls (`<img src="">`)
diff --git a/.changeset/silent-icons-guess.md b/.changeset/silent-icons-guess.md
new file mode 100644
index 000000000..84f11ea69
--- /dev/null
+++ b/.changeset/silent-icons-guess.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+fix: correctly serialize request url when using load `fetch`
diff --git a/.changeset/slimy-donkeys-itch.md b/.changeset/slimy-donkeys-itch.md
new file mode 100644
index 000000000..01aa7b70c
--- /dev/null
+++ b/.changeset/slimy-donkeys-itch.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+fix: ensure endpoints can fetch endpoints on the same host but not part of the application
diff --git a/packages/adapter-auto/adapters.js b/packages/adapter-auto/adapters.js
index 12b77910e..bbd36575c 100644
--- a/packages/adapter-auto/adapters.js
+++ b/packages/adapter-auto/adapters.js
@@ -1,22 +1,28 @@
+// List of adapters to check for. `version` is used to pin the installed adapter version and should point
+// to the latest version of the adapter that is compatible with adapter-auto's current peerDependency version of SvelteKit.
 export const adapters = [
 	{
 		name: 'Vercel',
 		test: () => !!process.env.VERCEL,
-		module: '@sveltejs/adapter-vercel'
+		module: '@sveltejs/adapter-vercel',
+		version: '1'
 	},
 	{
 		name: 'Cloudflare Pages',
 		test: () => !!process.env.CF_PAGES,
-		module: '@sveltejs/adapter-cloudflare'
+		module: '@sveltejs/adapter-cloudflare',
+		version: '2'
 	},
 	{
 		name: 'Netlify',
 		test: () => !!process.env.NETLIFY,
-		module: '@sveltejs/adapter-netlify'
+		module: '@sveltejs/adapter-netlify',
+		version: '1'
 	},
 	{
 		name: 'Azure Static Web Apps',
 		test: () => process.env.GITHUB_ACTION_REPOSITORY === 'Azure/static-web-apps-deploy',
-		module: 'svelte-adapter-azure-swa'
+		module: 'svelte-adapter-azure-swa',
+		version: '0.13'
 	}
 ];
diff --git a/packages/adapter-auto/index.js b/packages/adapter-auto/index.js
index 6caacb8b7..9e73f5092 100644
--- a/packages/adapter-auto/index.js
+++ b/packages/adapter-auto/index.js
@@ -5,11 +5,11 @@ import { adapters } from './adapters.js';
 import { dirname, join } from 'path';
 import { existsSync } from 'fs';
 
-/** @type {Record<string, (name: string) => string>} */
+/** @type {Record<string, (name: string, version: string) => string>} */
 const commands = {
-	npm: (name) => `npm install -D ${name}`,
-	pnpm: (name) => `pnpm add -D ${name}`,
-	yarn: (name) => `yarn add -D ${name}`
+	npm: (name, version) => `npm install -D ${name}@${version}`,
+	pnpm: (name, version) => `pnpm add -D ${name}@${version}`,
+	yarn: (name, version) => `yarn add -D ${name}@${version}`
 };
 
 function detect_lockfile() {
@@ -64,7 +64,7 @@ async function get_adapter() {
 			error.message.startsWith(`Cannot find package '${match.module}'`)
 		) {
 			const package_manager = detect_package_manager();
-			const command = commands[package_manager](match.module);
+			const command = commands[package_manager](match.module, match.version);
 
 			try {
 				console.log(`Installing ${match.module}...`);
diff --git a/packages/kit/src/core/postbuild/crawl.js b/packages/kit/src/core/postbuild/crawl.js
index 255d6e443..fd1bd4c9e 100644
--- a/packages/kit/src/core/postbuild/crawl.js
+++ b/packages/kit/src/core/postbuild/crawl.js
@@ -165,7 +165,7 @@ export function crawl(html) {
 							} else if (name === 'rel') {
 								rel = value;
 							} else if (name === 'src') {
-								hrefs.push(value);
+								if (value) hrefs.push(value);
 							} else if (name === 'srcset') {
 								const candidates = [];
 								let insideURL = true;
@@ -183,7 +183,7 @@ export function crawl(html) {
 								candidates.push(value);
 								for (const candidate of candidates) {
 									const src = candidate.split(WHITESPACE)[0];
-									hrefs.push(src);
+									if (src) hrefs.push(src);
 								}
 							}
 						} else {
diff --git a/packages/kit/src/core/postbuild/fixtures/basic-src/input.html b/packages/kit/src/core/postbuild/fixtures/basic-src/input.html
index 82c55c8f2..52d4ee72b 100644
--- a/packages/kit/src/core/postbuild/fixtures/basic-src/input.html
+++ b/packages/kit/src/core/postbuild/fixtures/basic-src/input.html
@@ -3,5 +3,6 @@
 	<head></head>
 	<body>
 		<img alt="A potato" src="/potato.jpg" />
+		<img alt="empty" src="" />
 	</body>
 </html>
diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js
index 85f979e90..ef4cf180d 100644
--- a/packages/kit/src/runtime/client/client.js
+++ b/packages/kit/src/runtime/client/client.js
@@ -594,12 +594,17 @@ export function create_client({ target }) {
 					}
 
 					// we must fixup relative urls so they are resolved from the target page
-					const resolved = new URL(requested, url).href;
-					depends(resolved);
+					const resolved = new URL(requested, url);
+					depends(resolved.href);
+
+					// match ssr serialized data url, which is important to find cached responses
+					if (resolved.origin === url.origin) {
+						requested = resolved.href.slice(url.origin.length);
+					}
 
 					// prerendered pages may be served from any origin, so `initial_fetch` urls shouldn't be resolved
 					return started
-						? subsequent_fetch(requested, resolved, init)
+						? subsequent_fetch(requested, resolved.href, init)
 						: initial_fetch(requested, init);
 				},
 				setHeaders: () => {}, // noop
@@ -745,7 +750,7 @@ export function create_client({ target }) {
 				server_data = await load_data(url, invalid_server_nodes);
 			} catch (error) {
 				return load_root_error_page({
-					status: 500,
+					status: error instanceof HttpError ? error.status : 500,
 					error: await handle_error(error, { url, params, route: { id: route.id } }),
 					url,
 					route
@@ -1693,7 +1698,8 @@ async function load_data(url, invalid) {
 
 	if (!res.ok) {
 		// error message is a JSON-stringified string which devalue can't handle at the top level
-		throw new Error(data);
+		// turn it into a HttpError to not call handleError on the client again (was already handled on the server)
+		throw new HttpError(res.status, data);
 	}
 
 	// revive devalue-flattened data
diff --git a/packages/kit/src/runtime/server/endpoint.js b/packages/kit/src/runtime/server/endpoint.js
index fcce53a98..571080368 100644
--- a/packages/kit/src/runtime/server/endpoint.js
+++ b/packages/kit/src/runtime/server/endpoint.js
@@ -4,11 +4,12 @@ import { method_not_allowed } from './utils.js';
 
 /**
  * @param {import('types').RequestEvent} event
+ * @param {import('types').SSRRoute} route
  * @param {import('types').SSREndpoint} mod
  * @param {import('types').SSRState} state
  * @returns {Promise<Response>}
  */
-export async function render_endpoint(event, mod, state) {
+export async function render_endpoint(event, route, mod, state) {
 	const method = /** @type {import('types').HttpMethod} */ (event.request.method);
 
 	let handler = mod[method];
@@ -38,6 +39,8 @@ export async function render_endpoint(event, mod, state) {
 		}
 	}
 
+	state.initiator = route;
+
 	try {
 		const response = await handler(
 			/** @type {import('types').RequestEvent<Record<string, any>>} */ (event)
diff --git a/packages/kit/src/runtime/server/respond.js b/packages/kit/src/runtime/server/respond.js
index 7b2283630..658b9b25d 100644
--- a/packages/kit/src/runtime/server/respond.js
+++ b/packages/kit/src/runtime/server/respond.js
@@ -358,7 +358,7 @@ export async function respond(request, options, manifest, state) {
 						trailing_slash ?? 'never'
 					);
 				} else if (route.endpoint && (!route.page || is_endpoint_request(event))) {
-					response = await render_endpoint(event, await route.endpoint(), state);
+					response = await render_endpoint(event, route, await route.endpoint(), state);
 				} else if (route.page) {
 					response = await render_page(
 						event,
diff --git a/packages/kit/src/runtime/server/utils.js b/packages/kit/src/runtime/server/utils.js
index bb9bb65df..ed8c4803d 100644
--- a/packages/kit/src/runtime/server/utils.js
+++ b/packages/kit/src/runtime/server/utils.js
@@ -2,7 +2,6 @@ import * as devalue from 'devalue';
 import { json, text } from '../../exports/index.js';
 import { coalesce_to_error } from '../../utils/error.js';
 import { negotiate } from '../../utils/http.js';
-import { has_data_suffix } from '../../utils/url.js';
 import { HttpError } from '../control.js';
 import { fix_stack_trace } from '../shared.js';
 
@@ -81,7 +80,7 @@ export async function handle_fatal_error(event, options, error) {
 		'text/html'
 	]);
 
-	if (has_data_suffix(new URL(event.request.url).pathname) || type === 'application/json') {
+	if (event.isDataRequest || type === 'application/json') {
 		return json(body, {
 			status
 		});
diff --git a/packages/kit/types/ambient.d.ts b/packages/kit/types/ambient.d.ts
index 79801c8a3..606475b4c 100644
--- a/packages/kit/types/ambient.d.ts
+++ b/packages/kit/types/ambient.d.ts
@@ -170,6 +170,7 @@ declare module '$app/navigation' {
 	export function disableScrollHandling(): void;
 	/**
 	 * Returns a Promise that resolves when SvelteKit navigates (or fails to navigate, in which case the promise rejects) to the specified `url`.
+	 * For external URLs, use `window.location = url` instead of calling `goto(url)`.
 	 *
 	 * @param url Where to navigate to. Note that if you've set [`config.kit.paths.base`](https://kit.svelte.dev/docs/configuration#paths) and the URL is root-relative, you need to prepend the base path if you want to navigate within the app.
 	 * @param opts Options related to the navigation
diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts
index e7440202a..ccc9a9d5e 100644
--- a/packages/kit/types/index.d.ts
+++ b/packages/kit/types/index.d.ts
@@ -50,9 +50,11 @@ export type AwaitedProperties<input extends Record<string, any> | void> =
 		? OptionalUnion<AwaitedPropertiesUnion<input>>
 		: AwaitedPropertiesUnion<input>;
 
-export type AwaitedActions<T extends Record<string, (...args: any) => any>> = {
-	[Key in keyof T]: OptionalUnion<UnpackValidationError<Awaited<ReturnType<T[Key]>>>>;
-}[keyof T];
+export type AwaitedActions<T extends Record<string, (...args: any) => any>> = OptionalUnion<
+	{
+		[Key in keyof T]: UnpackValidationError<Awaited<ReturnType<T[Key]>>>;
+	}[keyof T]
+>;
 
 // Takes a union type and returns a union type where each type also has all properties
 // of all possible types (typed as undefined), making accessing them more ergonomic
diff --git a/renovate.json b/renovate.json
index df2d62cbd..92b4007e7 100644
--- a/renovate.json
+++ b/renovate.json
@@ -3,7 +3,8 @@
 		"github>whitesource/merge-confidence:beta",
 		"config:base",
 		":preserveSemverRanges",
-		"group:allNonMajor"
+		"group:allNonMajor",
+		":semanticCommitTypeAll(chore)"
 	],
 	"pin": {
 		"enabled": false
diff --git a/sites/kit.svelte.dev/src/lib/search/SearchResultList.svelte b/sites/kit.svelte.dev/src/lib/search/SearchResultList.svelte
index 41fd1900a..2fd143429 100644
--- a/sites/kit.svelte.dev/src/lib/search/SearchResultList.svelte
+++ b/sites/kit.svelte.dev/src/lib/search/SearchResultList.svelte
@@ -9,14 +9,19 @@
 
 	const dispatch = createEventDispatcher();
 
+	/** @param {string} text */
 	function escape(text) {
 		return text.replace(/</g, '&lt;').replace(/>/g, '&gt;');
 	}
 
+	/**
+	 * @param {string} content
+	 * @param {string} query
+	 */
 	function excerpt(content, query) {
 		const index = content.toLowerCase().indexOf(query.toLowerCase());
 		if (index === -1) {
-			return content.slice(0, 100);
+			return escape(content.slice(0, 100));
 		}
 
 		const prefix = index > 20 ? `…${content.slice(index - 15, index)}` : content.slice(0, index);
@@ -34,7 +39,7 @@
 </script>
 
 <ul>
-	{#each results as result, i}
+	{#each results as result (result.href)}
 		<li>
 			<a
 				data-sveltekit-preload-data


Test patch:
diff --git a/packages/kit/test/apps/basics/src/routes/load/serialization/+page.js b/packages/kit/test/apps/basics/src/routes/load/serialization/+page.js
index fc0d82c6d..72f7c4d96 100644
--- a/packages/kit/test/apps/basics/src/routes/load/serialization/+page.js
+++ b/packages/kit/test/apps/basics/src/routes/load/serialization/+page.js
@@ -1,8 +1,8 @@
 /** @type {import('./$types').PageLoad} */
-export async function load({ fetch, data }) {
+export async function load({ fetch, data, url }) {
 	const { a } = data;
 
-	const res = await fetch('/load/serialization/fetched-from-shared.json');
+	const res = await fetch(new URL('/load/serialization/fetched-from-shared.json', url.origin));
 	const { b } = await res.json();
 
 	return { a, b, c: a + b };
diff --git a/packages/kit/test/apps/basics/src/routes/prerendering/prerendered-endpoint/proxy/+server.js b/packages/kit/test/apps/basics/src/routes/prerendering/prerendered-endpoint/proxy/+server.js
new file mode 100644
index 000000000..59396b64d
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/prerendering/prerendered-endpoint/proxy/+server.js
@@ -0,0 +1,3 @@
+export async function GET({ fetch }) {
+	return await fetch('/prerendering/prerendered-endpoint/api');
+}
diff --git a/packages/kit/test/apps/basics/test/server.test.js b/packages/kit/test/apps/basics/test/server.test.js
index a375d7621..cad3f9f88 100644
--- a/packages/kit/test/apps/basics/test/server.test.js
+++ b/packages/kit/test/apps/basics/test/server.test.js
@@ -97,6 +97,17 @@ test.describe('Endpoints', () => {
 		expect(headers.head).toEqual(headers.get);
 	});
 
+	test('Prerendered +server.js called from a non-prerendered +server.js works', async ({
+		baseURL
+	}) => {
+		const res = await fetch(`${baseURL}/prerendering/prerendered-endpoint/proxy`);
+
+		expect(res.status).toBe(200);
+		expect(await res.json()).toStrictEqual({
+			message: 'Im prerendered and called from a non-prerendered +page.server.js'
+		});
+	});
+
 	// TODO all the remaining tests in this section are really only testing
 	// setResponse, since we're not otherwise changing anything on the response.
 	// might be worth making these unit tests instead
diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js
index e67b827bc..cd95a6043 100644
--- a/packages/kit/test/apps/basics/test/test.js
+++ b/packages/kit/test/apps/basics/test/test.js
@@ -211,7 +211,9 @@ test.describe('Load', () => {
 
 		if (!javaScriptEnabled) {
 			// by the time JS has run, hydration will have nuked these scripts
-			const script_contents = await page.innerHTML('script[data-sveltekit-fetched]');
+			const script_contents = await page.innerHTML(
+				'script[data-sveltekit-fetched][data-url="/load/serialization/fetched-from-shared.json"]'
+			);
 
 			const payload = '{"status":200,"statusText":"","headers":{},"body":"{\\"b\\":2}"}';
 
diff --git a/packages/kit/test/types/actions.test.ts b/packages/kit/test/types/actions.test.ts
index 35a32f201..8ca089d18 100644
--- a/packages/kit/test/types/actions.test.ts
+++ b/packages/kit/test/types/actions.test.ts
@@ -11,3 +11,15 @@ form.message = '';
 form.success = true;
 // @ts-expect-error - cannot both be present at the same time
 form = { message: '', success: true };
+
+// Test: Actions with different return types are transformed into a union that has all types accessible
+type Actions2 = {
+	foo: () => Promise<{ message: string }>;
+	bar: () => Promise<{ success: boolean }>;
+};
+
+let form2: Kit.AwaitedActions<Actions2> = null as any;
+form2.message = '';
+form2.success = true;
+// @ts-expect-error - cannot both be present at the same time
+form2 = { message: '', success: true };