From 1ad0d437e1ee08d1b65fc3a6a8d659adbc4a8724 Mon Sep 17 00:00:00 2001 From: Tee Ming Date: Fri, 7 Aug 2026 00:10:32 +0800 Subject: [PATCH 01/13] chore: get Vercel split platform tests running (#16666) Don't think these will run for us on the `version-3` branch unless they're also on the `main` branch --------- Co-authored-by: github-actions[bot] --- .github/workflows/platform-tests-vercel.yml | 26 ++++++++++++++++++- .../test/apps/split/package.json | 20 ++++++++++++++ .../test/apps/split/playwright.config.js | 1 + .../test/apps/split/src/app.d.ts | 13 ++++++++++ .../test/apps/split/src/app.html | 11 ++++++++ .../test/apps/split/src/routes/+layout.svelte | 5 ++++ .../test/apps/split/src/routes/+page.svelte | 5 ++++ .../test/apps/split/src/routes/a/+page.svelte | 1 + .../test/apps/split/test/test.ts | 8 ++++++ .../test/apps/split/tsconfig.json | 15 +++++++++++ .../test/apps/split/vite.config.ts | 13 ++++++++++ packages/kit/src/exports/env/index.js | 4 +-- packages/kit/types/index.d.ts | 19 +++++++------- pnpm-lock.yaml | 18 +++++++++++++ 14 files changed, 145 insertions(+), 14 deletions(-) create mode 100644 packages/adapter-vercel/test/apps/split/package.json create mode 100644 packages/adapter-vercel/test/apps/split/playwright.config.js create mode 100644 packages/adapter-vercel/test/apps/split/src/app.d.ts create mode 100644 packages/adapter-vercel/test/apps/split/src/app.html create mode 100644 packages/adapter-vercel/test/apps/split/src/routes/+layout.svelte create mode 100644 packages/adapter-vercel/test/apps/split/src/routes/+page.svelte create mode 100644 packages/adapter-vercel/test/apps/split/src/routes/a/+page.svelte create mode 100644 packages/adapter-vercel/test/apps/split/test/test.ts create mode 100644 packages/adapter-vercel/test/apps/split/tsconfig.json create mode 100644 packages/adapter-vercel/test/apps/split/vite.config.ts diff --git a/.github/workflows/platform-tests-vercel.yml b/.github/workflows/platform-tests-vercel.yml index a0a4d0c4a826..13411077f5d0 100644 --- a/.github/workflows/platform-tests-vercel.yml +++ b/.github/workflows/platform-tests-vercel.yml @@ -28,9 +28,10 @@ jobs: timeout-minutes: 15 environment: '@sveltejs/adapter-vercel platform tests' steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ inputs.sha || github.sha }} + persist-credentials: false - uses: ./.github/actions/vercel-deploy id: deploy @@ -43,3 +44,26 @@ jobs: with: test-app-dir: packages/adapter-vercel/test/apps/basic deployment-url: ${{ steps.deploy.outputs.deployment-url }} + + test-split: + if: github.repository == 'sveltejs/kit' + runs-on: ubuntu-latest + timeout-minutes: 15 + environment: '@sveltejs/adapter-vercel platform tests' + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ inputs.sha || github.sha }} + persist-credentials: false + + - uses: ./.github/actions/vercel-deploy + id: deploy + with: + vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID_SPLIT }} + vercel-token: ${{ secrets.VERCEL_TOKEN }} + vercel-org-id: ${{ secrets.VERCEL_ORG_ID }} + + - uses: ./.github/actions/platform-test + with: + test-app-dir: packages/adapter-vercel/test/apps/split + deployment-url: ${{ steps.deploy.outputs.deployment-url }} diff --git a/packages/adapter-vercel/test/apps/split/package.json b/packages/adapter-vercel/test/apps/split/package.json new file mode 100644 index 000000000000..130925759762 --- /dev/null +++ b/packages/adapter-vercel/test/apps/split/package.json @@ -0,0 +1,20 @@ +{ + "name": "test-vercel-split", + "version": "0.0.1", + "private": true, + "scripts": { + "dev": "vite dev", + "preview": "vite preview", + "build": "vite build", + "prepare": "svelte-kit sync || echo ''", + "test:platform": "playwright test" + }, + "devDependencies": { + "@sveltejs/kit": "workspace:*", + "@sveltejs/vite-plugin-svelte": "catalog:", + "svelte": "catalog:", + "typescript": "catalog:", + "vite": "catalog:" + }, + "type": "module" +} diff --git a/packages/adapter-vercel/test/apps/split/playwright.config.js b/packages/adapter-vercel/test/apps/split/playwright.config.js new file mode 100644 index 000000000000..33d36b651014 --- /dev/null +++ b/packages/adapter-vercel/test/apps/split/playwright.config.js @@ -0,0 +1 @@ +export { config as default } from '../../utils.js'; diff --git a/packages/adapter-vercel/test/apps/split/src/app.d.ts b/packages/adapter-vercel/test/apps/split/src/app.d.ts new file mode 100644 index 000000000000..da08e6da592d --- /dev/null +++ b/packages/adapter-vercel/test/apps/split/src/app.d.ts @@ -0,0 +1,13 @@ +// See https://svelte.dev/docs/kit/types#app.d.ts +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/packages/adapter-vercel/test/apps/split/src/app.html b/packages/adapter-vercel/test/apps/split/src/app.html new file mode 100644 index 000000000000..f273cc58f7eb --- /dev/null +++ b/packages/adapter-vercel/test/apps/split/src/app.html @@ -0,0 +1,11 @@ + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/packages/adapter-vercel/test/apps/split/src/routes/+layout.svelte b/packages/adapter-vercel/test/apps/split/src/routes/+layout.svelte new file mode 100644 index 000000000000..a54cfdcb7efa --- /dev/null +++ b/packages/adapter-vercel/test/apps/split/src/routes/+layout.svelte @@ -0,0 +1,5 @@ + + +{@render children()} diff --git a/packages/adapter-vercel/test/apps/split/src/routes/+page.svelte b/packages/adapter-vercel/test/apps/split/src/routes/+page.svelte new file mode 100644 index 000000000000..57d50b6f11b3 --- /dev/null +++ b/packages/adapter-vercel/test/apps/split/src/routes/+page.svelte @@ -0,0 +1,5 @@ +

Hello from SvelteKit on Vercel

+ + diff --git a/packages/adapter-vercel/test/apps/split/src/routes/a/+page.svelte b/packages/adapter-vercel/test/apps/split/src/routes/a/+page.svelte new file mode 100644 index 000000000000..445d9a0d621b --- /dev/null +++ b/packages/adapter-vercel/test/apps/split/src/routes/a/+page.svelte @@ -0,0 +1 @@ +

This route will be served by a different function because we use the split config

diff --git a/packages/adapter-vercel/test/apps/split/test/test.ts b/packages/adapter-vercel/test/apps/split/test/test.ts new file mode 100644 index 000000000000..95250e9e1d71 --- /dev/null +++ b/packages/adapter-vercel/test/apps/split/test/test.ts @@ -0,0 +1,8 @@ +import { expect, test } from '@playwright/test'; + +test('split functions work', async ({ page }) => { + await page.goto('/'); + await expect(page.locator('h1')).toContainText('Hello from SvelteKit on Vercel'); + await page.goto('/a'); + await expect(page.locator('p')).toContainText(/split config/); +}); diff --git a/packages/adapter-vercel/test/apps/split/tsconfig.json b/packages/adapter-vercel/test/apps/split/tsconfig.json new file mode 100644 index 000000000000..619fe2dd9d72 --- /dev/null +++ b/packages/adapter-vercel/test/apps/split/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "$app/tsconfig", + "compilerOptions": { + "rewriteRelativeImportExtensions": true, + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "moduleResolution": "bundler" + }, + "include": ["src", "test", "playwright.config.js", "vite.config.ts"] +} diff --git a/packages/adapter-vercel/test/apps/split/vite.config.ts b/packages/adapter-vercel/test/apps/split/vite.config.ts new file mode 100644 index 000000000000..0b40b6378475 --- /dev/null +++ b/packages/adapter-vercel/test/apps/split/vite.config.ts @@ -0,0 +1,13 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; +import adapter from '../../../index.js'; + +export default defineConfig({ + plugins: [ + sveltekit({ + adapter: adapter({ + split: true + }) + }) + ] +}); diff --git a/packages/kit/src/exports/env/index.js b/packages/kit/src/exports/env/index.js index 1f3e591e4b20..04e3ea2e0792 100644 --- a/packages/kit/src/exports/env/index.js +++ b/packages/kit/src/exports/env/index.js @@ -1,9 +1,7 @@ -/** @import { EnvVarConfig } from '@sveltejs/kit' */ - /** * Utility for defining [environment variables](https://svelte.dev/docs/kit/environment-variables), * which are made available via `$app/env/public` and `$app/env/private`. - * @template {Record>} T + * @template {Record>} T * @param {T} variables * @returns {T} */ diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 23f2d730e9b6..34252a7e66d0 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -3026,7 +3026,7 @@ declare module '@sveltejs/kit' { }>; export const VERSION: string; class HttpError_1 { - + constructor(status: number, body: { message: string; } extends App.Error ? (App.Error | string | undefined) : App.Error); @@ -3035,7 +3035,7 @@ declare module '@sveltejs/kit' { toString(): string; } class Redirect_1 { - + constructor(status: 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308, location: string); status: 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308; location: string; @@ -3046,12 +3046,11 @@ declare module '@sveltejs/kit' { } declare module '@sveltejs/kit/env' { - import type { EnvVarConfig } from '@sveltejs/kit'; /** * Utility for defining [environment variables](https://svelte.dev/docs/kit/environment-variables), * which are made available via `$app/env/public` and `$app/env/private`. * */ - export function defineEnvVars>>(variables: T): T; + export function defineEnvVars>>(variables: T): T; export {}; } @@ -3668,9 +3667,9 @@ declare module '$app/server' { * * */ function live(fn: (arg: void) => RemoteLiveQueryUserFunctionReturnType): RemoteLiveQueryFunction; - + function live(validate: "unchecked", fn: (arg: Input) => RemoteLiveQueryUserFunctionReturnType): RemoteLiveQueryFunction; - + function live(schema: Schema, fn: (arg: StandardSchemaV1.InferOutput) => RemoteLiveQueryUserFunctionReturnType): RemoteLiveQueryFunction, Output, StandardSchemaV1.InferOutput>; } /** @@ -3837,11 +3836,11 @@ declare module '$app/state' { declare module '$app/stores' { export function getStores(): { - + page: typeof page; - + navigating: typeof navigating; - + updated: typeof updated; }; /** @@ -4025,4 +4024,4 @@ declare module '$app/types' { export type Asset = ReturnType; } -//# sourceMappingURL=index.d.ts.map +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index afae32924842..bd8e015f1da0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -475,6 +475,24 @@ importers: specifier: 'catalog:' version: 6.4.3(@types/node@18.19.130)(jiti@2.4.2)(lightningcss@1.30.1)(yaml@2.9.0) + packages/adapter-vercel/test/apps/split: + devDependencies: + '@sveltejs/kit': + specifier: workspace:* + version: link:../../../../kit + '@sveltejs/vite-plugin-svelte': + specifier: 'catalog:' + version: 6.2.4(svelte@5.56.3(@typescript-eslint/types@8.61.1))(vite@6.4.3(@types/node@18.19.130)(jiti@2.4.2)(lightningcss@1.30.1)(yaml@2.9.0)) + svelte: + specifier: 'catalog:' + version: 5.56.3(@typescript-eslint/types@8.61.1) + typescript: + specifier: 'catalog:' + version: 6.0.2 + vite: + specifier: 'catalog:' + version: 6.4.3(@types/node@18.19.130)(jiti@2.4.2)(lightningcss@1.30.1)(yaml@2.9.0) + packages/amp: devDependencies: '@sveltejs/kit': From 6a1fc1229a937dc3984bd3823ca16bac2addcd18 Mon Sep 17 00:00:00 2001 From: Nic Polumeyv Date: Tue, 11 Aug 2026 18:20:33 -0400 Subject: [PATCH 02/13] docs: add remote functions note to form actions page (#16746) After seeing #16691 --- documentation/docs/20-core-concepts/30-form-actions.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/documentation/docs/20-core-concepts/30-form-actions.md b/documentation/docs/20-core-concepts/30-form-actions.md index be0af6c990d3..a8fad4043e41 100644 --- a/documentation/docs/20-core-concepts/30-form-actions.md +++ b/documentation/docs/20-core-concepts/30-form-actions.md @@ -6,6 +6,8 @@ A `+page.server.js` file can export _actions_, which allow you to `POST` data to When using `
`, client-side JavaScript is optional, but you can easily _progressively enhance_ your form interactions with JavaScript to provide the best user experience. +> [!NOTE] The experimental [`form` remote function](remote-functions#form) covers the same use cases as form actions, adding type safety and [single-flight mutations](remote-functions#Single-flight-mutations). Form actions are feature-complete and will continue to work, but new development is focused on remote functions, which are intended to become the recommended way to communicate with the server. Consider remote functions for new projects, keeping in mind that the API may change while the feature is experimental. + ## Default actions In the simplest case, a page declares a `default` action: From b61018d0526e827d1ef8cf8e21ef62b92230ae3d Mon Sep 17 00:00:00 2001 From: Elliott Johnson Date: Wed, 12 Aug 2026 17:05:18 -0600 Subject: [PATCH 03/13] fix: avoid eagerly reading `$app/state` dependencies during module initialization --- .changeset/calm-lions-smile.md | 5 +++++ packages/kit/src/runtime/app/state/client.js | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/calm-lions-smile.md diff --git a/.changeset/calm-lions-smile.md b/.changeset/calm-lions-smile.md new file mode 100644 index 000000000000..3c1b284ea85e --- /dev/null +++ b/.changeset/calm-lions-smile.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: avoid eagerly reading `$app/state` dependencies during module initialization diff --git a/packages/kit/src/runtime/app/state/client.js b/packages/kit/src/runtime/app/state/client.js index 8dc5ed4c858a..1849186baf5e 100644 --- a/packages/kit/src/runtime/app/state/client.js +++ b/packages/kit/src/runtime/app/state/client.js @@ -68,5 +68,5 @@ export const updated = { get current() { return _updated.current; }, - check: stores.updated.check + check: () => stores.updated.check() }; From 39e8e1fbd4feba7f22dd46bfdf7335362c38de16 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 18 Aug 2026 10:57:39 -0400 Subject: [PATCH 04/13] Version Packages (#16771) This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated. # Releases ## @sveltejs/kit@2.70.3 ### Patch Changes - fix: avoid eagerly reading `$app/state` dependencies during module initialization --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Elliott Johnson Co-authored-by: Conduitry --- .changeset/calm-lions-smile.md | 5 ----- packages/kit/CHANGELOG.md | 8 +++++++- packages/kit/package.json | 2 +- packages/kit/src/version.js | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) delete mode 100644 .changeset/calm-lions-smile.md diff --git a/.changeset/calm-lions-smile.md b/.changeset/calm-lions-smile.md deleted file mode 100644 index 3c1b284ea85e..000000000000 --- a/.changeset/calm-lions-smile.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@sveltejs/kit': patch ---- - -fix: avoid eagerly reading `$app/state` dependencies during module initialization diff --git a/packages/kit/CHANGELOG.md b/packages/kit/CHANGELOG.md index bf52b4706764..4ca33c4f7989 100644 --- a/packages/kit/CHANGELOG.md +++ b/packages/kit/CHANGELOG.md @@ -1,10 +1,16 @@ # @sveltejs/kit +## 2.70.3 + +### Patch Changes + +- fix: avoid eagerly reading `$app/state` dependencies during module initialization ([`b61018d`](https://github.com/sveltejs/kit/commit/b61018d0526e827d1ef8cf8e21ef62b92230ae3d)) + ## 2.70.2 ### Patch Changes -- fix: prevent quadratic backtracking in `Accept` header content negotiation ([#1](https://github.com/homebase-garage/igeclouds.github.io/pull/1)) +- fix: prevent quadratic backtracking in `Accept` header content negotiation ([`82712fc`](https://github.com/sveltejs/kit/commit/82712fc02c24b1dcf5b25d7a52129cd8455f04f5)) ## 2.70.1 diff --git a/packages/kit/package.json b/packages/kit/package.json index 6f9bf8cb5219..6d160bc6e7be 100644 --- a/packages/kit/package.json +++ b/packages/kit/package.json @@ -1,6 +1,6 @@ { "name": "@sveltejs/kit", - "version": "2.70.2", + "version": "2.70.3", "description": "SvelteKit is the fastest way to build Svelte apps", "keywords": [ "framework", diff --git a/packages/kit/src/version.js b/packages/kit/src/version.js index bc5cc8471af1..528f583b52e2 100644 --- a/packages/kit/src/version.js +++ b/packages/kit/src/version.js @@ -1,4 +1,4 @@ // generated during release, do not modify /** @type {string} */ -export const VERSION = '2.70.2'; +export const VERSION = '2.70.3'; From 228baee9f2d9434b0dde543059b7f5a7b848dbd6 Mon Sep 17 00:00:00 2001 From: Tee Ming Date: Tue, 8 Sep 2026 11:20:27 +0800 Subject: [PATCH 05/13] chore: run split edge platform tests (#17052) Applies the GitHub Actions changes from #17045 to ensure split edge functions are deployed and exercised by Netlify platform tests. --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: Nic Polumeyv Co-authored-by: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com> --- .github/actions/netlify-deploy/action.yml | 24 +++++++++++++++--- .github/workflows/platform-tests-netlify.yml | 26 ++++++++++++++++++++ 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/.github/actions/netlify-deploy/action.yml b/.github/actions/netlify-deploy/action.yml index 1d2cc2361b30..9ce82766cf26 100644 --- a/.github/actions/netlify-deploy/action.yml +++ b/.github/actions/netlify-deploy/action.yml @@ -28,7 +28,7 @@ runs: - name: Install Netlify CLI shell: bash - run: pnpm i -g netlify-cli + run: pnpm i -g netlify-cli --allow-build=esbuild --allow-build=netlify-cli - name: Deploy to Netlify id: deploy @@ -38,5 +38,23 @@ runs: NETLIFY_AUTH_TOKEN: ${{ inputs.netlify-token }} NETLIFY_PROJECT_ID: ${{ inputs.netlify-project-id }} run: | - TEST_APP_NAME=$(cat "$TEST_APP_DIR/package.json" | jq -r '.name') - echo "url=$(netlify deploy --json --filter="$TEST_APP_NAME" --dir="$TEST_APP_DIR/build" --functions=".netlify/v1/functions" --auth="$NETLIFY_AUTH_TOKEN" --site="$NETLIFY_PROJECT_ID" | jq -r '.deploy_url')" >> "$GITHUB_OUTPUT" + set -euo pipefail + + TEST_APP_NAME=$(jq -r '.name' "$TEST_APP_DIR/package.json") + + if deploy_json=$(netlify deploy \ + --json \ + --verbose \ + --filter="$TEST_APP_NAME" \ + --dir="$TEST_APP_DIR/build" \ + --functions=".netlify/v1/functions" \ + --auth="$NETLIFY_AUTH_TOKEN" \ + --site="$NETLIFY_PROJECT_ID" + ); then + deploy_url=$(jq -er '.deploy_url' <<< "$deploy_json") + echo "url=$deploy_url" >> "$GITHUB_OUTPUT" + else + echo "::error::Netlify deployment failed" + echo "$deploy_json" >&2 + exit 1 + fi diff --git a/.github/workflows/platform-tests-netlify.yml b/.github/workflows/platform-tests-netlify.yml index fdad830b8b75..9a43937b31dc 100644 --- a/.github/workflows/platform-tests-netlify.yml +++ b/.github/workflows/platform-tests-netlify.yml @@ -93,3 +93,29 @@ jobs: with: test-app-dir: ${{ env.TEST_APP_DIR }} deployment-url: ${{ steps.deploy.outputs.deployment-url }} + + test-split-edge: + if: github.repository == 'sveltejs/kit' + runs-on: ubuntu-latest + timeout-minutes: 15 + environment: '@sveltejs/adapter-netlify platform tests' + env: + TEST_APP_DIR: packages/adapter-netlify/test/apps/split + EDGE: 'true' + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ inputs.sha || github.sha }} + persist-credentials: false + + - uses: ./.github/actions/netlify-deploy + id: deploy + with: + test-app-dir: ${{ env.TEST_APP_DIR }} + netlify-project-id: ${{ secrets.NETLIFY_PROJECT_ID_SPLIT }} + netlify-token: ${{ secrets.NETLIFY_TOKEN }} + + - uses: ./.github/actions/platform-test + with: + test-app-dir: ${{ env.TEST_APP_DIR }} + deployment-url: ${{ steps.deploy.outputs.deployment-url }} From beff73719b7ae582a7731136a05c0a8259ef201d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 17 Sep 2026 12:40:03 -0400 Subject: [PATCH 06/13] docs: fix blockquote in adapter-node docs (#17121) this shouldn't have the lightbulb icon --- documentation/docs/25-build-and-deploy/40-adapter-node.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/docs/25-build-and-deploy/40-adapter-node.md b/documentation/docs/25-build-and-deploy/40-adapter-node.md index 18102d03d363..652034c1b059 100644 --- a/documentation/docs/25-build-and-deploy/40-adapter-node.md +++ b/documentation/docs/25-build-and-deploy/40-adapter-node.md @@ -100,7 +100,7 @@ PROTOCOL_HEADER=x-forwarded-proto HOST_HEADER=x-forwarded-host node build If `adapter-node` can't correctly determine the URL of your deployment, you may experience this error when using [form actions](form-actions): -> [!NOTE] Cross-site POST form submissions are forbidden +> Cross-site POST form submissions are forbidden ### `ADDRESS_HEADER` and `XFF_DEPTH` From 349cdc12284d66494d373f0f02b61528485feec3 Mon Sep 17 00:00:00 2001 From: Tee Ming Date: Sat, 19 Sep 2026 04:37:36 +0800 Subject: [PATCH 07/13] chore: separate platform test build and deploy stages (#17143) Same as #17122 but based onto `main` because our bot only runs the workflows on that branch. Also includes the applicable corrections from #17152. Netlify and Vercel use isolated build, deploy, and test jobs with provider secrets confined to deployment. Node keeps a separate local Playwright test action, while deployment tests use the renamed deployment-specific action. Netlify now builds its adapter before the test app. The Bun and Netlify instrumentation jobs removed from `main` are intentionally not restored. --- ### 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` Validated changed YAML with oxfmt 0.68.0, actionlint 1.7.12, YAML parsing, and `git diff --check`. The restored Node platform flow passes all 4 Playwright tests, and a fresh Netlify adapter plus test-app build succeeds. ### 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:`. No changeset is needed for CI workflow-only changes. ### Edits - [x] Please ensure that 'Allow edits from maintainers' is checked. PRs without this option may be closed. --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/actions/deployment-test/action.yml | 60 +++++++++ .github/actions/netlify-deploy/action.yml | 47 ++++--- .../{platform-test => node-test}/action.yml | 6 +- .github/actions/vercel-deploy/action.yml | 50 +++++++- .github/workflows/platform-test-netlify.yml | 109 ++++++++++++++++ .github/workflows/platform-test-vercel.yml | 106 ++++++++++++++++ .github/workflows/platform-tests-all.yml | 3 - .github/workflows/platform-tests-netlify.yml | 118 ++++-------------- .github/workflows/platform-tests-node.yml | 2 +- .github/workflows/platform-tests-vercel.yml | 60 +++------ 10 files changed, 393 insertions(+), 168 deletions(-) create mode 100644 .github/actions/deployment-test/action.yml rename .github/actions/{platform-test => node-test}/action.yml (75%) create mode 100644 .github/workflows/platform-test-netlify.yml create mode 100644 .github/workflows/platform-test-vercel.yml diff --git a/.github/actions/deployment-test/action.yml b/.github/actions/deployment-test/action.yml new file mode 100644 index 000000000000..edec7af3ba59 --- /dev/null +++ b/.github/actions/deployment-test/action.yml @@ -0,0 +1,60 @@ +name: 'Platform Test' +description: 'Run platform tests against a deployed test app' + +inputs: + sha: + description: 'Commit SHA to test' + required: true + platform: + description: 'Name of the deployment platform' + required: true + test-app-dir: + description: 'Path to the test app directory' + required: true + variant: + description: 'Name of the test app variant' + required: true + +runs: + using: 'composite' + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ inputs.sha }} + persist-credentials: false + path: .target + + - uses: pnpm/setup@703c52620218391530e48b9e8870d5c0082e1b9b # v2.1.0 + with: + runtime: node@24 + working-directory: .target + + - name: Install Playwright + shell: bash + working-directory: .target + run: pnpm playwright install chromium --no-shell + + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: platform-${{ inputs.platform }}-${{ inputs.variant }}-deployment-${{ github.run_id }} + path: ${{ runner.temp }}/deployment + + - name: Read deployment URL + id: deployment + shell: bash + run: echo "url=$(cat '${{ runner.temp }}/deployment/url')" >> "$GITHUB_OUTPUT" + + - name: Run Playwright tests + shell: bash + working-directory: .target/${{ inputs.test-app-dir }} + env: + DEPLOYMENT_URL: ${{ steps.deployment.outputs.url }} + run: pnpm test:platform + + - name: Upload test artifacts + if: failure() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: platform-test-results-${{ inputs.platform }}-${{ inputs.variant }} + path: .target/${{ inputs.test-app-dir }}/test-results + retention-days: 3 diff --git a/.github/actions/netlify-deploy/action.yml b/.github/actions/netlify-deploy/action.yml index 9ce82766cf26..abc0d747701f 100644 --- a/.github/actions/netlify-deploy/action.yml +++ b/.github/actions/netlify-deploy/action.yml @@ -1,9 +1,9 @@ name: 'Netlify Deploy' -description: 'Set up the repo and deploy a test app to Netlify' +description: 'Deploy a prebuilt test app to Netlify' inputs: - test-app-dir: - description: 'Path to the test app directory' + variant: + description: 'Name of the test app variant' required: true netlify-project-id: description: 'Netlify project ID' @@ -19,12 +19,15 @@ outputs: runs: using: 'composite' steps: - - uses: ./.github/actions/node-setup + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: platform-netlify-${{ inputs.variant }}-${{ github.run_id }} + path: ${{ runner.temp }}/platform-output - - name: Build adapter-netlify files - shell: bash - working-directory: packages/adapter-netlify - run: pnpm prepublishOnly + - uses: pnpm/setup@703c52620218391530e48b9e8870d5c0082e1b9b # v2.1.0 + with: + runtime: node@24 + cache: true - name: Install Netlify CLI shell: bash @@ -33,20 +36,21 @@ runs: - name: Deploy to Netlify id: deploy shell: bash + working-directory: ${{ runner.temp }}/platform-output env: - TEST_APP_DIR: ${{ inputs.test-app-dir }} NETLIFY_AUTH_TOKEN: ${{ inputs.netlify-token }} NETLIFY_PROJECT_ID: ${{ inputs.netlify-project-id }} run: | set -euo pipefail - TEST_APP_NAME=$(jq -r '.name' "$TEST_APP_DIR/package.json") - - if deploy_json=$(netlify deploy \ + if deploy_json=$(env \ + -u NETLIFY_AUTH_TOKEN \ + -u NETLIFY_PROJECT_ID \ + netlify deploy \ --json \ --verbose \ - --filter="$TEST_APP_NAME" \ - --dir="$TEST_APP_DIR/build" \ + --no-build \ + --dir="build" \ --functions=".netlify/v1/functions" \ --auth="$NETLIFY_AUTH_TOKEN" \ --site="$NETLIFY_PROJECT_ID" @@ -58,3 +62,18 @@ runs: echo "$deploy_json" >&2 exit 1 fi + + - name: Save deployment URL + shell: bash + env: + DEPLOYMENT_URL: ${{ steps.deploy.outputs.url }} + run: | + mkdir "${{ runner.temp }}/deployment" + printf '%s' "$DEPLOYMENT_URL" > "${{ runner.temp }}/deployment/url" + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: platform-netlify-${{ inputs.variant }}-deployment-${{ github.run_id }} + path: ${{ runner.temp }}/deployment/url + if-no-files-found: error + retention-days: 1 diff --git a/.github/actions/platform-test/action.yml b/.github/actions/node-test/action.yml similarity index 75% rename from .github/actions/platform-test/action.yml rename to .github/actions/node-test/action.yml index 0bf5a85f011e..6ffa25206bd0 100644 --- a/.github/actions/platform-test/action.yml +++ b/.github/actions/node-test/action.yml @@ -1,12 +1,10 @@ name: 'Platform Test' -description: 'Run Playwright tests against a deployed platform preview' +description: 'Run Playwright tests against a local build' inputs: test-app-dir: description: 'Path to the test app directory' required: true - deployment-url: - description: 'URL of the deployed preview to test against' runs: using: 'composite' @@ -18,8 +16,6 @@ runs: - name: Run Playwright tests shell: bash working-directory: ${{ inputs.test-app-dir }} - env: - DEPLOYMENT_URL: ${{ inputs.deployment-url }} run: pnpm test:platform - name: Upload test artifacts diff --git a/.github/actions/vercel-deploy/action.yml b/.github/actions/vercel-deploy/action.yml index ab48d1216a13..c86ba2d5da06 100644 --- a/.github/actions/vercel-deploy/action.yml +++ b/.github/actions/vercel-deploy/action.yml @@ -1,12 +1,12 @@ name: 'Vercel Deploy' -description: 'Set up the repo and deploy a test app to Vercel' +description: 'Deploy a prebuilt test app to Vercel' inputs: + variant: + description: 'Name of the test app variant' + required: true # Important: When setting up a new Vercel test app, make sure # to set the root directory in the dashboard to the correct path. - # The `vercel deploy` command runs from the root of the repository, - # so it has to be able to look up the test app's dir in order to know - # what to deploy. vercel-project-id: description: 'Vercel project ID for the test app' required: true @@ -16,6 +16,10 @@ inputs: vercel-org-id: description: 'Vercel organization ID' required: true + purge-cache: + description: 'Purge the Vercel CDN cache before testing' + required: false + default: 'false' outputs: deployment-url: description: 'URL of the deployed preview' @@ -24,7 +28,15 @@ outputs: runs: using: 'composite' steps: - - uses: ./.github/actions/node-setup + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: platform-vercel-${{ inputs.variant }}-${{ github.run_id }} + path: ${{ runner.temp }}/platform-output + + - uses: pnpm/setup@703c52620218391530e48b9e8870d5c0082e1b9b # v2.1.0 + with: + runtime: node@24 + cache: true - name: Install Vercel CLI shell: bash @@ -33,8 +45,34 @@ runs: - name: Deploy to Vercel id: deploy shell: bash + working-directory: ${{ runner.temp }}/platform-output + env: + VERCEL_TOKEN: ${{ inputs.vercel-token }} + VERCEL_ORG_ID: ${{ inputs.vercel-org-id }} + VERCEL_PROJECT_ID: ${{ inputs.vercel-project-id }} + run: echo "url=$(vercel deploy --prebuilt --yes --token="$VERCEL_TOKEN")" >> "$GITHUB_OUTPUT" + + # avoid stale cache results when testing ISR routes + - name: Purge Vercel CDN Cache + if: inputs.purge-cache == 'true' + shell: bash env: VERCEL_TOKEN: ${{ inputs.vercel-token }} VERCEL_ORG_ID: ${{ inputs.vercel-org-id }} VERCEL_PROJECT_ID: ${{ inputs.vercel-project-id }} - run: echo "url=$(vercel deploy --yes --token="$VERCEL_TOKEN")" >> "$GITHUB_OUTPUT" + run: vercel cache purge --type cdn --token="$VERCEL_TOKEN" --yes + + - name: Save deployment URL + shell: bash + env: + DEPLOYMENT_URL: ${{ steps.deploy.outputs.url }} + run: | + mkdir "${{ runner.temp }}/deployment" + printf '%s' "$DEPLOYMENT_URL" > "${{ runner.temp }}/deployment/url" + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: platform-vercel-${{ inputs.variant }}-deployment-${{ github.run_id }} + path: ${{ runner.temp }}/deployment/url + if-no-files-found: error + retention-days: 1 diff --git a/.github/workflows/platform-test-netlify.yml b/.github/workflows/platform-test-netlify.yml new file mode 100644 index 000000000000..043b316d54d4 --- /dev/null +++ b/.github/workflows/platform-test-netlify.yml @@ -0,0 +1,109 @@ +name: Platform Test (Netlify) + +on: + workflow_call: + inputs: + sha: + description: 'Commit SHA to test' + required: true + type: string + variant: + description: 'Name of the test app variant' + required: true + type: string + test-app-dir: + description: 'Path to the test app directory' + required: true + type: string + edge: + description: 'Build the test app with Netlify Edge Functions' + required: false + type: boolean + default: false + +permissions: + contents: read + +env: + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1' + +jobs: + build: + if: github.repository == 'sveltejs/kit' + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ inputs.sha }} + persist-credentials: false + + - uses: pnpm/setup@703c52620218391530e48b9e8870d5c0082e1b9b # v2.1.0 + with: + runtime: node@24 + + - name: Build adapter + run: pnpm --filter @sveltejs/adapter-netlify build + + - name: Build test app + working-directory: ${{ inputs.test-app-dir }} + env: + EDGE: ${{ inputs.edge }} + run: pnpm build + + - name: Stage deployment artifact + env: + TARGET_DIR: ${{ github.workspace }}/${{ inputs.test-app-dir }} + run: | + mkdir platform-output + cp -a "$TARGET_DIR/build" "$TARGET_DIR/.netlify" platform-output/ + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: platform-netlify-${{ inputs.variant }}-${{ github.run_id }} + path: platform-output + include-hidden-files: true + if-no-files-found: error + retention-days: 1 + + deploy: + if: github.repository == 'sveltejs/kit' + needs: build + runs-on: ubuntu-latest + timeout-minutes: 15 + environment: '@sveltejs/adapter-netlify platform tests' + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.workflow_sha }} + persist-credentials: false + sparse-checkout: | + .github/actions + pnpm-lock.yaml + path: .workflow + - uses: ./.workflow/.github/actions/netlify-deploy + with: + netlify-project-id: ${{ inputs.variant == 'basic' && secrets.NETLIFY_PROJECT_ID_BASIC || inputs.variant == 'edge' && secrets.NETLIFY_PROJECT_ID_EDGE || inputs.variant == 'instrumentation' && secrets.NETLIFY_PROJECT_ID_INSTRUMENTATION || secrets.NETLIFY_PROJECT_ID_SPLIT }} + netlify-token: ${{ secrets.NETLIFY_TOKEN }} + variant: ${{ inputs.variant }} + + test: + if: github.repository == 'sveltejs/kit' + needs: deploy + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.workflow_sha }} + persist-credentials: false + sparse-checkout: | + .github/actions + pnpm-lock.yaml + path: .workflow + - uses: ./.workflow/.github/actions/deployment-test + with: + sha: ${{ inputs.sha }} + platform: netlify + test-app-dir: ${{ inputs.test-app-dir }} + variant: ${{ inputs.variant }} diff --git a/.github/workflows/platform-test-vercel.yml b/.github/workflows/platform-test-vercel.yml new file mode 100644 index 000000000000..f547e8e8645b --- /dev/null +++ b/.github/workflows/platform-test-vercel.yml @@ -0,0 +1,106 @@ +name: Platform Test (Vercel) + +on: + workflow_call: + inputs: + sha: + description: 'Commit SHA to test' + required: true + type: string + variant: + description: 'Name of the test app variant' + required: true + type: string + test-app-dir: + description: 'Path to the test app directory' + required: true + type: string + purge-cache: + description: 'Purge the Vercel CDN cache before testing' + required: false + type: boolean + default: false + +permissions: + contents: read + +env: + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1' + +jobs: + build: + if: github.repository == 'sveltejs/kit' + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ inputs.sha }} + persist-credentials: false + + - uses: pnpm/setup@703c52620218391530e48b9e8870d5c0082e1b9b # v2.1.0 + with: + runtime: node@24 + + - name: Build test app + working-directory: ${{ inputs.test-app-dir }} + run: pnpm build + + - name: Stage deployment artifact + env: + TARGET_DIR: ${{ github.workspace }}/${{ inputs.test-app-dir }} + run: | + mkdir platform-output + cp -a "$TARGET_DIR/.vercel" platform-output/ + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: platform-vercel-${{ inputs.variant }}-${{ github.run_id }} + path: platform-output + include-hidden-files: true + if-no-files-found: error + retention-days: 1 + + deploy: + if: github.repository == 'sveltejs/kit' + needs: build + runs-on: ubuntu-latest + timeout-minutes: 15 + environment: '@sveltejs/adapter-vercel platform tests' + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.workflow_sha }} + persist-credentials: false + sparse-checkout: | + .github/actions + pnpm-lock.yaml + path: .workflow + - uses: ./.workflow/.github/actions/vercel-deploy + with: + vercel-project-id: ${{ inputs.variant == 'basic' && secrets.VERCEL_PROJECT_ID_BASIC || secrets.VERCEL_PROJECT_ID_SPLIT }} + vercel-token: ${{ secrets.VERCEL_TOKEN }} + vercel-org-id: ${{ secrets.VERCEL_ORG_ID }} + variant: ${{ inputs.variant }} + purge-cache: ${{ inputs.purge-cache }} + + test: + if: github.repository == 'sveltejs/kit' + needs: deploy + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.workflow_sha }} + persist-credentials: false + sparse-checkout: | + .github/actions + pnpm-lock.yaml + path: .workflow + - uses: ./.workflow/.github/actions/deployment-test + with: + sha: ${{ inputs.sha }} + platform: vercel + test-app-dir: ${{ inputs.test-app-dir }} + variant: ${{ inputs.variant }} diff --git a/.github/workflows/platform-tests-all.yml b/.github/workflows/platform-tests-all.yml index 3b54b30aac21..944b3a2a9c64 100644 --- a/.github/workflows/platform-tests-all.yml +++ b/.github/workflows/platform-tests-all.yml @@ -16,19 +16,16 @@ jobs: uses: ./.github/workflows/platform-tests-vercel.yml with: sha: ${{ inputs.sha }} - secrets: inherit netlify: uses: ./.github/workflows/platform-tests-netlify.yml with: sha: ${{ inputs.sha }} - secrets: inherit node: uses: ./.github/workflows/platform-tests-node.yml with: sha: ${{ inputs.sha }} - secrets: inherit report-status: if: always() diff --git a/.github/workflows/platform-tests-netlify.yml b/.github/workflows/platform-tests-netlify.yml index 9a43937b31dc..820077177bdd 100644 --- a/.github/workflows/platform-tests-netlify.yml +++ b/.github/workflows/platform-tests-netlify.yml @@ -18,104 +18,36 @@ on: permissions: contents: read -env: - PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1' - jobs: - test-basic: + basic: if: github.repository == 'sveltejs/kit' - runs-on: ubuntu-latest - timeout-minutes: 15 - environment: '@sveltejs/adapter-netlify platform tests' - env: - TEST_APP_DIR: packages/adapter-netlify/test/apps/basic - steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: ${{ inputs.sha || github.sha }} - - - uses: ./.github/actions/netlify-deploy - id: deploy - with: - test-app-dir: ${{ env.TEST_APP_DIR }} - netlify-project-id: ${{ secrets.NETLIFY_PROJECT_ID_BASIC }} - netlify-token: ${{ secrets.NETLIFY_TOKEN }} - - - uses: ./.github/actions/platform-test - with: - test-app-dir: ${{ env.TEST_APP_DIR }} - deployment-url: ${{ steps.deploy.outputs.deployment-url }} + uses: ./.github/workflows/platform-test-netlify.yml + with: + sha: ${{ inputs.sha || github.sha }} + variant: basic + test-app-dir: packages/adapter-netlify/test/apps/basic - test-edge: + edge: if: github.repository == 'sveltejs/kit' - runs-on: ubuntu-latest - timeout-minutes: 15 - environment: '@sveltejs/adapter-netlify platform tests' - env: - TEST_APP_DIR: packages/adapter-netlify/test/apps/edge - steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: ${{ inputs.sha || github.sha }} - - - uses: ./.github/actions/netlify-deploy - id: deploy - with: - test-app-dir: ${{ env.TEST_APP_DIR }} - netlify-project-id: ${{ secrets.NETLIFY_PROJECT_ID_EDGE }} - netlify-token: ${{ secrets.NETLIFY_TOKEN }} - - - uses: ./.github/actions/platform-test - with: - test-app-dir: ${{ env.TEST_APP_DIR }} - deployment-url: ${{ steps.deploy.outputs.deployment-url }} + uses: ./.github/workflows/platform-test-netlify.yml + with: + sha: ${{ inputs.sha || github.sha }} + variant: edge + test-app-dir: packages/adapter-netlify/test/apps/edge - test-split: + split: if: github.repository == 'sveltejs/kit' - runs-on: ubuntu-latest - timeout-minutes: 15 - environment: '@sveltejs/adapter-netlify platform tests' - env: - TEST_APP_DIR: packages/adapter-netlify/test/apps/split - steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: ${{ inputs.sha || github.sha }} + uses: ./.github/workflows/platform-test-netlify.yml + with: + sha: ${{ inputs.sha || github.sha }} + variant: split + test-app-dir: packages/adapter-netlify/test/apps/split - - uses: ./.github/actions/netlify-deploy - id: deploy - with: - test-app-dir: ${{ env.TEST_APP_DIR }} - netlify-project-id: ${{ secrets.NETLIFY_PROJECT_ID_SPLIT }} - netlify-token: ${{ secrets.NETLIFY_TOKEN }} - - - uses: ./.github/actions/platform-test - with: - test-app-dir: ${{ env.TEST_APP_DIR }} - deployment-url: ${{ steps.deploy.outputs.deployment-url }} - - test-split-edge: + split-edge: if: github.repository == 'sveltejs/kit' - runs-on: ubuntu-latest - timeout-minutes: 15 - environment: '@sveltejs/adapter-netlify platform tests' - env: - TEST_APP_DIR: packages/adapter-netlify/test/apps/split - EDGE: 'true' - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: ${{ inputs.sha || github.sha }} - persist-credentials: false - - - uses: ./.github/actions/netlify-deploy - id: deploy - with: - test-app-dir: ${{ env.TEST_APP_DIR }} - netlify-project-id: ${{ secrets.NETLIFY_PROJECT_ID_SPLIT }} - netlify-token: ${{ secrets.NETLIFY_TOKEN }} - - - uses: ./.github/actions/platform-test - with: - test-app-dir: ${{ env.TEST_APP_DIR }} - deployment-url: ${{ steps.deploy.outputs.deployment-url }} + uses: ./.github/workflows/platform-test-netlify.yml + with: + sha: ${{ inputs.sha || github.sha }} + variant: split-edge + test-app-dir: packages/adapter-netlify/test/apps/split + edge: true diff --git a/.github/workflows/platform-tests-node.yml b/.github/workflows/platform-tests-node.yml index fba3585e3571..6b375d255162 100644 --- a/.github/workflows/platform-tests-node.yml +++ b/.github/workflows/platform-tests-node.yml @@ -42,6 +42,6 @@ jobs: working-directory: packages/adapter-node run: pnpm prepublishOnly - - uses: ./.github/actions/platform-test + - uses: ./.github/actions/node-test with: test-app-dir: packages/adapter-node/test/apps/basic diff --git a/.github/workflows/platform-tests-vercel.yml b/.github/workflows/platform-tests-vercel.yml index 13411077f5d0..8beeeea0082c 100644 --- a/.github/workflows/platform-tests-vercel.yml +++ b/.github/workflows/platform-tests-vercel.yml @@ -18,52 +18,20 @@ on: permissions: contents: read -env: - PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1' - jobs: - test-basic: + basic: if: github.repository == 'sveltejs/kit' - runs-on: ubuntu-latest - timeout-minutes: 15 - environment: '@sveltejs/adapter-vercel platform tests' - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: ${{ inputs.sha || github.sha }} - persist-credentials: false - - - uses: ./.github/actions/vercel-deploy - id: deploy - with: - vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID_BASIC }} - vercel-token: ${{ secrets.VERCEL_TOKEN }} - vercel-org-id: ${{ secrets.VERCEL_ORG_ID }} - - - uses: ./.github/actions/platform-test - with: - test-app-dir: packages/adapter-vercel/test/apps/basic - deployment-url: ${{ steps.deploy.outputs.deployment-url }} - - test-split: + uses: ./.github/workflows/platform-test-vercel.yml + with: + sha: ${{ inputs.sha || github.sha }} + variant: basic + test-app-dir: packages/adapter-vercel/test/apps/basic + purge-cache: true + + split: if: github.repository == 'sveltejs/kit' - runs-on: ubuntu-latest - timeout-minutes: 15 - environment: '@sveltejs/adapter-vercel platform tests' - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: ${{ inputs.sha || github.sha }} - persist-credentials: false - - - uses: ./.github/actions/vercel-deploy - id: deploy - with: - vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID_SPLIT }} - vercel-token: ${{ secrets.VERCEL_TOKEN }} - vercel-org-id: ${{ secrets.VERCEL_ORG_ID }} - - - uses: ./.github/actions/platform-test - with: - test-app-dir: packages/adapter-vercel/test/apps/split - deployment-url: ${{ steps.deploy.outputs.deployment-url }} + uses: ./.github/workflows/platform-test-vercel.yml + with: + sha: ${{ inputs.sha || github.sha }} + variant: split + test-app-dir: packages/adapter-vercel/test/apps/split From 1afb2d92b6a08c4b98e973a481d51eec005a98a6 Mon Sep 17 00:00:00 2001 From: Tee Ming Date: Sat, 19 Sep 2026 19:05:18 +0800 Subject: [PATCH 08/13] chore: platform test fixes (#17157) Three fixes: - specify a pnpm version when setting it up in the deploy step. Need this because there's no package.json in the deploy step which would have specified it. - ignore a missing build script in the Netlify package. The `version-3` branch does not have it but `main` does. - improve the platform job names --------- Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com> --- .github/actions/netlify-deploy/action.yml | 1 + .github/actions/vercel-deploy/action.yml | 1 + .github/workflows/platform-test-netlify.yml | 6 +++++- .github/workflows/platform-test-vercel.yml | 3 +++ 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/actions/netlify-deploy/action.yml b/.github/actions/netlify-deploy/action.yml index abc0d747701f..fe9eba7e99ec 100644 --- a/.github/actions/netlify-deploy/action.yml +++ b/.github/actions/netlify-deploy/action.yml @@ -26,6 +26,7 @@ runs: - uses: pnpm/setup@703c52620218391530e48b9e8870d5c0082e1b9b # v2.1.0 with: + version: ^12.0.0 runtime: node@24 cache: true diff --git a/.github/actions/vercel-deploy/action.yml b/.github/actions/vercel-deploy/action.yml index c86ba2d5da06..e5c1477ea4c6 100644 --- a/.github/actions/vercel-deploy/action.yml +++ b/.github/actions/vercel-deploy/action.yml @@ -35,6 +35,7 @@ runs: - uses: pnpm/setup@703c52620218391530e48b9e8870d5c0082e1b9b # v2.1.0 with: + version: ^12.0.0 runtime: node@24 cache: true diff --git a/.github/workflows/platform-test-netlify.yml b/.github/workflows/platform-test-netlify.yml index 043b316d54d4..4f7f74f4e1c4 100644 --- a/.github/workflows/platform-test-netlify.yml +++ b/.github/workflows/platform-test-netlify.yml @@ -29,6 +29,7 @@ env: jobs: build: + name: ${{ inputs.variant }} build if: github.repository == 'sveltejs/kit' runs-on: ubuntu-latest timeout-minutes: 15 @@ -42,8 +43,9 @@ jobs: with: runtime: node@24 + # TODO: version-3 branch has no build command, so remove this step - name: Build adapter - run: pnpm --filter @sveltejs/adapter-netlify build + run: pnpm --filter @sveltejs/adapter-netlify --if-present build - name: Build test app working-directory: ${{ inputs.test-app-dir }} @@ -67,6 +69,7 @@ jobs: retention-days: 1 deploy: + name: ${{ inputs.variant }} deploy if: github.repository == 'sveltejs/kit' needs: build runs-on: ubuntu-latest @@ -88,6 +91,7 @@ jobs: variant: ${{ inputs.variant }} test: + name: ${{ inputs.variant }} test if: github.repository == 'sveltejs/kit' needs: deploy runs-on: ubuntu-latest diff --git a/.github/workflows/platform-test-vercel.yml b/.github/workflows/platform-test-vercel.yml index f547e8e8645b..2fa243c6189b 100644 --- a/.github/workflows/platform-test-vercel.yml +++ b/.github/workflows/platform-test-vercel.yml @@ -29,6 +29,7 @@ env: jobs: build: + name: ${{ inputs.variant }} build if: github.repository == 'sveltejs/kit' runs-on: ubuntu-latest timeout-minutes: 15 @@ -62,6 +63,7 @@ jobs: retention-days: 1 deploy: + name: ${{ inputs.variant }} deploy if: github.repository == 'sveltejs/kit' needs: build runs-on: ubuntu-latest @@ -85,6 +87,7 @@ jobs: purge-cache: ${{ inputs.purge-cache }} test: + name: ${{ inputs.variant }} test if: github.repository == 'sveltejs/kit' needs: deploy runs-on: ubuntu-latest From c37e7aab5f2ef3faf8ecb44830460f1d77311fce Mon Sep 17 00:00:00 2001 From: Nic Polumeyv Date: Sat, 19 Sep 2026 10:54:51 -0400 Subject: [PATCH 09/13] chore: point the platform deploy cache at the checked-out lockfile (#17159) --- .github/actions/netlify-deploy/action.yml | 1 + .github/actions/vercel-deploy/action.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/actions/netlify-deploy/action.yml b/.github/actions/netlify-deploy/action.yml index fe9eba7e99ec..b411c756565a 100644 --- a/.github/actions/netlify-deploy/action.yml +++ b/.github/actions/netlify-deploy/action.yml @@ -29,6 +29,7 @@ runs: version: ^12.0.0 runtime: node@24 cache: true + cache-dependency-path: .workflow/pnpm-lock.yaml - name: Install Netlify CLI shell: bash diff --git a/.github/actions/vercel-deploy/action.yml b/.github/actions/vercel-deploy/action.yml index e5c1477ea4c6..31cad370b0f5 100644 --- a/.github/actions/vercel-deploy/action.yml +++ b/.github/actions/vercel-deploy/action.yml @@ -38,6 +38,7 @@ runs: version: ^12.0.0 runtime: node@24 cache: true + cache-dependency-path: .workflow/pnpm-lock.yaml - name: Install Vercel CLI shell: bash From 849a258b8a339c267efbef43e5ab9c6bcf867420 Mon Sep 17 00:00:00 2001 From: Tee Ming Date: Sun, 20 Sep 2026 05:16:50 +0800 Subject: [PATCH 10/13] docs: link to railway template (#17165) same as https://github.com/sveltejs/kit/pull/17155 but on `main` so I can get back to the Railway folks that it's on our site already --- documentation/docs/25-build-and-deploy/40-adapter-node.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/documentation/docs/25-build-and-deploy/40-adapter-node.md b/documentation/docs/25-build-and-deploy/40-adapter-node.md index 652034c1b059..e57dec781e48 100644 --- a/documentation/docs/25-build-and-deploy/40-adapter-node.md +++ b/documentation/docs/25-build-and-deploy/40-adapter-node.md @@ -4,6 +4,8 @@ title: Node servers To generate a standalone Node server, use [`adapter-node`](https://github.com/sveltejs/kit/tree/main/packages/adapter-node). +A quick way to get started is by deploying a project using SvelteKit's [official Railway template](https://railway.com/deploy/svelte-kit). + ## Usage Install with `npm i -D @sveltejs/adapter-node`, then add the adapter to your `svelte.config.js`: From 604dc1f989b56b842b74276757d4c5add88f771e Mon Sep 17 00:00:00 2001 From: Nic Polumeyv Date: Mon, 21 Sep 2026 10:03:32 -0400 Subject: [PATCH 11/13] chore: fix and simplify the platform tests (#17171) --- .github/actions/deployment-test/action.yml | 60 ------- .github/actions/netlify-deploy/action.yml | 81 --------- .github/actions/node-test/action.yml | 27 --- .github/actions/vercel-deploy/action.yml | 80 --------- .github/workflows/platform-test-netlify.yml | 113 ------------ .github/workflows/platform-test-vercel.yml | 109 ------------ .github/workflows/platform-test.yml | 174 +++++++++++++++++++ .github/workflows/platform-tests-all.yml | 139 +++++++++++---- .github/workflows/platform-tests-netlify.yml | 53 ------ .github/workflows/platform-tests-node.yml | 47 ----- .github/workflows/platform-tests-vercel.yml | 37 ---- 11 files changed, 278 insertions(+), 642 deletions(-) delete mode 100644 .github/actions/deployment-test/action.yml delete mode 100644 .github/actions/netlify-deploy/action.yml delete mode 100644 .github/actions/node-test/action.yml delete mode 100644 .github/actions/vercel-deploy/action.yml delete mode 100644 .github/workflows/platform-test-netlify.yml delete mode 100644 .github/workflows/platform-test-vercel.yml create mode 100644 .github/workflows/platform-test.yml delete mode 100644 .github/workflows/platform-tests-netlify.yml delete mode 100644 .github/workflows/platform-tests-node.yml delete mode 100644 .github/workflows/platform-tests-vercel.yml diff --git a/.github/actions/deployment-test/action.yml b/.github/actions/deployment-test/action.yml deleted file mode 100644 index edec7af3ba59..000000000000 --- a/.github/actions/deployment-test/action.yml +++ /dev/null @@ -1,60 +0,0 @@ -name: 'Platform Test' -description: 'Run platform tests against a deployed test app' - -inputs: - sha: - description: 'Commit SHA to test' - required: true - platform: - description: 'Name of the deployment platform' - required: true - test-app-dir: - description: 'Path to the test app directory' - required: true - variant: - description: 'Name of the test app variant' - required: true - -runs: - using: 'composite' - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: ${{ inputs.sha }} - persist-credentials: false - path: .target - - - uses: pnpm/setup@703c52620218391530e48b9e8870d5c0082e1b9b # v2.1.0 - with: - runtime: node@24 - working-directory: .target - - - name: Install Playwright - shell: bash - working-directory: .target - run: pnpm playwright install chromium --no-shell - - - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: platform-${{ inputs.platform }}-${{ inputs.variant }}-deployment-${{ github.run_id }} - path: ${{ runner.temp }}/deployment - - - name: Read deployment URL - id: deployment - shell: bash - run: echo "url=$(cat '${{ runner.temp }}/deployment/url')" >> "$GITHUB_OUTPUT" - - - name: Run Playwright tests - shell: bash - working-directory: .target/${{ inputs.test-app-dir }} - env: - DEPLOYMENT_URL: ${{ steps.deployment.outputs.url }} - run: pnpm test:platform - - - name: Upload test artifacts - if: failure() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: platform-test-results-${{ inputs.platform }}-${{ inputs.variant }} - path: .target/${{ inputs.test-app-dir }}/test-results - retention-days: 3 diff --git a/.github/actions/netlify-deploy/action.yml b/.github/actions/netlify-deploy/action.yml deleted file mode 100644 index b411c756565a..000000000000 --- a/.github/actions/netlify-deploy/action.yml +++ /dev/null @@ -1,81 +0,0 @@ -name: 'Netlify Deploy' -description: 'Deploy a prebuilt test app to Netlify' - -inputs: - variant: - description: 'Name of the test app variant' - required: true - netlify-project-id: - description: 'Netlify project ID' - required: true - netlify-token: - description: 'Netlify authentication token' - required: true -outputs: - deployment-url: - description: 'URL of the deployed preview' - value: ${{ steps.deploy.outputs.url }} - -runs: - using: 'composite' - steps: - - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: platform-netlify-${{ inputs.variant }}-${{ github.run_id }} - path: ${{ runner.temp }}/platform-output - - - uses: pnpm/setup@703c52620218391530e48b9e8870d5c0082e1b9b # v2.1.0 - with: - version: ^12.0.0 - runtime: node@24 - cache: true - cache-dependency-path: .workflow/pnpm-lock.yaml - - - name: Install Netlify CLI - shell: bash - run: pnpm i -g netlify-cli --allow-build=esbuild --allow-build=netlify-cli - - - name: Deploy to Netlify - id: deploy - shell: bash - working-directory: ${{ runner.temp }}/platform-output - env: - NETLIFY_AUTH_TOKEN: ${{ inputs.netlify-token }} - NETLIFY_PROJECT_ID: ${{ inputs.netlify-project-id }} - run: | - set -euo pipefail - - if deploy_json=$(env \ - -u NETLIFY_AUTH_TOKEN \ - -u NETLIFY_PROJECT_ID \ - netlify deploy \ - --json \ - --verbose \ - --no-build \ - --dir="build" \ - --functions=".netlify/v1/functions" \ - --auth="$NETLIFY_AUTH_TOKEN" \ - --site="$NETLIFY_PROJECT_ID" - ); then - deploy_url=$(jq -er '.deploy_url' <<< "$deploy_json") - echo "url=$deploy_url" >> "$GITHUB_OUTPUT" - else - echo "::error::Netlify deployment failed" - echo "$deploy_json" >&2 - exit 1 - fi - - - name: Save deployment URL - shell: bash - env: - DEPLOYMENT_URL: ${{ steps.deploy.outputs.url }} - run: | - mkdir "${{ runner.temp }}/deployment" - printf '%s' "$DEPLOYMENT_URL" > "${{ runner.temp }}/deployment/url" - - - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: platform-netlify-${{ inputs.variant }}-deployment-${{ github.run_id }} - path: ${{ runner.temp }}/deployment/url - if-no-files-found: error - retention-days: 1 diff --git a/.github/actions/node-test/action.yml b/.github/actions/node-test/action.yml deleted file mode 100644 index 6ffa25206bd0..000000000000 --- a/.github/actions/node-test/action.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: 'Platform Test' -description: 'Run Playwright tests against a local build' - -inputs: - test-app-dir: - description: 'Path to the test app directory' - required: true - -runs: - using: 'composite' - steps: - - name: Install Playwright - shell: bash - run: pnpm playwright install chromium --no-shell - - - name: Run Playwright tests - shell: bash - working-directory: ${{ inputs.test-app-dir }} - run: pnpm test:platform - - - name: Upload test artifacts - if: failure() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: platform-test-results-${{ github.job }} - path: ${{ inputs.test-app-dir }}/test-results - retention-days: 3 diff --git a/.github/actions/vercel-deploy/action.yml b/.github/actions/vercel-deploy/action.yml deleted file mode 100644 index 31cad370b0f5..000000000000 --- a/.github/actions/vercel-deploy/action.yml +++ /dev/null @@ -1,80 +0,0 @@ -name: 'Vercel Deploy' -description: 'Deploy a prebuilt test app to Vercel' - -inputs: - variant: - description: 'Name of the test app variant' - required: true - # Important: When setting up a new Vercel test app, make sure - # to set the root directory in the dashboard to the correct path. - vercel-project-id: - description: 'Vercel project ID for the test app' - required: true - vercel-token: - description: 'Vercel authentication token' - required: true - vercel-org-id: - description: 'Vercel organization ID' - required: true - purge-cache: - description: 'Purge the Vercel CDN cache before testing' - required: false - default: 'false' -outputs: - deployment-url: - description: 'URL of the deployed preview' - value: ${{ steps.deploy.outputs.url }} - -runs: - using: 'composite' - steps: - - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: platform-vercel-${{ inputs.variant }}-${{ github.run_id }} - path: ${{ runner.temp }}/platform-output - - - uses: pnpm/setup@703c52620218391530e48b9e8870d5c0082e1b9b # v2.1.0 - with: - version: ^12.0.0 - runtime: node@24 - cache: true - cache-dependency-path: .workflow/pnpm-lock.yaml - - - name: Install Vercel CLI - shell: bash - run: pnpm i -g vercel - - - name: Deploy to Vercel - id: deploy - shell: bash - working-directory: ${{ runner.temp }}/platform-output - env: - VERCEL_TOKEN: ${{ inputs.vercel-token }} - VERCEL_ORG_ID: ${{ inputs.vercel-org-id }} - VERCEL_PROJECT_ID: ${{ inputs.vercel-project-id }} - run: echo "url=$(vercel deploy --prebuilt --yes --token="$VERCEL_TOKEN")" >> "$GITHUB_OUTPUT" - - # avoid stale cache results when testing ISR routes - - name: Purge Vercel CDN Cache - if: inputs.purge-cache == 'true' - shell: bash - env: - VERCEL_TOKEN: ${{ inputs.vercel-token }} - VERCEL_ORG_ID: ${{ inputs.vercel-org-id }} - VERCEL_PROJECT_ID: ${{ inputs.vercel-project-id }} - run: vercel cache purge --type cdn --token="$VERCEL_TOKEN" --yes - - - name: Save deployment URL - shell: bash - env: - DEPLOYMENT_URL: ${{ steps.deploy.outputs.url }} - run: | - mkdir "${{ runner.temp }}/deployment" - printf '%s' "$DEPLOYMENT_URL" > "${{ runner.temp }}/deployment/url" - - - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: platform-vercel-${{ inputs.variant }}-deployment-${{ github.run_id }} - path: ${{ runner.temp }}/deployment/url - if-no-files-found: error - retention-days: 1 diff --git a/.github/workflows/platform-test-netlify.yml b/.github/workflows/platform-test-netlify.yml deleted file mode 100644 index 4f7f74f4e1c4..000000000000 --- a/.github/workflows/platform-test-netlify.yml +++ /dev/null @@ -1,113 +0,0 @@ -name: Platform Test (Netlify) - -on: - workflow_call: - inputs: - sha: - description: 'Commit SHA to test' - required: true - type: string - variant: - description: 'Name of the test app variant' - required: true - type: string - test-app-dir: - description: 'Path to the test app directory' - required: true - type: string - edge: - description: 'Build the test app with Netlify Edge Functions' - required: false - type: boolean - default: false - -permissions: - contents: read - -env: - PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1' - -jobs: - build: - name: ${{ inputs.variant }} build - if: github.repository == 'sveltejs/kit' - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: ${{ inputs.sha }} - persist-credentials: false - - - uses: pnpm/setup@703c52620218391530e48b9e8870d5c0082e1b9b # v2.1.0 - with: - runtime: node@24 - - # TODO: version-3 branch has no build command, so remove this step - - name: Build adapter - run: pnpm --filter @sveltejs/adapter-netlify --if-present build - - - name: Build test app - working-directory: ${{ inputs.test-app-dir }} - env: - EDGE: ${{ inputs.edge }} - run: pnpm build - - - name: Stage deployment artifact - env: - TARGET_DIR: ${{ github.workspace }}/${{ inputs.test-app-dir }} - run: | - mkdir platform-output - cp -a "$TARGET_DIR/build" "$TARGET_DIR/.netlify" platform-output/ - - - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: platform-netlify-${{ inputs.variant }}-${{ github.run_id }} - path: platform-output - include-hidden-files: true - if-no-files-found: error - retention-days: 1 - - deploy: - name: ${{ inputs.variant }} deploy - if: github.repository == 'sveltejs/kit' - needs: build - runs-on: ubuntu-latest - timeout-minutes: 15 - environment: '@sveltejs/adapter-netlify platform tests' - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: ${{ github.workflow_sha }} - persist-credentials: false - sparse-checkout: | - .github/actions - pnpm-lock.yaml - path: .workflow - - uses: ./.workflow/.github/actions/netlify-deploy - with: - netlify-project-id: ${{ inputs.variant == 'basic' && secrets.NETLIFY_PROJECT_ID_BASIC || inputs.variant == 'edge' && secrets.NETLIFY_PROJECT_ID_EDGE || inputs.variant == 'instrumentation' && secrets.NETLIFY_PROJECT_ID_INSTRUMENTATION || secrets.NETLIFY_PROJECT_ID_SPLIT }} - netlify-token: ${{ secrets.NETLIFY_TOKEN }} - variant: ${{ inputs.variant }} - - test: - name: ${{ inputs.variant }} test - if: github.repository == 'sveltejs/kit' - needs: deploy - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: ${{ github.workflow_sha }} - persist-credentials: false - sparse-checkout: | - .github/actions - pnpm-lock.yaml - path: .workflow - - uses: ./.workflow/.github/actions/deployment-test - with: - sha: ${{ inputs.sha }} - platform: netlify - test-app-dir: ${{ inputs.test-app-dir }} - variant: ${{ inputs.variant }} diff --git a/.github/workflows/platform-test-vercel.yml b/.github/workflows/platform-test-vercel.yml deleted file mode 100644 index 2fa243c6189b..000000000000 --- a/.github/workflows/platform-test-vercel.yml +++ /dev/null @@ -1,109 +0,0 @@ -name: Platform Test (Vercel) - -on: - workflow_call: - inputs: - sha: - description: 'Commit SHA to test' - required: true - type: string - variant: - description: 'Name of the test app variant' - required: true - type: string - test-app-dir: - description: 'Path to the test app directory' - required: true - type: string - purge-cache: - description: 'Purge the Vercel CDN cache before testing' - required: false - type: boolean - default: false - -permissions: - contents: read - -env: - PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1' - -jobs: - build: - name: ${{ inputs.variant }} build - if: github.repository == 'sveltejs/kit' - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: ${{ inputs.sha }} - persist-credentials: false - - - uses: pnpm/setup@703c52620218391530e48b9e8870d5c0082e1b9b # v2.1.0 - with: - runtime: node@24 - - - name: Build test app - working-directory: ${{ inputs.test-app-dir }} - run: pnpm build - - - name: Stage deployment artifact - env: - TARGET_DIR: ${{ github.workspace }}/${{ inputs.test-app-dir }} - run: | - mkdir platform-output - cp -a "$TARGET_DIR/.vercel" platform-output/ - - - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: platform-vercel-${{ inputs.variant }}-${{ github.run_id }} - path: platform-output - include-hidden-files: true - if-no-files-found: error - retention-days: 1 - - deploy: - name: ${{ inputs.variant }} deploy - if: github.repository == 'sveltejs/kit' - needs: build - runs-on: ubuntu-latest - timeout-minutes: 15 - environment: '@sveltejs/adapter-vercel platform tests' - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: ${{ github.workflow_sha }} - persist-credentials: false - sparse-checkout: | - .github/actions - pnpm-lock.yaml - path: .workflow - - uses: ./.workflow/.github/actions/vercel-deploy - with: - vercel-project-id: ${{ inputs.variant == 'basic' && secrets.VERCEL_PROJECT_ID_BASIC || secrets.VERCEL_PROJECT_ID_SPLIT }} - vercel-token: ${{ secrets.VERCEL_TOKEN }} - vercel-org-id: ${{ secrets.VERCEL_ORG_ID }} - variant: ${{ inputs.variant }} - purge-cache: ${{ inputs.purge-cache }} - - test: - name: ${{ inputs.variant }} test - if: github.repository == 'sveltejs/kit' - needs: deploy - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: ${{ github.workflow_sha }} - persist-credentials: false - sparse-checkout: | - .github/actions - pnpm-lock.yaml - path: .workflow - - uses: ./.workflow/.github/actions/deployment-test - with: - sha: ${{ inputs.sha }} - platform: vercel - test-app-dir: ${{ inputs.test-app-dir }} - variant: ${{ inputs.variant }} diff --git a/.github/workflows/platform-test.yml b/.github/workflows/platform-test.yml new file mode 100644 index 000000000000..be98e791dcf9 --- /dev/null +++ b/.github/workflows/platform-test.yml @@ -0,0 +1,174 @@ +name: Platform Test + +on: + workflow_call: + inputs: + sha: + description: 'Commit SHA to test' + required: true + type: string + platform: + description: 'netlify or vercel' + required: true + type: string + variant: + description: 'Name of the test app variant' + required: true + type: string + app: + description: 'Directory of the test app' + required: true + type: string + +permissions: + contents: read + +env: + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1' + APP: packages/adapter-${{ inputs.platform }}/test/apps/${{ inputs.app }} + +defaults: + run: + shell: bash + +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - &checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ inputs.sha }} + persist-credentials: false + + # reads the pnpm version from the commit under test: `main` is on v10, `version-3` on v12 + - &pnpm + uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + + - &node + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: 24 + + - &install + name: Install dependencies + run: pnpm install --frozen-lockfile + + # TODO: only adapter-netlify on `main` has a build script, remove this step after the v3 release + - name: Build adapter + env: + PLATFORM: ${{ inputs.platform }} + run: pnpm --filter "@sveltejs/adapter-$PLATFORM" --if-present build + + - name: Build test app + working-directory: ${{ env.APP }} + env: + EDGE: ${{ inputs.variant == 'split-edge' }} + run: pnpm build + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{ inputs.platform }}-${{ inputs.variant }} + # whichever of these the adapter wrote, relative to the test app + path: | + ${{ env.APP }}/build + ${{ env.APP }}/.netlify + ${{ env.APP }}/.vercel + include-hidden-files: true + if-no-files-found: error + retention-days: 1 + + deploy: + needs: build + runs-on: ubuntu-latest + timeout-minutes: 15 + environment: '@sveltejs/adapter-${{ inputs.platform }} platform tests' + steps: + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ inputs.platform }}-${{ inputs.variant }} + + - uses: pnpm/setup@703c52620218391530e48b9e8870d5c0082e1b9b # v2.1.0 + with: + version: ^12.0.0 + runtime: node@24 + + - name: Install CLI + env: + CLI: ${{ inputs.platform == 'netlify' && 'netlify-cli' || 'vercel' }} + run: pnpm i -g "$CLI" --allow-build=esbuild --allow-build=netlify-cli + + # the secrets are passed as flags and scrubbed from the environment the CLI bundles functions in + - name: Deploy to Netlify + if: inputs.platform == 'netlify' + env: + NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_TOKEN }} + NETLIFY_PROJECT_ID: ${{ inputs.variant == 'basic' && secrets.NETLIFY_PROJECT_ID_BASIC || inputs.variant == 'edge' && secrets.NETLIFY_PROJECT_ID_EDGE || secrets.NETLIFY_PROJECT_ID_SPLIT }} + run: | + env -u NETLIFY_AUTH_TOKEN -u NETLIFY_PROJECT_ID \ + netlify deploy --json --no-build --dir=build --functions=.netlify/v1/functions \ + --auth="$NETLIFY_AUTH_TOKEN" --site="$NETLIFY_PROJECT_ID" \ + | tee /dev/stderr | jq -er '.deploy_url' > url + + # Important: When setting up a new Vercel test app, make sure + # to set the root directory in the dashboard to the correct path. + - name: Deploy to Vercel + if: inputs.platform == 'vercel' + env: &vercel-secrets + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ inputs.variant == 'basic' && secrets.VERCEL_PROJECT_ID_BASIC || secrets.VERCEL_PROJECT_ID_SPLIT }} + # the CLI reads `.vercel/output` from here but checks that the project's root directory exists + run: | + mkdir -p "$APP" + vercel deploy --prebuilt --yes --token="$VERCEL_TOKEN" > url + + # avoid stale cache results when testing ISR routes + - name: Purge Vercel CDN Cache + if: inputs.platform == 'vercel' && inputs.variant == 'basic' + env: *vercel-secrets + run: vercel cache purge --type cdn --token="$VERCEL_TOKEN" --yes + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{ inputs.platform }}-${{ inputs.variant }}-url + path: url + if-no-files-found: error + retention-days: 1 + + test: + needs: deploy + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - *checkout + - *pnpm + - *node + - *install + + - name: Install Playwright + run: pnpm playwright install chromium --no-shell + + # some tests inspect the build output + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ inputs.platform }}-${{ inputs.variant }} + path: ${{ env.APP }} + + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ inputs.platform }}-${{ inputs.variant }}-url + path: ${{ runner.temp }} + + - name: Run Playwright tests + working-directory: ${{ env.APP }} + run: DEPLOYMENT_URL=$(cat "$RUNNER_TEMP/url") pnpm test:platform + + - name: Upload test artifacts + if: failure() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: test-results-${{ inputs.platform }}-${{ inputs.variant }} + path: ${{ env.APP }}/test-results + retention-days: 3 diff --git a/.github/workflows/platform-tests-all.yml b/.github/workflows/platform-tests-all.yml index 944b3a2a9c64..349688bc4461 100644 --- a/.github/workflows/platform-tests-all.yml +++ b/.github/workflows/platform-tests-all.yml @@ -7,56 +7,125 @@ on: description: 'Commit SHA to test and update check status on' required: false default: '' + platform: + description: 'Platform to test' + type: choice + options: [all, netlify, vercel, node] + default: all permissions: - contents: write + contents: read jobs: - vercel: - uses: ./.github/workflows/platform-tests-vercel.yml + netlify: + name: netlify / ${{ matrix.variant }} + if: github.repository == 'sveltejs/kit' && (inputs.platform == 'all' || inputs.platform == 'netlify') + strategy: + fail-fast: false + matrix: + variant: [basic, edge, split] + include: + - variant: split-edge + app: split + uses: ./.github/workflows/platform-test.yml with: - sha: ${{ inputs.sha }} + sha: ${{ inputs.sha || github.sha }} + platform: netlify + variant: ${{ matrix.variant }} + app: ${{ matrix.app || matrix.variant }} + # the secrets of a job's environment only resolve in a called workflow when the caller passes `secrets` + secrets: inherit - netlify: - uses: ./.github/workflows/platform-tests-netlify.yml + vercel: + name: vercel / ${{ matrix.variant }} + if: github.repository == 'sveltejs/kit' && (inputs.platform == 'all' || inputs.platform == 'vercel') + strategy: + fail-fast: false + matrix: + variant: [basic, split] + uses: ./.github/workflows/platform-test.yml with: - sha: ${{ inputs.sha }} + sha: ${{ inputs.sha || github.sha }} + platform: vercel + variant: ${{ matrix.variant }} + app: ${{ matrix.variant }} + secrets: inherit node: - uses: ./.github/workflows/platform-tests-node.yml - with: - sha: ${{ inputs.sha }} + name: node / ${{ matrix.node-version }} + if: github.repository == 'sveltejs/kit' && (inputs.platform == 'all' || inputs.platform == 'node') + runs-on: ubuntu-latest + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + node-version: [22, 24] + environment: '@sveltejs/adapter-node platform tests' + env: + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1' + APP: packages/adapter-node/test/apps/basic + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ inputs.sha || github.sha }} + persist-credentials: false + + - uses: ./.github/actions/node-setup + with: + node-version: ${{ matrix.node-version }} + + - name: Build adapter-node files + working-directory: packages/adapter-node + run: pnpm prepublishOnly + + - name: Install Playwright + run: pnpm playwright install chromium --no-shell + + - name: Run Playwright tests + working-directory: ${{ env.APP }} + run: pnpm test:platform + + - name: Upload test artifacts + if: failure() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: test-results-node-${{ matrix.node-version }} + path: ${{ env.APP }}/test-results + retention-days: 3 report-status: - if: always() - needs: [vercel, netlify, node] + if: always() && github.repository == 'sveltejs/kit' && inputs.platform == 'all' + needs: [netlify, vercel, node] runs-on: ubuntu-latest + permissions: + contents: write steps: - - name: Determine outcome - id: outcome - run: | - if [ "${{ contains(needs.*.result, 'failure') }}" = "true" ]; then - echo "conclusion=failure" >> "$GITHUB_OUTPUT" - echo "title=Platform tests failed" >> "$GITHUB_OUTPUT" - echo "summary=One or more platform test jobs failed. See the [workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details." >> "$GITHUB_OUTPUT" - elif [ "${{ contains(needs.*.result, 'cancelled') }}" = "true" ]; then - echo "conclusion=cancelled" >> "$GITHUB_OUTPUT" - echo "title=Platform tests cancelled" >> "$GITHUB_OUTPUT" - echo "summary=Platform tests were cancelled before completing." >> "$GITHUB_OUTPUT" - else - echo "conclusion=success" >> "$GITHUB_OUTPUT" - echo "title=Platform tests passed" >> "$GITHUB_OUTPUT" - echo "summary=All platform test jobs passed." >> "$GITHUB_OUTPUT" - fi - - name: Report results env: GH_TOKEN: ${{ github.token }} + SHA: ${{ inputs.sha || github.sha }} + CONCLUSION: ${{ contains(needs.*.result, 'failure') && 'failure' || contains(needs.*.result, 'cancelled') && 'cancelled' || 'success' }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} run: | - gh api repos/${{ github.repository }}/dispatches \ + case "$CONCLUSION" in + failure) + title='Platform tests failed' + summary="One or more platform test jobs failed. See the [workflow run]($RUN_URL) for details." + ;; + cancelled) + title='Platform tests cancelled' + summary='Platform tests were cancelled before completing.' + ;; + success) + title='Platform tests passed' + summary='All platform test jobs passed.' + ;; + esac + + gh api "repos/$GITHUB_REPOSITORY/dispatches" \ -f event_type=platform-tests-result \ - -f 'client_payload[sha]=${{ inputs.sha || github.sha }}' \ - -f 'client_payload[conclusion]=${{ steps.outcome.outputs.conclusion }}' \ - -f 'client_payload[title]=${{ steps.outcome.outputs.title }}' \ - -f 'client_payload[summary]=${{ steps.outcome.outputs.summary }}' \ - -f 'client_payload[details_url]=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}' + -f "client_payload[sha]=$SHA" \ + -f "client_payload[conclusion]=$CONCLUSION" \ + -f "client_payload[title]=$title" \ + -f "client_payload[summary]=$summary" \ + -f "client_payload[details_url]=$RUN_URL" diff --git a/.github/workflows/platform-tests-netlify.yml b/.github/workflows/platform-tests-netlify.yml deleted file mode 100644 index 820077177bdd..000000000000 --- a/.github/workflows/platform-tests-netlify.yml +++ /dev/null @@ -1,53 +0,0 @@ -name: Platform Tests (Netlify) - -on: - workflow_dispatch: - inputs: - sha: - description: 'Commit SHA to test' - required: false - default: '' - workflow_call: - inputs: - sha: - description: 'Commit SHA to test' - required: false - type: string - default: '' - -permissions: - contents: read - -jobs: - basic: - if: github.repository == 'sveltejs/kit' - uses: ./.github/workflows/platform-test-netlify.yml - with: - sha: ${{ inputs.sha || github.sha }} - variant: basic - test-app-dir: packages/adapter-netlify/test/apps/basic - - edge: - if: github.repository == 'sveltejs/kit' - uses: ./.github/workflows/platform-test-netlify.yml - with: - sha: ${{ inputs.sha || github.sha }} - variant: edge - test-app-dir: packages/adapter-netlify/test/apps/edge - - split: - if: github.repository == 'sveltejs/kit' - uses: ./.github/workflows/platform-test-netlify.yml - with: - sha: ${{ inputs.sha || github.sha }} - variant: split - test-app-dir: packages/adapter-netlify/test/apps/split - - split-edge: - if: github.repository == 'sveltejs/kit' - uses: ./.github/workflows/platform-test-netlify.yml - with: - sha: ${{ inputs.sha || github.sha }} - variant: split-edge - test-app-dir: packages/adapter-netlify/test/apps/split - edge: true diff --git a/.github/workflows/platform-tests-node.yml b/.github/workflows/platform-tests-node.yml deleted file mode 100644 index 6b375d255162..000000000000 --- a/.github/workflows/platform-tests-node.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: Platform Tests (Node) - -on: - workflow_dispatch: - inputs: - sha: - description: 'Commit SHA to test' - required: false - default: '' - workflow_call: - inputs: - sha: - description: 'Commit SHA to test' - required: false - type: string - default: '' - -permissions: - contents: read - -env: - PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1' - -jobs: - test-basic: - if: github.repository == 'sveltejs/kit' - runs-on: ubuntu-latest - strategy: - matrix: - node-version: [22, 24] - environment: '@sveltejs/adapter-node platform tests' - steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: ${{ inputs.sha || github.sha }} - - - uses: ./.github/actions/node-setup - with: - node-version: ${{ matrix.node-version }} - - - name: Build adapter-node files - working-directory: packages/adapter-node - run: pnpm prepublishOnly - - - uses: ./.github/actions/node-test - with: - test-app-dir: packages/adapter-node/test/apps/basic diff --git a/.github/workflows/platform-tests-vercel.yml b/.github/workflows/platform-tests-vercel.yml deleted file mode 100644 index 8beeeea0082c..000000000000 --- a/.github/workflows/platform-tests-vercel.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: Platform Tests (Vercel) - -on: - workflow_dispatch: - inputs: - sha: - description: 'Commit SHA to test' - required: false - default: '' - workflow_call: - inputs: - sha: - description: 'Commit SHA to test' - required: false - type: string - default: '' - -permissions: - contents: read - -jobs: - basic: - if: github.repository == 'sveltejs/kit' - uses: ./.github/workflows/platform-test-vercel.yml - with: - sha: ${{ inputs.sha || github.sha }} - variant: basic - test-app-dir: packages/adapter-vercel/test/apps/basic - purge-cache: true - - split: - if: github.repository == 'sveltejs/kit' - uses: ./.github/workflows/platform-test-vercel.yml - with: - sha: ${{ inputs.sha || github.sha }} - variant: split - test-app-dir: packages/adapter-vercel/test/apps/split From c6f9a786e1ccd3b6dc91d97637299ffe8283fc2f Mon Sep 17 00:00:00 2001 From: Nic Polumeyv Date: Mon, 21 Sep 2026 11:14:07 -0400 Subject: [PATCH 12/13] chore: let Netlify's bundler resolve Kit in the platform tests (#17175) --- .github/workflows/platform-test.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/platform-test.yml b/.github/workflows/platform-test.yml index be98e791dcf9..f7bce2859bfa 100644 --- a/.github/workflows/platform-test.yml +++ b/.github/workflows/platform-test.yml @@ -67,6 +67,13 @@ jobs: EDGE: ${{ inputs.variant == 'split-edge' }} run: pnpm build + # the Netlify CLI bundles the functions in the deploy job, which has no `node_modules` + - name: Copy Kit next to the Netlify functions + if: inputs.platform == 'netlify' + run: | + mkdir -p "$APP/.netlify/v1/node_modules/@sveltejs/kit" + cp -r packages/kit/package.json packages/kit/src "$APP/.netlify/v1/node_modules/@sveltejs/kit" + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ inputs.platform }}-${{ inputs.variant }} From 74aeae8e194da5a93787471b309ba1cf3b37cb8f Mon Sep 17 00:00:00 2001 From: svelte-triage-bot Date: Wed, 23 Sep 2026 18:07:18 +0000 Subject: [PATCH 13/13] fix: dispose adapter emulators after use --- .changeset/calm-workers-retire.md | 6 ++ packages/adapter-cloudflare/index.js | 13 ++- packages/adapter-cloudflare/index.spec.js | 69 ++++++++++++++ packages/adapter-cloudflare/tsconfig.json | 1 + packages/kit/src/core/postbuild/prerender.js | 90 ++++++++++--------- packages/kit/src/core/postbuild/queue.js | 19 ++-- packages/kit/src/core/postbuild/queue.spec.js | 27 ++++++ packages/kit/src/exports/public.d.ts | 5 ++ packages/kit/src/exports/vite/dev/index.js | 3 +- .../kit/src/exports/vite/preview/index.js | 3 +- packages/kit/src/exports/vite/utils.js | 21 +++++ packages/kit/src/exports/vite/utils.spec.js | 33 ++++++- .../kit/test/apps/basics/svelte.config.js | 4 + .../kit/test/apps/basics/test/server.test.js | 5 ++ packages/kit/test/apps/basics/test/setup.js | 2 + packages/kit/types/index.d.ts | 5 ++ 16 files changed, 254 insertions(+), 52 deletions(-) create mode 100644 .changeset/calm-workers-retire.md create mode 100644 packages/adapter-cloudflare/index.spec.js diff --git a/.changeset/calm-workers-retire.md b/.changeset/calm-workers-retire.md new file mode 100644 index 000000000000..b48408a3f6be --- /dev/null +++ b/.changeset/calm-workers-retire.md @@ -0,0 +1,6 @@ +--- +'@sveltejs/kit': patch +'@sveltejs/adapter-cloudflare': patch +--- + +fix: dispose adapter emulators when their lifecycle ends diff --git a/packages/adapter-cloudflare/index.js b/packages/adapter-cloudflare/index.js index 24e2dd503f07..fc4952c83078 100644 --- a/packages/adapter-cloudflare/index.js +++ b/packages/adapter-cloudflare/index.js @@ -181,8 +181,14 @@ export default function (options = {}) { emulate() { // we want to invoke `getPlatformProxy` only once, but await it only when it is accessed. // If we would await it here, it would hang indefinitely because the platform proxy only resolves once a request happens + /** @type {Promise | undefined} */ + let get_proxy; + /** @type {Promise | undefined} */ + let disposal; + const get_emulated = async () => { - const proxy = await getPlatformProxy(options.platformProxy); + get_proxy ??= getPlatformProxy(options.platformProxy); + const proxy = await get_proxy; const platform = { env: proxy.env, ctx: proxy.ctx, @@ -209,6 +215,11 @@ export default function (options = {}) { platform: async ({ prerender }) => { emulated ??= await get_emulated(); return prerender ? emulated.prerender_platform : emulated.platform; + }, + dispose: async () => { + if (!get_proxy) return; + disposal ??= get_proxy.then((proxy) => proxy.dispose()); + await disposal; } }; }, diff --git a/packages/adapter-cloudflare/index.spec.js b/packages/adapter-cloudflare/index.spec.js new file mode 100644 index 000000000000..fe1f971a040b --- /dev/null +++ b/packages/adapter-cloudflare/index.spec.js @@ -0,0 +1,69 @@ +import { beforeEach, describe, expect, test, vi } from 'vitest'; +import { getPlatformProxy } from 'wrangler'; +import adapter from './index.js'; + +vi.mock('wrangler', () => ({ + getPlatformProxy: vi.fn(), + unstable_readConfig: vi.fn() +})); + +/** @typedef {Awaited>} PlatformProxy */ + +/** @type {PlatformProxy} */ +let proxy; + +beforeEach(() => { + vi.mocked(getPlatformProxy).mockReset(); + proxy = /** @type {PlatformProxy} */ ( + /** @type {unknown} */ ({ + env: {}, + ctx: {}, + caches: {}, + cf: {}, + dispose: vi.fn() + }) + ); + vi.mocked(getPlatformProxy).mockResolvedValue(proxy); +}); + +describe('emulate', () => { + test('lazily creates and disposes one platform proxy', async () => { + const emulator = await adapter().emulate?.(); + expect(getPlatformProxy).not.toHaveBeenCalled(); + + await emulator?.platform?.({ config: {}, prerender: false }); + await emulator?.platform?.({ config: {}, prerender: true }); + expect(getPlatformProxy).toHaveBeenCalledOnce(); + + await emulator?.dispose?.(); + await emulator?.dispose?.(); + expect(proxy.dispose).toHaveBeenCalledOnce(); + }); + + test('does not create a platform proxy solely to dispose it', async () => { + const emulator = await adapter().emulate?.(); + await emulator?.dispose?.(); + + expect(getPlatformProxy).not.toHaveBeenCalled(); + expect(proxy.dispose).not.toHaveBeenCalled(); + }); + + test('disposes an in-flight platform proxy once during concurrent cleanup', async () => { + /** @type {(proxy: PlatformProxy) => void} */ + let resolve_proxy = () => {}; + /** @type {Promise} */ + const pending_proxy = new Promise((resolve) => { + resolve_proxy = resolve; + }); + vi.mocked(getPlatformProxy).mockReturnValue(pending_proxy); + + const emulator = await adapter().emulate?.(); + const platform = emulator?.platform?.({ config: {}, prerender: false }); + const disposal = Promise.all([emulator?.dispose?.(), emulator?.dispose?.()]); + + expect(proxy.dispose).not.toHaveBeenCalled(); + resolve_proxy(proxy); + await Promise.all([platform, disposal]); + expect(proxy.dispose).toHaveBeenCalledOnce(); + }); +}); diff --git a/packages/adapter-cloudflare/tsconfig.json b/packages/adapter-cloudflare/tsconfig.json index d1fdac44e7b3..eb2ef72226e2 100644 --- a/packages/adapter-cloudflare/tsconfig.json +++ b/packages/adapter-cloudflare/tsconfig.json @@ -15,6 +15,7 @@ }, "include": [ "index.js", + "index.spec.js", "utils.js", "utils.spec.js", "test/utils.js", diff --git a/packages/kit/src/core/postbuild/prerender.js b/packages/kit/src/core/postbuild/prerender.js index f03e80cd7e8f..114e2a9608f6 100644 --- a/packages/kit/src/core/postbuild/prerender.js +++ b/packages/kit/src/core/postbuild/prerender.js @@ -127,8 +127,6 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env }) { return { prerendered, prerender_map }; } - const emulator = await config.adapter?.emulate?.(); - /** @type {import('types').Logger} */ const log = logger({ verbose }); @@ -187,6 +185,8 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env }) { ); const q = queue(config.prerender.concurrency); + /** @type {InstanceType} */ + let server; /** * @param {string} path @@ -542,55 +542,61 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env }) { return { prerendered, prerender_map }; } - // only run the server after the `should_prerender` check so that we - // don't run the user's init hook unnecessarily - const server = new Server(manifest); - await server.init({ - env, - read: (file) => createReadableStream(`${config.outDir}/output/server/${file}`) - }); - - log.info('Prerendering'); - - for (const entry of config.prerender.entries) { - if (entry === '*') { - for (const [id, prerender] of prerender_map) { - if (prerender) { - // remove optional parameters from the route - const segments = get_route_segments(id).filter((segment) => !segment.startsWith('[[')); - const processed_id = '/' + segments.join('/'); - - if (processed_id.includes('[')) continue; - const path = `/${get_route_segments(processed_id).join('/')}`; - void enqueue(null, config.paths.base + path); + const emulator = await config.adapter?.emulate?.(); + + try { + // only run the server after the `should_prerender` check so that we + // don't run the user's init hook unnecessarily + server = new Server(manifest); + await server.init({ + env, + read: (file) => createReadableStream(`${config.outDir}/output/server/${file}`) + }); + + log.info('Prerendering'); + + for (const entry of config.prerender.entries) { + if (entry === '*') { + for (const [id, prerender] of prerender_map) { + if (prerender) { + // remove optional parameters from the route + const segments = get_route_segments(id).filter((segment) => !segment.startsWith('[[')); + const processed_id = '/' + segments.join('/'); + + if (processed_id.includes('[')) continue; + const path = `/${get_route_segments(processed_id).join('/')}`; + void enqueue(null, config.paths.base + path); + } } + } else { + void enqueue(null, config.paths.base + entry); } - } else { - void enqueue(null, config.paths.base + entry); } - } - for (const { id, entries } of route_level_entries) { - for (const entry of entries) { - void enqueue(null, config.paths.base + entry, undefined, id); + for (const { id, entries } of route_level_entries) { + for (const entry of entries) { + void enqueue(null, config.paths.base + entry, undefined, id); + } } - } - const transport = (await internal.get_hooks()).transport ?? {}; - for (const internals of prerender_functions) { - if (internals.has_arg) { - for (const arg of (await internals.inputs?.()) ?? []) { - void enqueue( - null, - remote_prefix + internals.id + '/' + stringify_remote_arg(arg, transport) - ); + const transport = (await internal.get_hooks()).transport ?? {}; + for (const internals of prerender_functions) { + if (internals.has_arg) { + for (const arg of (await internals.inputs?.()) ?? []) { + void enqueue( + null, + remote_prefix + internals.id + '/' + stringify_remote_arg(arg, transport) + ); + } + } else { + void enqueue(null, remote_prefix + internals.id); } - } else { - void enqueue(null, remote_prefix + internals.id); } - } - await q.done(); + await q.done(); + } finally { + await emulator?.dispose?.(); + } // handle invalid fragment links for (const [key, referrers] of expected_hashlinks) { diff --git a/packages/kit/src/core/postbuild/queue.js b/packages/kit/src/core/postbuild/queue.js index a816e6eb2350..d8b80776526c 100644 --- a/packages/kit/src/core/postbuild/queue.js +++ b/packages/kit/src/core/postbuild/queue.js @@ -17,12 +17,23 @@ export function queue(concurrency) { let current = 0; let closed = false; + /** @type {Error | undefined} */ + let error; promise.catch(() => { // this is necessary in case a catch handler is never added // to the done promise by the user }); + function settle() { + closed = true; + if (error) { + reject(error); + } else { + resolve(); + } + } + function dequeue() { if (current < concurrency) { const task = tasks.shift(); @@ -34,15 +45,14 @@ export function queue(concurrency) { void promise .then(task.fulfil, (err) => { task.reject(err); - reject(err); + error ??= err; }) .then(() => { current -= 1; dequeue(); }); } else if (current === 0) { - closed = true; - resolve(); + settle(); } } } @@ -62,8 +72,7 @@ export function queue(concurrency) { done: () => { if (current === 0) { - closed = true; - resolve(); + settle(); } return promise; diff --git a/packages/kit/src/core/postbuild/queue.spec.js b/packages/kit/src/core/postbuild/queue.spec.js index 329e5c1f99a6..8d6982db303f 100644 --- a/packages/kit/src/core/postbuild/queue.spec.js +++ b/packages/kit/src/core/postbuild/queue.spec.js @@ -109,3 +109,30 @@ test('q.done() rejects if task rejects', async () => { assert.equal(/** @type {Error} */ (e).message, 'nope'); } }); + +test('q.done() waits for remaining tasks before rejecting', async () => { + const q = queue(2); + /** @type {() => void} */ + let finish = () => {}; + + q.add(() => Promise.reject(new Error('nope'))).catch(() => {}); + void q.add( + () => + new Promise((resolve) => { + finish = () => resolve(undefined); + }) + ); + + let rejected = false; + const done = q.done().catch((error) => { + rejected = true; + throw error; + }); + + await sleep(1); + expect(rejected).toBe(false); + + finish(); + await expect(done).rejects.toThrow('nope'); + expect(rejected).toBe(true); +}); diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index 80fd1b82b4d6..5f9e4df9f385 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -327,6 +327,11 @@ export interface Emulator { * and returns an `App.Platform` object */ platform?(details: { config: any; prerender: PrerenderOption }): MaybePromise; + + /** + * A function that is called when the emulator is no longer needed, allowing adapters to release any resources they acquired + */ + dispose?(): MaybePromise; } export interface KitConfig { diff --git a/packages/kit/src/exports/vite/dev/index.js b/packages/kit/src/exports/vite/dev/index.js index aadd288cac63..db92ae592183 100644 --- a/packages/kit/src/exports/vite/dev/index.js +++ b/packages/kit/src/exports/vite/dev/index.js @@ -17,7 +17,7 @@ import { SVELTE_KIT_ASSETS } from '../../../constants.js'; import * as sync from '../../../core/sync/sync.js'; import { get_mime_lookup, runtime_base } from '../../../core/utils.js'; import { compact } from '../../../utils/array.js'; -import { is_chrome_devtools_request, not_found } from '../utils.js'; +import { dispose_emulator_on_close, is_chrome_devtools_request, not_found } from '../utils.js'; import { SCHEME } from '../../../utils/url.js'; import { check_feature } from '../../../utils/features.js'; import { escape_html } from '../../../utils/escape.js'; @@ -442,6 +442,7 @@ export async function dev(vite, vite_config, svelte_config, get_remotes) { const env = loadEnv(vite_config.mode, svelte_config.kit.env.dir, ''); const emulator = await svelte_config.kit.adapter?.emulate?.(); + dispose_emulator_on_close(vite, emulator); return () => { const serve_static_middleware = vite.middlewares.stack.find( diff --git a/packages/kit/src/exports/vite/preview/index.js b/packages/kit/src/exports/vite/preview/index.js index 02d70d5f3d54..528104f3dbe5 100644 --- a/packages/kit/src/exports/vite/preview/index.js +++ b/packages/kit/src/exports/vite/preview/index.js @@ -7,7 +7,7 @@ import { loadEnv, normalizePath } from 'vite'; import { createReadableStream, getRequest, setResponse } from '../../../exports/node/index.js'; import { installPolyfills } from '../../../exports/node/polyfills.js'; import { SVELTE_KIT_ASSETS } from '../../../constants.js'; -import { is_chrome_devtools_request, not_found } from '../utils.js'; +import { dispose_emulator_on_close, is_chrome_devtools_request, not_found } from '../utils.js'; /** @typedef {import('http').IncomingMessage} Req */ /** @typedef {import('http').ServerResponse} Res */ @@ -66,6 +66,7 @@ export async function preview(vite, vite_config, svelte_config) { } const emulator = await svelte_config.kit.adapter?.emulate?.(); + dispose_emulator_on_close(vite, emulator); return () => { // Remove the base middleware. It screws with the URL. diff --git a/packages/kit/src/exports/vite/utils.js b/packages/kit/src/exports/vite/utils.js index ff79acd75aa3..de0cd982ec65 100644 --- a/packages/kit/src/exports/vite/utils.js +++ b/packages/kit/src/exports/vite/utils.js @@ -227,3 +227,24 @@ export function error_for_missing_config(feature_name, path, value) { ` ); } + +/** + * @param {import('vite').ViteDevServer | import('vite').PreviewServer} server + * @param {import('@sveltejs/kit').Emulator | undefined} emulator + */ +export function dispose_emulator_on_close(server, emulator) { + if (!emulator?.dispose) return; + + const close = server.close.bind(server); + /** @type {Promise | undefined} */ + let disposal; + + server.close = async () => { + try { + await close(); + } finally { + disposal ??= Promise.resolve().then(() => emulator.dispose?.()); + await disposal; + } + }; +} diff --git a/packages/kit/src/exports/vite/utils.spec.js b/packages/kit/src/exports/vite/utils.spec.js index 90c9ee0c91e1..a033c6c6c7b6 100644 --- a/packages/kit/src/exports/vite/utils.spec.js +++ b/packages/kit/src/exports/vite/utils.spec.js @@ -1,9 +1,13 @@ import path from 'node:path'; -import { expect, test } from 'vitest'; +import { expect, test, vi } from 'vitest'; import { validate_config } from '../../core/config/index.js'; import { posixify } from '../../utils/filesystem.js'; import { dedent } from '../../core/sync/utils.js'; -import { get_config_aliases, error_for_missing_config } from './utils.js'; +import { + dispose_emulator_on_close, + get_config_aliases, + error_for_missing_config +} from './utils.js'; test('transform kit.alias to resolve.alias', () => { const config = validate_config({ @@ -116,3 +120,28 @@ test('error_for_missing_config - handles special characters in feature name', () ` ); }); + +test('disposes an emulator once when the Vite server closes', async () => { + const close = vi.fn(); + const dispose = vi.fn(); + const server = /** @type {import('vite').ViteDevServer} */ (/** @type {unknown} */ ({ close })); + + dispose_emulator_on_close(server, { dispose }); + await server.close(); + await server.close(); + + expect(close).toHaveBeenCalledTimes(2); + expect(dispose).toHaveBeenCalledOnce(); +}); + +test('disposes an emulator when Vite server shutdown fails', async () => { + const error = new Error('shutdown failed'); + const close = vi.fn().mockRejectedValue(error); + const dispose = vi.fn(); + const server = /** @type {import('vite').PreviewServer} */ (/** @type {unknown} */ ({ close })); + + dispose_emulator_on_close(server, { dispose }); + + await expect(server.close()).rejects.toBe(error); + expect(dispose).toHaveBeenCalledOnce(); +}); diff --git a/packages/kit/test/apps/basics/svelte.config.js b/packages/kit/test/apps/basics/svelte.config.js index 63e29e4c37ab..51321e5c63b3 100644 --- a/packages/kit/test/apps/basics/svelte.config.js +++ b/packages/kit/test/apps/basics/svelte.config.js @@ -1,3 +1,4 @@ +import fs from 'node:fs'; import process from 'node:process'; /** @type {import('@sveltejs/kit').Config} */ @@ -18,6 +19,9 @@ const config = { return { platform({ config, prerender }) { return { config, prerender }; + }, + dispose() { + fs.writeFileSync('.svelte-kit/emulator-disposed', ''); } }; }, diff --git a/packages/kit/test/apps/basics/test/server.test.js b/packages/kit/test/apps/basics/test/server.test.js index 806d0efd281c..8e03ef0422ab 100644 --- a/packages/kit/test/apps/basics/test/server.test.js +++ b/packages/kit/test/apps/basics/test/server.test.js @@ -13,6 +13,11 @@ test.skip(({ javaScriptEnabled }) => javaScriptEnabled); test.describe.configure({ mode: 'parallel' }); +test('disposes the adapter emulator after prerendering', () => { + test.skip(!!process.env.DEV, 'only runs during build'); + expect(fs.existsSync('.svelte-kit/emulator-disposed')).toBe(true); +}); + test.describe('Caching', () => { test('caches pages', async ({ request }) => { const response = await request.get('/caching'); diff --git a/packages/kit/test/apps/basics/test/setup.js b/packages/kit/test/apps/basics/test/setup.js index 3e258a512b48..499a3e094501 100644 --- a/packages/kit/test/apps/basics/test/setup.js +++ b/packages/kit/test/apps/basics/test/setup.js @@ -1,6 +1,8 @@ import fs from 'node:fs'; import process from 'node:process'; +fs.rmSync('.svelte-kit/emulator-disposed', { force: true }); + if (process.platform !== 'win32') { process.chdir('src/routes/routing'); fs.rmSync('symlink-from', { recursive: true, force: true }); diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 34252a7e66d0..f00d412774c5 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -300,6 +300,11 @@ declare module '@sveltejs/kit' { * and returns an `App.Platform` object */ platform?(details: { config: any; prerender: PrerenderOption }): MaybePromise; + + /** + * A function that is called when the emulator is no longer needed, allowing adapters to release any resources they acquired + */ + dispose?(): MaybePromise; } export interface KitConfig {