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/.github/actions/netlify-deploy/action.yml b/.github/actions/netlify-deploy/action.yml deleted file mode 100644 index 1d2cc2361b30..000000000000 --- a/.github/actions/netlify-deploy/action.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: 'Netlify Deploy' -description: 'Set up the repo and deploy a test app to Netlify' - -inputs: - test-app-dir: - description: 'Path to the test app directory' - 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: ./.github/actions/node-setup - - - name: Build adapter-netlify files - shell: bash - working-directory: packages/adapter-netlify - run: pnpm prepublishOnly - - - name: Install Netlify CLI - shell: bash - run: pnpm i -g netlify-cli - - - name: Deploy to Netlify - id: deploy - shell: bash - env: - TEST_APP_DIR: ${{ inputs.test-app-dir }} - 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" diff --git a/.github/actions/platform-test/action.yml b/.github/actions/platform-test/action.yml deleted file mode 100644 index 0bf5a85f011e..000000000000 --- a/.github/actions/platform-test/action.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: 'Platform Test' -description: 'Run Playwright tests against a deployed platform preview' - -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' - 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 }} - env: - DEPLOYMENT_URL: ${{ inputs.deployment-url }} - 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 ab48d1216a13..000000000000 --- a/.github/actions/vercel-deploy/action.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: 'Vercel Deploy' -description: 'Set up the repo and deploy a test app to Vercel' - -inputs: - # 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 - vercel-token: - description: 'Vercel authentication token' - required: true - vercel-org-id: - description: 'Vercel organization ID' - required: true -outputs: - deployment-url: - description: 'URL of the deployed preview' - value: ${{ steps.deploy.outputs.url }} - -runs: - using: 'composite' - steps: - - uses: ./.github/actions/node-setup - - - name: Install Vercel CLI - shell: bash - run: pnpm i -g vercel - - - name: Deploy to Vercel - id: deploy - 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" diff --git a/.github/workflows/platform-test.yml b/.github/workflows/platform-test.yml new file mode 100644 index 000000000000..f7bce2859bfa --- /dev/null +++ b/.github/workflows/platform-test.yml @@ -0,0 +1,181 @@ +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 + + # 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 }} + # 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 3b54b30aac21..349688bc4461 100644 --- a/.github/workflows/platform-tests-all.yml +++ b/.github/workflows/platform-tests-all.yml @@ -7,59 +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 }} - secrets: inherit + 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 fdad830b8b75..000000000000 --- a/.github/workflows/platform-tests-netlify.yml +++ /dev/null @@ -1,95 +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 - -env: - PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1' - -jobs: - test-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 }} - - test-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 }} - - test-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/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 }} diff --git a/.github/workflows/platform-tests-node.yml b/.github/workflows/platform-tests-node.yml deleted file mode 100644 index fba3585e3571..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/platform-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 a0a4d0c4a826..000000000000 --- a/.github/workflows/platform-tests-vercel.yml +++ /dev/null @@ -1,45 +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 - -env: - PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1' - -jobs: - test-basic: - if: github.repository == 'sveltejs/kit' - runs-on: ubuntu-latest - timeout-minutes: 15 - environment: '@sveltejs/adapter-vercel platform tests' - steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: ${{ inputs.sha || github.sha }} - - - 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 }} 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: 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..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`: @@ -100,7 +102,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` 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/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/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/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/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/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/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() }; 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'; 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 23f2d730e9b6..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 { @@ -3026,7 +3031,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 +3040,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 +3051,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 +3672,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 +3841,11 @@ declare module '$app/state' { declare module '$app/stores' { export function getStores(): { - + page: typeof page; - + navigating: typeof navigating; - + updated: typeof updated; }; /** @@ -4025,4 +4029,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':