Repository: sveltejs/kit
Pull request #4160 (2022-03-01 07:44:02)

differentiate between `input` and `output` params inside `Load`
### 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
- [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 `pnpx changeset` and following the prompts. All changesets should be `patch` until SvelteKit 1.0

I want to be able to type the Params of a `Load` function that get's it's props from an Endpoint.
Currently the `InputParams` are always typed as `Record<string, any>`

Here is a simplified example:

`/routes/[id].ts`
```ts
import type { RequestHandler } from '@sveltejs/kit'

export type Product = {
   name: string,
   color: string,
}

type Params = { id: string}
type OutputType = Product

export const get: RequestHandler<Params, OutputType> = ({ params }) => {
   return {
      body: {
         name: `${params.id} product`,
         color: 'green',
      }
   }
}
```

> `/routes/[id].svelte`
```svelte
<script lang="ts" context="module">
   import type { Load } from '@sveltejs/kit'
   import type { get } from './[id]'

   type Params = { id: string }
   type InputProps = NonNullable<Awaited<ReturnType<typeof get>>['body']> // would be `Product`
   type OutputProps = Params & InputProps

   export const load: Load<Params, InputProps, OutputProps> = async ({
      params,
      props, // <= `InputProps`
      session,
   }) => {

      // ... some logic

      return {
         props: { // <= `OutputProps`
            product: props,
            id: params.id,
         },
      }
   }
</script>
```

This would be a breaking change

changeset-bot[bot]: ###  🦋  Changeset detected

Latest commit: e0009449df8a3293aec70db0a9b851c9424e666e

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 1 package</summary>

  | Name          | Type  |
| ------------- | ----- |
| @sveltejs/kit | Minor |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/master/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/ivanhofer/kit/new/load-input-output-params?filename=.changeset/tender-lemons-trade.md&value=---%0A%22%40sveltejs%2Fkit%22%3A%20patch%0A---%0A%0Adifferentiate%20between%20%60input%60%20and%20%60output%60%20params%20inside%20%60Load%60%0A)


benmccann: you'll need to run `pnpm format` because `lint` is currently failing

Patch:
diff --git a/.changeset/forty-terms-peel.md b/.changeset/forty-terms-peel.md
new file mode 100644
index 000000000..f7fe0c466
--- /dev/null
+++ b/.changeset/forty-terms-peel.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+Add config.kit.outDir
diff --git a/.changeset/pre.json b/.changeset/pre.json
index e15a9f023..23d3248f5 100644
--- a/.changeset/pre.json
+++ b/.changeset/pre.json
@@ -19,7 +19,11 @@
     "prerendering-test-basics": "0.0.1",
     "prerendering-test-options": "0.0.1",
     "kit.svelte.dev": "0.0.1",
