Repository: sveltejs/kit
Pull request #8838 (2023-02-01 19:21:09)

feat: allow `$app/paths` to be used without an app
This implements `$app/paths` as a virtual module which contains an initial value that can be overridden by the preview and dev servers. That allows components using `$app/paths` to work without setting up an entire application which makes things like component testing with Vitest, using Storybook, etc. easier.

I had to move `get_option` because it was used by `analyse.js` and thus importing the whole runtime into the Vite config file

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

Latest commit: e4b89eb77dc56820fead0b631508545d60012126

**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/sveltejs/kit/new/virtual-paths?filename=.changeset/fluffy-melons-hear.md&value=---%0A%22%40sveltejs%2Fkit%22%3A%20patch%0A---%0A%0Afeat%3A%20allow%20%60%24app%2Fpaths%60%20to%20be%20used%20without%20an%20app%0A)


Rich-Harris: The test failures look legit
Rich-Harris: (which is unsurprising, since the `set_paths(...)` calls were removed from `dev` and `preview`)

Patch:
diff --git a/.changeset/cyan-ants-float.md b/.changeset/cyan-ants-float.md
new file mode 100644
index 000000000..4a7193bc3
--- /dev/null
+++ b/.changeset/cyan-ants-float.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': minor
+---
+
+feat: allow $app/paths to be used without an app
diff --git a/packages/adapter-node/CHANGELOG.md b/packages/adapter-node/CHANGELOG.md
index ebfc54994..ad634033a 100644
--- a/packages/adapter-node/CHANGELOG.md
+++ b/packages/adapter-node/CHANGELOG.md
@@ -1,5 +1,11 @@
 # @sveltejs/adapter-node
 
