Repository: sveltejs/kit (js)

Issue:
Reserve `config` export
### Describe the problem

We've alluded to a vague plan to support route-level deployment config in the future — something like this...

```js
export const config = {
  runtime: 'edge'
};
```

...where `config` is a freeform (but serializable) object that gets passed to adapters so that they can use it to customise the deployment.

At the moment, you can export anything from a `+(layout|page)(.server)?.js` file. This means that adding a new _known_ export would be a breaking change, as someone might already be doing `export const config = {...}` for some other reason.

### Describe the proposed solution

Reserve the `config` name, i.e. throw an error if a route file exports a binding called `config`.

### Alternatives considered

Prevent _any_ unknown exports. This is safer, but might feel unduly restrictive.

### Importance

would make my life easier

### Additional Information

_No response_

Pull request:
Disallow unknown exports
Closes #7845, by disallowing all unknown exports. For a minority of apps, this will be a breaking change, which is solved easily by moving the offending exports into a non-route file and importing them.

A related breaking change is that prerendering will fail (whether or not you're actively using prerendering) if modules cannot be imported for analysis at build time (e.g. because a file references a missing variable or something). This is a good thing, and something we should have done before, since it prevents surprising runtime errors.

### 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
- [x] Run the tests with `pnpm test` and lint the project with `pnpm lint` and `pnpm check`

### Changesets
- [x] If your PR makes a change that should be noted in one or more packages' changelogs, generate a changeset by running `pnpm changeset` and following the prompts. All changesets should be `patch` until SvelteKit 1.0


Patch:
diff --git a/.changeset/friendly-jobs-perform.md b/.changeset/friendly-jobs-perform.md
new file mode 100644
index 000000000..357987901
--- /dev/null
+++ b/.changeset/friendly-jobs-perform.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+[breaking] disallow unknown exports (except when starting with an underscore) from `+(layout|page)(.server)?.js` and `+server.js` files
diff --git a/package.json b/package.json
index 5e0100d73..14558010b 100644
--- a/package.json
+++ b/package.json
@@ -5,6 +5,7 @@
 	"private": true,
 	"scripts": {
 		"test": "turbo run test --filter=./packages/*",
+		"test:vite-ecosystem-ci": "pnpm test --dir packages/kit",
 		"check": "turbo run check",
 		"lint": "turbo run lint",
 		"format": "turbo run format",
diff --git a/packages/kit/src/core/prerender/prerender.js b/packages/kit/src/core/prerender/prerender.js
index 0f4957a80..8d4471943 100644
--- a/packages/kit/src/core/prerender/prerender.js
+++ b/packages/kit/src/core/prerender/prerender.js
@@ -11,6 +11,11 @@ import { logger } from '../utils.js';
 import { load_config } from '../config/index.js';
 import { get_route_segments } from '../../utils/routing.js';
 import { get_option } from '../../runtime/server/utils.js';
+import {
+	validate_common_exports,
+	validate_page_server_exports,
+	validate_server_exports
+} from '../../utils/exports.js';
 
 const [, , client_out_dir, manifest_path, results_path, verbose, env] = process.argv;
 
@@ -357,34 +362,46 @@ export async function prerender() {
 	}
 
 	for (const route of manifest._.routes) {
-		try {
-			if (route.endpoint) {
-				const mod = await route.endpoint();
-				if (mod.prerender !== undefined) {
-					if (mod.prerender && (mod.POST || mod.PATCH || mod.PUT || mod.DELETE)) {
-						throw new Error(
-							`Cannot prerender a +server file with POST, PATCH, PUT, or DELETE (${route.id})`
-						);
-					}
-
-					prerender_map.set(route.id, mod.prerender);
+		if (route.endpoint) {
+			const mod = await route.endpoint();
+			if (mod.prerender !== undefined) {
+				validate_server_exports(mod, route.id);
+
+				if (mod.prerender && (mod.POST || mod.PATCH || mod.PUT || mod.DELETE)) {
+					throw new Error(
+						`Cannot prerender a +server file with POST, PATCH, PUT, or DELETE (${route.id})`
+					);
 				}
+
+				prerender_map.set(route.id, mod.prerender);
 			}
+		}
+
+		if (route.page) {
+			const nodes = await Promise.all(
+				[...route.page.layouts, route.page.leaf].map((n) => {
+					if (n !== undefined) return manifest._.nodes[n]();
+				})
+			);
+
+			const layouts = nodes.slice(0, -1);
+			const page = nodes.at(-1);
 
-			if (route.page) {
-				const nodes = await Promise.all(
-					[...route.page.layouts, route.page.leaf].map((n) => {
-						if (n !== undefined) return manifest._.nodes[n]();
-					})
-				);
-				const prerender = get_option(nodes, 'prerender') ?? false;
+			for (const layout of layouts) {
+				if (layout) {
+					validate_common_exports(layout.server, route.id);
+					validate_common_exports(layout.shared, route.id);
+				}
+			}
 
-				prerender_map.set(route.id, prerender);
+			if (page) {
+				validate_page_server_exports(page.server, route.id);
+				validate_common_exports(page.shared, route.id);
 			}
-		} catch (e) {
-			// We failed to import the module, which indicates it can't be prerendered
-			// TODO should we catch these? It's almost certainly a bug in the app
-			console.error(e);
+
+			const prerender = get_option(nodes, 'prerender') ?? false;
+
+			prerender_map.set(route.id, prerender);
 		}
 	}
 
diff --git a/packages/kit/src/exports/vite/dev/index.js b/packages/kit/src/exports/vite/dev/index.js
index 02056f176..e751c0b07 100644
--- a/packages/kit/src/exports/vite/dev/index.js
+++ b/packages/kit/src/exports/vite/dev/index.js
@@ -31,6 +31,9 @@ export async function dev(vite, vite_config, svelte_config) {
 	// @ts-expect-error
 	globalThis.__SVELTEKIT_BROWSER__ = false;
 
+	// @ts-expect-error
+	globalThis.__SVELTEKIT_DEV__ = true;
+
 	sync.init(svelte_config, vite_config.mode);
 
 	/** @type {import('types').Respond} */
@@ -102,6 +105,7 @@ export async function dev(vite, vite_config, svelte_config) {
 							module_nodes.push(module_node);
 
 							result.shared = module;
+							result.shared_id = node.shared;
 						}
 
 						if (node.server) {
@@ -163,7 +167,8 @@ export async function dev(vite, vite_config, svelte_config) {
 										const url = path.resolve(cwd, endpoint.file);
 										return await vite.ssrLoadModule(url);
 								  }
-								: null
+								: null,
+							endpoint_id: endpoint?.file
 						};
 					})
 				),
diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js
index 4d567052e..70035c426 100644
--- a/packages/kit/src/runtime/client/client.js
+++ b/packages/kit/src/runtime/client/client.js
@@ -30,6 +30,7 @@ import { stores } from './singletons.js';
 import { unwrap_promises } from '../../utils/promises.js';
 import * as devalue from 'devalue';
 import { INDEX_KEY, PRELOAD_PRIORITIES, SCROLL_KEY } from './constants.js';
+import { validate_common_exports } from '../../utils/exports.js';
 
 const routes = parse(nodes, server_loads, dictionary, matchers);
 
@@ -558,6 +559,10 @@ export function create_client({ target, base }) {
 
 		const node = await loader();
 
+		if (__SVELTEKIT_DEV__) {
+			validate_common_exports(node.shared);
+		}
+
 		if (node.shared?.load) {
 			/** @param {string[]} deps */
 			function depends(...deps) {
diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js
index 699932b58..0f6dcef96 100644
--- a/packages/kit/src/runtime/server/index.js
+++ b/packages/kit/src/runtime/server/index.js
@@ -17,6 +17,11 @@ import { redirect_json_response, render_data } from './data/index.js';
 import { add_cookies_to_headers, get_cookies } from './cookie.js';
 import { create_fetch } from './fetch.js';
 import { Redirect } from '../control.js';
+import {
+	validate_common_exports,
+	validate_page_server_exports,
+	validate_server_exports
+} from '../../utils/exports.js';
 import { error, json } from '../../exports/index.js';
 
 /* global __SVELTEKIT_ADAPTER_NAME__ */
@@ -188,10 +193,31 @@ export async function respond(request, options, state) {
 					options.manifest._.nodes[route.page.leaf]()
 				]);
 
+				if (__SVELTEKIT_DEV__) {
+					const layouts = nodes.slice(0, -1);
+					const page = nodes.at(-1);
+
+					for (const layout of layouts) {
+						if (layout) {
+							validate_common_exports(layout.server, /** @type {string} */ (layout.server_id));
+							validate_common_exports(layout.shared, /** @type {string} */ (layout.shared_id));
+						}
+					}
+
+					if (page) {
+						validate_page_server_exports(page.server, /** @type {string} */ (page.server_id));
+						validate_common_exports(page.shared, /** @type {string} */ (page.shared_id));
+					}
+				}
+
 				trailing_slash = get_option(nodes, 'trailingSlash');
 			} else if (route.endpoint) {
 				const node = await route.endpoint();
 				trailing_slash = node.trailingSlash;
+
+				if (__SVELTEKIT_DEV__) {
+					validate_server_exports(node, /** @type {string} */ (route.endpoint_id));
+				}
 			}
 
 			const normalized = normalize_path(url.pathname, trailing_slash ?? 'never');
diff --git a/packages/kit/src/utils/exports.js b/packages/kit/src/utils/exports.js
new file mode 100644
index 000000000..80de5c160
--- /dev/null
+++ b/packages/kit/src/utils/exports.js
@@ -0,0 +1,54 @@
+/**
+ * @param {string[]} expected
+ */
+function validator(expected) {
+	const set = new Set(expected);
+
+	/**
+	 * @param {any} module
+	 * @param {string} [route_id]
+	 */
+	function validate(module, route_id) {
+		if (!module) return;
+
+		for (const key in module) {
+			if (key[0] !== '_' && !set.has(key)) {
+				const valid = expected.join(', ');
+				throw new Error(
+					`Invalid export '${key}'${
+						route_id ? ` in ${route_id}` : ''
+					} (valid exports are ${valid}, or anything with a '_' prefix)`
+				);
+			}
+		}
+	}
+
+	return validate;
+}
+
+export const validate_common_exports = validator([
+	'load',
+	'prerender',
+	'csr',
+	'ssr',
+	'trailingSlash'
+]);
+
+export const validate_page_server_exports = validator([
+	'load',
+	'prerender',
+	'csr',
+	'ssr',
+	'actions',
+	'trailingSlash'
+]);
+
+export const validate_server_exports = validator([
+	'GET',
+	'POST',
+	'PATCH',
+	'PUT',
+	'DELETE',
+	'prerender',
+	'trailingSlash'
+]);
diff --git a/packages/kit/src/utils/exports.spec.js b/packages/kit/src/utils/exports.spec.js
new file mode 100644
index 000000000..d0f6603cb
--- /dev/null
+++ b/packages/kit/src/utils/exports.spec.js
@@ -0,0 +1,58 @@
+import { test } from 'uvu';
+import * as assert from 'uvu/assert';
+import {
+	validate_common_exports,
+	validate_page_server_exports,
+	validate_server_exports
+} from './exports.js';
+
+test('validates +layout.server.js, +layout.js, +page.js', () => {
+	validate_common_exports({
+		load: () => {}
+	});
+
+	validate_common_exports({
+		_unknown: () => {}
+	});
+
+	assert.throws(() => {
+		validate_common_exports({
+			actions: {}
+		});
+	}, /Invalid export 'actions' \(valid exports are load, prerender, csr, ssr, trailingSlash, or anything with a '_' prefix\)/);
+});
+
+test('validates +page.server.js', () => {
+	validate_page_server_exports({
+		load: () => {},
+		actions: {}
+	});
+
+	validate_page_server_exports({
+		_unknown: () => {}
+	});
+
+	assert.throws(() => {
+		validate_page_server_exports({
+			answer: 42
+		});
+	}, /Invalid export 'answer' \(valid exports are load, prerender, csr, ssr, actions, trailingSlash, or anything with a '_' prefix\)/);
+});
+
+test('validates +server.js', () => {
+	validate_server_exports({
+		GET: () => {}
+	});
+
+	validate_server_exports({
+		_unknown: () => {}
+	});
+
+	assert.throws(() => {
+		validate_server_exports({
+			answer: 42
+		});
+	}, /Invalid export 'answer' \(valid exports are GET, POST, PATCH, PUT, DELETE, prerender, trailingSlash, or anything with a '_' prefix\)/);
+});
+
+test.run();
diff --git a/packages/kit/types/internal.d.ts b/packages/kit/types/internal.d.ts
index dbcfc8be2..2bc94ac73 100644
--- a/packages/kit/types/internal.d.ts
+++ b/packages/kit/types/internal.d.ts
@@ -288,6 +288,7 @@ export interface SSRNode {
 
 	// store this in dev so we can print serialization errors
 	server_id?: string;
+	shared_id?: string;
 }
 
 export type SSRNodeLoader = () => Promise<SSRNode>;
@@ -346,6 +347,7 @@ export interface SSRRoute {
 	params: RouteParam[];
 	page: PageNodeIndexes | null;
 	endpoint: (() => Promise<SSREndpoint>) | null;
+	endpoint_id?: string;
 }
 
 export interface SSRState {


Test patch:
diff --git a/packages/kit/test/apps/basics/src/routes/errors/init-error-endpoint/+server.js b/packages/kit/test/apps/basics/src/routes/errors/init-error-endpoint/+server.js
index df7092c8e..1d7af62e5 100644
--- a/packages/kit/test/apps/basics/src/routes/errors/init-error-endpoint/+server.js
+++ b/packages/kit/test/apps/basics/src/routes/errors/init-error-endpoint/+server.js
@@ -1,7 +1,10 @@
+import { building } from '$app/environment';
 import { json } from '@sveltejs/kit';
 
-// @ts-expect-error
-thisvariableisnotdefined;
+if (!building) {
+	// @ts-expect-error
+	thisvariableisnotdefined;
+}
 
 export function GET() {
 	return json({