-    "default-template": "0.0.1"
+    "default-template": "0.0.1",
+    "test-amp": "0.0.1",
+    "test-basics": "0.0.1",
+    "test-options": "0.0.1",
+    "test-options-2": "0.0.1"
   },
   "changesets": [
     "afraid-eels-tease",
@@ -41,6 +45,7 @@
     "blue-bulldogs-visit",
     "blue-cats-mate",
     "blue-llamas-exist",
+    "blue-parents-invite",
     "blue-poets-jam",
     "blue-squids-march",
     "blue-teachers-allow",
@@ -278,6 +283,7 @@
     "hip-windows-sit",
     "honest-beers-sing",
     "honest-bikes-pull",
+    "honest-cows-notice",
     "honest-islands-flash",
     "honest-jars-report",
     "honest-singers-guess",
@@ -574,6 +580,7 @@
     "silly-phones-worry",
     "silly-suns-look",
     "silver-elephants-tap",
+    "silver-horses-check",
     "silver-hounds-clean",
     "silver-lemons-fail",
     "silver-needles-judge",
diff --git a/.changeset/yellow-geese-tap.md b/.changeset/yellow-geese-tap.md
new file mode 100644
index 000000000..e8623fb4f
--- /dev/null
+++ b/.changeset/yellow-geese-tap.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+[breaking] allow `InputProps` and `OutputProps` to be typed separately in `Load`
diff --git a/documentation/docs/01-routing.md b/documentation/docs/01-routing.md
index 211f6f7bb..28c1c90a3 100644
--- a/documentation/docs/01-routing.md
+++ b/documentation/docs/01-routing.md
@@ -83,7 +83,7 @@ export async function get({ params }) {
 
 > All server-side code, including endpoints, has access to `fetch` in case you need to request data from external APIs. Don't worry about the `$lib` import, we'll get to that [later](/docs/modules#$lib).
 
-The type of the `get` function above comes from `./[id].d.ts`, which is a file generated by SvelteKit (in a hidden directory, using the [`rootDirs`](https://www.typescriptlang.org/tsconfig#rootDirs) option) that provides type safety when accessing `params`. See the section on [generated types](/docs/types#generated-types) for more detail.
+The type of the `get` function above comes from `./[id].d.ts`, which is a file generated by SvelteKit (inside your [`outDir`](/docs/configuration#outdir), using the [`rootDirs`](https://www.typescriptlang.org/tsconfig#rootDirs) option) that provides type safety when accessing `params`. See the section on [generated types](/docs/types#generated-types) for more detail.
 
 The job of a [request handler](/docs/types#sveltejs-kit-requesthandler) is to return a `{ status, headers, body }` object representing the response, where `status` is an [HTTP status code](https://httpstatusdogs.com):
 
diff --git a/documentation/docs/13-configuration.md b/documentation/docs/13-configuration.md
index 0bc7ff71b..154a6e150 100644
--- a/documentation/docs/13-configuration.md
+++ b/documentation/docs/13-configuration.md
@@ -43,6 +43,7 @@ const config = {
 			parameter: '_method',
 			allowed: []
 		},
+		outDir: '.svelte-kit',
 		package: {
 			dir: 'package',
 			emitTypes: true,
@@ -170,6 +171,10 @@ See [HTTP Method Overrides](/docs/routing#endpoints-http-method-overrides). An o
 - `parameter` — query parameter name to use for passing the intended method value
 - `allowed` - array of HTTP methods that can be used when overriding the original request method
 
+### outDir
+
+The directory that SvelteKit writes files to during `dev` and `build`. You should exclude this directory from version control.
+
 ### package
 
 Options related to [creating a package](/docs/packaging).
diff --git a/documentation/docs/14-types.md b/documentation/docs/14-types.md
index f7719dd63..48fadfcb2 100644
--- a/documentation/docs/14-types.md
+++ b/documentation/docs/14-types.md
@@ -71,6 +71,6 @@ export async function get({ params }) {
 </script>
 ```
 
-> For this to work, your own `tsconfig.json` or `jsconfig.json` should extend from the generated `.svelte-kit/tsconfig.json`:
+> For this to work, your own `tsconfig.json` or `jsconfig.json` should extend from the generated `.svelte-kit/tsconfig.json` (where `.svelte-kit` is your [`outDir`](/docs/configuration#outdir)):
 >
->     { "extends": ".svelte-kit/tsconfig.json" }
+>     { "extends": "./.svelte-kit/tsconfig.json" }
diff --git a/packages/create-svelte/CHANGELOG.md b/packages/create-svelte/CHANGELOG.md
index 9f39843f5..c015d29e6 100644
--- a/packages/create-svelte/CHANGELOG.md
+++ b/packages/create-svelte/CHANGELOG.md
@@ -1,5 +1,11 @@
 # create-svelte
 
+## 2.0.0-next.123
+
+### Patch Changes
+
+- Extend user tsconfig from generated .svelte-kit/tsconfig.json ([#4118](https://github.com/sveltejs/kit/pull/4118))
+
 ## 2.0.0-next.122
 
 ### Patch Changes
diff --git a/packages/create-svelte/package.json b/packages/create-svelte/package.json
index c91f77fd8..eda2c1013 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.122",
+	"version": "2.0.0-next.123",
 	"repository": {
 		"type": "git",
 		"url": "https://github.com/sveltejs/kit",
diff --git a/packages/kit/.gitignore b/packages/kit/.gitignore
index 8aceed42c..4d7a1dc19 100644
--- a/packages/kit/.gitignore
+++ b/packages/kit/.gitignore
@@ -7,4 +7,5 @@
 /test/**/build
 !/src/core/adapt/fixtures/*/.svelte-kit
 !/test/node_modules
-/test/apps/basics/test/errors.json
\ No newline at end of file
+/test/apps/basics/test/errors.json
+.custom-out-dir
\ No newline at end of file
diff --git a/packages/kit/CHANGELOG.md b/packages/kit/CHANGELOG.md
index a368c4eff..96e4bc5a6 100644
--- a/packages/kit/CHANGELOG.md
+++ b/packages/kit/CHANGELOG.md
@@ -1,5 +1,15 @@
 # @sveltejs/kit
 
+## 1.0.0-next.289
+
+### Patch Changes
+
+- Only fall back to full page reload if pathname has changed ([#4116](https://github.com/sveltejs/kit/pull/4116))
+
+* Generate types for each page/endpoint ([#4120](https://github.com/sveltejs/kit/pull/4120))
+
+- Extend user tsconfig from generated .svelte-kit/tsconfig.json ([#4118](https://github.com/sveltejs/kit/pull/4118))
+
 ## 1.0.0-next.288
 
 ### Patch Changes
diff --git a/packages/kit/package.json b/packages/kit/package.json
index 5d04f39ae..b98bb6dbc 100644
--- a/packages/kit/package.json
+++ b/packages/kit/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "@sveltejs/kit",
-	"version": "1.0.0-next.288",
+	"version": "1.0.0-next.289",
 	"repository": {
 		"type": "git",
 		"url": "https://github.com/sveltejs/kit",
diff --git a/packages/kit/src/core/adapt/builder.js b/packages/kit/src/core/adapt/builder.js
index 1a245e142..2a21be674 100644
--- a/packages/kit/src/core/adapt/builder.js
+++ b/packages/kit/src/core/adapt/builder.js
@@ -1,18 +1,16 @@
-import { SVELTE_KIT } from '../constants.js';
 import { copy, rimraf, mkdirp } from '../../utils/filesystem.js';
 import { prerender } from './prerender/prerender.js';
 import { generate_manifest } from '../generate_manifest/index.js';
 
 /**
  * @param {{
- *   cwd: string;
  *   config: import('types').ValidatedConfig;
  *   build_data: import('types').BuildData;
  *   log: import('types').Logger;
  * }} opts
  * @returns {import('types').Builder}
  */
-export function create_builder({ cwd, config, build_data, log }) {
+export function create_builder({ config, build_data, log }) {
 	/** @type {Set<string>} */
 	let prerendered_paths;
 
@@ -118,15 +116,15 @@ export function create_builder({ cwd, config, build_data, log }) {
 		},
 
 		getBuildDirectory(name) {
-			return `${cwd}/${SVELTE_KIT}/${name}`;
+			return `${config.kit.outDir}/${name}`;
 		},
 
 		getClientDirectory() {
-			return `${cwd}/${SVELTE_KIT}/output/client`;
+			return `${config.kit.outDir}/output/client`;
 		},
 
 		getServerDirectory() {
-			return `${cwd}/${SVELTE_KIT}/output/server`;
+			return `${config.kit.outDir}/output/server`;
 		},
 
 		getStaticDirectory() {
@@ -134,13 +132,13 @@ export function create_builder({ cwd, config, build_data, log }) {
 		},
 
 		writeClient(dest) {
-			return copy(`${cwd}/${SVELTE_KIT}/output/client`, dest, {
+			return copy(`${config.kit.outDir}/output/client`, dest, {
 				filter: (file) => file[0] !== '.'
 			});
 		},
 
 		writeServer(dest) {
-			return copy(`${cwd}/${SVELTE_KIT}/output/server`, dest, {
+			return copy(`${config.kit.outDir}/output/server`, dest, {
 				filter: (file) => file[0] !== '.'
 			});
 		},
@@ -159,7 +157,6 @@ export function create_builder({ cwd, config, build_data, log }) {
 			const prerendered = await prerender({
 				out: dest,
 				all,
-				cwd,
 				config,
 				build_data,
 				fallback,
diff --git a/packages/kit/src/core/adapt/builder.spec.js b/packages/kit/src/core/adapt/builder.spec.js
index abf07e037..24370092c 100644
--- a/packages/kit/src/core/adapt/builder.spec.js
+++ b/packages/kit/src/core/adapt/builder.spec.js
@@ -5,13 +5,13 @@ import * as assert from 'uvu/assert';
 import glob from 'tiny-glob/sync.js';
 import { create_builder } from './builder.js';
 import { fileURLToPath } from 'url';
-import { SVELTE_KIT } from '../constants.js';
 
 const __filename = fileURLToPath(import.meta.url);
 const __dirname = join(__filename, '..');
 
 test('copy files', () => {
 	const cwd = join(__dirname, 'fixtures/basic');
+	const outDir = join(cwd, '.svelte-kit');
 
 	/** @type {import('types').Config} */
 	const mocked = {
@@ -20,12 +20,12 @@ test('copy files', () => {
 			appDir: '_app',
 			files: {
 				assets: join(__dirname, 'fixtures/basic/static')
-			}
+			},
+			outDir
 		}
 	};
 
 	const builder = create_builder({
-		cwd,
 		config: /** @type {import('types').ValidatedConfig} */ (mocked),
 		// @ts-expect-error
 		build_data: {},
@@ -48,18 +48,12 @@ test('copy files', () => {
 	rmSync(dest, { recursive: true, force: true });
 	builder.writeClient(dest);
 
-	assert.equal(
-		glob('**', { cwd: `${cwd}/${SVELTE_KIT}/output/client` }),
-		glob('**', { cwd: dest })
-	);
+	assert.equal(glob('**', { cwd: `${outDir}/output/client` }), glob('**', { cwd: dest }));
 
 	rmSync(dest, { recursive: true, force: true });
 	builder.writeServer(dest);
 
-	assert.equal(
-		glob('**', { cwd: `${cwd}/${SVELTE_KIT}/output/server` }),
-		glob('**', { cwd: dest })
-	);
+	assert.equal(glob('**', { cwd: `${outDir}/output/server` }), glob('**', { cwd: dest }));
 
 	rmSync(dest, { force: true, recursive: true });
 });
diff --git a/packages/kit/src/core/adapt/index.js b/packages/kit/src/core/adapt/index.js
index e0187bc71..645b55df7 100644
--- a/packages/kit/src/core/adapt/index.js
+++ b/packages/kit/src/core/adapt/index.js
@@ -5,15 +5,15 @@ import { create_builder } from './builder.js';
 /**
  * @param {import('types').ValidatedConfig} config
  * @param {import('types').BuildData} build_data
- * @param {{ cwd?: string, verbose: boolean }} opts
+ * @param {{ verbose: boolean }} opts
  */
-export async function adapt(config, build_data, { cwd = process.cwd(), verbose }) {
+export async function adapt(config, build_data, { verbose }) {
 	const { name, adapt } = config.kit.adapter;
 
 	console.log(colors.bold().cyan(`\n> Using ${name}`));
 
 	const log = logger({ verbose });
-	const builder = create_builder({ cwd, config, build_data, log });
+	const builder = create_builder({ config, build_data, log });
 	await adapt(builder);
 
 	log.success('done');
diff --git a/packages/kit/src/core/adapt/prerender/prerender.js b/packages/kit/src/core/adapt/prerender/prerender.js
index 74ea0e1f4..9cdab9b90 100644
--- a/packages/kit/src/core/adapt/prerender/prerender.js
+++ b/packages/kit/src/core/adapt/prerender/prerender.js
@@ -1,9 +1,8 @@
 import { readFileSync, writeFileSync } from 'fs';
-import { dirname, join, resolve as resolve_path } from 'path';
+import { dirname, join } from 'path';
 import { pathToFileURL, URL } from 'url';
 import { mkdirp } from '../../../utils/filesystem.js';
 import { installFetch } from '../../../install-fetch.js';
-import { SVELTE_KIT } from '../../constants.js';
 import { is_root_relative, normalize_path, resolve } from '../../../utils/url.js';
 import { queue } from './queue.js';
 import { crawl } from './crawl.js';
@@ -41,7 +40,6 @@ const REDIRECT = 3;
 
 /**
  * @param {{
- *   cwd: string;
  *   out: string;
  *   log: Logger;
  *   config: import('types').ValidatedConfig;
@@ -50,7 +48,7 @@ const REDIRECT = 3;
  *   all: boolean; // disregard `export const prerender = true`
  * }} opts
  */
-export async function prerender({ cwd, out, log, config, build_data, fallback, all }) {
+export async function prerender({ out, log, config, build_data, fallback, all }) {
 	/** @type {import('types').Prerendered} */
 	const prerendered = {
 		pages: new Map(),
@@ -65,7 +63,7 @@ export async function prerender({ cwd, out, log, config, build_data, fallback, a
 
 	installFetch();
 
-	const server_root = resolve_path(cwd, `${SVELTE_KIT}/output`);
+	const server_root = join(config.kit.outDir, 'output');
 
 	/** @type {import('types').ServerModule} */
 	const { Server, override } = await import(pathToFileURL(`${server_root}/server/index.js`).href);
diff --git a/packages/kit/src/core/build/build_client.js b/packages/kit/src/core/build/build_client.js
index 3ac04585c..2678ce98b 100644
--- a/packages/kit/src/core/build/build_client.js
+++ b/packages/kit/src/core/build/build_client.js
@@ -6,7 +6,6 @@ import { print_config_conflicts } from '../config/index.js';
 import { create_app } from '../create_app/index.js';
 import { copy_assets, get_aliases } from '../utils.js';
 import { create_build, find_deps } from './utils.js';
-import { SVELTE_KIT } from '../constants.js';
 import { posixify } from '../../utils/filesystem.js';
 
 /**
@@ -39,7 +38,7 @@ export async function build_client({
 		cwd
 	});
 
-	copy_assets(`${SVELTE_KIT}/runtime`);
+	copy_assets(path.join(config.kit.outDir, 'runtime'));
 
 	process.env.VITE_SVELTEKIT_AMP = config.kit.amp ? 'true' : '';
 
diff --git a/packages/kit/src/core/build/build_server.js b/packages/kit/src/core/build/build_server.js
index dfc2cce9a..1d9e69020 100644
--- a/packages/kit/src/core/build/build_server.js
+++ b/packages/kit/src/core/build/build_server.js
@@ -4,9 +4,8 @@ import { svelte } from '@sveltejs/vite-plugin-svelte';
 import { mkdirp, posixify } from '../../utils/filesystem.js';
 import { deep_merge } from '../../utils/object.js';
 import { load_template, print_config_conflicts } from '../config/index.js';
-import { get_aliases, resolve_entry, runtime } from '../utils.js';
+import { get_aliases, get_runtime_path, resolve_entry } from '../utils.js';
 import { create_build, find_deps } from './utils.js';
-import { SVELTE_KIT } from '../constants.js';
 import { s } from '../../utils/misc.js';
 
 /**
@@ -14,11 +13,12 @@ import { s } from '../../utils/misc.js';
  *   hooks: string;
  *   config: import('types').ValidatedConfig;
  *   has_service_worker: boolean;
+ *   runtime: string;
  *   template: string;
  * }} opts
  * @returns
  */
-const server_template = ({ config, hooks, has_service_worker, template }) => `
+const server_template = ({ config, hooks, has_service_worker, runtime, template }) => `
 import root from '__GENERATED__/root.svelte';
 import { respond } from '${runtime}/server/index.js';
 import { set_paths, assets, base } from '${runtime}/paths.js';
@@ -133,7 +133,7 @@ export async function build_server(
 ) {
 	let hooks_file = resolve_entry(config.kit.files.hooks);
 	if (!hooks_file || !fs.existsSync(hooks_file)) {
-		hooks_file = path.resolve(cwd, `${SVELTE_KIT}/build/hooks.js`);
+		hooks_file = path.join(config.kit.outDir, 'build/hooks.js');
 		fs.writeFileSync(hooks_file, '');
 	}
 
@@ -177,6 +177,7 @@ export async function build_server(
 			config,
 			hooks: app_relative(hooks_file),
 			has_service_worker: service_worker_register && !!service_worker_entry_file,
+			runtime: get_runtime_path(config),
 			template: load_template(cwd, config)
 		})
 	);
diff --git a/packages/kit/src/core/build/build_service_worker.js b/packages/kit/src/core/build/build_service_worker.js
index 86a223e9d..965e91459 100644
--- a/packages/kit/src/core/build/build_service_worker.js
+++ b/packages/kit/src/core/build/build_service_worker.js
@@ -3,7 +3,6 @@ import vite from 'vite';
 import { s } from '../../utils/misc.js';
 import { deep_merge } from '../../utils/object.js';
 import { print_config_conflicts } from '../config/index.js';
-import { SVELTE_KIT } from '../constants.js';
 
 /**
  * @param {{
@@ -32,7 +31,7 @@ export async function build_service_worker(
 		}
 	}
 
-	const service_worker = `${cwd}/${SVELTE_KIT}/generated/service-worker.js`;
+	const service_worker = `${config.kit.outDir}/generated/service-worker.js`;
 
 	fs.writeFileSync(
 		service_worker,
diff --git a/packages/kit/src/core/build/index.js b/packages/kit/src/core/build/index.js
index 3fb90bc51..5515f727b 100644
--- a/packages/kit/src/core/build/index.js
+++ b/packages/kit/src/core/build/index.js
@@ -2,8 +2,7 @@ import fs from 'fs';
 import path from 'path';
 import { mkdirp, rimraf, posixify } from '../../utils/filesystem.js';
 import create_manifest_data from '../create_manifest_data/index.js';
-import { SVELTE_KIT } from '../constants.js';
-import { runtime, resolve_entry } from '../utils.js';
+import { get_runtime_path, resolve_entry } from '../utils.js';
 import { generate_manifest } from '../generate_manifest/index.js';
 import { build_service_worker } from './build_service_worker.js';
 import { build_client } from './build_client.js';
@@ -17,11 +16,11 @@ import { generate_tsconfig } from '../tsconfig.js';
 export async function build(config) {
 	const cwd = process.cwd(); // TODO is this necessary?
 
-	const build_dir = path.resolve(`${SVELTE_KIT}/build`);
+	const build_dir = path.join(config.kit.outDir, 'build');
 	rimraf(build_dir);
 	mkdirp(build_dir);
 
-	const output_dir = path.resolve(`${SVELTE_KIT}/output`);
+	const output_dir = path.join(config.kit.outDir, 'output');
 	rimraf(output_dir);
 	mkdirp(output_dir);
 
@@ -41,7 +40,7 @@ export async function build(config) {
 			cwd
 		}),
 		output_dir,
-		client_entry_file: path.relative(cwd, `${runtime}/client/start.js`),
+		client_entry_file: path.relative(cwd, `${get_runtime_path(config)}/client/start.js`),
 		service_worker_entry_file: resolve_entry(config.kit.files.serviceWorker),
 		service_worker_register: config.kit.serviceWorker.register
 	};
diff --git a/packages/kit/src/core/config/index.js b/packages/kit/src/core/config/index.js
index 972248c36..d2d00ebd5 100644
--- a/packages/kit/src/core/config/index.js
+++ b/packages/kit/src/core/config/index.js
@@ -40,6 +40,8 @@ export async function load_config({ cwd = process.cwd() } = {}) {
 
 	const validated = validate_config(config.default);
 
+	validated.kit.outDir = path.resolve(cwd, validated.kit.outDir);
+
 	validated.kit.files.assets = path.resolve(cwd, validated.kit.files.assets);
 	validated.kit.files.hooks = path.resolve(cwd, validated.kit.files.hooks);
 	validated.kit.files.lib = path.resolve(cwd, validated.kit.files.lib);
diff --git a/packages/kit/src/core/config/index.spec.js b/packages/kit/src/core/config/index.spec.js
index 9258bdaa0..b2c0f71a8 100644
--- a/packages/kit/src/core/config/index.spec.js
+++ b/packages/kit/src/core/config/index.spec.js
@@ -71,6 +71,7 @@ const get_defaults = (prefix = '') => ({
 			parameter: '_method',
 			allowed: []
 		},
+		outDir: join(prefix, '.svelte-kit'),
 		package: {
 			dir: 'package',
 			emitTypes: true
diff --git a/packages/kit/src/core/config/options.js b/packages/kit/src/core/config/options.js
index 03c7473fa..b6047884a 100644
--- a/packages/kit/src/core/config/options.js
+++ b/packages/kit/src/core/config/options.js
@@ -142,6 +142,8 @@ const options = object(
 				})
 			}),
 
+			outDir: string('.svelte-kit'),
+
 			package: object({
 				dir: string('package'),
 				// excludes all .d.ts and filename starting with _
diff --git a/packages/kit/src/core/constants.js b/packages/kit/src/core/constants.js
index 565b216bf..630bc3e90 100644
--- a/packages/kit/src/core/constants.js
+++ b/packages/kit/src/core/constants.js
@@ -1,5 +1,3 @@
-export const SVELTE_KIT = '.svelte-kit';
-
 // in `svelte-kit dev` and `svelte-kit preview`, we use a fake
 // asset path so that we can serve local assets while still
 // verifying that requests are correctly prefixed
diff --git a/packages/kit/src/core/create_app/index.js b/packages/kit/src/core/create_app/index.js
index ff4996168..0ee3ee10c 100644
--- a/packages/kit/src/core/create_app/index.js
+++ b/packages/kit/src/core/create_app/index.js
@@ -2,7 +2,6 @@ import fs from 'fs';
 import path from 'path';
 import { s } from '../../utils/misc.js';
 import { mkdirp } from '../../utils/filesystem.js';
-import { SVELTE_KIT } from '../constants.js';
 
 /** @type {Map<string, string>} */
 const previous_contents = new Map();
@@ -29,7 +28,7 @@ export function write_if_changed(file, code) {
  * }} options
  */
 export function create_app({ config, manifest_data, cwd = process.cwd() }) {
-	const output = `${SVELTE_KIT}/generated`;
+	const output = path.join(config.kit.outDir, 'generated');
 	const base = path.relative(cwd, output);
 
 	write_if_changed(`${output}/manifest.js`, generate_client_manifest(manifest_data, base));
@@ -248,7 +247,7 @@ function create_types(config, manifest_data) {
 			.filter(Boolean)
 			.join(', ');
 
-		const file = `${SVELTE_KIT}/types/${key || 'index'}.d.ts`;
+		const file = `${config.kit.outDir}/types/${key || 'index'}.d.ts`;
 		const content = [
 			'// this file is auto-generated',
 			`import type { ${imports} } from '@sveltejs/kit';`,
diff --git a/packages/kit/src/core/create_manifest_data/index.js b/packages/kit/src/core/create_manifest_data/index.js
index de72e262c..3c83f4d39 100644
--- a/packages/kit/src/core/create_manifest_data/index.js
+++ b/packages/kit/src/core/create_manifest_data/index.js
@@ -1,7 +1,7 @@
 import fs from 'fs';
 import path from 'path';
 import mime from 'mime';
-import { runtime } from '../utils.js';
+import { get_runtime_path } from '../utils.js';
 import { posixify } from '../../utils/filesystem.js';
 
 /**
@@ -37,7 +37,7 @@ const specials = new Set(['__layout', '__layout.reset', '__error']);
  */
 export default function create_manifest_data({
 	config,
-	fallback = `${runtime}/components`,
+	fallback = `${get_runtime_path(config)}/components`,
 	cwd = process.cwd()
 }) {
 	/**
diff --git a/packages/kit/src/core/dev/index.js b/packages/kit/src/core/dev/index.js
index f411b90a9..59f2c2442 100644
--- a/packages/kit/src/core/dev/index.js
+++ b/packages/kit/src/core/dev/index.js
@@ -3,8 +3,7 @@ import { svelte } from '@sveltejs/vite-plugin-svelte';
 import vite from 'vite';
 import { deep_merge } from '../../utils/object.js';
 import { print_config_conflicts } from '../config/index.js';
-import { SVELTE_KIT } from '../constants.js';
-import { copy_assets, get_aliases, runtime } from '../utils.js';
+import { copy_assets, get_aliases, get_runtime_path } from '../utils.js';
 import { create_plugin } from './plugin.js';
 import { generate_tsconfig } from '../tsconfig.js';
 
@@ -21,7 +20,7 @@ import { generate_tsconfig } from '../tsconfig.js';
 
 /** @param {Options} opts */
 export async function dev({ cwd, port, host, https, config }) {
-	copy_assets(`${SVELTE_KIT}/runtime`);
+	copy_assets(path.join(config.kit.outDir, 'runtime'));
 
 	generate_tsconfig(config);
 
@@ -34,8 +33,8 @@ export async function dev({ cwd, port, host, https, config }) {
 							config.kit.files.assets,
 							config.kit.files.lib,
 							config.kit.files.routes,
+							config.kit.outDir,
 							path.resolve(cwd, 'src'),
-							path.resolve(cwd, SVELTE_KIT),
 							path.resolve(cwd, 'node_modules'),
 							path.resolve(vite.searchForWorkspaceRoot(cwd), 'node_modules')
 						])
@@ -58,7 +57,7 @@ export async function dev({ cwd, port, host, https, config }) {
 			rollupOptions: {
 				// Vite dependency crawler needs an explicit JS entry point
 				// eventhough server otherwise works without it
-				input: `${runtime}/client/start.js`
+				input: `${get_runtime_path(config)}/client/start.js`
 			}
 		},
 		plugins: [
diff --git a/packages/kit/src/core/dev/plugin.js b/packages/kit/src/core/dev/plugin.js
index 5b3e8ffd3..f53877ea3 100644
--- a/packages/kit/src/core/dev/plugin.js
+++ b/packages/kit/src/core/dev/plugin.js
@@ -7,11 +7,12 @@ import { installFetch } from '../../install-fetch.js';
 import { create_app } from '../create_app/index.js';
 import create_manifest_data from '../create_manifest_data/index.js';
 import { getRequest, setResponse } from '../../node.js';
-import { SVELTE_KIT, SVELTE_KIT_ASSETS } from '../constants.js';
-import { get_mime_lookup, resolve_entry, runtime } from '../utils.js';
+import { SVELTE_KIT_ASSETS } from '../constants.js';
+import { get_mime_lookup, get_runtime_path, resolve_entry } from '../utils.js';
 import { coalesce_to_error } from '../../utils/error.js';
 import { load_template } from '../config/index.js';
 import { sequence } from '../../hooks.js';
+import { posixify } from '../../utils/filesystem.js';
 
 /**
  * @param {import('types').ValidatedConfig} config
@@ -19,6 +20,8 @@ import { sequence } from '../../hooks.js';
  * @returns {Promise<import('vite').Plugin>}
  */
 export async function create_plugin(config, cwd) {
+	const runtime = get_runtime_path(config);
+
 	/** @type {import('types').Handle} */
 	let amp;
 
@@ -228,9 +231,18 @@ export async function create_plugin(config, cwd) {
 							throw new Error('The serverFetch hook has been renamed to externalFetch.');
 						}
 
-						const root = (await vite.ssrLoadModule(`/${SVELTE_KIT}/generated/root.svelte`)).default;
+						// TODO the / prefix will probably fail if outDir is outside the cwd (which
+						// could be the case in a monorepo setup), but without it these modules
+						// can get loaded twice via different URLs, which causes failures. Might
+						// require changes to Vite to fix
+						const { default: root } = await vite.ssrLoadModule(
+							`/${posixify(path.relative(cwd, `${config.kit.outDir}/generated/root.svelte`))}`
+						);
+
 						const paths = await vite.ssrLoadModule(
-							process.env.BUNDLED ? `/${SVELTE_KIT}/runtime/paths.js` : `/@fs${runtime}/paths.js`
+							process.env.BUNDLED
+								? `/${posixify(path.relative(cwd, `${config.kit.outDir}/runtime/paths.js`))}`
+								: `/@fs${runtime}/paths.js`
 						);
 
 						paths.set_paths({
diff --git a/packages/kit/src/core/preview/index.js b/packages/kit/src/core/preview/index.js
index 11ec3dd66..59c5d1329 100644
--- a/packages/kit/src/core/preview/index.js
+++ b/packages/kit/src/core/preview/index.js
@@ -1,12 +1,12 @@
 import fs from 'fs';
 import http from 'http';
 import https from 'https';
-import { join, resolve } from 'path';
+import { join } from 'path';
 import sirv from 'sirv';
 import { pathToFileURL } from 'url';
 import { getRequest, setResponse } from '../../node.js';
 import { installFetch } from '../../install-fetch.js';
-import { SVELTE_KIT, SVELTE_KIT_ASSETS } from '../constants.js';
+import { SVELTE_KIT_ASSETS } from '../constants.js';
 
 /** @param {string} dir */
 const mutable = (dir) =>
@@ -24,17 +24,11 @@ const mutable = (dir) =>
  *   cwd?: string;
  * }} opts
  */
-export async function preview({
-	port,
-	host,
-	config,
-	https: use_https = false,
-	cwd = process.cwd()
-}) {
+export async function preview({ port, host, config, https: use_https = false }) {
 	installFetch();
 
-	const index_file = resolve(cwd, `${SVELTE_KIT}/output/server/index.js`);
-	const manifest_file = resolve(cwd, `${SVELTE_KIT}/output/server/manifest.js`);
+	const index_file = join(config.kit.outDir, 'output/server/index.js');
+	const manifest_file = join(config.kit.outDir, 'output/server/manifest.js');
 
 	/** @type {import('types').ServerModule} */
 	const { Server, override } = await import(pathToFileURL(index_file).href);
@@ -49,7 +43,7 @@ export async function preview({
 				return next();
 		  };
 
-	const assets_handler = sirv(resolve(cwd, `${SVELTE_KIT}/output/client`), {
+	const assets_handler = sirv(join(config.kit.outDir, 'output/client'), {
 		maxAge: 31536000,
 		immutable: true
 	});
diff --git a/packages/kit/src/core/tsconfig.js b/packages/kit/src/core/tsconfig.js
index 0767a87d5..5ecd6ee9f 100644
--- a/packages/kit/src/core/tsconfig.js
+++ b/packages/kit/src/core/tsconfig.js
@@ -2,25 +2,24 @@ import fs from 'fs';
 import path from 'path';
 import colors from 'kleur';
 import { mkdirp, posixify } from '../utils/filesystem.js';
-import { SVELTE_KIT } from './constants.js';
 
 /** @param {string} file */
 const exists = (file) => fs.existsSync(file) && file;
 
 /** @param {import('types').ValidatedConfig} config */
 export function generate_tsconfig(config) {
-	const out = path.resolve(SVELTE_KIT, 'tsconfig.json');
+	const out = path.join(config.kit.outDir, 'tsconfig.json');
 	const user_file = exists('tsconfig.json') || exists('jsconfig.json');
 
 	if (user_file) validate(config, out, user_file);
 
-	mkdirp(SVELTE_KIT);
+	mkdirp(config.kit.outDir);
 
 	/** @param {string} file */
 	const project_relative = (file) => posixify(path.relative('.', file));
 
 	/** @param {string} file */
-	const config_relative = (file) => posixify(path.relative(SVELTE_KIT, file));
+	const config_relative = (file) => posixify(path.relative(config.kit.outDir, file));
 
 	const dirs = new Set([
 		project_relative(path.dirname(config.kit.files.routes)),
@@ -36,7 +35,7 @@ export function generate_tsconfig(config) {
 	});
 
 	fs.writeFileSync(
-		`${SVELTE_KIT}/tsconfig.json`,
+		out,
 		JSON.stringify(
 			{
 				compilerOptions: {
@@ -117,7 +116,7 @@ function validate(config, out, user_file) {
 		}
 	} else {
 		let relative = posixify(path.relative('.', out));
-		if (relative.startsWith(SVELTE_KIT)) relative = './' + relative;
+		if (!relative.startsWith('./')) relative = './' + relative;
 
 		console.warn(
 			colors
diff --git a/packages/kit/src/core/utils.js b/packages/kit/src/core/utils.js
index bb778fea9..380f6bfaa 100644
--- a/packages/kit/src/core/utils.js
+++ b/packages/kit/src/core/utils.js
@@ -3,14 +3,14 @@ import path from 'path';
 import colors from 'kleur';
 import { copy } from '../utils/filesystem.js';
 import { fileURLToPath } from 'url';
-import { SVELTE_KIT } from './constants.js';
 
 const __filename = fileURLToPath(import.meta.url);
 const __dirname = path.dirname(__filename);
 
-export const runtime = process.env.BUNDLED
-	? posixify_path(path.resolve(`${SVELTE_KIT}/runtime`))
-	: posixify_path(fileURLToPath(new URL('../runtime', import.meta.url)));
+export const get_runtime_path = process.env.BUNDLED
+	? /** @param {import('types').ValidatedConfig} config */ (config) =>
+			posixify_path(path.join(config.kit.outDir, 'runtime'))
+	: () => posixify_path(fileURLToPath(new URL('../runtime', import.meta.url)));
 
 /** @param {string} str */
 function posixify_path(str) {
@@ -104,8 +104,8 @@ export function get_mime_lookup(manifest_data) {
 /** @param {import('types').ValidatedConfig} config */
 export function get_aliases(config) {
 	const alias = {
-		__GENERATED__: path.posix.resolve(`${SVELTE_KIT}/generated`),
-		$app: `${runtime}/app`,
+		__GENERATED__: path.posix.join(config.kit.outDir, 'generated'),
+		$app: `${get_runtime_path(config)}/app`,
 		$lib: config.kit.files.lib
 	};
 
diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts
index ff1a343b6..ca8065de6 100644
--- a/packages/kit/types/index.d.ts
+++ b/packages/kit/types/index.d.ts
@@ -55,6 +55,7 @@ export interface Config {
 			parameter?: string;
 			allowed?: string[];
 		};
+		outDir?: string;
 		package?: {
 			dir?: string;
 			emitTypes?: boolean;
@@ -115,8 +116,14 @@ export interface HandleError {
  *
  * Note that you can use [generated types](/docs/types#generated-types) instead of manually specifying the Params generic argument.
  */
-export interface Load<Params = Record<string, string>, Props = Record<string, any>> {
-	(input: LoadInput<Params>): MaybePromise<Either<Fallthrough, LoadOutput<Props>>>;
+export interface Load<
+	Params extends Record<string, string> = Record<string, string>,
+	InputProps extends Record<string, any> = Record<string, any>,
+	OutputProps extends Record<string, any> = InputProps
+> {
+	(input: LoadInput<Params, InputProps>): MaybePromise<
+		Either<Fallthrough, LoadOutput<OutputProps>>
+	>;
 }
 
 export interface Navigation {
diff --git a/packages/kit/types/private.d.ts b/packages/kit/types/private.d.ts
index 0e32d0512..48c0659f0 100644
--- a/packages/kit/types/private.d.ts
+++ b/packages/kit/types/private.d.ts
@@ -220,10 +220,10 @@ export type JSONValue =
 	| JSONValue[]
 	| JSONObject;
 
-export interface LoadInput<Params = Record<string, string>> {
+export interface LoadInput<Params = Record<string, string>, Props = Record<string, any>> {
 	url: URL;
 	params: Params;
-	props: Record<string, any>;
+	props: Props;
 	fetch(info: RequestInfo, init?: RequestInit): Promise<Response>;
 	session: App.Session;
 	stuff: Partial<App.Stuff>;
diff --git a/sites/kit.svelte.dev/src/hooks.js b/sites/kit.svelte.dev/src/hooks.js
new file mode 100644
index 000000000..6d7155550
--- /dev/null
+++ b/sites/kit.svelte.dev/src/hooks.js
@@ -0,0 +1,18 @@
+const redirects = {
+	'/docs/typescript': '/docs/types',
+	'/docs/amp': '/docs/seo#manual-setup-amp'
+};
+
+/** @type {import('@sveltejs/kit').Handle} */
+export function handle({ event, resolve }) {
+	if (event.url.pathname in redirects) {
+		return new Response(undefined, {
+			status: 308,
+			headers: {
+				location: redirects[event.url.pathname]
+			}
+		});
+	}
+
+	return resolve(event);
+}


Test patch:
diff --git a/packages/kit/test/apps/options/.gitignore b/packages/kit/test/apps/options/.gitignore
new file mode 100644
index 000000000..0eb299a59
--- /dev/null
+++ b/packages/kit/test/apps/options/.gitignore
@@ -0,0 +1,1 @@
+.custom-out-dir
\ No newline at end of file
diff --git a/packages/kit/test/apps/options/svelte.config.js b/packages/kit/test/apps/options/svelte.config.js
index 4d61d0503..a882a9b56 100644
--- a/packages/kit/test/apps/options/svelte.config.js
+++ b/packages/kit/test/apps/options/svelte.config.js
@@ -20,6 +20,7 @@ const config = {
 		appDir: '_wheee',
 		floc: true,
 		inlineStyleThreshold: 1024,
+		outDir: '.custom-out-dir',
 		trailingSlash: 'always',
 		vite: {
 			build: {
diff --git a/packages/kit/test/apps/options/tsconfig.json b/packages/kit/test/apps/options/tsconfig.json
index f8208f248..ea08cd531 100644
--- a/packages/kit/test/apps/options/tsconfig.json
+++ b/packages/kit/test/apps/options/tsconfig.json
@@ -7,5 +7,5 @@
 			"types": ["../../../types/internal"]
 		}
 	},
-	"extends": "./.svelte-kit/tsconfig.json"
+	"extends": "./.custom-out-dir/tsconfig.json"
 }