+## 1.1.6
+
+### Patch Changes
+
+- fix: correctly redirect trailing slashes for `adapter-node` ([#8857](https://github.com/sveltejs/kit/pull/8857))
+
 ## 1.1.5
 
 ### Patch Changes
diff --git a/packages/adapter-node/ambient.d.ts b/packages/adapter-node/ambient.d.ts
index c424cbb1c..7d45ea6dc 100644
--- a/packages/adapter-node/ambient.d.ts
+++ b/packages/adapter-node/ambient.d.ts
@@ -8,7 +8,9 @@ declare module 'HANDLER' {
 
 declare module 'MANIFEST' {
 	import { SSRManifest } from '@sveltejs/kit';
+
 	export const manifest: SSRManifest;
+	export const prerendered: Set<string>;
 }
 
 declare module 'SERVER' {
diff --git a/packages/adapter-node/index.js b/packages/adapter-node/index.js
index 7dc13e34d..ac16008bc 100644
--- a/packages/adapter-node/index.js
+++ b/packages/adapter-node/index.js
@@ -1,5 +1,5 @@
-import { readFileSync, writeFileSync } from 'fs';
-import { fileURLToPath } from 'url';
+import { readFileSync, writeFileSync } from 'node:fs';
+import { fileURLToPath } from 'node:url';
 import { rollup } from 'rollup';
 import { nodeResolve } from '@rollup/plugin-node-resolve';
 import commonjs from '@rollup/plugin-commonjs';
@@ -39,7 +39,8 @@ export default function (opts = {}) {
 
 			writeFileSync(
 				`${tmp}/manifest.js`,
-				`export const manifest = ${builder.generateManifest({ relativePath: './' })};`
+				`export const manifest = ${builder.generateManifest({ relativePath: './' })};\n\n` +
+					`export const prerendered = new Set(${JSON.stringify(builder.prerendered.paths)});\n`
 			);
 
 			const pkg = JSON.parse(readFileSync('package.json', 'utf8'));
diff --git a/packages/adapter-node/package.json b/packages/adapter-node/package.json
index 7f3f829fb..e530fc192 100644
--- a/packages/adapter-node/package.json
+++ b/packages/adapter-node/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "@sveltejs/adapter-node",
-	"version": "1.1.5",
+	"version": "1.1.6",
 	"repository": {
 		"type": "git",
 		"url": "https://github.com/sveltejs/kit",
diff --git a/packages/adapter-node/src/handler.js b/packages/adapter-node/src/handler.js
index 8166e654f..650ab3ce6 100644
--- a/packages/adapter-node/src/handler.js
+++ b/packages/adapter-node/src/handler.js
@@ -1,11 +1,11 @@
 import './shims';
-import fs from 'fs';
-import path from 'path';
+import fs from 'node:fs';
+import path from 'node:path';
 import sirv from 'sirv';
-import { fileURLToPath } from 'url';
+import { fileURLToPath } from 'node:url';
 import { getRequest, setResponse } from '@sveltejs/kit/node';
 import { Server } from 'SERVER';
-import { manifest } from 'MANIFEST';
+import { manifest, prerendered } from 'MANIFEST';
 import { env } from 'ENV';
 
 /* global ENV_PREFIX */
@@ -44,8 +44,39 @@ function serve(path, client = false) {
 	);
 }
 
+// required because the static file server ignores trailing slashes
+/** @returns {import('polka').Middleware} */
+function serve_prerendered() {
+	const handler = serve(path.join(dir, 'prerendered'));
+
+	return (req, res, next) => {
+		let pathname = req.path;
+
+		try {
+			pathname = decodeURIComponent(pathname);
+		} catch {
+			// ignore invalid URI
+		}
+
+		if (prerendered.has(pathname)) {
+			return handler(req, res, next);
+		}
+
+		// remove or add trailing slash as appropriate
+		let location = pathname.at(-1) === '/' ? pathname.slice(0, -1) : pathname + '/';
+		if (prerendered.has(location)) {
+			const search = req.url.split('?')[1];
+			if (search) location += `?${search}`;
+			res.writeHead(308, { location }).end();
+		} else {
+			next();
+		}
+	};
+}
+
 /** @type {import('polka').Middleware} */
 const ssr = async (req, res) => {
+	/** @type {Request | undefined} */
 	let request;
 
 	try {
@@ -139,7 +170,7 @@ export const handler = sequence(
 	[
 		serve(path.join(dir, 'client'), true),
 		serve(path.join(dir, 'static')),
-		serve(path.join(dir, 'prerendered')),
+		serve_prerendered(),
 		ssr
 	].filter(Boolean)
 );
diff --git a/packages/kit/package.json b/packages/kit/package.json
index c0c49b882..d44fabdd8 100644
--- a/packages/kit/package.json
+++ b/packages/kit/package.json
@@ -60,7 +60,7 @@
 		"lint": "prettier --check . --config ../../.prettierrc --ignore-path .gitignore && eslint src/**",
 		"check": "tsc",
 		"check:all": "tsc && pnpm -r --filter=\"./**\" check",
-		"format": "pnpm lint --write",
+		"format": "prettier --write . --config ../../.prettierrc --ignore-path .gitignore",
 		"test": "pnpm test:unit && pnpm test:integration",
 		"test:integration": "pnpm -r --workspace-concurrency 1 --filter=\"./test/**\" test",
 		"test:cross-platform:dev": "pnpm -r --workspace-concurrency 1 --filter=\"./test/**\" test:cross-platform:dev",
diff --git a/packages/kit/src/constants.js b/packages/kit/src/constants.js
index 8dc9aab27..60c17e2be 100644
--- a/packages/kit/src/constants.js
+++ b/packages/kit/src/constants.js
@@ -1,5 +1,7 @@
-// in `vite dev` and `vite preview`, we use a fake asset path so that we can
-// serve local assets while verifying that requests are correctly prefixed
+/**
+ * A fake asset path used in `vite dev` and `vite preview`, so that we can
+ * serve local assets while verifying that requests are correctly prefixed
+ */
 export const SVELTE_KIT_ASSETS = '/_svelte_kit_assets';
 
 export const GENERATED_COMMENT = '// this file is generated — do not edit it\n';
diff --git a/packages/kit/src/core/postbuild/analyse.js b/packages/kit/src/core/postbuild/analyse.js
index 12f61b06f..7aef3c55f 100644
--- a/packages/kit/src/core/postbuild/analyse.js
+++ b/packages/kit/src/core/postbuild/analyse.js
@@ -1,6 +1,6 @@
 import { join } from 'node:path';
 import { pathToFileURL } from 'node:url';
-import { get_option } from '../../runtime/server/utils.js';
+import { get_option } from '../../utils/options.js';
 import {
 	validate_common_exports,
 	validate_page_server_exports,
diff --git a/packages/kit/src/core/postbuild/fallback.js b/packages/kit/src/core/postbuild/fallback.js
index 6d22c998b..de49c1681 100644
--- a/packages/kit/src/core/postbuild/fallback.js
+++ b/packages/kit/src/core/postbuild/fallback.js
@@ -15,9 +15,7 @@ installPolyfills();
 const server_root = join(config.outDir, 'output');
 
 /** @type {import('types').ServerInternalModule} */
-const { set_building, set_paths } = await import(
-	pathToFileURL(`${server_root}/server/internal.js`).href
-);
+const { set_building } = await import(pathToFileURL(`${server_root}/server/internal.js`).href);
 
 /** @type {import('types').ServerModule} */
 const { Server } = await import(pathToFileURL(`${server_root}/server/index.js`).href);
@@ -26,7 +24,6 @@ const { Server } = await import(pathToFileURL(`${server_root}/server/index.js`).
 const manifest = (await import(pathToFileURL(manifest_path).href)).manifest;
 
 set_building(true);
-set_paths(config.paths);
 
 const server = new Server(manifest);
 await server.init({ env: JSON.parse(env) });
diff --git a/packages/kit/src/core/postbuild/prerender.js b/packages/kit/src/core/postbuild/prerender.js
index feee804b8..515d815e3 100644
--- a/packages/kit/src/core/postbuild/prerender.js
+++ b/packages/kit/src/core/postbuild/prerender.js
@@ -100,8 +100,6 @@ async function prerender({ out, manifest_path, metadata, verbose, env }) {
 	/** @type {Map<string, string>} */
 	const saved = new Map();
 
-	internal.set_paths(config.paths);
-
 	const server = new Server(manifest);
 	await server.init({ env });
 
diff --git a/packages/kit/src/core/sync/write_server.js b/packages/kit/src/core/sync/write_server.js
index 395a80f08..c4e81d262 100644
--- a/packages/kit/src/core/sync/write_server.js
+++ b/packages/kit/src/core/sync/write_server.js
@@ -25,9 +25,8 @@ const server_template = ({
 	error_page
 }) => `
 import root from '../root.svelte';
-import { set_building, set_paths, set_private_env, set_public_env, set_version } from '${runtime_directory}/shared.js';
+import { set_assets, set_building, set_private_env, set_public_env, set_version } from '${runtime_directory}/shared.js';
 
-set_paths(${s(config.kit.paths)});
 set_version(${s(config.kit.version.name)});
 
 export const options = {
@@ -58,7 +57,7 @@ export function get_hooks() {
 	return ${hooks ? `import(${s(hooks)})` : '{}'};
 }
 
-export { set_building, set_paths, set_private_env, set_public_env };
+export { set_assets, set_building, set_private_env, set_public_env };
 `;
 
 // TODO need to re-run this whenever src/app.html or src/error.html are
diff --git a/packages/kit/src/exports/vite/dev/index.js b/packages/kit/src/exports/vite/dev/index.js
index 927bc921a..5d7ca57ff 100644
--- a/packages/kit/src/exports/vite/dev/index.js
+++ b/packages/kit/src/exports/vite/dev/index.js
@@ -438,20 +438,17 @@ export async function dev(vite, vite_config, svelte_config) {
 					return;
 				}
 
-				// we have to import `Server` before calling `set_paths`
+				// we have to import `Server` before calling `set_assets`
 				const { Server } = /** @type {import('types').ServerModule} */ (
 					await vite.ssrLoadModule(`${runtime_base}/server/index.js`)
 				);
 
-				const { set_paths, set_version, set_fix_stack_trace } =
+				const { set_assets, set_version, set_fix_stack_trace } =
 					/** @type {import('types').ServerInternalModule} */ (
 						await vite.ssrLoadModule(`${runtime_base}/shared.js`)
 					);
 
-				set_paths({
-					base: svelte_config.kit.paths.base,
-					assets
-				});
+				set_assets(assets);
 
 				set_version(svelte_config.kit.version.name);
 
diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js
index 6d7a639c0..fe600f3b4 100644
--- a/packages/kit/src/exports/vite/index.js
+++ b/packages/kit/src/exports/vite/index.js
@@ -22,6 +22,7 @@ import { get_config_aliases, get_env } from './utils.js';
 import { write_client_manifest } from '../../core/sync/write_client_manifest.js';
 import prerender from '../../core/postbuild/prerender.js';
 import analyse from '../../core/postbuild/analyse.js';
+import { s } from '../../utils/misc.js';
 
 export { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
 
@@ -253,9 +254,9 @@ function kit({ svelte_config }) {
 				new_config.build.ssr = !secondary_build_started;
 
 				new_config.define = {
-					__SVELTEKIT_ADAPTER_NAME__: JSON.stringify(kit.adapter?.name),
-					__SVELTEKIT_APP_VERSION_FILE__: JSON.stringify(`${kit.appDir}/version.json`),
-					__SVELTEKIT_APP_VERSION_POLL_INTERVAL__: JSON.stringify(kit.version.pollInterval),
+					__SVELTEKIT_ADAPTER_NAME__: s(kit.adapter?.name),
+					__SVELTEKIT_APP_VERSION_FILE__: s(`${kit.appDir}/version.json`),
+					__SVELTEKIT_APP_VERSION_POLL_INTERVAL__: s(kit.version.pollInterval),
 					__SVELTEKIT_DEV__: 'false',
 					__SVELTEKIT_EMBEDDED__: kit.embedded ? 'true' : 'false'
 				};
@@ -315,7 +316,9 @@ function kit({ svelte_config }) {
 
 		async resolveId(id) {
 			// treat $env/static/[public|private] as virtual
-			if (id.startsWith('$env/') || id === '$service-worker') return `\0${id}`;
+			if (id.startsWith('$env/') || id === '$internal/paths' || id === '$service-worker') {
+				return `\0${id}`;
+			}
 		},
 
 		async load(id, options) {
@@ -351,6 +354,15 @@ function kit({ svelte_config }) {
 					);
 				case '\0$service-worker':
 					return create_service_worker_module(svelte_config);
+				case '\0$internal/paths':
+					const { assets, base } = svelte_config.kit.paths;
+					return `export const base = ${s(base)};
+export let assets = ${s(assets)};
+
+/** @param {string} path */
+export function set_assets(path) {
+	assets = path;
+}`;
 			}
 		}
 	};
@@ -552,7 +564,7 @@ function kit({ svelte_config }) {
 			this.emitFile({
 				type: 'asset',
 				fileName: `${kit.appDir}/version.json`,
-				source: JSON.stringify({ version: kit.version.name })
+				source: s({ version: kit.version.name })
 			});
 		},
 
@@ -788,9 +800,9 @@ export const build = [];
 export const files = [
 	${create_assets(config)
 		.filter((asset) => config.kit.serviceWorker.files(asset.file))
-		.map((asset) => `${JSON.stringify(`${config.kit.paths.base}/${asset.file}`)}`)
+		.map((asset) => `${s(`${config.kit.paths.base}/${asset.file}`)}`)
 		.join(',\n\t\t\t\t')}
 ];
 export const prerendered = [];
-export const version = ${JSON.stringify(config.kit.version.name)};
+export const version = ${s(config.kit.version.name)};
 `;
diff --git a/packages/kit/src/exports/vite/preview/index.js b/packages/kit/src/exports/vite/preview/index.js
index 1ff2ffb75..f70024bce 100644
--- a/packages/kit/src/exports/vite/preview/index.js
+++ b/packages/kit/src/exports/vite/preview/index.js
@@ -37,14 +37,14 @@ export async function preview(vite, vite_config, svelte_config) {
 	const dir = join(svelte_config.kit.outDir, 'output/server');
 
 	/** @type {import('types').ServerInternalModule} */
-	const { set_paths } = await import(pathToFileURL(join(dir, 'internal.js')).href);
+	const { set_assets } = await import(pathToFileURL(join(dir, 'internal.js')).href);
 
 	/** @type {import('types').ServerModule} */
 	const { Server } = await import(pathToFileURL(join(dir, 'index.js')).href);
 
 	const { manifest } = await import(pathToFileURL(join(dir, 'manifest.js')).href);
 
-	set_paths({ base, assets });
+	set_assets(assets);
 
 	const server = new Server(manifest);
 	await server.init({
diff --git a/packages/kit/src/runtime/app/paths.js b/packages/kit/src/runtime/app/paths.js
index 5a433b6dd..31d76ad8d 100644
--- a/packages/kit/src/runtime/app/paths.js
+++ b/packages/kit/src/runtime/app/paths.js
@@ -1,1 +1,1 @@
-export { base, assets } from '../shared.js';
+export { base, assets } from '$internal/paths';
diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js
index 4db7fce48..85f979e90 100644
--- a/packages/kit/src/runtime/client/client.js
+++ b/packages/kit/src/runtime/client/client.js
@@ -26,6 +26,7 @@ import { parse } from './parse.js';
 
 import Root from '__GENERATED__/root.svelte';
 import { nodes, server_loads, dictionary, matchers, hooks } from '__CLIENT__/manifest.js';
+import { base } from '$internal/paths';
 import { HttpError, Redirect } from '../control.js';
 import { stores } from './singletons.js';
 import { unwrap_promises } from '../../utils/promises.js';
@@ -66,11 +67,10 @@ function update_scroll_positions(index) {
 /**
  * @param {{
  *   target: HTMLElement;
- *   base: string;
  * }} opts
  * @returns {import('./types').Client}
  */
-export function create_client({ target, base }) {
+export function create_client({ target }) {
 	const container = __SVELTEKIT_EMBEDDED__ ? target : document.documentElement;
 	/** @type {Array<((url: URL) => boolean)>} */
 	const invalidated = [];
diff --git a/packages/kit/src/runtime/client/start.js b/packages/kit/src/runtime/client/start.js
index 1083898a9..aed3870f4 100644
--- a/packages/kit/src/runtime/client/start.js
+++ b/packages/kit/src/runtime/client/start.js
@@ -1,23 +1,20 @@
 import { DEV } from 'esm-env';
 import { create_client } from './client.js';
 import { init } from './singletons.js';
-import { set_paths, set_version, set_public_env } from '../shared.js';
+import { set_assets, set_version, set_public_env } from '../shared.js';
 
 /**
  * @param {{
+ *   assets: string;
  *   env: Record<string, string>;
  *   hydrate: Parameters<import('./types').Client['_hydrate']>[0];
- *   paths: {
- *     assets: string;
- *     base: string;
- *   },
  *   target: HTMLElement;
  *   version: string;
  * }} opts
  */
-export async function start({ env, hydrate, paths, target, version }) {
+export async function start({ assets, env, hydrate, target, version }) {
 	set_public_env(env);
-	set_paths(paths);
+	set_assets(assets);
 	set_version(version);
 
 	if (DEV && target === document.body) {
@@ -27,8 +24,7 @@ export async function start({ env, hydrate, paths, target, version }) {
 	}
 
 	const client = create_client({
-		target,
-		base: paths.base
+		target
 	});
 
 	init({ client });
diff --git a/packages/kit/src/runtime/client/utils.js b/packages/kit/src/runtime/client/utils.js
index 8c924c35b..1c3c392af 100644
--- a/packages/kit/src/runtime/client/utils.js
+++ b/packages/kit/src/runtime/client/utils.js
@@ -1,6 +1,7 @@
 import { BROWSER, DEV } from 'esm-env';
 import { writable } from 'svelte/store';
-import { assets, version } from '../shared.js';
+import { assets } from '$internal/paths';
+import { version } from '../shared.js';
 import { PRELOAD_PRIORITIES } from './constants.js';
 
 /* global __SVELTEKIT_APP_VERSION_FILE__, __SVELTEKIT_APP_VERSION_POLL_INTERVAL__ */
diff --git a/packages/kit/src/runtime/server/fetch.js b/packages/kit/src/runtime/server/fetch.js
index 9bb5b1dc9..ebde07df2 100644
--- a/packages/kit/src/runtime/server/fetch.js
+++ b/packages/kit/src/runtime/server/fetch.js
@@ -1,6 +1,6 @@
 import * as set_cookie_parser from 'set-cookie-parser';
 import { respond } from './respond.js';
-import * as paths from '../shared.js';
+import * as paths from '$internal/paths';
 
 /**
  * @param {{
diff --git a/packages/kit/src/runtime/server/page/index.js b/packages/kit/src/runtime/server/page/index.js
index a4f79ebd0..3bcebe191 100644
--- a/packages/kit/src/runtime/server/page/index.js
+++ b/packages/kit/src/runtime/server/page/index.js
@@ -4,7 +4,6 @@ import { normalize_error } from '../../../utils/error.js';
 import { add_data_suffix } from '../../../utils/url.js';
 import { HttpError, Redirect } from '../../control.js';
 import {
-	get_option,
 	redirect_response,
 	static_error_page,
 	handle_error_and_jsonify,
@@ -19,6 +18,7 @@ import {
 import { load_data, load_server_data } from './load_data.js';
 import { render_response } from './render.js';
 import { respond_with_error } from './respond_with_error.js';
+import { get_option } from '../../../utils/options.js';
 
 /**
  * @param {import('types').RequestEvent} event
diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js
index 215ca2b3f..7ccacaed3 100644
--- a/packages/kit/src/runtime/server/page/render.js
+++ b/packages/kit/src/runtime/server/page/render.js
@@ -1,13 +1,14 @@
 import * as devalue from 'devalue';
 import { readable, writable } from 'svelte/store';
 import { DEV } from 'esm-env';
+import { assets, base } from '$internal/paths';
 import { hash } from '../../hash.js';
 import { serialize_data } from './serialize_data.js';
 import { s } from '../../../utils/misc.js';
 import { Csp } from './csp.js';
 import { uneval_action_response } from './actions.js';
 import { clarify_devalue_error } from '../utils.js';
-import { assets, base, version, public_env } from '../../shared.js';
+import { version, public_env } from '../../shared.js';
 import { text } from '../../../exports/index.js';
 
 // TODO rename this function/module
@@ -265,8 +266,8 @@ export async function render_response({
 
 	if (page_config.csr) {
 		const opts = [
+			`assets: ${s(assets)}`,
 			`env: ${s(public_env)}`,
-			`paths: ${s({ assets, base })}`,
 			`target: document.querySelector('[data-sveltekit-hydrate="${target}"]').parentNode`,
 			`version: ${s(version)}`
 		];
diff --git a/packages/kit/src/runtime/server/page/respond_with_error.js b/packages/kit/src/runtime/server/page/respond_with_error.js
index 61082761c..591100446 100644
--- a/packages/kit/src/runtime/server/page/respond_with_error.js
+++ b/packages/kit/src/runtime/server/page/respond_with_error.js
@@ -2,11 +2,11 @@ import { render_response } from './render.js';
 import { load_data, load_server_data } from './load_data.js';
 import {
 	handle_error_and_jsonify,
-	get_option,
 	static_error_page,
 	redirect_response,
 	GENERIC_ERROR
 } from '../utils.js';
+import { get_option } from '../../../utils/options.js';
 import { HttpError, Redirect } from '../../control.js';
 
 /**
diff --git a/packages/kit/src/runtime/server/respond.js b/packages/kit/src/runtime/server/respond.js
index 1cd4047b0..7b2283630 100644
--- a/packages/kit/src/runtime/server/respond.js
+++ b/packages/kit/src/runtime/server/respond.js
@@ -1,10 +1,11 @@
 import { DEV } from 'esm-env';
+import { base } from '$internal/paths';
 import { is_endpoint_request, render_endpoint } from './endpoint.js';
 import { render_page } from './page/index.js';
 import { render_response } from './page/render.js';
 import { respond_with_error } from './page/respond_with_error.js';
 import { is_form_content_type } from '../../utils/http.js';
-import { GENERIC_ERROR, get_option, handle_fatal_error, redirect_response } from './utils.js';
+import { GENERIC_ERROR, handle_fatal_error, redirect_response } from './utils.js';
 import {
 	decode_pathname,
 	decode_params,
@@ -23,8 +24,8 @@ import {
 	validate_page_server_exports,
 	validate_server_exports
 } from '../../utils/exports.js';
+import { get_option } from '../../utils/options.js';
 import { error, json, text } from '../../exports/index.js';
-import * as paths from '../shared.js';
 
 /* global __SVELTEKIT_ADAPTER_NAME__ */
 
@@ -70,11 +71,11 @@ export async function respond(request, options, manifest, state) {
 	/** @type {Record<string, string>} */
 	let params = {};
 
-	if (paths.base && !state.prerendering?.fallback) {
-		if (!decoded.startsWith(paths.base)) {
+	if (base && !state.prerendering?.fallback) {
+		if (!decoded.startsWith(base)) {
 			return text('Not found', { status: 404 });
 		}
-		decoded = decoded.slice(paths.base.length) || '/';
+		decoded = decoded.slice(base.length) || '/';
 	}
 
 	const is_data_request = has_data_suffix(decoded);
diff --git a/packages/kit/src/runtime/server/utils.js b/packages/kit/src/runtime/server/utils.js
index 49f873e41..bb9bb65df 100644
--- a/packages/kit/src/runtime/server/utils.js
+++ b/packages/kit/src/runtime/server/utils.js
@@ -51,23 +51,6 @@ export function allowed_methods(mod) {
 	return allowed;
 }
 
-/**
- * @template {'prerender' | 'ssr' | 'csr' | 'trailingSlash'} Option
- * @template {Option extends 'prerender' ? import('types').PrerenderOption : Option extends 'trailingSlash' ? import('types').TrailingSlash : boolean} Value
- *
- * @param {Array<import('types').SSRNode | undefined>} nodes
- * @param {Option} option
- *
- * @returns {Value | undefined}
- */
-export function get_option(nodes, option) {
-	return nodes.reduce((value, node) => {
-		return /** @type {any} TypeScript's too dumb to understand this */ (
-			node?.universal?.[option] ?? node?.server?.[option] ?? value
-		);
-	}, /** @type {Value | undefined} */ (undefined));
-}
-
 /**
  * Return as a response that renders the error.html
  *
diff --git a/packages/kit/src/runtime/shared.js b/packages/kit/src/runtime/shared.js
index 0cb8fd91d..40c372a51 100644
--- a/packages/kit/src/runtime/shared.js
+++ b/packages/kit/src/runtime/shared.js
@@ -1,5 +1,5 @@
-export let assets = '';
-export let base = '';
+export { set_assets } from '$internal/paths';
+
 export let building = false;
 export let version = '';
 
@@ -12,12 +12,6 @@ export let public_env = {};
 /** @param {string} stack */
 export let fix_stack_trace = (stack) => stack;
 
-/** @param {{ base: string, assets: string }} paths */
-export function set_paths(paths) {
-	base = paths.base;
-	assets = paths.assets || base;
-}
-
 /** @param {boolean} value */
 export function set_building(value) {
 	building = value;
diff --git a/packages/kit/src/utils/options.js b/packages/kit/src/utils/options.js
new file mode 100644
index 000000000..a1c9088a4
--- /dev/null
+++ b/packages/kit/src/utils/options.js
@@ -0,0 +1,16 @@
+/**
+ * @template {'prerender' | 'ssr' | 'csr' | 'trailingSlash'} Option
+ * @template {Option extends 'prerender' ? import('types').PrerenderOption : Option extends 'trailingSlash' ? import('types').TrailingSlash : boolean} Value
+ *
+ * @param {Array<import('types').SSRNode | undefined>} nodes
+ * @param {Option} option
+ *
+ * @returns {Value | undefined}
+ */
+export function get_option(nodes, option) {
+	return nodes.reduce((value, node) => {
+		return /** @type {any} TypeScript's too dumb to understand this */ (
+			node?.universal?.[option] ?? node?.server?.[option] ?? value
+		);
+	}, /** @type {Value | undefined} */ (undefined));
+}
diff --git a/packages/kit/types/ambient.d.ts b/packages/kit/types/ambient.d.ts
index 474cd297b..79801c8a3 100644
--- a/packages/kit/types/ambient.d.ts
+++ b/packages/kit/types/ambient.d.ts
@@ -431,3 +431,10 @@ declare module '@sveltejs/kit/vite' {
 	export function sveltekit(): Promise<Plugin[]>;
 	export { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
 }
+
+/** Internal version of $app/paths */
+declare module '$internal/paths' {
+	export const base: `/${string}`;
+	export let assets: `https://${string}` | `http://${string}`;
+	export function set_assets(path: string): void;
+}
diff --git a/packages/kit/types/internal.d.ts b/packages/kit/types/internal.d.ts
index 17d03141f..0f053a167 100644
--- a/packages/kit/types/internal.d.ts
+++ b/packages/kit/types/internal.d.ts
@@ -1,4 +1,3 @@
-import { OutputChunk } from 'rollup';
 import { SvelteComponent } from 'svelte/internal';
 import {
 	Config,
@@ -30,7 +29,7 @@ export interface ServerModule {
 
 export interface ServerInternalModule {
 	set_building(building: boolean): void;
-	set_paths(paths: { base: string; assets: string }): void;
+	set_assets(path: string): void;
 	set_private_env(environment: Record<string, string>): void;
 	set_public_env(environment: Record<string, string>): void;
 	set_version(version: string): void;
diff --git a/sites/kit.svelte.dev/src/lib/docs/server/render.js b/sites/kit.svelte.dev/src/lib/docs/server/render.js
index e295909e2..d31435c50 100644
--- a/sites/kit.svelte.dev/src/lib/docs/server/render.js
+++ b/sites/kit.svelte.dev/src/lib/docs/server/render.js
@@ -69,7 +69,7 @@ export function replace_placeholders(content) {
 
 					let import_block = '';
 
-					if (module.exports.length > 0) {
+					if (module.exports.length > 0 && !module.name.startsWith('$internal')) {
 						// deduplication is necessary for now, because of `error()` overload
 						const exports = Array.from(new Set(module.exports.map((x) => x.name)));
 


Test patch:
diff --git a/packages/kit/test/apps/options/test/test.js b/packages/kit/test/apps/options/test/test.js
index 026d847f4..685c49400 100644
--- a/packages/kit/test/apps/options/test/test.js
+++ b/packages/kit/test/apps/options/test/test.js
@@ -42,7 +42,7 @@ test.describe('base path', () => {
 		});
 	}
 
-	test('sets_paths', async ({ page }) => {
+	test('paths available on server side', async ({ page }) => {
 		await page.goto('/path-base/base/');
 		expect(await page.textContent('[data-source="base"]')).toBe('/path-base');
 		expect(await page.textContent('[data-source="assets"]')).toBe('/_svelte_kit_assets');