Repository: sveltejs/kit (js)

Issue:
dynamic env is an empty object in production when using `config.kit.env.dir`
### Describe the bug

`config.kit.env.dir`, introduced in https://github.com/sveltejs/kit/pull/6175, only works in dev.

In dev, correct env is logged to console. In prod, empty object is logged to console. Minimal reproduction below.

### Reproduction

https://github.com/ambiguous48/dynamic-env-missing

### Logs

_No response_

### System Info

```shell
System:
    OS: Linux 5.18 Pop!_OS 22.04 LTS
    CPU: (24) x64 12th Gen Intel(R) Core(TM) i9-12900K
    Memory: 16.68 GB / 31.13 GB
    Container: Yes
    Shell: 5.8.1 - /usr/bin/zsh
  Binaries:
    Node: 18.3.0 - ~/.local/share/pnpm/node
    Yarn: 1.22.19 - ~/.local/share/pnpm/yarn
    npm: 8.11.0 - ~/.local/share/pnpm/npm
  Browsers:
    Chrome: 104.0.5112.79
    Firefox: 103.0
  npmPackages:
    @sveltejs/adapter-auto: next => 1.0.0-next.65 
    @sveltejs/kit: next => 1.0.0-next.431 
    svelte: ^3.44.0 => 3.49.0 
    vite: ^3.0.4 => 3.0.9
```


### Severity

annoyance

### Additional Information

_No response_

Pull request:
respect env.dir when running preview
closes #6204

