Repository: sveltejs/kit (js)

Issue:
Include FetchEvent (waitUntil) in platform data for Vercel edge adapter
### Describe the problem

No access to `event.waitUntil` when deploying to vercel edge.

I'm using the vercel adapter (edge mode) and trying to send trace data from `hooks.server.ts` without blocking the response. With the cloudflare adapter I can use `platform.context.waitUntil(sendTraceData)` to keep the process alive until the trace data is sent. 

### Describe the proposed solution

Have the FetchEvent (waitUntil) exposed in vercel adapter (in edge mode) to match capabilities of cloudflare adapters.



### Alternatives considered

- block the user response
- fetch and hope the runtime doesn't exit prematurely
- use cloudflare instead

### Importance

would make my life easier

### Additional Information

Vercel Build Output API (V3) has a link to an example function with an event argument. So I'm guessing it should be possible for svelte kit to expose it. Also Nextjs has it I think

https://github.com/vercel/examples/blob/f52a98fb2065e2ee75ae9cc59c9c2d3a612e6c07/build-output-api/edge-functions/.vercel/output/functions/index.func/index.js#L1

Pull request description:
provide access to request context via `event.platform`
closes #8706. If you have `adapter-vercel` installed, you'll get correct typings for `event.platform` automatically.

### 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.
- [ ] 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
- [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. Changesets that add features should be `minor` and those that fix bugs should be `patch`. Please prefix changeset messages with `feat:`, `fix:`, or `chore:`.


Patch:
diff --git a/.changeset/nasty-guests-sing.md b/.changeset/nasty-guests-sing.md
deleted file mode 100644
index 755ce0018..000000000
--- a/.changeset/nasty-guests-sing.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@sveltejs/kit': patch
----
-
-fix: successive optional route parameters can now be empty
diff --git a/.changeset/polite-files-relate.md b/.changeset/polite-files-relate.md
new file mode 100644
index 000000000..8ad02e77d
--- /dev/null
+++ b/.changeset/polite-files-relate.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/adapter-vercel': minor
+---
+
+feat: provide access to request context via `event.platform`
diff --git a/.changeset/thin-readers-speak.md b/.changeset/thin-readers-speak.md
new file mode 100644
index 000000000..6e2146a5b
--- /dev/null
+++ b/.changeset/thin-readers-speak.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+fix: preserve form state when submitting a second time
diff --git a/packages/adapter-node/CHANGELOG.md b/packages/adapter-node/CHANGELOG.md
index 3d1d0fa5e..b36e924d1 100644
--- a/packages/adapter-node/CHANGELOG.md
+++ b/packages/adapter-node/CHANGELOG.md
@@ -1,5 +1,14 @@
 # @sveltejs/adapter-node
 
+## 1.2.1
+
+### Patch Changes
+
+- fix: use `node` export condition when bundling ([#9276](https://github.com/sveltejs/kit/pull/9276))
+
+- Updated dependencies [[`1e2c8269`](https://github.com/sveltejs/kit/commit/1e2c82697d501c88c6027722ea9b106e09a0eb6d)]:
+  - @sveltejs/kit@1.9.3
+
 ## 1.2.0
 
 ### Minor Changes
diff --git a/packages/adapter-node/index.js b/packages/adapter-node/index.js
index 607e3a817..d5fa532df 100644
--- a/packages/adapter-node/index.js
+++ b/packages/adapter-node/index.js
@@ -57,7 +57,14 @@ export default function (opts = {}) {
 					// dependencies could have deep exports, so we need a regex
 					...Object.keys(pkg.dependencies || {}).map((d) => new RegExp(`^${d}(\\/.*)?$`))
 				],
-				plugins: [nodeResolve({ preferBuiltins: true }), commonjs({ strictRequires: true }), json()]
+				plugins: [
+					nodeResolve({
+						preferBuiltins: true,
+						exportConditions: ['node']
+					}),
+					commonjs({ strictRequires: true }),
+					json()
+				]
 			});
 
 			await bundle.write({
diff --git a/packages/adapter-node/package.json b/packages/adapter-node/package.json
index ef5edbd85..f60833df1 100644
--- a/packages/adapter-node/package.json
+++ b/packages/adapter-node/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "@sveltejs/adapter-node",
-	"version": "1.2.0",
+	"version": "1.2.1",
 	"repository": {
 		"type": "git",
 		"url": "https://github.com/sveltejs/kit",
diff --git a/packages/adapter-vercel/ambient.d.ts b/packages/adapter-vercel/ambient.d.ts
index 537f7cc04..a106f64e3 100644
--- a/packages/adapter-vercel/ambient.d.ts
+++ b/packages/adapter-vercel/ambient.d.ts
@@ -1,8 +1,12 @@
-declare module 'SERVER' {
-	export { Server } from '@sveltejs/kit';
-}
+import { RequestContext } from './index.js';
 
-declare module 'MANIFEST' {
-	import { SSRManifest } from '@sveltejs/kit';
-	export const manifest: SSRManifest;
+declare global {
+	namespace App {
+		export interface Platform {
+			/**
+			 * `context` is only available in Edge Functions
+			 */
+			context?: RequestContext;
+		}
+	}
 }
diff --git a/packages/adapter-vercel/files/edge.js b/packages/adapter-vercel/files/edge.js
index b7d58f9c9..ce603b6e3 100644
--- a/packages/adapter-vercel/files/edge.js
+++ b/packages/adapter-vercel/files/edge.js
@@ -8,12 +8,16 @@ const initialized = server.init({
 
 /**
  * @param {Request} request
+ * @param {import('../index.js').RequestContext} context
  */
-export default async (request) => {
+export default async (request, context) => {
 	await initialized;
 	return server.respond(request, {
 		getClientAddress() {
 			return /** @type {string} */ (request.headers.get('x-forwarded-for'));
+		},
+		platform: {
+			context
 		}
 	});
 };
diff --git a/packages/adapter-vercel/index.d.ts b/packages/adapter-vercel/index.d.ts
index 6f95260b4..ea45bf4e0 100644
--- a/packages/adapter-vercel/index.d.ts
+++ b/packages/adapter-vercel/index.d.ts
@@ -1,4 +1,5 @@
 import { Adapter } from '@sveltejs/kit';
+import './ambient.js';
 
 export default function plugin(config?: Config): Adapter;
 
@@ -77,3 +78,59 @@ export interface EdgeConfig {
 }
 
 export type Config = EdgeConfig | ServerlessConfig;
+
+// we copy the RequestContext interface from `@vercel/edge` because that package can't co-exist with `@types/node`.
+// see https://github.com/sveltejs/kit/pull/9280#issuecomment-1452110035
+
+/**
+ * An extension to the standard `Request` object that is passed to every Edge Function.
+ *
+ * @example
+ * ```ts
+ * import type { RequestContext } from '@vercel/edge';
+ *
+ * export default async function handler(request: Request, ctx: RequestContext): Promise<Response> {
+ *   // ctx is the RequestContext
+ * }
+ * ```
+ */
+export interface RequestContext {
+	/**
+	 * A method that can be used to keep the function running after a response has been sent.
+	 * This is useful when you have an async task that you want to keep running even after the
+	 * response has been sent and the request has ended.
+	 *
+	 * @example
+	 *
+	 * <caption>Sending an internal error to an error tracking service</caption>
+	 *
+	 * ```ts
+	 * import type { RequestContext } from '@vercel/edge';
+	 *
+	 * export async function handleRequest(request: Request, ctx: RequestContext): Promise<Response> {
+	 *  try {
+	 *    return await myFunctionThatReturnsResponse();
+	 *  } catch (e) {
+	 *    ctx.waitUntil((async () => {
+	 *      // report this error to your error tracking service
+	 *      await fetch('https://my-error-tracking-service.com', {
+	 *        method: 'POST',
+	 *        body: JSON.stringify({
+	 *          stack: e.stack,
+	 *          message: e.message,
+	 *          name: e.name,
+	 *          url: request.url,
+	 *        }),
+	 *      });
+	 *    })());
+	 *    return new Response('Internal Server Error', { status: 500 });
+	 *  }
+	 * }
+	 * ```
+	 */
+	waitUntil(
+		/**
+		 * A promise that will be kept alive until it resolves or rejects.
+		 */ promise: Promise<unknown>
+	): void;
+}
diff --git a/packages/adapter-vercel/internal.d.ts b/packages/adapter-vercel/internal.d.ts
new file mode 100644
index 000000000..537f7cc04
--- /dev/null
+++ b/packages/adapter-vercel/internal.d.ts
@@ -0,0 +1,8 @@
+declare module 'SERVER' {
+	export { Server } from '@sveltejs/kit';
+}
+
+declare module 'MANIFEST' {
+	import { SSRManifest } from '@sveltejs/kit';
+	export const manifest: SSRManifest;
+}
diff --git a/packages/adapter-vercel/tsconfig.json b/packages/adapter-vercel/tsconfig.json
index ddff01c72..f93d6d1bb 100644
--- a/packages/adapter-vercel/tsconfig.json
+++ b/packages/adapter-vercel/tsconfig.json
@@ -14,5 +14,5 @@
 			"@sveltejs/kit": ["../kit/types/index"]
 		}
 	},
-	"include": ["**/*.js", "ambient.d.ts"]
+	"include": ["**/*.js", "index.d.ts", "internal.d.ts"]
 }
diff --git a/packages/kit/CHANGELOG.md b/packages/kit/CHANGELOG.md
index 4e80516d4..a3cf7ce65 100644
--- a/packages/kit/CHANGELOG.md
+++ b/packages/kit/CHANGELOG.md
@@ -1,5 +1,11 @@
 # @sveltejs/kit
 
+## 1.9.3
+
+### Patch Changes
+
+- fix: successive optional route parameters can now be empty ([#9266](https://github.com/sveltejs/kit/pull/9266))
+
 ## 1.9.2
 
 ### Patch Changes
diff --git a/packages/kit/package.json b/packages/kit/package.json
index a208af7db..ab9e74076 100644
--- a/packages/kit/package.json
+++ b/packages/kit/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "@sveltejs/kit",
-	"version": "1.9.2",
+	"version": "1.9.3",
 	"repository": {
 		"type": "git",
 		"url": "https://github.com/sveltejs/kit",
diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js
index 5795105db..11f83e40b 100644
--- a/packages/kit/src/runtime/client/client.js
+++ b/packages/kit/src/runtime/client/client.js
@@ -1410,14 +1410,19 @@ export function create_client(app, target) {
 				goto(result.location, { invalidateAll: true }, []);
 			} else {
 				/** @type {Record<string, any>} */
-				const props = {
-					form: result.data,
+				root.$set({
+					// this brings Svelte's view of the world in line with SvelteKit's
+					// after use:enhance reset the form....
+					form: null,
 					page: { ...page, form: result.data, status: result.status }
-				};
-				root.$set(props);
+				});
+
+				// ...so that setting the `form` prop takes effect and isn't ignored
+				await tick();
+				root.$set({ form: result.data });
 
 				if (result.type === 'success') {
-					tick().then(reset_focus);
+					reset_focus();
 				}
 			}
 		},


Test patch:
diff --git a/packages/kit/test/apps/basics/src/routes/actions/enhance/+page.server.js b/packages/kit/test/apps/basics/src/routes/actions/enhance/+page.server.js
index da8b3975d..db3fe786c 100644
--- a/packages/kit/test/apps/basics/src/routes/actions/enhance/+page.server.js
+++ b/packages/kit/test/apps/basics/src/routes/actions/enhance/+page.server.js
@@ -32,5 +32,12 @@ export const actions = {
 	},
 	error: () => {
 		throw error(400, 'error');
+	},
+	echo: async ({ request }) => {
+		const data = await request.formData();
+
+		return {
+			message: data.get('message')
+		};
 	}
 };
diff --git a/packages/kit/test/apps/basics/src/routes/actions/enhance/+page.svelte b/packages/kit/test/apps/basics/src/routes/actions/enhance/+page.svelte
index e3b46da4b..5c63d42a5 100644
--- a/packages/kit/test/apps/basics/src/routes/actions/enhance/+page.svelte
+++ b/packages/kit/test/apps/basics/src/routes/actions/enhance/+page.svelte
@@ -14,12 +14,14 @@
 <pre class="formdata2">{JSON.stringify($page.form)}</pre>
 
 <form method="post" action="?/login" use:enhance>
-	<input type=hidden name="action" value="DOM clobbering" />
-	<input type=hidden name="reset" value="DOM clobbering" />
+	<input type="hidden" name="action" value="DOM clobbering" />
+	<input type="hidden" name="reset" value="DOM clobbering" />
 	<input name="username" type="text" />
 	<button class="form1">Submit</button>
 	<button class="form1-register" formAction="?/register">Submit</button>
-	<button class="form1-submitter" formAction="?/submitter" name="submitter" value="foo">Submit</button>
+	<button class="form1-submitter" formAction="?/submitter" name="submitter" value="foo">
+		Submit
+	</button>
 	<button class="form1-error" formAction="?/error">Submit</button>
 </form>
 
@@ -35,3 +37,8 @@
 >
 	<button class="form2">Submit</button>
 </form>
+
+<form method="post" action="?/echo" use:enhance>
+	<input name="message" type="text" value={form?.message ?? ''} />
+	<button class="form3">Submit</button>
+</form>
diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js
index 91d8a4e59..3e242b9e7 100644
--- a/packages/kit/test/apps/basics/test/test.js
+++ b/packages/kit/test/apps/basics/test/test.js
@@ -976,7 +976,7 @@ test.describe('Actions', () => {
 		}
 	});
 
-	test('use:enhance button with formAction', async ({ page, app }) => {
+	test('use:enhance button with formAction', async ({ page }) => {
 		await page.goto('/actions/enhance');
 
 		expect(await page.textContent('pre.formdata1')).toBe(JSON.stringify(null));
@@ -989,7 +989,7 @@ test.describe('Actions', () => {
 		);
 	});
 
-	test('use:enhance button with name', async ({ page, app }) => {
+	test('use:enhance button with name', async ({ page }) => {
 		await page.goto('/actions/enhance');
 
 		expect(await page.textContent('pre.formdata1')).toBe(JSON.stringify(null));
@@ -1004,6 +1004,21 @@ test.describe('Actions', () => {
 		);
 	});
 
+	test('use:enhance does not clear form on second submit', async ({ page }) => {
+		await page.goto('/actions/enhance');
+
+		await page.locator('input[name="message"]').fill('hello');
+
+		await page.locator('.form3').click();
+		await expect(page.locator('pre.formdata1')).toHaveText(JSON.stringify({ message: 'hello' }));
+		await expect(page.locator('pre.formdata2')).toHaveText(JSON.stringify({ message: 'hello' }));
+
+		await page.locator('.form3').click();
+		await page.waitForTimeout(0); // wait for next tick
+		await expect(page.locator('pre.formdata1')).toHaveText(JSON.stringify({ message: 'hello' }));
+		await expect(page.locator('pre.formdata2')).toHaveText(JSON.stringify({ message: 'hello' }));
+	});
+
 	test('redirect', async ({ page, javaScriptEnabled }) => {
 		await page.goto('/actions/redirect');