Repository: sveltejs/kit
Pull request #3456 (2022-01-20 17:37:31)

escape redirect locations when prerendering, dont encode them
Alternative to #3339. It also — finally — adds some proper tests for prerendering.

### 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
- [ ] Run the tests with `pnpm test` and lint the project with `pnpm lint` and `pnpm check`

### Changesets
- [ ] If your PR makes a change that should be noted in one or more packages' changelogs, generate a changeset by running `pnpx changeset` and following the prompts. All changesets should be `patch` until SvelteKit 1.0


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

Latest commit: 904f8f370b31bd058c3b0c039443155f8939601a

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/sveltejs/kit/new/prerendered-redirects?filename=.changeset/moody-toes-brush.md&value=---%0A%22%40sveltejs%2Fkit%22%3A%20patch%0A---%0A%0Aescape%20redirect%20locations%20when%20prerendering%2C%20dont%20encode%20them%0A)


netlify[bot]: 👷 Deploy Preview for *kit-demo* processing.


🔨 Explore the source changes: 904f8f370b31bd058c3b0c039443155f8939601a

🔍 Inspect the deploy log: [https://app.netlify.com/sites/kit-demo/deploys/61e99dddbfc66d0009872268](https://app.netlify.com/sites/kit-demo/deploys/61e99dddbfc66d0009872268)


Patch:
diff --git a/.changeset/breezy-games-count.md b/.changeset/breezy-games-count.md
new file mode 100644
index 000000000..2124b7a65
--- /dev/null
+++ b/.changeset/breezy-games-count.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+Escape prerendered redirect locations, instead of encoding them
diff --git a/.changeset/cool-bananas-share.md b/.changeset/cool-bananas-share.md
new file mode 100644
index 000000000..08fc64971
--- /dev/null
+++ b/.changeset/cool-bananas-share.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/adapter-netlify': patch
+---
+
+Avoid setting the body of the request when the request method is GET or HEAD
diff --git a/.changeset/lemon-camels-brush.md b/.changeset/lemon-camels-brush.md
new file mode 100644
index 000000000..1146afc55
--- /dev/null
+++ b/.changeset/lemon-camels-brush.md
@@ -0,0 +1,5 @@
+---
+'create-svelte': patch
+---
+
+Respect Ctrl-C when running create-svelte
diff --git a/.changeset/popular-hounds-love.md b/.changeset/popular-hounds-love.md
new file mode 100644
index 000000000..ea2aeea26
--- /dev/null
+++ b/.changeset/popular-hounds-love.md
@@ -0,0 +1,5 @@
+---
+'create-svelte': patch
+---
+
+Make project name an explicit option'
diff --git a/.changeset/pre.json b/.changeset/pre.json
index 73d338a52..97118a582 100644
--- a/.changeset/pre.json
+++ b/.changeset/pre.json
@@ -95,6 +95,7 @@
     "cold-panthers-learn",
     "cold-pants-unite",
     "cold-tables-shake",
+    "cool-bananas-share",
     "cool-ducks-cheat",
     "cool-hounds-divide",
     "cool-spoons-dress",
@@ -120,6 +121,7 @@
     "dull-schools-brake",
     "dull-tigers-type",
     "early-chairs-protect",
+    "early-clocks-sneeze",
     "early-lemons-collect",
     "early-news-taste",
     "early-snakes-peel",
@@ -271,6 +273,7 @@
     "lazy-mice-smile",
     "lazy-spies-wash",
     "lazy-ways-swim",
+    "lemon-camels-brush",
     "lemon-lies-attack",
     "lemon-mails-hope",
     "lemon-taxis-juggle",
@@ -385,6 +388,7 @@
     "poor-terms-explain",
     "poor-years-hear",
     "popular-ads-rhyme",
+    "popular-hounds-love",
     "popular-masks-cheat",
     "popular-schools-guess",
     "pretty-beans-wash",
@@ -629,6 +633,7 @@
     "two-tigers-cry",
     "unlucky-dodos-greet",
     "unlucky-mugs-dress",
+    "unlucky-otters-tickle",
     "unlucky-parrots-vanish",
     "unlucky-phones-promise",
     "unlucky-planets-battle",
@@ -680,6 +685,7 @@
     "young-apricots-dance",
     "young-beds-bathe",
     "young-cougars-poke",
+    "young-ears-rhyme",
     "young-hairs-cough",
     "young-students-chew",
     "young-swans-burn",
diff --git a/.changeset/unlucky-otters-tickle.md b/.changeset/unlucky-otters-tickle.md
new file mode 100644
index 000000000..2731924d5
--- /dev/null
+++ b/.changeset/unlucky-otters-tickle.md
@@ -0,0 +1,5 @@
+---
+'create-svelte': patch
+---
+
+Prompt for directory when running create-svelte without argument
diff --git a/.changeset/young-ears-rhyme.md b/.changeset/young-ears-rhyme.md
new file mode 100644
index 000000000..3b4685ea4
--- /dev/null
+++ b/.changeset/young-ears-rhyme.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+Type compilerOptions as CompileOptions instead of any
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 03bcc0255..d9f29ca56 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -20,10 +20,10 @@ jobs:
       - uses: pnpm/action-setup@v2.0.1
         with:
           version: 6.23.2
-      - name: Setup Node.js 12.x
+      - name: Setup Node.js
         uses: actions/setup-node@v2
         with:
-          node-version: 12.x
+          node-version: 16.x
           cache: pnpm
 
       - run: pnpm install --frozen-lockfile
@@ -38,6 +38,7 @@ jobs:
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
           NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
+          UPDATE_TEMPLATE_SSH_KEY: ${{ secrets.UPDATE_TEMPLATE_SSH_KEY }}
 
       # TODO alert discord
       # - name: Send a Slack notification if a publish happens
diff --git a/.github/workflows/update_template.yml b/.github/workflows/update_template.yml
new file mode 100644
index 000000000..dcca025e1
--- /dev/null
+++ b/.github/workflows/update_template.yml
@@ -0,0 +1,59 @@
+name: Update Template
+
+on:
+  workflow_run:
+    workflows: [Release]
+    types:
+      - completed
+  workflow_dispatch:
+
+jobs:
+  release:
+    # prevents this action from running on forks
+    if: github.repository == 'sveltejs/kit'
+    name: build
+    runs-on: ubuntu-latest
+
+    steps:
+      - name: Check out kit repository
+        uses: actions/checkout@v2
+        with:
+          path: kit
+
+      - name: Check out template repository
+        uses: actions/checkout@v2
+        with:
+          ssh-key: "${{ secrets.UPDATE_TEMPLATE_SSH_KEY }}"
+          repository: sveltejs/kit-template-default
+          path: template
+
+      - uses: pnpm/action-setup@v2.0.1
+        with:
+          version: 6.23.2
+
+      - name: Setup Node.js
+        uses: actions/setup-node@v2
+        with:
+          node-version: 16.x
+          cache: pnpm
+          cache-dependency-path: ${{ github.workspace }}/kit/pnpm-lock.yaml
+
+      - name: Build kit
+        working-directory: ${{ github.workspace }}/kit
+        run: |
+          pnpm install --frozen-lockfile
+          pnpm build
+
+      - name: Write template
+        working-directory: ${{ github.workspace }}/kit/packages/create-svelte
+        run: node ./scripts/update-template-repo-contents.js ${{ github.workspace }}/template
+
+      - name: Publish
+        working-directory: ${{ github.workspace }}/template
+        run: |
+          touch "generated-$(date +%s)"
+          git config user.email "noreply@svelte.dev"
+          git config user.name "[bot]"
+          git add -A
+          git commit -m "version $npm_package_version"
+          git push -u origin main -f
diff --git a/documentation/docs/01-routing.md b/documentation/docs/01-routing.md
index 964a99302..57b3f8703 100644
--- a/documentation/docs/01-routing.md
+++ b/documentation/docs/01-routing.md
@@ -51,10 +51,6 @@ Endpoints are modules written in `.js` (or `.ts`) files that export functions co
 // Declaration types for Endpoints
 // * declarations that are not exported are for internal use
 
-// type of string[] is only for set-cookie
-// everything else must be a type of string
-type ResponseHeaders = Record<string, string | string[]>;
-
 export interface RequestEvent<Locals = Record<string, any>> {
 	request: Request;
 	url: URL;
@@ -62,26 +58,19 @@ export interface RequestEvent<Locals = Record<string, any>> {
 	locals: Locals;
 }
 
-type Body = JSONResponse | Uint8Array | string | ReadableStream | stream.Readable;
-
+type Body = JSONString | Uint8Array | ReadableStream | stream.Readable;
 export interface EndpointOutput {
 	status?: number;
 	headers?: HeadersInit;
 	body?: Body;
 }
 
-export type MaybePromise<T> = T | Promise<T>;
-
-export interface Fallthrough {
-	fallthrough?: true;
+type MaybePromise<T> = T | Promise<T>;
+interface Fallthrough {
+	fallthrough: true;
 }
-
-export interface RequestHandler<
-	Locals = Record<string, any>,
-	Input = unknown,
-	Output extends DefaultBody = DefaultBody
-> {
-	(request: Request<Locals, Input>): MaybePromise<Fallthrough | EndpointOutput<Output>>;
+export interface RequestHandler<Locals = Record<string, any>> {
+	(event: RequestEvent<Locals>): MaybePromise<Either<Response | EndpointOutput, Fallthrough>>;
 }
 ```
 
@@ -126,7 +115,7 @@ If the returned `body` is an object, and no `content-type` header is returned, i
 For endpoints that handle other HTTP methods, like POST, export the corresponding function:
 
 ```js
-export function post(request) {...}
+export function post(event) {...}
 ```
 
 Since `delete` is a reserved word in JavaScript, DELETE requests are handled with a `del` function.
diff --git a/documentation/docs/03-loading.md b/documentation/docs/03-loading.md
index ae3a54c60..51d915714 100644
--- a/documentation/docs/03-loading.md
+++ b/documentation/docs/03-loading.md
@@ -8,10 +8,6 @@ A component that defines a page or a layout can export a `load` function that ru
 // Declaration types for Loading
 // * declarations that are not exported are for internal use
 
-export interface Fallthrough {
-	fallthrough?: true;
-}
-
 export interface LoadInput<
 	PageParams extends Record<string, string> = Record<string, string>,
 	Stuff extends Record<string, any> = Record<string, any>,
@@ -24,7 +20,7 @@ export interface LoadInput<
 	stuff: Stuff;
 }
 
-export type LoadOutput<
+export interface LoadOutput<
 	Props extends Record<string, any> = Record<string, any>,
 	Stuff extends Record<string, any> = Record<string, any>
 > {
@@ -34,7 +30,42 @@ export type LoadOutput<
 	props?: Props;
 	stuff?: Stuff;
 	maxage?: number;
-} | Fallthrough
+}
+
+interface LoadInputExtends {
+	stuff?: Record<string, any>;
+	pageParams?: Record<string, string>;
+	session?: any;
+}
+interface LoadOutputExtends {
+	stuff?: Record<string, any>;
+	props?: Record<string, any>;
+}
+
+type MaybePromise<T> = T | Promise<T>;
+interface Fallthrough {
+	fallthrough: true;
+}
+export interface Load<
+	Input extends LoadInputExtends = Required<LoadInputExtends>,
+	Output extends LoadOutputExtends = Required<LoadOutputExtends>
+> {
+	(
+		input: LoadInput<
+			InferValue<Input, 'pageParams', Record<string, string>>,
+			InferValue<Input, 'stuff', Record<string, any>>,
+			InferValue<Input, 'session', any>
+		>
+	): MaybePromise<
+		Either<
+			Fallthrough,
+			LoadOutput<
+				InferValue<Output, 'props', Record<string, any>>,
+				InferValue<Output, 'stuff', Record<string, any>>
+			>
+		>
+	>;
+}
 ```
 
 Our example blog page might contain a `load` function like the following:
@@ -143,7 +174,7 @@ If something goes wrong during `load`, return an `Error` object or a `string` de
 
 If the page should redirect (because the page is deprecated, or the user needs to be logged in, or whatever else) return a `string` containing the location to which they should be redirected alongside a `3xx` status code.
 
-The `redirect` string should be a [properly encoded](https://en.wikipedia.org/wiki/Percent-encoding) URI.  Both absolute and relative URIs are acceptable.
+The `redirect` string should be a [properly encoded](https://developer.mozilla.org/en-US/docs/Glossary/percent-encoding) URI. Both absolute and relative URIs are acceptable.
 
 #### maxage
 
diff --git a/documentation/docs/04-hooks.md b/documentation/docs/04-hooks.md
index 7d5b7a66d..5a5c8930a 100644
--- a/documentation/docs/04-hooks.md
+++ b/documentation/docs/04-hooks.md
@@ -125,7 +125,7 @@ export function getSession(event) {
 					email: event.locals.user.email,
 					avatar: event.locals.user.avatar
 				}
-		  }
+			}
 		: {};
 }
 ```
diff --git a/documentation/docs/11-page-options.md b/documentation/docs/11-page-options.md
index c409284b4..cb70b5c17 100644
--- a/documentation/docs/11-page-options.md
+++ b/documentation/docs/11-page-options.md
@@ -58,7 +58,7 @@ The basic rule is this: for a page to be prerenderable, any two users hitting it
 
 Note that you can still prerender pages that load data based on the page's parameters, like our `src/routes/blog/[slug].svelte` example from earlier. The prerenderer will intercept requests made inside `load`, so the data served from `src/routes/blog/[slug].json.js` will also be captured.
 
-Accessing [`page.query`](#loading-input-page) during prerendering is forbidden. If you need to use it, ensure you are only doing so in the browser (for example in `onMount`).
+Accessing [`url.searchParams`](#loading-input-url) during prerendering is forbidden. If you need to use it, ensure you are only doing so in the browser (for example in `onMount`).
 
 #### Route conflicts
 
diff --git a/documentation/faq/20-hmr.md b/documentation/faq/20-hmr.md
index 276104161..60f46dfd1 100644
--- a/documentation/faq/20-hmr.md
+++ b/documentation/faq/20-hmr.md
@@ -2,4 +2,4 @@
 question: How do I use HMR with SvelteKit?
 ---
 
-SvelteKit has HMR enabled by default powered by [svelte-hmr](https://github.com/rixo/svelte-hmr). If you saw [Rich's presentation at the 2020 Svelte Summit](https://svelte.dev/blog/whats-the-deal-with-sveltekit), you may have seen a more powerful-looking version of HMR presented. This demo had `svelte-hmr`'s `preserveLocalState` flag on. This flag is now off by default because it may lead to unexpected behaviour and edge cases. But don't worry, you are still getting HMR with SvelteKit! If you'd like to preserve local state you can use the `@hmr:keep` or `@hmr:keep-all` directives as documented on the [svelte-hmr](https://github.com/rixo/svelte-hmr) page.
+SvelteKit has HMR enabled by default powered by [svelte-hmr](https://github.com/sveltejs/svelte-hmr). If you saw [Rich's presentation at the 2020 Svelte Summit](https://svelte.dev/blog/whats-the-deal-with-sveltekit), you may have seen a more powerful-looking version of HMR presented. This demo had `svelte-hmr`'s `preserveLocalState` flag on. This flag is now off by default because it may lead to unexpected behaviour and edge cases. But don't worry, you are still getting HMR with SvelteKit! If you'd like to preserve local state you can use the `@hmr:keep` or `@hmr:keep-all` directives as documented on the [svelte-hmr](https://github.com/sveltejs/svelte-hmr) page.
diff --git a/documentation/migrating/99-integrations.md b/documentation/migrating/99-integrations.md
index a45db6149..072dae170 100644
--- a/documentation/migrating/99-integrations.md
+++ b/documentation/migrating/99-integrations.md
@@ -31,8 +31,8 @@ const minification_options = {
 	sortClassName: true
 };
 
-export async function handle({ request, resolve }) {
-  const response = await resolve(request);
+export async function handle({ event, resolve }) {
+  const response = await resolve(event);
 
   if (prerendering && response.headers['content-type'] === 'text/html') {
     response.body = minify(response.body, minification_options);
diff --git a/packages/adapter-auto/CHANGELOG.md b/packages/adapter-auto/CHANGELOG.md
index 8e3202379..7e063763c 100644
--- a/packages/adapter-auto/CHANGELOG.md
+++ b/packages/adapter-auto/CHANGELOG.md
@@ -1,5 +1,14 @@
 # @sveltejs/adapter-auto
 
+## 1.0.0-next.14
+
+### Patch Changes
+
+- Updated dependencies [[`4d58e10c`](https://github.com/sveltejs/kit/commit/4d58e10c36cab47d40c161a87c82512bebeb6b90)]:
+  - @sveltejs/adapter-netlify@1.0.0-next.42
+  - @sveltejs/adapter-cloudflare@1.0.0-next.8
+  - @sveltejs/adapter-vercel@1.0.0-next.39
+
 ## 1.0.0-next.13
 
 ### Patch Changes
diff --git a/packages/adapter-auto/package.json b/packages/adapter-auto/package.json
index a0ba9c39c..41f5cb82f 100644
--- a/packages/adapter-auto/package.json
+++ b/packages/adapter-auto/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "@sveltejs/adapter-auto",
-	"version": "1.0.0-next.13",
+	"version": "1.0.0-next.14",
 	"repository": {
 		"type": "git",
 		"url": "https://github.com/sveltejs/kit",
diff --git a/packages/adapter-cloudflare-workers/files/entry.d.ts b/packages/adapter-cloudflare-workers/files/entry.d.ts
index c0ee559ee..ea54b4bd2 100644
--- a/packages/adapter-cloudflare-workers/files/entry.d.ts
+++ b/packages/adapter-cloudflare-workers/files/entry.d.ts
@@ -1,6 +1,5 @@
 declare module 'APP' {
-	import { App } from '@sveltejs/kit';
-	export { App };
+	export { App } from '@sveltejs/kit';
 }
 
 declare module 'MANIFEST' {
diff --git a/packages/adapter-cloudflare/files/worker.d.ts b/packages/adapter-cloudflare/files/worker.d.ts
index c0ee559ee..ea54b4bd2 100644
--- a/packages/adapter-cloudflare/files/worker.d.ts
+++ b/packages/adapter-cloudflare/files/worker.d.ts
@@ -1,6 +1,5 @@
 declare module 'APP' {
-	import { App } from '@sveltejs/kit';
-	export { App };
+	export { App } from '@sveltejs/kit';
 }
 
 declare module 'MANIFEST' {
diff --git a/packages/adapter-netlify/CHANGELOG.md b/packages/adapter-netlify/CHANGELOG.md
index ceccf7ef7..a3c7f234d 100644
--- a/packages/adapter-netlify/CHANGELOG.md
+++ b/packages/adapter-netlify/CHANGELOG.md
@@ -1,5 +1,11 @@
 # @sveltejs/adapter-netlify
 
+## 1.0.0-next.42
+
+### Patch Changes
+
+- Avoid setting the body of the request when the request method is GET or HEAD ([#3459](https://github.com/sveltejs/kit/pull/3459))
+
 ## 1.0.0-next.41
 
 ### Patch Changes
diff --git a/packages/adapter-netlify/package.json b/packages/adapter-netlify/package.json
index a7a8567f4..72b58b6e6 100644
--- a/packages/adapter-netlify/package.json
+++ b/packages/adapter-netlify/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "@sveltejs/adapter-netlify",
-	"version": "1.0.0-next.41",
+	"version": "1.0.0-next.42",
 	"repository": {
 		"type": "git",
 		"url": "https://github.com/sveltejs/kit",
@@ -35,7 +35,7 @@
 		"tiny-glob": "^0.2.9"
 	},
 	"devDependencies": {
-		"@netlify/functions": "^0.10.0",
+		"@netlify/functions": "^0.11.0",
 		"@rollup/plugin-commonjs": "^21.0.0",
 		"@rollup/plugin-json": "^4.1.0",
 		"@rollup/plugin-node-resolve": "^13.0.5",
diff --git a/packages/adapter-netlify/src/handler.d.ts b/packages/adapter-netlify/src/handler.d.ts
index a34e56031..556c999c9 100644
--- a/packages/adapter-netlify/src/handler.d.ts
+++ b/packages/adapter-netlify/src/handler.d.ts
@@ -1,4 +1,3 @@
 declare module 'APP' {
-	import { App } from '@sveltejs/kit';
-	export { App };
+	export { App } from '@sveltejs/kit';
 }
diff --git a/packages/adapter-netlify/src/handler.js b/packages/adapter-netlify/src/handler.js
index 0835a3f6b..e75fdd97a 100644
--- a/packages/adapter-netlify/src/handler.js
+++ b/packages/adapter-netlify/src/handler.js
@@ -9,18 +9,7 @@ export function init(manifest) {
 	const app = new App(manifest);
 
 	return async (event) => {
-		const { httpMethod, headers, rawUrl, body, isBase64Encoded } = event;
-
-		const encoding = isBase64Encoded ? 'base64' : 'utf-8';
-		const rawBody = typeof body === 'string' ? Buffer.from(body, encoding) : body;
-
-		const rendered = await app.render(
-			new Request(rawUrl, {
-				method: httpMethod,
-				headers: new Headers(headers),
-				body: rawBody
-			})
-		);
+		const rendered = await app.render(to_request(event));
 
 		const partial_response = {
 			statusCode: rendered.status,
@@ -46,6 +35,27 @@ export function init(manifest) {
 	};
 }
 
+/**
+ * @param {import('@netlify/functions').HandlerEvent} event
+ * @returns {Request}
+ */
+function to_request(event) {
+	const { httpMethod, headers, rawUrl, body, isBase64Encoded } = event;
+
+	/** @type {RequestInit} */
+	const init = {
+		method: httpMethod,
+		headers: new Headers(headers)
+	};
+
+	if (httpMethod !== 'GET' && httpMethod !== 'HEAD') {
+		const encoding = isBase64Encoded ? 'base64' : 'utf-8';
+		init.body = typeof body === 'string' ? Buffer.from(body, encoding) : body;
+	}
+
+	return new Request(rawUrl, init);
+}
+
 /**
  * Splits headers into two categories: single value and multi value
  * @param {Headers} headers
diff --git a/packages/adapter-node/src/ambient.d.ts b/packages/adapter-node/src/ambient.d.ts
index 279516ccb..42c4f9f12 100644
--- a/packages/adapter-node/src/ambient.d.ts
+++ b/packages/adapter-node/src/ambient.d.ts
@@ -1,6 +1,5 @@
 declare module 'APP' {
-	import { App } from '@sveltejs/kit';
-	export { App };
+	export { App } from '@sveltejs/kit';
 }
 
 declare module 'MANIFEST' {
diff --git a/packages/adapter-vercel/files/ambient.d.ts b/packages/adapter-vercel/files/ambient.d.ts
index 279516ccb..42c4f9f12 100644
--- a/packages/adapter-vercel/files/ambient.d.ts
+++ b/packages/adapter-vercel/files/ambient.d.ts
@@ -1,6 +1,5 @@
 declare module 'APP' {
-	import { App } from '@sveltejs/kit';
-	export { App };
+	export { App } from '@sveltejs/kit';
 }
 
 declare module 'MANIFEST' {
diff --git a/packages/create-svelte/CHANGELOG.md b/packages/create-svelte/CHANGELOG.md
index 28cc7ba4a..57eb9f82d 100644
--- a/packages/create-svelte/CHANGELOG.md
+++ b/packages/create-svelte/CHANGELOG.md
@@ -1,5 +1,15 @@
 # create-svelte
 
+## 2.0.0-next.99
+
+### Patch Changes
+
+- Respect Ctrl-C when running create-svelte ([#3472](https://github.com/sveltejs/kit/pull/3472))
+
+* Make project name an explicit option' ([#3472](https://github.com/sveltejs/kit/pull/3472))
+
+- Prompt for directory when running create-svelte without argument ([#3472](https://github.com/sveltejs/kit/pull/3472))
+
 ## 2.0.0-next.98
 
 ### Patch Changes
diff --git a/packages/create-svelte/bin.js b/packages/create-svelte/bin.js
index 4ffb87a33..6eadd82e9 100755
--- a/packages/create-svelte/bin.js
+++ b/packages/create-svelte/bin.js
@@ -21,7 +21,21 @@ async function main() {
 	console.log(gray(`\ncreate-svelte version ${version}`));
 	console.log(disclaimer);
 
-	const cwd = process.argv[2] || '.';
+	let cwd = process.argv[2] || '.';
+
+	if (cwd === '.') {
+		const opts = await prompts([
+			{
+				type: 'text',
+				name: 'dir',
+				message: 'Where should we create your project?\n  (leave blank to use current directory)'
+			}
+		]);
+
+		if (opts.dir) {
+			cwd = opts.dir;
+		}
+	}
 
 	if (fs.existsSync(cwd)) {
 		if (fs.readdirSync(cwd).length > 0) {
@@ -39,48 +53,57 @@ async function main() {
 	}
 
 	const options = /** @type {import('./types/internal').Options} */ (
-		await prompts([
-			{
-				type: 'select',
-				name: 'template',
-				message: 'Which Svelte app template?',
-				choices: fs.readdirSync(dist('templates')).map((dir) => {
-					const meta_file = dist(`templates/${dir}/meta.json`);
-					const meta = JSON.parse(fs.readFileSync(meta_file, 'utf8'));
-
-					return {
-						title: meta.description,
-						value: dir
-					};
-				})
-			},
-			{
-				type: 'toggle',
-				name: 'typescript',
-				message: 'Use TypeScript?',
-				initial: false,
-				active: 'Yes',
-				inactive: 'No'
-			},
+		await prompts(
+			[
+				{
+					type: 'select',
+					name: 'template',
+					message: 'Which Svelte app template?',
+					choices: fs.readdirSync(dist('templates')).map((dir) => {
+						const meta_file = dist(`templates/${dir}/meta.json`);
+						const meta = JSON.parse(fs.readFileSync(meta_file, 'utf8'));
+
+						return {
+							title: meta.description,
+							value: dir
+						};
+					})
+				},
+				{
+					type: 'toggle',
+					name: 'typescript',
+					message: 'Use TypeScript?',
+					initial: false,
+					active: 'Yes',
+					inactive: 'No'
+				},
+				{
+					type: 'toggle',
+					name: 'eslint',
+					message: 'Add ESLint for code linting?',
+					initial: false,
+					active: 'Yes',
+					inactive: 'No'
+				},
+				{
+					type: 'toggle',
+					name: 'prettier',
+					message: 'Add Prettier for code formatting?',
+					initial: false,
+					active: 'Yes',
+					inactive: 'No'
+				}
+			],
 			{
-				type: 'toggle',
-				name: 'eslint',
-				message: 'Add ESLint for code linting?',
-				initial: false,
-				active: 'Yes',
-				inactive: 'No'
-			},
-			{
-				type: 'toggle',
-				name: 'prettier',
-				message: 'Add Prettier for code formatting?',
-				initial: false,
-				active: 'Yes',
-				inactive: 'No'
+				onCancel: () => {
+					process.exit(1);
+				}
 			}
-		])
+		)
 	);
 
+	options.name = path.basename(path.resolve(cwd));
+
 	await create(cwd, options);
 
 	console.log(bold(green('\nYour project is ready!')));
diff --git a/packages/create-svelte/index.js b/packages/create-svelte/index.js
index d6f599c07..65802ad11 100755
--- a/packages/create-svelte/index.js
+++ b/packages/create-svelte/index.js
@@ -11,10 +11,8 @@ import { mkdirp, copy, dist } from './utils.js';
 export async function create(cwd, options) {
 	mkdirp(cwd);
 
-	const name = path.basename(path.resolve(cwd));
-
-	write_template_files(options.template, options.typescript, name, cwd);
-	write_common_files(cwd, options, name);
+	write_template_files(options.template, options.typescript, options.name, cwd);
+	write_common_files(cwd, options, options.name);
 }
 
 /**
diff --git a/packages/create-svelte/package.json b/packages/create-svelte/package.json
index 1147f5dd6..8cce41469 100644
--- a/packages/create-svelte/package.json
+++ b/packages/create-svelte/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "create-svelte",
-	"version": "2.0.0-next.98",
+	"version": "2.0.0-next.99",
 	"repository": {
 		"type": "git",
 		"url": "https://github.com/sveltejs/kit",
diff --git a/packages/create-svelte/scripts/update-template-repo-contents.js b/packages/create-svelte/scripts/update-template-repo-contents.js
new file mode 100644
index 000000000..0cf14aa24
--- /dev/null
+++ b/packages/create-svelte/scripts/update-template-repo-contents.js
@@ -0,0 +1,22 @@
+import fs from 'fs';
+import path from 'path';
+import { create } from '../index.js';
+
+const repo = process.argv[2];
+
+fs.readdirSync(repo).forEach((file) => {
+	if (file !== '.git') {
+		fs.rmSync(path.join(repo, file), {
+			recursive: true,
+			force: true
+		});
+	}
+});
+
+await create(repo, {
+	name: 'kit-template-default',
+	template: 'default',
+	eslint: false,
+	typescript: false,
+	prettier: true
+});
diff --git a/packages/create-svelte/tsconfig.json b/packages/create-svelte/tsconfig.json
index 24c18e87b..28c25ce5e 100644
--- a/packages/create-svelte/tsconfig.json
+++ b/packages/create-svelte/tsconfig.json
@@ -4,8 +4,8 @@
 		"checkJs": true,
 		"noEmit": true,
 		"noImplicitAny": true,
-		"target": "es2020",
-		"module": "es2020",
+		"target": "esnext",
+		"module": "esnext",
 		"moduleResolution": "node",
 		"allowSyntheticDefaultImports": true
 	},
diff --git a/packages/create-svelte/types/internal.d.ts b/packages/create-svelte/types/internal.d.ts
index 81a10b96f..8be632663 100644
--- a/packages/create-svelte/types/internal.d.ts
+++ b/packages/create-svelte/types/internal.d.ts
@@ -1,4 +1,5 @@
 export type Options = {
+	name: string;
 	template: 'default' | 'skeleton';
 	typescript: boolean;
 	prettier: boolean;
@@ -15,8 +16,8 @@ export type Condition = 'eslint' | 'prettier' | 'typescript' | 'skeleton' | 'def
 export type Common = {
 	files: Array<{
 		name: string;
-		include: Array<Condition>;
-		exclude: Array<Condition>;
+		include: Condition[];
+		exclude: Condition[];
 		contents: string;
 	}>;
 };
diff --git a/packages/kit/CHANGELOG.md b/packages/kit/CHANGELOG.md
index 35b0081ed..7155e581c 100644
--- a/packages/kit/CHANGELOG.md
+++ b/packages/kit/CHANGELOG.md
@@ -1,5 +1,17 @@
 # @sveltejs/kit
 
+## 1.0.0-next.237
+
+### Patch Changes
+
+- Type compilerOptions as CompileOptions instead of any ([#3486](https://github.com/sveltejs/kit/pull/3486))
+
+## 1.0.0-next.236
+
+### Patch Changes
+
+- The redirect property returned from a module's load function must now be a properly encoded URI string value. ([#3404](https://github.com/sveltejs/kit/pull/3404))
+
 ## 1.0.0-next.235
 
 ### Patch Changes
diff --git a/packages/kit/package.json b/packages/kit/package.json
index aead4082a..6bff20d60 100644
--- a/packages/kit/package.json
+++ b/packages/kit/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "@sveltejs/kit",
-	"version": "1.0.0-next.235",
+	"version": "1.0.0-next.237",
 	"repository": {
 		"type": "git",
 		"url": "https://github.com/sveltejs/kit",
@@ -60,12 +60,14 @@
 		"build": "rollup -c && node scripts/cp.js src/runtime/components assets/components",
 		"dev": "rollup -cw",
 		"lint": "eslint --ignore-path .gitignore --ignore-pattern \"src/packaging/test/**\" \"{src,test}/**/*.{ts,mjs,js,svelte}\" && npm run check-format",
-		"check": "tsc && svelte-check --ignore \"src/packaging/test\"",
+		"check": "tsc && svelte-check --ignore test/prerendering,src/packaging/test",
 		"format": "npm run check-format -- --write",
 		"check-format": "prettier --check . --config ../../.prettierrc --ignore-path .gitignore",
 		"prepublishOnly": "npm run build",
-		"test": "npm run test:unit && npm run test:packaging && npm run test:integration",
+		"test": "npm run test:unit && npm run test:packaging && npm run test:prerendering && npm run test:integration",
 		"test:unit": "uvu src \"(spec\\.js|test[\\\\/]index\\.js)\" -i packaging",
+		"test:prerendering": "pnpm test:prerendering:basics",
+		"test:prerendering:basics": "cd test/prerendering/basics && pnpm test",
 		"test:packaging": "uvu src/packaging \"(spec\\.js|test[\\\\/]index\\.js)\"",
 		"test:integration": "pnpm test:integration:amp && pnpm test:integration:basics && pnpm test:integration:options && pnpm test:integration:options-2",
 		"test:integration:amp": "cd test/apps/amp && pnpm test",
diff --git a/packages/kit/src/core/adapt/prerender/prerender.js b/packages/kit/src/core/adapt/prerender/prerender.js
index 469c0b793..5b2d5131a 100644
--- a/packages/kit/src/core/adapt/prerender/prerender.js
+++ b/packages/kit/src/core/adapt/prerender/prerender.js
@@ -7,6 +7,7 @@ import { SVELTE_KIT } from '../../constants.js';
 import { is_root_relative, resolve } from '../../../utils/url.js';
 import { queue } from './queue.js';
 import { crawl } from './crawl.js';
+import { escape_html_attr } from '../../../utils/escape.js';
 
 /**
  * @typedef {import('types/config').PrerenderErrorHandler} PrerenderErrorHandler
@@ -158,7 +159,11 @@ export async function prerender({ cwd, out, log, config, build_data, fallback, a
 					mkdirp(dirname(file));
 
 					log.warn(`${rendered.status} ${decoded_path} -> ${location}`);
-					writeFileSync(file, `<meta http-equiv="refresh" content="0;url=${encodeURI(location)}">`);
+
+					writeFileSync(
+						file,
+						`<meta http-equiv="refresh" content=${escape_html_attr(`0;url=${location}`)}>`
+					);
 
 					const resolved = resolve(path, location);
 					if (is_root_relative(resolved)) {
diff --git a/packages/kit/tsconfig.json b/packages/kit/tsconfig.json
index e407884ba..f1641192a 100644
--- a/packages/kit/tsconfig.json
+++ b/packages/kit/tsconfig.json
@@ -17,5 +17,5 @@
 		}
 	},
 	"include": ["src/**/*", "test/**/*", "types/**/*"],
-	"exclude": ["src/packaging/test/fixtures/**/*"]
+	"exclude": ["src/packaging/test/fixtures/**/*", "test/prerendering/*/build/**/*"]
 }
diff --git a/packages/kit/types/ambient-modules.d.ts b/packages/kit/types/ambient-modules.d.ts
index a5e4aeb39..9eefd89b5 100644
--- a/packages/kit/types/ambient-modules.d.ts
+++ b/packages/kit/types/ambient-modules.d.ts
@@ -195,5 +195,7 @@ declare module '@sveltejs/kit/node' {
 declare module '@sveltejs/kit/install-fetch' {
 	import fetch, { Headers, Request, Response } from 'node-fetch';
 
+	export function __fetch_polyfill(): void;
+
 	export { fetch, Headers, Request, Response };
 }
diff --git a/packages/kit/types/config.d.ts b/packages/kit/types/config.d.ts
index fefeb6834..788bef4ce 100644
--- a/packages/kit/types/config.d.ts
+++ b/packages/kit/types/config.d.ts
@@ -1,3 +1,4 @@
+import { CompileOptions } from 'svelte/types/compiler/interfaces';
 import { UserConfig as ViteConfig } from 'vite';
 import { RecursiveRequired } from './helper';
 import { HttpMethod, Logger, RouteSegment, TrailingSlash } from './internal';
@@ -110,7 +111,7 @@ export interface PrerenderErrorHandler {
 export type PrerenderOnErrorValue = 'fail' | 'continue' | PrerenderErrorHandler;
 
 export interface Config {
-	compilerOptions?: any;
+	compilerOptions?: CompileOptions;
 	extensions?: string[];
 	kit?: {
 		adapter?: Adapter;
diff --git a/packages/kit/types/endpoint.d.ts b/packages/kit/types/endpoint.d.ts
index 8f2b97920..ace4fd18d 100644
--- a/packages/kit/types/endpoint.d.ts
+++ b/packages/kit/types/endpoint.d.ts
@@ -1,19 +1,18 @@
 import { RequestEvent } from './hooks';
-import { JSONString, MaybePromise, ResponseHeaders, Either, Fallthrough } from './helper';
+import { Either, JSONString, MaybePromise, ResponseHeaders } from './helper';
 
-type DefaultBody = JSONString | Uint8Array;
+type Body = JSONString | Uint8Array | ReadableStream | import('stream').Readable;
 
-export interface EndpointOutput<Body extends DefaultBody = DefaultBody> {
+export interface EndpointOutput {
 	status?: number;
 	headers?: Headers | Partial<ResponseHeaders>;
 	body?: Body;
 }
 
-export interface RequestHandler<
-	Locals = Record<string, any>,
-	Output extends DefaultBody = DefaultBody
-> {
-	(request: RequestEvent<Locals>): MaybePromise<
-		Either<Response | EndpointOutput<Output>, Fallthrough>
-	>;
+export interface Fallthrough {
+	fallthrough: true;
+}
+
+export interface RequestHandler<Locals = Record<string, any>> {
+	(event: RequestEvent<Locals>): MaybePromise<Either<Response | EndpointOutput, Fallthrough>>;
 }
diff --git a/packages/kit/types/helper.d.ts b/packages/kit/types/helper.d.ts
index 02ccfe84d..5f0d0624c 100644
--- a/packages/kit/types/helper.d.ts
+++ b/packages/kit/types/helper.d.ts
@@ -1,13 +1,3 @@
-interface ReadOnlyFormData {
-	get(key: string): string | null;
-	getAll(key: string): string[];
-	has(key: string): boolean;
-	entries(): Generator<[string, string], void>;
-	keys(): Generator<string, void>;
-	values(): Generator<string, void>;
-	[Symbol.iterator](): Generator<[string, string], void>;
-}
-
 type ToJSON = { toJSON(...args: any[]): JSONValue };
 type JSONValue = Exclude<JSONString, ToJSON>;
 export type JSONString =
@@ -22,12 +12,14 @@ export type JSONString =
 /** `string[]` is only for set-cookie, everything else must be type of `string` */
 export type ResponseHeaders = Record<string, string | string[]>;
 
-// Utility Types
+// <-- Utility Types -->
+type Only<T, U> = { [P in keyof T]: T[P] } & { [P in keyof U]?: never };
+
+export type Either<T, U> = Only<T, U> | Only<U, T>;
 export type InferValue<T, Key extends keyof T, Default> = T extends Record<Key, infer Val>
 	? Val
 	: Default;
 export type MaybePromise<T> = T | Promise<T>;
-export type Rec<T = any> = Record<string, T>;
 export type RecursiveRequired<T> = {
 	// Recursive implementation of TypeScript's Required utility type.
 	// Will recursively continue until it reaches primitive or union
@@ -38,15 +30,3 @@ export type RecursiveRequired<T> = {
 		? Extract<T[K], Function> // only take the Function type.
 		: T[K]; // Use the exact type for everything else
 };
-
-type Only<T, U> = {
-	[P in keyof T]: T[P];
-} & {
-	[P in keyof U]?: never;
-};
-
-export type Either<T, U> = Only<T, U> | Only<U, T>;
-
-export interface Fallthrough {
-	fallthrough: true;
-}
diff --git a/packages/kit/types/internal.d.ts b/packages/kit/types/internal.d.ts
index b82ff4b26..f3e6422e5 100644
--- a/packages/kit/types/internal.d.ts
+++ b/packages/kit/types/internal.d.ts
@@ -1,11 +1,9 @@
 import { OutputAsset, OutputChunk } from 'rollup';
-import { RequestHandler } from './endpoint';
 import { InternalApp, SSRManifest } from './app';
+import { Fallthrough, RequestHandler } from './endpoint';
+import { Either } from './helper';
 import { ExternalFetch, GetSession, HandleError, InternalHandle, RequestEvent } from './hooks';
 import { Load } from './page';
-import { Either, Fallthrough } from './helper';
-
-type PageId = string;
 
 export interface PrerenderOptions {
 	fallback?: string;
diff --git a/packages/kit/types/page.d.ts b/packages/kit/types/page.d.ts
index 16f2058fe..5e143862c 100644
--- a/packages/kit/types/page.d.ts
+++ b/packages/kit/types/page.d.ts
@@ -1,8 +1,9 @@
-import { InferValue, MaybePromise, Rec, Either, Fallthrough } from './helper';
+import { Fallthrough } from './endpoint';
+import { Either, InferValue, MaybePromise } from './helper';
 
 export interface LoadInput<
-	PageParams extends Rec<string> = Rec<string>,
-	Stuff extends Rec = Rec,
+	PageParams extends Record<string, string> = Record<string, string>,
+	Stuff extends Record<string, any> = Record<string, any>,
 	Session = any
 > {
 	url: URL;
@@ -13,15 +14,18 @@ export interface LoadInput<
 }
 
 export interface ErrorLoadInput<
-	PageParams extends Rec<string> = Rec<string>,
-	Stuff extends Rec = Rec,
+	PageParams extends Record<string, string> = Record<string, string>,
+	Stuff extends Record<string, any> = Record<string, any>,
 	Session = any
 > extends LoadInput<PageParams, Stuff, Session> {
 	status?: number;
 	error?: Error;
 }
 
-export interface LoadOutput<Props extends Rec = Rec, Stuff extends Rec = Rec> {
+export interface LoadOutput<
+	Props extends Record<string, any> = Record<string, any>,
+	Stuff extends Record<string, any> = Record<string, any>
+> {
 	status?: number;
 	error?: string | Error;
 	redirect?: string;
@@ -31,14 +35,14 @@ export interface LoadOutput<Props extends Rec = Rec, Stuff extends Rec = Rec> {
 }
 
 interface LoadInputExtends {
-	stuff?: Rec;
-	pageParams?: Rec<string>;
+	stuff?: Record<string, any>;
+	pageParams?: Record<string, string>;
 	session?: any;
 }
 
 interface LoadOutputExtends {
-	stuff?: Rec;
-	props?: Rec;
+	stuff?: Record<string, any>;
+	props?: Record<string, any>;
 }
 
 export interface Load<
@@ -47,14 +51,17 @@ export interface Load<
 > {
 	(
 		input: LoadInput<
-			InferValue<Input, 'pageParams', Rec<string>>,
-			InferValue<Input, 'stuff', Rec>,
+			InferValue<Input, 'pageParams', Record<string, string>>,
+			InferValue<Input, 'stuff', Record<string, any>>,
 			InferValue<Input, 'session', any>
 		>
 	): MaybePromise<
 		Either<
-			LoadOutput<InferValue<Output, 'props', Rec>, InferValue<Output, 'stuff', Rec>>,
-			Fallthrough
+			Fallthrough,
+			LoadOutput<
+				InferValue<Output, 'props', Record<string, any>>,
+				InferValue<Output, 'stuff', Record<string, any>>
+			>
 		>
 	>;
 }
@@ -65,9 +72,14 @@ export interface ErrorLoad<
 > {
 	(
 		input: ErrorLoadInput<
-			InferValue<Input, 'pageParams', Rec<string>>,
-			InferValue<Input, 'stuff', Rec>,
+			InferValue<Input, 'pageParams', Record<string, string>>,
+			InferValue<Input, 'stuff', Record<string, any>>,
 			InferValue<Input, 'session', any>
 		>
-	): MaybePromise<LoadOutput<InferValue<Output, 'props', Rec>, InferValue<Output, 'stuff', Rec>>>;
+	): MaybePromise<
+		LoadOutput<
+			InferValue<Output, 'props', Record<string, any>>,
+			InferValue<Output, 'stuff', Record<string, any>>
+		>
+	>;
 }
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 0d4e405a4..d3ac4795f 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -93,7 +93,7 @@ importers:
   packages/adapter-netlify:
     specifiers:
       '@iarna/toml': ^2.2.5
-      '@netlify/functions': ^0.10.0
+      '@netlify/functions': ^0.11.0
       '@rollup/plugin-commonjs': ^21.0.0
       '@rollup/plugin-json': ^4.1.0
       '@rollup/plugin-node-resolve': ^13.0.5
@@ -107,7 +107,7 @@ importers:
       esbuild: 0.13.15
       tiny-glob: 0.2.9
     devDependencies:
-      '@netlify/functions': 0.10.0
+      '@netlify/functions': 0.11.0
       '@rollup/plugin-commonjs': 21.0.1_rollup@2.60.2
       '@rollup/plugin-json': 4.1.0_rollup@2.60.2
       '@rollup/plugin-node-resolve': 13.0.6_rollup@2.60.2
@@ -1141,8 +1141,8 @@ packages:
       read-yaml-file: 1.1.0
     dev: true
 
-  /@netlify/functions/0.10.0:
-    resolution: {integrity: sha512-NNFADTPnokuoMY1OUhaXlE/Jrzk5lHOl1uB4L/8pw1UJ/CaectZJACMExijbJnqaKIhOQG0WmbBQUh1lgnK/Qg==}
+  /@netlify/functions/0.11.0:
+    resolution: {integrity: sha512-+WWX081UCkLZYkfh1Ru+PH5HTujNXugeskATGhZLTyS1U3Cqh2gYLArdG1a/vU8WWopu/wjpxEOdWq8/T5Nw5Q==}
     engines: {node: '>=8.3.0'}
     dependencies:
       is-promise: 4.0.0


Test patch:
diff --git a/packages/kit/test/prerendering/basics/package.json b/packages/kit/test/prerendering/basics/package.json
new file mode 100644
index 000000000..853b00a00
--- /dev/null
+++ b/packages/kit/test/prerendering/basics/package.json
@@ -0,0 +1,17 @@
+{
+	"name": "prerendering-test-basics",
+	"private": true,
+	"version": "0.0.1",
+	"scripts": {
+		"dev": "node ../../cli.js dev",
+		"build": "node ../../cli.js build",
+		"preview": "node ../../cli.js preview",
+		"test": "npm run build && uvu test"
+	},
+	"devDependencies": {
+		"@sveltejs/kit": "workspace:*",
+		"svelte": "^3.43.0",
+		"uvu": "^0.5.2"
+	},
+	"type": "module"
+}
diff --git a/packages/kit/test/prerendering/basics/src/app.html b/packages/kit/test/prerendering/basics/src/app.html
new file mode 100644
index 000000000..11ff812a1
--- /dev/null
+++ b/packages/kit/test/prerendering/basics/src/app.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<meta charset="utf-8" />
+		<meta name="viewport" content="width=device-width, initial-scale=1" />
+		%svelte.head%
+	</head>
+	<body>
+		%svelte.body%
+	</body>
+</html>
diff --git a/packages/kit/test/prerendering/basics/src/routes/index.svelte b/packages/kit/test/prerendering/basics/src/routes/index.svelte
new file mode 100644
index 000000000..6e7c1e6ac
--- /dev/null
+++ b/packages/kit/test/prerendering/basics/src/routes/index.svelte
@@ -0,0 +1,1 @@
+<h1>hello</h1>
diff --git a/packages/kit/test/prerendering/basics/src/routes/redirect-encoded.svelte b/packages/kit/test/prerendering/basics/src/routes/redirect-encoded.svelte
new file mode 100644
index 000000000..fe69f129a
--- /dev/null
+++ b/packages/kit/test/prerendering/basics/src/routes/redirect-encoded.svelte
@@ -0,0 +1,9 @@
+<script context="module">
+	/** @type {import('@sveltejs/kit').Load} */
+	export function load() {
+		return {
+			status: 301,
+			redirect: `https://example.com/redirected?returnTo=${encodeURIComponent('/foo?bar=baz')}`
+		};
+	}
+</script>
diff --git a/packages/kit/test/prerendering/basics/src/routes/redirect-malicious.svelte b/packages/kit/test/prerendering/basics/src/routes/redirect-malicious.svelte
new file mode 100644
index 000000000..a62c94ead
--- /dev/null
+++ b/packages/kit/test/prerendering/basics/src/routes/redirect-malicious.svelte
@@ -0,0 +1,9 @@
+<script context="module">
+	/** @type {import('@sveltejs/kit').Load} */
+	export function load() {
+		return {
+			status: 301,
+			redirect: 'https://example.com/</' + 'script>alert("pwned")'
+		};
+	}
+</script>
diff --git a/packages/kit/test/prerendering/basics/src/routes/redirect.svelte b/packages/kit/test/prerendering/basics/src/routes/redirect.svelte
new file mode 100644
index 000000000..85e2614d8
--- /dev/null
+++ b/packages/kit/test/prerendering/basics/src/routes/redirect.svelte
@@ -0,0 +1,9 @@
+<script context="module">
+	/** @type {import('@sveltejs/kit').Load} */
+	export function load() {
+		return {
+			status: 301,
+			redirect: 'https://example.com/redirected'
+		};
+	}
+</script>
diff --git a/packages/kit/test/prerendering/basics/svelte.config.js b/packages/kit/test/prerendering/basics/svelte.config.js
new file mode 100644
index 000000000..35f665a03
--- /dev/null
+++ b/packages/kit/test/prerendering/basics/svelte.config.js
@@ -0,0 +1,23 @@
+import path from 'path';
+import adapter from '../../../../adapter-static/index.js';
+
+/** @type {import('@sveltejs/kit').Config} */
+const config = {
+	kit: {
+		adapter: adapter(),
+
+		vite: {
+			build: {
+				minify: false
+			},
+			clearScreen: false,
+			server: {
+				fs: {
+					allow: [path.resolve('../../../src')]
+				}
+			}
+		}
+	}
+};
+
+export default config;
diff --git a/packages/kit/test/prerendering/basics/test/test.js b/packages/kit/test/prerendering/basics/test/test.js
new file mode 100644
index 000000000..d8c54fbe7
--- /dev/null
+++ b/packages/kit/test/prerendering/basics/test/test.js
@@ -0,0 +1,40 @@
+import fs from 'fs';
+import { fileURLToPath } from 'url';
+import { test } from 'uvu';
+import * as assert from 'uvu/assert';
+
+const build = fileURLToPath(new URL('../build', import.meta.url));
+
+/** @param {string} file */
+const read = (file) => fs.readFileSync(`${build}/${file}`, 'utf-8');
+
+test('prerenders /', () => {
+	const content = read('index.html');
+	assert.ok(content.includes('<h1>hello</h1>'));
+});
+
+test('renders a redirect', () => {
+	const content = read('redirect/index.html');
+	assert.equal(
+		content,
+		'<meta http-equiv="refresh" content="0;url=https://example.com/redirected">'
+	);
+});
+
+test('does not double-encode redirect locations', () => {
+	const content = read('redirect-encoded/index.html');
+	assert.equal(
+		content,
+		'<meta http-equiv="refresh" content="0;url=https://example.com/redirected?returnTo=%2Ffoo%3Fbar%3Dbaz">'
+	);
+});
+
+test('escapes characters in redirect', () => {
+	const content = read('redirect-malicious/index.html');
+	assert.equal(
+		content,
+		'<meta http-equiv="refresh" content="0;url=https://example.com/&lt;/script&gt;alert(&quot;pwned&quot;)">'
+	);
+});
+
+test.run();