### 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/forty-news-smell.md b/.changeset/forty-news-smell.md
new file mode 100644
index 000000000..6a71c37e1
--- /dev/null
+++ b/.changeset/forty-news-smell.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+Respect `config.kit.env.dir` when running `vite preview`
diff --git a/.changeset/tall-boxes-develop.md b/.changeset/tall-boxes-develop.md
new file mode 100644
index 000000000..a47f65683
--- /dev/null
+++ b/.changeset/tall-boxes-develop.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+Use custom x-sveltekit-action header to route enhanced form submissions to +page.server.js over +server.js
diff --git a/documentation/docs/06-form-actions.md b/documentation/docs/06-form-actions.md
index d5fe40ad9..b11a2f74e 100644
--- a/documentation/docs/06-form-actions.md
+++ b/documentation/docs/06-form-actions.md
@@ -352,6 +352,18 @@ We can also implement progressive enhancement ourselves, without `use:enhance`,
 </form>
 ```
 
+If you have a `+server.js` alongside your `+page.server.js`, `fetch` requests will be routed there by default. To `POST` to an action in `+page.server.js` instead, use the custom `x-sveltekit-action` header:
+
+```diff
+const response = await fetch(this.action, {
+	method: 'POST',
+	body: data,
++	headers: {
++		'x-sveltekit-action': 'true'
++	}
+});
+```
+
 ### Alternatives
 
 Form actions are the preferred way to send data to the server, since they can be progressively enhanced, but you can also use [`+server.js`](/docs/routing#server) files to expose (for example) a JSON API.
\ No newline at end of file
diff --git a/packages/kit/src/exports/vite/preview/index.js b/packages/kit/src/exports/vite/preview/index.js
index 0a24a0b9c..2948346e0 100644
--- a/packages/kit/src/exports/vite/preview/index.js
+++ b/packages/kit/src/exports/vite/preview/index.js
@@ -46,7 +46,7 @@ export async function preview(vite, vite_config, svelte_config) {
 
 	const server = new Server(manifest);
 	await server.init({
-		env: loadEnv(vite_config.mode, process.cwd(), '')
+		env: loadEnv(vite_config.mode, svelte_config.kit.env.dir, '')
 	});
 
 	return () => {
diff --git a/packages/kit/src/runtime/app/forms.js b/packages/kit/src/runtime/app/forms.js
index bf0601385..f55f3c837 100644
--- a/packages/kit/src/runtime/app/forms.js
+++ b/packages/kit/src/runtime/app/forms.js
@@ -73,7 +73,8 @@ export function enhance(form, submit = () => {}) {
 			const response = await fetch(action, {
 				method: 'POST',
 				headers: {
-					accept: 'application/json'
+					accept: 'application/json',
+					'x-sveltekit-action': 'true'
 				},
 				body: data,
 				signal: controller.signal
diff --git a/packages/kit/src/runtime/server/endpoint.js b/packages/kit/src/runtime/server/endpoint.js
index d47eb0c13..697b18b21 100644
--- a/packages/kit/src/runtime/server/endpoint.js
+++ b/packages/kit/src/runtime/server/endpoint.js
@@ -77,13 +77,16 @@ export async function render_endpoint(event, mod, state) {
  * @param {import('types').RequestEvent} event
  */
 export function is_endpoint_request(event) {
-	const { method } = event.request;
+	const { method, headers } = event.request;
 
 	if (method === 'PUT' || method === 'PATCH' || method === 'DELETE') {
 		// These methods exist exclusively for endpoints
 		return true;
 	}
 
+	// use:enhance uses a custom header to disambiguate
+	if (method === 'POST' && headers.get('x-sveltekit-action') === 'true') return false;
+
 	// GET/POST requests may be for endpoints or pages. We prefer endpoints if this isn't a text/html request
 	const accept = event.request.headers.get('accept') ?? '*/*';
 	return negotiate(accept, ['*', 'text/html']) !== 'text/html';


Test patch:
diff --git a/packages/kit/test/apps/basics/src/routes/routing/content-negotiation/+page.server.js b/packages/kit/test/apps/basics/src/routes/routing/content-negotiation/+page.server.js
new file mode 100644
index 000000000..da7d062a9
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/routing/content-negotiation/+page.server.js
@@ -0,0 +1,7 @@
+export const actions = {
+	default: () => {
+		return {
+			submitted: true
+		};
+	}
+};
diff --git a/packages/kit/test/apps/basics/src/routes/routing/endpoint-next-to-page/+page.svelte b/packages/kit/test/apps/basics/src/routes/routing/content-negotiation/+page.svelte
similarity index 59%
rename from packages/kit/test/apps/basics/src/routes/routing/endpoint-next-to-page/+page.svelte
rename to packages/kit/test/apps/basics/src/routes/routing/content-negotiation/+page.svelte
index b88ab6af7..8038c10f3 100644
--- a/packages/kit/test/apps/basics/src/routes/routing/endpoint-next-to-page/+page.svelte
+++ b/packages/kit/test/apps/basics/src/routes/routing/content-negotiation/+page.svelte
@@ -1,10 +1,15 @@
 <script>
+	import { enhance } from '$app/forms';
+
+	/** @type {import('./$types').ActionData} */
+	export let form;
+
 	let result;
 
 	/** @param {string} method */
 	async function request(method) {
 		result = 'loading';
-		const response = await fetch('/routing/endpoint-next-to-page', {method});
+		const response = await fetch('/routing/content-negotiation', { method });
 		result = await response.text();
 	}
 </script>
@@ -16,3 +21,9 @@
 <button on:click={() => request('POST')}>POST</button>
 <button on:click={() => request('DELETE')}>DELETE</button>
 <pre>{result}</pre>
+
+<form method="POST" use:enhance>
+	<button>Submit</button>
+</form>
+
+<p data-testid="form-result">form.submitted: {form?.submitted}</p>
diff --git a/packages/kit/test/apps/basics/src/routes/routing/endpoint-next-to-page/+server.js b/packages/kit/test/apps/basics/src/routes/routing/content-negotiation/+server.js
similarity index 100%
rename from packages/kit/test/apps/basics/src/routes/routing/endpoint-next-to-page/+server.js
rename to packages/kit/test/apps/basics/src/routes/routing/content-negotiation/+server.js
diff --git a/packages/kit/test/apps/basics/test/client.test.js b/packages/kit/test/apps/basics/test/client.test.js
index c350b161f..839354b49 100644
--- a/packages/kit/test/apps/basics/test/client.test.js
+++ b/packages/kit/test/apps/basics/test/client.test.js
@@ -900,15 +900,25 @@ test.describe('data-sveltekit attributes', () => {
 	});
 });
 
-test('+server.js next to +page.svelte works', async ({ page }) => {
-	await page.goto('/routing/endpoint-next-to-page');
-	expect(await page.textContent('p')).toBe('Hi');
-
-	for (const method of ['GET', 'PUT', 'PATCH', 'POST', 'DELETE']) {
-		await page.click(`button:has-text("${method}")`);
-		await page.waitForFunction(
-			(method) => document.querySelector('pre').textContent === method,
-			method
-		);
-	}
+test.describe('Content negotiation', () => {
+	test('+server.js next to +page.svelte works', async ({ page }) => {
+		await page.goto('/routing/content-negotiation');
+		expect(await page.textContent('p')).toBe('Hi');
+
+		for (const method of ['GET', 'PUT', 'PATCH', 'POST', 'DELETE']) {
+			await page.click(`button:has-text("${method}")`);
+			await page.waitForFunction(
+				(method) => document.querySelector('pre')?.textContent === method,
+				method
+			);
+		}
+	});
+
+	test('use:enhance uses action, not POST handler', async ({ page }) => {
+		await page.goto('/routing/content-negotiation');
+
+		page.click('button:has-text("Submit")');
+		await page.waitForResponse('/routing/content-negotiation');
+		await expect(page.locator('[data-testid="form-result"]')).toHaveText('form.submitted: true');
+	});
 });
diff --git a/packages/kit/test/apps/options-2/src/routes/env/+page.svelte b/packages/kit/test/apps/options-2/src/routes/env/+page.svelte
index a5ee26895..07cfc06fe 100644
--- a/packages/kit/test/apps/options-2/src/routes/env/+page.svelte
+++ b/packages/kit/test/apps/options-2/src/routes/env/+page.svelte
@@ -1,5 +1,7 @@
 <script>
 	import { PUBLIC_LOOK_IN_OPTIONS_2 } from '$env/static/public';
+	import { env } from '$env/dynamic/public';
 </script>
 
-<p>{PUBLIC_LOOK_IN_OPTIONS_2}</p>
+<p data-testid="static">static: {PUBLIC_LOOK_IN_OPTIONS_2}</p>
+<p data-testid="dynamic">dynamic: {env.PUBLIC_LOOK_IN_OPTIONS_2}</p>
diff --git a/packages/kit/test/apps/options-2/test/test.js b/packages/kit/test/apps/options-2/test/test.js
index 05a849daa..76056ef7f 100644
--- a/packages/kit/test/apps/options-2/test/test.js
+++ b/packages/kit/test/apps/options-2/test/test.js
@@ -8,7 +8,8 @@ test.describe.configure({ mode: 'parallel' });
 test.describe('env', () => {
 	test('resolves upwards', async ({ page }) => {
 		await page.goto('/basepath/env');
-		expect(await page.textContent('p')).toBe('resolves upwards!');
+		expect(await page.textContent('[data-testid="static"]')).toBe('static: resolves upwards!');
+		expect(await page.textContent('[data-testid="dynamic"]')).toBe('dynamic: resolves upwards!');
 	});
 });