diff --git a/.changeset/config.json b/.changeset/config.json index 55fef6e0b141..6228a2412840 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -9,5 +9,6 @@ "access": "public", "baseBranch": "main", "bumpVersionsWithWorkspaceProtocolOnly": true, - "ignore": ["!(@sveltejs/*)"] + "ignore": ["!(@sveltejs/*)"], + "format": false } diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 000000000000..421211bc431f --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,4 @@ +{ + "mode": "exit", + "tag": "next" +} diff --git a/.changeset/pre/action-error-result.md b/.changeset/pre/action-error-result.md new file mode 100644 index 000000000000..318565537a36 --- /dev/null +++ b/.changeset/pre/action-error-result.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +chore: share the action error result between form actions and remote forms diff --git a/.changeset/pre/adapter-node-manifest-mime-types.md b/.changeset/pre/adapter-node-manifest-mime-types.md new file mode 100644 index 000000000000..1d59ca72c56b --- /dev/null +++ b/.changeset/pre/adapter-node-manifest-mime-types.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-node': patch +--- + +fix: serve static files with the Content-Type recorded in the manifest diff --git a/.changeset/pre/adapter-node-no-prebuild.md b/.changeset/pre/adapter-node-no-prebuild.md new file mode 100644 index 000000000000..8be5832560c4 --- /dev/null +++ b/.changeset/pre/adapter-node-no-prebuild.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-node': patch +--- + +chore: bundle the adapter's server source directly instead of prebuilding it with rolldown diff --git a/.changeset/pre/afraid-peaches-stay.md b/.changeset/pre/afraid-peaches-stay.md new file mode 100644 index 000000000000..414bb33f131f --- /dev/null +++ b/.changeset/pre/afraid-peaches-stay.md @@ -0,0 +1,6 @@ +--- +"@sveltejs/kit": major +--- + +breaking: move remote function types to `$app/server` + \ No newline at end of file diff --git a/.changeset/pre/all-donkeys-rest.md b/.changeset/pre/all-donkeys-rest.md new file mode 100644 index 000000000000..ee89f1cc1561 --- /dev/null +++ b/.changeset/pre/all-donkeys-rest.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: persist global app state across module graph reloads diff --git a/.changeset/pre/allow-hyphenated-params-matchers.md b/.changeset/pre/allow-hyphenated-params-matchers.md new file mode 100644 index 000000000000..e1d4b387d5ca --- /dev/null +++ b/.changeset/pre/allow-hyphenated-params-matchers.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': minor +--- + +feat: allow hyphens in param and matcher names diff --git a/.changeset/pre/app-manifest-module.md b/.changeset/pre/app-manifest-module.md new file mode 100644 index 000000000000..0e115a368f83 --- /dev/null +++ b/.changeset/pre/app-manifest-module.md @@ -0,0 +1,5 @@ +--- +"@sveltejs/kit": minor +--- + +feat: add `$app/manifest` module with `immutable`, `assets`, `prerendered`, and `routes` exports diff --git a/.changeset/pre/app-types-path-template.md b/.changeset/pre/app-types-path-template.md new file mode 100644 index 000000000000..99c9c25d49cb --- /dev/null +++ b/.changeset/pre/app-types-path-template.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: generate valid `Path` types for routes with optional or rest params, several params in one segment, or escape sequences diff --git a/.changeset/pre/as-default-until-edited.md b/.changeset/pre/as-default-until-edited.md new file mode 100644 index 000000000000..614d3506f4cf --- /dev/null +++ b/.changeset/pre/as-default-until-edited.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: coerce values typed into remote form fields, and only apply the default given to `.as()` until the field is edited diff --git a/.changeset/pre/beige-lights-taste.md b/.changeset/pre/beige-lights-taste.md new file mode 100644 index 000000000000..ee1b2dde81ee --- /dev/null +++ b/.changeset/pre/beige-lights-taste.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': minor +--- + +feat: ignore files with + prefix if they contain test/spec/stories diff --git a/.changeset/pre/better-clocks-own.md b/.changeset/pre/better-clocks-own.md new file mode 100644 index 000000000000..44d14bf69c5a --- /dev/null +++ b/.changeset/pre/better-clocks-own.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: remove `experimental.handleRenderingErrors` flag diff --git a/.changeset/pre/better-glasses-decide.md b/.changeset/pre/better-glasses-decide.md new file mode 100644 index 000000000000..413018ee119c --- /dev/null +++ b/.changeset/pre/better-glasses-decide.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-cloudflare': major +--- + +breaking: minimum Wrangler version required is `4.118.0` diff --git a/.changeset/pre/better-rats-care.md b/.changeset/pre/better-rats-care.md new file mode 100644 index 000000000000..1466c192fb1a --- /dev/null +++ b/.changeset/pre/better-rats-care.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: make `getRequest` and `setResponse` synchronous diff --git a/.changeset/pre/blue-plants-work.md b/.changeset/pre/blue-plants-work.md new file mode 100644 index 000000000000..51ffe066c255 --- /dev/null +++ b/.changeset/pre/blue-plants-work.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: TypeScript 6 is now the minimum required version diff --git a/.changeset/pre/blur-svg-focus.md b/.changeset/pre/blur-svg-focus.md new file mode 100644 index 000000000000..77c2e50dc0c8 --- /dev/null +++ b/.changeset/pre/blur-svg-focus.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: blur focused SVG elements before the DOM update on navigation diff --git a/.changeset/pre/bold-aliens-know.md b/.changeset/pre/bold-aliens-know.md new file mode 100644 index 000000000000..caf8ae728821 --- /dev/null +++ b/.changeset/pre/bold-aliens-know.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-vercel': patch +--- + +chore: bump `@vercel/nft` dependency diff --git a/.changeset/pre/brave-canyons-crash.md b/.changeset/pre/brave-canyons-crash.md new file mode 100644 index 000000000000..b64985e7ea20 --- /dev/null +++ b/.changeset/pre/brave-canyons-crash.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': minor +--- + +feat: add `ErrorProps` to generated types diff --git a/.changeset/pre/brave-files-cheat.md b/.changeset/pre/brave-files-cheat.md new file mode 100644 index 000000000000..a351ee978454 --- /dev/null +++ b/.changeset/pre/brave-files-cheat.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': minor +--- + +feat: support sourcemaps in production diff --git a/.changeset/pre/brave-params-resolve.md b/.changeset/pre/brave-params-resolve.md new file mode 100644 index 000000000000..a46d085c4724 --- /dev/null +++ b/.changeset/pre/brave-params-resolve.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: support bigint params in server-side route resolution diff --git a/.changeset/pre/brave-streams-yield.md b/.changeset/pre/brave-streams-yield.md new file mode 100644 index 000000000000..ab3778d6008a --- /dev/null +++ b/.changeset/pre/brave-streams-yield.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +chore: build streamed responses from async generators diff --git a/.changeset/pre/brave-types-start.md b/.changeset/pre/brave-types-start.md new file mode 100644 index 000000000000..25827f08969a --- /dev/null +++ b/.changeset/pre/brave-types-start.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: generate types when dev server starts diff --git a/.changeset/pre/breezy-bananas-divide.md b/.changeset/pre/breezy-bananas-divide.md new file mode 100644 index 000000000000..086e36a3eba4 --- /dev/null +++ b/.changeset/pre/breezy-bananas-divide.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: refresh all load functions/queries when clicking a link to the current URL diff --git a/.changeset/pre/bright-mails-repair.md b/.changeset/pre/bright-mails-repair.md new file mode 100644 index 000000000000..f01879959d20 --- /dev/null +++ b/.changeset/pre/bright-mails-repair.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: allow `undefined` values to be passed to form field `.as(...)` where applicable diff --git a/.changeset/pre/bright-paths-proxy.md b/.changeset/pre/bright-paths-proxy.md new file mode 100644 index 000000000000..ce233906c15a --- /dev/null +++ b/.changeset/pre/bright-paths-proxy.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: respect `paths.relative` during development for client files diff --git a/.changeset/pre/bright-queues-wait.md b/.changeset/pre/bright-queues-wait.md new file mode 100644 index 000000000000..2afe609234be --- /dev/null +++ b/.changeset/pre/bright-queues-wait.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: validate prerender concurrency diff --git a/.changeset/pre/bright-trees-listen.md b/.changeset/pre/bright-trees-listen.md new file mode 100644 index 000000000000..6fe760eb1023 --- /dev/null +++ b/.changeset/pre/bright-trees-listen.md @@ -0,0 +1,5 @@ +--- +"@sveltejs/kit": major +--- + +breaking: move remote function types to `$app/server` diff --git a/.changeset/pre/brisk-otters-glow.md b/.changeset/pre/brisk-otters-glow.md new file mode 100644 index 000000000000..798b6f925f28 --- /dev/null +++ b/.changeset/pre/brisk-otters-glow.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-auto': patch +--- + +fix: convert resolved adapter path to a file URL before importing, so builds work on Windows diff --git a/.changeset/pre/brown-bears-retire-adapters.md b/.changeset/pre/brown-bears-retire-adapters.md new file mode 100644 index 000000000000..5b03c0f3415d --- /dev/null +++ b/.changeset/pre/brown-bears-retire-adapters.md @@ -0,0 +1,9 @@ +--- +'@sveltejs/adapter-node': patch +'@sveltejs/adapter-static': patch +'@sveltejs/adapter-cloudflare': patch +'@sveltejs/adapter-netlify': patch +'@sveltejs/adapter-vercel': patch +--- + +chore: use `node:fs` instead of deprecated `builder.rimraf` and `builder.mkdirp` diff --git a/.changeset/pre/brown-bears-retire.md b/.changeset/pre/brown-bears-retire.md new file mode 100644 index 000000000000..600c035e7b09 --- /dev/null +++ b/.changeset/pre/brown-bears-retire.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +chore: deprecate `builder.rimraf` and `builder.mkdirp` in favour of `node:fs` methods diff --git a/.changeset/pre/bumpy-houses-show.md b/.changeset/pre/bumpy-houses-show.md new file mode 100644 index 000000000000..0fce99cbcfd6 --- /dev/null +++ b/.changeset/pre/bumpy-houses-show.md @@ -0,0 +1,6 @@ +--- +"@sveltejs/kit": patch +--- + +fix: manipulate stack trace for errors that happen while generating prerender inputs + \ No newline at end of file diff --git a/.changeset/pre/bun-adapter-instrumented-build.md b/.changeset/pre/bun-adapter-instrumented-build.md new file mode 100644 index 000000000000..45b9f6ab3070 --- /dev/null +++ b/.changeset/pre/bun-adapter-instrumented-build.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-bun': patch +--- + +fix: build apps that use server instrumentation diff --git a/.changeset/pre/bun-adapter-requires-1-4.md b/.changeset/pre/bun-adapter-requires-1-4.md new file mode 100644 index 000000000000..373a59a66d9f --- /dev/null +++ b/.changeset/pre/bun-adapter-requires-1-4.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-bun': major +--- + +breaking: require Bun 1.4, which routes `HEAD` to `GET` handlers and settles `stop()` after a force close diff --git a/.changeset/pre/bun-external-dependencies.md b/.changeset/pre/bun-external-dependencies.md new file mode 100644 index 000000000000..180c596cd76f --- /dev/null +++ b/.changeset/pre/bun-external-dependencies.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-bun': major +--- + +breaking: build the server with Vite instead of a second `Bun.build` pass, keeping production dependencies external; `buildOptions` now only applies to executables diff --git a/.changeset/pre/callable-param-matchers.md b/.changeset/pre/callable-param-matchers.md new file mode 100644 index 000000000000..b6f6bf3107c7 --- /dev/null +++ b/.changeset/pre/callable-param-matchers.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: don't treat callable standard schemas as function param matchers diff --git a/.changeset/pre/calm-adapters-handoff.md b/.changeset/pre/calm-adapters-handoff.md new file mode 100644 index 000000000000..4a8c43ba8268 --- /dev/null +++ b/.changeset/pre/calm-adapters-handoff.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-node': patch +--- + +chore: hand adapt-time values to the server as a module instead of patching built chunks diff --git a/.changeset/pre/calm-bats-build.md b/.changeset/pre/calm-bats-build.md new file mode 100644 index 000000000000..b9f6aeb56c97 --- /dev/null +++ b/.changeset/pre/calm-bats-build.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-vercel': patch +--- + +fix: omit ISR data endpoints for server-only routes diff --git a/.changeset/pre/calm-cats-stay.md b/.changeset/pre/calm-cats-stay.md new file mode 100644 index 000000000000..5c9b83ca4f15 --- /dev/null +++ b/.changeset/pre/calm-cats-stay.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: prevent non-redirect enhanced form results from navigating to another origin diff --git a/.changeset/pre/calm-envs-explain.md b/.changeset/pre/calm-envs-explain.md new file mode 100644 index 000000000000..d313b76790c3 --- /dev/null +++ b/.changeset/pre/calm-envs-explain.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: clarify circular imports from `src/env` diff --git a/.changeset/pre/calm-files-compress.md b/.changeset/pre/calm-files-compress.md new file mode 100644 index 000000000000..565fda9070d8 --- /dev/null +++ b/.changeset/pre/calm-files-compress.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: keep memory flat when precompressing many files diff --git a/.changeset/pre/calm-forms-prerender.md b/.changeset/pre/calm-forms-prerender.md new file mode 100644 index 000000000000..ab7d6248f811 --- /dev/null +++ b/.changeset/pre/calm-forms-prerender.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: render remote form actions while prerendering diff --git a/.changeset/pre/calm-loaders-rest.md b/.changeset/pre/calm-loaders-rest.md new file mode 100644 index 000000000000..c61657f086ea --- /dev/null +++ b/.changeset/pre/calm-loaders-rest.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: ignore Vitest browser loader HTML transforms diff --git a/.changeset/pre/calm-otters-package.md b/.changeset/pre/calm-otters-package.md new file mode 100644 index 000000000000..36d842d4cca8 --- /dev/null +++ b/.changeset/pre/calm-otters-package.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/package': patch +--- + +chore: remove dependency on sade diff --git a/.changeset/pre/calm-preloads-rest.md b/.changeset/pre/calm-preloads-rest.md new file mode 100644 index 000000000000..d5d2f873db3e --- /dev/null +++ b/.changeset/pre/calm-preloads-rest.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: prevent failed link preloads from causing unhandled promise rejections in production diff --git a/.changeset/pre/calm-roots-align.md b/.changeset/pre/calm-roots-align.md new file mode 100644 index 000000000000..f99411102f9a --- /dev/null +++ b/.changeset/pre/calm-roots-align.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: resolve generated rootDirs from the project root diff --git a/.changeset/pre/calm-routes-smile.md b/.changeset/pre/calm-routes-smile.md new file mode 100644 index 000000000000..d032f79bc0e2 --- /dev/null +++ b/.changeset/pre/calm-routes-smile.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: generate route resolution modules as siblings of `.html` pages diff --git a/.changeset/pre/calm-streams-flow.md b/.changeset/pre/calm-streams-flow.md new file mode 100644 index 000000000000..86eb2dfe9493 --- /dev/null +++ b/.changeset/pre/calm-streams-flow.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +perf: parse large streamed frames in linear time diff --git a/.changeset/pre/chain-body-reads.md b/.changeset/pre/chain-body-reads.md new file mode 100644 index 000000000000..fcf976f1d5d5 --- /dev/null +++ b/.changeset/pre/chain-body-reads.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: keep at most one pending body read at a time when deserializing binary forms diff --git a/.changeset/pre/chilly-berries-press.md b/.changeset/pre/chilly-berries-press.md new file mode 100644 index 000000000000..f6f1dee5b098 --- /dev/null +++ b/.changeset/pre/chilly-berries-press.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: upgrade to `cookie` v2. Cookie names must now contain only ASCII characters diff --git a/.changeset/pre/chilly-ravens-kneel.md b/.changeset/pre/chilly-ravens-kneel.md new file mode 100644 index 000000000000..35cdd62003ae --- /dev/null +++ b/.changeset/pre/chilly-ravens-kneel.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-cloudflare': patch +--- + +chore: bump `@cloudflare/workers-types` to `4.20260621.1` diff --git a/.changeset/pre/clean-headers-escape.md b/.changeset/pre/clean-headers-escape.md new file mode 100644 index 000000000000..f08d46c6aa0d --- /dev/null +++ b/.changeset/pre/clean-headers-escape.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: escape cache-control headers in prerendered HTML diff --git a/.changeset/pre/clean-papers-give.md b/.changeset/pre/clean-papers-give.md new file mode 100644 index 000000000000..44d488dfee8f --- /dev/null +++ b/.changeset/pre/clean-papers-give.md @@ -0,0 +1,6 @@ +--- +'@sveltejs/adapter-netlify': major +'@sveltejs/adapter-vercel': major +--- + +chore: use `rolldown` for edge function bundling diff --git a/.changeset/pre/cloudflare-dispose-proxy.md b/.changeset/pre/cloudflare-dispose-proxy.md new file mode 100644 index 000000000000..6926b9151f8b --- /dev/null +++ b/.changeset/pre/cloudflare-dispose-proxy.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-cloudflare': patch +--- + +fix: dispose the platform proxy when the Vite dev or preview server closes diff --git a/.changeset/pre/cold-carrots-raise.md b/.changeset/pre/cold-carrots-raise.md new file mode 100644 index 000000000000..5b77b8c63f25 --- /dev/null +++ b/.changeset/pre/cold-carrots-raise.md @@ -0,0 +1,7 @@ +--- +'@sveltejs/package': major +'@sveltejs/kit': major +'@sveltejs/enhanced-img': major +--- + +breaking: require Node 22 or newer diff --git a/.changeset/pre/cold-pans-decode.md b/.changeset/pre/cold-pans-decode.md new file mode 100644 index 000000000000..0487f90c957f --- /dev/null +++ b/.changeset/pre/cold-pans-decode.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: decode all numeric character references, including above `ffff`, when crawling prerendered pages diff --git a/.changeset/pre/cold-seals-write.md b/.changeset/pre/cold-seals-write.md new file mode 100644 index 000000000000..d7e98041f7f7 --- /dev/null +++ b/.changeset/pre/cold-seals-write.md @@ -0,0 +1,6 @@ +--- +"@sveltejs/kit": patch +--- + +fix: enforce request body size limits when Content-Type is absent + \ No newline at end of file diff --git a/.changeset/pre/collect-nested-refreshes.md b/.changeset/pre/collect-nested-refreshes.md new file mode 100644 index 000000000000..c9a38622219d --- /dev/null +++ b/.changeset/pre/collect-nested-refreshes.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: include queries refreshed from within another query in the serialized response diff --git a/.changeset/pre/common-wings-doubt.md b/.changeset/pre/common-wings-doubt.md new file mode 100644 index 000000000000..594c9acb5d80 --- /dev/null +++ b/.changeset/pre/common-wings-doubt.md @@ -0,0 +1,5 @@ +--- +"@sveltejs/kit": patch +--- + +fix: reject malformed streamed data encoding diff --git a/.changeset/pre/compress-returns-files.md b/.changeset/pre/compress-returns-files.md new file mode 100644 index 000000000000..469b582811bc --- /dev/null +++ b/.changeset/pre/compress-returns-files.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': minor +--- + +feat: return the list of compressed files from `builder.compress` diff --git a/.changeset/pre/cookies-get-cached-parse.md b/.changeset/pre/cookies-get-cached-parse.md new file mode 100644 index 000000000000..116660555683 --- /dev/null +++ b/.changeset/pre/cookies-get-cached-parse.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +perf: cache the default cookie header parse and avoid allocations in `cookies.get` diff --git a/.changeset/pre/cookies-getall-delete-phantom.md b/.changeset/pre/cookies-getall-delete-phantom.md new file mode 100644 index 000000000000..98ef96f66ac3 --- /dev/null +++ b/.changeset/pre/cookies-getall-delete-phantom.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: exclude deleted cookies from `cookies.getAll()` so it stays consistent with `cookies.get()` diff --git a/.changeset/pre/cozy-crabs-attend.md b/.changeset/pre/cozy-crabs-attend.md new file mode 100644 index 000000000000..586ac1fe300c --- /dev/null +++ b/.changeset/pre/cozy-crabs-attend.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: hide stack traces for internal errors like 404s diff --git a/.changeset/pre/cozy-hairs-love.md b/.changeset/pre/cozy-hairs-love.md new file mode 100644 index 000000000000..70ce9b28029b --- /dev/null +++ b/.changeset/pre/cozy-hairs-love.md @@ -0,0 +1,6 @@ +--- +"@sveltejs/kit": major +--- + +major: error when a client-requested single-flight mutation isn't respected by the server, allow the server to explicitly ignore refreshes + diff --git a/.changeset/pre/crisp-adults-give.md b/.changeset/pre/crisp-adults-give.md new file mode 100644 index 000000000000..a89b0809bcfd --- /dev/null +++ b/.changeset/pre/crisp-adults-give.md @@ -0,0 +1,6 @@ +--- +"@sveltejs/package": patch +--- + +chore: bump `svelte2tsx` to 0.7.59 + \ No newline at end of file diff --git a/.changeset/pre/crisp-steaks-jump.md b/.changeset/pre/crisp-steaks-jump.md new file mode 100644 index 000000000000..1607fdfa13b4 --- /dev/null +++ b/.changeset/pre/crisp-steaks-jump.md @@ -0,0 +1,6 @@ +--- +"@sveltejs/kit": minor +--- + +feat: better response logging + \ No newline at end of file diff --git a/.changeset/pre/csp-unsafe-inline-helper.md b/.changeset/pre/csp-unsafe-inline-helper.md new file mode 100644 index 000000000000..04bcb7c46f80 --- /dev/null +++ b/.changeset/pre/csp-unsafe-inline-helper.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +chore: deduplicate repeated CSP directive handling diff --git a/.changeset/pre/cuddly-hands-buy.md b/.changeset/pre/cuddly-hands-buy.md new file mode 100644 index 000000000000..cb8838059f00 --- /dev/null +++ b/.changeset/pre/cuddly-hands-buy.md @@ -0,0 +1,6 @@ +--- +'@sveltejs/adapter-netlify': patch +'@sveltejs/adapter-vercel': patch +--- + +fix: correctly apply `reroute` results for apps configured with split serverless functions diff --git a/.changeset/pre/cuddly-radios-brush.md b/.changeset/pre/cuddly-radios-brush.md new file mode 100644 index 000000000000..96091fec4f61 --- /dev/null +++ b/.changeset/pre/cuddly-radios-brush.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: remove the `preloadStrategy` option. `modulepreload` will always be used diff --git a/.changeset/pre/cuddly-tigers-attend.md b/.changeset/pre/cuddly-tigers-attend.md new file mode 100644 index 000000000000..2d1d9c4b5929 --- /dev/null +++ b/.changeset/pre/cuddly-tigers-attend.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: default the cookie `path` option to `'/'` diff --git a/.changeset/pre/curly-cats-smile.md b/.changeset/pre/curly-cats-smile.md new file mode 100644 index 000000000000..4e95c05b4712 --- /dev/null +++ b/.changeset/pre/curly-cats-smile.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: render the nearest error page when a form submission receives a non-ActionResult error response diff --git a/.changeset/pre/curly-taxes-hug.md b/.changeset/pre/curly-taxes-hug.md new file mode 100644 index 000000000000..577e412b65cd --- /dev/null +++ b/.changeset/pre/curly-taxes-hug.md @@ -0,0 +1,6 @@ +--- +"@sveltejs/kit": patch +--- + +chore: bump `mrmime` to 2.0.1 + \ No newline at end of file diff --git a/.changeset/pre/curly-webs-lie.md b/.changeset/pre/curly-webs-lie.md new file mode 100644 index 000000000000..ffccd2bb4ad0 --- /dev/null +++ b/.changeset/pre/curly-webs-lie.md @@ -0,0 +1,6 @@ +--- +"@sveltejs/kit": patch +--- + +fix: correctly implement Vite plugin hook filters + \ No newline at end of file diff --git a/.changeset/pre/curly-wolves-watch.md b/.changeset/pre/curly-wolves-watch.md new file mode 100644 index 000000000000..da4164a20287 --- /dev/null +++ b/.changeset/pre/curly-wolves-watch.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: ignore nested outDir files outside generated diff --git a/.changeset/pre/curvy-seas-camp.md b/.changeset/pre/curvy-seas-camp.md new file mode 100644 index 000000000000..a27ed7fa6153 --- /dev/null +++ b/.changeset/pre/curvy-seas-camp.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': minor +--- + +feat: warn when tsconfig doesn't exclude service worker diff --git a/.changeset/pre/cute-buses-camp.md b/.changeset/pre/cute-buses-camp.md new file mode 100644 index 000000000000..3ca0727cc833 --- /dev/null +++ b/.changeset/pre/cute-buses-camp.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/package': minor +--- + +feat: warn when using a `.server.` file or file inside a `server` directory without importing a server-only module diff --git a/.changeset/pre/cyan-zoos-love.md b/.changeset/pre/cyan-zoos-love.md new file mode 100644 index 000000000000..391ee6403465 --- /dev/null +++ b/.changeset/pre/cyan-zoos-love.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: remove `@sveltejs/kit/node/polyfills` diff --git a/.changeset/pre/deferred-query-refresh.md b/.changeset/pre/deferred-query-refresh.md new file mode 100644 index 000000000000..1c9ab7689f08 --- /dev/null +++ b/.changeset/pre/deferred-query-refresh.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: defer `query.refresh()` in server commands until after the command body completes diff --git a/.changeset/pre/drain-unconsumed-request-body.md b/.changeset/pre/drain-unconsumed-request-body.md new file mode 100644 index 000000000000..f115df9917a7 --- /dev/null +++ b/.changeset/pre/drain-unconsumed-request-body.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: drain unconsumed request bodies so keep-alive connections don't hang diff --git a/.changeset/pre/drop-scroll-restoration-reset.md b/.changeset/pre/drop-scroll-restoration-reset.md new file mode 100644 index 000000000000..1ecc68fea3db --- /dev/null +++ b/.changeset/pre/drop-scroll-restoration-reset.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: keep `history.scrollRestoration` set to `manual` when leaving the page, so a document restored from the back/forward cache does not fall back to browser scroll restoration diff --git a/.changeset/pre/dry-experts-heal.md b/.changeset/pre/dry-experts-heal.md new file mode 100644 index 000000000000..bcd4ada0b0e9 --- /dev/null +++ b/.changeset/pre/dry-experts-heal.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: add `config.kit.output.linkHeaderPreload` to preload using the `Link` header diff --git a/.changeset/pre/dry-ties-scream.md b/.changeset/pre/dry-ties-scream.md new file mode 100644 index 000000000000..a3f924c1a867 --- /dev/null +++ b/.changeset/pre/dry-ties-scream.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: resolve service worker and `tsconfig.json` based on Vite `root` setting diff --git a/.changeset/pre/eager-bears-shake.md b/.changeset/pre/eager-bears-shake.md new file mode 100644 index 000000000000..531701a62b04 --- /dev/null +++ b/.changeset/pre/eager-bears-shake.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: rebuild the dev manifest when route files disappear during an incremental update diff --git a/.changeset/pre/eager-env-init.md b/.changeset/pre/eager-env-init.md new file mode 100644 index 000000000000..4b34809d99a1 --- /dev/null +++ b/.changeset/pre/eager-env-init.md @@ -0,0 +1,9 @@ +--- +'@sveltejs/kit': patch +'@sveltejs/adapter-node': major +'@sveltejs/adapter-cloudflare': major +'@sveltejs/adapter-vercel': major +'@sveltejs/adapter-netlify': major +--- + +breaking: populate env vars before `instrumentation.server.js` is evaluated and update the adapter instrumentation API diff --git a/.changeset/pre/eager-pans-wash.md b/.changeset/pre/eager-pans-wash.md new file mode 100644 index 000000000000..63df99f15c46 --- /dev/null +++ b/.changeset/pre/eager-pans-wash.md @@ -0,0 +1,6 @@ +--- +"@sveltejs/kit": patch +--- + +fix: only print prerender progress newline when necessary + \ No newline at end of file diff --git a/.changeset/pre/early-worlds-slide.md b/.changeset/pre/early-worlds-slide.md new file mode 100644 index 000000000000..f9280d7ee391 --- /dev/null +++ b/.changeset/pre/early-worlds-slide.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: require `@sveltejs/vite-plugin-svelte` v7 diff --git a/.changeset/pre/easy-items-smile.md b/.changeset/pre/easy-items-smile.md new file mode 100644 index 000000000000..e6b13aebd0bf --- /dev/null +++ b/.changeset/pre/easy-items-smile.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': minor +--- + +feat: validate that all remote form fields were created with form.fields.foo.as(...) diff --git a/.changeset/pre/easy-pianos-float.md b/.changeset/pre/easy-pianos-float.md new file mode 100644 index 000000000000..b755e97f0680 --- /dev/null +++ b/.changeset/pre/easy-pianos-float.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: make `page.url` immutable on a type level diff --git a/.changeset/pre/easy-shrimps-like.md b/.changeset/pre/easy-shrimps-like.md new file mode 100644 index 000000000000..07f74e1d4a1c --- /dev/null +++ b/.changeset/pre/easy-shrimps-like.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/enhanced-img': minor +--- + +breaking: require Vite 8 and `vite-plugin-svelte` 7 diff --git a/.changeset/pre/easy-weeks-tan.md b/.changeset/pre/easy-weeks-tan.md new file mode 100644 index 000000000000..8bfab1133b4b --- /dev/null +++ b/.changeset/pre/easy-weeks-tan.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: remove `base`, `assets`, and `resolveRoute` from `$app/paths` diff --git a/.changeset/pre/eight-frogs-drum.md b/.changeset/pre/eight-frogs-drum.md new file mode 100644 index 000000000000..e83993678eac --- /dev/null +++ b/.changeset/pre/eight-frogs-drum.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-node': minor +--- + +feat: serve static assets from tables recorded at adapt time, instead of using sirv diff --git a/.changeset/pre/eight-lions-argue.md b/.changeset/pre/eight-lions-argue.md new file mode 100644 index 000000000000..90fb99a280f5 --- /dev/null +++ b/.changeset/pre/eight-lions-argue.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +perf: match only unpaired surrogates when escaping HTML diff --git a/.changeset/pre/eight-melons-brake.md b/.changeset/pre/eight-melons-brake.md new file mode 100644 index 000000000000..539608f5cd6c --- /dev/null +++ b/.changeset/pre/eight-melons-brake.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-netlify': patch +--- + +fix: ensure types for `platform.context` work diff --git a/.changeset/pre/eighty-cups-jam.md b/.changeset/pre/eighty-cups-jam.md new file mode 100644 index 000000000000..1b5aaa788248 --- /dev/null +++ b/.changeset/pre/eighty-cups-jam.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: treat `data:` protocol URLs as external for redirect diff --git a/.changeset/pre/eighty-donuts-argue.md b/.changeset/pre/eighty-donuts-argue.md new file mode 100644 index 000000000000..bd7ce1305678 --- /dev/null +++ b/.changeset/pre/eighty-donuts-argue.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +chore: deduplicate request hashing for serialized fetch responses diff --git a/.changeset/pre/eighty-paws-love.md b/.changeset/pre/eighty-paws-love.md new file mode 100644 index 000000000000..b301fa8f1b2c --- /dev/null +++ b/.changeset/pre/eighty-paws-love.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: remove `createEntries` from the `Builder` object passed to adapter functions diff --git a/.changeset/pre/eleven-cases-guard.md b/.changeset/pre/eleven-cases-guard.md new file mode 100644 index 000000000000..0b070264ee9a --- /dev/null +++ b/.changeset/pre/eleven-cases-guard.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: don't treat `Object.prototype` members as param matchers during validation diff --git a/.changeset/pre/elliott-is-uncreative.md b/.changeset/pre/elliott-is-uncreative.md new file mode 100644 index 000000000000..454548f4f068 --- /dev/null +++ b/.changeset/pre/elliott-is-uncreative.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: return no content for 204 responses diff --git a/.changeset/pre/empty-path-never.md b/.changeset/pre/empty-path-never.md new file mode 100644 index 000000000000..dae47becf10b --- /dev/null +++ b/.changeset/pre/empty-path-never.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: generate a `never` `Path` type when there are no routes diff --git a/.changeset/pre/enhance-http-redirects.md b/.changeset/pre/enhance-http-redirects.md new file mode 100644 index 000000000000..c9db535d48d7 --- /dev/null +++ b/.changeset/pre/enhance-http-redirects.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: follow HTTP redirects from authentication proxies when enhancing form submissions diff --git a/.changeset/pre/env-empty-string.md b/.changeset/pre/env-empty-string.md new file mode 100644 index 000000000000..e90321326601 --- /dev/null +++ b/.changeset/pre/env-empty-string.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: don't report empty environment variables as missing diff --git a/.changeset/pre/env-exports-generator.md b/.changeset/pre/env-exports-generator.md new file mode 100644 index 000000000000..586b23100c45 --- /dev/null +++ b/.changeset/pre/env-exports-generator.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +chore: generate the env modules in a single pass diff --git a/.changeset/pre/env-function-validators.md b/.changeset/pre/env-function-validators.md new file mode 100644 index 000000000000..09c0d00fc946 --- /dev/null +++ b/.changeset/pre/env-function-validators.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': minor +--- + +feat: support function validators for environment variables diff --git a/.changeset/pre/externalize-opentelemetry-api.md b/.changeset/pre/externalize-opentelemetry-api.md new file mode 100644 index 000000000000..2c1f00dc698f --- /dev/null +++ b/.changeset/pre/externalize-opentelemetry-api.md @@ -0,0 +1,6 @@ +--- +'@sveltejs/kit': patch +'@sveltejs/adapter-node': patch +--- + +fix: externalize `@opentelemetry/api` to prevent bundler chunk colocation between `instrumentation.server.js` and application code diff --git a/.changeset/pre/fair-baths-pull.md b/.changeset/pre/fair-baths-pull.md new file mode 100644 index 000000000000..ceb8ee65e2d7 --- /dev/null +++ b/.changeset/pre/fair-baths-pull.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-netlify': patch +--- + +fix: include `utils.js` in package.json `files` diff --git a/.changeset/pre/fair-forms-work.md b/.changeset/pre/fair-forms-work.md new file mode 100644 index 000000000000..0a8718224523 --- /dev/null +++ b/.changeset/pre/fair-forms-work.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: support form fields named after Object prototype properties diff --git a/.changeset/pre/famous-bars-search.md b/.changeset/pre/famous-bars-search.md new file mode 100644 index 000000000000..3e1e8b8af2ab --- /dev/null +++ b/.changeset/pre/famous-bars-search.md @@ -0,0 +1,6 @@ +--- +'@sveltejs/kit': major +--- + +breaking: rename `Pathname` type to `Path` and `Asset` to `AssetPath` +breaking: remove leading `/` from `Path` and `AssetPath` diff --git a/.changeset/pre/famous-pandas-hammer.md b/.changeset/pre/famous-pandas-hammer.md new file mode 100644 index 000000000000..0bcfccf2635c --- /dev/null +++ b/.changeset/pre/famous-pandas-hammer.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: exclude routes without a page or endpoint from `routes` in `$app/manifest`, and remove directories with no route files from `LayoutParams` diff --git a/.changeset/pre/famous-yaks-kneel.md b/.changeset/pre/famous-yaks-kneel.md new file mode 100644 index 000000000000..c86154ee00e6 --- /dev/null +++ b/.changeset/pre/famous-yaks-kneel.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: `handle`'s `resolve` is now typed to always return a `Promise` diff --git a/.changeset/pre/fast-cats-traverse.md b/.changeset/pre/fast-cats-traverse.md new file mode 100644 index 000000000000..3ce2f363c3f9 --- /dev/null +++ b/.changeset/pre/fast-cats-traverse.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: record a history traversal before resolving its route diff --git a/.changeset/pre/fast-feet-travel.md b/.changeset/pre/fast-feet-travel.md new file mode 100644 index 000000000000..9203f202904c --- /dev/null +++ b/.changeset/pre/fast-feet-travel.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: avoid Vite dev server reload on initial page request diff --git a/.changeset/pre/feat-service-worker-module.md b/.changeset/pre/feat-service-worker-module.md new file mode 100644 index 000000000000..64f935119496 --- /dev/null +++ b/.changeset/pre/feat-service-worker-module.md @@ -0,0 +1,5 @@ +--- +"@sveltejs/kit": minor +--- + +feat: use `type: 'module'` for service worker registrations diff --git a/.changeset/pre/fetch-accept-language.md b/.changeset/pre/fetch-accept-language.md new file mode 100644 index 000000000000..0594126fd625 --- /dev/null +++ b/.changeset/pre/fetch-accept-language.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: don't set a `null` `accept-language` header on internal `fetch` sub-requests when the incoming request has none diff --git a/.changeset/pre/fetch-cache-non-canonical-url.md b/.changeset/pre/fetch-cache-non-canonical-url.md new file mode 100644 index 000000000000..55ed13dc06b3 --- /dev/null +++ b/.changeset/pre/fetch-cache-non-canonical-url.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: reuse SSR-cached fetch responses during hydration when a cross-origin URL is not in canonical form diff --git a/.changeset/pre/fetch-prerendered-base.md b/.changeset/pre/fetch-prerendered-base.md new file mode 100644 index 000000000000..87e2fa759ec8 --- /dev/null +++ b/.changeset/pre/fetch-prerendered-base.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: correctly detect prerendered paths in server `fetch` when `paths.base` is set diff --git a/.changeset/pre/field-name-error.md b/.changeset/pre/field-name-error.md new file mode 100644 index 000000000000..2158d27e5a98 --- /dev/null +++ b/.changeset/pre/field-name-error.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +chore: say what a valid remote form field name looks like when rejecting one diff --git a/.changeset/pre/fields-enumeration-warning.md b/.changeset/pre/fields-enumeration-warning.md new file mode 100644 index 000000000000..a6ed2878792c --- /dev/null +++ b/.changeset/pre/fields-enumeration-warning.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +chore: warn when remote form fields are enumerated in dev diff --git a/.changeset/pre/fifty-breads-judge.md b/.changeset/pre/fifty-breads-judge.md new file mode 100644 index 000000000000..4b49fc2d8725 --- /dev/null +++ b/.changeset/pre/fifty-breads-judge.md @@ -0,0 +1,6 @@ +--- +"@sveltejs/kit": minor +--- + +feat: allow adapters to receive the Svelte config as a function argument when adding Vite plugins + \ No newline at end of file diff --git a/.changeset/pre/fine-ghosts-camp.md b/.changeset/pre/fine-ghosts-camp.md new file mode 100644 index 000000000000..6eacab83c095 --- /dev/null +++ b/.changeset/pre/fine-ghosts-camp.md @@ -0,0 +1,7 @@ +--- +'@sveltejs/adapter-netlify': major +--- + +breaking: write output that conforms to the stable [Netlify Frameworks API](https://docs.netlify.com/build/frameworks/frameworks-api/). + +Deploying and previewing with Netlify CLI now requires [v17.31.0](https://github.com/netlify/cli/releases/tag/v17.31.0) or later. Run `npm i -g netlify-cli@latest` to upgrade. diff --git a/.changeset/pre/five-readers-march.md b/.changeset/pre/five-readers-march.md new file mode 100644 index 000000000000..e9fb4707d606 --- /dev/null +++ b/.changeset/pre/five-readers-march.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-cloudflare': major +--- + +breaking: upgrade `@cloudflare/workers-types` to 4.20260219.0 diff --git a/.changeset/pre/fix-app-env-vite-node.md b/.changeset/pre/fix-app-env-vite-node.md new file mode 100644 index 000000000000..0bc33ba2c195 --- /dev/null +++ b/.changeset/pre/fix-app-env-vite-node.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: populate `$app/env/*` dynamic variables in contexts that don't run the dev server, such as `vite-node` diff --git a/.changeset/pre/fix-client-manifest-vite-root.md b/.changeset/pre/fix-client-manifest-vite-root.md new file mode 100644 index 000000000000..3255bb191953 --- /dev/null +++ b/.changeset/pre/fix-client-manifest-vite-root.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: resolve client manifest imports against the Vite root diff --git a/.changeset/pre/fix-date-form-fields-set.md b/.changeset/pre/fix-date-form-fields-set.md new file mode 100644 index 000000000000..b9715242d4f4 --- /dev/null +++ b/.changeset/pre/fix-date-form-fields-set.md @@ -0,0 +1,5 @@ +--- +"@sveltejs/kit": patch +--- + +fix: properly handle Date objects in form.fields.set diff --git a/.changeset/pre/fix-form-action-prototype-pollution.md b/.changeset/pre/fix-form-action-prototype-pollution.md new file mode 100644 index 000000000000..788903de55cc --- /dev/null +++ b/.changeset/pre/fix-form-action-prototype-pollution.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: return 404 for form actions and remote functions whose name is an `Object.prototype` member diff --git a/.changeset/pre/fix-impossible-situation-server-only.md b/.changeset/pre/fix-impossible-situation-server-only.md new file mode 100644 index 000000000000..c083bfea84a5 --- /dev/null +++ b/.changeset/pre/fix-impossible-situation-server-only.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: no longer throw "An impossible situation occurred" when a server-only module is imported by both server and client code diff --git a/.changeset/pre/fix-vite-verbose.md b/.changeset/pre/fix-vite-verbose.md new file mode 100644 index 000000000000..7c0bf1da8889 --- /dev/null +++ b/.changeset/pre/fix-vite-verbose.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: respect Vite default log level diff --git a/.changeset/pre/flat-cases-see.md b/.changeset/pre/flat-cases-see.md new file mode 100644 index 000000000000..62cf4dbde2a6 --- /dev/null +++ b/.changeset/pre/flat-cases-see.md @@ -0,0 +1,5 @@ +--- +"@sveltejs/kit": major +--- + +breaking: remove `Server` constructor and `SSRManifest` from public types diff --git a/.changeset/pre/fluffy-clouds-sing.md b/.changeset/pre/fluffy-clouds-sing.md new file mode 100644 index 000000000000..747412a45b38 --- /dev/null +++ b/.changeset/pre/fluffy-clouds-sing.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/enhanced-img': patch +--- + +chore: replace the `$lib` alias with `#lib` in docs diff --git a/.changeset/pre/fluffy-dolls-grow.md b/.changeset/pre/fluffy-dolls-grow.md new file mode 100644 index 000000000000..9974b9f90f03 --- /dev/null +++ b/.changeset/pre/fluffy-dolls-grow.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: require Svelte config options to be passed through the Vite plugin diff --git a/.changeset/pre/fluffy-tires-judge.md b/.changeset/pre/fluffy-tires-judge.md new file mode 100644 index 000000000000..f5a5e090b2f9 --- /dev/null +++ b/.changeset/pre/fluffy-tires-judge.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-cloudflare': major +--- + +breaking: upgrade minimum `wrangler` version to ^4.67.0 diff --git a/.changeset/pre/focus-starting-point.md b/.changeset/pre/focus-starting-point.md new file mode 100644 index 000000000000..8f90475687c9 --- /dev/null +++ b/.changeset/pre/focus-starting-point.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: set the focus starting point without a fragment navigation, which leaked a `hashchange` to app listeners diff --git a/.changeset/pre/form-action-http-status-codes.md b/.changeset/pre/form-action-http-status-codes.md new file mode 100644 index 000000000000..f4aaabe17a0f --- /dev/null +++ b/.changeset/pre/form-action-http-status-codes.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: form action responses now use the HTTP status code returned from `fail` diff --git a/.changeset/pre/form-for-reconnect.md b/.changeset/pre/form-for-reconnect.md new file mode 100644 index 000000000000..8d3b89b43428 --- /dev/null +++ b/.changeset/pre/form-for-reconnect.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: keep a `form.for` instance registered when the derived that holds it disconnects and reconnects diff --git a/.changeset/pre/forty-ducks-fly.md b/.changeset/pre/forty-ducks-fly.md new file mode 100644 index 000000000000..98c98783ee29 --- /dev/null +++ b/.changeset/pre/forty-ducks-fly.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-cloudflare': major +--- + +breaking: remove `platform.context` in favour of `platform.ctx` diff --git a/.changeset/pre/forty-owls-move.md b/.changeset/pre/forty-owls-move.md new file mode 100644 index 000000000000..74f18113f9f8 --- /dev/null +++ b/.changeset/pre/forty-owls-move.md @@ -0,0 +1,5 @@ +--- +"@sveltejs/adapter-netlify": patch +--- + +chore: replace @netlify/functions with @netlify/types for the Context type diff --git a/.changeset/pre/fresh-deserts-shake.md b/.changeset/pre/fresh-deserts-shake.md new file mode 100644 index 000000000000..eedf52b6f164 --- /dev/null +++ b/.changeset/pre/fresh-deserts-shake.md @@ -0,0 +1,5 @@ +--- +"@sveltejs/kit": patch +--- + +fix: make cookie options optional diff --git a/.changeset/pre/fresh-dodos-share.md b/.changeset/pre/fresh-dodos-share.md new file mode 100644 index 000000000000..637c73718c24 --- /dev/null +++ b/.changeset/pre/fresh-dodos-share.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: don't duplicate remote modules in the generated manifest diff --git a/.changeset/pre/fresh-fetches-evict.md b/.changeset/pre/fresh-fetches-evict.md new file mode 100644 index 000000000000..bc3b31cc9648 --- /dev/null +++ b/.changeset/pre/fresh-fetches-evict.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: evict hashed fetch cache entries after mutations diff --git a/.changeset/pre/fresh-pandas-smile.md b/.changeset/pre/fresh-pandas-smile.md new file mode 100644 index 000000000000..044c9dc6e98c --- /dev/null +++ b/.changeset/pre/fresh-pandas-smile.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-netlify': patch +--- + +fix: route requests with omitted optional parameters to split serverless functions diff --git a/.changeset/pre/fresh-pots-smoke.md b/.changeset/pre/fresh-pots-smoke.md new file mode 100644 index 000000000000..7ebeba8835bf --- /dev/null +++ b/.changeset/pre/fresh-pots-smoke.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: write tsconfig to `node_modules/$app/tsconfig` diff --git a/.changeset/pre/fresh-shallows-navigate.md b/.changeset/pre/fresh-shallows-navigate.md new file mode 100644 index 000000000000..10c57b644436 --- /dev/null +++ b/.changeset/pre/fresh-shallows-navigate.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': minor +--- + +feat: add shallow routing to `goto` and deprecate `pushState` and `replaceState` diff --git a/.changeset/pre/fresh-shoes-count.md b/.changeset/pre/fresh-shoes-count.md new file mode 100644 index 000000000000..b87a98b31a48 --- /dev/null +++ b/.changeset/pre/fresh-shoes-count.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +chore: reuse base64 and text decoding helpers diff --git a/.changeset/pre/friendly-removed-modules.md b/.changeset/pre/friendly-removed-modules.md new file mode 100644 index 000000000000..51e5a4f24698 --- /dev/null +++ b/.changeset/pre/friendly-removed-modules.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: explain the removal of `$lib` and `$service-worker` when their imports fail to resolve diff --git a/.changeset/pre/fruity-readers-send.md b/.changeset/pre/fruity-readers-send.md new file mode 100644 index 000000000000..bb64a9488dd4 --- /dev/null +++ b/.changeset/pre/fruity-readers-send.md @@ -0,0 +1,6 @@ +--- +"@sveltejs/kit": patch +--- + +fix: tweak response logging for remote requests + \ No newline at end of file diff --git a/.changeset/pre/full-turtles-tell.md b/.changeset/pre/full-turtles-tell.md new file mode 100644 index 000000000000..0a08847d9f61 --- /dev/null +++ b/.changeset/pre/full-turtles-tell.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: correctly massage stack traces with async frames diff --git a/.changeset/pre/funky-turkeys-serve.md b/.changeset/pre/funky-turkeys-serve.md new file mode 100644 index 000000000000..e422cb293019 --- /dev/null +++ b/.changeset/pre/funky-turkeys-serve.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: mark `RequestEvent` properties as `readonly` diff --git a/.changeset/pre/fuzzy-ducks-fix.md b/.changeset/pre/fuzzy-ducks-fix.md new file mode 100644 index 000000000000..60532c6c7bf8 --- /dev/null +++ b/.changeset/pre/fuzzy-ducks-fix.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: render the nearest `+error.svelte` at the depth it occupies when an error is thrown during rendering diff --git a/.changeset/pre/fuzzy-images-click.md b/.changeset/pre/fuzzy-images-click.md new file mode 100644 index 000000000000..843696a80924 --- /dev/null +++ b/.changeset/pre/fuzzy-images-click.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: support coordinate objects from image inputs in remote forms diff --git a/.changeset/pre/fuzzy-kings-sit.md b/.changeset/pre/fuzzy-kings-sit.md new file mode 100644 index 000000000000..e08a9fad8bce --- /dev/null +++ b/.changeset/pre/fuzzy-kings-sit.md @@ -0,0 +1,6 @@ +--- +"@sveltejs/adapter-cloudflare": major +--- + +breaking: remove cloudflare `platform`, emulate the `cloudflare:workers` module instead + \ No newline at end of file diff --git a/.changeset/pre/gentle-impalas-heal.md b/.changeset/pre/gentle-impalas-heal.md new file mode 100644 index 000000000000..89a42162dc97 --- /dev/null +++ b/.changeset/pre/gentle-impalas-heal.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: move `defineParams` and associated types to `@sveltejs/kit/params` diff --git a/.changeset/pre/gentle-radios-go.md b/.changeset/pre/gentle-radios-go.md new file mode 100644 index 000000000000..33d159e8acb5 --- /dev/null +++ b/.changeset/pre/gentle-radios-go.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: remove the deprecated CSRF `checkOrigin` option in favor of `trustedOrigins` diff --git a/.changeset/pre/gentle-signs-cross.md b/.changeset/pre/gentle-signs-cross.md new file mode 100644 index 000000000000..b819bf018351 --- /dev/null +++ b/.changeset/pre/gentle-signs-cross.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: the `delta` property now only exists for `popstate` navigation events diff --git a/.changeset/pre/gentle-toys-talk.md b/.changeset/pre/gentle-toys-talk.md new file mode 100644 index 000000000000..cb9b7082229b --- /dev/null +++ b/.changeset/pre/gentle-toys-talk.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: change `form.error` type from `any` to `App.Error | undefined` diff --git a/.changeset/pre/get-request-response-close.md b/.changeset/pre/get-request-response-close.md new file mode 100644 index 000000000000..318e3c8aa801 --- /dev/null +++ b/.changeset/pre/get-request-response-close.md @@ -0,0 +1,6 @@ +--- +'@sveltejs/kit': minor +'@sveltejs/adapter-node': patch +--- + +feat: abort `request.signal` when the response closes prematurely, via a new `response` option for `getRequest` diff --git a/.changeset/pre/giant-items-cry.md b/.changeset/pre/giant-items-cry.md new file mode 100644 index 000000000000..74d6abd385ee --- /dev/null +++ b/.changeset/pre/giant-items-cry.md @@ -0,0 +1,5 @@ +--- +"@sveltejs/kit": patch +--- + +fix: revert route metadata caching to regenerate missing root-route types during sync diff --git a/.changeset/pre/giant-numbers-care.md b/.changeset/pre/giant-numbers-care.md new file mode 100644 index 000000000000..e033cd7e2d28 --- /dev/null +++ b/.changeset/pre/giant-numbers-care.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: remove deprecated `pragma` header in version polling for improved CORS support diff --git a/.changeset/pre/gold-tips-lead.md b/.changeset/pre/gold-tips-lead.md new file mode 100644 index 000000000000..a8fd52037ada --- /dev/null +++ b/.changeset/pre/gold-tips-lead.md @@ -0,0 +1,6 @@ +--- +"@sveltejs/kit": patch +--- + +chore: bump `@sveltejs/acorn-typescript` to 1.0.12 + \ No newline at end of file diff --git a/.changeset/pre/goto-errors-external-route.md b/.changeset/pre/goto-errors-external-route.md new file mode 100644 index 000000000000..78955c4e1130 --- /dev/null +++ b/.changeset/pre/goto-errors-external-route.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: `goto` now rejects when called with a URL that does not resolve to a route within the app, matching the existing behaviour for external URLs \ No newline at end of file diff --git a/.changeset/pre/great-ears-thank.md b/.changeset/pre/great-ears-thank.md new file mode 100644 index 000000000000..8152b898a22e --- /dev/null +++ b/.changeset/pre/great-ears-thank.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: update `match` parameter type diff --git a/.changeset/pre/green-globs-shift.md b/.changeset/pre/green-globs-shift.md new file mode 100644 index 000000000000..5dd3b1d482d6 --- /dev/null +++ b/.changeset/pre/green-globs-shift.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/package': patch +--- + +fix: rewrite `.ts` extensions in `import.meta.glob` patterns when `rewriteRelativeImportExtensions` is enabled diff --git a/.changeset/pre/grumpy-sites-float.md b/.changeset/pre/grumpy-sites-float.md new file mode 100644 index 000000000000..55d6ebc378a8 --- /dev/null +++ b/.changeset/pre/grumpy-sites-float.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-static': patch +--- + +fix: avoid caching immutable asset 404s when configuring for Vercel diff --git a/.changeset/pre/handle-error-all-errors.md b/.changeset/pre/handle-error-all-errors.md new file mode 100644 index 000000000000..f40f914478e3 --- /dev/null +++ b/.changeset/pre/handle-error-all-errors.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: run all errors through the `handleError` hook diff --git a/.changeset/pre/happy-coins-think.md b/.changeset/pre/happy-coins-think.md new file mode 100644 index 000000000000..76fdac4f7088 --- /dev/null +++ b/.changeset/pre/happy-coins-think.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/package': minor +--- + +feat: transform import aliases into relative imports in files diff --git a/.changeset/pre/happy-deer-drive.md b/.changeset/pre/happy-deer-drive.md new file mode 100644 index 000000000000..945d4b46a4e1 --- /dev/null +++ b/.changeset/pre/happy-deer-drive.md @@ -0,0 +1,5 @@ +--- +"@sveltejs/kit": patch +--- + +fix: generate sourcemaps for remote modules diff --git a/.changeset/pre/hash-resolve-document.md b/.changeset/pre/hash-resolve-document.md new file mode 100644 index 000000000000..1a66ae11b842 --- /dev/null +++ b/.changeset/pre/hash-resolve-document.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: keep hash-router links on the current document when resolving paths diff --git a/.changeset/pre/heavy-showers-leave.md b/.changeset/pre/heavy-showers-leave.md new file mode 100644 index 000000000000..4cc474f845a0 --- /dev/null +++ b/.changeset/pre/heavy-showers-leave.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: require Svelte 5.56.4 or newer diff --git a/.changeset/pre/hide-query-event-url.md b/.changeset/pre/hide-query-event-url.md new file mode 100644 index 000000000000..4c4af4a22c65 --- /dev/null +++ b/.changeset/pre/hide-query-event-url.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: error on `event.url`, `event.params` and `event.route` access inside queries diff --git a/.changeset/pre/hip-apes-marry.md b/.changeset/pre/hip-apes-marry.md new file mode 100644 index 000000000000..f41133600485 --- /dev/null +++ b/.changeset/pre/hip-apes-marry.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-cloudflare': patch +--- + +fix: avoid overriding user's existing `_headers` rules diff --git a/.changeset/pre/hip-friends-boil.md b/.changeset/pre/hip-friends-boil.md new file mode 100644 index 000000000000..55716e24adc9 --- /dev/null +++ b/.changeset/pre/hip-friends-boil.md @@ -0,0 +1,6 @@ +--- +"@sveltejs/adapter-netlify": major +--- + +breaking: write static files based on the `publish` adapter option rather than reading the `netlify.toml` file + \ No newline at end of file diff --git a/.changeset/pre/hip-lions-check.md b/.changeset/pre/hip-lions-check.md new file mode 100644 index 000000000000..3723cc6dd598 --- /dev/null +++ b/.changeset/pre/hip-lions-check.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: avoid empty getElementById() call on hash routing navigation diff --git a/.changeset/pre/honest-pumas-leave.md b/.changeset/pre/honest-pumas-leave.md new file mode 100644 index 000000000000..c3e7c1d7ec27 --- /dev/null +++ b/.changeset/pre/honest-pumas-leave.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': minor +--- + +feat: make `$app/paths` importable in service workers diff --git a/.changeset/pre/hooks-module-extensions.md b/.changeset/pre/hooks-module-extensions.md new file mode 100644 index 000000000000..92b8e6818aa6 --- /dev/null +++ b/.changeset/pre/hooks-module-extensions.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: resolve every module entry point using `kit.moduleExtensions` diff --git a/.changeset/pre/hungry-hounds-argue.md b/.changeset/pre/hungry-hounds-argue.md new file mode 100644 index 000000000000..1ef15ff4f4fa --- /dev/null +++ b/.changeset/pre/hungry-hounds-argue.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': minor +--- + +feat: pass the project-relative source `filename` to the `preload` filter for fonts diff --git a/.changeset/pre/hungry-jokes-repeat.md b/.changeset/pre/hungry-jokes-repeat.md new file mode 100644 index 000000000000..b29a7a10f6e8 --- /dev/null +++ b/.changeset/pre/hungry-jokes-repeat.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: enhanced cross-page form actions now navigate to the action page on success and failure, matching native form behavior diff --git a/.changeset/pre/ico-mime-type.md b/.changeset/pre/ico-mime-type.md new file mode 100644 index 000000000000..4d0923ee7581 --- /dev/null +++ b/.changeset/pre/ico-mime-type.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: serve `.ico` files with `image/x-icon` Content-Type diff --git a/.changeset/pre/icy-turtles-leave.md b/.changeset/pre/icy-turtles-leave.md new file mode 100644 index 000000000000..6634ac126978 --- /dev/null +++ b/.changeset/pre/icy-turtles-leave.md @@ -0,0 +1,7 @@ +--- +"@sveltejs/enhanced-img": patch +"@sveltejs/kit": patch +--- + +chore: bump `magic-string` to 1.1.0 + \ No newline at end of file diff --git a/.changeset/pre/inline-page-manifest.md b/.changeset/pre/inline-page-manifest.md new file mode 100644 index 000000000000..4840de26320a --- /dev/null +++ b/.changeset/pre/inline-page-manifest.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: exclude inlined files from the page's `$app/manifest` immutable list diff --git a/.changeset/pre/invalidate-during-resolution.md b/.changeset/pre/invalidate-during-resolution.md new file mode 100644 index 000000000000..52f9f3395d49 --- /dev/null +++ b/.changeset/pre/invalidate-during-resolution.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: discard invalidation results when a navigation completes while they load diff --git a/.changeset/pre/isr-trailing-slash.md b/.changeset/pre/isr-trailing-slash.md new file mode 100644 index 000000000000..bc116043a396 --- /dev/null +++ b/.changeset/pre/isr-trailing-slash.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-vercel': patch +--- + +fix: pass the requested pathname, including any trailing slash, to ISR functions diff --git a/.changeset/pre/jolly-adults-wave.md b/.changeset/pre/jolly-adults-wave.md new file mode 100644 index 000000000000..8768b91212ba --- /dev/null +++ b/.changeset/pre/jolly-adults-wave.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: avoid client-side code being bundled by Cloudflare Wrangler diff --git a/.changeset/pre/khaki-lights-design.md b/.changeset/pre/khaki-lights-design.md new file mode 100644 index 000000000000..d406cd9ef060 --- /dev/null +++ b/.changeset/pre/khaki-lights-design.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: delete `$service-worker` module diff --git a/.changeset/pre/kind-forms.md b/.changeset/pre/kind-forms.md new file mode 100644 index 000000000000..1dcaede8fc23 --- /dev/null +++ b/.changeset/pre/kind-forms.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: wait for the redirect navigation before remote form submissions resolve diff --git a/.changeset/pre/kit-prerendered-mime-types.md b/.changeset/pre/kit-prerendered-mime-types.md new file mode 100644 index 000000000000..c368d781dcb8 --- /dev/null +++ b/.changeset/pre/kit-prerendered-mime-types.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: record the mime types of prerendered paths in the server manifest diff --git a/.changeset/pre/large-onions-attack.md b/.changeset/pre/large-onions-attack.md new file mode 100644 index 000000000000..aef0217173b9 --- /dev/null +++ b/.changeset/pre/large-onions-attack.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-node': major +--- + +chore: migrate from rollup to rolldown diff --git a/.changeset/pre/large-pugs-clap.md b/.changeset/pre/large-pugs-clap.md new file mode 100644 index 000000000000..1bf95a91ef76 --- /dev/null +++ b/.changeset/pre/large-pugs-clap.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-static': patch +--- + +fix: match adapter-vercel's prerendered redirect handling when deploying to Vercel diff --git a/.changeset/pre/late-lands-watch.md b/.changeset/pre/late-lands-watch.md new file mode 100644 index 000000000000..dee8903df837 --- /dev/null +++ b/.changeset/pre/late-lands-watch.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': minor +--- + +feat: preserve page state set through `goto(..., { state, persistState: true })` across reloads diff --git a/.changeset/pre/late-lions-crash.md b/.changeset/pre/late-lions-crash.md new file mode 100644 index 000000000000..f120c80d8caa --- /dev/null +++ b/.changeset/pre/late-lions-crash.md @@ -0,0 +1,9 @@ +--- +"@sveltejs/adapter-auto": major +"@sveltejs/adapter-cloudflare": major +"@sveltejs/adapter-netlify": major +"@sveltejs/adapter-node": major +"@sveltejs/adapter-static": major +--- + +breaking: require SvelteKit 3 diff --git a/.changeset/pre/lazy-glasses-love.md b/.changeset/pre/lazy-glasses-love.md new file mode 100644 index 000000000000..89570d79d0ad --- /dev/null +++ b/.changeset/pre/lazy-glasses-love.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: move `defineEnvVars` to `@sveltejs/kit/env` diff --git a/.changeset/pre/lazy-nights-lay.md b/.changeset/pre/lazy-nights-lay.md new file mode 100644 index 000000000000..d90b6229d358 --- /dev/null +++ b/.changeset/pre/lazy-nights-lay.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: nested server-only directories diff --git a/.changeset/pre/lazy-pugs-refuse.md b/.changeset/pre/lazy-pugs-refuse.md new file mode 100644 index 000000000000..7707628b8902 --- /dev/null +++ b/.changeset/pre/lazy-pugs-refuse.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-netlify': patch +--- + +fix: strip trailing slashes from prerendered paths in the edge function exclude list, so the root page is served statically when using a base path diff --git a/.changeset/pre/lazy-queens-refuse.md b/.changeset/pre/lazy-queens-refuse.md new file mode 100644 index 000000000000..281aa9870bc1 --- /dev/null +++ b/.changeset/pre/lazy-queens-refuse.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: only require the `svelte-trusted-html` trusted-types policy when client-side code is shipped, allowing builds where all pages have `csr: false` diff --git a/.changeset/pre/lazy-tools-bathe.md b/.changeset/pre/lazy-tools-bathe.md new file mode 100644 index 000000000000..f0fe3d2d267d --- /dev/null +++ b/.changeset/pre/lazy-tools-bathe.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +feat: allow adapters to provide additional Vite plugins diff --git a/.changeset/pre/legacy-env-imports.md b/.changeset/pre/legacy-env-imports.md new file mode 100644 index 000000000000..4ca3ffec668b --- /dev/null +++ b/.changeset/pre/legacy-env-imports.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': minor +--- + +feat: reinstate `$env/static/private`, `$env/dynamic/private`, `$env/static/public`, `$env/dynamic/public` and `$app/environment` as deprecated aliases for `$app/env/private` `$app/env/public` and `$app/env` diff --git a/.changeset/pre/legal-boxes-eat.md b/.changeset/pre/legal-boxes-eat.md new file mode 100644 index 000000000000..0b5cc3e3d97e --- /dev/null +++ b/.changeset/pre/legal-boxes-eat.md @@ -0,0 +1,6 @@ +--- +"@sveltejs/adapter-netlify": patch +--- + +fix: copy `_redirects` when deploying edge function + \ No newline at end of file diff --git a/.changeset/pre/lemon-poems-refuse.md b/.changeset/pre/lemon-poems-refuse.md new file mode 100644 index 000000000000..72ce047cadd3 --- /dev/null +++ b/.changeset/pre/lemon-poems-refuse.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +chore: clarify which hooks run during server route resolution diff --git a/.changeset/pre/lemon-pots-tease.md b/.changeset/pre/lemon-pots-tease.md new file mode 100644 index 000000000000..d8945ba267a3 --- /dev/null +++ b/.changeset/pre/lemon-pots-tease.md @@ -0,0 +1,6 @@ +--- +"@sveltejs/kit": patch +--- + +fix: yield to allow prerender updates to be visible + \ No newline at end of file diff --git a/.changeset/pre/lemon-turkeys-drop.md b/.changeset/pre/lemon-turkeys-drop.md new file mode 100644 index 000000000000..12244229414d --- /dev/null +++ b/.changeset/pre/lemon-turkeys-drop.md @@ -0,0 +1,5 @@ +--- +"@sveltejs/kit": patch +--- + +fix: make `paths.origin` type looser diff --git a/.changeset/pre/lemon-ways-glow.md b/.changeset/pre/lemon-ways-glow.md new file mode 100644 index 000000000000..9e61ce1ace12 --- /dev/null +++ b/.changeset/pre/lemon-ways-glow.md @@ -0,0 +1,6 @@ +--- +"@sveltejs/kit": patch +--- + +chore: bump `devalue` to 5.9.0 + \ No newline at end of file diff --git a/.changeset/pre/lib-alias-to-hash-lib.md b/.changeset/pre/lib-alias-to-hash-lib.md new file mode 100644 index 000000000000..63922d566c56 --- /dev/null +++ b/.changeset/pre/lib-alias-to-hash-lib.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: replace the `$lib` alias with `#lib` and remove `files.lib` config. diff --git a/.changeset/pre/light-apples-sleep.md b/.changeset/pre/light-apples-sleep.md new file mode 100644 index 000000000000..bf9ff2429491 --- /dev/null +++ b/.changeset/pre/light-apples-sleep.md @@ -0,0 +1,6 @@ +--- +"@sveltejs/kit": major +--- + +breaking: remove `#lib` definition from `paths`; requires explicit module extensions as a result + \ No newline at end of file diff --git a/.changeset/pre/light-singers-lie.md b/.changeset/pre/light-singers-lie.md new file mode 100644 index 000000000000..745edec0deb3 --- /dev/null +++ b/.changeset/pre/light-singers-lie.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +chore: change `error`, `isHttpError`, `redirect`, and `isRedirect` to refer to public type instead of internal class diff --git a/.changeset/pre/live-query-keep-alive.md b/.changeset/pre/live-query-keep-alive.md new file mode 100644 index 000000000000..fefa78a6cd92 --- /dev/null +++ b/.changeset/pre/live-query-keep-alive.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +feat: send periodic `keep-alive` SSE comments from `query.live` to prevent idle-timeout errors diff --git a/.changeset/pre/live-query-sse-accept.md b/.changeset/pre/live-query-sse-accept.md new file mode 100644 index 000000000000..5f51fa526172 --- /dev/null +++ b/.changeset/pre/live-query-sse-accept.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: send an explicit SSE Accept header for `query.live` requests to avoid buffering by proxies diff --git a/.changeset/pre/live-query-stream-teardown.md b/.changeset/pre/live-query-stream-teardown.md new file mode 100644 index 000000000000..7e6667abb7a6 --- /dev/null +++ b/.changeset/pre/live-query-stream-teardown.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: don't touch the `query.live` stream controller after teardown, and make response cancellation observable via the generator's `request.signal` diff --git a/.changeset/pre/logger-warn-stderr.md b/.changeset/pre/logger-warn-stderr.md new file mode 100644 index 000000000000..607dec3f6b22 --- /dev/null +++ b/.changeset/pre/logger-warn-stderr.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: print `builder.log.warn` messages to stderr, as documented diff --git a/.changeset/pre/loose-cases-know.md b/.changeset/pre/loose-cases-know.md new file mode 100644 index 000000000000..2a95d4074b7e --- /dev/null +++ b/.changeset/pre/loose-cases-know.md @@ -0,0 +1,6 @@ +--- +"@sveltejs/kit": major +--- + +breaking: move hooks-related types to `@sveltejs/kit/hooks` + \ No newline at end of file diff --git a/.changeset/pre/loose-pants-notice.md b/.changeset/pre/loose-pants-notice.md new file mode 100644 index 000000000000..976030f41074 --- /dev/null +++ b/.changeset/pre/loose-pants-notice.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: warn if hook files are spelled as "hook" instead of "hooks" diff --git a/.changeset/pre/lucky-donkeys-argue.md b/.changeset/pre/lucky-donkeys-argue.md new file mode 100644 index 000000000000..cc43b02287ee --- /dev/null +++ b/.changeset/pre/lucky-donkeys-argue.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: only suggest a `+` prefix for route filenames that are valid with the file's extension diff --git a/.changeset/pre/lucky-moons-resolve.md b/.changeset/pre/lucky-moons-resolve.md new file mode 100644 index 000000000000..83155bcaa073 --- /dev/null +++ b/.changeset/pre/lucky-moons-resolve.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +chore: only generate each route's resolution module once when prerendering diff --git a/.changeset/pre/lucky-pandas-listen.md b/.changeset/pre/lucky-pandas-listen.md new file mode 100644 index 000000000000..3505202e4cae --- /dev/null +++ b/.changeset/pre/lucky-pandas-listen.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: don't throw from remote form `validate()` if the form unmounts while it is waiting for a tick diff --git a/.changeset/pre/manifest-route-capabilities.md b/.changeset/pre/manifest-route-capabilities.md new file mode 100644 index 000000000000..e60555b02fcc --- /dev/null +++ b/.changeset/pre/manifest-route-capabilities.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': minor +--- + +feat: add `page` and `endpoint` booleans to `$app/manifest`'s `routes`, and export a `ManifestRoute` type diff --git a/.changeset/pre/metal-bats-care.md b/.changeset/pre/metal-bats-care.md new file mode 100644 index 000000000000..d1cddcaa0dfe --- /dev/null +++ b/.changeset/pre/metal-bats-care.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-netlify': patch +--- + +fix: provide distinct names for each split serverless function diff --git a/.changeset/pre/modern-dodos-hang.md b/.changeset/pre/modern-dodos-hang.md new file mode 100644 index 000000000000..8ec45c840c9a --- /dev/null +++ b/.changeset/pre/modern-dodos-hang.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: add 'error' result type to `preloadData` diff --git a/.changeset/pre/moody-bags-heal.md b/.changeset/pre/moody-bags-heal.md new file mode 100644 index 000000000000..1b12a3b4509d --- /dev/null +++ b/.changeset/pre/moody-bags-heal.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': minor +--- + +feat: `$app/service-worker` module diff --git a/.changeset/pre/more-frequent-updated-checks.md b/.changeset/pre/more-frequent-updated-checks.md new file mode 100644 index 000000000000..9193a86af27d --- /dev/null +++ b/.changeset/pre/more-frequent-updated-checks.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: detect new deployments on data, remote, and form action responses, tab focus, and visibility change, and default `version.pollInterval` to 1 hour diff --git a/.changeset/pre/nasty-impalas-wear.md b/.changeset/pre/nasty-impalas-wear.md new file mode 100644 index 000000000000..14fef49885f0 --- /dev/null +++ b/.changeset/pre/nasty-impalas-wear.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': minor +--- + +feat: resolve paths using the Vite config `root` option instead of `process.cwd()` to better support monorepo configurations such as Vitest workspaces diff --git a/.changeset/pre/navigate-completion.md b/.changeset/pre/navigate-completion.md new file mode 100644 index 000000000000..24e6db70588b --- /dev/null +++ b/.changeset/pre/navigate-completion.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +chore: share navigation completion between navigate and shallow routing diff --git a/.changeset/pre/neat-gifts-feel.md b/.changeset/pre/neat-gifts-feel.md new file mode 100644 index 000000000000..eb05df95d0d7 --- /dev/null +++ b/.changeset/pre/neat-gifts-feel.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: disallow cross-origin form submissions without a `Content-Type` header diff --git a/.changeset/pre/neat-llamas-guess.md b/.changeset/pre/neat-llamas-guess.md new file mode 100644 index 000000000000..1916fee7ba48 --- /dev/null +++ b/.changeset/pre/neat-llamas-guess.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: Server-only directories (`/server/` in the path) are now treated as server-only everywhere inside the project (except `src/routes` and the assets directory) diff --git a/.changeset/pre/neat-otters-pause.md b/.changeset/pre/neat-otters-pause.md new file mode 100644 index 000000000000..c7479e78de8d --- /dev/null +++ b/.changeset/pre/neat-otters-pause.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: route dev-server response logging through Vite's logger so it respects `logLevel` and `customLogger` \ No newline at end of file diff --git a/.changeset/pre/neat-showers-allow.md b/.changeset/pre/neat-showers-allow.md new file mode 100644 index 000000000000..dc52696c98aa --- /dev/null +++ b/.changeset/pre/neat-showers-allow.md @@ -0,0 +1,6 @@ +--- +'@sveltejs/adapter-node': minor +'@sveltejs/kit': minor +--- + +feat: better error logging diff --git a/.changeset/pre/new-bun-adapter.md b/.changeset/pre/new-bun-adapter.md new file mode 100644 index 000000000000..c68d9ce16589 --- /dev/null +++ b/.changeset/pre/new-bun-adapter.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-bun': minor +--- + +feat: add a Bun-native adapter with static file serving and single-executable support diff --git a/.changeset/pre/nice-mammals-move.md b/.changeset/pre/nice-mammals-move.md new file mode 100644 index 000000000000..e58b67ee2fcd --- /dev/null +++ b/.changeset/pre/nice-mammals-move.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: `config` exported from a universal route file takes precedence over a server one diff --git a/.changeset/pre/nine-coins-cheer.md b/.changeset/pre/nine-coins-cheer.md new file mode 100644 index 000000000000..6880deb22899 --- /dev/null +++ b/.changeset/pre/nine-coins-cheer.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: require Vite 8. Provides new functionality even for existing Vite 8 users such as faster builds with Vite hook filters and more powerful SvelteKit adapters with the Vite environment API diff --git a/.changeset/pre/nine-suns-nail.md b/.changeset/pre/nine-suns-nail.md new file mode 100644 index 000000000000..9481dfd70d9f --- /dev/null +++ b/.changeset/pre/nine-suns-nail.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: remove `data-sveltekit-*` option `'off'` in favour of `false` diff --git a/.changeset/pre/o11y-leave-experimental.md b/.changeset/pre/o11y-leave-experimental.md new file mode 100644 index 000000000000..c25b33b097af --- /dev/null +++ b/.changeset/pre/o11y-leave-experimental.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: move tracing out of the experimental namespace and remove the instrumentation flag diff --git a/.changeset/pre/odd-seas-refuse.md b/.changeset/pre/odd-seas-refuse.md new file mode 100644 index 000000000000..00300ec4b44b --- /dev/null +++ b/.changeset/pre/odd-seas-refuse.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: skip clean fields when programmatically validating forms diff --git a/.changeset/pre/olive-boxes-tan.md b/.changeset/pre/olive-boxes-tan.md new file mode 100644 index 000000000000..f2eda707e12b --- /dev/null +++ b/.changeset/pre/olive-boxes-tan.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +perf: skip import graph collection outside client environments diff --git a/.changeset/pre/olive-boxes-walk.md b/.changeset/pre/olive-boxes-walk.md new file mode 100644 index 000000000000..8480b853aa87 --- /dev/null +++ b/.changeset/pre/olive-boxes-walk.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +breaking: experimental remote form `validate({ includeUntouched })` option is now `all` diff --git a/.changeset/pre/olive-buses-shake.md b/.changeset/pre/olive-buses-shake.md new file mode 100644 index 000000000000..4d8d3b7ef1a0 --- /dev/null +++ b/.changeset/pre/olive-buses-shake.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: rerun load functions when the number of values of a tracked search parameter changes diff --git a/.changeset/pre/olive-hoops-shave.md b/.changeset/pre/olive-hoops-shave.md new file mode 100644 index 000000000000..3c5eb70941d8 --- /dev/null +++ b/.changeset/pre/olive-hoops-shave.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +chore: read build-time config from defines on the server instead of carrying it in `options` diff --git a/.changeset/pre/open-wasps-shop.md b/.changeset/pre/open-wasps-shop.md new file mode 100644 index 000000000000..366573abe6fc --- /dev/null +++ b/.changeset/pre/open-wasps-shop.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: avoid client build warning about externalising `node:async_hooks` diff --git a/.changeset/pre/package-fs-helpers.md b/.changeset/pre/package-fs-helpers.md new file mode 100644 index 000000000000..5b2e03cd03c1 --- /dev/null +++ b/.changeset/pre/package-fs-helpers.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/package': patch +--- + +chore: replace private fs helpers with Node built-ins diff --git a/.changeset/pre/package-load-config.md b/.changeset/pre/package-load-config.md new file mode 100644 index 000000000000..89b9f6d31970 --- /dev/null +++ b/.changeset/pre/package-load-config.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/package': patch +--- + +chore: read the Svelte config via `@sveltejs/load-config` diff --git a/.changeset/pre/page-endpoint-route-ids.md b/.changeset/pre/page-endpoint-route-ids.md new file mode 100644 index 000000000000..dac895d2d6d7 --- /dev/null +++ b/.changeset/pre/page-endpoint-route-ids.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': minor +--- + +feat: add `PageRouteId` and `EndpointRouteId` to `$app/types` diff --git a/.changeset/pre/paths-origin.md b/.changeset/pre/paths-origin.md new file mode 100644 index 000000000000..d16c61eecd92 --- /dev/null +++ b/.changeset/pre/paths-origin.md @@ -0,0 +1,6 @@ +--- +'@sveltejs/kit': major +'@sveltejs/adapter-node': major +--- + +breaking: add `kit.paths.origin` config option, remove `kit.prerender.origin` and the `adapter-node` `ORIGIN` environment variable diff --git a/.changeset/pre/perky-badgers-learn.md b/.changeset/pre/perky-badgers-learn.md new file mode 100644 index 000000000000..1a10e5f0f634 --- /dev/null +++ b/.changeset/pre/perky-badgers-learn.md @@ -0,0 +1,6 @@ +--- +"@sveltejs/kit": major +--- + +breaking: move `Page`, `ReadonlyURL` and `ReadonlyURLSearchParams` from `@sveltejs/kit` to `$app/state` + \ No newline at end of file diff --git a/.changeset/pre/petite-pens-love.md b/.changeset/pre/petite-pens-love.md new file mode 100644 index 000000000000..9ffdc8b578cf --- /dev/null +++ b/.changeset/pre/petite-pens-love.md @@ -0,0 +1,5 @@ +--- +"@sveltejs/kit": patch +--- + +chore: stop externalizing `cookie` dependency during build diff --git a/.changeset/pre/petite-sites-enter.md b/.changeset/pre/petite-sites-enter.md new file mode 100644 index 000000000000..9c10b6508e49 --- /dev/null +++ b/.changeset/pre/petite-sites-enter.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-netlify': minor +--- + +feat: support `split` and `edge` options combined diff --git a/.changeset/pre/pink-cobras-rush.md b/.changeset/pre/pink-cobras-rush.md new file mode 100644 index 000000000000..2eee1de363e6 --- /dev/null +++ b/.changeset/pre/pink-cobras-rush.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: reinstate `$app/environment` as an alias for `$app/env`, in case dependencies import it diff --git a/.changeset/pre/plain-jobs-flash.md b/.changeset/pre/plain-jobs-flash.md new file mode 100644 index 000000000000..600633abee7a --- /dev/null +++ b/.changeset/pre/plain-jobs-flash.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': minor +--- + +feat: better tsconfig validation diff --git a/.changeset/pre/plenty-cougars-wave.md b/.changeset/pre/plenty-cougars-wave.md new file mode 100644 index 000000000000..03d6b75dd276 --- /dev/null +++ b/.changeset/pre/plenty-cougars-wave.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-cloudflare': patch +--- + +chore: check the `WORKERS_CI` environment variable to determine if we're building for Cloudflare Workers diff --git a/.changeset/pre/plenty-wasps-hang.md b/.changeset/pre/plenty-wasps-hang.md new file mode 100644 index 000000000000..45947bf988b9 --- /dev/null +++ b/.changeset/pre/plenty-wasps-hang.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/enhanced-img': patch +--- + +chore: bump `vite-imagetools` to v12 diff --git a/.changeset/pre/polite-lemons-refuse.md b/.changeset/pre/polite-lemons-refuse.md new file mode 100644 index 000000000000..053810bb3e1d --- /dev/null +++ b/.changeset/pre/polite-lemons-refuse.md @@ -0,0 +1,6 @@ +--- +'@sveltejs/adapter-netlify': major +'@sveltejs/adapter-vercel': major +--- + +breaking: edge function build target is now `es2022` diff --git a/.changeset/pre/prerelease-peer-deps.md b/.changeset/pre/prerelease-peer-deps.md new file mode 100644 index 000000000000..9b0a69fe24fa --- /dev/null +++ b/.changeset/pre/prerelease-peer-deps.md @@ -0,0 +1,9 @@ +--- +'@sveltejs/adapter-auto': patch +'@sveltejs/adapter-cloudflare': patch +'@sveltejs/adapter-node': patch +'@sveltejs/adapter-static': patch +'@sveltejs/adapter-vercel': patch +--- + +fix: allow prerelease versions of SvelteKit 3 to satisfy the peer dependency range diff --git a/.changeset/pre/pretty-shrimps-write.md b/.changeset/pre/pretty-shrimps-write.md new file mode 100644 index 000000000000..be844f40bfd8 --- /dev/null +++ b/.changeset/pre/pretty-shrimps-write.md @@ -0,0 +1,6 @@ +--- +"@sveltejs/adapter-node": patch +"@sveltejs/adapter-bun": patch +--- + +fix: always bundle dev dependencies diff --git a/.changeset/pre/preview-base-trailing-slash-redirect.md b/.changeset/pre/preview-base-trailing-slash-redirect.md new file mode 100644 index 000000000000..0542629ed4f5 --- /dev/null +++ b/.changeset/pre/preview-base-trailing-slash-redirect.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: preserve `paths.base` when `vite preview` redirects a prerendered page to the correct trailing slash diff --git a/.changeset/pre/public-items-agree.md b/.changeset/pre/public-items-agree.md new file mode 100644 index 000000000000..0c2243447cd7 --- /dev/null +++ b/.changeset/pre/public-items-agree.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': minor +--- + +feat: add `dirty()` property to form fields diff --git a/.changeset/pre/query-method-server-export.md b/.changeset/pre/query-method-server-export.md new file mode 100644 index 000000000000..b2c8d54886ff --- /dev/null +++ b/.changeset/pre/query-method-server-export.md @@ -0,0 +1,5 @@ +--- +"@sveltejs/kit": minor +--- + +feat: support the `QUERY` HTTP method in `+server.js` diff --git a/.changeset/pre/quick-badgers-notice.md b/.changeset/pre/quick-badgers-notice.md new file mode 100644 index 000000000000..eac206f9e905 --- /dev/null +++ b/.changeset/pre/quick-badgers-notice.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: remove `$app/stores` diff --git a/.changeset/pre/quick-cookies-guarded.md b/.changeset/pre/quick-cookies-guarded.md new file mode 100644 index 000000000000..fd2dd735afc2 --- /dev/null +++ b/.changeset/pre/quick-cookies-guarded.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: error when reading non-serialized `set-cookie` headers via `getSetCookie` in `load` diff --git a/.changeset/pre/quick-dirs-walk.md b/.changeset/pre/quick-dirs-walk.md new file mode 100644 index 000000000000..371ef2901727 --- /dev/null +++ b/.changeset/pre/quick-dirs-walk.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: walk and copy directories without a stat per file diff --git a/.changeset/pre/quick-forms-merge.md b/.changeset/pre/quick-forms-merge.md new file mode 100644 index 000000000000..f1c79564bcbf --- /dev/null +++ b/.changeset/pre/quick-forms-merge.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +perf: avoid quadratic remote form issue merging diff --git a/.changeset/pre/quick-forms-unite.md b/.changeset/pre/quick-forms-unite.md new file mode 100644 index 000000000000..ffaf02dfd957 --- /dev/null +++ b/.changeset/pre/quick-forms-unite.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: widen remote form fields for union schemas and string enums diff --git a/.changeset/pre/quiet-aliases-retire.md b/.changeset/pre/quiet-aliases-retire.md new file mode 100644 index 000000000000..2938a89e4049 --- /dev/null +++ b/.changeset/pre/quiet-aliases-retire.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +chore: deprecate the `alias` option diff --git a/.changeset/pre/quiet-bodies-repeat.md b/.changeset/pre/quiet-bodies-repeat.md new file mode 100644 index 000000000000..92950c357f78 --- /dev/null +++ b/.changeset/pre/quiet-bodies-repeat.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: don't attempt to serialize fetch responses when the request body is not a string or TypedArray diff --git a/.changeset/pre/quiet-heads-rest.md b/.changeset/pre/quiet-heads-rest.md new file mode 100644 index 000000000000..f25afa87062c --- /dev/null +++ b/.changeset/pre/quiet-heads-rest.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: respond to `HEAD` requests without a body diff --git a/.changeset/pre/quiet-hooks-update.md b/.changeset/pre/quiet-hooks-update.md new file mode 100644 index 000000000000..674dd348e208 --- /dev/null +++ b/.changeset/pre/quiet-hooks-update.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +chore: replace deprecated Vite dev server APIs diff --git a/.changeset/pre/quiet-images-call.md b/.changeset/pre/quiet-images-call.md new file mode 100644 index 000000000000..94f28978796b --- /dev/null +++ b/.changeset/pre/quiet-images-call.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/enhanced-img': patch +--- + +fix: evaluate dynamic image source expressions once diff --git a/.changeset/pre/quiet-maps-iterate.md b/.changeset/pre/quiet-maps-iterate.md new file mode 100644 index 000000000000..c0796b2e7b57 --- /dev/null +++ b/.changeset/pre/quiet-maps-iterate.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +chore: iterate the query cache maps through a single generator diff --git a/.changeset/pre/quiet-masks-shop.md b/.changeset/pre/quiet-masks-shop.md new file mode 100644 index 000000000000..2be51678bfaa --- /dev/null +++ b/.changeset/pre/quiet-masks-shop.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: handle rejected streamed server data after delayed loads diff --git a/.changeset/pre/quiet-pans-shake.md b/.changeset/pre/quiet-pans-shake.md new file mode 100644 index 000000000000..42155f926df6 --- /dev/null +++ b/.changeset/pre/quiet-pans-shake.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: don't crash on interactions inside a form whose controls shadow `nodeName` diff --git a/.changeset/pre/quiet-phones-bet.md b/.changeset/pre/quiet-phones-bet.md new file mode 100644 index 000000000000..149a0a33b763 --- /dev/null +++ b/.changeset/pre/quiet-phones-bet.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-cloudflare': major +--- + +breaking: remove use of the Cloudflare Workers Cache API in favour of Workers Caching diff --git a/.changeset/pre/quiet-preload-redirect.md b/.changeset/pre/quiet-preload-redirect.md new file mode 100644 index 000000000000..4fc6a4af2540 --- /dev/null +++ b/.changeset/pre/quiet-preload-redirect.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: don't replay a preloaded redirect when a later hop of the navigation returns to the route diff --git a/.changeset/pre/quiet-previews-stay.md b/.changeset/pre/quiet-previews-stay.md new file mode 100644 index 000000000000..11382e1553bc --- /dev/null +++ b/.changeset/pre/quiet-previews-stay.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: prevent path traversal when previewing prerendered pages and data on Windows diff --git a/.changeset/pre/quiet-queries-settle.md b/.changeset/pre/quiet-queries-settle.md new file mode 100644 index 000000000000..c86bb4bec684 --- /dev/null +++ b/.changeset/pre/quiet-queries-settle.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: settle a query's pending request in place when its value arrives through `set()` diff --git a/.changeset/pre/quiet-ravens-smile.md b/.changeset/pre/quiet-ravens-smile.md new file mode 100644 index 000000000000..cfac9eb02bf4 --- /dev/null +++ b/.changeset/pre/quiet-ravens-smile.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: render pages over sibling endpoints without GET or HEAD handlers diff --git a/.changeset/pre/quiet-taxis-build.md b/.changeset/pre/quiet-taxis-build.md new file mode 100644 index 000000000000..fa4aab10427b --- /dev/null +++ b/.changeset/pre/quiet-taxis-build.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-node': patch +--- + +chore: avoid bundling the server twice diff --git a/.changeset/pre/quiet-workers-exit.md b/.changeset/pre/quiet-workers-exit.md new file mode 100644 index 000000000000..8db45e4ce6e3 --- /dev/null +++ b/.changeset/pre/quiet-workers-exit.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: exit build workers after completing their tasks while allowing synchronous exit handlers to run diff --git a/.changeset/pre/rare-lions-reply.md b/.changeset/pre/rare-lions-reply.md new file mode 100644 index 000000000000..88e765354b0e --- /dev/null +++ b/.changeset/pre/rare-lions-reply.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-node': patch +--- + +fix: correctly bundle entrypoints on Windows diff --git a/.changeset/pre/rare-schools-itch.md b/.changeset/pre/rare-schools-itch.md new file mode 100644 index 000000000000..75eaee275638 --- /dev/null +++ b/.changeset/pre/rare-schools-itch.md @@ -0,0 +1,6 @@ +--- +"@sveltejs/kit": minor +--- + +feat: allow adapters to override `getRequest` and `setResponse` during `vite dev` and `vite preview` + \ No newline at end of file diff --git a/.changeset/pre/re-refresh-reruns.md b/.changeset/pre/re-refresh-reruns.md new file mode 100644 index 000000000000..06c92044fceb --- /dev/null +++ b/.changeset/pre/re-refresh-reruns.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: prevent infinite loops when server-side queries refresh each other in a cycle during the single-flight drain diff --git a/.changeset/pre/ready-chairs-fold.md b/.changeset/pre/ready-chairs-fold.md new file mode 100644 index 000000000000..b44cb58ea001 --- /dev/null +++ b/.changeset/pre/ready-chairs-fold.md @@ -0,0 +1,6 @@ +--- +"@sveltejs/enhanced-img": patch +--- + +chore: bump `zimmerframe` to 1.1.4 + \ No newline at end of file diff --git a/.changeset/pre/ready-dragons-mix.md b/.changeset/pre/ready-dragons-mix.md new file mode 100644 index 000000000000..877d3fee119c --- /dev/null +++ b/.changeset/pre/ready-dragons-mix.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: populate `version` in service workers diff --git a/.changeset/pre/red-masks-remain.md b/.changeset/pre/red-masks-remain.md new file mode 100644 index 000000000000..f6c87e2418f4 --- /dev/null +++ b/.changeset/pre/red-masks-remain.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +feat: add field.touched() helper to remote form fields diff --git a/.changeset/pre/refresh-page-state.md b/.changeset/pre/refresh-page-state.md new file mode 100644 index 000000000000..f69d1876700a --- /dev/null +++ b/.changeset/pre/refresh-page-state.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: add `refreshAll` and deprecate `invalidateAll` diff --git a/.changeset/pre/reject-reserved-params.md b/.changeset/pre/reject-reserved-params.md new file mode 100644 index 000000000000..f8e47ebce29b --- /dev/null +++ b/.changeset/pre/reject-reserved-params.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: reject query parameters beginning with `x-sveltekit-` diff --git a/.changeset/pre/relative-route-resolution.md b/.changeset/pre/relative-route-resolution.md new file mode 100644 index 000000000000..164010aaedd3 --- /dev/null +++ b/.changeset/pre/relative-route-resolution.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: respect `paths.relative` for server-side route resolution imports diff --git a/.changeset/pre/remote-error-status.md b/.changeset/pre/remote-error-status.md new file mode 100644 index 000000000000..cf588fc9d7f8 --- /dev/null +++ b/.changeset/pre/remote-error-status.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: read the error status of `App.Error` in the `prerender` and `query.live` remote functions diff --git a/.changeset/pre/remote-exports-prebundle.md b/.changeset/pre/remote-exports-prebundle.md new file mode 100644 index 000000000000..8feaf30b0ec8 --- /dev/null +++ b/.changeset/pre/remote-exports-prebundle.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: resolve remote modules as external during dev prebundling so packages can re-export remote functions diff --git a/.changeset/pre/remote-form-diagnostics.md b/.changeset/pre/remote-form-diagnostics.md new file mode 100644 index 000000000000..4209ed9c535c --- /dev/null +++ b/.changeset/pre/remote-form-diagnostics.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +chore: standardize remote-function and shared form diagnostics diff --git a/.changeset/pre/remote-functions-no-withresolvers.md b/.changeset/pre/remote-functions-no-withresolvers.md new file mode 100644 index 000000000000..9162194f55db --- /dev/null +++ b/.changeset/pre/remote-functions-no-withresolvers.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: avoid `Promise.withResolvers` in client remote functions for older browser support diff --git a/.changeset/pre/remote-functions-required-flag.md b/.changeset/pre/remote-functions-required-flag.md new file mode 100644 index 000000000000..c049456a34e7 --- /dev/null +++ b/.changeset/pre/remote-functions-required-flag.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: disallow `*.remote.ts/js` files unless `experimental.remoteFunctions` is enabled diff --git a/.changeset/pre/remote-prerender-error-propagation.md b/.changeset/pre/remote-prerender-error-propagation.md new file mode 100644 index 000000000000..72c9c8a879de --- /dev/null +++ b/.changeset/pre/remote-prerender-error-propagation.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: propagate errors from prerendered remote responses instead of re-running the function diff --git a/.changeset/pre/remove-unused-helpers.md b/.changeset/pre/remove-unused-helpers.md new file mode 100644 index 000000000000..79be5f9bcd0d --- /dev/null +++ b/.changeset/pre/remove-unused-helpers.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +chore: remove unused helpers diff --git a/.changeset/pre/rename-noscroll-keepfocus-reset.md b/.changeset/pre/rename-noscroll-keepfocus-reset.md new file mode 100644 index 000000000000..9bc4615d3ab9 --- /dev/null +++ b/.changeset/pre/rename-noscroll-keepfocus-reset.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: replace the `noScroll` and `keepFocus` options of `goto` with a single `reset` option, and the `data-sveltekit-noscroll` and `data-sveltekit-keepfocus` attributes with `data-sveltekit-reset` diff --git a/.changeset/pre/reserved-keyword-remote-exports.md b/.changeset/pre/reserved-keyword-remote-exports.md new file mode 100644 index 000000000000..89919a7b0923 --- /dev/null +++ b/.changeset/pre/reserved-keyword-remote-exports.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: allow reserved words (e.g. `delete`, `class`) as remote function export names diff --git a/.changeset/pre/reset-failed-boundary-on-navigation.md b/.changeset/pre/reset-failed-boundary-on-navigation.md new file mode 100644 index 000000000000..1f5d0b793fa0 --- /dev/null +++ b/.changeset/pre/reset-failed-boundary-on-navigation.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: reset failed `` on client navigation so a stale `+error.svelte` is torn down diff --git a/.changeset/pre/reset-navigating-shallow-popstate.md b/.changeset/pre/reset-navigating-shallow-popstate.md new file mode 100644 index 000000000000..e65af87170dd --- /dev/null +++ b/.changeset/pre/reset-navigating-shallow-popstate.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: clear `navigating` when a shallow popstate aborts an in-flight navigation diff --git a/.changeset/pre/resolve-route-escape-sequences.md b/.changeset/pre/resolve-route-escape-sequences.md new file mode 100644 index 000000000000..0387f6c9c660 --- /dev/null +++ b/.changeset/pre/resolve-route-escape-sequences.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: expand `[x+nn]` and `[u+nnnn]` escape sequences when resolving a route id to a pathname diff --git a/.changeset/pre/respect-user-cors-config.md b/.changeset/pre/respect-user-cors-config.md new file mode 100644 index 000000000000..ee22cc26678e --- /dev/null +++ b/.changeset/pre/respect-user-cors-config.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: respect user-set `server.cors` and `preview.cors` config instead of overriding them diff --git a/.changeset/pre/respond-with-error-status.md b/.changeset/pre/respond-with-error-status.md new file mode 100644 index 000000000000..abefa63dfaa2 --- /dev/null +++ b/.changeset/pre/respond-with-error-status.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: respect the status returned from `handleError` on the fallback error page served to error-page sub-requests diff --git a/.changeset/pre/restore-bfcache-scroll.md b/.changeset/pre/restore-bfcache-scroll.md new file mode 100644 index 000000000000..99493f073021 --- /dev/null +++ b/.changeset/pre/restore-bfcache-scroll.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: restore scroll position after back-forward cache returns diff --git a/.changeset/pre/rich-poems-happen.md b/.changeset/pre/rich-poems-happen.md new file mode 100644 index 000000000000..2489bcfa6da0 --- /dev/null +++ b/.changeset/pre/rich-poems-happen.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: warn if there are plugins using `transformIndexHtml` diff --git a/.changeset/pre/ripe-boats-think.md b/.changeset/pre/ripe-boats-think.md new file mode 100644 index 000000000000..25396d2de64d --- /dev/null +++ b/.changeset/pre/ripe-boats-think.md @@ -0,0 +1,6 @@ +--- +"@sveltejs/kit": major +--- + +breaking: don't abort navigation when calling `invalidate(All)` during navigation + \ No newline at end of file diff --git a/.changeset/pre/ripe-months-count.md b/.changeset/pre/ripe-months-count.md new file mode 100644 index 000000000000..e56903a11a1a --- /dev/null +++ b/.changeset/pre/ripe-months-count.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: consistent special filename patterns diff --git a/.changeset/pre/ripe-tires-chew.md b/.changeset/pre/ripe-tires-chew.md new file mode 100644 index 000000000000..17a8c85a9fd4 --- /dev/null +++ b/.changeset/pre/ripe-tires-chew.md @@ -0,0 +1,6 @@ +--- +"@sveltejs/kit": major +--- + +breaking: allow `handleError` to influence status code + \ No newline at end of file diff --git a/.changeset/pre/ripe-trains-dance.md b/.changeset/pre/ripe-trains-dance.md new file mode 100644 index 000000000000..5b737326fa27 --- /dev/null +++ b/.changeset/pre/ripe-trains-dance.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: delegate CORS handling to Vite for static directory requests during development diff --git a/.changeset/pre/rolldown-min-version.md b/.changeset/pre/rolldown-min-version.md new file mode 100644 index 000000000000..04daa1b806f6 --- /dev/null +++ b/.changeset/pre/rolldown-min-version.md @@ -0,0 +1,9 @@ +--- +'@sveltejs/kit': major +'@sveltejs/enhanced-img': major +'@sveltejs/adapter-node': major +'@sveltejs/adapter-netlify': major +'@sveltejs/adapter-vercel': major +--- + +breaking: require `vite@^8.0.12`, the first Vite 8 release bundling stable `rolldown` 1.0.0 diff --git a/.changeset/pre/route-changed-after-error-page.md b/.changeset/pre/route-changed-after-error-page.md new file mode 100644 index 000000000000..b6601a40d164 --- /dev/null +++ b/.changeset/pre/route-changed-after-error-page.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: refetch route-tracking server data when navigating away from an error page diff --git a/.changeset/pre/route-id-bracket-escapes.md b/.changeset/pre/route-id-bracket-escapes.md new file mode 100644 index 000000000000..750bf027c2b8 --- /dev/null +++ b/.changeset/pre/route-id-bracket-escapes.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: allow routes to contain `[` and `]` via the `[x+5b]` and `[x+5d]` escapes diff --git a/.changeset/pre/routeid-only-real-routes.md b/.changeset/pre/routeid-only-real-routes.md new file mode 100644 index 000000000000..014f1c6e160e --- /dev/null +++ b/.changeset/pre/routeid-only-real-routes.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: only include routes with a `+page` or `+server` in `RouteId` diff --git a/.changeset/pre/runtime-export-conditions.md b/.changeset/pre/runtime-export-conditions.md new file mode 100644 index 000000000000..096f39914e9d --- /dev/null +++ b/.changeset/pre/runtime-export-conditions.md @@ -0,0 +1,6 @@ +--- +'@sveltejs/adapter-node': patch +'@sveltejs/adapter-bun': patch +--- + +fix: bundle Svelte libraries listed in `dependencies` instead of externalising them diff --git a/.changeset/pre/runtime-response-json.md b/.changeset/pre/runtime-response-json.md new file mode 100644 index 000000000000..46f56be3ff6c --- /dev/null +++ b/.changeset/pre/runtime-response-json.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +chore: use `Response.json` instead of the deprecated `json` helper in runtime responses diff --git a/.changeset/pre/safe-configs-smile.md b/.changeset/pre/safe-configs-smile.md new file mode 100644 index 000000000000..5c00e729d440 --- /dev/null +++ b/.changeset/pre/safe-configs-smile.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: recommend safe include and exclude patterns in tsconfig warning diff --git a/.changeset/pre/salty-chefs-sleep.md b/.changeset/pre/salty-chefs-sleep.md new file mode 100644 index 000000000000..1f2cb46beafb --- /dev/null +++ b/.changeset/pre/salty-chefs-sleep.md @@ -0,0 +1,6 @@ +--- +'@sveltejs/package': patch +'@sveltejs/kit': patch +--- + +chore: remove dependency on kleur diff --git a/.changeset/pre/self-fetch-redirects.md b/.changeset/pre/self-fetch-redirects.md new file mode 100644 index 000000000000..e6feca93c0b0 --- /dev/null +++ b/.changeset/pre/self-fetch-redirects.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: prevent server-side self-fetch fallbacks from following redirects diff --git a/.changeset/pre/serialize-query-set-during-ssr.md b/.changeset/pre/serialize-query-set-during-ssr.md new file mode 100644 index 000000000000..6fb1f9932a99 --- /dev/null +++ b/.changeset/pre/serialize-query-set-during-ssr.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: serialize `query(...).set(...)`/`query(...).refresh()` values into the rendered HTML when called from within a query during SSR diff --git a/.changeset/pre/server-boot-configure.md b/.changeset/pre/server-boot-configure.md new file mode 100644 index 000000000000..2819ceb05fe0 --- /dev/null +++ b/.changeset/pre/server-boot-configure.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +chore: configure the server runtime in one place, deprecate `Server` in favour of the `server` object written by `builder.generateServerInstance` diff --git a/.changeset/pre/set-response-fixed-bodies.md b/.changeset/pre/set-response-fixed-bodies.md new file mode 100644 index 000000000000..9c38b4f96b0f --- /dev/null +++ b/.changeset/pre/set-response-fixed-bodies.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +chore: derive `content-length` from fixed response bodies in `setResponse` diff --git a/.changeset/pre/shared-read-range.md b/.changeset/pre/shared-read-range.md new file mode 100644 index 000000000000..f3b11b92b6a1 --- /dev/null +++ b/.changeset/pre/shared-read-range.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: correctly read zero-length files at the end of a binary form payload diff --git a/.changeset/pre/sharp-endpoints-fallback.md b/.changeset/pre/sharp-endpoints-fallback.md new file mode 100644 index 000000000000..e1c14e0593a4 --- /dev/null +++ b/.changeset/pre/sharp-endpoints-fallback.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: reject fallback handlers on prerendered endpoints diff --git a/.changeset/pre/shiny-feet-crash.md b/.changeset/pre/shiny-feet-crash.md new file mode 100644 index 000000000000..0d1485671007 --- /dev/null +++ b/.changeset/pre/shiny-feet-crash.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': minor +--- + +chore: deprecate `Response` helpers in favor of platform-provided alternatives diff --git a/.changeset/pre/shiny-lines-guard.md b/.changeset/pre/shiny-lines-guard.md new file mode 100644 index 000000000000..7a8523ab1cf4 --- /dev/null +++ b/.changeset/pre/shiny-lines-guard.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: don't destroy partial-line app output with the prerender progress line diff --git a/.changeset/pre/shy-squids-film.md b/.changeset/pre/shy-squids-film.md new file mode 100644 index 000000000000..eca370bcff7a --- /dev/null +++ b/.changeset/pre/shy-squids-film.md @@ -0,0 +1,6 @@ +--- +"@sveltejs/kit": patch +--- + +chore: remove virtual modules + \ No newline at end of file diff --git a/.changeset/pre/silent-bushes-grow.md b/.changeset/pre/silent-bushes-grow.md new file mode 100644 index 000000000000..d2befebda630 --- /dev/null +++ b/.changeset/pre/silent-bushes-grow.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: surface prerender errors during development diff --git a/.changeset/pre/silent-dogs-warn.md b/.changeset/pre/silent-dogs-warn.md new file mode 100644 index 000000000000..b6c4e94fe52e --- /dev/null +++ b/.changeset/pre/silent-dogs-warn.md @@ -0,0 +1,6 @@ +--- +"@sveltejs/kit": patch +--- + +chore: bump `cookie` to 2.0.1 + \ No newline at end of file diff --git a/.changeset/pre/silly-islands-greet.md b/.changeset/pre/silly-islands-greet.md new file mode 100644 index 000000000000..1168f1bf419e --- /dev/null +++ b/.changeset/pre/silly-islands-greet.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': minor +--- + +feat: add `cookies.parse` method diff --git a/.changeset/pre/six-items-obey.md b/.changeset/pre/six-items-obey.md new file mode 100644 index 000000000000..1fa5752378f5 --- /dev/null +++ b/.changeset/pre/six-items-obey.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: require Node 22.17 diff --git a/.changeset/pre/sixty-clubs-show.md b/.changeset/pre/sixty-clubs-show.md new file mode 100644 index 000000000000..3b47604e3385 --- /dev/null +++ b/.changeset/pre/sixty-clubs-show.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-cloudflare': patch +--- + +fix: avoid caching immutable asset 404s diff --git a/.changeset/pre/sixty-dragons-hide.md b/.changeset/pre/sixty-dragons-hide.md new file mode 100644 index 000000000000..ad02352ba196 --- /dev/null +++ b/.changeset/pre/sixty-dragons-hide.md @@ -0,0 +1,6 @@ +--- +"@sveltejs/adapter-netlify": patch +"@sveltejs/adapter-node": patch +--- + +chore: bump `rolldown` to 1.2.3 diff --git a/.changeset/pre/sixty-words-strive.md b/.changeset/pre/sixty-words-strive.md new file mode 100644 index 000000000000..ad5ae4e633ad --- /dev/null +++ b/.changeset/pre/sixty-words-strive.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: deprecate `error(status, {...})` in favour of `error(status, message, {...})` diff --git a/.changeset/pre/smart-pugs-refuse.md b/.changeset/pre/smart-pugs-refuse.md new file mode 100644 index 000000000000..88b7bbf216ec --- /dev/null +++ b/.changeset/pre/smart-pugs-refuse.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: detect `$app/server` and `$app/env/private` client imports when SvelteKit is installed inside the project root diff --git a/.changeset/pre/smooth-flies-begin.md b/.changeset/pre/smooth-flies-begin.md new file mode 100644 index 000000000000..80d6c5efc17a --- /dev/null +++ b/.changeset/pre/smooth-flies-begin.md @@ -0,0 +1,10 @@ +--- +"@sveltejs/adapter-cloudflare": major +"@sveltejs/adapter-netlify": major +"@sveltejs/adapter-node": major +"@sveltejs/adapter-vercel": major +"@sveltejs/kit": major +--- + +breaking: replace the `builder.generateManifest` with `builder.generateServerInstance` and `builder.manifest` + \ No newline at end of file diff --git a/.changeset/pre/snapshot-deprecate.md b/.changeset/pre/snapshot-deprecate.md new file mode 100644 index 000000000000..2b61da240ea3 --- /dev/null +++ b/.changeset/pre/snapshot-deprecate.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': minor +--- + +chore: deprecate `export const snapshot` in favour of the `snapshot` helper diff --git a/.changeset/pre/snapshot-helper.md b/.changeset/pre/snapshot-helper.md new file mode 100644 index 000000000000..0b571c50f25c --- /dev/null +++ b/.changeset/pre/snapshot-helper.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': minor +--- + +feat: add `snapshot` helper to `$app/navigation` diff --git a/.changeset/pre/soft-ants-work.md b/.changeset/pre/soft-ants-work.md new file mode 100644 index 000000000000..b342f72dd64f --- /dev/null +++ b/.changeset/pre/soft-ants-work.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': minor +--- + +feat: support custom values in `page.state` via `transport` hook diff --git a/.changeset/pre/soft-jars-repeat.md b/.changeset/pre/soft-jars-repeat.md new file mode 100644 index 000000000000..19f554997a1d --- /dev/null +++ b/.changeset/pre/soft-jars-repeat.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +chore: read `options` from a single module instead of passing it through the server runtime diff --git a/.changeset/pre/soft-poems.md b/.changeset/pre/soft-poems.md new file mode 100644 index 000000000000..3825ab8f1dbf --- /dev/null +++ b/.changeset/pre/soft-poems.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: wait for the redirect navigation before prerendered remote functions resolve diff --git a/.changeset/pre/some-groups-return.md b/.changeset/pre/some-groups-return.md new file mode 100644 index 000000000000..a7ee2cb52233 --- /dev/null +++ b/.changeset/pre/some-groups-return.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: forbid external redirects by default diff --git a/.changeset/pre/sort-manifest-readdir.md b/.changeset/pre/sort-manifest-readdir.md new file mode 100644 index 000000000000..4a660f4563c8 --- /dev/null +++ b/.changeset/pre/sort-manifest-readdir.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: sort directory entries when building the route manifest so node indices are deterministic across runtimes (e.g. Bun and Node) diff --git a/.changeset/pre/sparkly-candies-serve.md b/.changeset/pre/sparkly-candies-serve.md new file mode 100644 index 000000000000..0c48b6d8765a --- /dev/null +++ b/.changeset/pre/sparkly-candies-serve.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: skip unnecessary `version.json` checks if `updated.current` is already `true` diff --git a/.changeset/pre/sparkly-kids-shine.md b/.changeset/pre/sparkly-kids-shine.md new file mode 100644 index 000000000000..7562e41ac6b0 --- /dev/null +++ b/.changeset/pre/sparkly-kids-shine.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: allow generation of $app/tsconfig without TypeScript installed diff --git a/.changeset/pre/spicy-drinks-build.md b/.changeset/pre/spicy-drinks-build.md new file mode 100644 index 000000000000..453c6dc1b288 --- /dev/null +++ b/.changeset/pre/spicy-drinks-build.md @@ -0,0 +1,5 @@ +--- +"@sveltejs/kit": patch +--- + +chore: remove dependency on `set-cookie-parser` diff --git a/.changeset/pre/spicy-planes-refactor.md b/.changeset/pre/spicy-planes-refactor.md new file mode 100644 index 000000000000..7324f8778ab5 --- /dev/null +++ b/.changeset/pre/spicy-planes-refactor.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +chore: share the nearest error page walk between client and server diff --git a/.changeset/pre/spicy-taxis-jump.md b/.changeset/pre/spicy-taxis-jump.md new file mode 100644 index 000000000000..d2c293ae410a --- /dev/null +++ b/.changeset/pre/spicy-taxis-jump.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: fall back to the page's form actions when a sibling endpoint has no POST handler diff --git a/.changeset/pre/spotty-moons-sit.md b/.changeset/pre/spotty-moons-sit.md new file mode 100644 index 000000000000..2d6e050b7a2a --- /dev/null +++ b/.changeset/pre/spotty-moons-sit.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: record client output extensions in `builder.mimeTypes` diff --git a/.changeset/pre/spotty-pandas-brake.md b/.changeset/pre/spotty-pandas-brake.md new file mode 100644 index 000000000000..ce4c21736855 --- /dev/null +++ b/.changeset/pre/spotty-pandas-brake.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +perf: use a `Set` to check element ids when validating fragment links during prerendering diff --git a/.changeset/pre/spotty-pandas-wait.md b/.changeset/pre/spotty-pandas-wait.md new file mode 100644 index 000000000000..975e838fa90c --- /dev/null +++ b/.changeset/pre/spotty-pandas-wait.md @@ -0,0 +1,6 @@ +--- +'@sveltejs/adapter-netlify': patch +'@sveltejs/adapter-vercel': patch +--- + +fix: await `init` on every request to prevent race condition diff --git a/.changeset/pre/stable-version-build-hashes.md b/.changeset/pre/stable-version-build-hashes.md new file mode 100644 index 000000000000..b961c466d5d9 --- /dev/null +++ b/.changeset/pre/stable-version-build-hashes.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: preserve shared client chunk hashes when the app version changes diff --git a/.changeset/pre/stale-monkeys-type.md b/.changeset/pre/stale-monkeys-type.md new file mode 100644 index 000000000000..069703e657e7 --- /dev/null +++ b/.changeset/pre/stale-monkeys-type.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: include hoisted packages in Vite's `server.fs.allow` list diff --git a/.changeset/pre/standardize-app-template-diagnostics.md b/.changeset/pre/standardize-app-template-diagnostics.md new file mode 100644 index 000000000000..48d25f87598a --- /dev/null +++ b/.changeset/pre/standardize-app-template-diagnostics.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +chore: standardize app template diagnostics diff --git a/.changeset/pre/standardize-build-prerender-diagnostics.md b/.changeset/pre/standardize-build-prerender-diagnostics.md new file mode 100644 index 000000000000..2c14c46e30f9 --- /dev/null +++ b/.changeset/pre/standardize-build-prerender-diagnostics.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +chore: standardize Vite plugin, build, prerender and adapter diagnostics diff --git a/.changeset/pre/standardize-client-diagnostics.md b/.changeset/pre/standardize-client-diagnostics.md new file mode 100644 index 000000000000..956e7ea6cbf3 --- /dev/null +++ b/.changeset/pre/standardize-client-diagnostics.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +chore: standardize client navigation, preload, snapshot and enhancement diagnostics diff --git a/.changeset/pre/standardize-config-sync-diagnostics.md b/.changeset/pre/standardize-config-sync-diagnostics.md new file mode 100644 index 000000000000..2dcaf46010ab --- /dev/null +++ b/.changeset/pre/standardize-config-sync-diagnostics.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +chore: standardize configuration, environment and tsconfig diagnostics diff --git a/.changeset/pre/standardize-public-shared-diagnostics.md b/.changeset/pre/standardize-public-shared-diagnostics.md new file mode 100644 index 000000000000..d659efae5819 --- /dev/null +++ b/.changeset/pre/standardize-public-shared-diagnostics.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +chore: standardize public API, `$app/*` and deprecated module diagnostics diff --git a/.changeset/pre/standardize-routing-params-diagnostics.md b/.changeset/pre/standardize-routing-params-diagnostics.md new file mode 100644 index 000000000000..9c74308a9de3 --- /dev/null +++ b/.changeset/pre/standardize-routing-params-diagnostics.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +chore: standardize route discovery, parameter and route export diagnostics diff --git a/.changeset/pre/standardize-server-diagnostics.md b/.changeset/pre/standardize-server-diagnostics.md new file mode 100644 index 000000000000..17c928eea4bc --- /dev/null +++ b/.changeset/pre/standardize-server-diagnostics.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +chore: standardize server request, hook and page diagnostics diff --git a/.changeset/pre/static-assets-build-time.md b/.changeset/pre/static-assets-build-time.md new file mode 100644 index 000000000000..e4f16a491226 --- /dev/null +++ b/.changeset/pre/static-assets-build-time.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-node': major +--- + +breaking: record the list of static assets at build time, files added to the output afterwards are not served diff --git a/.changeset/pre/static-assets-etag.md b/.changeset/pre/static-assets-etag.md new file mode 100644 index 000000000000..794b2f3b51be --- /dev/null +++ b/.changeset/pre/static-assets-etag.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-node': major +--- + +breaking: validate static assets with content-hash `ETag`s and stop sending `Last-Modified` diff --git a/.changeset/pre/static-assets-methods.md b/.changeset/pre/static-assets-methods.md new file mode 100644 index 000000000000..eb0afc9057c9 --- /dev/null +++ b/.changeset/pre/static-assets-methods.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-node': major +--- + +breaking: serve static assets only to `GET` and `HEAD` requests diff --git a/.changeset/pre/steady-pages-stream.md b/.changeset/pre/steady-pages-stream.md new file mode 100644 index 000000000000..a3b2ebb9e8c0 --- /dev/null +++ b/.changeset/pre/steady-pages-stream.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: preserve metadata on streamed page responses diff --git a/.changeset/pre/strict-bees-arrive.md b/.changeset/pre/strict-bees-arrive.md new file mode 100644 index 000000000000..9943a53c21cd --- /dev/null +++ b/.changeset/pre/strict-bees-arrive.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: separate adapter Vite plugins into `pre` and `post` diff --git a/.changeset/pre/sunny-streets-go.md b/.changeset/pre/sunny-streets-go.md new file mode 100644 index 000000000000..faae47c11ab4 --- /dev/null +++ b/.changeset/pre/sunny-streets-go.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-auto': minor +--- + +feat: add zero-config deployment support for Render diff --git a/.changeset/pre/sw-manifest-immutable.md b/.changeset/pre/sw-manifest-immutable.md new file mode 100644 index 000000000000..99fae75cd78a --- /dev/null +++ b/.changeset/pre/sw-manifest-immutable.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: only include `_app/immutable` files in `$app/manifest`'s `immutable` in the service worker diff --git a/.changeset/pre/swift-otters-fork.md b/.changeset/pre/swift-otters-fork.md new file mode 100644 index 000000000000..5a68377573e7 --- /dev/null +++ b/.changeset/pre/swift-otters-fork.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +chore: remove the dead `SSRState.fallback` field and name the server state fork semantics diff --git a/.changeset/pre/swift-spiders-sync.md b/.changeset/pre/swift-spiders-sync.md new file mode 100644 index 000000000000..ad00e5d604ce --- /dev/null +++ b/.changeset/pre/swift-spiders-sync.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: atomically replace route metadata during sync diff --git a/.changeset/pre/tall-frogs-argue.md b/.changeset/pre/tall-frogs-argue.md new file mode 100644 index 000000000000..54c3f7723264 --- /dev/null +++ b/.changeset/pre/tall-frogs-argue.md @@ -0,0 +1,6 @@ +--- +'@sveltejs/kit': patch +'@sveltejs/package': patch +--- + +fix: import resolved peer dependencies as file URLs so project-relative resolution works on Windows diff --git a/.changeset/pre/tame-cookies-relax.md b/.changeset/pre/tame-cookies-relax.md new file mode 100644 index 000000000000..873edfff833e --- /dev/null +++ b/.changeset/pre/tame-cookies-relax.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': minor +--- + +fix: default cookies to `secure` to `false` during development diff --git a/.changeset/pre/tame-maps-travel.md b/.changeset/pre/tame-maps-travel.md new file mode 100644 index 000000000000..8d305cb284fc --- /dev/null +++ b/.changeset/pre/tame-maps-travel.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: preserve sourcemap source paths when adapters copy build output diff --git a/.changeset/pre/tangy-flies-pay.md b/.changeset/pre/tangy-flies-pay.md new file mode 100644 index 000000000000..64bb06f5745b --- /dev/null +++ b/.changeset/pre/tangy-flies-pay.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: log errors caught by the Vite dev server handler diff --git a/.changeset/pre/tangy-llamas-attack.md b/.changeset/pre/tangy-llamas-attack.md new file mode 100644 index 000000000000..83cf86f5c081 --- /dev/null +++ b/.changeset/pre/tangy-llamas-attack.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-netlify': patch +--- + +fix: import the generated manifest via `pathToFileURL`, which handles special characters in the project path diff --git a/.changeset/pre/ten-lands-send.md b/.changeset/pre/ten-lands-send.md new file mode 100644 index 000000000000..e870ae34ded9 --- /dev/null +++ b/.changeset/pre/ten-lands-send.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: remove param files in folder in favor of `params.js/ts` file diff --git a/.changeset/pre/ten-signs-sing.md b/.changeset/pre/ten-signs-sing.md new file mode 100644 index 000000000000..d66f7146a58c --- /dev/null +++ b/.changeset/pre/ten-signs-sing.md @@ -0,0 +1,6 @@ +--- +"@sveltejs/kit": major +--- + +breaking: move env-related types to `@sveltejs/kit/env` + \ No newline at end of file diff --git a/.changeset/pre/ten-snakes-show.md b/.changeset/pre/ten-snakes-show.md new file mode 100644 index 000000000000..5e4cf65f7260 --- /dev/null +++ b/.changeset/pre/ten-snakes-show.md @@ -0,0 +1,6 @@ +--- +"@sveltejs/kit": patch +--- + +chore: bump `acorn` to 8.18.0 + \ No newline at end of file diff --git a/.changeset/pre/ten-worms-shine.md b/.changeset/pre/ten-worms-shine.md new file mode 100644 index 000000000000..af24fd3da5aa --- /dev/null +++ b/.changeset/pre/ten-worms-shine.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-vercel': major +--- + +breaking: remove support for edge and Node 20 runtimes diff --git a/.changeset/pre/thick-needles-fold.md b/.changeset/pre/thick-needles-fold.md new file mode 100644 index 000000000000..e4a5ca93c541 --- /dev/null +++ b/.changeset/pre/thick-needles-fold.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: resolve `root` per instance of the SvelteKit Vite plugin diff --git a/.changeset/pre/thirty-hands-prove.md b/.changeset/pre/thirty-hands-prove.md new file mode 100644 index 000000000000..e6eef77e96f7 --- /dev/null +++ b/.changeset/pre/thirty-hands-prove.md @@ -0,0 +1,5 @@ +--- +"@sveltejs/kit": patch +--- + +fix: report tsconfig parse errors on Windows diff --git a/.changeset/pre/tidy-adapters-config.md b/.changeset/pre/tidy-adapters-config.md new file mode 100644 index 000000000000..e48c02eec51b --- /dev/null +++ b/.changeset/pre/tidy-adapters-config.md @@ -0,0 +1,7 @@ +--- +'@sveltejs/adapter-cloudflare': patch +'@sveltejs/adapter-node': patch +'@sveltejs/adapter-static': patch +--- + +chore: use `builder.config` to access SvelteKit config settings diff --git a/.changeset/pre/tidy-cats-smile.md b/.changeset/pre/tidy-cats-smile.md new file mode 100644 index 000000000000..823dae25d161 --- /dev/null +++ b/.changeset/pre/tidy-cats-smile.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: return root layout server data for error-page data requests to non-existent routes diff --git a/.changeset/pre/tidy-crawlers-stop.md b/.changeset/pre/tidy-crawlers-stop.md new file mode 100644 index 000000000000..ea7e498ab4ab --- /dev/null +++ b/.changeset/pre/tidy-crawlers-stop.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: prevent prerender crawler from hanging on unterminated unquoted attributes diff --git a/.changeset/pre/tidy-donkeys-shake.md b/.changeset/pre/tidy-donkeys-shake.md new file mode 100644 index 000000000000..6e2a444af4b5 --- /dev/null +++ b/.changeset/pre/tidy-donkeys-shake.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/package': patch +--- + +fix: emit declarations when the tsconfig lives above the package root diff --git a/.changeset/pre/tidy-edges-inline.md b/.changeset/pre/tidy-edges-inline.md new file mode 100644 index 000000000000..4c73423be8b4 --- /dev/null +++ b/.changeset/pre/tidy-edges-inline.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-netlify': patch +--- + +chore: include edge function configuration in the function module diff --git a/.changeset/pre/tidy-elephants-jump.md b/.changeset/pre/tidy-elephants-jump.md new file mode 100644 index 000000000000..971d1209d2af --- /dev/null +++ b/.changeset/pre/tidy-elephants-jump.md @@ -0,0 +1,5 @@ +--- +"@sveltejs/kit": patch +--- + +fix: prevent `await_reactivity_loss` warning and `state_unsafe_mutation` error when the new version detector runs after an `await` diff --git a/.changeset/pre/tidy-forms-redact.md b/.changeset/pre/tidy-forms-redact.md new file mode 100644 index 000000000000..d65a4b5b08e8 --- /dev/null +++ b/.changeset/pre/tidy-forms-redact.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: redact nested and typed underscore-prefixed remote form fields after invalid submissions diff --git a/.changeset/pre/tidy-hoops-brake.md b/.changeset/pre/tidy-hoops-brake.md new file mode 100644 index 000000000000..9a52bef88cec --- /dev/null +++ b/.changeset/pre/tidy-hoops-brake.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: ignore path casing differences when warning about overridden Vite config on Windows diff --git a/.changeset/pre/tidy-icons-rest.md b/.changeset/pre/tidy-icons-rest.md new file mode 100644 index 000000000000..1bb0cb62ef1e --- /dev/null +++ b/.changeset/pre/tidy-icons-rest.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: return a lightweight 404 instead of rendering the error page for subresource requests diff --git a/.changeset/pre/tidy-owls-preload.md b/.changeset/pre/tidy-owls-preload.md new file mode 100644 index 000000000000..c3a8fc8848e1 --- /dev/null +++ b/.changeset/pre/tidy-owls-preload.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: `preloadCode` now takes a route ID (e.g. `/blog/[slug]`) instead of a pathname. Route IDs are not prefixed with `paths.base` diff --git a/.changeset/pre/tidy-pages-route.md b/.changeset/pre/tidy-pages-route.md new file mode 100644 index 000000000000..ae2e8e934d28 --- /dev/null +++ b/.changeset/pre/tidy-pages-route.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-vercel': patch +--- + +fix: match sibling `.html__route.js` route resolution requests diff --git a/.changeset/pre/tidy-pumas-travel.md b/.changeset/pre/tidy-pumas-travel.md new file mode 100644 index 000000000000..cee4088e7800 --- /dev/null +++ b/.changeset/pre/tidy-pumas-travel.md @@ -0,0 +1,6 @@ +--- +'@sveltejs/kit': patch +'@sveltejs/adapter-node': patch +--- + +fix: preserve stripped path prefixes by making trailing-slash redirects relative diff --git a/.changeset/pre/tidy-remote-form-search.md b/.changeset/pre/tidy-remote-form-search.md new file mode 100644 index 000000000000..f49c5355bafe --- /dev/null +++ b/.changeset/pre/tidy-remote-form-search.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: preserve the current URL search parameters when submitting a remote form without JavaScript diff --git a/.changeset/pre/tidy-remotes-own.md b/.changeset/pre/tidy-remotes-own.md new file mode 100644 index 000000000000..7f73c71f9ea3 --- /dev/null +++ b/.changeset/pre/tidy-remotes-own.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: only treat files in node_modules as remote modules when the package has a peer dependency on @sveltejs/kit diff --git a/.changeset/pre/tidy-remotes-shake.md b/.changeset/pre/tidy-remotes-shake.md new file mode 100644 index 000000000000..748ba8047014 --- /dev/null +++ b/.changeset/pre/tidy-remotes-shake.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: treeshake prerendered remote functions in the right chunks diff --git a/.changeset/pre/tidy-tomls-parse.md b/.changeset/pre/tidy-tomls-parse.md new file mode 100644 index 000000000000..f58ad1b4bed8 --- /dev/null +++ b/.changeset/pre/tidy-tomls-parse.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-netlify': patch +--- + +chore: replace `@iarna/toml` with `smol-toml` diff --git a/.changeset/pre/tidy-waves.md b/.changeset/pre/tidy-waves.md new file mode 100644 index 000000000000..462dd555f4ea --- /dev/null +++ b/.changeset/pre/tidy-waves.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: move `BeforeNavigate`, `OnNavigate`, `AfterNavigate`, `Navigation`, `NavigationTarget`, `NavigationType`, `GotoOptions` and the `Navigation*` variant types from `@sveltejs/kit` to `$app/navigation` diff --git a/.changeset/pre/tiny-buffers-serialize.md b/.changeset/pre/tiny-buffers-serialize.md new file mode 100644 index 000000000000..0ee574ad0728 --- /dev/null +++ b/.changeset/pre/tiny-buffers-serialize.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: correctly serialize Node.js buffers returned from remote functions during SSR diff --git a/.changeset/pre/tiny-cases-help.md b/.changeset/pre/tiny-cases-help.md new file mode 100644 index 000000000000..85d2c4ef3219 --- /dev/null +++ b/.changeset/pre/tiny-cases-help.md @@ -0,0 +1,6 @@ +--- +"@sveltejs/package": patch +--- + +chore: bump `svelte2tsx` to 0.7.60 + \ No newline at end of file diff --git a/.changeset/pre/tough-errors-smile.md b/.changeset/pre/tough-errors-smile.md new file mode 100644 index 000000000000..064911a3b7a3 --- /dev/null +++ b/.changeset/pre/tough-errors-smile.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: preserve errors with read-only stack properties diff --git a/.changeset/pre/true-yaks-smell.md b/.changeset/pre/true-yaks-smell.md new file mode 100644 index 000000000000..99d72742e9df --- /dev/null +++ b/.changeset/pre/true-yaks-smell.md @@ -0,0 +1,7 @@ +--- +'@sveltejs/adapter-netlify': patch +'@sveltejs/adapter-node': patch +'@sveltejs/adapter-vercel': patch +--- + +chore: bump Rolldown to `1.2.0` diff --git a/.changeset/pre/tweak-types-dedup.md b/.changeset/pre/tweak-types-dedup.md new file mode 100644 index 000000000000..dc98d21ba82b --- /dev/null +++ b/.changeset/pre/tweak-types-dedup.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +chore: deduplicate type-stripping logic in `tweak_types` diff --git a/.changeset/pre/twenty-actors-relax.md b/.changeset/pre/twenty-actors-relax.md new file mode 100644 index 000000000000..4923d09caf46 --- /dev/null +++ b/.changeset/pre/twenty-actors-relax.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: avoid infinite loop when building with `--watch` flag diff --git a/.changeset/pre/twenty-squids-tie.md b/.changeset/pre/twenty-squids-tie.md new file mode 100644 index 000000000000..89d354471e90 --- /dev/null +++ b/.changeset/pre/twenty-squids-tie.md @@ -0,0 +1,6 @@ +--- +"@sveltejs/kit": major +--- + +breaking: move `ActionResult` and `SubmitFunction` from `@sveltejs/kit` to `$app/forms` + \ No newline at end of file diff --git a/.changeset/pre/unify-walk-helpers.md b/.changeset/pre/unify-walk-helpers.md new file mode 100644 index 000000000000..ad1a984c2d85 --- /dev/null +++ b/.changeset/pre/unify-walk-helpers.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +chore: unify the `walk` and `list_files` filesystem helpers diff --git a/.changeset/pre/upset-cities-vanish.md b/.changeset/pre/upset-cities-vanish.md new file mode 100644 index 000000000000..8cbaa19ed861 --- /dev/null +++ b/.changeset/pre/upset-cities-vanish.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: error during development if the adapter does not support instrumentation and it exists diff --git a/.changeset/pre/upset-needles-beg.md b/.changeset/pre/upset-needles-beg.md new file mode 100644 index 000000000000..68fbbc5ae54e --- /dev/null +++ b/.changeset/pre/upset-needles-beg.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: use mouseover+mousemove for preloading to reduce events diff --git a/.changeset/pre/valid-files-rest.md b/.changeset/pre/valid-files-rest.md new file mode 100644 index 000000000000..17e5324fa96e --- /dev/null +++ b/.changeset/pre/valid-files-rest.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: reject invalid binary form file metadata diff --git a/.changeset/pre/validation-errors-handle-error.md b/.changeset/pre/validation-errors-handle-error.md new file mode 100644 index 000000000000..4be9e70b2300 --- /dev/null +++ b/.changeset/pre/validation-errors-handle-error.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: remove `handleValidationError` and pass remote function validation errors to `handleError` with `kind: 'validation'` diff --git a/.changeset/pre/vary-only-when-precompressed.md b/.changeset/pre/vary-only-when-precompressed.md new file mode 100644 index 000000000000..6a543c91002d --- /dev/null +++ b/.changeset/pre/vary-only-when-precompressed.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-node': patch +--- + +fix: don't send `Vary: Accept-Encoding` for assets that were never precompressed diff --git a/.changeset/pre/vercel-symlink-eexist.md b/.changeset/pre/vercel-symlink-eexist.md new file mode 100644 index 000000000000..133bbc970271 --- /dev/null +++ b/.changeset/pre/vercel-symlink-eexist.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-vercel': patch +--- + +fix: ignore `EEXIST` errors when symlinking traced files that resolve to the same destination diff --git a/.changeset/pre/vercel-trace-cwd-root-glob.md b/.changeset/pre/vercel-trace-cwd-root-glob.md new file mode 100644 index 000000000000..133ebd0bff0d --- /dev/null +++ b/.changeset/pre/vercel-trace-cwd-root-glob.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-vercel': patch +--- + +fix: trace `process.cwd()`-relative files from the project directory and never glob from the filesystem root diff --git a/.changeset/pre/violet-hooks-guard.md b/.changeset/pre/violet-hooks-guard.md new file mode 100644 index 000000000000..9ee7f87248ef --- /dev/null +++ b/.changeset/pre/violet-hooks-guard.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: error on server-only imports reachable from hooks or service worker files outside the project root diff --git a/.changeset/pre/violet-lamps-repeat.md b/.changeset/pre/violet-lamps-repeat.md new file mode 100644 index 000000000000..50e9f2785c34 --- /dev/null +++ b/.changeset/pre/violet-lamps-repeat.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-node': patch +--- + +chore: remove polka, attach the handler to the http server directly diff --git a/.changeset/pre/violet-planets-tell.md b/.changeset/pre/violet-planets-tell.md new file mode 100644 index 000000000000..b3a5fa38f208 --- /dev/null +++ b/.changeset/pre/violet-planets-tell.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: write generated tsconfig to `node_modules/$app/tsconfig.json` so that tools with simplified tsconfig resolution can find it diff --git a/.changeset/pre/wacky-mice-attack.md b/.changeset/pre/wacky-mice-attack.md new file mode 100644 index 000000000000..5e6d531bf847 --- /dev/null +++ b/.changeset/pre/wacky-mice-attack.md @@ -0,0 +1,6 @@ +--- +"@sveltejs/adapter-cloudflare": patch +--- + +chore: bump `@cloudflare/worker-types` to 5.20260809.1 + \ No newline at end of file diff --git a/.changeset/pre/warm-boats-taste.md b/.changeset/pre/warm-boats-taste.md new file mode 100644 index 000000000000..b381aecec82d --- /dev/null +++ b/.changeset/pre/warm-boats-taste.md @@ -0,0 +1,6 @@ +--- +"@sveltejs/kit": patch +--- + +chore: emit env modules to disk + \ No newline at end of file diff --git a/.changeset/pre/whole-buckets-clean.md b/.changeset/pre/whole-buckets-clean.md new file mode 100644 index 000000000000..71b7326958b0 --- /dev/null +++ b/.changeset/pre/whole-buckets-clean.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: more informative error message when running a command inside a query or prerender function diff --git a/.changeset/pre/wicked-masks-end.md b/.changeset/pre/wicked-masks-end.md new file mode 100644 index 000000000000..85ebff9c7747 --- /dev/null +++ b/.changeset/pre/wicked-masks-end.md @@ -0,0 +1,5 @@ +--- +"@sveltejs/adapter-netlify": patch +--- + +chore: identify serverless functions with a UUID instead of Netlify's function token diff --git a/.changeset/pre/wicked-olives-fold.md b/.changeset/pre/wicked-olives-fold.md new file mode 100644 index 000000000000..1c9e47e47be2 --- /dev/null +++ b/.changeset/pre/wicked-olives-fold.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + + fix: adjust error overload for optional `App.Error` parameters diff --git a/.changeset/pre/wide-mangos-admire.md b/.changeset/pre/wide-mangos-admire.md new file mode 100644 index 000000000000..da5acefa53e0 --- /dev/null +++ b/.changeset/pre/wide-mangos-admire.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +breaking: remove deprecated `.run()` method from live queries diff --git a/.changeset/pre/wild-donkeys-listen.md b/.changeset/pre/wild-donkeys-listen.md new file mode 100644 index 000000000000..001f7165fc91 --- /dev/null +++ b/.changeset/pre/wild-donkeys-listen.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: prerender and crawl pages whose `content-type` header carries a `charset` parameter diff --git a/.changeset/pre/wild-impalas-do.md b/.changeset/pre/wild-impalas-do.md new file mode 100644 index 000000000000..c74033faba44 --- /dev/null +++ b/.changeset/pre/wild-impalas-do.md @@ -0,0 +1,6 @@ +--- +"@sveltejs/adapter-netlify": patch +--- + +fix: ensure serverless function files have unique names + \ No newline at end of file diff --git a/.changeset/pre/wild-steaks-visit.md b/.changeset/pre/wild-steaks-visit.md new file mode 100644 index 000000000000..e70fd2893870 --- /dev/null +++ b/.changeset/pre/wild-steaks-visit.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': minor +--- + +feat: warn when naively proxying requests that result in responses with a `content-encoding` header diff --git a/.changeset/pre/wise-moons-read.md b/.changeset/pre/wise-moons-read.md new file mode 100644 index 000000000000..839ccf11669d --- /dev/null +++ b/.changeset/pre/wise-moons-read.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: stream promised `read` results lazily instead of eagerly buffering them diff --git a/.changeset/pre/wise-pandas-cheat.md b/.changeset/pre/wise-pandas-cheat.md new file mode 100644 index 000000000000..efb3d3cf38fa --- /dev/null +++ b/.changeset/pre/wise-pandas-cheat.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: copy worker files emitted by the server build to the client output directory diff --git a/.changeset/pre/wise-parsers-swim.md b/.changeset/pre/wise-parsers-swim.md new file mode 100644 index 000000000000..7c9ad2f82079 --- /dev/null +++ b/.changeset/pre/wise-parsers-swim.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +chore: parse page options and remote modules with Vite's `parseSync` instead of `acorn` diff --git a/.changeset/pre/wise-pumas-deny.md b/.changeset/pre/wise-pumas-deny.md new file mode 100644 index 000000000000..ff0b7d4a2f4a --- /dev/null +++ b/.changeset/pre/wise-pumas-deny.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/package': patch +--- + +fix: declare typescript as an optional peer dependency so svelte-package works under strict node-linkers diff --git a/.changeset/pre/wise-radios-remember.md b/.changeset/pre/wise-radios-remember.md new file mode 100644 index 000000000000..a81ca3837d45 --- /dev/null +++ b/.changeset/pre/wise-radios-remember.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': minor +--- + +feat: accept the checked state as a third argument to `.as('radio', ...)` and `.as('checkbox', ...)` so that these inputs reset to it after a submission diff --git a/.changeset/pre/witty-moons-smile.md b/.changeset/pre/witty-moons-smile.md new file mode 100644 index 000000000000..2b665846fe86 --- /dev/null +++ b/.changeset/pre/witty-moons-smile.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: correctly decode `[u+nnnn]` escape sequences above `ffff` diff --git a/.changeset/pre/yellow-apes-unite.md b/.changeset/pre/yellow-apes-unite.md new file mode 100644 index 000000000000..90fb45ff6667 --- /dev/null +++ b/.changeset/pre/yellow-apes-unite.md @@ -0,0 +1,6 @@ +--- +"@sveltejs/kit": patch +--- + +fix: Reject all pending query promises when a query fails before resolving with a value for the first time + \ No newline at end of file diff --git a/.changeset/pre/yellow-items-sink.md b/.changeset/pre/yellow-items-sink.md new file mode 100644 index 000000000000..2b380eaf8a56 --- /dev/null +++ b/.changeset/pre/yellow-items-sink.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': minor +--- + +feat: add an `applyReroute` helper for adapters that support split serverless function deployments diff --git a/.changeset/quiet-foxes-stay.md b/.changeset/quiet-foxes-stay.md new file mode 100644 index 000000000000..33856a224602 --- /dev/null +++ b/.changeset/quiet-foxes-stay.md @@ -0,0 +1,6 @@ +--- +'@sveltejs/kit': patch +'@sveltejs/adapter-node': patch +--- + +fix: prevent scheme-like path segments from causing off-site trailing-slash redirects diff --git a/.eslint/no-exports-to-runtime-imports.js b/.eslint/no-exports-to-runtime-imports.js new file mode 100644 index 000000000000..5a1873531882 --- /dev/null +++ b/.eslint/no-exports-to-runtime-imports.js @@ -0,0 +1,52 @@ +// @ts-expect-error no types here +import path from 'node:path'; + +/** + * @type {import('eslint').Rule.RuleModule} + */ +export default { + meta: { + type: 'problem', + docs: { + description: 'disallow imports from src/exports to src/runtime', + category: 'Possible Errors', + recommended: true + }, + schema: [], + messages: { + noExportsToRuntimeImport: + 'Imports from src/exports to src/runtime are not allowed. Move shared code outside src/runtime instead.' + } + }, + + create(context) { + const runtime_path = path.resolve(import.meta.dirname, '../packages/kit/src/runtime'); + + /** @param {import('estree').Literal} node */ + function check(node) { + const import_path = node.value; + if (typeof import_path !== 'string') return; + + const is_runtime_subpath = import_path.startsWith('#app/'); + const is_relative_runtime_import = + import_path.startsWith('.') && + path + .relative(runtime_path, path.resolve(path.dirname(context.filename), import_path)) + .split(path.sep)[0] !== '..'; + + if (is_runtime_subpath || is_relative_runtime_import) { + context.report({ + node, + messageId: 'noExportsToRuntimeImport' + }); + } + } + + return { + ImportDeclaration: (node) => check(node.source), + ExportNamedDeclaration: (node) => node.source && check(node.source), + ExportAllDeclaration: (node) => check(node.source), + ImportExpression: (node) => check(node.source) + }; + } +}; diff --git a/.eslint/require-path-to-file-url.js b/.eslint/require-path-to-file-url.js new file mode 100644 index 000000000000..81f206ca8a38 --- /dev/null +++ b/.eslint/require-path-to-file-url.js @@ -0,0 +1,89 @@ +/** + * @type {import('eslint').Rule.RuleModule} + */ +export default { + meta: { + type: 'problem', + docs: { + description: 'require dynamic imports of computed file paths to go through pathToFileURL' + }, + schema: [], + messages: { + requirePathToFileURL: + 'Dynamic import() of a computed path must be wrapped in pathToFileURL(...).href — on Windows, absolute paths parse as URLs with the drive letter as protocol and fail to import. See https://github.com/sveltejs/kit/issues/16620' + } + }, + + create(context) { + /** + * Returns true if the expression is safe to pass to `import()`: + * a static specifier, a relative computed specifier, or a value + * wrapped in `pathToFileURL`/`new URL` + * @param {import('estree').Node} node + * @param {number} depth + * @returns {boolean} + */ + function is_safe(node, depth = 0) { + if (node.type === 'Literal') { + return true; + } + + if (node.type === 'TemplateLiteral') { + if (node.expressions.length === 0) return true; + + // a computed specifier with a static relative prefix resolves + // relative to the importing module — no drive letters involved + const prefix = node.quasis[0].value.cooked ?? ''; + return prefix.startsWith('./') || prefix.startsWith('../'); + } + + if (node.type === 'CallExpression' || node.type === 'NewExpression') { + const callee = node.callee; + if (callee.type !== 'Identifier') return false; + + // single-argument new URL(path) misparses absolute windows paths just like import(), so require a base + return ( + callee.name === 'pathToFileURL' || (callee.name === 'URL' && node.arguments.length > 1) + ); + } + + // `pathToFileURL(...).href` and `new URL(...).href` + if (node.type === 'MemberExpression') { + return ( + node.property.type === 'Identifier' && + node.property.name === 'href' && + is_safe(node.object, depth) + ); + } + + // resolve an identifier one hop to its declaration + if (node.type === 'Identifier' && depth === 0) { + const variable = context.sourceCode + .getScope(node) + .references.find((ref) => ref.identifier === node)?.resolved; + + const def = variable?.defs[0]; + + if ( + variable?.defs.length === 1 && + def?.type === 'Variable' && + def.node.init && + // a reassignment after a safe initializer would not be seen here + variable.references.every((ref) => !ref.isWrite() || ref.init) + ) { + return is_safe(def.node.init, depth + 1); + } + } + + return false; + } + + return { + ImportExpression(node) { + if (!is_safe(node.source)) { + context.report({ node: node.source, messageId: 'requirePathToFileURL' }); + } + } + }; + } +}; diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index a94d39153bd8..1b48302b9232 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -25,7 +25,7 @@ body: id: logs attributes: label: Logs - description: "Please include browser console and server logs around the time this bug occurred. Please try not to insert an image but copy paste the log text." + description: 'Please include browser console and server logs around the time this bug occurred. Please try not to insert an image but copy paste the log text.' render: Shell - type: textarea id: system-info diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 5324344585ba..65c4f44f76c8 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -1,4 +1,4 @@ -name: "Feature Request" +name: 'Feature Request' description: Suggest an idea for this project body: - type: markdown diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index fcff7dc51487..b4e383020db8 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -5,14 +5,17 @@ closes #\n\n\n\n\n{@render children()}\n```" + }, + "page_option_in_component": { + "variants": [ + { + "text": "`%file%`: `%option%` will be ignored — move it to `%fixed%` instead. See https://svelte.dev/docs/kit/page-options for more information.", + "variables": [ + "file", + "option", + "fixed" + ] + } + ], + "details": "[Page options](https://svelte.dev/docs/kit/page-options) such as `prerender`, `ssr`, `csr` and `trailingSlash` are read from `+page.js`, `+page.server.js`, `+layout.js` or `+layout.server.js`, not from the `.svelte` component. Move the export to one of those files." + }, + "transform_index_html_unsupported": { + "variants": [ + { + "text": "The following plugins may not work correctly because they use the `transformIndexHtml` hook which is not supported:\n%plugins%", + "variables": [ + "plugins" + ] + } + ], + "details": "SvelteKit renders HTML itself, from `src/app.html`, rather than using an `index.html` file processed by Vite, so Vite's `transformIndexHtml` hook never runs for your pages. Plugins that rely on it to inject tags won't have any effect. Use [`transformPageChunk`](https://svelte.dev/docs/kit/hooks#Server-hooks-handle) in your `handle` hook or edit `src/app.html` instead." + }, + "vite_config_overridden": { + "variants": [ + { + "text": "The following Vite config options will be overridden by SvelteKit:\n%options%", + "variables": [ + "options" + ] + } + ], + "details": "SvelteKit controls these Vite options because its build depends on them, so your values are replaced. Remove them from your Vite config. SvelteKit usually provides its own option for the same purpose; see the [configuration reference](https://svelte.dev/docs/kit/configuration)." + }, + "prerender_redirect_location_missing": { + "variants": [ + { + "text": "`location` header missing on redirect received from `%path%`", + "variables": [ + "path" + ] + } + ], + "details": "While [prerendering](https://svelte.dev/docs/kit/page-options#prerender), this path responded with a 3xx status but no `location` header, so SvelteKit doesn't know where it redirects to and doesn't save it. Use [`redirect`](https://svelte.dev/docs/kit/@sveltejs-kit#redirect) from `@sveltejs/kit`, which sets the header for you, or add a `location` header to the response." + }, + "config_option_deprecated": { + "variants": [ + { + "text": "The `%keypath%` option is deprecated, and will be removed in a future version", + "variables": [ + "keypath" + ] + } + ], + "details": "This option still works, but it will be removed in a future major version of SvelteKit. See the [configuration reference](https://svelte.dev/docs/kit/configuration) for the recommended alternative." + }, + "config_option_deprecated_alias": { + "variants": [ + { + "text": "The `%keypath%` option is deprecated, and will be removed in a future version of SvelteKit. Use subpath imports instead: https://svelte.dev/docs/kit/$lib", + "variables": [ + "keypath" + ] + } + ], + "details": "Node's [subpath imports](https://nodejs.org/api/packages.html#subpath-imports), declared in the `imports` field of your `package.json`, are understood natively by Vite, TypeScript and other tools, so they don't need a separate SvelteKit option. For example, instead of `alias: { $utils: 'src/utils' }`, add this to your `package.json`...\n\n```json\n{\n\t\"imports\": {\n\t\t\"#utils/*\": \"./src/utils/*\"\n\t}\n}\n```\n\n...and import from `#utils/...`. See [`$lib`](https://svelte.dev/docs/kit/$lib) for how SvelteKit uses the same mechanism for `#lib`." + }, + "config_option_deprecated_typescript": { + "variants": [ + { + "text": "The `%keypath%` option is deprecated, and will be removed in a future version. Add configuration to `tsconfig.json` directly", + "variables": [ + "keypath" + ] + } + ], + "details": "Rather than modifying SvelteKit's generated TypeScript configuration with a function, add the options to your own `tsconfig.json`, which [extends `$app/tsconfig`](https://svelte.dev/docs/kit/$app-tsconfig)." + }, + "route_file_prefix_missing": { + "variants": [ + { + "text": "Missing route file prefix. Did you mean %corrected%? at %file%", + "variables": [ + "corrected", + "file" + ] + } + ], + "details": "SvelteKit only recognises [route files](https://svelte.dev/docs/kit/routing) whose names start with `+`, such as `+page.svelte`, `+layout.svelte`, `+page.js` and `+server.js`. This file matches a route filename except for the missing prefix, so it will be ignored. Rename it to the suggested name if it is intended to be a route file, or choose a different name to distinguish it from one." + }, + "file_name_misspelled": { + "variants": [ + { + "text": "%description%. Did you mean %corrected%? at %file%", + "variables": [ + "description", + "corrected", + "file" + ] + } + ], + "details": "SvelteKit only recognises files with specific names, such as `src/hooks.server.js` or `src/hooks.client.js`. It found a file whose name looks like a misspelling of one of them, so that file will be ignored. Rename it to the suggested name." + }, + "tsconfig_extends_missing": { + "variants": [ + { + "text": "`%file%` should extend SvelteKit's built-in configuration:\n%example%", + "variables": [ + "file", + "example" + ] + } + ], + "details": "Your TypeScript configuration needs to extend the one SvelteKit generates, so that TypeScript knows about generated types, path aliases and the compiler options SvelteKit relies on. The warning includes an example configuration. For your root `tsconfig.json`, that looks like this:\n\n```json\n/// file: tsconfig.json\n{\n\t\"extends\": \"$app/tsconfig\",\n\t\"include\": [\"src\", \"test\", \"*\"],\n\t\"exclude\": [\"src/service-worker\"]\n}\n```\n\nSee [`$app/tsconfig`](https://svelte.dev/docs/kit/$app-tsconfig) and, for service workers, [`$app/tsconfig/service-worker`](https://svelte.dev/docs/kit/$app-tsconfig-service-worker)." + }, + "tsconfig_invalid": { + "variants": [ + { + "text": "Found issues while validating `%file%`:\n%issues%", + "variables": [ + "file", + "issues" + ] + } + ], + "details": "Your `tsconfig.json` extends SvelteKit's generated configuration, but overrides settings that SvelteKit relies on. In every case, the fix is to stop overriding what [`$app/tsconfig`](https://svelte.dev/docs/kit/$app-tsconfig) sets, or to add back what's missing:\n\n- **`types` was overwritten** — SvelteKit includes `\"$app/types\"` in `compilerOptions.types`, so that TypeScript can see generated module declarations for things like [environment variables](https://svelte.dev/docs/kit/environment-variables). Setting `types` yourself replaces that array rather than merging with it, so include the missing entries, for example `\"types\": [\"$app/types\", \"node\"]`.\n- **`paths` was overwritten** — SvelteKit generates `compilerOptions.paths` from the (deprecated) [`alias`](https://svelte.dev/docs/kit/configuration#alias) option, so that TypeScript resolves these imports the same way Vite does. Remove your `paths`, and declare aliases with [subpath imports](https://nodejs.org/api/packages.html#subpath-imports) in `package.json` instead.\n- **A file should be added to the `exclude` array** — service workers run in a different environment from the rest of your app and need their own TypeScript project, which extends [`$app/tsconfig/service-worker`](https://svelte.dev/docs/kit/$app-tsconfig-service-worker). Exclude them from your root `tsconfig.json`, for example `\"exclude\": [\"src/service-worker\"]`.\n- **An essential option was overwritten** — `isolatedModules` must be `true` because Vite compiles modules one at a time, and `verbatimModuleSyntax` must be `true` so that type imports are safely removed from `.svelte` files. Remove the override." + }, + "enhance_file_without_enctype": { + "variants": [ + { + "text": "Your form contains `` fields, but is missing the necessary `enctype=\"multipart/form-data\"` attribute", + "variables": [] + } + ], + "details": "Without `enctype=\"multipart/form-data\"`, a browser submitting the form natively only sends the names of the selected files, not their contents. An enhanced form would behave differently, so [`use:enhance`](https://svelte.dev/docs/kit/form-actions#Progressive-enhancement-use:enhance) rejects the submission rather than letting the two drift apart (see [#9819](https://github.com/sveltejs/kit/issues/9819)). Add the attribute to the `
`:\n\n```svelte\n\n\t\n
\n```" + }, + "enhance_invalid_method": { + "variants": [ + { + "text": "`use:enhance` can only be used on `
` elements with `method=\"POST\"`", + "variables": [] + } + ], + "details": "[`use:enhance`](https://svelte.dev/docs/kit/form-actions#Progressive-enhancement-use:enhance) progressively enhances forms that submit to [form actions](https://svelte.dev/docs/kit/form-actions), which only handle `POST` requests. Add `method=\"POST\"` to the form. A `GET` form doesn't need enhancing: SvelteKit's router already handles its submissions client-side, like a link." + }, + "goto_options_removed": { + "variants": [ + { + "text": "The `goto(..., { noScroll: true, keepFocus: true })` options have been replaced by `reset: false`", + "variables": [] + } + ], + "details": "[`goto`](https://svelte.dev/docs/kit/$app-navigation#goto) no longer accepts separate `noScroll` and `keepFocus` options. Pass `reset: false` instead, which preserves both the scroll position and the focused element:\n\n```js\nimport { goto } from '$app/navigation';\n\nawait goto('/somewhere', { reset: false });\n```" + }, + "navigation_before_start": { + "variants": [ + { + "text": "Cannot call `%caller%(...)` before router is initialized", + "variables": [ + "caller" + ] + } + ], + "details": "Shallow routing functions such as [`pushState`](https://svelte.dev/docs/kit/$app-navigation#pushState), [`replaceState`](https://svelte.dev/docs/kit/$app-navigation#replaceState) and `goto(url, { shallow: true })` update the router's history state, which only exists once the app has started in the browser. Call them from event handlers, effects or `onMount` rather than while modules or components are first being evaluated." + }, + "navigation_external_url": { + "variants": [ + { + "text": "Cannot use `%caller%` with an external URL. Use `window.location = \"%url%\"` instead", + "variables": [ + "caller", + "url" + ] + } + ], + "details": "[`goto`](https://svelte.dev/docs/kit/$app-navigation#goto) and the [shallow routing](https://svelte.dev/docs/kit/shallow-routing) functions navigate within your app using SvelteKit's client-side router, so they can't go to a different origin. To leave the app, perform a full-page navigation by assigning to `window.location` or rendering a regular link." + }, + "navigation_route_missing": { + "variants": [ + { + "text": "Cannot use `%caller%` with a URL that does not resolve to a route within the app. Use `window.location = \"%url%\"` instead", + "variables": [ + "caller", + "url" + ] + } + ], + "details": "[`goto`](https://svelte.dev/docs/kit/$app-navigation#goto) and the [shallow routing](https://svelte.dev/docs/kit/shallow-routing) functions only navigate to pages of your app. The URL is on the same origin, but no route matches it — check it for typos, and remember that routes without a `+page` (for example, those with only a `+server.js`) can't be navigated to client-side. If the URL is served by something other than SvelteKit, navigate to it with `window.location` instead." + }, + "redirect_loop": { + "variants": [ + { + "text": "Redirect loop while navigating to `%url%`", + "variables": [ + "url" + ] + } + ], + "details": "A navigation followed more than 20 redirects without reaching a page, which usually means two or more `load` functions redirect to each other, or a `load` function redirects to its own route. Check the conditions under which each [`redirect`](https://svelte.dev/docs/kit/@sveltejs-kit#redirect) is thrown, and make sure the destination doesn't redirect back." + }, + "reserved_query_parameter": { + "variants": [ + { + "text": "Cannot use reserved query parameter `%key%`", + "variables": [ + "key" + ] + } + ], + "details": "SvelteKit uses query parameters starting with `x-sveltekit-` internally when it requests the data for a page, so a URL can't contain them. Rename the parameter." + }, + "scroll_handling_outside_navigation": { + "variants": [ + { + "text": "Can only disable scroll handling during navigation", + "variables": [] + } + ], + "details": "[`disableScrollHandling`](https://svelte.dev/docs/kit/$app-navigation#disableScrollHandling) stops SvelteKit from updating the scroll position after the current navigation. It only has an effect while the page is being updated following a navigation — for example in `onMount`, [`afterNavigate`](https://svelte.dev/docs/kit/$app-navigation#afterNavigate) or an action — so calling it at any other time is a mistake." + }, + "preload_invalid_route_id": { + "variants": [ + { + "text": "`preloadCode` expects a route ID starting with `/` (such as `/blog/[slug]`), but received `%id%`", + "variables": [ + "id" + ] + } + ], + "details": "[`preloadCode`](https://svelte.dev/docs/kit/$app-navigation#preloadCode) takes the ID of a route, which is its path inside `src/routes` starting with `/`, such as `/blog/[slug]`. Route IDs are never prefixed with the app's [base path](https://svelte.dev/docs/kit/configuration#paths). If you have a URL or pathname, convert it with [`match`](https://svelte.dev/docs/kit/$app-paths#match) from `$app/paths`:\n\n```js\nimport { match } from '$app/paths';\nimport { preloadCode } from '$app/navigation';\n\nconst matched = await match('/blog/hello-world');\nif (matched) await preloadCode(matched.id);\n```" + }, + "preload_url_outside_app": { + "variants": [ + { + "text": "Attempted to preload a URL that does not belong to this app: `%url%`", + "variables": [ + "url" + ] + } + ], + "details": "[`preloadData`](https://svelte.dev/docs/kit/$app-navigation#preloadData) runs the `load` functions of one of your app's pages, so the URL must be on the app's origin (and inside its [base path](https://svelte.dev/docs/kit/configuration#paths)) and match a route with a `+page`. Check the URL for typos, and don't preload external URLs." + }, + "remote_command_redirect": { + "variants": [ + { + "text": "Redirects are not allowed in commands. Return a result instead and use `goto` on the client", + "variables": [] + } + ], + "details": "Remote commands are imperative mutations and cannot redirect. Return the destination or another result from the command, then call [`goto`](https://svelte.dev/docs/kit/$app-navigation#goto) in the browser after awaiting it. Remote forms support redirects when you need a progressively enhanced submission." + }, + "remote_form_multiple_elements": { + "variants": [ + { + "text": "A form object can only be attached to a single `` element", + "variables": [] + } + ], + "details": "Each remote form instance tracks one element's input, validation issues and submission state. To render several forms from the same remote function, use `myForm.for(key)` with a different stable key for each element. An instance returned by `.for(key)` must also be attached to only one element; reusing its key does not create a second independent instance. This includes keys such as `0`, `false` and `''`." + }, + "remote_form_mixed_inputs": { + "variants": [ + { + "text": "Cannot mix and match file and non-file inputs under the same name (`%name%`)", + "variables": [ + "name" + ] + } + ], + "details": "Inputs that share an array field must all contain the same kind of data. Use separate fields for file inputs and non-file inputs, and spread the corresponding `form.fields.field.as(...)` props onto each control." + }, + "remote_form_multiple_files": { + "variants": [ + { + "text": "Can only use the `multiple` attribute when `name` includes a `[]` suffix — consider changing `%name%` to `%name%[]`", + "variables": [ + "name" + ] + } + ], + "details": "Use `form.fields.files.as('file multiple')` rather than adding `multiple` to the props for a single file input. The array suffix tells SvelteKit to collect all selected files instead of a single file." + }, + "remote_form_not_attached": { + "variants": [ + { + "text": "Cannot call `submit()` before the form is attached", + "variables": [] + } + ], + "details": "Spread the remote form object onto a `` element before calling its `submit()` method. Call it from a browser event handler after the element has mounted, and do not submit an instance whose element has been removed." + }, + "remote_form_reserved_field": { + "variants": [ + { + "text": "`$` is used to collect all FormData validation issues and cannot be used as the `name` of a form control", + "variables": [] + } + ], + "details": "`form.fields.allIssues()` uses the reserved `$` path for the form's complete issue list. Choose a different field name, including for nested paths and arrays that start with `$`." + }, + "remote_updates_duplicate_override": { + "variants": [ + { + "text": "Multiple overrides for the same query are not allowed in a single `updates()` invocation", + "variables": [] + } + ], + "details": "Pass at most one `withOverride(...)` result for each query instance to `.updates(...)`. Combine changes to the same instance into a single override callback. Different arguments identify different query instances and may each have an override." + }, + "remote_updates_invalid_argument": { + "variants": [ + { + "text": "`updates()` expects a query or live query function, query resource, or query override", + "variables": [] + } + ], + "details": "Pass a query function to update all its active instances, a query resource to update one instance, or a `withOverride(...)` result to apply an optimistic update. A release callback from an integration is also accepted. Do not pass a command, form, prerender function or an arbitrary value." + }, + "snapshot_duplicate_id": { + "variants": [ + { + "text": "`snapshot()` was called multiple times from the same call site. Pass a unique `id` to distinguish the instances.", + "variables": [] + }, + { + "text": "A snapshot with id `%id%` is already registered. Pass a unique `id`.", + "variables": [ + "id" + ] + } + ], + "details": "Each [`snapshot`](https://svelte.dev/docs/kit/snapshots) needs an `id` that is unique among the snapshots mounted at the same time, so that SvelteKit knows which captured value to restore to which component. By default, the `id` is derived from the place in your code where `snapshot` is called, so calling it from a helper function, or from a component that is rendered more than once, produces the same `id` for every instance. Pass an explicit `id` that differs between them, for example one based on a prop." + }, + "snapshot_id_missing": { + "variants": [ + { + "text": "Could not generate a snapshot id from the stack trace. Pass an `id` instead.", + "variables": [] + } + ], + "details": "Without an `id` option, [`snapshot`](https://svelte.dev/docs/kit/snapshots) derives one from its call site, using the stack trace of an error. That doesn't work when the environment doesn't provide stack traces (or a library has removed them), so pass an explicit `id`, which also keeps snapshots stable across deployments." + }, + "enhance_invalidate_all_deprecated": { + "variants": [ + { + "text": "The `update({ invalidateAll })` option has been deprecated in favour of `update({ refreshAll })`", + "variables": [] + } + ], + "details": "The `invalidateAll` option of the `update` function passed to a [`use:enhance`](https://svelte.dev/docs/kit/form-actions#Progressive-enhancement-Customising-use:enhance) callback has been renamed to `refreshAll`, which also refreshes active remote functions. Replace `invalidateAll` with `refreshAll`." + }, + "link_option_invalid": { + "variants": [ + { + "text": "Unexpected value for `%name%` — should be one of %options%", + "variables": [ + "name", + "options" + ] + } + ], + "details": "The value of a [link option](https://svelte.dev/docs/kit/link-options) attribute such as `data-sveltekit-preload-data` isn't one that SvelteKit recognises, so it's ignored. The element is logged alongside this warning. Use one of the listed values, or `\"false\"` to [disable the option](https://svelte.dev/docs/kit/link-options#Disabling-options) for this element and its children." + }, + "link_option_replaced": { + "variants": [ + { + "text": "`data-sveltekit-%name%=\"true\"` has been replaced with `data-sveltekit-reset=\"false\"`", + "variables": [ + "name" + ] + } + ], + "details": "The `data-sveltekit-keepfocus` and `data-sveltekit-noscroll` [link options](https://svelte.dev/docs/kit/link-options) have been combined into [`data-sveltekit-reset`](https://svelte.dev/docs/kit/link-options#data-sveltekit-reset). Replace them with `data-sveltekit-reset=\"false\"`, which preserves both the scroll position and the focused element. The element is logged alongside this warning." + }, + "window_fetch_in_load": { + "variants": [ + { + "text": "Loading `%url%` using `window.fetch`. For best results, use the `fetch` that is passed to your `load` function", + "variables": [ + "url" + ] + } + ], + "details": "The [`fetch`](https://svelte.dev/docs/kit/load#Making-fetch-requests) passed to `load` functions reuses responses that were inlined into the page during server-side rendering, so hydration doesn't request the same data twice, and it tracks the URL as a dependency for [`invalidate`](https://svelte.dev/docs/kit/$app-navigation#invalidate). Take `fetch` from the `load` event instead of using the global one:\n\n```js\n/// file: src/routes/+page.js\n/** @type {import('./$types').PageLoad} */\nexport async function load({ fetch }) {\n\tconst response = await fetch('/api/items');\n\treturn { items: await response.json() };\n}\n```" + }, + "app_body_unwrapped": { + "variants": [ + { + "text": "Placing `%tag%` directly inside `` is not recommended, as your app may break for users who have certain browser extensions installed", + "variables": [ + "tag" + ] + } + ], + "details": "Browser extensions often inject elements into ``. When your app is rendered directly into ``, hydration can remove those elements or be confused by them, which breaks the extension or your app. Wrap the placeholder in an element in your [app template](https://svelte.dev/docs/kit/project-structure#Project-files-src):\n\n```html\n\n\t
%sveltekit.body%
\n\n```" + }, + "full_reload_after_error": { + "variants": [ + { + "text": "An error occurred while loading the page. This will cause a full page reload", + "variables": [] + } + ], + "details": "When a client-side navigation fails in a way SvelteKit can't render an error page for (for example, loading the route's code failed), it falls back to a full-page navigation so that the server can respond instead. This warning only appears during development, and execution pauses at a `debugger` statement if your devtools are open so that you can inspect the error before the page reloads. Check the console and network tab for the underlying failure." + }, + "goto_invalidate_all_deprecated": { + "variants": [ + { + "text": "The `goto(..., { invalidateAll: %value% })` option has been deprecated in favour of `refreshAll`", + "variables": [ + "value" + ] + } + ], + "details": "The `invalidateAll` option of [`goto`](https://svelte.dev/docs/kit/$app-navigation#goto) has been renamed to `refreshAll`, which also refreshes active remote functions. Replace `invalidateAll` with `refreshAll`." + }, + "goto_replace_state_deprecated": { + "variants": [ + { + "text": "The `goto(..., { replaceState: %value% })` option has been deprecated in favour of `replace`", + "variables": [ + "value" + ] + } + ], + "details": "The `replaceState` option of [`goto`](https://svelte.dev/docs/kit/$app-navigation#goto) has been renamed to `replace`. Replace `replaceState` with `replace`." + }, + "history_api_conflict": { + "variants": [ + { + "text": "Avoid using `history.pushState(...)` and `history.replaceState(...)` as these will conflict with SvelteKit's router. Use `goto(...)` from `$app/navigation` instead.", + "variables": [] + } + ], + "details": "SvelteKit's router stores its own state in each history entry, which it uses to restore scroll positions, snapshots and [`page.state`](https://svelte.dev/docs/kit/$app-state#page) when navigating back and forward. Calling the browser's history methods directly creates entries without that state. Use [`goto`](https://svelte.dev/docs/kit/$app-navigation#goto) to navigate, or `goto(url, { state, shallow: true })` for [shallow routing](https://svelte.dev/docs/kit/shallow-routing)." + }, + "hmr_reload_after_error": { + "variants": [ + { + "text": "The next HMR update will cause the page to reload", + "variables": [] + } + ], + "details": "An unexpected error was handled during development. Because the app may be in an inconsistent state after such an error, SvelteKit reloads the page on the next hot module replacement update rather than patching it in place." + }, + "push_state_deprecated": { + "variants": [ + { + "text": "`pushState(...)` is deprecated. Use `goto(url, { state, shallow: true })` instead.", + "variables": [] + } + ], + "details": "[Shallow routing](https://svelte.dev/docs/kit/shallow-routing) is now done with [`goto`](https://svelte.dev/docs/kit/$app-navigation#goto), which accepts the same `state`:\n\n```js\nimport { goto } from '$app/navigation';\n\n// instead of pushState('/photos/1', { selected: 1 })\nawait goto('/photos/1', { state: { selected: 1 }, shallow: true });\n```" + }, + "replace_state_deprecated": { + "variants": [ + { + "text": "`replaceState(...)` is deprecated. Use `goto(url, { state, shallow: true, replace: true })` instead.", + "variables": [] + } + ], + "details": "[Shallow routing](https://svelte.dev/docs/kit/shallow-routing) is now done with [`goto`](https://svelte.dev/docs/kit/$app-navigation#goto), which accepts the same `state`:\n\n```js\nimport { goto } from '$app/navigation';\n\n// instead of replaceState('/photos/1', { selected: 1 })\nawait goto('/photos/1', { state: { selected: 1 }, shallow: true, replace: true });\n```" + }, + "snapshot_export_deprecated": { + "variants": [ + { + "text": "`export const snapshot` is deprecated. Use the `snapshot` helper from `$app/navigation` instead.", + "variables": [] + } + ], + "details": "Rather than exporting a `snapshot` object from a page or layout, call the [`snapshot`](https://svelte.dev/docs/kit/snapshots) function from `$app/navigation` during component initialization. It works in any component, not just pages and layouts:\n\n```svelte\n\n```" + }, + "preload_code_endpoint_only": { + "variants": [ + { + "text": "`%id%` has no `+page`, so there is no code to preload. If you meant to warm up an endpoint, request it with `fetch` instead.", + "variables": [ + "id" + ] + } + ], + "details": "[`preloadCode`](https://svelte.dev/docs/kit/$app-navigation#preloadCode) imports the components and universal `load` modules of a page. The route exists, but it only has a `+server.js`, so there's nothing to preload." + }, + "preload_data_failed": { + "variants": [ + { + "text": "Preloading data for `%path%` failed with the following error: %message%", + "variables": [ + "path", + "message" + ] + } + ], + "details": "A link with [`data-sveltekit-preload-data`](https://svelte.dev/docs/kit/link-options#data-sveltekit-preload-data) caused SvelteKit to run the `load` functions of its page ahead of time, and one of them failed. If the error is transient, you can ignore it — the page's data is loaded again when the user navigates. Otherwise, fix the `load` function, or disable preloading for this link with `data-sveltekit-preload-data=\"false\"`." + }, + "preload_route_is_pathname": { + "variants": [ + { + "text": "`%id%` did not match any route, but it does match as a pathname — use `match(...)` from `$app/paths` to convert a pathname into a route ID", + "variables": [ + "id" + ] + } + ], + "details": "[`preloadCode`](https://svelte.dev/docs/kit/$app-navigation#preloadCode) takes a route ID such as `/blog/[slug]`, not a pathname such as `/blog/hello-world` (which it accepted in earlier versions). Convert the pathname with [`match`](https://svelte.dev/docs/kit/$app-paths#match):\n\n```js\nimport { match } from '$app/paths';\nimport { preloadCode } from '$app/navigation';\n\nconst matched = await match('/blog/hello-world');\nif (matched) await preloadCode(matched.id);\n```" + }, + "preload_route_missing": { + "variants": [ + { + "text": "`%id%` did not match any route", + "variables": [ + "id" + ] + } + ], + "details": "[`preloadCode`](https://svelte.dev/docs/kit/$app-navigation#preloadCode) takes the ID of a route, which is its path inside `src/routes`, such as `/blog/[slug]`. Check the ID for typos. Routes without a `+page` have no code to preload, and with client-side route resolution they aren't known to the browser at all, so they also cause this warning." + }, + "remote_form_issues_ignored": { + "variants": [ + { + "text": "Form submission had invalid data, but the validation issues were ignored:\n\n%issues%\n\nMake sure you provide actionable feedback to users, using e.g. `myForm.fields.myField.issues()` or `myForm.fields.allIssues()`", + "variables": [ + "issues" + ] + } + ], + "details": "A remote form submission or preflight validation failed, but your UI did not read all its issues. Display field-specific issues with `form.fields.field.issues()`, or display all issues with `form.fields.allIssues()`, so the user knows what to fix. The list contains the validator's own issue messages and paths; it does not change those issues. This warning is only emitted in development, after giving the UI time to read them." + }, + "remote_updates_repeated": { + "variants": [ + { + "text": "Updates can only be sent once per %invocation%. Ignoring additional updates.", + "variables": [ + "invocation" + ] + } + ], + "details": "Call `.updates(...)` once on a command invocation or form submission, passing every query or override in that call. Subsequent calls are ignored and return the original promise; they do not send more updates. Calling `.updates()` without arguments is still a call and opts out of automatic invalidation." + }, + "handle_error_hook_failed": { + "variants": [ + { + "text": "The `handleError` hook failed", + "variables": [] + } + ], + "details": "Your [`handleError`](https://svelte.dev/docs/kit/hooks#Shared-hooks-handleError) hook threw an error (or returned a promise that rejected) while it was handling another error. The `cause` of this error is what the hook threw, and the error it was handling is logged after it. SvelteKit responds with a generic `Internal Error` instead of the hook's result. Make sure the hook can handle any value, since anything can be thrown — for example, check `error instanceof Error` before reading `error.message`, and catch failures of any service that it reports errors to." + }, + "action_throw_fail": { + "variants": [ + { + "text": "Cannot `throw fail()`. Use `return fail()`", + "variables": [] + } + ], + "details": "[`fail`](https://svelte.dev/docs/kit/@sveltejs-kit#fail) creates a value that a form action returns to report a [validation error](https://svelte.dev/docs/kit/form-actions#Anatomy-of-an-action-Validation-errors). Thrown values are treated as unexpected errors instead, so return it:\n\n```js\n/// file: src/routes/login/+page.server.js\nimport { fail } from '@sveltejs/kit';\n\nexport const actions = {\n\tdefault: async ({ request }) => {\n\t\tconst data = await request.formData();\n\n\t\tif (!data.get('email')) {\n\t\t\treturn fail(400, { missing: true });\n\t\t}\n\t}\n};\n```" + }, + "action_default_with_named": { + "variants": [ + { + "text": "When using named actions, the default action cannot be used. See the docs for more info: https://svelte.dev/docs/kit/form-actions#named-actions", + "variables": [] + } + ], + "details": "A page can have either a single `default` [action](https://svelte.dev/docs/kit/form-actions#Default-actions) or any number of [named actions](https://svelte.dev/docs/kit/form-actions#Named-actions), but not both — a form that posts to the page without choosing an action would be ambiguous. Rename the `default` action and point the form that uses it to that name, for example with `action=\"?/create\"`." + }, + "action_name_reserved": { + "variants": [ + { + "text": "Cannot use reserved action name `default`", + "variables": [] + } + ], + "details": "The request's URL chose the action named `default` with `?/default`. That name is reserved for the page's [default action](https://svelte.dev/docs/kit/form-actions#Default-actions), which is used by forms that don't choose an action. Remove the `?/default` from the form's `action` attribute (or the `formaction` of its button)." + }, + "action_return_redirect": { + "variants": [ + { + "text": "Cannot `return redirect(...)` — use `redirect(...)` instead", + "variables": [] + } + ], + "details": "[`redirect`](https://svelte.dev/docs/kit/@sveltejs-kit#redirect) throws, so its result can't be returned. Call it without `return` — for example `redirect(303, '/login')`." + }, + "action_return_error": { + "variants": [ + { + "text": "Cannot `return error(...)` — use `error(...)` or `return fail(...)` instead", + "variables": [] + } + ], + "details": "[`error`](https://svelte.dev/docs/kit/@sveltejs-kit#error) throws, so its result can't be returned. Call it without `return` to show the error page, or return [`fail`](https://svelte.dev/docs/kit/@sveltejs-kit#fail) to report a [validation error](https://svelte.dev/docs/kit/form-actions#Anatomy-of-an-action-Validation-errors) back to the form instead." + }, + "action_response_not_serializable": { + "variants": [ + { + "text": "Data returned from action inside `%id%` is not serializable. Form actions need to return plain objects or `fail()`. E.g. `return { success: true }` or `return fail(400, { message: \"invalid\" });`", + "variables": [ + "id" + ] + } + ], + "details": "Form actions return data to the page as [`form`](https://svelte.dev/docs/kit/form-actions#Anatomy-of-an-action), not as a response of their own, so they can't return a `Response` (for example one created with `json(...)`). Return a plain object instead. If you need a custom response, use a [`+server`](https://svelte.dev/docs/kit/routing#server) file." + }, + "action_data_not_serializable": { + "variants": [ + { + "text": "Data returned from action inside `%id%` is not serializable: %message%", + "variables": [ + "id", + "message" + ] + }, + { + "text": "Data returned from action inside `%id%` is not serializable: %message% (`%path%`)", + "variables": [ + "id", + "message", + "path" + ] + } + ], + "details": "The data returned from a form action is serialized with [devalue](https://github.com/sveltejs/devalue) to send it to the page, and this value isn't supported. Devalue supports JSON values as well as things like `Date`, `Map`, `Set`, `BigInt` and `URL`, but not functions or class instances. Return plain data instead, or register the type with a [transport hook](https://svelte.dev/docs/kit/hooks#Universal-hooks-transport) to tell SvelteKit how to serialize it." + }, + "load_not_serializable": { + "variants": [ + { + "text": "Data returned from `load` while rendering `%id%` is not serializable: %message% (`%path%`). If you need to serialize/deserialize custom types, use transport hooks: https://svelte.dev/docs/kit/hooks#transport.", + "variables": [ + "id", + "message", + "path" + ] + } + ], + "details": "The data returned from a server `load` function is serialized with [devalue](https://github.com/sveltejs/devalue) to send it to the browser, and the value at the listed path isn't supported. Devalue supports JSON values as well as things like `Date`, `Map`, `Set`, `BigInt`, `URL` and promises, but not functions or class instances. Return plain data instead, move the value into a [universal `load`](https://svelte.dev/docs/kit/load#Universal-vs-server) function, or register the type with a [transport hook](https://svelte.dev/docs/kit/hooks#Universal-hooks-transport)." + }, + "load_not_plain_object": { + "variants": [ + { + "text": "Data returned from `load` while rendering `%id%` is not a plain object", + "variables": [ + "id" + ] + } + ], + "details": "A server `load` function must return a plain object (such as `return { post }`), because each of its properties becomes a property of `data`. Wrap the value you want to return in an object." + }, + "load_promise_not_serializable": { + "variants": [ + { + "text": "Failed to serialize promise while rendering `%id%`", + "variables": [ + "id" + ] + } + ], + "details": "A server `load` function returned a promise, which SvelteKit [streams](https://svelte.dev/docs/kit/load#Streaming-with-promises) to the browser once it resolves. The value it resolved to can't be serialized with [devalue](https://github.com/sveltejs/devalue), so the promise is rejected in the browser instead — the `cause` of this error says which value was the problem. Resolve the promise to plain data, or register the type with a [transport hook](https://svelte.dev/docs/kit/hooks#Universal-hooks-transport)." + }, + "load_response_header_not_serialized": { + "variants": [ + { + "text": "Failed to get response header `%name%` — it must be included by the `filterSerializedResponseHeaders` option: https://svelte.dev/docs/kit/hooks#handle (at `%id%`)", + "variables": [ + "name", + "id" + ] + } + ], + "details": "A response fetched with `event.fetch` during server-side rendering is [serialized into the page](https://svelte.dev/docs/kit/load#Making-fetch-requests) so that `load` can use it again in the browser without making the request twice. Only headers that you choose are included, because they may contain sensitive information, so reading any other header would fail in the browser. Include the header by returning `true` for it from the [`filterSerializedResponseHeaders`](https://svelte.dev/docs/kit/hooks#Server-hooks-handle) option of `resolve`:\n\n```js\n/// file: src/hooks.server.js\nexport async function handle({ event, resolve }) {\n\treturn resolve(event, {\n\t\tfilterSerializedResponseHeaders: (name) => name === 'content-type'\n\t});\n}\n```" + }, + "load_fetch_cors": { + "variants": [ + { + "text": "CORS error: %reason% `Access-Control-Allow-Origin` header is present on the requested resource", + "variables": [ + "reason" + ] + } + ], + "details": "Universal `load` functions also run in the browser, where a request to another origin only succeeds if that origin allows it with [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS) headers. SvelteKit applies the same check when `load` runs on the server, so that the page doesn't work during server-side rendering and then fail after navigation. Allow your app's origin on the other server with an `access-control-allow-origin` header, or make the request from a [server `load`](https://svelte.dev/docs/kit/load#Universal-vs-server) function or an endpoint instead, where CORS doesn't apply." + }, + "ssr_fetch_relative_url": { + "variants": [ + { + "text": "Cannot call `fetch` eagerly during server-side rendering with relative URL (`%url%`) — put your `fetch` calls inside `onMount` or a `load` function instead", + "variables": [ + "url" + ] + } + ], + "details": "During server-side rendering there's no page URL that a relative URL could be resolved against. Fetch the data in a [`load`](https://svelte.dev/docs/kit/load) function, whose `fetch` understands relative URLs and passes the data to the page, or call `fetch` in `onMount` so that it only runs in the browser." + }, + "prerender_actions": { + "variants": [ + { + "text": "Cannot prerender pages with actions", + "variables": [] + } + ], + "details": "A [prerendered](https://svelte.dev/docs/kit/page-options#prerender) page is saved as a static file, so there's no server to handle its [form actions](https://svelte.dev/docs/kit/form-actions). Remove `export const prerender = true` (which may also be set by a layout) from the page, or move the actions to a page that isn't prerendered." + }, + "prerender_endpoint_methods": { + "variants": [ + { + "text": "Cannot prerender a `+server` file with %methods% or fallback handlers (`%id%`)", + "variables": [ + "methods", + "id" + ] + } + ], + "details": "A prerendered endpoint is saved as a static file, which can only answer `GET` requests. Methods that depend on a request body, and `fallback` handlers, would stop working. Move these handlers to a route that isn't prerendered, or remove `export const prerender = true` from this one." + }, + "prerender_endpoint_not_prerenderable": { + "variants": [ + { + "text": "`%id%` is not prerenderable", + "variables": [ + "id" + ] + } + ], + "details": "A prerendered page requested this `+server` route with `fetch`, but the route isn't prerendered. The prerendered page would contain its response, while the route itself wouldn't exist in the built app. Add `export const prerender = true` to the `+server` file, or stop requesting it while the page is prerendered." + }, + "prerender_nonce": { + "variants": [ + { + "text": "Cannot use prerendering if `config.csp.mode === 'nonce'`", + "variables": [] + } + ], + "details": "A nonce must be different for every response, but a [prerendered](https://svelte.dev/docs/kit/page-options#prerender) page is generated once and then served to everyone. Set [`csp.mode`](https://svelte.dev/docs/kit/configuration#csp) to `'auto'` or `'hash'` instead, which uses nonces only for pages that are rendered dynamically." + }, + "prerender_template_nonce": { + "variants": [ + { + "text": "Cannot use prerendering if page template contains `%tag%`", + "variables": [ + "tag" + ] + } + ], + "details": "A nonce must be different for every response, but a [prerendered](https://svelte.dev/docs/kit/page-options#prerender) page is generated once and then served to everyone. Remove the nonce placeholder from `src/app.html`, or don't prerender this page. SvelteKit adds the nonce to the scripts and styles it generates itself, and uses hashes instead on prerendered pages." + }, + "csp_report_only_missing_report": { + "variants": [ + { + "text": "`content-security-policy-report-only` must be specified with either the `report-to` or `report-uri` directives, or both", + "variables": [] + } + ], + "details": "A [report-only content security policy](https://svelte.dev/docs/kit/configuration#csp) doesn't block anything — it only reports violations. Without a `report-to` or `report-uri` directive, there's nowhere to send those reports. Add one of them to `csp.reportOnly`, or remove `csp.reportOnly`." + }, + "remote_invalid_validator": { + "variants": [ + { + "text": "Invalid validator passed to remote function. Expected `'unchecked'` or a Standard Schema (https://standardschema.dev)", + "variables": [] + } + ], + "details": "Pass a [Standard Schema](https://standardschema.dev) validator as the first argument to a [remote function](https://svelte.dev/docs/kit/remote-functions), followed by its callback. Libraries such as Valibot, Zod and ArkType implement this interface. Use `'unchecked'` only when you intentionally handle validation yourself. A function without an input argument can be declared with just its callback." + }, + "remote_headers_forbidden": { + "variants": [ + { + "text": "`setHeaders` is not allowed in remote functions", + "variables": [] + } + ], + "details": "Remote functions can run inside another request or share a response with other queries, so they cannot set response headers. Set headers in a server `load` function, endpoint or `handle` hook instead." + }, + "remote_cookie_forbidden": { + "variants": [ + { + "text": "Cannot %operation% cookies in `query` or `prerender` functions", + "variables": [ + "operation" + ] + } + ], + "details": "Queries and prerender functions read data rather than mutate it. Set or delete cookies inside a remote `command` or `form` handler instead, or in an endpoint or form action. Reading cookies is allowed." + }, + "remote_cookie_path_relative": { + "variants": [ + { + "text": "Cookies %operation% in remote functions must have an absolute path", + "variables": [ + "operation" + ] + } + ], + "details": "Remote functions can be invoked from different pages, so a relative cookie path would depend on which page invoked them. Pass an absolute `path`, such as `'/'`, to `cookies.set` or `cookies.delete`. Use the same path when deleting a cookie as when setting it." + }, + "remote_request_property": { + "variants": [ + { + "text": "Cannot access `event.%property%` in a query. Pass the value as an argument to the query instead", + "variables": [ + "property" + ] + } + ], + "details": "A query's cache key is determined by its argument, not by the page that calls it. Read `url`, `params` or `route` in the caller and pass the required value as an argument so that different values produce different cache entries. This also applies to queries nested inside live queries." + }, + "remote_query_live_not_iterable": { + "variants": [ + { + "text": "`query.live` `%name%` must return an `Iterator`, `Iterable`, `AsyncIterator` or `AsyncIterable`", + "variables": [ + "name" + ] + } + ], + "details": "A [`query.live`](https://svelte.dev/docs/kit/remote-functions#query.live) callback must provide a sequence of values. Use an async generator (`async function*`) and `yield` values, or return an iterator or iterable. Returning a single object or a promise of a single value is not sufficient; use a regular `query` for that." + }, + "remote_query_live_no_value": { + "variants": [ + { + "text": "`query.live` `%name%` did not yield a value", + "variables": [ + "name" + ] + } + ], + "details": "When a live query is awaited on the server, it must yield at least one value before completing. Check for branches that return without yielding. Yield an initial value (including `null` if appropriate) before waiting for updates." + }, + "remote_query_prerender": { + "variants": [ + { + "text": "Cannot call `%type%` `%name%` while prerendering, as prerendered pages need static data. Use `prerender` from `$app/server` instead", + "variables": [ + "type", + "name" + ] + } + ], + "details": "Regular, batched and live queries fetch runtime data, while a prerendered page must be generated at build time. Use [`prerender`](https://svelte.dev/docs/kit/remote-functions#prerender) for static data, or disable prerendering for pages that need these queries." + }, + "remote_command_readonly": { + "variants": [ + { + "text": "Cannot call a command (`%name%`) inside a query or prerender function", + "variables": [ + "name" + ] + } + ], + "details": "Commands mutate data, so they cannot run inside a query or prerender function. Invoke the command from a browser event handler, a remote form handler, a form action or an endpoint that handles a mutative HTTP method." + }, + "remote_command_method": { + "variants": [ + { + "text": "Cannot call a command (`%name%`) from a `%method%` handler", + "variables": [ + "name", + "method" + ] + } + ], + "details": "Commands mutate data and cannot be invoked by read-only HTTP requests. Use a handler for `POST`, `PUT`, `PATCH` or `DELETE`, or invoke the command from a browser event handler. Do not change state in `load` functions." + }, + "remote_command_render": { + "variants": [ + { + "text": "Cannot call a command (`%name%`) during server-side rendering", + "variables": [ + "name" + ] + } + ], + "details": "Rendering must not mutate application state. Call the command in response to a user interaction, such as a button click, or use a remote form for progressively enhanced submissions." + }, + "remote_form_fail": { + "variants": [ + { + "text": "`fail(...)` is for form actions. A remote `form` handler should call `invalid(...)` instead. See https://svelte.dev/docs/kit/remote-functions#form-Programmatic-validation", + "variables": [] + } + ], + "details": "[`fail`](https://svelte.dev/docs/kit/@sveltejs-kit#fail) returns an action failure for a page's form actions. Remote forms use [`invalid`](https://svelte.dev/docs/kit/@sveltejs-kit#invalid) to report validation issues. Call `invalid(issue.field('Message'))` with the issue builder supplied to the form callback, or pass issue strings for errors that apply to the whole form. Do not return or throw the result of `fail` from a remote form handler." + }, + "remote_module_default_export": { + "variants": [ + { + "text": "Cannot export `default` from a remote module (`%file%`) — please use named exports instead", + "variables": [ + "file" + ] + } + ], + "details": "Each remote function needs a named export so SvelteKit can identify it in requests and generate its client implementation. Replace the default export with a named export created by `query`, `query.batch`, `query.live`, `command`, `form` or `prerender` from `$app/server`." + }, + "remote_module_invalid_export": { + "variants": [ + { + "text": "`%name%` exported from `%file%` is invalid — all exports from this file must be remote functions", + "variables": [ + "name", + "file" + ] + } + ], + "details": "A `.remote.js` or `.remote.ts` module can only export remote functions created with `$app/server`. Keep helper values private, or move values that other modules need into a separate module. Type-only exports are allowed in TypeScript because they are erased at runtime." + }, + "remote_requested_invalid_query": { + "variants": [ + { + "text": "`requested(...)` expects a query function created with `query(...)`, `query.batch(...)`, or `query.live(...)`", + "variables": [] + } + ], + "details": "Pass the query function itself to [`requested`](https://svelte.dev/docs/kit/remote-functions#Single-flight-mutations-Client-requested-refreshes), not a query resource returned by calling it. Commands, forms and prerender functions are not queries." + }, + "remote_requested_context": { + "variants": [ + { + "text": "`requested(...)` can only be called in the context of a command/form remote function", + "variables": [] + } + ], + "details": "`requested` reads the queries that a client asked a mutation to refresh or reconnect. Use it inside a remote `command` or `form` callback, where that request information exists, rather than in a `load` function, endpoint or query." + }, + "remote_requested_async_validator": { + "variants": [ + { + "text": "`requested(%name%, %limit%)` cannot be used with synchronous iteration because the query validator is async. Use `for await ... of` instead", + "variables": [ + "name", + "limit" + ] + } + ], + "details": "Standard Schema validation can be asynchronous. Iterate the result of `requested` with `for await (const { arg, query } of requested(...))` so validation completes before you use each entry. `refreshAll`, `reconnectAll` and `ignoreAll` already use asynchronous iteration." + }, + "remote_requested_wrong_method": { + "variants": [ + { + "text": "`%method%()` is invalid for %type% queries. Use `%replacement%()` instead.", + "variables": [ + "method", + "type", + "replacement" + ] + } + ], + "details": "Use `refreshAll()` for regular and batched queries, and `reconnectAll()` for live queries. A live query needs to restart its stream rather than perform a one-off fetch. Call the appropriate method on the result of `requested`." + }, + "remote_requested_invalid_limit": { + "variants": [ + { + "text": "Limit must be a non-negative integer or `Infinity`", + "variables": [] + } + ], + "details": "The second argument to `requested` limits how many client-requested query instances the mutation will handle. Pass a non-negative integer, or `Infinity` to handle all instances. Negative numbers, fractions and `NaN` are not valid limits." + }, + "request_event_unavailable": { + "variants": [ + { + "text": "Can only read the current request event inside functions invoked during `handle`, such as server `load` functions, actions, endpoints, and other server hooks.", + "variables": [] + } + ], + "details": "[`getRequestEvent`](https://svelte.dev/docs/kit/$app-server#getRequestEvent) returns the event of the request that SvelteKit is currently handling, so it only works while that request is being handled — in `handle`, server `load` functions, form actions, `+server` handlers, remote functions and the functions they call. It can't be used at the top level of a module, in `init`, or in code that runs after the response has been sent, such as a `setTimeout` callback. Pass the values you need from the event to that code instead." + }, + "request_event_after_await": { + "variants": [ + { + "text": "Can only read the current request event inside functions invoked during `handle`, such as server `load` functions, actions, endpoints, and other server hooks. In environments without `AsyncLocalStorage`, the event must be read synchronously, not after an `await`.", + "variables": [] + } + ], + "details": "[`getRequestEvent`](https://svelte.dev/docs/kit/$app-server#getRequestEvent) uses [`AsyncLocalStorage`](https://nodejs.org/api/async_context.html#class-asynclocalstorage) to find the current request after an `await`. This environment doesn't provide it, so the event can only be read synchronously, before the first `await` of the function that SvelteKit called:\n\n```js\nimport { getRequestEvent } from '$app/server';\n\nexport async function load() {\n\t// read the event first...\n\tconst event = getRequestEvent();\n\n\t// ...and then await\n\tconst user = await get_user(event.cookies.get('session'));\n\n\treturn { user };\n}\n```\n\nOtherwise, the function was called outside of `handle`, server `load` functions, actions or endpoints, where there's no current request." + }, + "request_store_after_await": { + "variants": [ + { + "text": "Could not get the request store. In environments without `AsyncLocalStorage`, the request store (used by e.g. remote functions) must be accessed synchronously, not after an `await`. If it was accessed synchronously then this is an internal error.", + "variables": [] + } + ], + "details": "Some SvelteKit APIs, such as [remote functions](https://svelte.dev/docs/kit/remote-functions), need to know which request they are part of. They use [`AsyncLocalStorage`](https://nodejs.org/api/async_context.html#class-asynclocalstorage) to find it after an `await`, but this environment doesn't provide it. Call these APIs synchronously, before the first `await` of the function that SvelteKit called. If that's already the case, please [open an issue](https://github.com/sveltejs/kit/issues)." + }, + "client_address_unsupported": { + "variants": [ + { + "text": "`%adapter%` does not specify `getClientAddress`. Please raise an issue", + "variables": [ + "adapter" + ] + } + ], + "details": "[`event.getClientAddress()`](https://svelte.dev/docs/kit/@sveltejs-kit#RequestEvent) returns the address provided by your [adapter](https://svelte.dev/docs/kit/adapters), and this adapter doesn't provide one. Raise an issue with the adapter's maintainers. In the meantime, if your app is behind a proxy that forwards the client's address, you can read it from a header such as `x-forwarded-for` with `event.request.headers.get(...)`." + }, + "set_headers_cookie": { + "variants": [ + { + "text": "Use `event.cookies.set(name, value, options)` instead of `event.setHeaders` to set cookies", + "variables": [] + } + ], + "details": "`setHeaders` can't set `set-cookie`, because a response can contain several `set-cookie` headers and SvelteKit needs to know about cookies to apply them correctly — for example, to [forward them to `fetch` requests](https://svelte.dev/docs/kit/load#Cookies) and to read them back with `cookies.get` during the same request. Use [`cookies.set`](https://svelte.dev/docs/kit/@sveltejs-kit#Cookies) instead." + }, + "header_already_set": { + "variants": [ + { + "text": "`%name%` header is already set", + "variables": [ + "name" + ] + } + ], + "details": "[`setHeaders`](https://svelte.dev/docs/kit/load#Headers) can only set each header once per request (except `server-timing`, whose values are combined), because otherwise it would be unclear which value should win. This often happens when both a layout and a page `load` function set the same header, such as `cache-control`. Set the header in one place only. To change a header of the finished response, use `response.headers.set(...)` in the [`handle`](https://svelte.dev/docs/kit/hooks#Server-hooks-handle) hook instead." + }, + "cookies_set_after_response": { + "variants": [ + { + "text": "Cannot use `cookies.set(...)` after the response has been generated", + "variables": [] + } + ], + "details": "Cookies are sent in the headers of the response, so they can't be changed once the response has been created. This usually happens when `cookies.set` is called in code that runs later, such as a promise that is [streamed](https://svelte.dev/docs/kit/load#Streaming-with-promises) from a `load` function, or a callback that runs after `handle` has returned. Set cookies before your `load` function, action or handler returns." + }, + "set_headers_after_response": { + "variants": [ + { + "text": "Cannot use `setHeaders(...)` after the response has been generated", + "variables": [] + } + ], + "details": "Headers are sent before the body of a response, so they can't be changed once the response has been created. This usually happens when `setHeaders` is called in code that runs later, such as a promise that is [streamed](https://svelte.dev/docs/kit/load#Streaming-with-promises) from a `load` function, or a callback that runs after `handle` has returned. Set headers before your `load` function or action returns." + }, + "cookies_serialize_before_route": { + "variants": [ + { + "text": "Cannot serialize cookies until after the route is determined", + "variables": [] + } + ], + "details": "[`cookies.serialize`](https://svelte.dev/docs/kit/@sveltejs-kit#Cookies) resolves the cookie's `path` against the current route, whose trailing slash depends on the [`trailingSlash`](https://svelte.dev/docs/kit/page-options#trailingSlash) option. The route isn't known until SvelteKit has matched the request, so calling `serialize` in `handle` before `resolve` fails for cookies on the current hostname. Call it after `resolve`, or in `load` functions, actions and endpoints. If you only need to set the cookie, use `cookies.set` instead, which waits for the route to be determined." + }, + "cookie_too_large": { + "variants": [ + { + "text": "Cookie `%name%` is too large, and will be discarded by the browser", + "variables": [ + "name" + ] + } + ], + "details": "Browsers ignore cookies whose name and value are larger than 4096 bytes, so this cookie would silently never be sent back. Store less data in the cookie — for example, a session ID that refers to data stored on the server. This is only checked during development." + }, + "fetch_response_decoded": { + "variants": [ + { + "text": "Cannot return `fetch(...)` directly from a handler if the response has a `Content-Encoding: %encoding%` header. The body has already been decoded", + "variables": [ + "encoding" + ] + } + ], + "details": "When you `fetch` a compressed response, the body you read from it has already been decompressed, but its headers still say that it's compressed. Returning that response as-is would make the browser try to decompress it again. Create a new response without the `content-encoding` (and `content-length`) headers instead:\n\n```js\n/// file: src/routes/proxy/+server.js\nexport async function GET({ fetch }) {\n\tconst response = await fetch('https://example.com/data');\n\n\tconst headers = new Headers(response.headers);\n\theaders.delete('content-encoding');\n\theaders.delete('content-length');\n\n\treturn new Response(response.body, { status: response.status, headers });\n}\n```" + }, + "endpoint_invalid_response": { + "variants": [ + { + "text": "Invalid response from route `%path%`: handler should return a `Response` object", + "variables": [ + "path" + ] + } + ], + "details": "Request handlers in [`+server`](https://svelte.dev/docs/kit/routing#server) files must return a [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response). Check that every code path returns one — a missing `return` is a common cause. You can use the [`json`](https://svelte.dev/docs/kit/@sveltejs-kit#json) and [`text`](https://svelte.dev/docs/kit/@sveltejs-kit#text) helpers from `@sveltejs/kit` to create responses." + }, + "server_api_unavailable": { + "variants": [ + { + "text": "Cannot call `%name%` on the server", + "variables": [ + "name" + ] + } + ], + "details": "This function only works in the browser, because it depends on the client-side router or on browser state. Don't call it while rendering on the server — for example at the top level of a component's ` @@ -691,9 +689,9 @@ To summarize, a `load` function will rerun in the following situations: - It references a property of `url` (such as `url.pathname` or `url.search`) whose value has changed. Properties in `request.url` are _not_ tracked - It calls `url.searchParams.get(...)`, `url.searchParams.getAll(...)` or `url.searchParams.has(...)` and the parameter in question changes. Accessing other properties of `url.searchParams` will have the same effect as accessing `url.search`. - It calls `await parent()` and a parent `load` function reran -- A child `load` function calls `await parent()` and is rerunning, and the parent is a server load function +- A child server `load` function calls `await parent()` and is rerunning, and the parent is also a server `load` function. A universal `load` function that calls `await parent()` reuses the parent's existing data instead - It declared a dependency on a specific URL via [`fetch`](#Making-fetch-requests) (universal load only) or [`depends`](@sveltejs-kit#LoadEvent), and that URL was marked invalid with [`invalidate(url)`]($app-navigation#invalidate) -- All active `load` functions were forcibly rerun with [`invalidateAll()`]($app-navigation#invalidateAll) +- All active `load` functions were forcibly rerun with [`refreshAll()`]($app-navigation#refreshAll) `params` and `url` can change in response to a `` link click, a [`` interaction](form-actions#GET-vs-POST), a [`goto`]($app-navigation#goto) invocation, or a [`redirect`](@sveltejs-kit#redirect). @@ -758,7 +756,7 @@ Now, you can call `requireLogin` in any `load` function (or [form action](form-a /// file: +page.server.js // @filename: ambient.d.ts -declare module '$lib/server/auth' { +declare module '#lib/server/auth.js' { interface User { name: string; } @@ -768,7 +766,7 @@ declare module '$lib/server/auth' { // @filename: index.ts // ---cut--- -import { requireLogin } from '$lib/server/auth'; +import { requireLogin } from '#lib/server/auth.js'; export function load() { const user = requireLogin(); diff --git a/documentation/docs/20-core-concepts/30-form-actions.md b/documentation/docs/20-core-concepts/30-form-actions.md index a8fad4043e41..3fe54dbbb5fe 100644 --- a/documentation/docs/20-core-concepts/30-form-actions.md +++ b/documentation/docs/20-core-concepts/30-form-actions.md @@ -52,6 +52,8 @@ We can also invoke the action from other pages (for example if there's a login w ``` +Submitting this form takes you to `/login`, whether the action succeeds or fails — that's what the browser does, and [`use:enhance`](#Progressive-enhancement-use:enhance) emulates it. If you'd rather stay where you are and handle the result in place, there's a recipe for that in the [progressive enhancement](#Progressive-enhancement-use:enhance) section. + ## Named actions Instead of one `default` action, a page can have as many named actions as it needs: @@ -109,11 +111,11 @@ Each action receives a `RequestEvent` object, allowing you to read the data with ```js /// file: src/routes/login/+page.server.js // @filename: ambient.d.ts -declare module '$lib/server/db'; +declare module '#lib/server/db.js'; // @filename: index.js // ---cut--- -import * as db from '$lib/server/db'; +import * as db from '#lib/server/db.js'; /** @type {import('./$types').PageServerLoad} */ export async function load({ cookies }) { @@ -129,7 +131,7 @@ export const actions = { const password = data.get('password'); const user = await db.getUser(email); - cookies.set('sessionid', await db.createSession(user), { path: '/' }); + cookies.set('sessionid', await db.createSession(user)); return { success: true }; }, @@ -167,15 +169,17 @@ export const actions = { If the request couldn't be processed because of invalid data, you can return validation errors — along with the previously submitted form values — back to the user so that they can try again. The `fail` function lets you return an HTTP status code (typically 400 or 422, in the case of validation errors) along with the data. The status code is available through `page.status` and the data through `form`: +When using `use:enhance` (or making a `fetch` request with the `accept: application/json` header), the HTTP response status code will match the status code passed to `fail`. When an action returns data, the response will have status 200, and when it returns nothing (i.e. `undefined`), the response will have status 204. This makes it easier to track form submission outcomes using observability tools. + ```js /// file: src/routes/login/+page.server.js // @filename: ambient.d.ts -declare module '$lib/server/db'; +declare module '#lib/server/db.js'; // @filename: index.js // ---cut--- +++import { fail } from '@sveltejs/kit';+++ -import * as db from '$lib/server/db'; +import * as db from '#lib/server/db.js'; /** @satisfies {import('./$types').Actions} */ export const actions = { @@ -194,7 +198,7 @@ export const actions = { return fail(400, { email, incorrect: true }); }+++ - cookies.set('sessionid', await db.createSession(user), { path: '/' }); + cookies.set('sessionid', await db.createSession(user)); return { success: true }; }, @@ -234,12 +238,12 @@ Redirects (and errors) work exactly the same as in [`load`](load#Redirects): // @errors: 2345 /// file: src/routes/login/+page.server.js // @filename: ambient.d.ts -declare module '$lib/server/db'; +declare module '#lib/server/db.js'; // @filename: index.js // ---cut--- import { fail, +++redirect+++ } from '@sveltejs/kit'; -import * as db from '$lib/server/db'; +import * as db from '#lib/server/db.js'; /** @satisfies {import('./$types').Actions} */ export const actions = { @@ -257,7 +261,7 @@ export const actions = { return fail(400, { email, incorrect: true }); } - cookies.set('sessionid', await db.createSession(user), { path: '/' }); + cookies.set('sessionid', await db.createSession(user)); +++ if (url.searchParams.has('redirectTo')) { redirect(303, url.searchParams.get('redirectTo')); @@ -299,7 +303,7 @@ export {}; // @filename: index.js // ---cut--- -/** @type {import('@sveltejs/kit').Handle} */ +/** @type {import('@sveltejs/kit/hooks').Handle} */ export async function handle({ event, resolve }) { event.locals.user = await getUser(event.cookies.get('sessionid')); return resolve(event); @@ -329,7 +333,7 @@ export function load(event) { /** @satisfies {import('./$types').Actions} */ export const actions = { logout: async (event) => { - event.cookies.delete('sessionid', { path: '/' }); + event.cookies.delete('sessionid'); event.locals.user = null; } }; @@ -361,13 +365,37 @@ The easiest way to progressively enhance a form is to add the `use:enhance` acti Without an argument, `use:enhance` will emulate the browser-native behaviour, just without the full-page reloads. It will: -- update the `form` property, `page.form` and `page.status` on a successful or invalid response, but only if the action is on the same page you're submitting from. For example, if your form looks like `
`, the `form` prop and the `page.form` state will _not_ be updated. This is because in the native form submission case you would be redirected to the page the action is on. If you want to have them updated either way, use [`applyAction`](#Progressive-enhancement-Customising-use:enhance) +- if the action is on the page you're submitting from, emulate a reload of the current page — updating the `form` property, `page.form` and `page.status` on a successful or invalid response +- if the action is on a different page (for example ``), navigate to the action's URL on a successful _or_ invalid response, populating that page's `form` property and `page.status`, just as a native submission would. A new history entry is pushed - reset the `` element -- invalidate all data using `invalidateAll` on a successful response +- refresh all data using `refreshAll` on a successful response - call `goto` on a redirect response -- render the nearest `+error` boundary if an error occurs +- render the nearest `+error` boundary if an unexpected error occurs — the boundary nearest the _action's_ route, if the action is on a different page - [reset focus](accessibility#Focus-management) to the appropriate element +> [!NOTE] Two details are worth knowing about the URL: +> +> Where a submission lands is `result.location` — the form's `action` with the `?/actionName` parameter removed, since that parameter has done its job once the action has run. `action="/login?/do-login"` lands on `/login`, and any other query parameters are kept, so `action="/login?redirectTo=/dashboard&/do-login"` lands on `/login?redirectTo=/dashboard`. The destination's `load` functions see that URL via `url.search`. +> +> Two consequences worth knowing: +> +> - A _relative_ action replaces the entire query string as a side effect of URL resolution — `action="?/delete"` on `/items?page=2` resolves to `/items?/delete`, so both enhanced and native submissions drop `page=2`. Write `action="?page=2&/delete"` to preserve it. +> - Without JavaScript the browser can't strip anything, so an unenhanced submission leaves you on the raw action URL (`/login?/do-login`). Reloading that URL is a plain `GET`, which doesn't re-run the action and renders the page normally, so the difference is cosmetic — but a `load` function can observe a different `url.search` depending on whether JavaScript is available. + +If you don't want a cross-page submission to navigate — a common need for login or newsletter widgets that live in a layout and post to another page's action — pass `navigate: false` to `update`, which gives you the same behaviour as SvelteKit 2: + +```svelte + async ({ update }) => { + await update({ navigate: false }); + }} +> +``` + +The result is applied to the page you're on: `form`, `page.form` and `page.status` are updated, and error results render the current route's nearest `+error` boundary. + ### Customising use:enhance To customise the behaviour, you can provide a `SubmitFunction` that runs immediately before the form is submitted, and (optionally) returns a callback that runs with the `ActionResult`. @@ -392,7 +420,7 @@ To customise the behaviour, you can provide a `SubmitFunction` that runs immedia You can use these functions to show and hide loading UI, and so on. -If you return a callback, you override the default post-submission behavior. To get it back, call `update`, which accepts `invalidateAll` and `reset` parameters, or use `applyAction` on the result: +If you return a callback, you override the default post-submission behavior. To get it back, call `update`, which accepts `navigate`, `refreshAll` and `reset` parameters, or use `applyAction` on the result: ```svelte /// file: src/routes/login/+page.svelte @@ -418,13 +446,19 @@ If you return a callback, you override the default post-submission behavior. To > ``` +> [!NOTE] `update` accepts these options: +> +> - `reset: false` if you don't want the `` values to be reset after a successful submission +> - `refreshAll` controls whether all data is refreshed after submission. It defaults to `true` for successful results and `false` for failures. When the submission navigates to another page, setting it to `false` does _not_ prevent the destination's own `load` functions from running — it only allows shared layout data to be reused. `invalidateAll` is a deprecated alias for `refreshAll` +> - `navigate: false` applies a non-redirect result to the current page instead of navigating to `result.location`. Redirects are always followed + The behaviour of `applyAction(result)` depends on `result.type`: -- `success`, `failure` — sets `page.status` to `result.status` and updates `form` and `page.form` to `result.data` (regardless of where you are submitting from, in contrast to `update` from `enhance`) -- `redirect` — calls `goto(result.location, { invalidateAll: true })` -- `error` — renders the nearest `+error` boundary with `result.error` +- `success`, `failure` — sets `page.status` to `result.status` and updates `form` and `page.form` to `result.data` +- `redirect` — calls `goto(result.location, { refreshAll: true })` +- `error` — renders the current route's nearest `+error` boundary with `result.error` -In all cases, [focus will be reset](accessibility#Focus-management). +In all cases, [focus will be reset](accessibility#Focus-management). `applyAction` does not navigate to the `location` of a non-redirect result or refresh data. Use `update` to get the complete default enhanced behavior, including carrying form data and status into a cross-page navigation. When implementing enhancement manually, inspect `result.location` and use [`goto`]($app-navigation#goto) or [`refreshAll`]($app-navigation#refreshAll) as appropriate. ### Custom event listener @@ -433,7 +467,7 @@ We can also implement progressive enhancement ourselves, without `use:enhance`, ```svelte @@ -522,7 +559,7 @@ Some forms don't need to `POST` data to the server — search inputs, for exampl
``` -Submitting this form will navigate to `/search?q=...` and invoke your load function but will not invoke an action. As with `
` elements, you can set the [`data-sveltekit-reload`](link-options#data-sveltekit-reload), [`data-sveltekit-replacestate`](link-options#data-sveltekit-replacestate), [`data-sveltekit-keepfocus`](link-options#data-sveltekit-keepfocus) and [`data-sveltekit-noscroll`](link-options#data-sveltekit-noscroll) attributes on the `
` to control the router's behaviour. +Submitting this form will navigate to `/search?q=...` and invoke your load function but will not invoke an action. As with `` elements, you can set the [`data-sveltekit-reload`](link-options#data-sveltekit-reload), [`data-sveltekit-replacestate`](link-options#data-sveltekit-replacestate) and [`data-sveltekit-reset`](link-options#data-sveltekit-reset) attributes on the `` to control the router's behaviour. ## Further reading diff --git a/documentation/docs/20-core-concepts/40-page-options.md b/documentation/docs/20-core-concepts/40-page-options.md index d9023c341eee..3136792378cf 100644 --- a/documentation/docs/20-core-concepts/40-page-options.md +++ b/documentation/docs/20-core-concepts/40-page-options.md @@ -33,9 +33,9 @@ export const prerender = 'auto'; > [!NOTE] If your entire app is suitable for prerendering, you can use [`adapter-static`](adapter-static), which will output files suitable for use with any static webserver. -The prerenderer will start at the root of your app and generate files for any prerenderable pages or `+server.js` routes it finds. Each page is scanned for `` elements that point to other pages that are candidates for prerendering — because of this, you generally don't need to specify which pages should be accessed. If you _do_ need to specify which pages should be accessed by the prerenderer, you can do so with [`config.kit.prerender.entries`](configuration#prerender), or by exporting an [`entries`](#entries) function from your dynamic route. +The prerenderer will start at the root of your app and generate files for any prerenderable pages or `+server.js` routes it finds. Each page is scanned for `` elements that point to other pages that are candidates for prerendering — because of this, you generally don't need to specify which pages should be accessed. If you _do_ need to specify which pages should be accessed by the prerenderer, you can do so with [`config.prerender.entries`](configuration#prerender), or by exporting an [`entries`](#entries) function from your dynamic route. -While prerendering, the value of `building` imported from [`$app/environment`]($app-environment) will be `true`. +While prerendering, the value of `building` imported from [`$app/env`]($app-env) will be `true`. ### Prerendering server routes @@ -80,7 +80,7 @@ If you encounter an error like 'The following routes were marked as prerenderabl Since these routes cannot be dynamically server-rendered, this will cause errors when people try to access the route in question. There are a few ways to fix it: -* Ensure that SvelteKit can find the route by following links from [`config.kit.prerender.entries`](configuration#prerender) or the [`entries`](#entries) page option. Add links to dynamic routes (i.e. pages with `[parameters]` ) to this option if they are not found through crawling the other entry points, else they are not prerendered because SvelteKit doesn't know what value the parameters should have. Pages not marked as prerenderable will be ignored and their links to other pages will not be crawled, even if some of them would be prerenderable. +* Ensure that SvelteKit can find the route by following links from [`config.prerender.entries`](configuration#prerender) or the [`entries`](#entries) page option. Add links to dynamic routes (i.e. pages with `[parameters]` ) to this option if they are not found through crawling the other entry points, else they are not prerendered because SvelteKit doesn't know what value the parameters should have. Pages not marked as prerenderable will be ignored and their links to other pages will not be crawled, even if some of them would be prerenderable. * Ensure that SvelteKit can find the route by discovering a link to it from one of your other prerendered pages that have server-side rendering enabled. * Change `export const prerender = true` to `export const prerender = 'auto'`. Routes with `'auto'` can be dynamically server rendered @@ -98,7 +98,7 @@ SvelteKit will discover pages to prerender automatically, by starting at _entry Most of the time, that's enough. In some situations, links to pages like `/blog/hello-world` might not exist (or might not exist on prerendered pages), in which case we need to tell SvelteKit about their existence. -This can be done with [`config.kit.prerender.entries`](configuration#prerender), or by exporting an `entries` function from a `+page.js`, a `+page.server.js` or a `+server.js` belonging to a dynamic route: +This can be done with [`config.prerender.entries`](configuration#prerender), or by exporting an `entries` function from a `+page.js`, a `+page.server.js` or a `+server.js` belonging to a dynamic route: ```js /// file: src/routes/blog/[slug]/+page.server.js @@ -153,7 +153,7 @@ You can enable `csr` during development (for example to take advantage of HMR) l ```js /// file: +page.js -import { dev } from '$app/environment'; +import { dev } from '$app/env'; export const csr = dev; ``` diff --git a/documentation/docs/20-core-concepts/50-state-management.md b/documentation/docs/20-core-concepts/50-state-management.md index 874bc6968e1b..22bca9c5304b 100644 --- a/documentation/docs/20-core-concepts/50-state-management.md +++ b/documentation/docs/20-core-concepts/50-state-management.md @@ -45,13 +45,13 @@ For the same reason, your `load` functions should be _pure_ — no side-effects ```js /// file: +page.js // @filename: ambient.d.ts -declare module '$lib/user' { +declare module '#lib/user.js' { export const user: { set: (value: any) => void }; } // @filename: index.js // ---cut--- -import { user } from '$lib/user'; +import { user } from '#lib/user.js'; /** @type {import('./$types').PageLoad} */ export async function load({ fetch }) { @@ -82,7 +82,7 @@ If you're not using SSR, then there's no risk of accidentally exposing one user' ## Using state and stores with context -You might wonder how we're able to use `page.data` and other [app state]($app-state) (or [app stores]($app-stores)) if we can't use global state. The answer is that app state and app stores on the server use Svelte's [context API](/tutorial/svelte/context-api) — the state (or store) is attached to the component tree with `setContext`, and when you subscribe you retrieve it with `getContext`. We can do the same thing with our own state: +You might wonder how we're able to use `page.data` and other [app state]($app-state) if we can't use global state. The answer is that app state and app stores on the server use Svelte's [context API](/tutorial/svelte/context-api) — the state (or store) is attached to the component tree with `setContext`, and when you subscribe you retrieve it with `getContext`. We can do the same thing with our own state: ```svelte diff --git a/documentation/docs/20-core-concepts/60-remote-functions.md b/documentation/docs/20-core-concepts/60-remote-functions.md index ec9a76f9a16e..d1f3cad5c66a 100644 --- a/documentation/docs/20-core-concepts/60-remote-functions.md +++ b/documentation/docs/20-core-concepts/60-remote-functions.md @@ -10,30 +10,32 @@ Remote functions are a tool for type-safe communication between client and serve Combined with Svelte's experimental support for [`await`](/docs/svelte/await-expressions), it allows you to load and manipulate data directly inside your components. -This feature is currently experimental, meaning it is likely to contain bugs and is subject to change without notice. You must opt in by adding the `compilerOptions.experimental.async` and `kit.experimental.remoteFunctions` options in your `svelte.config.js`: +This feature is currently experimental, meaning it is likely to contain bugs and is subject to change without notice. You must opt in by adding the `compilerOptions.experimental.async` and `experimental.remoteFunctions` options to the SvelteKit plugin in your `vite.config.js`: ```js -/// file: svelte.config.js -/** @type {import('@sveltejs/kit').Config} */ -const config = { - kit: { - experimental: { - +++remoteFunctions: true+++ - } - }, - compilerOptions: { - experimental: { - +++async: true+++ - } - } -}; - -export default config; +/// file: vite.config.js +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [ + sveltekit({ + experimental: { + +++remoteFunctions: true+++ + }, + compilerOptions: { + experimental: { + +++async: true+++ + } + } + }) + ], +}); ``` ## Overview -Remote functions are exported from a `.remote.js` or `.remote.ts` file, and come in four flavours: `query`, `form`, `command` and `prerender`. On the client, the exported functions are transformed to `fetch` wrappers that invoke their counterparts on the server via a generated HTTP endpoint. Remote files can be placed anywhere in your `src` directory (except inside the `src/lib/server` directory), and third party libraries can provide them, too. +Remote functions are exported from a _remote module_, which is a file whose name includes a `remote` segment (e.g. `remote.ts` or `data.remote.ts`). They come in four flavours: `query`, `form`, `command` and `prerender`. On the client, the exported functions are transformed to `fetch` wrappers that invoke their counterparts on the server via a generated HTTP endpoint. Remote modules can be placed anywhere in your `src` directory (except inside a `server` directory — files here [cannot be imported into client-side code](server-only-modules)), and third-party libraries that list `@sveltejs/kit` in their `peerDependencies` can provide them too. ## query @@ -44,13 +46,13 @@ The `query` function allows you to read dynamic data from the server. ```js /// file: src/routes/blog/data.remote.js // @filename: ambient.d.ts -declare module '$lib/server/database' { +declare module '#lib/server/database.js' { export function sql(strings: TemplateStringsArray, ...values: any[]): Promise; } // @filename: index.js // ---cut--- import { query } from '$app/server'; -import * as db from '$lib/server/database'; +import * as db from '#lib/server/database.js'; export const getPosts = query(async () => { const posts = await db.sql` @@ -64,7 +66,7 @@ export const getPosts = query(async () => { }); ``` -> [!NOTE] Throughout this page, you'll see imports from fictional modules like `$lib/server/database` and `$lib/server/auth`. These are purely for illustrative purposes — you can use whatever database client and auth setup you like. +> [!NOTE] Throughout this page, you'll see imports from fictional modules like `#lib/server/database.js` and `#lib/server/auth.js`. These are purely for illustrative purposes — you can use whatever database client and auth setup you like. > > The `db.sql` function above is a [tagged template function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#tagged_templates) that escapes any interpolated values. @@ -137,7 +139,7 @@ Since `getPost` exposes an HTTP endpoint, it's important to validate this argume ```js /// file: src/routes/blog/data.remote.js // @filename: ambient.d.ts -declare module '$lib/server/database' { +declare module '#lib/server/database.js' { export function sql(strings: TemplateStringsArray, ...values: any[]): Promise; } // @filename: index.js @@ -145,7 +147,7 @@ declare module '$lib/server/database' { import * as v from 'valibot'; import { error } from '@sveltejs/kit'; import { query } from '$app/server'; -import * as db from '$lib/server/database'; +import * as db from '#lib/server/database.js'; export const getPosts = query(async () => { /* ... */ }); @@ -209,14 +211,14 @@ On the server, the callback receives an array of the arguments the function was ```js /// file: weather.remote.js // @filename: ambient.d.ts -declare module '$lib/server/database' { +declare module '#lib/server/database.js' { export function sql(strings: TemplateStringsArray, ...values: any[]): Promise; } // @filename: index.js // ---cut--- import * as v from 'valibot'; import { query } from '$app/server'; -import * as db from '$lib/server/database'; +import * as db from '#lib/server/database.js'; export const getWeather = query.batch(v.string(), async (cityIds) => { const weather = await db.sql` @@ -294,7 +296,7 @@ If you need direct, imperative access to the underlying stream of values (rather ```js // @filename: time.remote.ts -import { RemoteLiveQueryFunction } from '@sveltejs/kit'; +import type { RemoteLiveQueryFunction } from '$app/server'; export declare const getTime: RemoteLiveQueryFunction; // @errors: 2304 // @filename: index.js @@ -321,11 +323,11 @@ The `form` function makes it easy to write data to the server. It takes a callba ```ts /// file: src/routes/blog/data.remote.js // @filename: ambient.d.ts -declare module '$lib/server/database' { +declare module '#lib/server/database.js' { export function sql(strings: TemplateStringsArray, ...values: any[]): Promise; } -declare module '$lib/server/auth' { +declare module '#lib/server/auth.js' { interface User { name: string; } @@ -340,8 +342,8 @@ declare module '$lib/server/auth' { import * as v from 'valibot'; import { error, redirect } from '@sveltejs/kit'; import { query, form } from '$app/server'; -import * as db from '$lib/server/database'; -import * as auth from '$lib/server/auth'; +import * as db from '#lib/server/database.js'; +import * as auth from '#lib/server/auth.js'; export const getPosts = query(async () => { /* ... */ }); @@ -414,7 +416,7 @@ A form is composed of a set of _fields_, which are defined by the schema. In the These attributes allow SvelteKit to set the correct input type, set a `name` that is used to construct the `data` passed to the handler, populate the `value` of the form (for example following a failed submission, to save the user having to re-enter everything), and set the [`aria-invalid`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-invalid) state. -Passing a second argument to `.as(...)` is useful when rendering a form from existing data, such as an edit form or multiple instances created with [`for(...)`](#form-Multiple-instances-of-a-form). As well as setting the value of the element when it is rendered, it controls the value of the element when the form is reset. `radio`, `submit` and `hidden` inputs always need this value, and `checkbox` inputs need it when they represent one option in an array field. `file` inputs cannot be populated this way. +Passing a second argument to `.as(...)` is useful when rendering a form from existing data, such as an edit form or multiple instances created with [`for(...)`](#form-Multiple-instances-of-a-form). As well as setting the value of the element when it is rendered, it controls the value of the element when the form is reset. `radio`, `submit` and `hidden` inputs always need this value, and `checkbox` inputs need it when they represent one option in an array field. For these `radio` and `checkbox` inputs the second argument identifies the option, so whether it is currently checked goes in a third argument, e.g. `.as('radio', option, settings.visibility === option)` or `.as('checkbox', tag, settings.tags.includes(tag))`. `file` inputs cannot be populated this way. > [!NOTE] The generated `name` attribute uses JS object notation (e.g. `nested.array[0].value`). String keys that require quotes such as `object['nested-array'][0].value` are not supported. Under the hood, boolean checkbox and number field names are prefixed with `b:` and `n:`, respectively, to signal SvelteKit to coerce the values from strings prior to validation. @@ -475,7 +477,7 @@ export const createProfile = form(datingProfile, (data) => { /* ... */ }); Because our form contains a `file` input, we've added an `enctype="multipart/form-data"` attribute. The values for `info.height` and `info.likesDogs` are coerced to a number and a boolean respectively. -> [!NOTE] If a `checkbox` input is unchecked, the value is not included in the [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) object that SvelteKit constructs the data from. As such, we have to make the value optional in our schema. In Valibot that means using `v.optional(v.boolean(), false)` instead of just `v.boolean()`, whereas in Zod it would mean using `z.coerce.boolean()`. +> [!NOTE] If a `checkbox` input is unchecked, the value is not included in the [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) object that SvelteKit constructs the data from. As such, we have to give the value a default of `false` in our schema, rather than merely marking it optional. In Valibot that means using `v.optional(v.boolean(), false)` instead of just `v.boolean()`, whereas in Zod it means using `z.boolean().default(false)` instead of just `z.boolean()`. In the case of `radio` and `checkbox` inputs that all belong to the same field, the `value` must be specified as a second argument to `.as(...)`: @@ -566,7 +568,7 @@ In addition to declarative schema validation, you can programmatically mark fiel // @errors: 18046 /// file: src/routes/shop/data.remote.js // @filename: ambient.d.ts -declare module '$lib/server/database' { +declare module '#lib/server/database.js' { export function buy(qty: number): Promise } // @filename: index.js @@ -574,7 +576,7 @@ declare module '$lib/server/database' { import * as v from 'valibot'; import { invalid } from '@sveltejs/kit'; import { form } from '$app/server'; -import * as db from '$lib/server/database'; +import * as db from '#lib/server/database.js'; export const buyHotcakes = form( v.object({ @@ -737,11 +739,11 @@ The example above uses [`redirect(...)`](@sveltejs-kit#redirect), which sends th ```ts /// file: src/routes/blog/data.remote.js // @filename: ambient.d.ts -declare module '$lib/server/database' { +declare module '#lib/server/database.js' { export function sql(strings: TemplateStringsArray, ...values: any[]): Promise; } -declare module '$lib/server/auth' { +declare module '#lib/server/auth.js' { interface User { name: string; } @@ -755,8 +757,8 @@ declare module '$lib/server/auth' { import * as v from 'valibot'; import { error, redirect } from '@sveltejs/kit'; import { query, form } from '$app/server'; -import * as db from '$lib/server/database'; -import * as auth from '$lib/server/auth'; +import * as db from '#lib/server/database.js'; +import * as auth from '#lib/server/auth.js'; export const getPosts = query(async () => { /* ... */ }); @@ -804,7 +806,7 @@ We can customize what happens when the form is submitted with the `enhance` meth

Create a new post

@@ -845,7 +847,7 @@ When each instance should render different values, pass them as the second argum

Todos

{#each await getTodos() as todo} - {@const modify = modifyTodo.for(todo.id)} + {const modify = modifyTodo.for(todo.id)} @@ -862,7 +864,7 @@ To accomplish this, add a field to your schema for the button value, and use `as ```svelte @@ -884,7 +886,7 @@ To accomplish this, add a field to your schema for the button value, and use `as In your form handler, you can check which button was clicked: ```js -/// file: $lib/auth.js +/// file: #lib/auth.js import * as v from 'valibot'; import { form } from '$app/server'; @@ -915,14 +917,14 @@ As with `query` and `form`, if the function accepts an argument, it should be [v ```ts /// file: likes.remote.js // @filename: ambient.d.ts -declare module '$lib/server/database' { +declare module '#lib/server/database.js' { export function sql(strings: TemplateStringsArray, ...values: any[]): Promise; } // @filename: index.js // ---cut--- import * as v from 'valibot'; import { query, command } from '$app/server'; -import * as db from '$lib/server/database'; +import * as db from '#lib/server/database.js'; export const getLikes = query(v.string(), async (id) => { const [row] = await db.sql` @@ -949,7 +951,7 @@ Now simply call `addLike`, from (for example) an event handler: @@ -1025,6 +1027,8 @@ export const updatePost = form( Because queries are keyed based on their arguments, `getPost(post.id).set(result)` on the server knows to look up the matching `getPost(id)` on the client to update it. The same goes for `getPosts().refresh()` -- it knows to look up `getPosts()` with no argument on the client. +Calling `refresh()`, `set()` or `reconnect()` anywhere in a `form` handler replaces the default invalidation of all queries and load functions for that submission, so only the queries you refreshed will update. + ### Reconnecting live queries in mutations Single-flight mutations can also reconnect `query.live` instances. In a `form`/`command` handler, call `.reconnect()` on the live query resource you want to reconnect: @@ -1055,7 +1059,7 @@ Unfortunately, life isn't always as simple as the preceding example. The server SvelteKit makes this easy by allowing the client to _request_ that the server updates specific data using `submit().updates` (for `form`) or `myCommand().updates` (for `command`): ```ts -import type { RemoteQueryUpdate, RemoteQuery } from '@sveltejs/kit'; +import type { RemoteQueryUpdate, RemoteQuery } from '$app/server'; interface Post {} declare function submit(): Promise & { updates(...updates: RemoteQueryUpdate[]): Promise; @@ -1103,21 +1107,56 @@ export const createPost = form( ); ``` -`requested` gives you access to the queries the client requested to refresh. Each entry is an `{ arg, query }` object: `arg` is the value the query's implementation function received — i.e. the argument *after* the schema has validated and (where applicable) transformed it — and `query` is a `RemoteQuery` already bound to the client's original cache key, so calling `query.refresh()` / `query.set(...)` updates the correct client instance. If parsing an argument fails, that query will error, but the entire command will not fail. `requested`'s second parameter, `limit`, is the maximum number of items it will return. Any refresh requests beyond this limit will fail. +`requested` gives you access to the queries the client requested to refresh. Each entry is an `{ arg, query, ignore }` object: `arg` is the value the query's implementation function received — i.e. the argument *after* the schema has validated and (where applicable) transformed it — and `query` is a `RemoteQuery` already bound to the client's original cache key, so calling `query.refresh()` / `query.set(...)` updates the correct client instance. Call `ignore()` if you intentionally do not want to update a particular instance: + +```js +import { requested, query } from '$app/server'; + +export const getPosts = query('unchecked', ({ filter }) => { + return ['foo', 'bar']; +}); +// ---cut--- +for (const { arg, query, ignore } of requested(getPosts, 10)) { + if (arg.filter === 'author:santa') { + void query.refresh(); + } else { + ignore(); + } +} +``` + +Every requested update must be refreshed, set, reconnected (for a live query), or explicitly ignored. Otherwise, the corresponding query will enter an error state on the client. If parsing or validating an argument fails, that query will also error, but the entire command will not fail. + +`requested`'s second parameter, `limit`, is the maximum number of items it will return. Any refresh requests beyond this limit will fail on the client. > [!NOTE] `limit` is required because the list of refresh requests is controlled by the client — each entry causes the server to validate an argument and usually re-fetch data, so an unbounded list is a denial-of-service risk. Choose a limit that reflects the worst case you're willing to handle per request. You _can_ pass `Infinity` if you have explicitly decided to accept any number of refreshes, but it is not recommended. Additionally, `requested` allows a simple shorthand when all you want to do is refresh the requested query instances: ```ts -import type { RemoteQueryFunction } from '@sveltejs/kit'; import { requested } from '$app/server'; +import type { RemoteQueryFunction } from '$app/server'; declare const getPosts: RemoteQueryFunction; // ---cut--- // this is the same as looping over the result and calling `void query.refresh()`. await requested(getPosts, 1).refreshAll(); ``` +If you want to intentionally ignore every selected update, use `ignoreAll`: + +```js +import { requested, query } from '$app/server'; + +export const getPosts = query('unchecked', ({ filter }) => { + return ['foo', 'bar']; +}); + +// ---cut--- +await requested(getPosts, 10).ignoreAll(); +``` + +`ignoreAll` only ignores entries within the specified `limit`. Any excess entries still fail on the client. + > [!NOTE] Why does the command have to name every query it's willing to refresh? Two reasons: > > - **Bundle size.** If a command could implicitly refresh *any* query in your app, SvelteKit would have to include every query's code in the command's server bundle, because it can't know ahead of time which ones will be called. @@ -1130,13 +1169,13 @@ The `prerender` function is similar to `query`, except that it will be invoked a ```js /// file: src/routes/blog/data.remote.js // @filename: ambient.d.ts -declare module '$lib/server/database' { +declare module '#lib/server/database.js' { export function sql(strings: TemplateStringsArray, ...values: any[]): Promise; } // @filename: index.js // ---cut--- import { prerender } from '$app/server'; -import * as db from '$lib/server/database'; +import * as db from '#lib/server/database.js'; export const getPosts = prerender(async () => { const posts = await db.sql` @@ -1161,7 +1200,7 @@ As with queries, prerender functions can accept an argument, which should be [va ```js /// file: src/routes/blog/data.remote.js // @filename: ambient.d.ts -declare module '$lib/server/database' { +declare module '#lib/server/database.js' { export function sql(strings: TemplateStringsArray, ...values: any[]): Promise; } // @filename: index.js @@ -1169,7 +1208,7 @@ declare module '$lib/server/database' { import * as v from 'valibot'; import { error } from '@sveltejs/kit'; import { prerender } from '$app/server'; -import * as db from '$lib/server/database'; +import * as db from '#lib/server/database.js'; export const getPosts = prerender(async () => { /* ... */ }); @@ -1234,18 +1273,23 @@ As long as _you're_ not passing invalid data to your remote functions, there are - the function signature changed between deployments, and some users are currently on an older version of your app - someone is trying to attack your site by poking your exposed endpoints with bad data -In the second case, we don't want to give the attacker any help, so SvelteKit will generate a generic [400 Bad Request](https://http.dog/400) response. You can control the message by implementing the [`handleValidationError`](hooks#handleValidationError) server hook, which, like [`handleError`](hooks#handleError), must return an [`App.Error`](errors#Type-safety) (which defaults to `{ message: string }`): +In the second case, we don't want to give the attacker any help, so SvelteKit will generate a generic [400 Bad Request](https://http.dog/400) response. Validation failures pass through the server [`handleError`](hooks#handleError) hook with `kind: 'validation'`, an `error` object containing `{ status, message }`, and the validation `issues`. You can use the issues to log the failure or customise the response: ```js /// file: src/hooks.server.js -/** @type {import('@sveltejs/kit').HandleValidationError} */ -export function handleValidationError({ event, issues }) { - return { - message: 'Nice try, hacker!' - }; +/** @type {import('@sveltejs/kit/hooks').HandleServerError} */ +export function handleError({ kind, issues }) { + if (kind === 'validation') { + console.error(issues); + return { + message: 'Nice try, hacker!' + }; + } } ``` +Be thoughtful about exposing validation issues, as they may give an attacker useful information. Returning `error` unchanged is safe — unlike `issues`, it only contains the generic status and message. + If you know what you're doing and want to opt out of validation, you can pass the string `'unchecked'` in place of a schema: ```ts @@ -1270,14 +1314,14 @@ interface User { avatar: string; } -declare module '$lib/server/database' { +declare module '#lib/server/database.js' { export function findUser(sessionId: string | undefined): Promise; } // @filename: index.js // ---cut--- import { getRequestEvent, query } from '$app/server'; -import { findUser } from '$lib/server/database'; +import { findUser } from '#lib/server/database.js'; export const getProfile = query(async () => { const user = await getUser(); @@ -1300,7 +1344,8 @@ const getUser = query(async () => { Note that some properties of `RequestEvent` are different inside remote functions: - you cannot set headers (other than writing cookies, and then only inside `form` and `command` functions) -- `route`, `params` and `url` relate to the page the remote function was called from, _not_ the URL of the endpoint SvelteKit creates for the remote function. Never use them to determine whether or not a user is authorized to access certain data, as these values are part of the request which could be manipulated. Queries are also not re-run when the user navigates (unless the argument to the query changes as a result of navigation), and so you should be mindful of how you use these values. +- inside `query` functions (including `query.batch` and `query.live`), accessing `route`, `params` or `url` throws an error. Queries are cached by their arguments and are not re-run when the user navigates, so results depending on these values would go stale without the cache noticing. Pass values from the page as arguments to the query instead. You can still derive trusted context from cookies or headers in the [`handle`](hooks#handle) hook and put it on `locals`, but note that for query requests `handle` sees the URL of the remote endpoint, not of the page +- in `form` and `command` functions, `route`, `params` and `url` relate to the page the remote function was called from, _not_ the URL of the endpoint SvelteKit creates for the remote function. Never use them to determine whether or not a user is authorized to access certain data, as these values are part of the request which could be manipulated ## Redirects diff --git a/documentation/docs/20-core-concepts/70-environment-variables.md b/documentation/docs/20-core-concepts/70-environment-variables.md index 7d27e16e7022..8b282fef7855 100644 --- a/documentation/docs/20-core-concepts/70-environment-variables.md +++ b/documentation/docs/20-core-concepts/70-environment-variables.md @@ -11,40 +11,17 @@ During development, and at build time, variables defined in a `.env` or `.env.lo API_KEY=19f401ba-e8b0-48c4-8c77-b0ebb26d97fe ``` -By default, every environment variable is implicitly available inside your app via the following modules: - -- [`$env/static/private`]($env-static-private) -- [`$env/static/public`]($env-static-public) -- [`$env/dynamic/private`]($env-dynamic-private) -- [`$env/dynamic/public`]($env-dynamic-public) - -## Explicit environment variables - -As of SvelteKit 2.63, you can opt into _explicit_ environment variables, in which case you instead import environment variables from these modules: +After following the setup below, they can be imported via the following modules: - [`$app/env/private`]($app-env-private) - [`$app/env/public`]($app-env-public) -Additionally, the [`$app/environment`]($app-environment) module is renamed to [`$app/env`]($app-env). - -> [!NOTE] Explicit environment variables will become the default in SvelteKit 3. The `$env/*` modules, along with `$app/environment`, will be removed. - -### Setup - -To opt in, update your configuration... +> [!LEGACY] +> The `$env/*` modules, along with `$app/environment` were deprecated in SvelteKit 3 (and will be removed in SvelteKit 4) in favour of explicit environment variables that were added in SvelteKit 2.62 as an experimental option. -```js -/// file: svelte.config.js -export default { - kit: { - experimental: { - +++explicitEnvironmentVariables: true+++ - } - } -}; -``` +## Setup -...and add a `src/env.ts` (or `src/env.js`) file that exports a `variables` object: +Add a `src/env.ts` (or `src/env.js`) file that exports a `variables` object: ```ts /// file: src/env.ts @@ -55,11 +32,11 @@ export const variables = defineEnvVars({ }); ``` -Each value in the object passed to [`defineEnvVars`](@sveltejs-kit-env#defineEnvVars) is an [`EnvVarConfig`](@sveltejs-kit#EnvVarConfig) object that configures the environment variable. +Each value in the object passed to [`defineEnvVars`](@sveltejs-kit-env#defineEnvVars) is an [`EnvVarConfig`](@sveltejs-kit-env#EnvVarConfig) object that configures the environment variable. > [!NOTE] `defineEnvVars` returns its argument unaltered — it exists purely to help with type safety. -### Private variables +## Private variables By default, all variables are considered private. For example, you don't want to reveal your `API_KEY`: @@ -82,7 +59,7 @@ import { API_KEY } from '$app/env/private'; The `$app/env/private` module cannot be imported into code that runs in the browser, so that you can't accidentally reveal your secrets in a JavaScript bundle. -### Public variables +## Public variables Some variables are perfectly safe — necessary, even — to expose to the browser. For these, we can specify `public: true`: @@ -127,7 +104,7 @@ export const variables = defineEnvVars({ ``` -### Validation +## Validation You can specify a [Standard Schema](https://standardschema.dev/) validator such as [Zod](https://zod.dev/) or [Valibot](https://valibot.dev/) to check that an environment variable value is correct: @@ -144,6 +121,23 @@ export const variables = defineEnvVars({ }); ``` +If you don't want to bring in a schema library, you can pass a function that returns the (possibly transformed) value, or throws an error explaining the problem: + +```ts +/// file: src/env.ts +import { defineEnvVars } from '@sveltejs/kit/env'; + +export const variables = defineEnvVars({ + GOOGLE_ANALYTICS_ID: { + public: true, + schema: (value) => { + if (!value?.startsWith('G-')) throw new Error('expected a Google Analytics ID'); + return value; + } + } +}); +``` + If a value is invalid, the app will fail to start (or build). To opt out of one or the other, use [`building`]($app-env#building) from `$app/env` along with a validator that accepts an optional value: ```ts @@ -162,7 +156,7 @@ export const variables = defineEnvVars({ You can use validators to make values optional, or transform them (such as turning a string into a boolean, or parsing JSON) — see your validation library's documentation to learn how. -### Static variables +## Static variables By default, variables are dynamic. If a variable is configured with `static: true`, it will be inlined into your application code, enabling optimisations like dead-code elimination: @@ -190,7 +184,7 @@ Because this variable is `static`, the `` component shown here wil ```svelte {#if SHOW_DEBUG_OVERLAY} @@ -206,7 +200,7 @@ SHOW_DEBUG_OVERLAY=true npm run build ...then the component will be included and shown. -### Documenting variables +## Documenting variables You can document the purpose of an environment variable by adding a `description`: diff --git a/documentation/docs/25-build-and-deploy/10-building-your-app.md b/documentation/docs/25-build-and-deploy/10-building-your-app.md index ca3fd211fbc9..263e73ad793c 100644 --- a/documentation/docs/25-build-and-deploy/10-building-your-app.md +++ b/documentation/docs/25-build-and-deploy/10-building-your-app.md @@ -10,11 +10,11 @@ Secondly, an _adapter_ takes this production build and tunes it for your target ## During the build -SvelteKit will load your `+page/layout(.server).js` files (and all files they import) for analysis during the build. Any code that should _not_ be executed at this stage must check that `building` from [`$app/environment`]($app-environment) is `false`: +SvelteKit will load your `+page/layout(.server).js` files (and all files they import) for analysis during the build. Any code that should _not_ be executed at this stage must check that `building` from [`$app/env`]($app-env) is `false`: ```js -+++import { building } from '$app/environment';+++ -import { initialiseDatabase } from '$lib/server/database'; ++++import { building } from '$app/env';+++ +import { initialiseDatabase } from '#lib/server/database.js'; +++if (!building) {+++ initialiseDatabase(); diff --git a/documentation/docs/25-build-and-deploy/20-adapters.md b/documentation/docs/25-build-and-deploy/20-adapters.md index 0e6062a5101a..e1ddedcc6785 100644 --- a/documentation/docs/25-build-and-deploy/20-adapters.md +++ b/documentation/docs/25-build-and-deploy/20-adapters.md @@ -9,6 +9,7 @@ Official adapters exist for a variety of platforms — these are documented on t - [`@sveltejs/adapter-cloudflare`](adapter-cloudflare) for Cloudflare Workers and Cloudflare Pages - [`@sveltejs/adapter-netlify`](adapter-netlify) for Netlify - [`@sveltejs/adapter-node`](adapter-node) for Node servers +- [`@sveltejs/adapter-bun`](adapter-bun) for Bun servers - [`@sveltejs/adapter-static`](adapter-static) for static site generation (SSG) - [`@sveltejs/adapter-vercel`](adapter-vercel) for Vercel @@ -16,33 +17,34 @@ Additional [community-provided adapters](/packages#sveltekit-adapters) exist for ## Using adapters -Your adapter is specified in `svelte.config.js`: +Run [`npx sv add sveltekit-adapter`](/docs/cli/sveltekit-adapter) to install an adapter and add it to your config. + +Your adapter is specified in `vite.config.js`: ```js -/// file: svelte.config.js +/// file: vite.config.js // @filename: ambient.d.ts declare module 'svelte-adapter-foo' { - const adapter: (opts: any) => import('@sveltejs/kit').Adapter; + const adapter: (opts?: any) => import('@sveltejs/kit').Adapter; export default adapter; } +// @errors: 2554 // @filename: index.js // ---cut--- -import adapter from 'svelte-adapter-foo'; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - kit: { - adapter: adapter({ - // adapter options go here +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; ++++import adapter from 'svelte-adapter-foo';+++ + +export default defineConfig({ + plugins: [ + sveltekit({ + +++adapter: adapter()+++ }) - } -}; - -export default config; + ] +}); ``` ## Platform-specific context Some adapters may have access to additional information about the request. For example, Cloudflare Workers can access an `env` object containing KV namespaces etc. This can be passed to the `RequestEvent` used in [hooks](hooks) and [server routes](routing#server) as the `platform` property — consult each adapter's documentation to learn more. - diff --git a/documentation/docs/25-build-and-deploy/30-adapter-auto.md b/documentation/docs/25-build-and-deploy/30-adapter-auto.md index 0e60deae69e1..fc7639358006 100644 --- a/documentation/docs/25-build-and-deploy/30-adapter-auto.md +++ b/documentation/docs/25-build-and-deploy/30-adapter-auto.md @@ -4,12 +4,13 @@ title: Zero-config deployments When you create a new SvelteKit project with `npx sv create`, it installs [`adapter-auto`](https://github.com/sveltejs/kit/tree/main/packages/adapter-auto) by default. This adapter automatically installs and uses the correct adapter for supported environments when you deploy: -- [`@sveltejs/adapter-cloudflare`](adapter-cloudflare) for [Cloudflare Pages](https://developers.cloudflare.com/pages/) +- [`@sveltejs/adapter-cloudflare`](adapter-cloudflare) for [Cloudflare Workers](https://www.cloudflare.com/products/workers/) and [Pages](https://developers.cloudflare.com/pages/) - [`@sveltejs/adapter-netlify`](adapter-netlify) for [Netlify](https://netlify.com/) - [`@sveltejs/adapter-vercel`](adapter-vercel) for [Vercel](https://vercel.com/) - [`svelte-adapter-azure-swa`](https://github.com/geoffrich/svelte-adapter-azure-swa) for [Azure Static Web Apps](https://docs.microsoft.com/en-us/azure/static-web-apps/) - [`svelte-kit-sst`](https://github.com/sst/v2/tree/master/packages/svelte-kit-sst) for [AWS via SST](https://sst.dev/docs/start/aws/svelte) - [`@sveltejs/adapter-node`](adapter-node) for [Google Cloud Run](https://cloud.google.com/run) +- [`@sveltejs/adapter-node`](adapter-node) for [Render](https://render.com) It's recommended to install the appropriate adapter to your `devDependencies` once you've settled on a target environment, since this will add the adapter to your lockfile and slightly improve install times on CI. 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 e57dec781e48..e2d9b01cacf6 100644 --- a/documentation/docs/25-build-and-deploy/40-adapter-node.md +++ b/documentation/docs/25-build-and-deploy/40-adapter-node.md @@ -8,21 +8,22 @@ A quick way to get started is by deploying a project using SvelteKit's [official ## Usage -Install with `npm i -D @sveltejs/adapter-node`, then add the adapter to your `svelte.config.js`: +Run [`npx sv add sveltekit-adapter="adapter:node"`](/docs/cli/sveltekit-adapter), or install with `npm i -D @sveltejs/adapter-node` and add the adapter to your `vite.config.js`: ```js -// @errors: 2307 -/// file: svelte.config.js +// @errors: 2307 2554 +/// file: vite.config.js import adapter from '@sveltejs/adapter-node'; +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; -/** @type {import('@sveltejs/kit').Config} */ -const config = { - kit: { - adapter: adapter() - } -}; - -export default config; +export default defineConfig({ + plugins: [ + sveltekit({ + adapter: adapter() + }) + ] +}); ``` ## Deploying @@ -35,7 +36,9 @@ You will need the output directory, the project's `package.json`, and the produc node build ``` -Development dependencies will be bundled into your app using [Rollup](https://rollupjs.org). To control whether a given package is bundled or externalised, place it in `devDependencies` or `dependencies` respectively in your `package.json`. +Development dependencies will be bundled into your app using [Rolldown](https://rolldown.rs/). To control whether a given package is bundled or externalised, place it in `devDependencies` or `dependencies` respectively in your `package.json`. + +Client assets and prerendered output are served from a list of files recorded during the build. ### Compressing responses @@ -47,19 +50,7 @@ However, if you're building a [custom server](#Custom-server) and do want to add In `dev` and `preview`, SvelteKit will read environment variables from your `.env` file (or `.env.local`, or `.env.[mode]`, [as determined by Vite](https://vitejs.dev/guide/env-and-mode.html#env-files).) -In production, `.env` files are _not_ automatically loaded. To do so, install `dotenv` in your project... - -```sh -npm install dotenv -``` - -...and invoke it before running the built app: - -```sh -node +++-r dotenv/config+++ build -``` - -If you use Node.js v20.6+, you can use the [`--env-file`](https://nodejs.org/en/learn/command-line/how-to-read-environment-variables-from-nodejs) flag instead: +In production, `.env` files are _not_ automatically loaded. To do so, use the [`--env-file`](https://nodejs.org/en/learn/command-line/how-to-read-environment-variables-from-nodejs) flag when running the built app: ```sh node +++--env-file=.env+++ build @@ -79,18 +70,34 @@ Alternatively, the server can be configured to accept connections on a specified SOCKET_PATH=/tmp/socket node build ``` -### `ORIGIN`, `PROTOCOL_HEADER`, `HOST_HEADER`, and `PORT_HEADER` +### `PROTOCOL_HEADER`, `HOST_HEADER`, and `PORT_HEADER` -HTTP doesn't give SvelteKit a reliable way to know the URL that is currently being requested. The simplest way to tell SvelteKit where the app is being served is to set the `ORIGIN` environment variable: +HTTP doesn't give SvelteKit a reliable way to know the URL that is currently being requested. By default, SvelteKit will derive the origin from the request's `host` header (and the `https` protocol, if no `PROTOCOL_HEADER` is set). -```sh -ORIGIN=https://my.site node build +If your app is served from an origin that isn't known at request time — for example because it's behind a reverse proxy that doesn't pass the `host` header, or because you want to use a canonical origin for CSRF checks that differs from the request's host — you can set the [`paths.origin`](configuration#paths) option in your `vite.config.js`: -# or e.g. for local previewing and testing -ORIGIN=http://localhost:3000 node build +```js +// @errors: 2307 +/// file: vite.config.js +import adapter from '@sveltejs/adapter-node'; +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [ + sveltekit({ + adapter: adapter(), + paths: { + origin: process.env.ORIGIN + } + }) + ] +}); ``` -With this, a request for the `/stuff` pathname will correctly resolve to `https://my.site/stuff`. Alternatively, you can specify headers that tell SvelteKit about the request protocol and host, from which it can construct the origin URL: +When `paths.origin` is not set (the default), `adapter-node` derives the origin from the request — using the `host` header (and `PROTOCOL_HEADER`/`PORT_HEADER` if set) — and sets `request.url` accordingly. Otherwise, the configured value is used as the trusted self-origin for CSRF checks on form submissions and remote function calls, and as the value of `url.origin` during prerendering. + +Alternatively, you can specify headers that tell SvelteKit about the request protocol and host, from which it can construct the origin URL: ```sh PROTOCOL_HEADER=x-forwarded-proto HOST_HEADER=x-forwarded-host node build @@ -151,23 +158,24 @@ The number of seconds for [`keepAliveTimeout`](https://nodejs.org/api/http.html# The adapter can be configured with various options: ```js -// @errors: 2307 -/// file: svelte.config.js +// @errors: 2307 2554 +/// file: vite.config.js import adapter from '@sveltejs/adapter-node'; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - kit: { - adapter: adapter({ - // default options are shown - out: 'build', - precompress: true, - envPrefix: '' +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [ + sveltekit({ + adapter: adapter({ + // default options are shown + out: 'build', + precompress: true, + envPrefix: '' + }) }) - } -}; - -export default config; + ] +}); ``` ### out @@ -176,7 +184,7 @@ The directory to build the server to. It defaults to `build` — i.e. `node buil ### precompress -Enables precompressing using gzip and brotli for assets and prerendered pages. It defaults to `true`. +Generates `.br` and `.gz` variants of client and prerendered assets during the build. The server negotiates `Accept-Encoding` per request, preferring brotli over gzip, and each variant carries its own ETag. It defaults to `true`. ### envPrefix @@ -189,7 +197,6 @@ envPrefix: 'MY_CUSTOM_'; ```sh MY_CUSTOM_HOST=127.0.0.1 \ MY_CUSTOM_PORT=4000 \ -MY_CUSTOM_ORIGIN=https://my.site \ node build ``` @@ -208,8 +215,8 @@ You can listen to the `sveltekit:shutdown` event which is emitted after the HTTP ```js // @errors: 2304 process.on('sveltekit:shutdown', async (reason) => { - await jobs.stop(); - await db.close(); + await jobs.stop(); + await db.close(); }); ``` @@ -251,7 +258,7 @@ WantedBy=sockets.target ## Custom server -The adapter creates two files in your build directory — `index.js` and `handler.js`. Running `index.js` — e.g. `node build`, if you use the default build directory — will start a server on the configured port. +The build directory contains two entry points, `index.js` and `handler.js`. Running `index.js` — e.g. `node build`, if you use the default build directory — will start a server on the configured port. Alternatively, you can import the `handler.js` file, which exports a handler suitable for use with [Express](https://github.com/expressjs/express), [Connect](https://github.com/senchalabs/connect) or [Polka](https://github.com/lukeed/polka) (or even just the built-in [`http.createServer`](https://nodejs.org/dist/latest/docs/api/http.html#httpcreateserveroptions-requestlistener)) and set up your own server: @@ -276,6 +283,6 @@ app.listen(3000, () => { }); ``` -> [!NOTE] When you use `handler.js` in a custom server, only the environment variables read by the handler itself take effect: `ORIGIN`, `PROTOCOL_HEADER`, `HOST_HEADER`, `PORT_HEADER`, `ADDRESS_HEADER`, `XFF_DEPTH`, and `BODY_SIZE_LIMIT`. +> [!NOTE] When you use `handler.js` in a custom server, only the environment variables read by the handler itself take effect: `PROTOCOL_HEADER`, `HOST_HEADER`, `PORT_HEADER`, `ADDRESS_HEADER`, `XFF_DEPTH`, and `BODY_SIZE_LIMIT`. > > The server-lifecycle variables (`PORT`, `HOST`, `SOCKET_PATH`, `SHUTDOWN_TIMEOUT`, `IDLE_TIMEOUT`, `KEEP_ALIVE_TIMEOUT`, `HEADERS_TIMEOUT`, `LISTEN_PID`, `LISTEN_FDS`) are only honored by the default `node build` server. Implement them yourself in a custom server if you need the same behavior — for example, the snippet above listens on a hardcoded `3000` regardless of `PORT`. diff --git a/documentation/docs/25-build-and-deploy/45-adapter-bun.md b/documentation/docs/25-build-and-deploy/45-adapter-bun.md new file mode 100644 index 000000000000..8a1c99f6200b --- /dev/null +++ b/documentation/docs/25-build-and-deploy/45-adapter-bun.md @@ -0,0 +1,259 @@ +--- +title: Bun servers +--- + +[`adapter-bun`](https://github.com/sveltejs/kit/tree/main/packages/adapter-bun) builds a SvelteKit application into a standalone [Bun](https://bun.com/) server. The generated server uses `Bun.serve` for requests and `Bun.file` responses for client assets, prerendered output, and files read with [`read`](https://svelte.dev/docs/kit/$app-server#read) from `$app/server`. + +> [!NOTE] Bun 1.4 or newer is required. + +## Usage + +Install the adapter: + +```sh +bun add -D @sveltejs/adapter-bun +``` + +Configure it in `vite.config.js`: + +```js +// @errors: 2307 2554 +/// file: vite.config.js +import adapter from '@sveltejs/adapter-bun'; +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [ + sveltekit({ + adapter: adapter() + }) + ] +}); +``` + +The adapter calls Bun's build API, so the production build itself must run in Bun. The `--bun` flag overrides Vite's Node.js shebang: + +```sh +bun run --bun build +``` + +The default build is written to `build`. Start it with: + +```sh +bun ./build +``` + +You will need the output directory, the project's `package.json`, and the production dependencies in `node_modules` to run the application. Production dependencies can be generated by copying the `package.json` and `bun.lock` and then running `bun install --production --frozen-lockfile` (you can skip this step if your app doesn't have any dependencies). Development dependencies are bundled into your app. To control whether a given package is bundled or externalised, place it in `devDependencies` or `dependencies` respectively in your `package.json`. + +Client assets and prerendered output are registered as native Bun routes. Only `GET` and `HEAD` requests are served by those routes; other methods continue to SvelteKit. Every asset carries an ETag computed during the build, so conditional requests revalidate with an empty `304` response. Bun supplies MIME types, byte ranges for filesystem-backed files, and streaming without buffering every asset in memory. Files below SvelteKit's `immutable` directory receive `Cache-Control: public,max-age=31536000,immutable`. + +> [!NOTE] Bun treats `*` in a route pathname as a wildcard. The adapter rejects client and prerendered filenames that contain a literal `*`; rename those files before building. + +## Options + +```js +// @errors: 2307 2554 +/// file: vite.config.js +import adapter from '@sveltejs/adapter-bun'; +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [ + sveltekit({ + adapter: adapter({ + out: 'build', + envPrefix: '', + serverOptions: { + idleTimeout: 30 + } + }) + }) + ] +}); +``` + +### out + +The output directory. It defaults to `build`. + +### precompress + +Set `precompress: true` to generate `.br` and `.gz` variants of client and prerendered assets during the build. The generated routes negotiate `Accept-Encoding` per request, preferring brotli over gzip, and each variant carries its own ETag. The option is ignored when `buildOptions.compile` is set, because embedded assets are imported by identity path. + +### envPrefix + +A prefix for every deployment environment variable documented below. This is useful when the unprefixed names conflict with variables managed by your host: + +```js +// @errors: 2307 +import adapter from '@sveltejs/adapter-bun'; +// ---cut--- +adapter({ envPrefix: 'MY_APP_' }); +``` + +```sh +MY_APP_HOST=127.0.0.1 MY_APP_PORT=4000 bun ./build +``` + +When a prefix is configured, the server fails at startup if it finds an unknown environment variable with that prefix. This catches collisions and misspellings. + +### serverOptions + +JSON-serializable defaults passed to `Bun.serve`. The supported properties are: + +- `hostname` +- `port` +- `unix` +- `reusePort` +- `ipv6Only` +- `idleTimeout` +- `maxRequestBodySize` +- `development` + +Environment variables take precedence over these defaults. A configured Unix socket takes precedence over `hostname`, `port`, `reusePort`, and `ipv6Only`. + +The generated server owns `fetch` and `routes`. It does not expose `websocket`, `error`, TLS, HTTP/3, or HTTP/1 configuration through `serverOptions`. Use a custom Bun integration if your application requires those `Bun.serve` options. + +### buildOptions + +`buildOptions` configures the `Bun.build` call that turns the server into an executable, so it only applies when `compile` is set. The adapter accepts `sourcemap`, `minify`, `bytecode`, `banner`, `footer`, `drop`, `features`, `optimizeImports`, `external`, and `compile`. The entrypoint, output directory, top-level `target`, and module `format` are reserved. Source maps default to `external`; set `sourcemap: 'none'` to disable them. + +#### Compiled executables + +Set `compile: true` to generate a single executable at `/server`: + +```js +// @errors: 2307 +import adapter from '@sveltejs/adapter-bun'; +// ---cut--- +adapter({ + buildOptions: { + compile: true + } +}); +``` + +Build and run it without a separately installed Bun runtime: + +```sh +bun run --bun build +./build/server +``` + +The executable embeds the server code, client assets, prerendered output, and Bun runtime. `compile` can also be a Bun target string, which keeps the default `server` filename, or an options object. To change the executable name or cross-compile, provide an options object: + +```js +// @errors: 2307 +import adapter from '@sveltejs/adapter-bun'; +// ---cut--- +adapter({ + out: 'dist', + buildOptions: { + compile: { + outfile: 'application', + target: 'bun-linux-x64' + }, + minify: true, + bytecode: true, + sourcemap: 'linked' + } +}); +``` + +The result in this example is `dist/application`. Platform targets, native dependencies, and other limitations follow [Bun's executable compilation rules](https://bun.com/docs/bundler/executables). + +Executables bundle production dependencies, since there is no `node_modules` to resolve them from. To resolve a package from `node_modules` at runtime instead, list it in `external` and set `compile: { autoloadPackageJson: true }`. Packages that read files relative to their own location, such as `jsdom`, only work this way. + +## Environment variables + +Bun loads `.env` files automatically. If `envPrefix` is set, add that prefix to each name in this section. + +### Listener + +`HOST` and `PORT` configure the TCP listener. Without either value or a `serverOptions` default, the server listens on port `3000`. + +```sh +HOST=127.0.0.1 PORT=4000 bun ./build +``` + +`SOCKET_PATH` selects a Unix domain socket instead. When it is present, TCP-only options are ignored: + +```sh +SOCKET_PATH=/tmp/sveltekit.sock bun ./build +``` + +`REUSE_PORT` enables Bun's `reusePort` option and `IPV6_ONLY` enables `ipv6Only`. Boolean variables accept `1`, `true`, `yes`, and `on`, or `0`, `false`, `no`, and `off`, without regard to letter case. + +### Request limits and diagnostics + +`BODY_SIZE_LIMIT` controls `Bun.serve`'s `maxRequestBodySize`. It defaults to `512K`. The value must resolve to a whole number of bytes and may use a case-insensitive binary `K`, `M`, or `G` suffix, such as `768K` or `1.5M`. `Infinity` disables the limit. + +`CONNECTION_IDLE_TIMEOUT` sets Bun's per-request inactivity timeout in seconds. It must be an integer from `0` through `255`; `0` disables the timeout. The generated handler disables the timeout for responses whose content type starts with `text/event-stream` and also adds `X-Accel-Buffering: no`. It is deliberately not called `IDLE_TIMEOUT`, which on adapter-node means something different (shut the server down after N seconds without requests). + +`DEVELOPMENT` enables Bun's development-mode error pages. It defaults to `false` for the generated server. + +### Public origin behind a proxy + +If [`paths.origin`](configuration#paths) is configured, that value is the trusted origin for every request. Otherwise, the adapter derives the host from the `Host` header and assumes the scheme is `https`, since production deployments usually terminate TLS upstream. Configure `paths.origin` or `PROTOCOL_HEADER` if that assumption is wrong, for example when serving plain HTTP directly. + +Behind a trusted reverse proxy, `PROTOCOL_HEADER`, `HOST_HEADER`, and `PORT_HEADER` name headers that contain the public scheme, host, and port: + +```sh +PROTOCOL_HEADER=x-forwarded-proto \ +HOST_HEADER=x-forwarded-host \ +PORT_HEADER=x-forwarded-port \ +bun ./build +``` + +The protocol header must contain `http` or `https`, without a colon. The port header must contain a number. Invalid values produce a `400 Bad Request` response. A header that is present but empty is ignored in favour of the fallback. + +> [!CAUTION] Only trust forwarded headers when requests can reach the server through a proxy you control. A direct client can spoof these headers. + +### Client addresses behind a proxy + +[`event.getClientAddress()`](https://svelte.dev/docs/kit/@sveltejs-kit#RequestEvent) uses `server.requestIP(request).address` by default. Set `ADDRESS_HEADER` to the name of a trusted proxy header when the direct peer is a proxy: + +```sh +ADDRESS_HEADER=true-client-ip bun ./build +``` + +For `x-forwarded-for`, also set `XFF_DEPTH` to the number of trusted proxies. The default depth is `1`, and the adapter selects from the right side of the comma-separated list so client-supplied entries to the left cannot change the trusted result: + +```sh +ADDRESS_HEADER=x-forwarded-for XFF_DEPTH=2 bun ./build +``` + +`XFF_DEPTH` must be an integer of at least `1`. `getClientAddress()` throws if the configured header is absent or contains fewer addresses than the configured depth. + +## Platform API + +The request event's `platform` property exposes the Bun server instance: + +```js +/** @type {import('./$types').RequestHandler} */ +export function GET({ getClientAddress, platform }) { + return Response.json({ + address: getClientAddress(), + serverId: platform.server.id, + pendingRequests: platform.server.pendingRequests, + pendingWebSockets: platform.server.pendingWebSockets + }); +} +``` + +## Graceful shutdown + +On `SIGINT` or `SIGTERM`, the generated server calls `server.stop()`. Bun stops accepting new connections and the adapter waits for pending requests before emitting a `sveltekit:shutdown` process event with the signal name: + +```js +process.on('sveltekit:shutdown', async (reason) => { + await jobs.stop(); + await db.close(); +}); +``` + +Connections that are still open after `SHUTDOWN_TIMEOUT` seconds are closed forcefully, so idle connections such as open event streams cannot delay the shutdown indefinitely. The value must be a non-negative integer and defaults to `30`. + +Sending a second shutdown signal forces the process to exit with status `1`. diff --git a/documentation/docs/25-build-and-deploy/50-adapter-static.md b/documentation/docs/25-build-and-deploy/50-adapter-static.md index d6f5e8f249b7..28a8d7a9a084 100644 --- a/documentation/docs/25-build-and-deploy/50-adapter-static.md +++ b/documentation/docs/25-build-and-deploy/50-adapter-static.md @@ -8,29 +8,30 @@ This will prerender your entire site as a collection of static files. If you'd l ## Usage -Install with `npm i -D @sveltejs/adapter-static`, then add the adapter to your `svelte.config.js`: +Run [`npx sv add sveltekit-adapter="adapter:static"`](/docs/cli/sveltekit-adapter), or install with `npm i -D @sveltejs/adapter-static` and add the adapter to your `vite.config.js`: ```js // @errors: 2307 -/// file: svelte.config.js +/// file: vite.config.js import adapter from '@sveltejs/adapter-static'; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - kit: { - adapter: adapter({ - // default options are shown. On some platforms - // these options are set automatically — see below - pages: 'build', - assets: 'build', - fallback: undefined, - precompress: false, - strict: true +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [ + sveltekit({ + adapter: adapter({ + // default options are shown. On some platforms + // these options are set automatically — see below + pages: 'build', + assets: 'build', + fallback: undefined, + precompress: false, + strict: true + }) }) - } -}; - -export default config; + ] +}); ``` ...and add the [`prerender`](page-options#prerender) option to your root layout: @@ -57,17 +58,18 @@ On these platforms, you should omit the adapter options so that `adapter-static` ```js // @errors: 2307 -/// file: svelte.config.js +/// file: vite.config.js import adapter from '@sveltejs/adapter-static'; +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; -/** @type {import('@sveltejs/kit').Config} */ -const config = { - kit: { - adapter: adapter(---{...}---) - } -}; - -export default config; +export default defineConfig({ + plugins: [ + sveltekit({ + adapter: adapter(---{...}---) + }) + ] +}); ``` ## Options @@ -96,7 +98,7 @@ By default, `adapter-static` checks that either all pages and endpoints (if any) ## GitHub Pages -When building for [GitHub Pages](https://docs.github.com/en/pages/getting-started-with-github-pages/about-github-pages), if your repo name is not equivalent to `your-username.github.io`, make sure to update [`config.kit.paths.base`](configuration#paths) to match your repo name. This is because the site will be served from `https://your-username.github.io/your-repo-name` rather than from the root. +When building for [GitHub Pages](https://docs.github.com/en/pages/getting-started-with-github-pages/about-github-pages), if your repo name is not equivalent to `your-username.github.io`, make sure to update [`config.paths.base`](configuration#paths) to match your repo name. This is because the site will be served from `https://your-username.github.io/your-repo-name` rather than from the root. You'll also want to generate a fallback `404.html` page to replace the default 404 page shown by GitHub Pages. @@ -104,22 +106,24 @@ A config for GitHub Pages might look like the following: ```js // @errors: 2307 2322 -/// file: svelte.config.js +/// file: vite.config.js +import process from 'node:process'; import adapter from '@sveltejs/adapter-static'; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - kit: { - adapter: adapter({ - fallback: '404.html' - }), - paths: { - base: process.argv.includes('dev') ? '' : process.env.BASE_PATH - } - } -}; - -export default config; +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [ + sveltekit({ + adapter: adapter({ + fallback: '404.html' + }), + paths: { + base: process.argv.includes('dev') ? '' : process.env.BASE_PATH + }, + }) + ] +}); ``` You can use GitHub actions to automatically deploy your site to GitHub Pages when you make a change. Here's an example workflow: diff --git a/documentation/docs/25-build-and-deploy/55-single-page-apps.md b/documentation/docs/25-build-and-deploy/55-single-page-apps.md index 646f54eaf666..5314dda869f8 100644 --- a/documentation/docs/25-build-and-deploy/55-single-page-apps.md +++ b/documentation/docs/25-build-and-deploy/55-single-page-apps.md @@ -16,23 +16,24 @@ First, disable SSR for the pages you don't want to prerender. These pages will b export const ssr = false; ``` -If you don't have any server-side logic (i.e. `+page.server.js`, `+layout.server.js` or `+server.js` files) you can use [`adapter-static`](adapter-static) to create your SPA. Install `adapter-static` with `npm i -D @sveltejs/adapter-static` and add it to your `svelte.config.js` with the `fallback` option: +If you don't have any server-side logic (i.e. `+page.server.js`, `+layout.server.js` or `+server.js` files) you can use [`adapter-static`](adapter-static) to create your SPA. Install `adapter-static` with `npm i -D @sveltejs/adapter-static` and add it to your `vite.config.js` with the `fallback` option: ```js // @errors: 2307 -/// file: svelte.config.js +/// file: vite.config.js import adapter from '@sveltejs/adapter-static'; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - kit: { - adapter: adapter({ - fallback: '200.html' // may differ from host to host +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [ + sveltekit({ + adapter: adapter({ + fallback: '200.html' // may differ from host to host + }) }) - } -}; - -export default config; + ] +}); ``` The `fallback` page is an HTML page created by SvelteKit from your page template (e.g. `app.html`) that loads your app and navigates to the correct route. For example [Surge](https://surge.sh/help/adding-a-200-page-for-client-side-routing), a static web host, lets you add a `200.html` file that will handle any requests that don't correspond to static assets or prerendered pages. diff --git a/documentation/docs/25-build-and-deploy/60-adapter-cloudflare.md b/documentation/docs/25-build-and-deploy/60-adapter-cloudflare.md index a1617fdffbca..693ccfd7df41 100644 --- a/documentation/docs/25-build-and-deploy/60-adapter-cloudflare.md +++ b/documentation/docs/25-build-and-deploy/60-adapter-cloudflare.md @@ -4,7 +4,7 @@ title: Cloudflare To deploy to [Cloudflare Workers](https://workers.cloudflare.com/) or [Cloudflare Pages](https://pages.cloudflare.com/), use [`adapter-cloudflare`](https://github.com/sveltejs/kit/tree/main/packages/adapter-cloudflare). -This adapter will be installed by default when you use [`adapter-auto`](adapter-auto). If you plan on staying with Cloudflare, you can switch from [`adapter-auto`](adapter-auto) to using this adapter directly so that `event.platform` is emulated during local development, type declarations are automatically applied, and the ability to set Cloudflare-specific options is provided. +This adapter will be installed by default when you use [`adapter-auto`](adapter-auto). If you plan on staying with Cloudflare, you can switch from [`adapter-auto`](adapter-auto) to using this adapter directly so that `cloudflare:workers` is emulated during local development, type declarations are automatically applied, and the ability to set Cloudflare-specific options is provided. ## Comparisons @@ -14,34 +14,35 @@ This adapter will be installed by default when you use [`adapter-auto`](adapter- ## Usage -Install with `npm i -D @sveltejs/adapter-cloudflare`, then add the adapter to your `svelte.config.js`: +Run [`npx sv add sveltekit-adapter="adapter:cloudflare"`](/docs/cli/sveltekit-adapter), or install with `npm i -D @sveltejs/adapter-cloudflare` and add the adapter to your `vite.config.js`: ```js // @errors: 2307 -/// file: svelte.config.js +/// file: vite.config.js import adapter from '@sveltejs/adapter-cloudflare'; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - kit: { - adapter: adapter({ - // See below for an explanation of these options - config: undefined, - platformProxy: { - configPath: undefined, - environment: undefined, - persist: undefined - }, - fallback: 'plaintext', - routes: { - include: ['/*'], - exclude: [''] - } +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [ + sveltekit({ + adapter: adapter({ + // See below for an explanation of these options + config: undefined, + platformProxy: { + configPath: undefined, + environment: undefined, + persist: undefined + }, + fallback: 'plaintext', + routes: { + include: ['/*'], + exclude: [''] + } + }) }) - } -}; - -export default config; + ] +}); ``` ## Options @@ -52,7 +53,7 @@ Path to your [Wrangler configuration file](https://developers.cloudflare.com/wor ### platformProxy -Preferences for the emulated `platform.env` local bindings. See the [getPlatformProxy](https://developers.cloudflare.com/workers/wrangler/api/#parameters-1) Wrangler API documentation for a full list of options. +Preferences for the emulated `env` local bindings. See the [getPlatformProxy](https://developers.cloudflare.com/workers/wrangler/api/#parameters-1) Wrangler API documentation for a full list of options. ### fallback @@ -129,56 +130,36 @@ Functions contained in the [`/functions` directory](https://developers.cloudflar ## Runtime APIs -The [`env`](https://developers.cloudflare.com/workers/runtime-apis/fetch-event#parameters) object contains your project's [bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/), which consist of KV/DO namespaces, etc. It is passed to SvelteKit via the `platform` property, along with [`ctx`](https://developers.cloudflare.com/workers/runtime-apis/context/), [`caches`](https://developers.cloudflare.com/workers/runtime-apis/cache/), and [`cf`](https://developers.cloudflare.com/workers/runtime-apis/request/#incomingrequestcfproperties), meaning that you can access it in hooks and endpoints: +The [`env`](https://developers.cloudflare.com/workers/runtime-apis/fetch-event#parameters) object contains your project's [bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/), which consist of KV/DO namespaces, etc. It is available on the [`cloudflare:workers`](https://developers.cloudflare.com/workers/runtime-apis/bindings/#importing-env-as-a-global) module. ```js +/// file: +server.js // @filename: ambient.d.ts -import { DurableObjectNamespace } from '@cloudflare/workers-types'; - -declare global { - namespace App { - interface Platform { - env: { - YOUR_DURABLE_OBJECT_NAMESPACE: DurableObjectNamespace; - }; - } - } +declare module 'cloudflare:workers' { + export const env: { + YOUR_DURABLE_OBJECT_NAMESPACE: import('@cloudflare/workers-types').DurableObjectNamespace; + }; } // @filename: +server.js // ---cut--- // @errors: 2355 2322 -/// file: +server.js +import { env } from 'cloudflare:workers'; + /** @type {import('./$types').RequestHandler} */ -export async function POST({ request, platform }) { - const x = platform?.env.YOUR_DURABLE_OBJECT_NAMESPACE.idFromName('x'); +export async function POST() { + const x = env.YOUR_DURABLE_OBJECT_NAMESPACE.idFromName('x'); } ``` -> [!NOTE] SvelteKit's built-in [`$env` module]($env-static-private) should be preferred for environment variables. - -To make these types available to your app, install [`@cloudflare/workers-types`](https://www.npmjs.com/package/@cloudflare/workers-types) and reference them in your `src/app.d.ts`: - -```ts -/// file: src/app.d.ts -+++import { KVNamespace, DurableObjectNamespace } from '@cloudflare/workers-types';+++ +> [!NOTE] SvelteKit's built-in [`$app/env/*` modules](environment-variables) should be preferred for environment variables. -declare global { - namespace App { - interface Platform { -+++ env: { - YOUR_KV_NAMESPACE: KVNamespace; - YOUR_DURABLE_OBJECT_NAMESPACE: DurableObjectNamespace; - };+++ - } - } -} - -export {}; -``` +To make these types available to your app, install [`wrangler`](https://www.npmjs.com/package/wrangler) and run [`wrangler types`](https://developers.cloudflare.com/workers/languages/typescript/). ### Testing locally -Cloudflare specific values in the `platform` property are emulated during dev and preview modes. Local [bindings](https://developers.cloudflare.com/workers/wrangler/configuration/#bindings) are created based on your [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/) and are used to populate `platform.env` during development and preview. Use the adapter config [`platformProxy` option](#Options-platformProxy) to change your preferences for the bindings. +Cloudflare specific values are emulated during dev and preview modes. Local [bindings](https://developers.cloudflare.com/workers/wrangler/configuration/#bindings) are created based on your [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/) and are used to populate `env` during development and preview. Use the adapter config [`platformProxy` option](#Options-platformProxy) to change your preferences for the bindings. + +> [!NOTE] [Durable Objects](https://developers.cloudflare.com/durable-objects/) and [Workflows](https://developers.cloudflare.com/workflows/), which require custom classes to be exported from your worker, are not currently supported. For testing the build, you should use [Wrangler](https://developers.cloudflare.com/workers/wrangler/) version 4. Once you have built your site, run `wrangler dev .svelte-kit/cloudflare/_worker.js` if you're testing for Cloudflare Workers or `wrangler pages dev .svelte-kit/cloudflare` for Cloudflare Pages. @@ -217,22 +198,20 @@ Alternatively, you can [prerender](page-options#prerender) the routes in questio Cloudflare no longer recommends using [Workers Sites](https://developers.cloudflare.com/workers/configuration/sites/configuration/) and instead recommends using [Workers Static Assets](https://developers.cloudflare.com/workers/static-assets/). To migrate, replace `@sveltejs/adapter-cloudflare-workers` with `@sveltejs/adapter-cloudflare` and remove all `site` configuration settings from your Wrangler configuration file, then add the `assets.directory` and `assets.binding` configuration settings: -### svelte.config.js - ```js // @errors: 2307 -/// file: svelte.config.js ----import adapter from '@sveltejs/adapter-cloudflare-workers';--- +/// file: vite.config.js +++import adapter from '@sveltejs/adapter-cloudflare';+++ +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; -/** @type {import('@sveltejs/kit').Config} */ -const config = { - kit: { - adapter: adapter() - } -}; - -export default config; +export default defineConfig({ + plugins: [ + sveltekit({ + +++adapter: adapter()+++ + }) + ] +}); ``` ### wrangler.toml diff --git a/documentation/docs/25-build-and-deploy/70-adapter-cloudflare-workers.md b/documentation/docs/25-build-and-deploy/70-adapter-cloudflare-workers.md index 418a7e198adf..ab0dd1192b58 100644 --- a/documentation/docs/25-build-and-deploy/70-adapter-cloudflare-workers.md +++ b/documentation/docs/25-build-and-deploy/70-adapter-cloudflare-workers.md @@ -8,23 +8,24 @@ To deploy to [Cloudflare Workers](https://workers.cloudflare.com/) with [Workers ## Usage -Install with `npm i -D @sveltejs/adapter-cloudflare-workers`, then add the adapter to your `svelte.config.js`: +Install with `npm i -D @sveltejs/adapter-cloudflare-workers`, then add the adapter to your `vite.config.js`: ```js // @errors: 2307 -/// file: svelte.config.js +/// file: vite.config.js +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; import adapter from '@sveltejs/adapter-cloudflare-workers'; -/** @type {import('@sveltejs/kit').Config} */ -const config = { - kit: { - adapter: adapter({ - // see below for options that can be set here +export default defineConfig({ + plugins: [ + sveltekit({ ++++ adapter: adapter({ + // see below for options that can be set here + })+++ }) - } -}; - -export default config; + ] +}); ``` ## Options @@ -105,7 +106,7 @@ export async function POST({ request, platform }) { } ``` -> [!NOTE] SvelteKit's built-in [`$env` module]($env-static-private) should be preferred for environment variables. +> [!NOTE] SvelteKit's built-in [`$app/env/*` modules](environment-variables) should be preferred for environment variables. To make these types available to your app, install [`@cloudflare/workers-types`](https://www.npmjs.com/package/@cloudflare/workers-types) and reference them in your `src/app.d.ts`: diff --git a/documentation/docs/25-build-and-deploy/80-adapter-netlify.md b/documentation/docs/25-build-and-deploy/80-adapter-netlify.md index 9316eaf7af7d..088eb8d2d16c 100644 --- a/documentation/docs/25-build-and-deploy/80-adapter-netlify.md +++ b/documentation/docs/25-build-and-deploy/80-adapter-netlify.md @@ -8,69 +8,42 @@ This adapter will be installed by default when you use [`adapter-auto`](adapter- ## Usage -Install with `npm i -D @sveltejs/adapter-netlify`, then add the adapter to your `svelte.config.js`: +Run [`npx sv add sveltekit-adapter="adapter:netlify"`](/docs/cli/sveltekit-adapter), or install with `npm i -D @sveltejs/adapter-netlify` and add the adapter to your `vite.config.js`: ```js // @errors: 2307 -/// file: svelte.config.js +/// file: vite.config.js import adapter from '@sveltejs/adapter-netlify'; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - kit: { - // default options are shown - adapter: adapter({ - // if true, will create a Netlify Edge Function rather - // than using standard Node-based functions - edge: false, - - // if true, will split your app into multiple functions - // instead of creating a single one for the entire app. - // if `edge` is true, this option cannot be used - split: false +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [ + sveltekit({ + adapter: adapter({ + // See below for an explanation of these options + edge: false, + split: false, + publish: 'build' + }) }) - } -}; - -export default config; + ] +}); ``` -Then, make sure you have a [netlify.toml](https://docs.netlify.com/configure-builds/file-based-configuration) file in the project root. This will determine where to write static assets based on the `build.publish` settings, as per this sample configuration: - -```toml -[build] - command = "npm run build" - publish = "build" -``` +## Options -If the `netlify.toml` file or the `build.publish` value is missing, a default value of `"build"` will be used. Note that if you have set the publish directory in the Netlify UI to something else then you will need to set it in `netlify.toml` too, or use the default value of `"build"`. +### `edge` -### Node version +If `true`, your app will be deployed as a [Netlify Edge Function](https://docs.netlify.com/build/edge-functions/overview/) rather than the standard Node-based function. -New projects will use the current Node LTS version by default. However, if you're upgrading a project you created a while ago it may be stuck on an older version. See [the Netlify docs](https://docs.netlify.com/configure-builds/manage-dependencies/#node-js-and-javascript) for details on manually specifying a current Node version. +### `split` -## Netlify Edge Functions +If `true`, your app will be split into multiple functions instead of a single one for the entire app. -SvelteKit supports [Netlify Edge Functions](https://docs.netlify.com/build/edge-functions/overview/). If you pass the option `edge: true` to the `adapter` function, server-side rendering will happen in a Deno-based edge function that's deployed close to the site visitor. If set to `false` (the default), the site will deploy to Node-based Netlify Functions. +### `publish` -```js -// @errors: 2307 -/// file: svelte.config.js -import adapter from '@sveltejs/adapter-netlify'; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - kit: { - adapter: adapter({ - // will create a Netlify Edge Function using Deno-based - // rather than using standard Node-based functions - edge: true - }) - } -}; - -export default config; -``` +The directory to write prerendered pages and static assets (the contents of `static`, plus client-side JS and CSS generated by SvelteKit) to. It defaults to `build`. ## Netlify alternatives to SvelteKit functionality @@ -83,7 +56,7 @@ The [`_headers`](https://docs.netlify.com/routing/headers/#syntax-for-the-header ### Netlify Forms 1. Create your Netlify HTML form as described [here](https://docs.netlify.com/forms/setup/#html-forms), e.g. as `/routes/contact/+page.svelte`. (Don't forget to add the hidden `form-name` input element!) -2. Netlify's build bot parses your HTML files at deploy time, which means your form must be [prerendered](page-options#prerender) as HTML. You can either add `export const prerender = true` to your `contact.svelte` to prerender just that page or set the `kit.prerender.force: true` option to prerender all pages. +2. Netlify's build bot parses your HTML files at deploy time, which means your form must be [prerendered](page-options#prerender) as HTML. You can either add `export const prerender = true` to your `contact.svelte` to prerender just that page or set the `prerender.force: true` option to prerender all pages. 3. If your Netlify form has a [custom success message](https://docs.netlify.com/forms/setup/#success-messages) like `` then ensure the corresponding `/routes/success/+page.svelte` exists and is prerendered. ### Netlify Functions diff --git a/documentation/docs/25-build-and-deploy/90-adapter-vercel.md b/documentation/docs/25-build-and-deploy/90-adapter-vercel.md index 50f25f4a9299..3641abe2c239 100644 --- a/documentation/docs/25-build-and-deploy/90-adapter-vercel.md +++ b/documentation/docs/25-build-and-deploy/90-adapter-vercel.md @@ -6,24 +6,28 @@ To deploy to Vercel, use [`adapter-vercel`](https://github.com/sveltejs/kit/tree This adapter will be installed by default when you use [`adapter-auto`](adapter-auto), but adding it to your project allows you to specify Vercel-specific options. +If you'd like to try out SvelteKit on Vercel, you can start from one of the [Vercel SvelteKit templates](https://vercel.com/templates/svelte?search=SvelteKit). + ## Usage -Install with `npm i -D @sveltejs/adapter-vercel`, then add the adapter to your `svelte.config.js`: +Run [`npx sv add sveltekit-adapter="adapter:vercel"`](/docs/cli/sveltekit-adapter), or install with `npm i -D @sveltejs/adapter-vercel` and add the adapter to your `vite.config.js`: ```js -/// file: svelte.config.js +// @errors: 2554 +/// file: vite.config.js import adapter from '@sveltejs/adapter-vercel'; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - kit: { - adapter: adapter({ - // see below for options that can be set here +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [ + sveltekit({ + adapter: adapter({ + // see below for options that can be set here + }) }) - } -}; - -export default config; + ] +}); ``` ## Deployment configuration @@ -40,17 +44,11 @@ export const config = { }; ``` -The following options apply to all functions: +You can set the following options: -- `runtime`: `'edge'`, `'nodejs20.x'` or `'nodejs22.x'`. By default, the adapter will select the `'nodejs.x'` corresponding to the Node version your project is configured to use on the Vercel dashboard - > [!NOTE] This option is deprecated and will be removed in a future version, at which point all your functions will use whichever Node version is specified in the project configuration on Vercel -- `regions`: an array of [edge network regions](https://vercel.com/docs/concepts/edge-network/regions) (defaulting to `["iad1"]` for serverless functions) or `'all'` if `runtime` is `edge` (its default). Note that multiple regions for serverless functions are only supported on Enterprise plans +- `runtime`: `'nodejs22.x'`, `'nodejs24.x'` or `bun1.x`. By default, the adapter will select the runtime used for the build, which corresponds to the Node version your project is configured to use on the Vercel dashboard unless you [build the app with Bun](https://bun.com/docs/guides/ecosystem/vite) +- `regions`: an array of [edge network regions](https://vercel.com/docs/concepts/edge-network/regions) (defaulting to `["iad1"]`). Note that multiple regions for serverless functions are only supported on Enterprise plans - `split`: if `true`, causes a route to be deployed as an individual function. If `split` is set to `true` at the adapter level, all routes will be deployed as individual functions - -Additionally, the following option applies to edge functions: -- `external`: an array of dependencies that esbuild should treat as external when bundling functions. This should only be used to exclude optional dependencies that will not run outside Node - -And the following option apply to serverless functions: - `memory`: the amount of memory available to the function. Defaults to `1024` Mb, and can be decreased to `128` Mb or [increased](https://vercel.com/docs/concepts/limits/overview#serverless-function-memory) in 64Mb increments up to `3008` Mb on Pro or Enterprise accounts - `maxDuration`: [maximum execution duration](https://vercel.com/docs/functions/runtimes#max-duration) of the function. Defaults to `10` seconds for Hobby accounts, `15` for Pro and `900` for Enterprise - `isr`: configuration Incremental Static Regeneration, described below @@ -64,24 +62,26 @@ If your functions need to access data in a specific region, it's recommended tha You may set the `images` config to control how Vercel builds your images. See the [image configuration reference](https://vercel.com/docs/build-output-api/v3/configuration#images) for full details. As an example, you may set: ```js -/// file: svelte.config.js +// @errors: 2554 +/// file: vite.config.js import adapter from '@sveltejs/adapter-vercel'; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - kit: { - adapter: adapter({ - images: { - sizes: [640, 828, 1200, 1920, 3840], - formats: ['image/avif', 'image/webp'], - minimumCacheTTL: 300, - domains: ['example-app.vercel.app'], - } +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [ + sveltekit({ + adapter: adapter({ + images: { + sizes: [640, 828, 1200, 1920, 3840], + formats: ['image/avif', 'image/webp'], + minimumCacheTTL: 300, + domains: ['example-app.vercel.app'], + } + }) }) - } -}; - -export default config; + ] +}); ``` ## Incremental Static Regeneration @@ -93,7 +93,13 @@ Vercel supports [Incremental Static Regeneration](https://vercel.com/docs/increm To add ISR to a route, include the `isr` property in your `config` object: ```js -import { BYPASS_TOKEN } from '$env/static/private'; +// @filename: env.d.ts +declare module '$app/env/private' { + export const BYPASS_TOKEN: string; +} +// @filename: +page.server.js +// ---cut--- +import { BYPASS_TOKEN } from '$app/env/private'; /** @type {import('@sveltejs/adapter-vercel').Config} */ export const config = { @@ -115,9 +121,12 @@ The expiration time (in seconds) before the cached asset will be re-generated by ### bypassToken -A random token that can be provided in the URL to bypass the cached version of the asset, by requesting the asset with a `__prerender_bypass=` cookie. +A random token that can be used for two things: + +- Draft mode, which bypasses the cache so that you can, for example, view live content from your CMS as you edit it. Send the token in a `__prerender_bypass=` cookie to render the route at request time. See [Vercel's Draft Mode documentation](https://vercel.com/docs/draft-mode?framework=sveltekit) for more details. +- Revalidation, which empties the route's ISR cache and repopulates it for future visitors. To do so, send a `GET` or `HEAD` request with a `x-prerender-revalidate: ` header. -Making a `GET` or `HEAD` request with `x-prerender-revalidate: ` will force the asset to be re-validated. +The `BYPASS_TOKEN` is saved at build time; setting or changing it after the application has been built requires a new deployment. Note that the `BYPASS_TOKEN` string must be at least 32 characters long. You could generate one using the JavaScript console like so: @@ -141,11 +150,17 @@ A list of valid query parameters that contribute to the cache key. Other paramet ## Environment variables -Vercel makes a set of [deployment-specific environment variables](https://vercel.com/docs/concepts/projects/environment-variables#system-environment-variables) available. Like other environment variables, these are accessible from `$env/static/private` and `$env/dynamic/private` (sometimes — more on that later), and inaccessible from their public counterparts. To access one of these variables from the client: +Vercel makes a set of [deployment-specific environment variables](https://vercel.com/docs/concepts/projects/environment-variables#system-environment-variables) available. Like other environment variables, these are accessible from `$app/env/private` if explicitly defined in `src/env.ts`. To access one of these variables from the client: ```js /// file: +layout.server.js -import { VERCEL_COMMIT_REF } from '$env/static/private'; +// @filename: env.d.ts +declare module '$app/env/private' { + export const VERCEL_COMMIT_REF: string; +} +// @filename: +layout.server.js +// ---cut--- +import { VERCEL_COMMIT_REF } from '$app/env/private'; /** @type {import('./$types').LayoutServerLoad} */ export function load() { @@ -165,7 +180,7 @@ export function load() {

This staging environment was deployed from {data.deploymentGitBranch}.

``` -Since all of these variables are unchanged between build time and run time when building on Vercel, we recommend using `$env/static/private` — which will statically replace the variables, enabling optimisations like dead code elimination — rather than `$env/dynamic/private`. +Since all of these variables are unchanged between build time and run time when building on Vercel, we recommend configuring the variable with `static: true` — which will statically replace the variables, enabling optimisations like dead code elimination. ## Skew protection @@ -189,16 +204,14 @@ If you have Vercel functions contained in the `api` directory at the project's r Projects created before a certain date may default to using an older Node version than what SvelteKit currently requires. You can [change the Node version in your project settings](https://vercel.com/docs/concepts/functions/serverless-functions/runtimes/node-js#node.js-version). +### More information + +You can find more information on how to use SvelteKit on Vercel [in the Vercel documentation](https://vercel.com/docs/frameworks/full-stack/sveltekit). + ## Troubleshooting ### Accessing the file system -You can't use `fs` in edge functions. - -You _can_ use it in serverless functions, but it won't work as expected, since files are not copied from your project into your deployment. Instead, use the [`read`]($app-server#read) function from `$app/server` to access your files. It also works inside routes deployed as edge functions by fetching the file from the deployed public assets location. +Using `node:fs` directly in serverless functions most likely won't work as you expect, since files are not copied from your project into your deployment. Instead, use the [`read`]($app-server#read) function from `$app/server` to access your files. Alternatively, you can [prerender](page-options#prerender) the routes in question. - -### Deployment protection - -If using [`read`]($app-server#read) in an edge function, SvelteKit will `fetch` the file in question from your deployment. If you are using [Deployment Protection](https://vercel.com/docs/deployment-protection), you must also enable [Protection Bypass for Automation](https://vercel.com/docs/deployment-protection/methods-to-bypass-deployment-protection/protection-bypass-automation) so that the request does not result in a [401 Unauthorized](https://http.dog/401) response. diff --git a/documentation/docs/25-build-and-deploy/99-writing-adapters.md b/documentation/docs/25-build-and-deploy/99-writing-adapters.md index 931420b8fb8f..6ca9156c3bbe 100644 --- a/documentation/docs/25-build-and-deploy/99-writing-adapters.md +++ b/documentation/docs/25-build-and-deploy/99-writing-adapters.md @@ -13,6 +13,8 @@ type AdapterSpecificOptions = any; // @filename: index.js // ---cut--- +import { getRequest, setResponse } from '@sveltejs/kit/node'; + /** @param {AdapterSpecificOptions} options */ export default function (options) { /** @type {import('@sveltejs/kit').Adapter} */ @@ -39,6 +41,22 @@ export default function (options) { // Return `true` if this adapter supports loading `instrumentation.server.js`. // Return `false if it can't, or throw a descriptive error. } + }, + vite: { + getRequest(options) { + const request = getRequest(options); + // modify the Request object here if needed + return request; + }, + setResponse(res, response) { + // handle WebSockets here, for example + setResponse(res, response); + }, + plugins: { + // add plugins here to integrate with Vite + pre: [], + post: [] + } } }; @@ -46,18 +64,17 @@ export default function (options) { } ``` -Of these, `name` and `adapt` are required. `emulate` and `supports` are optional. +Of these, `name` and `adapt` are required. `emulate`, `vite`, and `supports` are optional. Within the `adapt` method, there are a number of things that an adapter should do: - Clear out the build directory - Write SvelteKit output with `builder.writeClient`, `builder.writeServer`, and `builder.writePrerendered` - Output code that: - - Imports `Server` from `${builder.getServerDirectory()}/index.js` - - Instantiates the app with a manifest generated with `builder.generateManifest({ relativePath })` + - Imports a `server` instance generated by `${builder.generateServerInstance()}` + - Runs `await server.init({ env, read })` - Listens for requests from the platform, converts them to a standard [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) if necessary, calls the `server.respond(request, { getClientAddress })` function to generate a [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) and responds with it - expose any platform-specific information to SvelteKit via the `platform` option passed to `server.respond` - - Globally shims `fetch` to work on the target platform, if necessary. SvelteKit provides a `@sveltejs/kit/node/polyfills` helper for platforms that can use `undici` - Bundle the output to avoid needing to install dependencies on the target platform, if necessary - Put the user's static files and the generated JS/CSS in the correct location for the target platform diff --git a/documentation/docs/30-advanced/10-advanced-routing.md b/documentation/docs/30-advanced/10-advanced-routing.md index 8b5ddba0a7e1..22656e8aa3bf 100644 --- a/documentation/docs/30-advanced/10-advanced-routing.md +++ b/documentation/docs/30-advanced/10-advanced-routing.md @@ -61,7 +61,7 @@ export function load(event) { } ``` -> [!NOTE] If you don't handle 404 cases, they will appear in [`handleError`](hooks#handleError) +> [!NOTE] If you don't handle 404 cases, they will appear in [`handleError`](hooks#handleError) as [framework errors](errors#Framework-errors), with `kind: 'framework'`. Otherwise, they will appear with `kind: 'app'`. ## Optional parameters @@ -71,21 +71,18 @@ Note that an optional route parameter cannot follow a rest parameter (`[...rest] ## Matching -A route like `src/routes/fruits/[page]` would match `/fruits/apple`, but it would also match `/fruits/rocketship`. We don't want that. You can ensure that route parameters are well-formed by adding a _matcher_ — which takes the parameter string (`"apple"` or `"rocketship"`) and returns `true` if it is valid — to your `src/params` directory... +A route like `src/routes/fruits/[page]` would match `/fruits/apple`, but it would also match `/fruits/rocketship`. We don't want that. You can ensure that route parameters are well-formed by adding a _matcher_ to your `src/params.js` file (or `src/params.ts`)... ```js -// TODO: remove the ts error once kit 3 docs are main -// ---cut--- -// @errors: 1360 -/// file: src/params/fruit.js -/** - * @param {string} param - * @return {param is ('apple' | 'orange')} - * @satisfies {import('@sveltejs/kit').ParamMatcher} - */ -export function match(param) { - return param === 'apple' || param === 'orange'; -} +/// file: src/params.js +import { defineParams } from '@sveltejs/kit/params'; + +export const params = defineParams({ + fruit: (param) => { + if (param !== 'apple' && param !== 'orange') return; + return param; + } +}); ``` ...and augmenting your routes: @@ -94,12 +91,42 @@ export function match(param) { src/routes/fruits/[page+++=fruit+++] ``` -If the pathname doesn't match, SvelteKit will try to match other routes (using the sort order specified below), before eventually returning a 404. +If the pathname doesn't match, SvelteKit will try to match other routes (using the sort order specified below), before eventually returning a 404. If it does match, the returned value is passed as the param value. + +You can also use a [Standard Schema](https://standardschema.dev) — for example with [Valibot](https://valibot.dev): + +```js +/// file: src/params.js +import { defineParams } from '@sveltejs/kit/params'; +import * as v from 'valibot'; + +export const params = defineParams({ + number: v.pipe(v.string(), v.toNumber()) +}); +``` + +If validation fails, the route does not match. If it succeeds, the param is typed with the output type of the schema, which must extend `string`, `boolean`, `number` (as in the example above) or `bigint`: + +```js +/// file: src/routes/items/[id=number]/+page.js +/** @type {import('./$types').PageLoad} */ +export function load({ params }) { + console.log(typeof params.id); // 'number' +} +``` + +Transformations should be _symmetrical_ — if converting to a number, then calling `toString()` on that number should return the original string. This allows the [`resolve`]($app-paths#resolve) function to construct a pathname correctly: -Each module in the `params` directory corresponds to a matcher, with the exception of `*.test.js` and `*.spec.js` files which may be used to unit test your matchers. +```js +import { resolve } from '$app/paths'; + +resolve('/blog/[id=number]', { id: 1 }); +``` > [!NOTE] Matchers run both on the server and in the browser. +> [!NOTE] Prior to SvelteKit 3, you had to define each param matcher in a separate file, all listed under a `params` folder (for example `src/params/foo.js` with `export const match = (param) => param === 'foo';`), and matching was determined by whether or not the matcher returns a truthy value (which means no value transformation took place). + ## Sorting It's possible for multiple routes to match a given path. For example each of these routes would match `/foo-abc`: @@ -262,7 +289,7 @@ Not all use cases are suited for layout grouping, nor should you feel compelled ```svelte @@ -274,12 +301,12 @@ Not all use cases are suited for layout grouping, nor should you feel compelled ```js /// file: src/routes/nested/route/+layout.js // @filename: ambient.d.ts -declare module "$lib/reusable-load-function" { +declare module "#lib/reusable-load-function.js" { export function reusableLoad(event: import('@sveltejs/kit').LoadEvent): Promise>; } // @filename: index.js // ---cut--- -import { reusableLoad } from '$lib/reusable-load-function'; +import { reusableLoad } from '#lib/reusable-load-function.js'; /** @type {import('./$types').PageLoad} */ export function load(event) { diff --git a/documentation/docs/30-advanced/20-hooks.md b/documentation/docs/30-advanced/20-hooks.md index a157dd699d6e..cb5c65e37500 100644 --- a/documentation/docs/30-advanced/20-hooks.md +++ b/documentation/docs/30-advanced/20-hooks.md @@ -20,7 +20,7 @@ This function runs every time the SvelteKit server receives a [request](web-stan ```js /// file: src/hooks.server.js -/** @type {import('@sveltejs/kit').Handle} */ +/** @type {import('@sveltejs/kit/hooks').Handle} */ export async function handle({ event, resolve }) { if (event.url.pathname.startsWith('/custom')) { return new Response('custom response'); @@ -45,11 +45,11 @@ You can define multiple `handle` functions and execute them with the [`sequence` - `transformPageChunk(opts: { html: string, done: boolean }): MaybePromise` — applies custom transforms to HTML. If `done` is true, it's the final chunk. Chunks are not guaranteed to be well-formed HTML (they could include an element's opening tag but not its closing tag, for example) but they will always be split at sensible boundaries such as `%sveltekit.head%` or layout/page components. - `filterSerializedResponseHeaders(name: string, value: string): boolean` — determines which headers should be included in serialized responses when a `load` function loads a resource with `fetch`. By default, none will be included. -- `preload(input: { type: 'js' | 'css' | 'font' | 'asset', path: string }): boolean` — determines which files should be preloaded. Files are preloaded via `` tags added to the `` tag; if [`output.linkHeaderPreload`](configuration#output) is enabled, dynamically rendered pages use the [`Link` response header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link) instead. The method is called with each file that was found at build time while constructing the code chunks — so if you for example have `import './styles.css` in your `+page.svelte`, `preload` will be called with the resolved path to that CSS file when visiting that page. Note that in dev mode `preload` is _not_ called, since it depends on analysis that happens at build time. Preloading can improve performance by downloading assets sooner, but it can also hurt if too much is downloaded unnecessarily. By default, `js` and `css` files will be preloaded. `asset` files are not preloaded at all currently, but we may add this later after evaluating feedback. +- `preload(input: { type: 'js' | 'css' | 'asset', path: string } | { type: 'font', path: string, filename: string }): boolean` — determines which files should be preloaded. Files are preloaded via `` tags added to the `` tag; if [`output.linkHeaderPreload`](configuration#output) is enabled, dynamically rendered pages use the [`Link` response header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link) instead. The method is called with each file that was found at build time while constructing the code chunks — so if you for example have `import './styles.css` in your `+page.svelte`, `preload` will be called with the resolved path to that CSS file when visiting that page. Note that in dev mode `preload` is _not_ called, since it depends on analysis that happens at build time. Preloading can improve performance by downloading assets sooner, but it can also hurt if too much is downloaded unnecessarily. By default, `js` and `css` files will be preloaded. `asset` files are not preloaded at all currently, but we may add this later after evaluating feedback. For `font` files, `input` also has a `filename` property, the source file's pathname relative to the project root, so that a filter can match on it instead of the hashed path. ```js /// file: src/hooks.server.js -/** @type {import('@sveltejs/kit').Handle} */ +/** @type {import('@sveltejs/kit/hooks').Handle} */ export async function handle({ event, resolve }) { const response = await resolve(event, { transformPageChunk: ({ html }) => html.replace('old', 'new'), @@ -84,7 +84,7 @@ const getUserInformation: (cookie: string | void) => Promise; // @filename: index.js // ---cut--- -/** @type {import('@sveltejs/kit').Handle} */ +/** @type {import('@sveltejs/kit/hooks').Handle} */ export async function handle({ event, resolve }) { event.locals.user = await getUserInformation(event.cookies.get('sessionid')); @@ -112,7 +112,7 @@ For example, your `load` function might make a request to a public URL like `htt ```js /// file: src/hooks.server.js -/** @type {import('@sveltejs/kit').HandleFetch} */ +/** @type {import('@sveltejs/kit/hooks').HandleFetch} */ export async function handleFetch({ request, fetch }) { if (request.url.startsWith('https://api.yourapp.com/')) { // clone the original request, but change the URL @@ -133,7 +133,7 @@ There is one caveat: if your app and your API are on sibling subdomains — `www ```js /// file: src/hooks.server.js // @errors: 2345 -/** @type {import('@sveltejs/kit').HandleFetch} */ +/** @type {import('@sveltejs/kit/hooks').HandleFetch} */ export async function handleFetch({ event, request, fetch }) { if (request.url.startsWith('https://api.my-domain.com/')) { request.headers.set('cookie', event.request.headers.get('cookie')); @@ -143,59 +143,46 @@ export async function handleFetch({ event, request, fetch }) { } ``` -## handleValidationError -> [!NOTE] Can be added to `src/hooks.server.js` - -This hook is called when a remote function is called with an argument that does not match the provided [Standard Schema](https://standardschema.dev/). It must return an object matching the shape of [`App.Error`](types#Error). -Say you have a remote function that expects a string as its argument ... - -```js -/// file: todos.remote.js -import * as v from 'valibot'; -import { query } from '$app/server'; - -export const getTodo = query(v.string(), (id) => { - // implementation... -}); -``` +## handleError -...but it is called with something that doesn't match the schema — such as a number (e.g. `await getTodos(1)`) — then validation will fail, the server will respond with a [400 status code](https://http.dog/400), and the function will throw with the message 'Bad Request'. +> [!NOTE] Can be added to `src/hooks.server.js` and `src/hooks.client.js` -To customise this message and add additional properties to the error object, implement `handleValidationError`: +This function is called for every error thrown while loading, rendering, or responding to a request. This allows for two things: -```js -/// file: src/hooks.server.js -/** @type {import('@sveltejs/kit').HandleValidationError} */ -export function handleValidationError({ issues }) { - return { - message: 'No thank you' - }; -} -``` +- you can log the error +- you can generate a custom representation of the error that is safe to show to users, omitting sensitive details like messages and stack traces -Be thoughtful about what information you expose here, as the most likely reason for validation to fail is that someone is sending malicious requests to your server. +Alongside the `event`, the hook receives a `kind` discriminant that tells you where the error came from, and the `error` itself: -## handleError +- `'app'` — the error came from your app via [`error(...)`](@sveltejs-kit#error) + - `error` is the error body, which matches [`App.Error`](types#Error) + - defaults to the error body itself +- `'framework'` — the error came from SvelteKit, such as a 404, 405 or 413 + - `error` is `{ status, message }`, where `message` is safe text like `Not Found` + - defaults to that same `{ status, message }` +- `'validation'` (server only) — the error came from validating a remote function argument against its [Standard Schema](https://standardschema.dev/) + - `error` is `{ status: 400, message: 'Bad Request' }`, and `issues` contains the validation issues + - defaults to the `error` object; the issues are not exposed unless you explicitly return them + - to access validation-library-specific issue properties, parameterise `HandleServerError` with the issue type, for example `HandleServerError` +- `'unknown'` — we don't know what went wrong; the error was thrown by your code, or code it calls + - `error` is the thrown value, which may contain information unsafe to expose + - defaults to `{ status: 500, message: 'Internal Error' }` -> [!NOTE] Can be added to `src/hooks.server.js` and `src/hooks.client.js` +The next section, [Errors](errors), explains the other categories in more detail. Redirects are not errors, and never reach the hook. -If an [unexpected error](errors#Unexpected-errors) is thrown during loading, rendering, or from an endpoint, this function will be called with the `error`, `event`, `status` code and `message`. This allows for two things: +The hook returns an object matching [`App.Error`](types#Error), in which `status` and `message` are optional — return them only to override the defaults in the list above. -- you can log the error -- you can generate a custom representation of the error that is safe to show to users, omitting sensitive details like messages and stack traces. The returned value, which defaults to `{ message }`, becomes the value of `page.error`. +> [!NOTE] If you augment `App.Error` with additional _required_ properties, the hook must return them. -For errors thrown from your code (or library code called by your code) the status will be 500 and the message will be "Internal Error". While `error.message` may contain sensitive information that should not be exposed to users, `message` is safe (albeit meaningless to the average user). - -To add more information to the `page.error` object in a type-safe way, you can customize the expected shape by declaring an `App.Error` interface (which must include `message: string`, to guarantee sensible fallback behavior). This allows you to — for example — append a tracking ID for users to quote in correspondence with your technical support staff: +To add more information to the `page.error` object in a type-safe way, augment the existing `App.Error` interface with your additional properties. The built-in `status` and `message` properties are already present and do not need to be redeclared. For example, you can add a tracking ID for users to quote when contacting support: ```ts /// file: src/app.d.ts declare global { namespace App { interface Error { - message: string; errorId: string; } } @@ -219,15 +206,29 @@ import * as Sentry from '@sentry/sveltekit'; Sentry.init({/*...*/}) -/** @type {import('@sveltejs/kit').HandleServerError} */ -export async function handleError({ error, event, status, message }) { +/** @type {import('@sveltejs/kit/hooks').HandleServerError} */ +export async function handleError({ kind, error, event }) { + if (kind === 'app') { + // you created this error with `error(...)`, so it already + // matches `App.Error` — pass it through unchanged + return error; + } + const errorId = crypto.randomUUID(); + if (kind === 'framework') { + // a 404 (or similar) — `error.status` and `error.message` are safe to + // expose, so we keep them and just add our own property + return { ...error, errorId }; + } + // example integration with https://sentry.io/ Sentry.captureException(error, { - extra: { event, errorId, status } + extra: { event, errorId } }); + // `status` and `message` are optional — we only override `message`, + // so the status stays at its default of 500 return { message: 'Whoops!', errorId @@ -250,13 +251,21 @@ import * as Sentry from '@sentry/sveltekit'; Sentry.init({/*...*/}) -/** @type {import('@sveltejs/kit').HandleClientError} */ -export async function handleError({ error, event, status, message }) { +/** @type {import('@sveltejs/kit/hooks').HandleClientError} */ +export async function handleError({ kind, error, event }) { + if (kind === 'app') { + return error; + } + const errorId = crypto.randomUUID(); + if (kind === 'framework') { + return { ...error, errorId }; + } + // example integration with https://sentry.io/ Sentry.captureException(error, { - extra: { event, errorId, status } + extra: { event, errorId } }); return { @@ -268,7 +277,7 @@ export async function handleError({ error, event, status, message }) { > [!NOTE] In `src/hooks.client.js`, the type of `handleError` is `HandleClientError` instead of `HandleServerError`, and `event` is a `NavigationEvent` rather than a `RequestEvent`. -This function is not called for _expected_ errors (those thrown with the [`error`](@sveltejs-kit#error) function imported from `@sveltejs/kit`). +Errors that were already transformed by the server-side hook are not passed to the client-side hook a second time. During development, if an error occurs because of a syntax error in your Svelte code, the passed in error has a `frame` property appended highlighting the location of the error. @@ -285,9 +294,9 @@ This function runs once, when the server is created or the app starts in the bro ```js // @errors: 2307 /// file: src/hooks.server.js -import * as db from '$lib/server/database'; +import * as db from '#lib/server/database.js'; -/** @type {import('@sveltejs/kit').ServerInit} */ +/** @type {import('@sveltejs/kit/hooks').ServerInit} */ export async function init() { await db.connect(); } @@ -315,7 +324,7 @@ const translated = { '/fr/a-propos': '/fr/about', }; -/** @type {import('@sveltejs/kit').Reroute} */ +/** @type {import('@sveltejs/kit/hooks').Reroute} */ export function reroute({ url }) { if (url.pathname in translated) { return translated[url.pathname]; @@ -333,7 +342,7 @@ Since version 2.18, the `reroute` hook can be asynchronous, allowing it to (for // @errors: 2345 2304 /// file: src/hooks.js -/** @type {import('@sveltejs/kit').Reroute} */ +/** @type {import('@sveltejs/kit/hooks').Reroute} */ export async function reroute({ url, fetch }) { // Ask a special endpoint within your app about the destination if (url.pathname === '/api/reroute') return; @@ -358,9 +367,9 @@ This is a collection of _transporters_, which allow you to pass custom types — ```js // @errors: 2307 /// file: src/hooks.js -import { Vector } from '$lib/math'; +import { Vector } from '#lib/math.js'; -/** @type {import('@sveltejs/kit').Transport} */ +/** @type {import('@sveltejs/kit/hooks').Transport} */ export const transport = { Vector: { encode: (value) => value instanceof Vector && [value.x, value.y], diff --git a/documentation/docs/30-advanced/25-errors.md b/documentation/docs/30-advanced/25-errors.md index 49f826ad1ec0..493b8d7a54fe 100644 --- a/documentation/docs/30-advanced/25-errors.md +++ b/documentation/docs/30-advanced/25-errors.md @@ -6,55 +6,52 @@ Errors are an inevitable fact of software development. SvelteKit handles errors ## Error objects -SvelteKit distinguishes between expected and unexpected errors, both of which are represented as simple `{ message: string }` objects by default. +Every error passes through the [`handleError`](hooks#handleError) hook — which can log it and customise it — before it is rendered. The hook's `kind` property identifies where the error came from: your app (`'app'`), SvelteKit (`'framework'`), validation of a [remote function](remote-functions) argument (`'validation'`) or an unknown source (`'unknown'`). By default, all are represented as simple `{ status: number, message: string }` objects. -You can add additional properties, like a `code` or a tracking `id`, as shown in the examples below. (When using TypeScript this requires you to redefine the `Error` type as described in [type safety](errors#Type-safety)). +You can add additional properties, like a `code` or a tracking `id`, as shown in the examples below. (When using TypeScript this requires you to redefine the `Error` type as described in [Type safety](errors#Type-safety) below). -## Expected errors +## App errors -An _expected_ error is one created with the [`error`](@sveltejs-kit#error) helper imported from `@sveltejs/kit`: +An app error is one thrown from your app code using the [`error`](@sveltejs-kit#error) function imported from `@sveltejs/kit`: ```js /// file: src/routes/blog/[slug]/+page.server.js // @filename: ambient.d.ts -declare module '$lib/server/database' { +declare module '#lib/server/database.js' { export function getPost(slug: string): Promise<{ title: string, content: string } | undefined> } // @filename: index.js // ---cut--- import { error } from '@sveltejs/kit'; -import * as db from '$lib/server/database'; +import * as db from '#lib/server/database.js'; /** @type {import('./$types').PageServerLoad} */ export async function load({ params }) { const post = await db.getPost(params.slug); if (!post) { - error(404, { - message: 'Not found' - }); + error(404, 'Not found'); } return { post }; } ``` -This throws an exception that SvelteKit catches, causing it to set the response status code to 404 and render an [`+error.svelte`](routing#error) component, where `page.error` is the object provided as the second argument to `error(...)`. +This throws an exception that SvelteKit catches, causing it to set the response status code to 404 and render an [`+error.svelte`](routing#error) component, where the `error` is an `App.Error` object with the provided `status` and `message`. + +On its way there, the error passes through the [`handleError`](hooks#handleError) hook with `kind: 'app'`. Since the shape of the error is determined by your app, it is considered safe to expose, and the hook can pass it through unchanged. ```svelte -

{page.error.message}

+

{error.message}

``` -> [!LEGACY] -> `$app/state` was added in SvelteKit 2.12. If you're using an earlier version or are using Svelte 4, use `$app/stores` instead. - -You can add extra properties to the error object if needed... +You can add extra properties to the error object if needed: ```js // @filename: ambient.d.ts @@ -71,79 +68,74 @@ export {} // @filename: index.js import { error } from '@sveltejs/kit'; // ---cut--- -error(404, { - message: 'Not found', +error(404, 'Not found', { +++code: 'NOT_FOUND'+++ }); ``` -...otherwise, for convenience, you can pass a string as the second argument: +> [!NOTE] [In SvelteKit 1.x](migrating-to-sveltekit-2#redirect-and-error-are-no-longer-thrown-by-you) you had to `throw` the `error` yourself -```js -import { error } from '@sveltejs/kit'; -// ---cut--- ----error(404, { message: 'Not found' });--- -+++error(404, 'Not found');+++ -``` +## Framework errors -> [!NOTE] [In SvelteKit 1.x](migrating-to-sveltekit-2#redirect-and-error-are-no-longer-thrown-by-you) you had to `throw` the `error` yourself +Some errors are generated by SvelteKit itself rather than by your code — a request that doesn't match any route (404), a `POST` request to a page without actions (405), a request body that exceeds the size limit (413), and so on. -## Unexpected errors +These also go through `handleError`, with `kind: 'framework'`. The `error` you receive is a `{ status, message }` object whose `message` is a terse but safe description of what went wrong, such as `'Not Found'`, so it can be exposed to users as-is. -An _unexpected_ error is any other exception that occurs while handling a request. Since these can contain sensitive information, unexpected error messages and stack traces are not exposed to users. +If you log errors inside `handleError`, remember that framework errors such as 404s are routine — you will generally want to avoid logging them. -By default, unexpected errors are printed to the console (or, in production, your server logs), while the error that is exposed to the user has a generic shape: +## Validation errors -```json -{ "message": "Internal Error" } -``` +Validation errors occur when a [remote function](remote-functions) is called with invalid data. When these are passed to `handleError`, they are accompanied by an array of `issues`. See [Handling validation errors](remote-functions#Handling-validation-errors) for more details. + +## Unknown errors -Unexpected errors will go through the [`handleError`](hooks#handleError) hook, where you can add your own error handling — for example, sending errors to a reporting service, or returning a custom error object which becomes `page.error`. +An _unknown_ error is any other exception that occurs while handling a request. Since these can contain sensitive information, unknown error messages and stack traces are not exposed to users. -## Rendering errors +By default, unknown errors are printed to the console (or, in production, your server logs), while the error that is exposed to the user has a generic shape: -Ordinarily, if an error happens during server-side rendering (for example inside a component's ` - -

{error.message}

-``` +## Error boundaries -The same applies for other error boundaries you define in your code: +Errors that occur during `load` or rendering (for example inside a component's ` @@ -27,8 +28,45 @@ To do this, export a `snapshot` object with `capture` and `restore` methods from ``` -When you navigate away from this page, the `capture` function is called immediately before the page updates, and the returned value is associated with the current entry in the browser's history stack. If you navigate back, the `restore` function is called with the stored value as soon as the page is updated. +When you navigate away from this page — including via [shallow routing](shallow-routing) — the `capture` function is called immediately before the page updates, and the returned value is associated with the current entry in the browser's history stack. If you navigate back, the `restore` function is called with the stored value as soon as the page is updated. + +Snapshots must have a unique ID in order to survive across component remounts and page reloads. By default, this is generated from the stack trace when `snapshot(...)` is called, but you can also explicitly provide an `id` to (for example) keep snapshots stable across deployments, even if the stack trace differs because of changes to the source code, or to distinguish snapshots created via a shared wrapper function or in multiple instances of the same component: + +```js +import { snapshot } from '$app/navigation'; + +let comment = ''; + +// ---cut--- +snapshot({ + +++id: 'comment',+++ + capture: () => comment, + restore: (value) => (comment = value) +}); +``` + +The optional `reset` callback runs on navigations where there is no captured value to restore, such as when a new history entry is created. -The data must be serializable as JSON so that it can be persisted to `sessionStorage`. This allows the state to be restored when the page is reloaded, or when the user navigates back from a different site. +Captured values are serialized with [devalue](https://github.com/sveltejs/devalue), which handles JSON, objects such as `Date` and `Map`, and custom types specified in your [`transport`](hooks#transport) hook. The serialized data is persisted to `sessionStorage`, which allows the state to be restored when the page is reloaded, or when the user navigates back from a different site. > [!NOTE] Avoid returning very large objects from `capture` — once captured, objects will be retained in memory for the duration of the session, and in extreme cases may be too large to persist to `sessionStorage`. + +## export const snapshot + +> [!LEGACY] +> Previously, snapshots were created by exporting a `snapshot` object with `capture` and `restore` methods from a `+page.svelte` or `+layout.svelte`. This form is deprecated in favour of the `snapshot` helper, which can be called from any component. + +```svelte + + +``` + +Values captured this way are serialized as JSON, and shallow navigations do not capture them. diff --git a/documentation/docs/30-advanced/67-shallow-routing.md b/documentation/docs/30-advanced/67-shallow-routing.md index b69cee744fb1..6af1b3e008b3 100644 --- a/documentation/docs/30-advanced/67-shallow-routing.md +++ b/documentation/docs/30-advanced/67-shallow-routing.md @@ -1,44 +1,121 @@ --- -title: Shallow routing +title: Page state & shallow routing --- -As you navigate around a SvelteKit app, you create _history entries_. Clicking the back and forward buttons traverses through this list of entries, re-running any `load` functions and replacing page components as necessary. +As you navigate around a SvelteKit app, you create _history entries_. Clicking the back and forward buttons traverses through this list of entries, re-running any `load` functions, replacing page components, and updating the scroll position and focused element as necessary. -Sometimes, it's useful to create history entries _without_ navigating. For example, you might want to show a modal dialog that the user can dismiss by navigating back. This is particularly valuable on mobile devices, where swipe gestures are often more natural than interacting directly with the UI. In these cases, a modal that is _not_ associated with a history entry can be a source of frustration, as a user may swipe backwards in an attempt to dismiss it and find themselves on the wrong page. +Sometimes, it's useful to create history entries _without_ performing a full navigation. We call this _shallow routing_. -SvelteKit makes this possible with the [`pushState`]($app-navigation#pushState) and [`replaceState`]($app-navigation#replaceState) functions, which allow you to associate state with a history entry without navigating. For example, to implement a history-driven modal: +For example, you might want to show a modal dialog that the user can dismiss by navigating back. This is particularly valuable on mobile devices, where swipe gestures are often more natural than interacting directly with the UI. In these cases, a modal that is _not_ associated with a history entry can be a source of frustration, as a user may swipe backwards in an attempt to dismiss it and find themselves on the wrong page. + +SvelteKit makes this possible with the [`goto`]($app-navigation#goto) function, which allows you to associate state with a history entry without navigating with the `shallow: true` option: ```svelte + + {#if page.state.showModal} history.back()} /> {/if} ``` +State is globally accessible via the [page object]($app-state#page) as `page.state`. You can make page state type-safe by declaring an [`App.PageState`](types#PageState) interface (usually in `src/app.d.ts`). + The modal can be dismissed by navigating back (unsetting `page.state.showModal`) or by interacting with it in a way that causes the `close` callback to run, which will navigate back programmatically. -## API +You can also update the contents of the browser's URL bar during a shallow navigation: + +```js +import { goto } from '$app/navigation'; +const state = { active: true }; +// ---cut--- +goto('/another/page', { + state, + shallow: true +}); +``` + +> [!NOTE] If the user reloads, they will land on `/another/page`, _not_ the currently rendered page — see [Caveats](#Caveats). + +Regardless of whether you choose to update the visible URL or not, [`beforeNavigate`]($app-navigation#beforeNavigate), [`onNavigate`]($app-navigation#onNavigate) and [`afterNavigate`]($app-navigation#afterNavigate) will run with `navigation.type === 'goto'` and `navigation.shallow === true`. + +Once shallow routing is active, `page.shallow` becomes a `{ url, params, route }` object describing the page that _would_ be rendered if the user were to navigate there (which would happen if, for example, they reloaded the page). `page.url`, `page.params` and `page.route` continue to describe the page that is currently rendered. + +```svelte + + + +

The user-visible URL is {page.shallow?.url.href ?? page.url.href}

+

The actual page you're on is {page.url.href}

-The first argument to `pushState` is the URL, relative to the current URL. To stay on the current URL, use `''`. + +``` -The second argument is the new page state, which can be accessed via the [page object]($app-state#page) as `page.state`. You can make page state type-safe by declaring an [`App.PageState`](types#PageState) interface (usually in `src/app.d.ts`). +A regular `goto` call without `shallow: true`, or a standard link click, exits shallow routing. -To set page state without creating a new history entry, use `replaceState` instead of `pushState`. +Navigating back or forward to a shallow entry restores its `page.state` and `page.shallow`. The rendered page, and therefore `page.url`, is still the one the user was on when `goto` was called. To navigate to the visible URL, call `goto(page.shallow.url)` without `shallow: true`. > [!LEGACY] -> `page.state` from `$app/state` was added in SvelteKit 2.12. If you're using an earlier version or are using Svelte 4, use `$page.state` from `$app/stores` instead. +> In SvelteKit 2 this functionality was achieved using `pushState` and `replaceState`, which are now deprecated. Use `goto` with `shallow: true` instead, and use the `replace` option when replacing the current history entry. + +## Routing options + +By default, the above examples create a new navigation entry in the history stack. If you don't want that, you can replace the existing navigation entry instead: + +```js +import { goto } from '$app/navigation'; +const url = new URL('https://example.com'); +const state = { showModal: true }; +// ---cut--- +goto(url, { + state, + replace: true +}); +``` + +By default, state set with `goto` is not restored after a reload. To change that, use `persistState`: + +```js +import { goto } from '$app/navigation'; +const url = new URL('https://example.com'); +const state = { showModal: true }; +// ---cut--- +goto(url, { + state, + persistState: true +}); +``` + +Shallow navigations preserve the current scroll position and focused element by default. You can opt out of this behavior with `reset: true`: + +```js +import { goto } from '$app/navigation'; +const url = new URL('https://example.com'); +// ---cut--- +goto(url, { + shallow: true, + reset: true +}); +``` + +> [!NOTE] +> `page.state` is only populated after JavaScript loads, which can cause flickering UI. Use it carefully. ## Loading data for a route @@ -49,7 +126,7 @@ For this to work, you need to load the data that the `+page.svelte` expects. A c ```svelte The project logo @@ -39,14 +39,15 @@ npm i -D @sveltejs/enhanced-img Adjust `vite.config.js`: ```js -import { sveltekit } from '@sveltejs/kit/vite'; +/// file: vite.config.js +++import { enhancedImages } from '@sveltejs/enhanced-img';+++ +import { sveltekit } from '@sveltejs/kit/vite'; import { defineConfig } from 'vite'; export default defineConfig({ plugins: [ +++enhancedImages(), // must come before the SvelteKit plugin+++ - sveltekit() + sveltekit({/* ... */}) ] }); ``` diff --git a/documentation/docs/40-best-practices/10-accessibility.md b/documentation/docs/40-best-practices/10-accessibility.md index 4c6b3d8be18e..5e174655d7bc 100644 --- a/documentation/docs/40-best-practices/10-accessibility.md +++ b/documentation/docs/40-best-practices/10-accessibility.md @@ -45,7 +45,7 @@ afterNavigate(() => { }); ``` -You can also programmatically navigate to a different page using the [`goto`]($app-navigation#goto) function. By default, this will have the same client-side routing behavior as clicking on a link. However, `goto` also accepts a `keepFocus` option that will preserve the currently-focused element instead of resetting focus. If you enable this option, make sure the currently-focused element still exists on the page after navigation. If the element no longer exists, the user's focus will be lost, making for a confusing experience for assistive technology users. +You can also programmatically navigate to a different page using the [`goto`]($app-navigation#goto) function. By default, this will have the same client-side routing behavior as clicking on a link. However, `goto` also accepts a `reset: false` option that will preserve the current scroll position and the currently-focused element instead of resetting them. If you use this option, make sure the currently-focused element still exists on the page after navigation. If the element no longer exists, the user's focus will be lost, making for a confusing experience for assistive technology users. ## The "lang" attribute @@ -73,7 +73,7 @@ export function get_lang(event: import('@sveltejs/kit').RequestEvent) { // @filename: hooks.server.js import { get_lang } from './utils'; // ---cut--- -/** @type {import('@sveltejs/kit').Handle} */ +/** @type {import('@sveltejs/kit/hooks').Handle} */ export function handle({ event, resolve }) { return resolve(event, { transformPageChunk: ({ html }) => html.replace('%lang%', get_lang(event)) diff --git a/documentation/docs/40-best-practices/20-seo.md b/documentation/docs/40-best-practices/20-seo.md index c878ba8d9e66..e630daf6b99c 100644 --- a/documentation/docs/40-best-practices/20-seo.md +++ b/documentation/docs/40-best-practices/20-seo.md @@ -56,95 +56,3 @@ export async function GET() { ); } ``` - -### AMP - -An unfortunate reality of modern web development is that it is sometimes necessary to create an [Accelerated Mobile Pages (AMP)](https://amp.dev/) version of your site. In SvelteKit this can be done by setting the [`inlineStyleThreshold`](configuration#inlineStyleThreshold) option... - -```js -/// file: svelte.config.js -/** @type {import('@sveltejs/kit').Config} */ -const config = { - kit: { - // since isn't - // allowed, inline all styles - inlineStyleThreshold: Infinity - } -}; - -export default config; -``` - -...disabling `csr` in your root `+layout.js`/`+layout.server.js`... - -```js -/// file: src/routes/+layout.server.js -export const csr = false; -``` - -...adding `amp` to your `app.html` - -```html - -... -``` - -...and transforming the HTML using `transformPageChunk` along with `transform` imported from `@sveltejs/amp`: - -```js -/// file: src/hooks.server.js -import * as amp from '@sveltejs/amp'; - -/** @type {import('@sveltejs/kit').Handle} */ -export async function handle({ event, resolve }) { - let buffer = ''; - return await resolve(event, { - transformPageChunk: ({ html, done }) => { - buffer += html; - if (done) return amp.transform(buffer); - } - }); -} -``` - -To prevent shipping any unused CSS as a result of transforming the page to amp, we can use [`dropcss`](https://www.npmjs.com/package/dropcss): - -```js -// @filename: ambient.d.ts -declare module 'dropcss'; - -// @filename: index.js -// ---cut--- -/// file: src/hooks.server.js -// @errors: 2307 -import * as amp from '@sveltejs/amp'; -import dropcss from 'dropcss'; - -/** @type {import('@sveltejs/kit').Handle} */ -export async function handle({ event, resolve }) { - let buffer = ''; - - return await resolve(event, { - transformPageChunk: ({ html, done }) => { - buffer += html; - - if (done) { - let css = ''; - const markup = amp - .transform(buffer) - .replace('⚡', 'amp') // dropcss can't handle this character - .replace(/`; - }); - - css = dropcss({ css, html: markup }).css; - return markup.replace('', `${css}`); - } - } - }); -} - -``` - -> [!NOTE] It's a good idea to use the `handle` hook to validate the transformed HTML using `amphtml-validator`, but only if you're prerendering pages since it's very slow. diff --git a/documentation/docs/60-appendix/10-faq.md b/documentation/docs/60-appendix/10-faq.md index 8eb4af1f420c..a224bb35e62f 100644 --- a/documentation/docs/60-appendix/10-faq.md +++ b/documentation/docs/60-appendix/10-faq.md @@ -16,7 +16,7 @@ If you'd like to include your application's version number or other information ```ts // @errors: 2732 -/// file: svelte.config.js +/// file: vite.config.js import pkg from './package.json' with { type: 'json' }; ``` @@ -31,7 +31,7 @@ Here are a few things to keep in mind when checking if a library is packaged cor - `main` should be defined if `exports` is not. It should be either a CommonJS or ESM file and adhere to the previous bullet. If a `module` field is defined, it should refer to an ESM file. - Svelte components should be distributed as uncompiled `.svelte` files with any JS in the package written as ESM only. Custom script and style languages, like TypeScript and SCSS, should be preprocessed as vanilla JS and CSS respectively. We recommend using [`svelte-package`](./packaging) for packaging Svelte libraries, which will do this for you. -Libraries work best in the browser with Vite when they distribute an ESM version, especially if they are dependencies of a Svelte component library. You may wish to suggest to library authors that they provide an ESM version. However, CommonJS (CJS) dependencies should work as well since, by default, [`vite-plugin-svelte` will ask Vite to pre-bundle them](https://github.com/sveltejs/vite-plugin-svelte/blob/main/docs/faq.md#what-is-going-on-with-vite-and-pre-bundling-dependencies) using `esbuild` to convert them to ESM. +Libraries work best in the browser with Vite when they distribute an ESM version, especially if they are dependencies of a Svelte component library. You may wish to suggest to library authors that they provide an ESM version. However, CommonJS (CJS) dependencies should work as well since, by default, [`vite-plugin-svelte` will ask Vite to pre-bundle them](https://github.com/sveltejs/vite-plugin-svelte/blob/main/docs/faq.md#what-is-going-on-with-vite-and-pre-bundling-dependencies) using `rolldown` to convert them to ESM. If you are still encountering issues we recommend searching both [the Vite issue tracker](https://github.com/vitejs/vite/issues) and the issue tracker of the library in question. Sometimes issues can be worked around by fiddling with the [`optimizeDeps`](https://vitejs.dev/config/#dep-optimization-options) or [`ssr`](https://vitejs.dev/config/#ssr-options) config values though we recommend this as only a short-term workaround in favor of fixing the library in question. @@ -70,7 +70,7 @@ If you need access to the `document` or `window` variables or otherwise need cod ```js /// // ---cut--- -import { browser } from '$app/environment'; +import { browser } from '$app/env'; if (browser) { // client-only code here @@ -115,7 +115,7 @@ Finally, you may also consider using an `{#await}` block: ```svelte + +

current pathname: {page.url.pathname}

+``` + +## `$app/tsconfig` + +Your project's `tsconfig.json` should now extend `$app/tsconfig` rather than `./.svelte-kit/tsconfig.json`. It should also specify `include` and `exclude` arrays, as `$app/tsconfig` does not specify these: + +```json +/// file: tsconfig.json +{ + "extends": "$app/tsconfig", + "include": ["src", "test", "*"], + "exclude": ["src/service-worker"] +} +``` + +Some essential `compilerOptions` (`isolatedModules` and `verbatimModuleSyntax`) are included in `$app/tsconfig`, alongside various options that are strongly recommend but which can be overridden in your own config. + +## `$app/tsconfig/service-worker` + +Your service worker needs to be part of a separate TypeScript project, otherwise the types for things like `fetch` events will be incorrect. To do this, exclude the service worker from your project's root `tsconfig.json`, and add a `src/service-worker/tsconfig.json` that extends `$app/tsconfig/service-worker`: + +```json +/// file: src/service-worker/tsconfig.json +{ + "extends": "$app/tsconfig/service-worker" +} +``` + +## `$env/...` (deprecated) + +The various `$env/...` modules have been deprecated in favour of `$app/env/private` and `$app/env/public` — see [Environment variables](environment-variables) for more details. + +## `$service-worker` (removed) + +The `$service-worker` module has been removed. Import `version` from [`$app/env`]($app-env), `assets`, `immutable` and `prerendered` from [`$app/manifest`]($app-manifest), and `resolved` from [`$app/paths`]($app-paths) instead. + +## `@sveltejs/kit` + +### `error`, `isHttpError`, `redirect`, and `isRedirect` refer to public types + +`error`, `isHttpError`, `redirect`, and `isRedirect` now refer to the public types rather than the internal classes. If you were importing the internal `HttpError`/`Redirect` classes from `@sveltejs/kit/internal`, or doing `instanceof` checks against them, use `isHttpError`/`isRedirect` from `@sveltejs/kit` instead. + +### `json` and `text` are deprecated + +The `json(...)` and `text(...)` helpers for generating responses are deprecated. Use `Response.json(...)` and `new Response(text)` instead. + +### `defineParams` moved to `@sveltejs/kit/params` + +The `defineParams` function for creating [param matchers](advanced-routing#Matching), along with the associated types, now live in [`@sveltejs/kit/params`](@sveltejs-kit-params). + +### Env-related types moved to `@sveltejs/kit/env` + +Types like `EnvVarConfig`, used with [`defineEnvVars`](@sveltejs-kit-env#defineEnvVars) hook, now live in `@sveltejs/kit/env`. + +### Hooks-related types moved to `@sveltejs/kit/hooks` + +Types like `Handle`, which defines the type of your [`handle`](hooks#handle) hook, now live in `@sveltejs/kit/hooks`. + +### Remote function types moved to `$app/server` + +Types describing remote functions, such as `RemoteQuery`, `RemoteForm` and `RemoteCommand`, now live in `$app/server` alongside the functions themselves. + +## `@sveltejs/kit/hooks` + +The `defineEnvVars` function has moved from `@sveltejs/kit/hooks` to `@sveltejs/kit/env`. + +## `@sveltejs/kit/node` + +The [`getRequest`](@sveltejs-kit-node#getRequest) and [`setResponse`](@sveltejs-kit-node#setResponse) helpers are now synchronous and no longer return Promises. Remove `await` from calls in custom Node servers or adapters. + +## `@sveltejs/kit/node/polyfills` (removed) + +The `@sveltejs/kit/node/polyfills` module (and the Node global shims in `adapter-node` and `adapter-netlify`) applied to Node versions that are no longer supported. Remove any `import '@sveltejs/kit/node/polyfills'` statements from your custom server code. + +## Security + +### `csrf.checkOrigin` replaced by `csrf.trustedOrigins` + +The deprecated `csrf.checkOrigin` option has been removed. CSRF protection is always on; instead of disabling it with `checkOrigin: false`, allow trusted cross-origin hosts with `csrf.trustedOrigins`. + +```js +/// file: vite.config.js +import { defineConfig } from 'vite'; +import { sveltekit } from '@sveltejs/kit/vite'; +// ---cut--- +export default defineConfig({ + plugins: [ + sveltekit({ + csrf: { + ---checkOrigin: false--- + +++trustedOrigins: ['https://trusted-site.com']+++ + } + }) + ] +}); +``` + +### Cross-origin form submissions require a `Content-Type` header + +Cross-origin mutative requests that omit a `Content-Type` header are now rejected as CSRF. Ensure cross-origin form submissions include a `Content-Type` header, or add the origin to `csrf.trustedOrigins`. + +### CORS for static assets in development is handled by Vite + +SvelteKit no longer sets `access-control-allow-origin: *` on every static asset request in development. CORS is now delegated to Vite's built-in middleware. If you rely on cross-origin access to static assets in dev, configure it in your Vite config: + +```js +/// file: vite.config.js +import { defineConfig } from 'vite'; +// ---cut--- +export default defineConfig({ + server: { ++++ cors: { origin: '*' }+++ + } +}); +``` + +## Cookies + +### Updated to `cookie` v2 + +SvelteKit now uses [`cookie`](https://npmx.dev/package/cookie) v2, which involves certain changes: + +- cookie names can only contain ASCII characters. Non-ASCII characters (including Latin-1 Supplement characters like `á`) are rejected +- the `CookieSerializeOptions` type has been renamed to `SerializeOptions` +- the `CookieParseOptions` type has been renamed to `ParseOptions` + +### Paths default to `'/'` + +When setting a cookie without an explicit `path` (which was previously forbidden), the path defaults to `'/'` rather than the current request path, meaning the cookie applies to the entire site. This matches what most developers expect. You can pass an explicit `path` if necessary: + +```ts +declare const cookies: import('@sveltejs/kit').Cookies; +declare const name: string; +declare const value: string; +// ---cut--- +cookies.set(name, value, +++{ path: '/some/path' }+++); +``` + +## Error handling + +### `App.Error` always includes `status` + +An `App.Error` object, such as the `error` prop of an `+error.svelte` component, has a `status` property reflecting the HTTP status of the error that caused it (e.g. 404 for a Not Found error, or 500 for a generic internal error), in addition to `message` and whatever properties you define in your [`app.d.ts`](types#app.d.ts) file. + +### `error(...)` arguments changed + +Previously, the second argument to [`error(...)`](@sveltejs-kit#error) could be either the `message` as a `string`, or an object containing `message` alongside any additional properties defined in `app.d.ts` (such as a tracking `code`). + +Now, the second argument must always be a `string`. If there are additional properties, they must be passed as a third argument. + +### `handleValidationError` is removed + +Validation errors are now passed to [`handleError`](hooks#handleError) with `kind: 'validation'`. The generic `{ status, message }` `error` object is safe to return, while the validation `issues` are provided separately for logging or custom handling. + +### `handleError` receives all errors + +In SvelteKit 2, `handleError` was not called in the case of _expected_ errors, which is to say those created with the [`error(...)`](@sveltejs-kit#error) helper. In SvelteKit 3, _all_ errors are passed to `handleError`. See the [docs](hooks#handleError) for more information. + +### `handleError` can influence the status code + +If you need to control the HTTP status code used to render a page in the case of an error, you can do so by returning a `status` property from `handleError` alongside any other required properties of `App.Error`. + +### Rendering errors are now handled + +Errors thrown during rendering are now always routed through `handleError` and then passed to the nearest [error boundary](../svelte/svelte-boundary). Error boundaries are automatically created for each of your `+error.svelte` components. + +If you have an async `handleError` hook in `hooks.client.ts`, enable `compilerOptions.experimental.async` in the `sveltekit(...)` plugin options of your Vite config so it can be awaited during rendering. + +### Form action responses use the `fail` status code + +Enhanced form action responses now use the HTTP status code passed to `fail(...)` instead of always returning `200`. If you inspect status codes on enhanced form submissions (for example in a `use:enhance` callback or in tests), they now reflect the value passed to `fail`. + +### Sourcemaps are applied + +Sourcemaps are generated by default, and applied to stack traces when errors occur. For this to work in production (as opposed to `vite preview`), adapters need to avoid any destructive changes (such as rebundling without generating additional — and correct — sourcemaps). For first-party adapters this is a work in progress. + +## Params + +[Param matchers](advanced-routing#Matching) are no longer files inside the `src/params` directory. Declare all matchers in a single `src/params.ts` (or `src/params.js`) file using the `defineParams` helper. A matcher can be a function that returns a parsed value (or `undefined`, if the param does not match), or a [Standard Schema](https://standardschema.dev). + +```js +/// file: src/params.js +import { defineParams } from '@sveltejs/kit/params'; +import * as v from 'valibot'; + +export const params = defineParams({ + // schema variant + integer: v.pipe(v.string(), v.toNumber()), + + // function variant + fruit: (param) => { + if (param === 'apple' || param === 'orange') { + return param; + } + } +}); +``` + +## Observability + +Server-side [instrumentation](observability) now happens automatically if a `src/instrumentation.server.js` file exists. + +To opt into [OpenTelemetry](https://opentelemetry.io/) tracing, add `tracing.server` configuration: + +```js +import { defineConfig } from 'vite'; +import { sveltekit } from '@sveltejs/kit/vite'; +/// file: vite.config.js +// ---cut--- +export default defineConfig({ + plugins: [ + sveltekit({ + +++tracing: { server: true }+++ + }) + ] +}); +``` + +## Adapters + +All first-party adapters now require SvelteKit 3, alongside these adapter-specific changes: + +### `adapter-cloudflare` + +Cloudflare-specific APIs are no longer available on `platform`. Instead, find them where you would expect on a Cloudflare worker: + +- `env`, `ctx.waitUntil`, and other `ctx` properties should be imported from `cloudflare:workers`: +```js +// @filename: ambient.d.ts +declare module 'cloudflare:workers' { + export const env: { KV: { get(): Promise } }; + export function waitUntil(promise: Promise): void; +} +// ---cut--- +import { env, waitUntil } from 'cloudflare:workers'; + +const value = await env.KV.get('key'); +``` +- `cf` is now a property of the `Request` object: +```js +/// file: src/routes/cf/+server.js +// @filename: ambient.d.ts +interface Request { + cf: import('@cloudflare/workers-types').IncomingRequestCfProperties; +} +// @filename: index.js +// @errors: 7031 +// ---cut--- +export async function GET({ request }) { + const { country } = request.cf; +} +``` +- `caches` is now a global variable: +```js +/// file: src/routes/cache/+server.js +let request = new Request(''); +// ---cut--- +const myCache = await caches.open('foo'); +await myCache.match(request); +``` + +- To make these types available to your app, install [`wrangler`](https://www.npmjs.com/package/wrangler) and run [`wrangler types`](https://developers.cloudflare.com/workers/languages/typescript/). + +- minimum `wrangler` is now `^4.67.0` +- `@cloudflare/workers-types` upgraded + +### `adapter-node` + +- bundling now happens with `rolldown` +- the `ORIGIN` environment variable is removed (set `paths.origin` in your Vite config instead) +- static assets are served from a list recorded at build time; files added to the output directory afterwards are not served, and replaced ones keep their old size and `ETag` (use environment variables for runtime configuration) +- `ETag`s for static assets are content hashes, and `Last-Modified` is no longer sent +- only `GET` and `HEAD` requests are served static assets; other methods result in a 405 response + +### `adapter-netlify` + +- output now conforms to the stable [Netlify Frameworks API](https://docs.netlify.com/build/frameworks/frameworks-api/) +- deploying/previewing with the Netlify CLI requires `v17.31.0` or later (`npm i -g netlify-cli@latest`) +- edge function build target is `es2022` +- the publish directory is now an adapter option rather than being read from the `netlify.toml` file + +### `adapter-vercel` + +- the `edge` runtime is no longer supported + +### Adapter API changes + +For adapter authors, there are some additional changes: + +- adapters can augment the Vite config with additional plugins +- `builder.config.kit` no longer exists — the configuration now lives at the top level +- `builder.createEntries` has been removed — use `builder.writeClient`, `builder.writeServer` and `builder.writePrerendered` directly +- `builder.compress` returns a list of compressed files +- `builder.mkdirp` and `builder.rimraf` are deprecated in favour of `node:fs` methods +- `builder.generateManifest` has been removed — use `builder.generateServerInstance` to replace it, and `builder.manifest` to access the manifest +- the `Server` class exported from the server output is deprecated — use the `server` object written by `builder.generateServerInstance` + +#### Server instrumentation + +Adapters that use `builder.instrument` must now generate an environment initializer before any +bundling step, include the returned module as an entrypoint in that step, and pass its final path to +`builder.instrument`: + +```js +// @filename: ambient.d.ts +import { Builder } from '@sveltejs/kit'; + +declare global { + const builder: Builder; + const temporary_directory: string; + const entrypoint: string; + const instrumentation: string; +} + +export {}; + +// @filename: index.js +// ---cut--- +const initializer = builder.createInstrumentationInitializer({ outputDirectory: temporary_directory }); + +// Include `initializer` in any bundling or file tracing here + +builder.instrument({ + entrypoint, + instrumentation, + initializer +}); +``` + +For runtimes that do not expose environment variables through `process.env`, pass the contents of +a module whose default export is the platform environment: + +```js +// @filename: ambient.d.ts +import { Builder } from '@sveltejs/kit'; + +declare global { + const builder: Builder; + const temporary_directory: string; +} + +export {}; + +// @filename: index.js +// ---cut--- +const initializer = builder.createInstrumentationInitializer({ + outputDirectory: temporary_directory, + environment: `import { env } from 'cloudflare:workers';\nexport default env;` +}); +``` + +## Responses + +### 204 responses return no content + +Returning a `204` (or any empty `2xx`) response from a `+server.js` handler now results in a response with no body, per the HTTP spec, rather than a SvelteKit envelope. Code that consumed the body of such responses needs to handle the empty body. + +### `resolve` always returns a `Promise` + +The `resolve` function passed to [`handle`](hooks#handle) is now typed to always return a `Promise` rather than `MaybePromise`. + +## Server-only modules + +### Files + +Server-only modules are now designated by a filename with a `server` segment, rather than a `.server.` infix — in other words, `stuff.server.ts`, `stuff.server.test.ts` and `server.ts` are all treated as server-only modules, whereas `server.ts` previously was not. + +### Directories + +Previously, any module inside `src/lib/server` was treated as server-only. This treatment now applies to _any_ `server` directory in the project with the exception of `src/routes` and your `static` directory. + +## Remote functions + +Remote functions are still considered experimental — opt in via the `experimental.remoteFunctions` flag alongside `compilerOptions.experimental.async`: + +```js +/// file: vite.config.js +import { defineConfig } from 'vite'; +import { sveltekit } from '@sveltejs/kit/vite'; +// ---cut--- +export default defineConfig({ + plugins: [ + sveltekit({ + compilerOptions: { + experimental: { + +++async: true+++ + } + }, + experimental: { + +++remoteFunctions: true+++ + } + }) + ] +}); +``` + +### Remote module filenames + +As with server-only modules, a `remote` segment in a filename designates a remote module — `stuff.remote.ts`, `stuff.remote.test.ts` and `remote.ts` are all remote modules. + +If `experimental.remoteFunctions` is not enabled, the existence of these files will cause an error. + +### `event.url`, `event.params`, and `event.route` cannot be accessed inside queries + +Accessing `event.url`, `event.params`, or `event.route` inside a remote `query` function now throws an error. These properties are not meaningful in the context of a remote function (which can be called from anywhere). Pass any values you need explicitly as arguments to the function. + +### Errors are typed as `App.Error | undefined` + +The `error` property on remote function resources (queries, live queries, forms, prerender functions) is now typed as `App.Error | undefined` rather than `any`, as the error is always transformed by `handleError`. + +### Form submissions require `field.as(...)` + +A form control must use attributes from a field associated with the current `form` object: + +```svelte + +``` + +Manually specifying a name (``) will cause the submission to be rejected. + +## Miscellaneous + +### Links to the current page cause a refresh + +If the user clicks a link that points to the current location, SvelteKit will `refreshAll()` instead of doing nothing. + +### `data-sveltekit-*` uses `false` instead of `'off'` + +The `'off'` value for `data-sveltekit-*` link attributes has been removed in favour of `false`. + +```svelte +---
--- +++++++ +``` + +### External redirects must be opted into + +To [`redirect`](@sveltejs-kit#redirect) to an external URL you must now pass an `external` option — either `true` to allow any external URL (except `javascript:` URLs, which remain blocked), or an array of allowed origins (which _can_ include `javascript:` URLs). + +```js +import { redirect } from '@sveltejs/kit'; +// ---cut--- +redirect(307, 'https://example.com', +++{ external: true }+++); +``` + +### Universal `config` takes precedence over server `config` + +Route `config` exported from a universal `+page.js` or `+layout.js` now takes precedence over `config` exported from the corresponding `+page.server.js` or `+layout.server.js`, matching how other page options are resolved. If you export `config` from both, move the canonical export to the universal file or consolidate them. + +### Service worker registrations use `type: 'module'` + +Module service workers are now widely supported. As such, SvelteKit will bundle and register your service worker as a module, rather than as a script. diff --git a/documentation/docs/60-appendix/40-migrating.md b/documentation/docs/60-appendix/40-migrating.md index b799465e8097..3b40169ac60f 100644 --- a/documentation/docs/60-appendix/40-migrating.md +++ b/documentation/docs/60-appendix/40-migrating.md @@ -37,7 +37,7 @@ The bulk of your app, in `src/routes`, can be left where it is, but several proj ### Configuration -Your `webpack.config.js` or `rollup.config.js` should be replaced with a `svelte.config.js`, as documented [here](configuration). Svelte preprocessor options should be moved to `config.preprocess`. +Your `webpack.config.js` or `rollup.config.js` should be replaced with a `vite.config.js`, as documented [here](configuration). Svelte preprocessor options should be moved to the SvelteKit plugin `config.preprocess` setting. You will need to add an [adapter](adapters). `sapper build` is roughly equivalent to [adapter-node](adapter-node) while `sapper export` is roughly equivalent to [adapter-static](adapter-static), though you might prefer to use an adapter designed for the platform you're deploying to. @@ -89,7 +89,7 @@ The `goto`, `prefetch` and `prefetchRoutes` imports from `@sapper/app` should be The `stores` import from `@sapper/app` should be replaced — see the [Stores](migrating#Pages-and-layouts-Stores) section below. -Any files you previously imported from directories in `src/node_modules` will need to be replaced with [`$lib`]($lib) imports. +Any files you previously imported from directories in `src/node_modules` will need to be replaced with [`#lib`]($lib) imports. ### Preload @@ -134,7 +134,7 @@ This caused problems and is no longer the case in SvelteKit. Instead, relative U ### <a> attributes - `sapper:prefetch` is now `data-sveltekit-preload-data` -- `sapper:noscroll` is now `data-sveltekit-noscroll` +- `sapper:noscroll` is now `data-sveltekit-reset="false"` ## Endpoints @@ -160,7 +160,7 @@ declare module 'html-minifier'; // @filename: index.js // ---cut--- import { minify } from 'html-minifier'; -import { building } from '$app/environment'; +import { building } from '$app/env'; const minification_options = { collapseBooleanAttributes: true, @@ -181,7 +181,7 @@ const minification_options = { sortClassName: true }; -/** @type {import('@sveltejs/kit').Handle} */ +/** @type {import('@sveltejs/kit/hooks').Handle} */ export async function handle({ event, resolve }) { let page = ''; diff --git a/documentation/docs/60-appendix/60-glossary.md b/documentation/docs/60-appendix/60-glossary.md index 7186b86f62e1..8fedfe40602f 100644 --- a/documentation/docs/60-appendix/60-glossary.md +++ b/documentation/docs/60-appendix/60-glossary.md @@ -40,7 +40,7 @@ For content to be prerenderable, any two users hitting it directly must get the Pre-rendered pages are not limited to static content. You can build personalized pages if user-specific data is fetched and rendered client-side. This is subject to the caveat that you will experience the downsides of not doing SSR for that content as discussed above. -In SvelteKit, you can control prerendering with [the `prerender` page option](page-options#prerender) and [`prerender` config](configuration#prerender) in `svelte.config.js`. +In SvelteKit, you can control prerendering with [the `prerender` page option](page-options#prerender) and [`prerender` config](configuration#prerender) of the SvelteKit plugin in `vite.config.js`. ## PWA @@ -63,7 +63,7 @@ In SvelteKit, you can [build SPAs with `adapter-static`](single-page-apps). Static Site Generation (SSG) is a term that refers to a site where every page is prerendered. One benefit of fully prerendering a site is that you do not need to maintain or pay for servers to perform SSR. Once generated, the site can be served from CDNs, leading to great “time to first byte” performance. This delivery model is often referred to as JAMstack. -In SvelteKit, you can do static site generation by using [`adapter-static`](adapter-static) or by configuring every page to be [prerendered](#Prerendering) using [the `prerender` page option](page-options#prerender) or [`prerender` config](configuration#prerender) in `svelte.config.js`. +In SvelteKit, you can do static site generation by using [`adapter-static`](adapter-static) or by configuring every page to be [prerendered](#Prerendering) using [the `prerender` page option](page-options#prerender) or [`prerender` config](configuration#prerender) of the SvelteKit plugin in `vite.config.js`. ## SSR diff --git a/documentation/docs/98-reference/10-@sveltejs-kit.md b/documentation/docs/98-reference/10-@sveltejs-kit.md index a89badb6359f..78a25fbc07c6 100644 --- a/documentation/docs/98-reference/10-@sveltejs-kit.md +++ b/documentation/docs/98-reference/10-@sveltejs-kit.md @@ -8,4 +8,4 @@ title: @sveltejs/kit The following are referenced by the public types documented above, but cannot be imported directly: -> TYPES: Private types +> TYPES: Private types {depth=3} diff --git a/documentation/docs/98-reference/15-@sveltejs-kit-adapter.md b/documentation/docs/98-reference/15-@sveltejs-kit-adapter.md new file mode 100644 index 000000000000..8c3c72df9319 --- /dev/null +++ b/documentation/docs/98-reference/15-@sveltejs-kit-adapter.md @@ -0,0 +1,5 @@ +--- +title: @sveltejs/kit/adapter +--- + +> MODULE: @sveltejs/kit/adapter diff --git a/documentation/docs/98-reference/15-@sveltejs-kit-node-polyfills.md b/documentation/docs/98-reference/15-@sveltejs-kit-node-polyfills.md deleted file mode 100644 index a80c80ab3139..000000000000 --- a/documentation/docs/98-reference/15-@sveltejs-kit-node-polyfills.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: @sveltejs/kit/node/polyfills ---- - -> MODULE: @sveltejs/kit/node/polyfills diff --git a/documentation/docs/98-reference/15-@sveltejs-kit-params.md b/documentation/docs/98-reference/15-@sveltejs-kit-params.md new file mode 100644 index 000000000000..401fc98c68df --- /dev/null +++ b/documentation/docs/98-reference/15-@sveltejs-kit-params.md @@ -0,0 +1,5 @@ +--- +title: @sveltejs/kit/params +--- + +> MODULE: @sveltejs/kit/params diff --git a/documentation/docs/98-reference/15-@sveltejs-kit-vite.md b/documentation/docs/98-reference/15-@sveltejs-kit-vite.md index 4935ede2c1d5..8a2eb6e0ab16 100644 --- a/documentation/docs/98-reference/15-@sveltejs-kit-vite.md +++ b/documentation/docs/98-reference/15-@sveltejs-kit-vite.md @@ -2,4 +2,10 @@ title: @sveltejs/kit/vite --- -> MODULE: @sveltejs/kit/vite +## sveltekit + +> EXPORT_SNIPPET: @sveltejs/kit/vite#sveltekit + +## Config + +> EXPANDED_TYPES: @sveltejs/kit/vite#Config diff --git a/documentation/docs/98-reference/19-$app-env.md b/documentation/docs/98-reference/19-$app-env.md index 88531a242957..a0ff228db374 100644 --- a/documentation/docs/98-reference/19-$app-env.md +++ b/documentation/docs/98-reference/19-$app-env.md @@ -2,6 +2,4 @@ title: $app/env --- -> [!NOTE] This is an alias of [`$app/environment`]($app-environment), used when [explicit environment variables](environment-variables#Explicit-environment-variables) are enabled. - > MODULE: $app/env diff --git a/documentation/docs/98-reference/20-$app-env-private.md b/documentation/docs/98-reference/20-$app-env-private.md index 51c2ea22124d..7944fea1f49d 100644 --- a/documentation/docs/98-reference/20-$app-env-private.md +++ b/documentation/docs/98-reference/20-$app-env-private.md @@ -2,8 +2,6 @@ title: $app/env/private --- -Private [environment variables](environment-variables) defined in `src/env.ts` (or `src/env.js`). - -To use this module, you must enable the `experimental.explicitEnvironmentVariables` flag in your project configuration. - +Private environment variables defined in `src/env.ts` (or `src/env.js`). +See the [Environment variables](environment-variables) page for more information. diff --git a/documentation/docs/98-reference/20-$app-env-public.md b/documentation/docs/98-reference/20-$app-env-public.md index 6845c96858a9..862e1664a4a0 100644 --- a/documentation/docs/98-reference/20-$app-env-public.md +++ b/documentation/docs/98-reference/20-$app-env-public.md @@ -2,8 +2,6 @@ title: $app/env/public --- -Public [environment variables](environment-variables) defined in `src/env.ts` (or `src/env.js`). - -To use this module, you must enable the `experimental.explicitEnvironmentVariables` flag in your project configuration. - +Public environment variables defined in `src/env.ts` (or `src/env.js`). +See the [Environment variables](environment-variables) page for more information. diff --git a/documentation/docs/98-reference/20-$app-environment.md b/documentation/docs/98-reference/20-$app-environment.md deleted file mode 100644 index 4f81897214c5..000000000000 --- a/documentation/docs/98-reference/20-$app-environment.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: $app/environment ---- - -> MODULE: $app/environment diff --git a/documentation/docs/98-reference/20-$app-manifest.md b/documentation/docs/98-reference/20-$app-manifest.md new file mode 100644 index 000000000000..9421c0e9f356 --- /dev/null +++ b/documentation/docs/98-reference/20-$app-manifest.md @@ -0,0 +1,5 @@ +--- +title: $app/manifest +--- + +> MODULE: $app/manifest diff --git a/documentation/docs/98-reference/20-$app-paths.md b/documentation/docs/98-reference/20-$app-paths.md index 32a350965e4a..b5a1cb781b68 100644 --- a/documentation/docs/98-reference/20-$app-paths.md +++ b/documentation/docs/98-reference/20-$app-paths.md @@ -3,3 +3,6 @@ title: $app/paths --- > MODULE: $app/paths + +> [!LEGACY] +> `base`, `assets`, and `resolveRoute` were removed in 3.0 diff --git a/documentation/docs/98-reference/20-$app-service-worker.md b/documentation/docs/98-reference/20-$app-service-worker.md new file mode 100644 index 000000000000..796d4adbe735 --- /dev/null +++ b/documentation/docs/98-reference/20-$app-service-worker.md @@ -0,0 +1,7 @@ +--- +title: $app/service-worker +--- + +This module can only be imported in service workers. + +> MODULE: $app/service-worker diff --git a/documentation/docs/98-reference/20-$app-state.md b/documentation/docs/98-reference/20-$app-state.md index 9362eacad82f..cbb39bc4b609 100644 --- a/documentation/docs/98-reference/20-$app-state.md +++ b/documentation/docs/98-reference/20-$app-state.md @@ -4,7 +4,4 @@ title: $app/state SvelteKit makes three read-only state objects available via the `$app/state` module — `page`, `navigating` and `updated`. -> [!NOTE] -> This module was added in 2.12. If you're using an earlier version of SvelteKit, use [`$app/stores`]($app-stores) instead. - > MODULE: $app/state diff --git a/documentation/docs/98-reference/20-$app-stores.md b/documentation/docs/98-reference/20-$app-stores.md deleted file mode 100644 index ff7cf8999e82..000000000000 --- a/documentation/docs/98-reference/20-$app-stores.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: $app/stores ---- - -This module contains store-based equivalents of the exports from [`$app/state`]($app-state). If you're using SvelteKit 2.12 or later, use that module instead. - -> MODULE: $app/stores diff --git a/documentation/docs/98-reference/20-$app-tsconfig.md b/documentation/docs/98-reference/20-$app-tsconfig.md new file mode 100644 index 000000000000..9da67fb2fad1 --- /dev/null +++ b/documentation/docs/98-reference/20-$app-tsconfig.md @@ -0,0 +1,23 @@ +--- +title: $app/tsconfig +--- + +This module contains TypeScript configuration tailored for your app. Your own config should extend it — a typical `tsconfig.json` looks like this: + +```json +/// file: tsconfig.json +{ + "extends": "$app/tsconfig", + "include": ["src", "test"], + "exclude": ["src/service-worker"] +} +``` + +You can extend this configuration with your own `compilerOptions`. Overriding the following properties may cause things to break — SvelteKit will warn you if this happens: + +- `paths` — this is derived from the (deprecated) [`alias`](configuration#alias) config option, together with any [subpath imports](https://nodejs.org/api/packages.html#subpath-imports) specified in your `package.json`, to align behaviour between Vite and TypeScript. Ideally, configure subpath imports rather than using `paths` directly +- `types` — your app needs to be able to 'see' generated module declarations for things like [environment variables](environment-variables), and as such this array must include `"$app/types"` +- `isolatedModules` — must be `true`, as Vite compiles modules one at a time +- `verbatimModuleSyntax` — must be `true`, so that you can safely use type imports in `.svelte` files + +Note that the example configuration above excludes `src/service-worker`, because service workers need to be in their own TypeScript project. If you are using a service worker, create a `src/service-worker/tsconfig.json` that extends [`$app/tsconfig/service-worker`]($app-tsconfig-service-worker). diff --git a/documentation/docs/98-reference/20-$app-types.md b/documentation/docs/98-reference/20-$app-types.md deleted file mode 100644 index 97053e762869..000000000000 --- a/documentation/docs/98-reference/20-$app-types.md +++ /dev/null @@ -1,91 +0,0 @@ ---- -title: $app/types ---- - -This module contains generated types for the routes in your app. - -
-

Available since 2.26

-
- -```js -// @noErrors -import type { RouteId, RouteParams, LayoutParams } from '$app/types'; -``` - -## Asset - -A union of all the filenames of assets contained in your `static` directory, plus a `string` wildcard for asset paths generated from `import` declarations. - -
- -```dts -type Asset = '/favicon.png' | '/robots.txt' | (string & {}); -``` - -
- -## RouteId - -A union of all the route IDs in your app. Used for `page.route.id` and `event.route.id`. - -
- -```dts -type RouteId = '/' | '/my-route' | '/my-other-route/[param]'; -``` - -
- -## Pathname - -A union of all valid pathnames in your app. - -
- -```dts -type Pathname = '/' | '/my-route' | `/my-other-route/${string}` & {}; -``` - -
- -## ResolvedPathname - -Similar to `Pathname`, but possibly prefixed with a [base path](configuration#paths). Used for `page.url.pathname`. - -
- -```dts -type ResolvedPathname = `${'' | `/${string}`}/` | `${'' | `/${string}`}/my-route` | `${'' | `/${string}`}/my-other-route/${string}` | {}; -``` - -
- -## RouteParams - -A utility for getting the parameters associated with a given route. - -```ts -// @errors: 2552 -type BlogParams = RouteParams<'/blog/[slug]'>; // { slug: string } -``` - -
- -```dts -type RouteParams = { /* generated */ } | Record; -``` - -
- -## LayoutParams - -A utility for getting the parameters associated with a given layout, which is similar to `RouteParams` but also includes optional parameters for any child route. - -
- -```dts -type RouteParams = { /* generated */ } | Record; -``` - -
diff --git a/documentation/docs/98-reference/21-$app-tsconfig-service-worker.md b/documentation/docs/98-reference/21-$app-tsconfig-service-worker.md new file mode 100644 index 000000000000..1c8fa148dab4 --- /dev/null +++ b/documentation/docs/98-reference/21-$app-tsconfig-service-worker.md @@ -0,0 +1,14 @@ +--- +title: $app/tsconfig/service-worker +--- + +This module contains TypeScript configuration tailored for your service worker: + +```json +/// file: src/service-worker/tsconfig.json +{ + "extends": "$app/tsconfig/service-worker" +} +``` + +You can extend this configuration with your own `compilerOptions`, adhering to the same restrictions as [`$app/tsconfig`]($app-tsconfig). diff --git a/documentation/docs/98-reference/22-$app-types.md b/documentation/docs/98-reference/22-$app-types.md new file mode 100644 index 000000000000..0f156717a2fd --- /dev/null +++ b/documentation/docs/98-reference/22-$app-types.md @@ -0,0 +1,119 @@ +--- +title: $app/types +--- + +This module contains generated types for the routes in your app. + +
+

Available since 2.26

+
+ +```js +// @noErrors +import type { RouteId, PageRouteId, EndpointRouteId, RouteParams, LayoutParams } from '$app/types'; +``` + +## AssetPath + +A union of all the filenames of assets contained in your `static` directory, relative to the `base` path. + +
+ +```dts +type AssetPath = 'favicon.png' | 'robots.txt' | (string & {}); +``` + +
+ +## RouteId + +A union of all the route IDs in your app — the union of `PageRouteId` and `EndpointRouteId`. Used for `page.route.id` and `event.route.id`. + +
+ +```dts +type RouteId = '/' | '/my-route' | '/my-other-route/[param]' | '/my-endpoint'; +``` + +
+ +## PageRouteId + +A union of the route IDs in your app that have a `+page`. + +A route ID can be in both `PageRouteId` and `EndpointRouteId`, if its directory contains both a `+page` and a `+server`. In the example below, `/my-route` has both. + +
+ +```dts +type PageRouteId = '/' | '/my-route' | '/my-other-route/[param]'; +``` + +
+ +## EndpointRouteId + +A union of the route IDs in your app that have a `+server`. + +A route ID can be in both `PageRouteId` and `EndpointRouteId`, if its directory contains both a `+page` and a `+server`. In the example below, `/my-route` has both. + +
+ +```dts +type EndpointRouteId = '/my-route' | '/my-endpoint'; +``` + +
+ +## Path + +A union of all valid paths in your app, relative to the `base` path. + +
+ +```dts +type Path = '' | 'my-route' | `my-other-route/${string}` & {}; +``` + +
+ +## ResolvedPathname + +Similar to `Path`, but prefixed with a [base path](configuration#paths). Used for `page.url.pathname`. + +
+ +```dts +type ResolvedPathname = `${'' | `/${string}`}/` | `${'' | `/${string}`}/my-route` | `${'' | `/${string}`}/my-other-route/${string}` | {}; +``` + +
+ +## RouteParams + +A utility for getting the parameters associated with a given route. + +```ts +// @errors: 2552 +type BlogParams = RouteParams<'/blog/[slug]'>; // { slug: string } +``` + +
+ +```dts +type RouteParams = { /* generated */ } | Record; +``` + +
+ +## LayoutParams + +A utility for getting the parameters associated with a given layout, which is similar to `RouteParams` but also includes optional parameters for any child route. It accepts the route ID of any directory containing a layout, including layout-only directories that are not part of `RouteId`. + +
+ +```dts +type LayoutParams = { /* generated */ }; +``` + +
diff --git a/documentation/docs/98-reference/25-$env-dynamic-private.md b/documentation/docs/98-reference/25-$env-dynamic-private.md deleted file mode 100644 index e16a1b68dec8..000000000000 --- a/documentation/docs/98-reference/25-$env-dynamic-private.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: $env/dynamic/private ---- - -> MODULE: $env/dynamic/private diff --git a/documentation/docs/98-reference/25-$env-dynamic-public.md b/documentation/docs/98-reference/25-$env-dynamic-public.md deleted file mode 100644 index 6f6ee967fafa..000000000000 --- a/documentation/docs/98-reference/25-$env-dynamic-public.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: $env/dynamic/public ---- - -> MODULE: $env/dynamic/public diff --git a/documentation/docs/98-reference/25-$env-static-private.md b/documentation/docs/98-reference/25-$env-static-private.md deleted file mode 100644 index d33ddd685c59..000000000000 --- a/documentation/docs/98-reference/25-$env-static-private.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: $env/static/private ---- - -> MODULE: $env/static/private diff --git a/documentation/docs/98-reference/25-$env-static-public.md b/documentation/docs/98-reference/25-$env-static-public.md deleted file mode 100644 index 590aabd8efc5..000000000000 --- a/documentation/docs/98-reference/25-$env-static-public.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: $env/static/public ---- - -> MODULE: $env/static/public diff --git a/documentation/docs/98-reference/26-$lib.md b/documentation/docs/98-reference/26-$lib.md index 8781f5ce863a..b0de43071244 100644 --- a/documentation/docs/98-reference/26-$lib.md +++ b/documentation/docs/98-reference/26-$lib.md @@ -1,8 +1,22 @@ --- -title: $lib +title: '#lib' --- -SvelteKit automatically makes files under `src/lib` available using the `$lib` import alias. +When scaffolding a new SvelteKit project through the [`sv` CLI](/docs/cli/overview), it automatically creates a `#lib` import alias for your `src/lib` directory, by adding the following to your `package.json`: + +```json +{ + "imports": { + "#lib": "./src/lib/index.js", + "#lib/*": "./src/lib/*" + } +} +``` + +The `#` prefix leverages Node's built-in [subpath imports](https://nodejs.org/api/packages.html#subpath-imports) feature, which reserves `#` for package-internal aliases. Vite and TypeScript both resolve these natively. + +> [!LEGACY] +> Previously, this alias was `$lib` and was automatically configured by SvelteKit. It is now `#lib` and must be declared in your `package.json` `imports` field. `import { foo } from '$lib/foo.js'` becomes `import { foo } from '#lib/foo.js'`. ```svelte @@ -12,7 +26,7 @@ A reusable component ```svelte diff --git a/documentation/docs/98-reference/27-$service-worker.md b/documentation/docs/98-reference/27-$service-worker.md deleted file mode 100644 index a8f7388ba7a9..000000000000 --- a/documentation/docs/98-reference/27-$service-worker.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: $service-worker ---- - -> MODULE: $service-worker diff --git a/documentation/docs/98-reference/50-configuration.md b/documentation/docs/98-reference/50-configuration.md deleted file mode 100644 index c4208cd17e8f..000000000000 --- a/documentation/docs/98-reference/50-configuration.md +++ /dev/null @@ -1,76 +0,0 @@ ---- -title: Configuration ---- - -Your project's configuration lives in a `svelte.config.js` file at the root of your project. As well as SvelteKit, this config object is used by other tooling that integrates with Svelte such as editor extensions. - -```js -/// file: svelte.config.js -// @filename: ambient.d.ts -declare module '@sveltejs/adapter-auto' { - const plugin: () => import('@sveltejs/kit').Adapter; - export default plugin; -} - -// @filename: index.js -// ---cut--- -import adapter from '@sveltejs/adapter-auto'; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - kit: { - adapter: adapter() - } -}; - -export default config; -``` - -Since version 2.62.0 you can also pass your configuration to the `sveltekit` plugin in your Vite config, along with the Svelte compiler options: - -```js -/// file: vite.config.js -// @filename: ambient.d.ts -declare module '@sveltejs/adapter-auto' { - const plugin: () => import('@sveltejs/kit').Adapter; - export default plugin; -} - -// @filename: index.js -// ---cut--- -import adapter from '@sveltejs/adapter-auto'; -import { sveltekit } from '@sveltejs/kit/vite'; -import { defineConfig } from 'vite'; - -export default defineConfig({ - plugins: [ - sveltekit({ - compilerOptions: { - experimental: { - async: true - } - }, - adapter: adapter(), - experimental: { - remoteFunctions: true - } - }) - ] -}); -``` - -> [!NOTE] The `kit` namespace is at the same level as the other top level entries; this is the only difference to the `svelte.config.js` layout. - -Any options that don't belong to SvelteKit are passed through to [`vite-plugin-svelte`](https://github.com/sveltejs/vite-plugin-svelte/blob/main/docs/config.md), so you can set options like `inspector` here too. The `experimental` namespace is shared — SvelteKit reads its own flags and forwards the rest. - -If the config is defined via the plugin, the `svelte.config.js` file is ignored. - -## Config - -> TYPES: Configuration#Config - -## KitConfig - -The `kit` property configures SvelteKit, and can have the following properties: - -> EXPANDED_TYPES: Configuration#KitConfig diff --git a/documentation/docs/98-reference/54-types.md b/documentation/docs/98-reference/54-types.md index d0393ad89441..303609691982 100644 --- a/documentation/docs/98-reference/54-types.md +++ b/documentation/docs/98-reference/54-types.md @@ -119,88 +119,9 @@ Starting with version 2.16.0, two additional helper types are provided: `PagePro > > ``` -> [!NOTE] For this to work, your own `tsconfig.json` or `jsconfig.json` should extend from the generated `.svelte-kit/tsconfig.json` (where `.svelte-kit` is your [`outDir`](configuration#outDir)): +> [!NOTE] For this to work, your own `tsconfig.json` or `jsconfig.json` should extend from the generated `$app/types`: > -> `{ "extends": "./.svelte-kit/tsconfig.json" }` - -### Default tsconfig.json - -The generated `.svelte-kit/tsconfig.json` file contains a mixture of options. Some are generated programmatically based on your project configuration, and should generally not be overridden without good reason: - -```json -/// file: .svelte-kit/tsconfig.json -{ - "compilerOptions": { - "paths": { - "$lib": ["../src/lib"], - "$lib/*": ["../src/lib/*"] - }, - "rootDirs": ["..", "./types"] - }, - "include": [ - "ambient.d.ts", - "non-ambient.d.ts", - "./types/**/$types.d.ts", - "../vite.config.js", - "../vite.config.ts", - "../src/**/*.js", - "../src/**/*.ts", - "../src/**/*.svelte", - "../tests/**/*.js", - "../tests/**/*.ts", - "../tests/**/*.svelte" - ], - "exclude": [ - "../node_modules/**", - "../src/service-worker.js", - "../src/service-worker/**/*.js", - "../src/service-worker.ts", - "../src/service-worker/**/*.ts", - "../src/service-worker.d.ts", - "../src/service-worker/**/*.d.ts" - ] -} -``` - -Others are required for SvelteKit to work properly, and should also be left untouched unless you know what you're doing: - -```json -/// file: .svelte-kit/tsconfig.json -{ - "compilerOptions": { - // this ensures that types are explicitly - // imported with `import type`, which is - // necessary as Svelte/Vite cannot - // otherwise compile components correctly - "verbatimModuleSyntax": true, - - // Vite compiles one TypeScript module - // at a time, rather than compiling - // the entire module graph - "isolatedModules": true, - - // Tell TS it's used only for type-checking - "noEmit": true, - - // This ensures both `vite build` - // and `svelte-package` work correctly - "lib": ["esnext", "DOM", "DOM.Iterable"], - "moduleResolution": "bundler", - "module": "esnext", - "target": "esnext" - } -} -``` - -Use the [`typescript.config` setting](configuration#typescript) in `svelte.config.js` to extend or modify the generated `tsconfig.json`. - -## $lib - -This is a simple alias to `src/lib`. It allows you to access common components and utility modules without `../../../../` nonsense. - -### $lib/server - -A subdirectory of `$lib`. SvelteKit will prevent you from importing any modules in `$lib/server` into client-side code. See [server-only modules](server-only-modules). +> `{ "extends": "$app/tsconfig" }` ## app.d.ts diff --git a/documentation/docs/99-legacy-reference/10-$service-worker.md b/documentation/docs/99-legacy-reference/10-$service-worker.md new file mode 100644 index 000000000000..ab638b18c5a8 --- /dev/null +++ b/documentation/docs/99-legacy-reference/10-$service-worker.md @@ -0,0 +1,26 @@ +--- +title: $service-worker +--- + +The `service-worker` module existed in SvelteKit 2, and provided access to the following exports: + +## base + +A root-relative path representing the application's base path. Use [`resolve(...)`]($app-paths#resolve) from `$app/paths` instead. + +## build + +A `string[]` array of files generated by Vite. Empty during development. Use [`immutable`]($app-manifest#immutable) from `$app/manifest` instead. + +## files + +A `string[]` array of files in your `static` directory. Use [`assets`]($app-manifest#assets) from `$app/manifest` instead. + + +## prerendered + +A `string[]` array of prerendered pages. Empty during dev. Use [`prerendered`]($app-manifest#prerendered) from `$app/manifest` instead. + +## version + +The value of [`config.version.name`](configuration#version), used for populating caches. Use [`version`]($app-env#version) from `$app/env` instead. diff --git a/documentation/docs/99-legacy-reference/20-$app-environment.md b/documentation/docs/99-legacy-reference/20-$app-environment.md new file mode 100644 index 000000000000..2968e7f45367 --- /dev/null +++ b/documentation/docs/99-legacy-reference/20-$app-environment.md @@ -0,0 +1,5 @@ +--- +title: $app/environment +--- + +This module was deprecated in SvelteKit 3 in favour of [$app/env]($app-env). It will be removed in SvelteKit 4. diff --git a/documentation/docs/99-legacy-reference/20-$app-stores.md b/documentation/docs/99-legacy-reference/20-$app-stores.md new file mode 100644 index 000000000000..e33c307095d2 --- /dev/null +++ b/documentation/docs/99-legacy-reference/20-$app-stores.md @@ -0,0 +1,5 @@ +--- +title: $app/stores +--- + +This module contained store-based equivalents of the exports from [`$app/state`]($app-state) but was removed in 3.0. diff --git a/documentation/docs/99-legacy-reference/25-$env-dynamic-private.md b/documentation/docs/99-legacy-reference/25-$env-dynamic-private.md new file mode 100644 index 000000000000..72b7391d4881 --- /dev/null +++ b/documentation/docs/99-legacy-reference/25-$env-dynamic-private.md @@ -0,0 +1,5 @@ +--- +title: $env/dynamic/private +--- + +This module was deprecated in SvelteKit 3 in favour of [explicit environment variables](environment-variables). It will be removed in SvelteKit 4. diff --git a/documentation/docs/99-legacy-reference/25-$env-dynamic-public.md b/documentation/docs/99-legacy-reference/25-$env-dynamic-public.md new file mode 100644 index 000000000000..793995e9ba96 --- /dev/null +++ b/documentation/docs/99-legacy-reference/25-$env-dynamic-public.md @@ -0,0 +1,5 @@ +--- +title: $env/dynamic/public +--- + +This module was deprecated in SvelteKit 3 in favour of [explicit environment variables](environment-variables). It will be removed in SvelteKit 4. diff --git a/documentation/docs/99-legacy-reference/25-$env-static-private.md b/documentation/docs/99-legacy-reference/25-$env-static-private.md new file mode 100644 index 000000000000..19b14e4e2ce1 --- /dev/null +++ b/documentation/docs/99-legacy-reference/25-$env-static-private.md @@ -0,0 +1,5 @@ +--- +title: $env/static/private +--- + +This module was deprecated in SvelteKit 3 in favour of [explicit environment variables](environment-variables). It will be removed in SvelteKit 4. diff --git a/documentation/docs/99-legacy-reference/25-$env-static-public.md b/documentation/docs/99-legacy-reference/25-$env-static-public.md new file mode 100644 index 000000000000..7e936817d3d5 --- /dev/null +++ b/documentation/docs/99-legacy-reference/25-$env-static-public.md @@ -0,0 +1,5 @@ +--- +title: $env/static/public +--- + +This module was deprecated in SvelteKit 3 in favour of [explicit environment variables](environment-variables). It will be removed in SvelteKit 4. diff --git a/documentation/docs/99-legacy-reference/index.md b/documentation/docs/99-legacy-reference/index.md new file mode 100644 index 000000000000..e30833e4fd86 --- /dev/null +++ b/documentation/docs/99-legacy-reference/index.md @@ -0,0 +1,3 @@ +--- +title: Legacy +--- diff --git a/eslint.config.js b/eslint.config.js index 4c4aaefb917c..9a4e7d035d7e 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,5 +1,7 @@ import svelte_config from '@sveltejs/eslint-config'; +import noExportsToRuntimeImports from './.eslint/no-exports-to-runtime-imports.js'; import noRuntimeToExportsImports from './.eslint/no-runtime-to-exports-imports.js'; +import requirePathToFileURL from './.eslint/require-path-to-file-url.js'; /** @type {import('eslint').Linter.Config[]} */ export default [ @@ -11,6 +13,19 @@ export default [ 'svelte/prefer-svelte-reactivity': 'off' } }, + { + files: ['packages/kit/src/exports/**/*.js'], + plugins: { + 'kit-exports-custom': { + rules: { + 'no-exports-to-runtime-imports': noExportsToRuntimeImports + } + } + }, + rules: { + 'kit-exports-custom/no-exports-to-runtime-imports': 'error' + } + }, { files: ['packages/kit/src/runtime/**/*.js'], plugins: { @@ -24,6 +39,56 @@ export default [ 'kit-custom/no-runtime-to-exports-imports': 'error' } }, + { + // code that ends up in user bundles must not pull in Vite or the build pipeline + files: [ + 'packages/kit/src/runtime/**/*.js', + 'packages/kit/src/exports/**/*.js', + 'packages/kit/src/utils/**/*.js' + ], + ignores: ['packages/kit/src/exports/vite/**'], + rules: { + 'no-restricted-imports': [ + 'error', + { + patterns: [ + { + group: ['vite', '**/vite/**', '**/core/**', '**/cli.js', '**/runner.js'], + message: + 'This code is bundled into user apps and must not import Vite or the build pipeline (src/core, src/exports/vite).' + }, + { + group: ['**/messages/build-*.js', '**/messages/internal/build.js'], + message: + 'Build diagnostics always include their full text. Use src/messages/shared-errors.js in code that is bundled into user apps.' + } + ] + } + ] + } + }, + { + // code that runs in Node, where dynamic imports of absolute paths + // need `pathToFileURL` to work on Windows + files: [ + 'packages/kit/src/**/*.js', + 'packages/adapter-*/*.js', + 'packages/adapter-*/src/**/*.js', + 'packages/package/src/**/*.js' + ], + // the client runtime's dynamic imports are resolved by vite, not Node + ignores: ['packages/kit/src/runtime/**'], + plugins: { + 'kit-node-custom': { + rules: { + 'require-path-to-file-url': requirePathToFileURL + } + } + }, + rules: { + 'kit-node-custom/require-path-to-file-url': 'error' + } + }, { ignores: [ '**/.svelte-kit', @@ -33,19 +98,28 @@ export default [ '**/test-results', '**/dist', '**/.custom-out-dir', - 'packages/adapter-*/files', 'packages/kit/src/core/config/fixtures/multiple', // dir contains svelte config with multiple extensions tripping eslint + 'packages/kit/src/core/sync/create_manifest_data/test/samples/**/*', + 'packages/kit/src/core/sync/write_types/test/*/**/*', + 'packages/kit/scripts/process-messages/templates/**/*', // contains generator placeholders 'packages/kit/types/index.d.ts', // generated file + 'packages/*/test/apps/**/*', 'packages/*/test/**/build/**', - 'packages/package/test/fixtures/typescript-svelte-config/expected', + 'packages/kit/test/build-errors/**/*', + 'packages/kit/test/prerendering/**/*', + 'packages/test-redirect-importer/index.js', 'packages/package/test/errors/**/*', - 'packages/package/test/fixtures/**/*' + 'packages/package/test/fixtures/**/*', + 'packages/package/test/watch/expected/**/*', + 'packages/package/test/watch/package/**/*' ] }, { languageOptions: { parserOptions: { - projectService: true + projectService: { + allowDefaultProject: ['packages/kit/src/runtime/app/service-worker/index.js'] + } } }, rules: { @@ -53,6 +127,17 @@ export default [ '@typescript-eslint/no-unused-expressions': 'off', '@typescript-eslint/require-await': 'error', '@typescript-eslint/no-floating-promises': 'error', + '@typescript-eslint/no-misused-promises': [ + 'error', + { + // we turn these off because it's common to pass an async callback to + // a synchronous callback parameter such as `setTimeout(...)` + checksVoidReturn: { + arguments: false, + properties: false + } + } + ], '@typescript-eslint/no-unused-vars': [ 'error', { @@ -65,20 +150,6 @@ export default [ ignoreRestSiblings: true } ] - }, - ignores: [ - 'packages/adapter-cloudflare/test/apps/**/*', - 'packages/adapter-netlify/test/apps/**/*', - 'packages/adapter-node/rollup.config.js', - 'packages/adapter-node/smoke.spec_disabled.js', - 'packages/adapter-static/test/apps/**/*', - 'packages/adapter-vercel/test/apps/**/*', - 'packages/kit/src/core/sync/create_manifest_data/test/samples/**/*', - 'packages/kit/test/apps/**/*', - 'packages/kit/test/build-errors/**/*', - 'packages/kit/test/prerendering/**/*', - 'packages/test-redirect-importer/index.js', - 'packages/adapter-netlify/test/preview.js' - ] + } } ]; diff --git a/package.json b/package.json index a42dedd6c0f3..baacba238cf7 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,13 @@ "type": "module", "scripts": { "test:kit": "pnpm run --dir packages/kit test", + "test:kit:unit": "cd packages/kit && vitest run -c vitest.kit.config.js", + "test:kit:dev": "pnpm run --dir packages/kit test:dev", + "test:kit:build": "pnpm run --dir packages/kit test:build", + "test:kit:dev:basics": "pnpm run --dir packages/kit test:dev:basics", + "test:kit:dev:rest": "pnpm run --dir packages/kit test:dev:rest", + "test:kit:build:basics": "pnpm run --dir packages/kit test:build:basics", + "test:kit:build:rest": "pnpm run --dir packages/kit test:build:rest", "test:cross-platform:dev": "pnpm run --dir packages/kit test:cross-platform:dev", "test:cross-platform:build": "pnpm run --dir packages/kit test:cross-platform:build", "test:server-side-route-resolution:dev": "pnpm run --dir packages/kit test:server-side-route-resolution:dev", @@ -13,14 +20,15 @@ "test:svelte-async:dev": "pnpm run --dir packages/kit test:svelte-async:dev", "test:svelte-async:build": "pnpm run --dir packages/kit test:svelte-async:build", "test:vite-ecosystem-ci": "pnpm --dir packages/kit test", - "test:others": "pnpm -r --filter='./packages/*' --filter=!./packages/kit/ --workspace-concurrency=1 test", - "check": "pnpm -r prepublishOnly && pnpm -r check", - "lint": "pnpm -r lint && eslint --cache --cache-location node_modules/.eslintcache 'packages/**/*.js'", - "format": "pnpm -r format", + "test:others": "pnpm -F @sveltejs/kit prepublishOnly && pnpm -r --filter='./packages/*' --filter=!./packages/kit/ test", + "test:others:unit": "pnpm -F @sveltejs/kit prepublishOnly && pnpm -r --filter=\"./packages/*\" --filter=\"!./packages/kit\" --workspace-concurrency=1 test:unit", + "test:unit": "vitest run", + "check": "pnpm --filter='./packages/**' prepublishOnly && pnpm --filter='./packages/**' --filter='!./packages/kit/test/build-errors/**' check", + "lint": "oxfmt --check . && echo '\nRunning eslint...' && eslint --cache --cache-location node_modules/.eslintcache 'packages/**/*.{js,d.ts}'", + "format": "oxfmt --write .", "precommit": "pnpm format && pnpm lint", "changeset:version": "changeset version && pnpm -r generate:version && git add --all", "changeset:release": "changeset publish", - "build": "pnpm --filter '@sveltejs/*' -r build", "sync-all": "node scripts/sync-all.js" }, "devDependencies": { @@ -28,12 +36,13 @@ "@changesets/cli": "catalog:", "@playwright/test": "catalog:", "@sveltejs/eslint-config": "catalog:", + "@types/node": "catalog:", "eslint": "catalog:", - "prettier": "catalog:", - "prettier-plugin-svelte": "catalog:" + "oxfmt": "^0.70.0", + "vitest": "catalog:" }, - "packageManager": "pnpm@10.34.3+sha512.f2c531b08829d7be7f03c90addc266615f9c5477463e4bf1d275cb263ce9ea57cf0d5599110d94649a9bb3fed9f0b5efcea74a7016b61532a24cb9ad87860c1d", + "packageManager": "pnpm@12.5.0+sha512.9cdbaa34ffacae1768635ac0d23e94db6201c7d59bf3da236b23d67c8f6b794d1dab323bcd5bcc51b55c8cafbf6f19a24e4aa61d6ab7772aa3b5cc85e325dc4d", "engines": { - "pnpm": ">=9.0.0" + "pnpm": ">=11.0.0" } } diff --git a/packages/adapter-auto/CHANGELOG.md b/packages/adapter-auto/CHANGELOG.md index 4e788f2b032e..96cc0897b168 100644 --- a/packages/adapter-auto/CHANGELOG.md +++ b/packages/adapter-auto/CHANGELOG.md @@ -1,5 +1,58 @@ # @sveltejs/adapter-auto +## 8.0.0-next.4 + +### Minor Changes + +- feat: add zero-config deployment support for Render ([#16850](https://github.com/sveltejs/kit/pull/16850)) + +### Patch Changes + +- Updated dependencies [[`9a4b343`](https://github.com/sveltejs/kit/commit/9a4b343bd8ab0060dc624fbdd139155fc4a0a1dc), [`ee2f55b`](https://github.com/sveltejs/kit/commit/ee2f55b1e3338d2fa92a25fe53d095129edf1002), [`65673a4`](https://github.com/sveltejs/kit/commit/65673a4dd35054c16f8555eb896dfb773d86a556), [`1b79845`](https://github.com/sveltejs/kit/commit/1b79845e117f417b63fa399149816d73882c27ea), [`b12bb78`](https://github.com/sveltejs/kit/commit/b12bb789c17c30e7ff835de4af004d2f3eb36159), [`03f1687`](https://github.com/sveltejs/kit/commit/03f1687fe612ce3d2d9131139b5b188d9cf90c64), [`49f0808`](https://github.com/sveltejs/kit/commit/49f0808f3e983d0cb5a4d586cf0d1678467431ed), [`e7c74bf`](https://github.com/sveltejs/kit/commit/e7c74bf8227dc228cb36e822eaff5992569b830a), [`0876d63`](https://github.com/sveltejs/kit/commit/0876d633145088c7a5213e9f530cf2fed96ce35d), [`9a2b9ba`](https://github.com/sveltejs/kit/commit/9a2b9bacbb2de0533966bb029ef526b357aac086), [`c9f54b5`](https://github.com/sveltejs/kit/commit/c9f54b5d66acd9443d36b0ae9aa0a656809bd7f9), [`22b8152`](https://github.com/sveltejs/kit/commit/22b8152f74882775ac63e1cf96d3016b92ac8256), [`f6d44cf`](https://github.com/sveltejs/kit/commit/f6d44cf007d6c6b54de57f6ae4ef8e2e7fea2f44), [`3c3a27b`](https://github.com/sveltejs/kit/commit/3c3a27bf67a26164266e132dbb7f9266643cf724), [`7f376b1`](https://github.com/sveltejs/kit/commit/7f376b109cd770638806184cf5d8bf4cf88c5bf2), [`8a587c7`](https://github.com/sveltejs/kit/commit/8a587c7b3919408ec66a362eb249fe5cf43f5316), [`aa4f960`](https://github.com/sveltejs/kit/commit/aa4f9605761e686f6aaf414b9186c68ddd2468dc)]: + - @sveltejs/kit@3.0.0-next.27 + +## 8.0.0-next.3 + +### Major Changes + +- breaking: require SvelteKit 3 + +### Patch Changes + +- fix: convert resolved adapter path to a file URL before importing, so builds work on Windows + +- fix: allow prerelease versions of SvelteKit 3 to satisfy the peer dependency range +- Updated dependencies [[`bbd1e2f`](https://github.com/sveltejs/kit/commit/bbd1e2fcc9af58edb9846393b081be61df4789d5), [`e21d27b`](https://github.com/sveltejs/kit/commit/e21d27ba490c89da98dbad24fca364d0163137c4), [`6044d05`](https://github.com/sveltejs/kit/commit/6044d055031bc17deedc069dc4016565364bd4f4), [`7fb611f`](https://github.com/sveltejs/kit/commit/7fb611fb604e4d74cd889f5fe21ccd255f876382), [`9f06b3a`](https://github.com/sveltejs/kit/commit/9f06b3ac78be30d27b07fc00ea40bda493c573aa), [`8763327`](https://github.com/sveltejs/kit/commit/87633271ce40ac0839be9468598e965bde3cad51), [`7dc2379`](https://github.com/sveltejs/kit/commit/7dc23798cde97b5616049ba8d443a07e8c558bdc), [`a70c3eb`](https://github.com/sveltejs/kit/commit/a70c3eb31fe1e28f1532c84ec569ec7099da3344), [`89f49c7`](https://github.com/sveltejs/kit/commit/89f49c70b7fb4883cf704d2fb57d2d8595790ad2), [`f36b3a4`](https://github.com/sveltejs/kit/commit/f36b3a4cd307bd8c89e03fa191becc175e7b3077), [`1144a63`](https://github.com/sveltejs/kit/commit/1144a63d318c3c3255d06b374bb7a656a469fabf), [`9f06b3a`](https://github.com/sveltejs/kit/commit/9f06b3ac78be30d27b07fc00ea40bda493c573aa)]: + - @sveltejs/kit@3.0.0-next.15 + +## 8.0.0-next.2 + +### Patch Changes + +- fix: convert resolved adapter path to a file URL before importing, so builds work on Windows ([#16618](https://github.com/sveltejs/kit/pull/16618)) +- Updated dependencies [[`c3f58bb`](https://github.com/sveltejs/kit/commit/c3f58bb0eb6bde790897f1e8b89019bc39a062e5), [`b655dc7`](https://github.com/sveltejs/kit/commit/b655dc7cf043ff01d53e494e9744e548bb3ad9fd), [`a70cc4f`](https://github.com/sveltejs/kit/commit/a70cc4f2ca24e7099c898456085ffe18d191657e), [`b325afb`](https://github.com/sveltejs/kit/commit/b325afb509b54445f678348e88ea2bf0152f4cec), [`e76b3d7`](https://github.com/sveltejs/kit/commit/e76b3d778bacb429fdb40a41b13ff04b54d97c1f), [`58f47eb`](https://github.com/sveltejs/kit/commit/58f47eb94016c5405829667bbc873e027c7b363c), [`fe9d8d9`](https://github.com/sveltejs/kit/commit/fe9d8d939b6d1a07b2957104fda6a1123f5eebd3), [`0c7bbd7`](https://github.com/sveltejs/kit/commit/0c7bbd7bd4a346c560e7cbf9b39ba8bda7c718f0), [`08d7e2a`](https://github.com/sveltejs/kit/commit/08d7e2a1b327d4e68c217f6e7d6b305732c82b1b), [`f70f679`](https://github.com/sveltejs/kit/commit/f70f679143d4330b6d3b7d0e690145b5487df97f), [`0624b26`](https://github.com/sveltejs/kit/commit/0624b2678efd41223f70972f7bff2d81d4a5c7f9), [`0624b26`](https://github.com/sveltejs/kit/commit/0624b2678efd41223f70972f7bff2d81d4a5c7f9), [`f4222ad`](https://github.com/sveltejs/kit/commit/f4222adc504a7041b50325424a3eb86af02961ed), [`089628b`](https://github.com/sveltejs/kit/commit/089628b0d9637578085ad61b5786cd1c4476e399), [`d8617b3`](https://github.com/sveltejs/kit/commit/d8617b3f2f08144f6acfc7c636b89d62b982a33d), [`c7369a1`](https://github.com/sveltejs/kit/commit/c7369a148169dfd51af150c461ff266977b6befd), [`5e5e592`](https://github.com/sveltejs/kit/commit/5e5e592f093d05c6ea30a41a0ebf543829069c5f), [`60057c1`](https://github.com/sveltejs/kit/commit/60057c1d2a1e6429e5f315d587ebc01baef6dc94), [`f70f679`](https://github.com/sveltejs/kit/commit/f70f679143d4330b6d3b7d0e690145b5487df97f), [`38f1528`](https://github.com/sveltejs/kit/commit/38f15289e249942275eab1281516702de08b7a30), [`40092f3`](https://github.com/sveltejs/kit/commit/40092f34aa063d033fc7a4d1d6fb1c10abd8cbf6), [`ff4247e`](https://github.com/sveltejs/kit/commit/ff4247e2376862207dd9df7317f39d8bb8dd9ee4), [`79a5de5`](https://github.com/sveltejs/kit/commit/79a5de581fc1bb708879678b8295706221ba0f0f), [`a70cc4f`](https://github.com/sveltejs/kit/commit/a70cc4f2ca24e7099c898456085ffe18d191657e)]: + - @sveltejs/kit@3.0.0-next.14 + +## 8.0.0-next.1 + +### Patch Changes + +- fix: allow prerelease versions of SvelteKit 3 to satisfy the peer dependency range ([#16286](https://github.com/sveltejs/kit/pull/16286)) +- Updated dependencies [[`737d119`](https://github.com/sveltejs/kit/commit/737d119730ec163832f838b3813c9cbb75dd799f), [`fa78efb`](https://github.com/sveltejs/kit/commit/fa78efb626031497eecc3a88fa7b6f6da36eca0f), [`07c207e`](https://github.com/sveltejs/kit/commit/07c207ed919570d4ed4954f751056b4f838abdea), [`a47071b`](https://github.com/sveltejs/kit/commit/a47071bb6581fe5f6df4cc47b80e9f0f594624e5), [`14d7d5a`](https://github.com/sveltejs/kit/commit/14d7d5abd9cd8462e4c79ed6a5c10bc43258e97a), [`5c38e51`](https://github.com/sveltejs/kit/commit/5c38e515db7fbb92e5ae01db84b4f0040a02f187), [`0702baa`](https://github.com/sveltejs/kit/commit/0702baa58b2a4600e83b55ef4a85e9cbe3391a4f), [`e1938c6`](https://github.com/sveltejs/kit/commit/e1938c62348b8ccc83e186b1acad76a0a10cc9ea), [`8293144`](https://github.com/sveltejs/kit/commit/8293144b6fbfff9f97cdb1d2224a5eaa04c84fc4), [`f76d7d9`](https://github.com/sveltejs/kit/commit/f76d7d91e18a0afd1f6798c095eb8c170c108cff), [`ab5c253`](https://github.com/sveltejs/kit/commit/ab5c253efb2546ead74db4c8c9b4c7639d97e0ed), [`b557b1b`](https://github.com/sveltejs/kit/commit/b557b1ba76903e1fb0fc3bb87bddc5420332137a), [`4a513e2`](https://github.com/sveltejs/kit/commit/4a513e27ab2ad44dae0c6a4a6c8deda840008530)]: + - @sveltejs/kit@3.0.0-next.8 + +## 8.0.0-next.0 +### Major Changes + + +- breaking: require SvelteKit 3 ([#15506](https://github.com/sveltejs/kit/pull/15506)) + + +### Patch Changes + +- Updated dependencies [[`fa335bd`](https://github.com/sveltejs/kit/commit/fa335bd8af3a982d68c4d34b813b68887dc7cce8), [`3031d89`](https://github.com/sveltejs/kit/commit/3031d89087d49297e89b6566108a1a1105c38475), [`cb9d416`](https://github.com/sveltejs/kit/commit/cb9d416d05266b6237f2856fb94df5b8a9b04279), [`caf3a18`](https://github.com/sveltejs/kit/commit/caf3a18de6581ab7d454d6fa8ad37ebdbdef95e6), [`4777827`](https://github.com/sveltejs/kit/commit/4777827246727045cf0209399751377564e7951d), [`a2792e2`](https://github.com/sveltejs/kit/commit/a2792e2cdc2dee91c20c7bfd921a104b539abae5), [`ba36148`](https://github.com/sveltejs/kit/commit/ba36148577ed0bc2675034a3d57001d71d328cd1), [`48e8710`](https://github.com/sveltejs/kit/commit/48e8710889b348a039322e05f3d2672327d56184), [`e2f3075`](https://github.com/sveltejs/kit/commit/e2f3075ef299b91175fae116e6a76269002029cf), [`047d6a0`](https://github.com/sveltejs/kit/commit/047d6a0f1c3fd1bc6f90a81177a2834b023a64b3), [`87603d1`](https://github.com/sveltejs/kit/commit/87603d13d009cada171c9b5d5daad5adad4f72de), [`096962c`](https://github.com/sveltejs/kit/commit/096962c47b0b0f63acd0d539d3973797e4867487), [`d545970`](https://github.com/sveltejs/kit/commit/d54597018308068aea5268e6c45b26398cd57607), [`e2f3075`](https://github.com/sveltejs/kit/commit/e2f3075ef299b91175fae116e6a76269002029cf), [`d06affc`](https://github.com/sveltejs/kit/commit/d06affc9246c676fc32b644efc0bbf53b42bc67d), [`8af47eb`](https://github.com/sveltejs/kit/commit/8af47eb06f9a1a883058de059ccf81aab4e85627), [`e2f3075`](https://github.com/sveltejs/kit/commit/e2f3075ef299b91175fae116e6a76269002029cf), [`5c4d130`](https://github.com/sveltejs/kit/commit/5c4d130ea2953464d8b4b462eb5aa4641aa78da1), [`3f11f35`](https://github.com/sveltejs/kit/commit/3f11f35125c5b3625891f0d00d9c7c826e6eaea2), [`caf3a18`](https://github.com/sveltejs/kit/commit/caf3a18de6581ab7d454d6fa8ad37ebdbdef95e6), [`8823037`](https://github.com/sveltejs/kit/commit/8823037b4a341013cc40a9bb688ee23bda3dbb04), [`1d76212`](https://github.com/sveltejs/kit/commit/1d76212740f59ed58839ef569899c653870834fd), [`0dc0548`](https://github.com/sveltejs/kit/commit/0dc0548307a439b6a6f24b14405366ced213f828), [`00d81fa`](https://github.com/sveltejs/kit/commit/00d81fab618435bac95b1bacb5e9734b61ba1af6)]: + - @sveltejs/kit@3.0.0-next.0 + ## 7.0.1 ### Patch Changes diff --git a/packages/adapter-auto/adapters.js b/packages/adapter-auto/adapters.js index 77f1990e8c7f..1f99037f50f5 100644 --- a/packages/adapter-auto/adapters.js +++ b/packages/adapter-auto/adapters.js @@ -7,19 +7,19 @@ export const adapters = [ name: 'Vercel', test: () => !!process.env.VERCEL, module: '@sveltejs/adapter-vercel', - version: '6' + version: '7' }, { name: 'Cloudflare Pages', test: () => !!process.env.CF_PAGES, module: '@sveltejs/adapter-cloudflare', - version: '7' + version: '8' }, { name: 'Netlify', test: () => !!process.env.NETLIFY, module: '@sveltejs/adapter-netlify', - version: '6' + version: '7' }, { name: 'Azure Static Web Apps', @@ -37,6 +37,13 @@ export const adapters = [ name: 'Google Cloud Run', test: () => !!process.env.GCP_BUILDPACKS, module: '@sveltejs/adapter-node', - version: '5' + version: '6' + }, + { + name: 'Render', + test: () => !!process.env.RENDER, + module: '@sveltejs/adapter-node', + // TODO replace with a stable version + version: 'next' } ]; diff --git a/packages/adapter-auto/index.js b/packages/adapter-auto/index.js index 64bc04312afc..ef8aaf7ae0a4 100644 --- a/packages/adapter-auto/index.js +++ b/packages/adapter-auto/index.js @@ -1,20 +1,10 @@ import { execSync } from 'node:child_process'; +import { pathToFileURL } from 'node:url'; import { adapters } from './adapters.js'; import path from 'node:path'; import fs from 'node:fs'; import process from 'node:process'; -/** - * @template T - * @template {keyof T} K - * @typedef {Partial> & Required>} PartialExcept - */ - -/** - * We use a custom `Builder` type here to support the minimum version of SvelteKit. - * @typedef {PartialExcept} Builder2_0_0 - */ - /** @type {Record string>} */ const commands = { npm: (name, version) => `npm install -D ${name}@${version}`, @@ -133,7 +123,7 @@ async function get_adapter() { } /** @type {{ default: () => Adapter }} */ - const module = await import(resolved); + const module = await import(pathToFileURL(resolved).href); const adapter = module.default(); @@ -146,14 +136,13 @@ async function get_adapter() { }; } -/** @type {() => Adapter} */ +/** @type {typeof import('./index.js').default} */ export default () => ({ name: '@sveltejs/adapter-auto', - /** @param {Builder2_0_0} builder */ adapt: async (builder) => { const adapter = await get_adapter(); - if (adapter) return adapter.adapt(/** @type {import('@sveltejs/kit').Builder} */ (builder)); + if (adapter) return adapter.adapt(builder); builder.log.warn( 'Could not detect a supported production environment. See https://svelte.dev/docs/kit/adapters to learn how to configure your app to run on the platform of your choosing' diff --git a/packages/adapter-auto/package.json b/packages/adapter-auto/package.json index ae2e87f09b59..03713566583c 100644 --- a/packages/adapter-auto/package.json +++ b/packages/adapter-auto/package.json @@ -1,6 +1,6 @@ { "name": "@sveltejs/adapter-auto", - "version": "7.0.1", + "version": "8.0.0-next.4", "description": "Automatically chooses the SvelteKit adapter for your current environment, if possible.", "keywords": [ "adapter", @@ -28,24 +28,24 @@ }, "types": "index.d.ts", "files": [ - "files", "index.js", "index.d.ts", "adapters.js" ], "scripts": { - "lint": "prettier --check .", - "format": "pnpm lint --write", - "check": "tsc", - "test": "vitest run" + "lint": "oxfmt --check .", + "format": "oxfmt --write .", + "check": "node node_modules/@typescript/native/bin/tsc", + "test": "vitest run", + "test:unit": "vitest run" }, "devDependencies": { "@sveltejs/kit": "workspace:^", "@types/node": "catalog:", - "typescript": "catalog:", + "@typescript/native": "catalog:", "vitest": "catalog:" }, "peerDependencies": { - "@sveltejs/kit": "^2.0.0" + "@sveltejs/kit": "^3.0.0-next.0" } } diff --git a/packages/adapter-auto/test/adapters.spec.js b/packages/adapter-auto/test/adapters.spec.js index 89b98c0bbbfb..c5c08abea33d 100644 --- a/packages/adapter-auto/test/adapters.spec.js +++ b/packages/adapter-auto/test/adapters.spec.js @@ -11,6 +11,12 @@ test('adapter versions are up to date', () => { } const adapter_version = JSON.parse(readFileSync(package_json, 'utf-8')).version; const [major] = adapter_version.split('.'); - assert.equal(adapter.version, major, `${adapter.name} adapter is outdated`); + + // TODO get rid of `next` support once we exit pre-release mode + if (adapter.version === 'next') { + console.warn(`Update ${adapter.name} config to use a stable version`); + } else { + assert.equal(adapter.version, major, `${adapter.name} adapter is outdated`); + } } }); diff --git a/packages/adapter-bun/.gitignore b/packages/adapter-bun/.gitignore new file mode 100644 index 000000000000..7b1c9a0ade28 --- /dev/null +++ b/packages/adapter-bun/.gitignore @@ -0,0 +1,5 @@ +.DS_Store +node_modules +/files +coverage/ + diff --git a/packages/adapter-bun/CHANGELOG.md b/packages/adapter-bun/CHANGELOG.md new file mode 100644 index 000000000000..e1066bb850a9 --- /dev/null +++ b/packages/adapter-bun/CHANGELOG.md @@ -0,0 +1,57 @@ +# @sveltejs/adapter-bun + +## 1.0.0-next.5 + +### Patch Changes + +- fix: bundle Svelte libraries listed in `dependencies` instead of externalising them ([#17234](https://github.com/sveltejs/kit/pull/17234)) +- Updated dependencies [[`f8a90be`](https://github.com/sveltejs/kit/commit/f8a90be73c61dee7d586b26d19dad59637de63b0), [`30d06f5`](https://github.com/sveltejs/kit/commit/30d06f5698528cf540899900366fc0cfeb3dc7f6), [`0107721`](https://github.com/sveltejs/kit/commit/0107721dbdde1310d4c5427f7ad33a2ee5593d3e)]: + - @sveltejs/kit@3.0.0-next.30 + +## 1.0.0-next.4 + +### Patch Changes + +- fix: always bundle dev dependencies ([#17210](https://github.com/sveltejs/kit/pull/17210)) +- Updated dependencies [[`058d29b`](https://github.com/sveltejs/kit/commit/058d29be2cbedb91546b6f6ddf157b881b87360b), [`5e11958`](https://github.com/sveltejs/kit/commit/5e119589bb8784cb43fd631afaa81b78b3ff6f36)]: + - @sveltejs/kit@3.0.0-next.29 + +## 1.0.0-next.3 + +### Major Changes + +- breaking: build the server with Vite instead of a second `Bun.build` pass, keeping production dependencies external; `buildOptions` now only applies to executables ([#17120](https://github.com/sveltejs/kit/pull/17120)) + +### Patch Changes + +- Updated dependencies [[`abf47df`](https://github.com/sveltejs/kit/commit/abf47df887f914f09c39a7f771d32cdd55fb346d), [`df3f003`](https://github.com/sveltejs/kit/commit/df3f0034b541701bd3319225f4e6003579d4ebd6), [`3c044cd`](https://github.com/sveltejs/kit/commit/3c044cda5c356dede92167dcca549cd58e73e61f), [`509dd14`](https://github.com/sveltejs/kit/commit/509dd146696b2c77ae3ac8ac2435773864520016), [`5d8722c`](https://github.com/sveltejs/kit/commit/5d8722c60977efb6e4606fefd9750c2349af410a), [`462111a`](https://github.com/sveltejs/kit/commit/462111a1411551386c6e1aa6c14f967f4ae875c2), [`9f3342f`](https://github.com/sveltejs/kit/commit/9f3342fb9305d1fd731a49497bec894469073fd0), [`224e791`](https://github.com/sveltejs/kit/commit/224e791cb05f95a5c2d881e5a7d5341352f3c052), [`8614b01`](https://github.com/sveltejs/kit/commit/8614b016e3be4247cb1aae615870f4c23a0622b3), [`65f93c8`](https://github.com/sveltejs/kit/commit/65f93c83b29f47ebc05bfdeac53f8c92c8d522e1), [`147f09a`](https://github.com/sveltejs/kit/commit/147f09ab7397727f17b5f35ff7535eb1a089180f), [`09b8de8`](https://github.com/sveltejs/kit/commit/09b8de849561b5c81112dbe029e9279e8184903c), [`68c6ef9`](https://github.com/sveltejs/kit/commit/68c6ef97fad9baf6c883e203b1ceb686e2ca74a1), [`377fc5d`](https://github.com/sveltejs/kit/commit/377fc5dd93054ba01ec0e9623036735ff7aadfb5), [`84cb5fd`](https://github.com/sveltejs/kit/commit/84cb5fd2173028e29edf87bcbfc551f34dc2e1a9), [`99492ac`](https://github.com/sveltejs/kit/commit/99492ac605a74e066623d8934fa9f662d4350c99), [`4ac8c9b`](https://github.com/sveltejs/kit/commit/4ac8c9b4b9f015769f81adb941c85cf42fc17424), [`8efeeb3`](https://github.com/sveltejs/kit/commit/8efeeb3dea17c6dd3a1cdf55fc25ba0a53e6ee4c), [`4c812c8`](https://github.com/sveltejs/kit/commit/4c812c808c6b62c78b89fb0021dcfe45e06afcc7), [`fbbb4c7`](https://github.com/sveltejs/kit/commit/fbbb4c7d3c4ff3a3792a58db26c6be77996a392d), [`c6ba86b`](https://github.com/sveltejs/kit/commit/c6ba86ba7983a53f0d1e16c0a6462009d5245d4d), [`7847e4e`](https://github.com/sveltejs/kit/commit/7847e4e27f17a0a94b29f88d79050d704436957c), [`68791be`](https://github.com/sveltejs/kit/commit/68791be662e01afd7235e1eb6a2b6b7acf332b17), [`f8258ee`](https://github.com/sveltejs/kit/commit/f8258ee5e042d48d198cca3f35982e90db0077cb), [`4da6320`](https://github.com/sveltejs/kit/commit/4da6320db8d70b73b93e142e4bb6ba9173be3b97), [`b21766f`](https://github.com/sveltejs/kit/commit/b21766f362e39f51cb4ae8f894ea5aca40cf305e), [`a2bfcaf`](https://github.com/sveltejs/kit/commit/a2bfcafbe496e23b1e0bcfce442309bb94783efd), [`680405d`](https://github.com/sveltejs/kit/commit/680405d2d06f02774a8d7df5351df4a0d259c338), [`755024a`](https://github.com/sveltejs/kit/commit/755024a1af3ffcb80cdf939b2cf50ada3a2cb684), [`3a7d329`](https://github.com/sveltejs/kit/commit/3a7d3290e28dffa730d0c71699906dbc6d2fd584)]: + - @sveltejs/kit@3.0.0-next.28 + +## 1.0.0-next.2 + +### Major Changes + +- breaking: require Bun 1.4, which routes `HEAD` to `GET` handlers and settles `stop()` after a force close ([#16880](https://github.com/sveltejs/kit/pull/16880)) + +### Patch Changes + +- fix: build apps that use server instrumentation ([#16898](https://github.com/sveltejs/kit/pull/16898)) +- Updated dependencies [[`ff8cdd4`](https://github.com/sveltejs/kit/commit/ff8cdd4df8f6a0ee26c25854aa0656f2dc968caf), [`723572c`](https://github.com/sveltejs/kit/commit/723572c76053ebcae02b19166fe7400f45175c70), [`3b8e034`](https://github.com/sveltejs/kit/commit/3b8e034d62f1372f0e450da88c0705bd9828a816), [`f2c5102`](https://github.com/sveltejs/kit/commit/f2c5102079c80c904b3fe165666813023248facc), [`c66a6ed`](https://github.com/sveltejs/kit/commit/c66a6ed5bb6ba4594b0a952744a1d9c7e457b001), [`428e5ef`](https://github.com/sveltejs/kit/commit/428e5efeedba49ccd42d43f85e53707f6704931c)]: + - @sveltejs/kit@3.0.0-next.26 + +## 1.0.0-next.1 + +### Minor Changes + +- feat: add a Bun-native adapter with static file serving and single-executable support ([#16695](https://github.com/sveltejs/kit/pull/16695)) + +### Patch Changes + +- Updated dependencies [[`9b3d195`](https://github.com/sveltejs/kit/commit/9b3d1955cb40042cbc411637dab3064ddfa6b1a5), [`385d378`](https://github.com/sveltejs/kit/commit/385d378dd1281e1b69417bf4980a169d67a49314), [`3782448`](https://github.com/sveltejs/kit/commit/37824483ca6ccf802942740f0da31b149d09e077), [`d0d3a33`](https://github.com/sveltejs/kit/commit/d0d3a33535cd2f3db1ed09680ccb34624b3abb77), [`4b7a483`](https://github.com/sveltejs/kit/commit/4b7a4830e962ee404161197b3b016182c376ed61), [`e325d7d`](https://github.com/sveltejs/kit/commit/e325d7dfa4313c889d3f9ebea9e70552722c427e), [`4f63c79`](https://github.com/sveltejs/kit/commit/4f63c799ab8dd9ae01eb301d7a9f5d712b724d23)]: + - @sveltejs/kit@3.0.0-next.25 + +## 1.0.0-next.0 + +### Patch Changes + +- Initial release diff --git a/packages/adapter-bun/LICENSE b/packages/adapter-bun/LICENSE new file mode 100644 index 000000000000..22efd0c0dfa3 --- /dev/null +++ b/packages/adapter-bun/LICENSE @@ -0,0 +1,7 @@ +Copyright (c) 2020 [these people](https://github.com/sveltejs/kit/graphs/contributors) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/packages/adapter-bun/README.md b/packages/adapter-bun/README.md new file mode 100644 index 000000000000..98d1b28dda77 --- /dev/null +++ b/packages/adapter-bun/README.md @@ -0,0 +1,30 @@ +# @sveltejs/adapter-bun + +SvelteKit adapter that builds a standalone server for the [Bun](https://bun.com/) runtime. + +```sh +bun add -D @sveltejs/adapter-bun +``` + +```js +import adapter from '@sveltejs/adapter-bun'; +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [sveltekit({ adapter: adapter() })] +}); +``` + +Build with `bun run --bun build`, then run the default output with `bun ./build`. + +See the [adapter-bun documentation](https://svelte.dev/docs/kit/adapter-bun) for configuration, +environment variables, compiled executables, proxy setup, and the Bun-specific platform API. + +## Changelog + +[View the package changelog](https://github.com/sveltejs/kit/blob/main/packages/adapter-bun/CHANGELOG.md). + +## License + +[MIT](LICENSE) diff --git a/packages/adapter-bun/ambient.d.ts b/packages/adapter-bun/ambient.d.ts new file mode 100644 index 000000000000..27dcc17f0ce1 --- /dev/null +++ b/packages/adapter-bun/ambient.d.ts @@ -0,0 +1,12 @@ +/// + +import type { Server } from 'bun'; + +declare global { + namespace App { + export interface Platform { + /** The Bun HTTP server handling the request. */ + server: Server; + } + } +} diff --git a/packages/adapter-bun/index.d.ts b/packages/adapter-bun/index.d.ts new file mode 100644 index 000000000000..2208d3ce1c32 --- /dev/null +++ b/packages/adapter-bun/index.d.ts @@ -0,0 +1,71 @@ +import type { Adapter } from '@sveltejs/kit'; +import './ambient.js'; + +interface AdapterOptions { + /** + * The directory to build the server to. + * @default 'build' + */ + out?: string; + /** + * Generate `.br` and `.gz` variants of client and prerendered assets during the + * build. The generated routes negotiate `Accept-Encoding` per request, preferring + * brotli over gzip. Ignored when `buildOptions.compile` is set, because embedded + * assets are imported by identity path. + * @default false + */ + precompress?: boolean; + /** + * If you need to change the name of the environment variables used to configure + * the deployment (for example, to deconflict with environment variables you + * don't control), you can specify a prefix: + * + * ```js + * envPrefix: 'MY_CUSTOM_' + * ``` + * + * ```sh + * MY_CUSTOM_HOST=127.0.0.1 \ + * MY_CUSTOM_PORT=4000 \ + * bun ./build + * ``` + */ + envPrefix?: string; + /** + * Default options passed to `Bun.serve`. Environment variables take precedence. + * The options must be JSON-serializable. + */ + serverOptions?: Pick< + import('bun').Serve.Options, + | 'development' + | 'hostname' + | 'port' + | 'idleTimeout' + | 'maxRequestBodySize' + | 'reusePort' + | 'unix' + | 'ipv6Only' + >; + /** + * Options for the `Bun.build` call that turns the server into an executable. Set `compile` + * to create one; if it does not specify an outfile, the executable is written to + * `/server`. The entrypoint, output directory, top-level target, and module format + * are reserved. Without `compile`, the server is built by Vite and these options are ignored. + * @default {} + */ + buildOptions?: Pick< + import('bun').BuildConfig, + | 'sourcemap' + | 'minify' + | 'bytecode' + | 'banner' + | 'footer' + | 'drop' + | 'features' + | 'optimizeImports' + | 'external' + | 'compile' + >; +} + +export default function plugin(options?: AdapterOptions): Adapter; diff --git a/packages/adapter-bun/index.js b/packages/adapter-bun/index.js new file mode 100644 index 000000000000..6153b23b4c0d --- /dev/null +++ b/packages/adapter-bun/index.js @@ -0,0 +1,323 @@ +/** @import { Builder } from '@sveltejs/kit' */ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +// posix so it matches the module ids Vite reports on every platform +const files = fileURLToPath(new URL('./src', import.meta.url)).replaceAll('\\', '/'); +const handoff = '#@sveltejs/adapter-bun'; + +/** + * Matches sirv's default behaviour in adapter-node: dotfiles are not served, + * with an exception for the `.well-known` directory. + * @param {string} file + */ +function is_dotfile(file) { + return file + .split('/') + .some((segment, i) => segment.startsWith('.') && !(i === 0 && segment === '.well-known')); +} + +// bounds open file handles while every asset hashes concurrently +const MAX_OPEN_FILES = 64; +let open_files = 0; +/** @type {Array<() => void>} */ +const file_waiters = []; + +/** + * Streams the file through the hasher so build memory stays bounded by chunk + * size instead of total asset size. + * @param {string} file + * @returns {Promise} + */ +async function hash_file(file) { + if (open_files === MAX_OPEN_FILES) { + await new Promise((resolve) => { + file_waiters.push(() => resolve(undefined)); + }); + } + open_files++; + try { + const hasher = new Bun.CryptoHasher('blake2b256'); + for await (const chunk of Bun.file(file).stream()) { + hasher.update(chunk); + } + return hasher.digest('hex').slice(0, 16); + } finally { + open_files--; + file_waiters.shift()?.(); + } +} + +/** + * The build-time validator for conditional requests: Bun only generates ETags for + * in-memory static routes, not file-backed responses, so the adapter ships its own. + * @param {string} file + * @param {boolean} [precompress] + * @returns {Promise<{ hash: string, mtime: number, br?: boolean, gz?: boolean }>} + */ +async function asset_meta(file, precompress = false) { + const hash = await hash_file(file); + + /** @type {{ hash: string, mtime: number, br?: boolean, gz?: boolean }} */ + const meta = { hash, mtime: Bun.file(file).lastModified }; + if (precompress) { + if (fs.existsSync(`${file}.br`)) meta.br = true; + if (fs.existsSync(`${file}.gz`)) meta.gz = true; + } + + return meta; +} + +/** @param {string[]} files */ +function validate_file_paths(files) { + for (const file of files) { + if (file.includes('*')) { + throw new Error( + `Cannot build with ${JSON.stringify(file)} because Bun treats literal \`*\` characters in route paths as wildcards. Rename the file or route to remove the \`*\` character.` + ); + } + // a leading ':' would need percent-encoding, but browsers request the colon raw + if (file.split('/').some((segment) => segment.startsWith(':'))) { + throw new Error( + `Cannot build with ${JSON.stringify(file)} because Bun treats a route segment starting with \`:\` as a parameter. Rename the file or route so no segment starts with \`:\`.` + ); + } + } +} + +/** @type {import('./index.js').default} */ +export default function (opts = {}) { + const { + out = 'build', + envPrefix = '', + precompress = false, + serverOptions = {}, + buildOptions = {} + } = opts; + const embed = !!buildOptions.compile; + + return { + name: '@sveltejs/adapter-bun', + async adapt(builder) { + if (typeof Bun === 'undefined') { + throw new Error( + 'adapter-bun requires running the SvelteKit build with Bun. Use `bun run --bun build`.' + ); + } + + fs.rmSync(out, { recursive: true, force: true }); + + if (precompress && embed) { + builder.log.warn( + 'precompress is ignored with buildOptions.compile: embedded assets are imported by identity path' + ); + } + + builder.log.minor('Building server'); + + const server = builder.getServerDirectory(); + builder.generateServerInstance(`${server}/server.js`); + + // values only known once the app is built, next to server/ so the chunks' `../adapter-bun.js` resolves + const handoff_file = path.resolve(server, '../adapter-bun.js'); + fs.writeFileSync( + handoff_file, + [ + `import { dirname } from 'node:path';`, + `import { fileURLToPath } from 'node:url';`, + `export { server } from './server/server.js';`, + `export const dir = dirname(fileURLToPath(import.meta.url));`, + `export const app_dir = ${JSON.stringify(builder.config.appDir)};`, + `export const base = ${JSON.stringify(builder.config.paths.base || '/')};`, + `export const embed = ${embed};`, + `export const env_prefix = ${JSON.stringify(envPrefix)};`, + `export const origin = ${JSON.stringify(builder.config.paths.origin)};`, + `export const server_options = ${JSON.stringify(serverOptions)};`, + ...(await create_routes({ builder, out, embed, precompress: precompress && !embed })) + ].join('\n') + ); + + const entrypoint = `${server}/adapter-index.js`; + + if (builder.hasServerInstrumentationFile()) { + builder.instrument({ + entrypoint, + instrumentation: `${server}/instrumentation.server.js`, + initializer: builder.createInstrumentationInitializer({ outputDirectory: server }), + module: { exports: [] } + }); + } + + if (!embed) { + builder.copy(server, `${out}/server`); + builder.copy(handoff_file, `${out}/adapter-bun.js`); + fs.writeFileSync(`${out}/index.js`, `import './server/adapter-index.js';\n`); + return; + } + + const result = await Bun.build({ + ...buildOptions, + sourcemap: buildOptions.sourcemap ?? 'external', + entrypoints: [entrypoint], + target: 'bun', + format: 'esm', + conditions: ['bun', 'node'], + throw: false, + outdir: out, + compile: { + outfile: 'server', + ...(typeof buildOptions.compile === 'string' ? { target: buildOptions.compile } : {}), + ...(typeof buildOptions.compile === 'object' ? buildOptions.compile : {}) + } + }); + if (!result.success) { + for (const log of result.logs) { + // BuildMessage properties are not enumerable, so console.error(log) prints `{}` + const message = log.message ?? String(log); + if (log.level === 'error') builder.log.error(message); + else if (log.level === 'warning') builder.log.warn(message); + else builder.log.info(message); + } + throw new AggregateError(result.logs); + } + }, + + supports: { + read: () => true, + instrumentation: () => true + }, + + vite: { + plugins: { + post: [ + { + name: 'vite-plugin-sveltejs-adapter-bun', + apply: 'build', + config(config) { + const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); + // vite-plugin-svelte lists Svelte libraries here so their export conditions resolve at build time + const no_external = Array.isArray(config.ssr?.noExternal) + ? config.ssr.noExternal + : []; + + return { + ssr: { + // Vite doesn't bundle dependencies for SSR by default. Bundle everything + // except production dependencies that don't need the build's export conditions + external: Object.keys(pkg.dependencies || {}).filter( + (dep) => + !no_external.some((rule) => + typeof rule === 'string' ? rule === dep : rule.test(dep) + ) + ), + noExternal: true + }, + environments: { + ssr: { + build: { + rolldownOptions: { + // bundled with the app's server code so shared modules aren't duplicated + input: { 'adapter-index': `${files}/index.js` }, + // generated after the Vite build and rewritten to an output-relative path + external: [handoff], + output: { + paths: { [handoff]: '../adapter-bun.js' }, + // the hand-off path only holds at the output root, so adapter chunks may not nest + chunkFileNames: (chunk) => + chunk.moduleIds.some((id) => id.startsWith(files)) + ? 'adapter-bun-[name].js' + : 'chunks/[name].js' + } + } + } + } + } + }; + } + } + ] + } + } + }; +} + +/** + * The static route table, as data the bundled `src/routes.js` turns into Bun routes at startup. + * @param {object} options + * @param {Builder} options.builder + * @param {string} options.out + * @param {boolean} options.embed + * @param {boolean} options.precompress + * @returns {Promise} + */ +async function create_routes({ builder, out, embed, precompress }) { + // executables embed the files from a staging directory instead of shipping them in `out` + const dest = embed ? builder.getBuildDirectory('adapter-bun') : out; + if (embed) fs.rmSync(dest, { recursive: true, force: true }); + + const client_files = builder.writeClient(`${dest}/client`).filter((file) => !is_dotfile(file)); + const prerendered_files = builder.writePrerendered(`${dest}/prerendered`); + validate_file_paths([ + ...client_files, + ...prerendered_files, + ...builder.prerendered.pages.keys(), + ...builder.prerendered.redirects.keys() + ]); + + if (precompress) { + await Promise.all([ + builder.compress(`${dest}/client`), + builder.compress(`${dest}/prerendered`) + ]); + } + + /** @type {Map} */ + const embedded = new Map(); + + /** + * @param {string} helper + * @param {string} url + * @param {string} dir + * @param {string} [filename] + */ + const entry = async (helper, url, dir, filename = url) => { + const file = `${dest}/${dir}/${filename}`; + if (embed) embedded.set(file, `asset_${embedded.size}`); + return `[${JSON.stringify(helper)}, ${JSON.stringify(url)}, ${embedded.get(file) ?? JSON.stringify(filename)}, ${JSON.stringify(await asset_meta(file, precompress))}]`; + }; + + const pages = [...builder.prerendered.pages]; + const page_files = new Set(pages.map(([_, { file }]) => file)); + + const assets = await Promise.all([ + ...client_files.map((file) => entry('client_asset', file, 'client')), + ...pages.map(([path, { file }]) => entry('prerendered_page', path, 'prerendered', file)), + ...prerendered_files + .filter((file) => !page_files.has(file)) + .map((file) => entry('prerendered_asset', file, 'prerendered')) + ]); + + const server_assets = builder + .findServerAssets(builder.routes.filter((route) => route.prerender !== true)) + .map((file) => { + if (!embed) return `[${JSON.stringify(file)}]`; + const asset = embedded.get(`${dest}/client/${file}`); + if (asset === undefined) throw new Error(`Could not find server asset ${file}`); + return `[${JSON.stringify(file)}, ${asset}]`; + }); + + const redirects = [...builder.prerendered.redirects].map(([src, { status, location }]) => { + return [src, status, location]; + }); + + return [ + ...[...embedded].map( + ([file, asset]) => + `import ${asset} from ${JSON.stringify(path.resolve(file))} with { type: 'file' };` + ), + `export const assets = [\n${assets.join(',\n')}\n];`, + `export const redirects = ${JSON.stringify(redirects)};`, + `export const server_assets = [\n${server_assets.join(',\n')}\n];` + ]; +} diff --git a/packages/adapter-bun/internal.d.ts b/packages/adapter-bun/internal.d.ts new file mode 100644 index 000000000000..84f738bfb388 --- /dev/null +++ b/packages/adapter-bun/internal.d.ts @@ -0,0 +1,36 @@ +// generated by `adapt` in the output directory +declare module '#@sveltejs/adapter-bun' { + export const server: import('@sveltejs/kit').Server; + /** the output directory */ + export const dir: string; + export const app_dir: string; + export const base: string; + /** assets are embedded in an executable and imported by identity path */ + export const embed: boolean; + export const env_prefix: string; + export const origin: string | undefined; + export const server_options: Pick< + import('bun').Serve.Options, + | 'development' + | 'hostname' + | 'port' + | 'idleTimeout' + | 'maxRequestBodySize' + | 'reusePort' + | 'unix' + | 'ipv6Only' + >; + /** `[helper, url, file, meta]`, where `file` is an embedded asset or a path below `dir` */ + export const assets: Array< + [ + 'client_asset' | 'prerendered_asset' | 'prerendered_page', + string, + string, + { hash: string; mtime: number; br?: boolean; gz?: boolean } + ] + >; + /** `[url, status, location]` */ + export const redirects: Array<[string, number, string]>; + /** `[file, embedded asset]` */ + export const server_assets: Array<[string, string?]>; +} diff --git a/packages/adapter-bun/package.json b/packages/adapter-bun/package.json new file mode 100644 index 000000000000..759f00be4c9b --- /dev/null +++ b/packages/adapter-bun/package.json @@ -0,0 +1,56 @@ +{ + "name": "@sveltejs/adapter-bun", + "version": "1.0.0-next.5", + "description": "Adapter for SvelteKit apps that generates a standalone Bun server", + "keywords": [ + "adapter", + "bun", + "deploy", + "hosting", + "svelte", + "sveltekit" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/sveltejs/kit.git", + "directory": "packages/adapter-bun" + }, + "license": "MIT", + "homepage": "https://svelte.dev/docs/kit/adapter-bun", + "type": "module", + "exports": { + ".": { + "types": "./index.d.ts", + "import": "./index.js" + }, + "./package.json": "./package.json" + }, + "types": "index.d.ts", + "files": [ + "src/*.js", + "index.js", + "index.d.ts", + "ambient.d.ts" + ], + "scripts": { + "test": "bun test .spec", + "check": "tsc", + "lint": "prettier --check .", + "format": "pnpm lint --write" + }, + "devDependencies": { + "@playwright/test": "catalog:", + "@sveltejs/kit": "workspace:^", + "@types/node": "catalog:", + "typescript": "catalog:" + }, + "dependencies": { + "bun-types": "^1.3.14" + }, + "peerDependencies": { + "@sveltejs/kit": "^3.0.0-next.0" + }, + "engines": { + "bun": ">=1.4.0" + } +} diff --git a/packages/adapter-bun/src/env.js b/packages/adapter-bun/src/env.js new file mode 100644 index 000000000000..2dc88c6529f0 --- /dev/null +++ b/packages/adapter-bun/src/env.js @@ -0,0 +1,143 @@ +import process from 'node:process'; +import { env_prefix } from '#@sveltejs/adapter-bun'; + +const expected = new Set([ + 'SOCKET_PATH', + 'HOST', + 'PORT', + 'REUSE_PORT', + 'IPV6_ONLY', + 'CONNECTION_IDLE_TIMEOUT', + 'BODY_SIZE_LIMIT', + 'SHUTDOWN_TIMEOUT', + 'DEVELOPMENT', + 'XFF_DEPTH', + 'ADDRESS_HEADER', + 'PROTOCOL_HEADER', + 'HOST_HEADER', + 'PORT_HEADER' +]); + +if (env_prefix) { + for (const name in process.env) { + if (name.startsWith(env_prefix) && !expected.has(name.slice(env_prefix.length))) { + throw new Error( + `You should change envPrefix (${env_prefix}) to avoid conflicts with existing environment variables — unexpectedly saw ${name}` + ); + } + } +} + +/** + * @param {string} name + * @param {string} value + * @param {string} expected + * @returns {never} + */ +function parsing_error(name, value, expected) { + throw new Error( + `Invalid value for environment variable ${env_prefix + name}: ${JSON.stringify(value)} (expected ${expected})` + ); +} + +/** + * @template {string | undefined} [T=undefined] + * @param {string} name + * @param {T} [fallback] + * @returns {string | T} + */ +export function env(name, fallback) { + const prefixed = env_prefix + name; + return prefixed in process.env + ? /** @type {string} */ (process.env[prefixed]) + : /** @type {T} */ (fallback); +} + +/** @type {Record} */ +const BOOLEANS = { + 1: true, + true: true, + yes: true, + on: true, + 0: false, + false: false, + no: false, + off: false +}; + +/** + * @template {boolean | undefined} [T=undefined] + * @param {string} name + * @param {T} [fallback] + * @returns {boolean | T} + */ +export function boolean_env(name, fallback) { + const value = env(name); + if (value === undefined) return /** @type {T} */ (fallback); + return BOOLEANS[value.toLowerCase()] ?? parsing_error(name, value, 'a boolean'); +} + +/** + * @template {number | undefined} [T=undefined] + * @param {string} name + * @param {T} [fallback] + * @param {{ min?: number; max?: number }} [limits] + * @returns {number | T} + */ +export function number_env(name, fallback, limits = {}) { + const value = env(name); + if (value === undefined) return /** @type {T} */ (fallback); + if (!/^\d+$/.test(value)) { + parsing_error(name, value, 'a non-negative integer'); + } + + const number = Number(value); + if ( + !Number.isSafeInteger(number) || + number < (limits.min ?? 0) || + number > (limits.max ?? Infinity) + ) { + const range = + limits.max === undefined + ? `at least ${limits.min ?? 0}` + : `between ${limits.min ?? 0} and ${limits.max}`; + parsing_error(name, value, `an integer ${range}`); + } + + return number; +} + +/** + * @template {number | undefined} [T=undefined] + * @param {string} name + * @param {T} [fallback] + * @returns {number | T} + */ +export function bytes_env(name, fallback) { + const value = env(name); + if (value === undefined) return /** @type {T} */ (fallback); + // adapter-node documents Infinity as the value that disables the limit + if (value === 'Infinity') return Infinity; + if (!/^(?:\d+(?:\.\d*)?|\.\d+)(?:[KMG])?$/i.test(value)) { + parsing_error( + name, + value, + 'a non-negative number with an optional K, M, or G suffix, or Infinity' + ); + } + + const suffix = value.at(-1)?.toUpperCase(); + const multiplier = + { + K: 1024, + M: 1024 * 1024, + G: 1024 * 1024 * 1024 + }[/** @type {'K' | 'M' | 'G'} */ (suffix)] ?? 1; + const number = Number(multiplier === 1 ? value : value.slice(0, -1)) * multiplier; + + if (!Number.isSafeInteger(number)) { + parsing_error(name, value, 'a non-negative number of whole bytes'); + } + + return number; +} diff --git a/packages/adapter-bun/src/handler.js b/packages/adapter-bun/src/handler.js new file mode 100644 index 000000000000..a5cce744b787 --- /dev/null +++ b/packages/adapter-bun/src/handler.js @@ -0,0 +1,122 @@ +/** @import { Server as BunServer } from 'bun' */ +import { env_prefix, origin, server } from '#@sveltejs/adapter-bun'; +import { server_assets } from './routes.js'; +import { env, number_env } from './env.js'; + +const address_header = env('ADDRESS_HEADER', '').toLowerCase(); +const protocol_header = env('PROTOCOL_HEADER', '').toLowerCase(); +const host_header = env('HOST_HEADER', '').toLowerCase(); +const port_header = env('PORT_HEADER', '').toLowerCase(); +const xff_depth = number_env('XFF_DEPTH', 1, { min: 1 }); + +await server.init({ + env: Bun.env, + read: (file) => server_assets.get(file)?.stream() ?? null +}); + +/** + * The Bun-native SvelteKit request handler used by the generated server. + * @param {Request} request + * @param {BunServer} bun_server + * @returns {Promise} + */ +export async function handler(request, bun_server) { + const normalized_request = normalize_request(request); + if (normalized_request instanceof Response) return normalized_request; + + const response = await server.respond(normalized_request, { + platform: { server: bun_server }, + getClientAddress: () => get_client_address(request, bun_server) + }); + + if (response.headers.get('content-type')?.startsWith('text/event-stream')) { + bun_server.timeout(request, 0); + response.headers.set('x-accel-buffering', 'no'); + } + + return response; +} + +/** + * Rewrites the request onto the public origin the user actually requested. + * @param {Request} request + * @returns {Request | Response} + */ +function normalize_request(request) { + try { + // an empty Host header makes request.url relative, so parsing belongs in the try + const url = new URL(request.url); + const request_origin = origin || get_origin(request, url); + return request_origin === url.origin + ? request + : new Request(request_origin + url.pathname + url.search, request); + } catch (error) { + console.error( + `Could not determine request origin: ${error instanceof Error ? error.message : String(error)}` + ); + return new Response('Bad Request', { status: 400 }); + } +} + +/** + * @param {Request} request + * @param {URL} url + * @returns {string} + */ +function get_origin(request, url) { + // assume TLS terminates upstream, like adapter-node; an http origin would fail CSRF checks + const protocol = decodeURIComponent( + (protocol_header && request.headers.get(protocol_header)) || 'https' + ); + if (!/^https?$/i.test(protocol)) { + throw new Error( + `The ${protocol_header} header specified ${protocol} which is an invalid protocol scheme. It should only contain the protocol scheme (e.g. \`https\`)` + ); + } + + const host = + (host_header && request.headers.get(host_header)) || (request.headers.get('host') ?? url.host); + if (!host) { + throw new Error( + `Could not determine host from the ${host_header ? `${host_header} or ` : ''}host header` + ); + } + + const port = port_header ? request.headers.get(port_header) : null; + if (port && isNaN(+port)) { + throw new Error( + `The ${port_header} header specified ${port} which is an invalid port because it is not a number. The value should only contain the port number (e.g. 443)` + ); + } + + // canonicalized so the caller's comparison with url.origin matches (case, default ports) + return new URL(`${protocol}://${host}${port ? `:${port}` : ''}`).origin; +} + +/** + * @param {Request} request + * @param {BunServer} bun_server + * @returns {string} + */ +function get_client_address(request, bun_server) { + if (!address_header) { + // requestIP() is null over unix sockets; undefined matches adapter-node + return /** @type {string} */ (bun_server.requestIP(request)?.address); + } + + const value = request.headers.get(address_header); + if (value === null) { + throw new Error( + `Address header was specified with ${env_prefix}ADDRESS_HEADER=${address_header} but is absent from request` + ); + } + if (address_header !== 'x-forwarded-for') return value; + + const addresses = value.split(','); + if (xff_depth > addresses.length) { + throw new Error( + `${env_prefix}XFF_DEPTH is ${xff_depth}, but only found ${addresses.length} addresses` + ); + } + return addresses[addresses.length - xff_depth].trim(); +} diff --git a/packages/adapter-bun/src/index.js b/packages/adapter-bun/src/index.js new file mode 100644 index 000000000000..54e54a8cd44e --- /dev/null +++ b/packages/adapter-bun/src/index.js @@ -0,0 +1,98 @@ +/** @import { Serve } from 'bun' */ +import fs from 'node:fs'; +import process from 'node:process'; +import { server_options } from '#@sveltejs/adapter-bun'; +import { routes } from './routes.js'; +import { handler } from './handler.js'; +import { boolean_env, bytes_env, env, number_env } from './env.js'; + +// nothing enforces `engines.bun` at install time, and the build may have run on a newer Bun. +// order() rather than satisfies(): a range would reject canary builds such as 1.5.0-canary.1 +if (Bun.semver.order(Bun.version, '1.4.0') < 0) { + throw new Error( + `@sveltejs/adapter-bun requires Bun 1.4 or newer, but this is Bun ${Bun.version}` + ); +} + +const options = /** @type {Serve.Options} */ ({ ...server_options }); + +const unix = env('SOCKET_PATH', options.unix); + +if (unix) { + options.unix = unix; + delete options.hostname; + delete options.port; + delete options.reusePort; + delete options.ipv6Only; + + // an unclean shutdown leaves the socket file behind and the next listen would + // fail with EADDRINUSE; the zero-size check (same heuristic as adapter-node) + // avoids deleting a regular file that happens to sit at this path + try { + if (fs.statSync(unix).size === 0) fs.rmSync(unix); + } catch { + // ignore + } +} else { + delete options.unix; + options.hostname = env('HOST', options.hostname); + // always set an explicit port: left undefined, Bun.serve reads the unprefixed + // BUN_PORT/PORT/NODE_PORT itself, bypassing envPrefix isolation + options.port = env('PORT', options.port?.toString()) ?? 3000; + options.reusePort = boolean_env('REUSE_PORT', options.reusePort); + options.ipv6Only = boolean_env('IPV6_ONLY', options.ipv6Only); +} + +// not IDLE_TIMEOUT: that name means idle-shutdown seconds on adapter-node, and a +// carried-over value would crash on the 255 cap or silently kill slow requests +options.idleTimeout = number_env('CONNECTION_IDLE_TIMEOUT', options.idleTimeout, { max: 255 }); +options.development = boolean_env('DEVELOPMENT') ?? options.development ?? false; + +options.maxRequestBodySize = bytes_env('BODY_SIZE_LIMIT', options.maxRequestBodySize ?? 512 * 1024); + +const shutdown_timeout = number_env('SHUTDOWN_TIMEOUT', 30); + +options.fetch = handler; +options.routes = routes; + +const server = Bun.serve(options); + +console.log(unix ? `Listening on ${unix}` : `Listening on ${server.url}`); + +let shutting_down = false; + +/** @param {'SIGINT' | 'SIGTERM'} reason */ +async function graceful_shutdown(reason) { + if (shutting_down) return process.exit(1); + shutting_down = true; + + if (server.pendingRequests !== 0) { + console.log( + `Waiting for ${server.pendingRequests} requests to finish before shutting down...\n` + + 'Press Ctrl+C again to force shutdown.' + ); + } + + // stop() waits for in-flight requests, and an open event stream is one forever, + // so race the drain against the deadline. The timer must stay referenced: a + // draining server no longer holds the event loop open on its own + /** @type {ReturnType | undefined} */ + let deadline; + const drained = await Promise.race([ + server.stop().then(() => true), + new Promise((resolve) => { + deadline = setTimeout(() => resolve(false), shutdown_timeout * 1000); + }) + ]); + clearTimeout(deadline); + + // force-close aborts the in-flight handlers, so shutdown listeners do not tear + // down resources those handlers still hold + if (!drained) await server.stop(true); + + // @ts-expect-error custom events cannot be typed + process.emit('sveltekit:shutdown', reason); +} + +process.on('SIGTERM', () => graceful_shutdown('SIGTERM')); +process.on('SIGINT', () => graceful_shutdown('SIGINT')); diff --git a/packages/adapter-bun/src/routes-util.js b/packages/adapter-bun/src/routes-util.js new file mode 100644 index 000000000000..6504ad01fbf2 --- /dev/null +++ b/packages/adapter-bun/src/routes-util.js @@ -0,0 +1,247 @@ +/** @import { BunFile, BunRequest, Serve } from 'bun' */ +import { app_dir, base, dir, embed } from '#@sveltejs/adapter-bun'; +import path from 'node:path'; + +/** + * Embedded assets are imported by identity path; on-disk assets live under the + * output directory next to this module. + * @param {string} subdir + * @param {string} filename + * @returns {string} + */ +function resolve_file(subdir, filename) { + return embed ? filename : path.resolve(dir, subdir, filename); +} + +/** + * @typedef {Serve.Routes[string]} RouteHandler + * @typedef {{ hash: string, mtime: number, br?: boolean, gz?: boolean }} AssetMeta + */ + +const CONTENT_ENCODING = { br: 'br', gz: 'gzip' }; + +// the URL Standard's path percent-encode set, plus `%` and `\` so they stay literal +// eslint-disable-next-line no-control-regex -- control characters are part of the encode set +const ESCAPED_PATH_CHAR = /[\u0000-\u001f\u007f-\u{10ffff} "#<>?`{}%\\]/gu; + +/** + * WHATWG path serialization: the exact bytes user agents put on the wire, because + * Bun matches route keys against the raw request pathname. + * @param {string} pathname + * @returns {string} + */ +function encode_pathname(pathname) { + return pathname.replace(ESCAPED_PATH_CHAR, (char) => encodeURIComponent(char)); +} + +/** + * Registers a path both as user agents send it and fully percent-encoded, + * so clients that escape sub-delims still match. + * @param {string} pathname + * @returns {string[]} + */ +function route_paths(pathname) { + const minimal = encode_pathname(pathname); + const full = pathname.split('/').map(encodeURIComponent).join('/'); + return minimal === full ? [minimal] : [minimal, full]; +} + +/** + * @param {string} url + * @returns {string[]} + */ +function to_paths(url) { + return route_paths(path.posix.join(base, url)); +} + +/** + * @param {string} url + * @returns {string[]} + */ +function to_directory_paths(url) { + const directory = `${path.posix.join(base, url).replace(/\/$/, '')}/`; + const paths = route_paths(directory); + + if (directory !== '/') { + paths.push(...route_paths(directory.slice(0, -1))); + } + + return paths; +} + +/** + * If-None-Match takes precedence over If-Modified-Since (RFC 9110 §13.1.3); + * dates compare at whole-second precision because HTTP dates have none finer. + * @param {Request} request + * @param {string} etag + * @param {number} mtime + * @returns {boolean} + */ +function is_fresh(request, etag, mtime) { + const header = request.headers.get('if-none-match'); + if (header !== null) { + return header + .split(',') + .some((value) => ['*', etag].includes(value.trim().replace(/^W\//, ''))); + } + + const since = Date.parse(request.headers.get('if-modified-since') ?? ''); + return Number.isFinite(since) && Math.trunc(mtime / 1000) <= Math.trunc(since / 1000); +} + +/** + * @param {string | null} accept + * @param {AssetMeta} meta + * @returns {'br' | 'gz' | null} + */ +function negotiate(accept, meta) { + if (accept === null || (!meta.br && !meta.gz)) return null; + + const accepted = new Set(); + for (const part of accept.split(',')) { + const [name = '', ...params] = part.trim().toLowerCase().split(';'); + if (params.some((param) => /^q=0(\.0*)?$/.test(param.trim()))) continue; + accepted.add(name.trim()); + } + + if (meta.br && (accepted.has('br') || accepted.has('*'))) return 'br'; + if (meta.gz && (accepted.has('gzip') || accepted.has('*'))) return 'gz'; + return null; +} + +/** + * @param {string[]} paths + * @param {RouteHandler} route + * @returns {Array<[string, RouteHandler]>} + */ +function route_entries(paths, route) { + return paths.map((route_path) => [route_path, route]); +} + +/** + * Serves one file with its build-time validator and precompressed variants. + * @param {string} file + * @param {AssetMeta} meta + * @param {Record} [extra_headers] + * @returns {RouteHandler} + */ +function file_route(file, meta, extra_headers = {}) { + const content_type = Bun.file(file).type; + const last_modified = new Date(meta.mtime).toUTCString(); + + /** @param {BunRequest} request */ + const handler = (request) => { + // Bun serializes Range itself for file bodies; ranges apply to the identity representation + const encoding = + request.headers.get('range') === null + ? negotiate(request.headers.get('accept-encoding'), meta) + : null; + const etag = encoding === null ? `"${meta.hash}"` : `"${meta.hash}-${encoding}"`; + + /** @type {Record} */ + const response_headers = { + 'content-type': content_type, + ...extra_headers, + etag, + 'last-modified': last_modified + }; + if (meta.br || meta.gz) response_headers['vary'] = 'accept-encoding'; + + if (is_fresh(request, etag, meta.mtime)) { + return new Response(null, { status: 304, headers: response_headers }); + } + + let body_file = file; + if (encoding !== null) { + response_headers['content-encoding'] = CONTENT_ENCODING[encoding]; + body_file = `${file}.${encoding}`; + } + return new Response(Bun.file(body_file), { headers: response_headers }); + }; + + return { GET: handler }; +} + +/** + * @param {string} url + * @param {string | undefined} filename + * @param {AssetMeta} meta + * @returns {Array<[string, RouteHandler]>} + */ +export function client_asset(url, filename = url, meta) { + const immutable = url.startsWith(`${app_dir}/immutable/`); + const route = file_route( + resolve_file('client', filename), + meta, + immutable ? { 'cache-control': 'public,max-age=31536000,immutable' } : {} + ); + + const paths = to_paths(url); + if (url.endsWith('/index.html') || url === 'index.html') { + paths.push(...to_directory_paths(url.slice(0, -'index.html'.length))); + } else if (url.endsWith('.html')) { + // sirv also serves `page.html` at `/page` + paths.push(...to_paths(url.slice(0, -'.html'.length))); + } + + return route_entries(paths, route); +} + +/** + * @param {string} url + * @param {string} [filename] + * @returns {BunFile} + */ +export function server_asset(url, filename = url) { + return Bun.file(resolve_file('client', filename)); +} + +/** + * @param {string} url + * @param {string | undefined} filename + * @param {AssetMeta} meta + * @returns {Array<[string, RouteHandler]>} + */ +export function prerendered_asset(url, filename = url, meta) { + const route = file_route(resolve_file('prerendered', filename), meta); + return route_entries(to_paths(url), route); +} + +/** + * @param {string} url + * @param {string} filename + * @param {AssetMeta} meta + * @returns {Array<[string, RouteHandler]>} + */ +export function prerendered_page(url, filename, meta) { + const route = file_route(resolve_file('prerendered', filename), meta); + // path already contains base, no need to add it here + const entries = route_entries(route_paths(url), route); + + const inverted = url.endsWith('/') ? url.slice(0, -1) : `${url}/`; + if (inverted) { + const canonical = encode_pathname(url); + entries.push( + ...route_entries(route_paths(inverted), { + GET: (req) => { + const location = canonical + new URL(req.url).search; + return new Response(null, { status: 308, headers: { location } }); + } + }) + ); + } + + return entries; +} + +/** + * @param {string} url + * @param {number} status + * @param {string} location + * @returns {Array<[string, RouteHandler]>} + */ +export function prerendered_redirect(url, status, location) { + const route = { GET: new Response(null, { status, headers: { location } }) }; + // path already contains base, no need to add it here + return route_entries(route_paths(url), route); +} diff --git a/packages/adapter-bun/src/routes.js b/packages/adapter-bun/src/routes.js new file mode 100644 index 000000000000..9783738afdf4 --- /dev/null +++ b/packages/adapter-bun/src/routes.js @@ -0,0 +1,23 @@ +import { assets, redirects, server_assets as server_files } from '#@sveltejs/adapter-bun'; +import { + client_asset, + prerendered_asset, + prerendered_page, + prerendered_redirect, + server_asset +} from './routes-util.js'; + +const helpers = { client_asset, prerendered_asset, prerendered_page }; + +// reversed because Object.fromEntries keeps the last duplicate: the first generated +// entry for a path must win so exact files beat aliases, like sirv's lookup order +export const routes = Object.fromEntries( + [ + ...assets.flatMap(([helper, url, file, meta]) => helpers[helper](url, file, meta)), + ...redirects.flatMap(([url, status, location]) => prerendered_redirect(url, status, location)) + ].reverse() +); + +export const server_assets = new Map( + server_files.map(([file, embedded]) => [file, server_asset(file, embedded)]) +); diff --git a/packages/adapter-bun/test/adapter.spec.ts b/packages/adapter-bun/test/adapter.spec.ts new file mode 100644 index 000000000000..c78b368f56d7 --- /dev/null +++ b/packages/adapter-bun/test/adapter.spec.ts @@ -0,0 +1,470 @@ +import fs from 'node:fs'; +import { afterEach, beforeEach, describe, expect, mock, spyOn, test, type Mock } from 'bun:test'; +import adapter from '../index.js'; + +const package_dir = new URL('..', import.meta.url).pathname.replace(/\/$/, ''); +const src_dir = `${package_dir}/src`; +const server_dir = '.svelte-kit/output/server'; +const handoff_file = `${process.cwd()}/.svelte-kit/output/adapter-bun.js`; +const entrypoint = `${server_dir}/adapter-index.js`; +const handoff = '#@sveltejs/adapter-bun'; + +let bun_build: Mock<(options: any) => Promise>; +let read_file: Mock; +let write_file: Mock; + +// the real Bun.build would bundle and the real hashers would read assets off +// disk, so the build APIs stay test doubles even under Bun +beforeEach(() => { + bun_build = spyOn(Bun, 'build').mockImplementation((async (_options: any): Promise => ({ + success: true, + logs: [], + outputs: [] + })) as any) as any; + spyOn(Bun, 'file').mockImplementation(((_path: string) => ({ + stream: () => new Blob([]).stream(), + lastModified: 0 + })) as never); + spyOn(Bun, 'CryptoHasher').mockImplementation(function () { + return { + update() {}, + digest() { + return 'abc'; + } + }; + } as never); + + spyOn(fs, 'existsSync').mockReturnValue(true); + spyOn(fs, 'rmSync').mockImplementation(() => {}); + read_file = spyOn(fs, 'readFileSync').mockImplementation((() => undefined) as any) as any; + write_file = spyOn(fs, 'writeFileSync').mockImplementation(() => {}); +}); + +afterEach(() => { + mock.restore(); +}); + +describe('adapter contract', () => { + test('identifies itself and declares supported SvelteKit features', () => { + const instance = adapter(); + + expect(instance.name).toBe('@sveltejs/adapter-bun'); + expect(instance.supports?.read?.({ route: { id: '/file' }, config: {} })).toBe(true); + expect(instance.supports?.instrumentation?.()).toBe(true); + }); + + test('requires the SvelteKit build to run in Bun', () => { + // the Bun global cannot be unset inside Bun itself, so run the guard under Node + const result = Bun.spawnSync([ + 'node', + '--input-type=module', + '-e', + `import adapter from ${JSON.stringify(`${package_dir}/index.js`)};\n` + + 'await adapter().adapt({}).then(\n' + + '\t() => process.exit(0),\n' + + '\t(error) => { console.error(error.message); process.exit(1); }\n' + + ');' + ]); + + expect(result.exitCode).toBe(1); + expect(result.stderr.toString()).toContain( + 'adapter-bun requires running the SvelteKit build with Bun' + ); + }); +}); + +describe('Vite build configuration', () => { + test('bundles the server source with the app and keeps production dependencies external', () => { + read_file.mockReturnValue(JSON.stringify({ dependencies: { jsdom: '1.0.0' } })); + + const config = vite_config(); + const options = config.environments.ssr.build.rolldownOptions; + + expect(read_file).toHaveBeenCalledWith('package.json', 'utf8'); + expect(config.ssr).toEqual({ external: ['jsdom'], noExternal: true }); + expect(options.input).toEqual({ 'adapter-index': `${src_dir}/index.js` }); + expect(options.external).toEqual([handoff]); + expect(options.output.paths).toEqual({ [handoff]: '../adapter-bun.js' }); + }); + + test('bundles production dependencies that vite-plugin-svelte marks noExternal', () => { + read_file.mockReturnValue( + JSON.stringify({ + dependencies: { jsdom: '1.0.0', 'svelte-lib': '1.0.0', '@ui/kit': '1.0.0' } + }) + ); + + const { ssr } = vite_config({ ssr: { noExternal: ['svelte-lib', /^@ui\//] } }); + + expect(ssr.external).toEqual(['jsdom']); + }); + + test('keeps adapter chunks at the output root so the hand-off path resolves', () => { + read_file.mockReturnValue('{}'); + + const { chunkFileNames } = vite_config().environments.ssr.build.rolldownOptions.output; + + expect(chunkFileNames({ moduleIds: [`${src_dir}/handler.js`, '/app/src/hooks.js'] })).toBe( + 'adapter-bun-[name].js' + ); + expect(chunkFileNames({ moduleIds: ['/app/src/hooks.js'] })).toBe('chunks/[name].js'); + }); +}); + +describe('build output', () => { + test('cleans the output and copies the Vite build next to the hand-off module', async () => { + const builder = create_builder(); + await adapter().adapt(builder); + + expect(fs.rmSync).toHaveBeenCalledWith('build', { recursive: true, force: true }); + expect(builder.log.minor).toHaveBeenCalledWith('Building server'); + expect(builder.generateServerInstance).toHaveBeenCalledWith(`${server_dir}/server.js`); + expect(builder.copy).toHaveBeenCalledWith(server_dir, 'build/server'); + expect(builder.copy).toHaveBeenCalledWith(handoff_file, 'build/adapter-bun.js'); + expect(write_file).toHaveBeenCalledWith( + 'build/index.js', + "import './server/adapter-index.js';\n" + ); + expect(bun_build).not.toHaveBeenCalled(); + expect(builder.instrument).not.toHaveBeenCalled(); + expect(builder.compress).not.toHaveBeenCalled(); + }); + + test('hands the configured values to the server', async () => { + await adapter({ + envPrefix: 'APP_', + serverOptions: { hostname: '127.0.0.1', port: 4000, development: true } + }).adapt( + create_builder({ + base: '/docs', + origin: 'https://example.com', + prerendered_files: ['page/index.html'], + prerendered_pages: [['/docs/page/', { file: 'page/index.html' }]] + }) + ); + + expect(handoff_source()).toContain( + "import { dirname } from 'node:path';\n" + + "import { fileURLToPath } from 'node:url';\n" + + "export { server } from './server/server.js';\n" + + 'export const dir = dirname(fileURLToPath(import.meta.url));\n' + + 'export const app_dir = "_app";\n' + + 'export const base = "/docs";\n' + + 'export const embed = false;\n' + + 'export const env_prefix = "APP_";\n' + + 'export const origin = "https://example.com";\n' + + 'export const server_options = {"hostname":"127.0.0.1","port":4000,"development":true};\n' + ); + // prerendered paths already contain the base + expect(handoff_source()).toContain('["prerendered_page", "/docs/page/", "page/index.html", '); + }); + + test('defaults the base to the root and leaves the origin undefined', async () => { + await adapter().adapt(create_builder()); + + expect(handoff_source()).toContain('export const base = "/";\n'); + expect(handoff_source()).toContain('export const origin = undefined;\n'); + expect(handoff_source()).toContain('export const server_options = {};\n'); + }); + + test('loads instrumentation before the generated server entrypoint', async () => { + const builder = create_builder({ instrumentation: true }); + await adapter().adapt(builder); + + expect(builder.createInstrumentationInitializer).toHaveBeenCalledWith({ + outputDirectory: server_dir + }); + expect(builder.instrument).toHaveBeenCalledWith({ + entrypoint, + instrumentation: `${server_dir}/instrumentation.server.js`, + initializer: `${server_dir}/__sveltekit_env_init.js`, + module: { exports: [] } + }); + }); + + test('compiles an executable from the Vite build instead of copying it', async () => { + const builder = create_builder(); + await adapter({ buildOptions: { compile: true } }).adapt(builder); + + expect(builder.copy).not.toHaveBeenCalled(); + expect(write_file).not.toHaveBeenCalledWith('build/index.js', expect.anything()); + expect(bun_build.mock.calls[0][0]).toEqual({ + entrypoints: [entrypoint], + outdir: 'build', + target: 'bun', + format: 'esm', + sourcemap: 'external', + conditions: ['bun', 'node'], + throw: false, + compile: { outfile: 'server' } + }); + expect(handoff_source()).toContain('export const embed = true;\n'); + }); + + test('passes supported advanced options while retaining reserved options', async () => { + await adapter({ + out: 'dist', + buildOptions: { + compile: { outfile: 'application', target: 'bun-linux-x64' }, + minify: true, + bytecode: true, + sourcemap: 'linked', + drop: ['debugger'], + external: ['sharp'] + } + }).adapt(create_builder()); + + expect(bun_build.mock.calls[0][0]).toMatchObject({ + entrypoints: [entrypoint], + outdir: 'dist', + target: 'bun', + format: 'esm', + minify: true, + bytecode: true, + sourcemap: 'linked', + drop: ['debugger'], + external: ['sharp'], + compile: { outfile: 'application', target: 'bun-linux-x64' } + }); + }); + + test.each([ + ['bun-linux-x64', { outfile: 'server', target: 'bun-linux-x64' }], + [ + { target: 'bun-windows-x64', windows: { hideConsole: true } }, + { + outfile: 'server', + target: 'bun-windows-x64', + windows: { hideConsole: true } + } + ] + ] as const)('normalizes compile option %j', async (compile, expected) => { + await adapter({ buildOptions: { compile } }).adapt(create_builder()); + + expect(bun_build.mock.calls[0][0].compile).toEqual(expected); + }); + + test('reports every Bun diagnostic before failing the build', async () => { + bun_build.mockResolvedValueOnce({ + success: false, + logs: [ + { level: 'error', message: 'broken' }, + { level: 'warning', message: 'careful' }, + { level: 'info', message: 'context' } + ], + outputs: [] + }); + const builder = create_builder(); + + await expect( + adapter({ buildOptions: { compile: true } }).adapt(builder) + ).rejects.toBeInstanceOf(AggregateError); + expect(builder.log.error).toHaveBeenCalledWith('broken'); + expect(builder.log.warn).toHaveBeenCalledWith('careful'); + expect(builder.log.info).toHaveBeenCalledWith('context'); + }); +}); + +describe('generated routes', () => { + test('writes regular-build assets and excludes prerendered routes from server reads', async () => { + const dynamic = { id: '/read', prerender: false }; + const prerendered = { id: '/prerendered', prerender: true }; + const builder = create_builder({ + client_files: ['.env', 'data.json', '.well-known/security.txt', '_app/immutable/read.txt'], + prerendered_files: ['page/index.html', 'icon.png'], + prerendered_pages: [['/page/', { file: 'page/index.html' }]], + prerendered_redirects: [['/old', { status: 301, location: '/new' }]], + routes: [dynamic, prerendered], + server_assets: ['_app/immutable/read.txt'] + }); + + await adapter().adapt(builder); + + expect(builder.writeClient).toHaveBeenCalledWith('build/client'); + expect(builder.writePrerendered).toHaveBeenCalledWith('build/prerendered'); + expect(builder.findServerAssets).toHaveBeenCalledWith([dynamic]); + const source = handoff_source(); + expect(source).toContain( + '["client_asset", "data.json", "data.json", {"hash":"abc","mtime":0}]' + ); + expect(source).toContain( + '["client_asset", "_app/immutable/read.txt", "_app/immutable/read.txt", {"hash":"abc","mtime":0}]' + ); + // like sirv, dotfiles are not served, apart from .well-known + expect(source).not.toContain('.env'); + expect(source).toContain( + '["client_asset", ".well-known/security.txt", ".well-known/security.txt", {"hash":"abc","mtime":0}]' + ); + expect(source).toContain( + '["prerendered_page", "/page/", "page/index.html", {"hash":"abc","mtime":0}]' + ); + expect(source).toContain( + '["prerendered_asset", "icon.png", "icon.png", {"hash":"abc","mtime":0}]' + ); + expect(source).toContain('export const redirects = [["/old",301,"/new"]];'); + expect(source).toContain('export const server_assets = [\n["_app/immutable/read.txt"]\n];'); + expect(source).not.toContain("with { type: 'file' }"); + }); + + test('embeds assets in compiled executables from a staging directory', async () => { + const builder = create_builder({ + client_files: [ + '.secret', + 'data.json', + '.well-known/asset.txt', + '_app/read.txt', + 'page/index.html' + ], + prerendered_files: ['page/index.html', 'favicon.ico', 'dependency.json', 'page/__data.json'], + prerendered_pages: [['/page/', { file: 'page/index.html' }]], + server_assets: ['_app/read.txt'] + }); + + await adapter({ buildOptions: { compile: true } }).adapt(builder); + + expect(builder.writeClient).toHaveBeenCalledWith('.svelte-kit/adapter-bun/client'); + expect(builder.writePrerendered).toHaveBeenCalledWith('.svelte-kit/adapter-bun/prerendered'); + const source = handoff_source(); + expect(source).toContain( + `import asset_0 from ${JSON.stringify(`${process.cwd()}/.svelte-kit/adapter-bun/client/data.json`)} with { type: 'file' };` + ); + expect(source).toContain('["client_asset", "data.json", asset_0, {"hash":"abc","mtime":0}]'); + expect(source).toContain( + '["client_asset", ".well-known/asset.txt", asset_1, {"hash":"abc","mtime":0}]' + ); + // a skipped dotfile takes no import, and the same relative path in the client + // and prerendered output stays two imports + expect(source).not.toContain('.secret'); + expect(source).toContain( + '["client_asset", "page/index.html", asset_3, {"hash":"abc","mtime":0}]' + ); + expect(source).toContain('["prerendered_page", "/page/", asset_4, {"hash":"abc","mtime":0}]'); + expect(source).toContain( + '["prerendered_asset", "favicon.ico", asset_5, {"hash":"abc","mtime":0}]' + ); + expect(source).toContain( + '["prerendered_asset", "dependency.json", asset_6, {"hash":"abc","mtime":0}]' + ); + expect(source).toContain( + '["prerendered_asset", "page/__data.json", asset_7, {"hash":"abc","mtime":0}]' + ); + expect(source).toContain('["_app/read.txt", asset_2]'); + }); + + test.each([false, true])('rejects wildcard filenames when compile is %s', async (compile) => { + const builder = create_builder({ client_files: ['literal*.txt'] }); + + await expect(adapter({ buildOptions: { compile } }).adapt(builder)).rejects.toThrow( + 'Bun treats literal `*` characters in route paths as wildcards' + ); + expect(write_file).not.toHaveBeenCalled(); + }); + + test('precompresses assets and marks the variants in the generated routes', async () => { + const builder = create_builder({ client_files: ['app.js'] }); + + await adapter({ precompress: true }).adapt(builder); + + expect(builder.compress).toHaveBeenCalledWith('build/client'); + expect(builder.compress).toHaveBeenCalledWith('build/prerendered'); + expect(handoff_source()).toContain( + '["client_asset", "app.js", "app.js", {"hash":"abc","mtime":0,"br":true,"gz":true}]' + ); + }); + + test('warns when precompress is combined with compile', async () => { + const builder = create_builder(); + + await adapter({ precompress: true, buildOptions: { compile: true } }).adapt(builder); + + expect(builder.log.warn).toHaveBeenCalledWith( + expect.stringContaining('precompress is ignored') + ); + expect(builder.compress).not.toHaveBeenCalled(); + }); + + test('rejects route segments starting with a colon', async () => { + const builder = create_builder({ client_files: [':tag.txt'] }); + + await expect(adapter().adapt(builder)).rejects.toThrow('starts with `:`'); + expect(write_file).not.toHaveBeenCalled(); + }); + + test('rejects wildcard characters in prerendered redirect sources', async () => { + const builder = create_builder({ + prerendered_redirects: [['/docs/*', { status: 308, location: '/new' }]] + }); + + await expect(adapter().adapt(builder)).rejects.toThrow( + 'Bun treats literal `*` characters in route paths as wildcards' + ); + expect(write_file).not.toHaveBeenCalled(); + }); + + test('fails when a server-readable asset is absent from compiled build output', async () => { + await expect( + adapter({ buildOptions: { compile: true } }).adapt( + create_builder({ server_assets: ['missing.txt'] }) + ) + ).rejects.toThrow('Could not find server asset missing.txt'); + }); +}); + +function vite_config(config = {}) { + return (adapter() as any).vite.plugins.post[0].config(config); +} + +function handoff_source() { + const call = write_file.mock.calls.find(([file]) => file === handoff_file); + if (!call) throw new Error('the hand-off module was not written'); + return String(call[1]); +} + +function create_builder({ + client_files = [], + prerendered_files = [], + prerendered_pages = [], + prerendered_redirects = [], + routes = [], + server_assets = [], + base = '', + origin, + instrumentation = false +}: { + client_files?: string[]; + prerendered_files?: string[]; + prerendered_pages?: Array<[string, { file: string }]>; + prerendered_redirects?: Array<[string, { status: number; location: string }]>; + routes?: Array<{ id: string; prerender: boolean | string }>; + server_assets?: string[]; + base?: string; + origin?: string; + instrumentation?: boolean; +} = {}) { + return { + config: { outDir: '.svelte-kit', paths: { base, origin }, appDir: '_app' }, + routes, + prerendered: { + pages: new Map(prerendered_pages), + redirects: new Map(prerendered_redirects) + }, + log: { + minor: mock((_message: string) => {}), + error: mock((_message: string) => {}), + warn: mock((_message: string) => {}), + info: mock((_message: string) => {}) + }, + getAppPath: () => `${base}/_app`, + generateServerInstance: mock(() => {}), + getServerDirectory: () => server_dir, + getBuildDirectory: (name: string) => `.svelte-kit/${name}`, + writeClient: mock(() => client_files), + writePrerendered: mock(() => prerendered_files), + copy: mock(() => []), + compress: mock(async (_directory: string) => {}), + findServerAssets: mock(() => server_assets), + hasServerInstrumentationFile: () => instrumentation, + createInstrumentationInitializer: mock(() => `${server_dir}/__sveltekit_env_init.js`), + instrument: mock(() => {}) + } as any; +} diff --git a/packages/adapter-bun/test/apps/basic/.gitignore b/packages/adapter-bun/test/apps/basic/.gitignore new file mode 100644 index 000000000000..00d16428cb9c --- /dev/null +++ b/packages/adapter-bun/test/apps/basic/.gitignore @@ -0,0 +1,5 @@ +.svelte-kit +build +node_modules +test-results + diff --git a/packages/adapter-bun/test/apps/basic/package.json b/packages/adapter-bun/test/apps/basic/package.json new file mode 100644 index 000000000000..bb9088d559f2 --- /dev/null +++ b/packages/adapter-bun/test/apps/basic/package.json @@ -0,0 +1,23 @@ +{ + "name": "test-bun-basic", + "version": "0.0.1", + "private": true, + "scripts": { + "dev": "vite dev", + "build": "MY_CUSTOM_PORT=4174 vite build", + "preview": "MY_CUSTOM_PORT=4174 bun ./build", + "prepare": "svelte-kit sync || echo ''", + "test:platform": "playwright test" + }, + "dependencies": { + "jsdom": "catalog:" + }, + "devDependencies": { + "@playwright/test": "catalog:", + "@sveltejs/kit": "workspace:^", + "@sveltejs/vite-plugin-svelte": "catalog:", + "svelte": "catalog:", + "vite": "catalog:" + }, + "type": "module" +} diff --git a/packages/adapter-bun/test/apps/basic/playwright.config.js b/packages/adapter-bun/test/apps/basic/playwright.config.js new file mode 100644 index 000000000000..33d36b651014 --- /dev/null +++ b/packages/adapter-bun/test/apps/basic/playwright.config.js @@ -0,0 +1 @@ +export { config as default } from '../../utils.js'; diff --git a/packages/adapter-bun/test/apps/basic/src/app.html b/packages/adapter-bun/test/apps/basic/src/app.html new file mode 100644 index 000000000000..f273cc58f7eb --- /dev/null +++ b/packages/adapter-bun/test/apps/basic/src/app.html @@ -0,0 +1,11 @@ + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/packages/adapter-bun/test/apps/basic/src/instrumentation.server.js b/packages/adapter-bun/test/apps/basic/src/instrumentation.server.js new file mode 100644 index 000000000000..00368839194c --- /dev/null +++ b/packages/adapter-bun/test/apps/basic/src/instrumentation.server.js @@ -0,0 +1 @@ +globalThis.__INSTRUMENTATION_RAN__ = true; diff --git a/packages/kit/test/apps/basics/src/routes/asset-preload/prerendered/+page.server.js b/packages/adapter-bun/test/apps/basic/src/routes/+page.js similarity index 100% rename from packages/kit/test/apps/basics/src/routes/asset-preload/prerendered/+page.server.js rename to packages/adapter-bun/test/apps/basic/src/routes/+page.js diff --git a/packages/adapter-bun/test/apps/basic/src/routes/+page.svelte b/packages/adapter-bun/test/apps/basic/src/routes/+page.svelte new file mode 100644 index 000000000000..f1ef75ba7a5d --- /dev/null +++ b/packages/adapter-bun/test/apps/basic/src/routes/+page.svelte @@ -0,0 +1,6 @@ + + +

Hello from Bun!

+ diff --git a/packages/adapter-bun/test/apps/basic/src/routes/data.json/+server.js b/packages/adapter-bun/test/apps/basic/src/routes/data.json/+server.js new file mode 100644 index 000000000000..5dae4aac8917 --- /dev/null +++ b/packages/adapter-bun/test/apps/basic/src/routes/data.json/+server.js @@ -0,0 +1,3 @@ +export function POST() { + return Response.json({ message: 'hello from a server endpoint' }); +} diff --git a/packages/adapter-bun/test/apps/basic/src/routes/dependency/+server.js b/packages/adapter-bun/test/apps/basic/src/routes/dependency/+server.js new file mode 100644 index 000000000000..6cbaaed4ef5d --- /dev/null +++ b/packages/adapter-bun/test/apps/basic/src/routes/dependency/+server.js @@ -0,0 +1,7 @@ +import { JSDOM } from 'jsdom'; + +/** @type {import('./$types').RequestHandler} */ +export function GET() { + const { document } = new JSDOM('

hello from a dependency

').window; + return new Response(document.querySelector('p')?.textContent); +} diff --git a/packages/adapter-bun/test/apps/basic/src/routes/event-stream/+server.js b/packages/adapter-bun/test/apps/basic/src/routes/event-stream/+server.js new file mode 100644 index 000000000000..cc152e0033ba --- /dev/null +++ b/packages/adapter-bun/test/apps/basic/src/routes/event-stream/+server.js @@ -0,0 +1,7 @@ +export function GET() { + return new Response('data: hello\n\n', { + headers: { + 'content-type': 'text/event-stream' + } + }); +} diff --git a/packages/adapter-bun/test/apps/basic/src/routes/instrumented/+server.js b/packages/adapter-bun/test/apps/basic/src/routes/instrumented/+server.js new file mode 100644 index 000000000000..def88b135b5f --- /dev/null +++ b/packages/adapter-bun/test/apps/basic/src/routes/instrumented/+server.js @@ -0,0 +1,3 @@ +export function GET() { + return new Response(String(globalThis.__INSTRUMENTATION_RAN__ === true)); +} diff --git a/packages/adapter-bun/test/apps/basic/src/routes/platform/+server.js b/packages/adapter-bun/test/apps/basic/src/routes/platform/+server.js new file mode 100644 index 000000000000..b95820379b26 --- /dev/null +++ b/packages/adapter-bun/test/apps/basic/src/routes/platform/+server.js @@ -0,0 +1,14 @@ +import { json } from '@sveltejs/kit'; + +/** @type {import('./$types').RequestHandler} */ +export function GET({ getClientAddress, platform }) { + return json({ + address: getClientAddress(), + server: typeof platform?.server?.requestIP === 'function', + id: platform?.server.id, + protocol: platform?.server.protocol, + pendingRequests: platform?.server.pendingRequests, + pendingWebSockets: platform?.server.pendingWebSockets, + subscribers: platform?.server.subscriberCount('adapter-bun-test') + }); +} diff --git a/packages/adapter-bun/test/apps/basic/src/routes/prerendered.ico/+server.js b/packages/adapter-bun/test/apps/basic/src/routes/prerendered.ico/+server.js new file mode 100644 index 000000000000..ea5bb293b791 --- /dev/null +++ b/packages/adapter-bun/test/apps/basic/src/routes/prerendered.ico/+server.js @@ -0,0 +1,7 @@ +export const prerender = true; + +export function GET() { + return new Response(new Uint8Array([0, 0, 1, 0]), { + headers: { 'content-type': 'image/x-icon' } + }); +} diff --git a/packages/adapter-bun/test/apps/basic/src/routes/prerendered/+page.js b/packages/adapter-bun/test/apps/basic/src/routes/prerendered/+page.js new file mode 100644 index 000000000000..ba58d86071e4 --- /dev/null +++ b/packages/adapter-bun/test/apps/basic/src/routes/prerendered/+page.js @@ -0,0 +1,2 @@ +export const prerender = true; +export const trailingSlash = 'always'; diff --git a/packages/adapter-bun/test/apps/basic/src/routes/prerendered/+page.svelte b/packages/adapter-bun/test/apps/basic/src/routes/prerendered/+page.svelte new file mode 100644 index 000000000000..a994afef288b --- /dev/null +++ b/packages/adapter-bun/test/apps/basic/src/routes/prerendered/+page.svelte @@ -0,0 +1 @@ +

Prerendered

diff --git a/packages/adapter-bun/test/apps/basic/src/routes/read/+server.js b/packages/adapter-bun/test/apps/basic/src/routes/read/+server.js new file mode 100644 index 000000000000..3b844ed60555 --- /dev/null +++ b/packages/adapter-bun/test/apps/basic/src/routes/read/+server.js @@ -0,0 +1,6 @@ +import { read } from '$app/server'; +import file from './file.txt?url&no-inline'; + +export function GET() { + return read(file); +} diff --git a/packages/adapter-bun/test/apps/basic/src/routes/read/file.txt b/packages/adapter-bun/test/apps/basic/src/routes/read/file.txt new file mode 100644 index 000000000000..7ef31b114b64 --- /dev/null +++ b/packages/adapter-bun/test/apps/basic/src/routes/read/file.txt @@ -0,0 +1 @@ +Hello from $app/server read diff --git a/packages/adapter-bun/test/apps/basic/static/.well-known/adapter-bun.txt b/packages/adapter-bun/test/apps/basic/static/.well-known/adapter-bun.txt new file mode 100644 index 000000000000..f57fb296b834 --- /dev/null +++ b/packages/adapter-bun/test/apps/basic/static/.well-known/adapter-bun.txt @@ -0,0 +1 @@ +adapter bun diff --git a/packages/adapter-bun/test/apps/basic/static/data.json b/packages/adapter-bun/test/apps/basic/static/data.json new file mode 100644 index 000000000000..e3adbd4e3a31 --- /dev/null +++ b/packages/adapter-bun/test/apps/basic/static/data.json @@ -0,0 +1 @@ +{ "message": "hello from a static file" } diff --git a/packages/adapter-bun/test/apps/basic/static/encoded name.txt b/packages/adapter-bun/test/apps/basic/static/encoded name.txt new file mode 100644 index 000000000000..4851e9b09386 --- /dev/null +++ b/packages/adapter-bun/test/apps/basic/static/encoded name.txt @@ -0,0 +1 @@ +hello from an encoded filename diff --git a/packages/adapter-bun/test/apps/basic/static/sub/index.html b/packages/adapter-bun/test/apps/basic/static/sub/index.html new file mode 100644 index 000000000000..c3b5888bb16d --- /dev/null +++ b/packages/adapter-bun/test/apps/basic/static/sub/index.html @@ -0,0 +1,3 @@ + +Static directory index +

Static directory index

diff --git a/packages/adapter-bun/test/apps/basic/test/browser.test.js b/packages/adapter-bun/test/apps/basic/test/browser.test.js new file mode 100644 index 000000000000..981b5e09e8cc --- /dev/null +++ b/packages/adapter-bun/test/apps/basic/test/browser.test.js @@ -0,0 +1,10 @@ +import { expect, test } from '@playwright/test'; + +test('renders on the server and hydrates in the browser', async ({ page }) => { + await page.goto('/'); + + await expect(page.locator('h1')).toHaveText('Hello from Bun!'); + await expect(page.getByRole('button')).toHaveText('Toggle: false'); + await page.getByRole('button').click(); + await expect(page.getByRole('button')).toHaveText('Toggle: true'); +}); diff --git a/packages/adapter-bun/test/apps/basic/test/server.test.js b/packages/adapter-bun/test/apps/basic/test/server.test.js new file mode 100644 index 000000000000..80ab0e049098 --- /dev/null +++ b/packages/adapter-bun/test/apps/basic/test/server.test.js @@ -0,0 +1,132 @@ +import { expect, test } from '@playwright/test'; +import process from 'node:process'; + +const compiled = process.env.COMPILE === 'true'; + +test('provides the Bun server on platform', async ({ request }) => { + const response = await request.get('/platform'); + const platform = await response.json(); + + expect(platform.address).toBeTruthy(); + expect(platform.server).toBe(true); + expect(platform.id).toEqual(expect.any(String)); + expect(platform.protocol).toBe('http'); + expect(platform.pendingRequests).toBeGreaterThanOrEqual(1); + expect(platform.pendingWebSockets).toBe(0); + expect(platform.subscribers).toBe(0); +}); + +test('keeps production dependencies external', async ({ request }) => { + // TODO drop the skip once Bun bundles css-tree's createRequire (oven-sh/bun#36565) + test.skip(compiled, 'executables bundle dependencies, which breaks jsdom'); + const response = await request.get('/dependency'); + expect(response.status()).toBe(200); + expect(await response.text()).toBe('hello from a dependency'); +}); + +test('runs server instrumentation before accepting requests', async ({ request }) => { + const response = await request.get('/instrumented'); + expect(response.status()).toBe(200); + expect(await response.text()).toBe('true'); +}); + +test('serves static files and implements HEAD natively', async ({ request }) => { + const response = await request.get('/data.json'); + expect(response.status()).toBe(200); + expect(response.headers()['content-type']).toContain('application/json'); + expect(await response.json()).toEqual({ message: 'hello from a static file' }); + + const head = await request.head('/data.json'); + expect(head.status()).toBe(200); + expect(head.headers()['content-length']).toBe(response.headers()['content-length']); + expect(await head.text()).toBe(''); +}); + +for (const [url, content_type, body] of [ + ['/sub/', 'text/html;charset=utf-8', 'directory index'], + ['/encoded%20name.txt', 'text/plain;charset=utf-8', 'encoded filename'], + ['/.well-known/adapter-bun.txt', 'text/plain;charset=utf-8', 'adapter bun'] +]) { + test(`serves ${url} with its MIME type`, async ({ request }) => { + const response = await request.get(url); + expect(response.status()).toBe(200); + expect(response.headers()['content-type']).toBe(content_type); + expect(await response.text()).toContain(body); + }); +} + +test('uses Bun conditional requests and byte ranges for filesystem assets', async ({ request }) => { + const initial = await request.get('/data.json'); + const body = await initial.text(); + + if (compiled) { + const etag = initial.headers()['etag']; + expect(etag).toBeTruthy(); + const cached = await request.get('/data.json', { headers: { 'if-none-match': etag } }); + expect(cached.status()).toBe(304); + } else { + const last_modified = initial.headers()['last-modified']; + expect(last_modified).toBeTruthy(); + const cached = await request.get('/data.json', { + headers: { 'if-modified-since': last_modified } + }); + expect(cached.status()).toBe(304); + + const range = await request.get('/data.json', { headers: { range: 'bytes=0-3' } }); + expect(range.status()).toBe(206); + expect(range.headers()['accept-ranges']).toBe('bytes'); + expect(range.headers()['content-range']).toBe(`bytes 0-3/${body.length}`); + expect(await range.text()).toBe(body.slice(0, 4)); + } +}); + +test('serves prerendered pages, endpoints, and canonical redirects', async ({ request }) => { + const page = await request.get('/prerendered/'); + expect(page.status()).toBe(200); + expect(await page.text()).toContain('Prerendered'); + + const icon = await request.get('/prerendered.ico'); + expect(icon.status()).toBe(200); + expect(icon.headers()['content-type']).toBe('image/x-icon'); + expect(await icon.body()).toEqual(Buffer.from([0, 0, 1, 0])); + + const redirect = await request.get('/prerendered?via=test', { maxRedirects: 0 }); + expect(redirect.status()).toBe(308); + expect(redirect.headers()['location']).toBe('/prerendered/?via=test'); +}); + +test('uses SvelteKit for non-GET requests that share a static pathname', async ({ request }) => { + // the adapter assumes TLS terminates upstream, so the origin it computes is https + const response = await request.post('/data.json', { + headers: { origin: 'https://localhost:4174' } + }); + expect(response.status()).toBe(200); + expect(await response.json()).toEqual({ message: 'hello from a server endpoint' }); +}); + +test('makes imported assets available to $app/server read', async ({ request }) => { + const response = await request.get('/read'); + expect(response.status()).toBe(200); + expect(await response.text()).toBe('Hello from $app/server read\n'); +}); + +test('sets immutable caching only on generated immutable assets', async ({ request }) => { + const document = await request.get('/'); + const asset = /["']([^"']*_app\/immutable\/[^"']+)["']/.exec(await document.text())?.[1]; + expect(asset).toBeTruthy(); + + const immutable = await request.get(/** @type {string} */ (asset)); + expect(immutable.headers()['cache-control']).toBe('public,max-age=31536000,immutable'); + + const regular = await request.get('/data.json'); + expect(regular.headers()['cache-control']).toBeUndefined(); +}); + +test('disables timeouts and proxy buffering for server-sent events', async ({ request }) => { + const events = await request.get('/event-stream'); + expect(events.headers()['content-type']).toContain('text/event-stream'); + expect(events.headers()['x-accel-buffering']).toBe('no'); + + const regular = await request.get('/platform'); + expect(regular.headers()['x-accel-buffering']).toBeUndefined(); +}); diff --git a/packages/adapter-bun/test/apps/basic/tsconfig.json b/packages/adapter-bun/test/apps/basic/tsconfig.json new file mode 100644 index 000000000000..00ef9b61a37b --- /dev/null +++ b/packages/adapter-bun/test/apps/basic/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "$app/tsconfig", + "include": ["src"] +} diff --git a/packages/adapter-bun/test/apps/basic/vite.config.js b/packages/adapter-bun/test/apps/basic/vite.config.js new file mode 100644 index 000000000000..c90053293e35 --- /dev/null +++ b/packages/adapter-bun/test/apps/basic/vite.config.js @@ -0,0 +1,38 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; +import adapter from '../../../index.js'; + +const buildOptions = + process.env.ADVANCED_COMPILE === 'true' + ? { + compile: { + outfile: 'advanced-app', + ...(process.env.COMPILE_TARGET + ? { + target: /** @type {import('bun').Build.CompileTarget} */ ( + process.env.COMPILE_TARGET + ) + } + : {}) + }, + minify: true, + bytecode: true, + sourcemap: /** @type {const} */ ('linked') + } + : process.env.COMPILE === 'true' + ? { compile: true } + : {}; + +export default defineConfig({ + build: { + minify: false + }, + plugins: [ + sveltekit({ + adapter: adapter({ + envPrefix: 'MY_CUSTOM_', + buildOptions + }) + }) + ] +}); diff --git a/packages/adapter-bun/test/env.spec.ts b/packages/adapter-bun/test/env.spec.ts new file mode 100644 index 000000000000..803d45af0a71 --- /dev/null +++ b/packages/adapter-bun/test/env.spec.ts @@ -0,0 +1,160 @@ +import process from 'node:process'; +import { afterEach, describe, expect, test } from 'bun:test'; +import { mock_handoff } from './mocks.js'; + +const changed = new Set(); +let instance = 0; + +afterEach(() => { + for (const name of changed) delete process.env[name]; + changed.clear(); +}); + +describe('env', () => { + test('uses the prefixed value when present and otherwise returns the fallback', async () => { + set_env('APP_PORT', '4000'); + const { env } = await load_env('APP_'); + + expect(env('PORT', '3000')).toBe('4000'); + expect(env('HOST', 'localhost')).toBe('localhost'); + }); + + test('treats an explicitly empty value as present', async () => { + set_env('APP_HOST', ''); + const { env } = await load_env('APP_'); + + expect(env('HOST', 'localhost')).toBe(''); + }); + + test('rejects unexpected variables that use a configured prefix', async () => { + set_env('UNIQUE_ADAPTER_OPTION', 'value'); + + await expect(load_env('UNIQUE_ADAPTER_')).rejects.toThrow( + 'unexpectedly saw UNIQUE_ADAPTER_OPTION' + ); + }); +}); + +describe('boolean_env', () => { + test('parses the accepted truthy and falsy spellings', async () => { + const { boolean_env } = await load_env(); + + for (const value of ['1', 'true', 'TRUE', 'yes', 'YES', 'on', 'ON']) { + set_env('OPTION', value); + expect(boolean_env('OPTION'), value).toBe(true); + } + for (const value of ['0', 'false', 'FALSE', 'no', 'NO', 'off', 'OFF']) { + set_env('OPTION', value); + expect(boolean_env('OPTION'), value).toBe(false); + } + }); + + test('returns the fallback for an absent variable', async () => { + const { boolean_env } = await load_env(); + expect(boolean_env('OPTION', true)).toBe(true); + }); + + test('rejects any other value', async () => { + set_env('OPTION', 'enabled'); + const { boolean_env } = await load_env(); + expect(() => boolean_env('OPTION')).toThrow( + 'Invalid value for environment variable OPTION: "enabled" (expected a boolean)' + ); + }); +}); + +describe('number_env', () => { + test.each([ + ['0', 0], + ['42', 42], + ['9007199254740991', Number.MAX_SAFE_INTEGER] + ])('parses %s as %d', async (value, expected) => { + set_env('OPTION', value); + const { number_env } = await load_env(); + expect(number_env('OPTION')).toBe(expected); + }); + + test('returns the fallback for an absent variable', async () => { + const { number_env } = await load_env(); + expect(number_env('OPTION', 10)).toBe(10); + }); + + test.each(['-1', '+1', '1.5', '1e2', ' 1', ''])( + 'rejects non-integer syntax %j', + async (value) => { + set_env('OPTION', value); + const { number_env } = await load_env(); + expect(() => number_env('OPTION')).toThrow('expected a non-negative integer'); + } + ); + + test('enforces a minimum', async () => { + set_env('OPTION', '0'); + const { number_env } = await load_env(); + expect(() => number_env('OPTION', undefined, { min: 1 })).toThrow( + 'expected an integer at least 1' + ); + }); + + test('enforces a bounded range', async () => { + set_env('OPTION', '256'); + const { number_env } = await load_env(); + expect(() => number_env('OPTION', undefined, { max: 255 })).toThrow( + 'expected an integer between 0 and 255' + ); + }); + + test('rejects integers beyond the safe range', async () => { + set_env('OPTION', '9007199254740992'); + const { number_env } = await load_env(); + expect(() => number_env('OPTION')).toThrow('expected an integer at least 0'); + }); +}); + +describe('bytes_env', () => { + test.each([ + ['0', 0], + ['512', 512], + ['.5K', 512], + ['512K', 512 * 1024], + ['1.5M', 1.5 * 1024 * 1024], + ['2g', 2 * 1024 * 1024 * 1024], + ['Infinity', Infinity] + ])('parses %s as %d bytes', async (value, expected) => { + set_env('OPTION', value); + const { bytes_env } = await load_env(); + expect(bytes_env('OPTION')).toBe(expected); + }); + + test('returns the fallback for an absent variable', async () => { + const { bytes_env } = await load_env(); + expect(bytes_env('OPTION', 512 * 1024)).toBe(512 * 1024); + }); + + test.each(['', '-1', '1KB', '1T', 'one'])('rejects invalid syntax %j', async (value) => { + set_env('OPTION', value); + const { bytes_env } = await load_env(); + expect(() => bytes_env('OPTION')).toThrow('expected a non-negative number'); + }); + + test.each(['0.1', '9007199254740992'])( + 'rejects a non-whole or unsafe byte count %s', + async (value) => { + set_env('OPTION', value); + const { bytes_env } = await load_env(); + expect(() => bytes_env('OPTION')).toThrow('expected a non-negative number of whole bytes'); + } + ); +}); + +async function load_env(prefix = '') { + mock_handoff({ env_prefix: prefix }); + // a fresh query string re-runs the module-level prefix check + const specifier = `../src/env.js?instance=${++instance}`; + return (await import(specifier)) as typeof import('../src/env.js'); +} + +function set_env(name: string, value: string) { + changed.add(name); + process.env[name] = value; +} diff --git a/packages/adapter-bun/test/handler.spec.ts b/packages/adapter-bun/test/handler.spec.ts new file mode 100644 index 000000000000..463628a7dfb7 --- /dev/null +++ b/packages/adapter-bun/test/handler.spec.ts @@ -0,0 +1,238 @@ +import process from 'node:process'; +import { afterEach, expect, mock, spyOn, test } from 'bun:test'; +import { mock_handoff } from './mocks.js'; + +const environment = new Set(); +let instance = 0; + +afterEach(() => { + for (const name of environment) delete process.env[name]; + environment.clear(); + mock.restore(); +}); + +test('initializes SvelteKit with Bun environment variables and server-readable assets', async () => { + const loaded = await load_handler(); + + expect(loaded.init).toHaveBeenCalledWith({ + env: Bun.env, + read: expect.any(Function) + }); + const { read } = loaded.init.mock.calls[0][0]; + expect(read('asset.txt')).toBe(loaded.stream); + expect(read('missing.txt')).toBeNull(); +}); + +test('normalizes the request origin from the Host header', async () => { + const loaded = await load_handler(); + const original = new Request('http://127.0.0.1:3000/path?query=yes', { + headers: { host: 'public.example:8080' } + }); + + await loaded.handler(original, loaded.bun_server); + + const [request, options] = loaded.respond.mock.calls[0]; + expect(request.url).toBe('https://public.example:8080/path?query=yes'); + expect(options.platform).toEqual({ server: loaded.bun_server }); + loaded.request_ip.mockReturnValue({ address: '127.0.0.1', port: 5000, family: 'IPv4' }); + expect(options.getClientAddress()).toBe('127.0.0.1'); +}); + +test('uses paths.origin as the trusted request origin when configured', async () => { + const loaded = await load_handler({ origin: 'https://canonical.example' }); + + await loaded.handler(new Request('http://internal/path'), loaded.bun_server); + + expect(loaded.respond.mock.calls[0][0].url).toBe('https://canonical.example/path'); +}); + +test('derives the public origin from configured proxy headers', async () => { + set_env('APP_PROTOCOL_HEADER', 'x-forwarded-proto'); + set_env('APP_HOST_HEADER', 'x-forwarded-host'); + set_env('APP_PORT_HEADER', 'x-forwarded-port'); + const loaded = await load_handler({ envPrefix: 'APP_' }); + const request = new Request('http://internal/path', { + headers: { + 'x-forwarded-proto': 'https', + 'x-forwarded-host': 'public.example', + 'x-forwarded-port': '8443' + } + }); + + await loaded.handler(request, loaded.bun_server); + + expect(loaded.respond.mock.calls[0][0].url).toBe('https://public.example:8443/path'); +}); + +test.each([ + ['APP_PROTOCOL_HEADER', 'x-proto', { 'x-proto': 'https%3A' }, 'invalid protocol scheme'], + ['APP_PROTOCOL_HEADER', 'x-proto', { 'x-proto': 'foo' }, 'invalid protocol scheme'], + ['APP_PORT_HEADER', 'x-port', { 'x-port': 'not-a-port' }, 'invalid port'] +])('returns 400 for an invalid origin from %s', async (name, value, headers, message) => { + set_env(name, value); + const loaded = await load_handler({ envPrefix: 'APP_' }); + const error = spyOn(console, 'error').mockImplementation(() => {}); + + const response = await loaded.handler( + new Request('http://internal/path', { headers }), + loaded.bun_server + ); + + expect(response.status).toBe(400); + expect(await response.text()).toBe('Bad Request'); + expect(loaded.respond).not.toHaveBeenCalled(); + expect(error).toHaveBeenCalledWith(expect.stringContaining(message)); +}); + +test('rejects a present but empty Host header', async () => { + const loaded = await load_handler(); + const error = spyOn(console, 'error').mockImplementation(() => {}); + const request = new Request('http://internal/path'); + request.headers.set('host', ''); + + const response = await loaded.handler(request, loaded.bun_server); + + expect(response.status).toBe(400); + expect(loaded.respond).not.toHaveBeenCalled(); + expect(error).toHaveBeenCalledWith(expect.stringContaining('Could not determine host')); +}); + +test('falls back past proxy headers that are present but empty', async () => { + set_env('APP_PROTOCOL_HEADER', 'x-forwarded-proto'); + set_env('APP_HOST_HEADER', 'x-forwarded-host'); + const loaded = await load_handler({ envPrefix: 'APP_' }); + const request = new Request('http://internal/path', { + headers: { 'x-forwarded-proto': '', 'x-forwarded-host': '' } + }); + + await loaded.handler(request, loaded.bun_server); + + expect(loaded.respond.mock.calls[0][0].url).toBe('https://internal/path'); +}); + +test('reads a configured client address header', async () => { + set_env('APP_ADDRESS_HEADER', 'true-client-ip'); + const loaded = await load_handler({ envPrefix: 'APP_' }); + + await loaded.handler( + new Request('http://localhost/', { headers: { 'true-client-ip': '203.0.113.10' } }), + loaded.bun_server + ); + + const get_client_address = loaded.respond.mock.calls[0][1].getClientAddress; + expect(get_client_address()).toBe('203.0.113.10'); +}); + +test('selects a trusted X-Forwarded-For address from the right', async () => { + set_env('APP_ADDRESS_HEADER', 'x-forwarded-for'); + set_env('APP_XFF_DEPTH', '2'); + const loaded = await load_handler({ envPrefix: 'APP_' }); + + await loaded.handler( + new Request('http://localhost/', { + headers: { 'x-forwarded-for': 'spoofed, 203.0.113.10, 10.0.0.2' } + }), + loaded.bun_server + ); + + expect(loaded.respond.mock.calls[0][1].getClientAddress()).toBe('203.0.113.10'); +}); + +test('reports absent and too-short forwarded address headers', async () => { + set_env('APP_ADDRESS_HEADER', 'x-forwarded-for'); + set_env('APP_XFF_DEPTH', '3'); + const loaded = await load_handler({ envPrefix: 'APP_' }); + + await loaded.handler(new Request('http://localhost/'), loaded.bun_server); + let get_client_address = loaded.respond.mock.calls[0][1].getClientAddress; + expect(() => get_client_address()).toThrow( + 'APP_ADDRESS_HEADER=x-forwarded-for but is absent from request' + ); + + loaded.respond.mockClear(); + await loaded.handler( + new Request('http://localhost/', { headers: { 'x-forwarded-for': 'client, proxy' } }), + loaded.bun_server + ); + get_client_address = loaded.respond.mock.calls[0][1].getClientAddress; + expect(() => get_client_address()).toThrow('APP_XFF_DEPTH is 3, but only found 2 addresses'); +}); + +test('returns undefined when Bun cannot determine the peer address', async () => { + const loaded = await load_handler(); + loaded.request_ip.mockReturnValue(null); + + await loaded.handler(new Request('http://localhost/'), loaded.bun_server); + + expect(loaded.respond.mock.calls[0][1].getClientAddress()).toBeUndefined(); +}); + +test('disables timeouts and proxy buffering for event streams', async () => { + const loaded = await load_handler({ + response: new Response('data: ready\n\n', { + headers: { 'content-type': 'text/event-stream; charset=utf-8' } + }) + }); + const request = new Request('http://localhost/events'); + + const response = await loaded.handler(request, loaded.bun_server); + + expect(loaded.timeout).toHaveBeenCalledWith(request, 0); + expect(response.headers.get('x-accel-buffering')).toBe('no'); +}); + +test('leaves ordinary responses and their timeouts unchanged', async () => { + const loaded = await load_handler({ response: new Response('ok') }); + + const response = await loaded.handler(new Request('http://localhost/'), loaded.bun_server); + + expect(loaded.timeout).not.toHaveBeenCalled(); + expect(response.headers.has('x-accel-buffering')).toBe(false); +}); + +async function load_handler({ + origin, + envPrefix = '', + response = new Response('ok') +}: { origin?: string; envPrefix?: string; response?: Response } = {}) { + const stream = new ReadableStream(); + const asset = { stream: mock(() => stream) }; + const init = mock(async (_options: any) => {}); + const respond = mock(async (_request: Request, _options: any) => response); + + class Server { + init = init; + respond = respond; + } + + mock_handoff({ server: new Server(), origin, env_prefix: envPrefix }); + mock.module('../src/routes.js', () => ({ + routes: {}, + server_assets: new Map([['asset.txt', asset]]) + })); + + const request_ip = mock((_request: Request): any => ({ + address: '127.0.0.1', + port: 5000, + family: 'IPv4' + })); + const timeout = mock((_request: Request, _seconds: number) => {}); + const bun_server = { requestIP: request_ip, timeout } as any; + const specifier = `../src/handler.js?instance=${++instance}`; + const { handler } = (await import(specifier)) as typeof import('../src/handler.js'); + + return { + handler, + stream, + init, + respond, + request_ip, + timeout, + bun_server + }; +} + +function set_env(name: string, value: string) { + environment.add(name); + process.env[name] = value; +} diff --git a/packages/adapter-bun/test/mocks.ts b/packages/adapter-bun/test/mocks.ts new file mode 100644 index 000000000000..b502d348d72c --- /dev/null +++ b/packages/adapter-bun/test/mocks.ts @@ -0,0 +1,44 @@ +import { mock } from 'bun:test'; + +// bun:test fixes a mocked module's export names on first registration and only +// updates their values afterwards, so every mock supplies the union of the +// exports that src modules pull from the generated hand-off module +export function mock_handoff({ + server, + dir = '/build', + app_dir = '_app', + base = '/', + embed = false, + env_prefix = '', + origin, + server_options = {}, + assets = [], + redirects = [], + server_assets = [] +}: { + server?: unknown; + dir?: string; + app_dir?: string; + base?: string; + embed?: boolean; + env_prefix?: string; + origin?: string; + server_options?: unknown; + assets?: unknown[]; + redirects?: unknown[]; + server_assets?: unknown[]; +} = {}) { + mock.module('#@sveltejs/adapter-bun', () => ({ + server, + dir, + app_dir, + base, + embed, + env_prefix, + origin, + server_options, + assets, + redirects, + server_assets + })); +} diff --git a/packages/adapter-bun/test/routes.spec.ts b/packages/adapter-bun/test/routes.spec.ts new file mode 100644 index 000000000000..5fbc7c69c31e --- /dev/null +++ b/packages/adapter-bun/test/routes.spec.ts @@ -0,0 +1,291 @@ +import { afterEach, expect, mock, spyOn, test } from 'bun:test'; +import { mock_handoff } from './mocks.js'; + +const meta = { hash: 'abc', mtime: 0 }; +const dir = '/build'; +let instance = 0; + +afterEach(() => { + mock.restore(); +}); + +test('client assets use the configured base and URL-encode path segments', async () => { + const { routes, file } = await load_routes({ base: '/base' }); + + const entries = routes.client_asset('folder/encoded name#1.txt', undefined, meta); + + expect(entries).toHaveLength(1); + expect(entries[0][0]).toBe('/base/folder/encoded%20name%231.txt'); + expect(file).toHaveBeenCalledWith(`${dir}/client/folder/encoded name#1.txt`); + expect(entries[0][1]).toHaveProperty('GET'); + const response = (entries[0][1] as any).GET(new Request('http://localhost/')); + expect(response.headers.get('content-type')).toBe('text/plain;charset=utf-8'); +}); + +test('client index files are also available at their directory URL', async () => { + const { routes } = await load_routes({ base: '/base' }); + + expect(routes.client_asset('index.html', undefined, meta).map(([path]) => path)).toEqual([ + '/base/index.html', + '/base/', + '/base' + ]); + expect(routes.client_asset('docs/index.html', undefined, meta).map(([path]) => path)).toEqual([ + '/base/docs/index.html', + '/base/docs/', + '/base/docs' + ]); +}); + +test('other client HTML files are also available without their extension', async () => { + const { routes } = await load_routes({ base: '/base' }); + + expect(routes.client_asset('page.html', undefined, meta).map(([path]) => path)).toEqual([ + '/base/page.html', + '/base/page' + ]); +}); + +test('sub-delims stay raw in route paths with a fully-encoded alias', async () => { + const { routes } = await load_routes({ base: '/base' }); + + expect(routes.client_asset('a&b.txt', undefined, meta).map(([path]) => path)).toEqual([ + '/base/a&b.txt', + '/base/a%26b.txt' + ]); +}); + +test('route paths use WHATWG serialization, the form user agents send', async () => { + const { routes } = await load_routes({ base: '/base' }); + + expect(routes.client_asset('photo[1]^a|b.png', undefined, meta).map(([path]) => path)).toEqual([ + '/base/photo[1]^a|b.png', + '/base/photo%5B1%5D%5Ea%7Cb.png' + ]); +}); + +test('a root deployment registers routes with a leading slash', async () => { + const { routes } = await load_routes(); + + expect(routes.client_asset('data.json', undefined, meta)[0][0]).toBe('/data.json'); + expect(routes.client_asset('index.html', undefined, meta).map(([path]) => path)).toEqual([ + '/index.html', + '/' + ]); +}); + +test('immutable SvelteKit assets receive a long-lived cache policy', async () => { + const { routes } = await load_routes({ app_dir: '_app' }); + + const request = new Request('http://localhost/'); + const immutable = ( + routes.client_asset('_app/immutable/chunk.js', undefined, meta)[0][1] as any + ).GET(request); + const mutable = (routes.client_asset('favicon.ico', undefined, meta)[0][1] as any).GET(request); + + expect(immutable.headers.get('cache-control')).toBe('public,max-age=31536000,immutable'); + expect(mutable.headers.has('cache-control')).toBe(false); +}); + +test('static routes revalidate against the build-time hash', async () => { + const { routes } = await load_routes(); + + const route = routes.client_asset('data.json', undefined, meta)[0][1] as any; + + const fresh = route.GET(new Request('http://localhost/data.json')); + expect(fresh.status).toBe(200); + expect(fresh.headers.get('etag')).toBe('"abc"'); + + const revalidated = route.GET( + new Request('http://localhost/data.json', { headers: { 'if-none-match': '"abc"' } }) + ); + expect(revalidated.status).toBe(304); + expect(revalidated.headers.get('etag')).toBe('"abc"'); + + const weak = route.GET( + new Request('http://localhost/data.json', { headers: { 'if-none-match': 'W/"abc", "other"' } }) + ); + expect(weak.status).toBe(304); + + const stale = route.GET( + new Request('http://localhost/data.json', { headers: { 'if-none-match': '"old"' } }) + ); + expect(stale.status).toBe(200); + + const wildcard = route.GET( + new Request('http://localhost/data.json', { headers: { 'if-none-match': '*' } }) + ); + expect(wildcard.status).toBe(304); +}); + +test('static routes revalidate by date when the client has no ETag', async () => { + const { routes } = await load_routes(); + + const route = routes.client_asset('data.json', undefined, meta)[0][1] as any; + + const fresh = route.GET(new Request('http://localhost/data.json')); + expect(fresh.headers.get('last-modified')).toBe('Thu, 01 Jan 1970 00:00:00 GMT'); + + const dated = route.GET( + new Request('http://localhost/data.json', { + headers: { 'if-modified-since': 'Thu, 01 Jan 1970 00:00:00 GMT' } + }) + ); + expect(dated.status).toBe(304); + + const stale_etag_wins = route.GET( + new Request('http://localhost/data.json', { + headers: { + 'if-modified-since': 'Thu, 01 Jan 1970 00:00:00 GMT', + 'if-none-match': '"old"' + } + }) + ); + expect(stale_etag_wins.status).toBe(200); +}); + +test('precompressed variants are negotiated with their own validators', async () => { + const { routes, file } = await load_routes(); + + const route = routes.client_asset('app.js', undefined, { + hash: 'abc', + mtime: 0, + br: true, + gz: true + })[0][1] as any; + + const br = route.GET( + new Request('http://localhost/app.js', { headers: { 'accept-encoding': 'br, gzip' } }) + ); + expect(br.headers.get('content-encoding')).toBe('br'); + expect(br.headers.get('etag')).toBe('"abc-br"'); + expect(br.headers.get('vary')).toBe('accept-encoding'); + expect(file).toHaveBeenLastCalledWith(`${dir}/client/app.js.br`); + + const gzip = route.GET( + new Request('http://localhost/app.js', { headers: { 'accept-encoding': 'br;q=0, gzip' } }) + ); + expect(gzip.headers.get('content-encoding')).toBe('gzip'); + expect(gzip.headers.get('etag')).toBe('"abc-gz"'); + expect(file).toHaveBeenLastCalledWith(`${dir}/client/app.js.gz`); + + const any = route.GET( + new Request('http://localhost/app.js', { headers: { 'accept-encoding': '*' } }) + ); + expect(any.headers.get('content-encoding')).toBe('br'); + + const identity = route.GET(new Request('http://localhost/app.js')); + expect(identity.headers.has('content-encoding')).toBe(false); + expect(identity.headers.get('etag')).toBe('"abc"'); + + const revalidated = route.GET( + new Request('http://localhost/app.js', { + headers: { 'accept-encoding': 'br', 'if-none-match': '"abc-br"' } + }) + ); + expect(revalidated.status).toBe(304); +}); + +test('range requests are served from the identity representation', async () => { + const { routes, file } = await load_routes(); + + const route = routes.client_asset('app.js', undefined, { + hash: 'abc', + mtime: 0, + br: true + })[0][1] as any; + const response = route.GET( + new Request('http://localhost/app.js', { + headers: { 'accept-encoding': 'br', range: 'bytes=0-9' } + }) + ); + + expect(response.headers.has('content-encoding')).toBe(false); + expect(response.headers.get('etag')).toBe('"abc"'); + expect(file).toHaveBeenLastCalledWith(`${dir}/client/app.js`); +}); + +test('embedded routes use the imported asset instead of a filesystem path', async () => { + const { routes, file } = await load_routes({ embed: true }); + + routes.client_asset('asset.txt', '/embedded/client.txt', meta); + routes.prerendered_asset('asset.txt', '/embedded/prerendered.txt', meta); + const server_file = routes.server_asset('asset.txt', '/embedded/server.txt'); + + expect(file).toHaveBeenNthCalledWith(1, '/embedded/client.txt'); + expect(file).toHaveBeenNthCalledWith(2, '/embedded/prerendered.txt'); + expect(file).toHaveBeenNthCalledWith(3, '/embedded/server.txt'); + expect(server_file.name).toBe('/embedded/server.txt'); +}); + +test('server assets resolve from the client output in regular builds', async () => { + const { routes, file } = await load_routes(); + + const result = routes.server_asset('nested/read.txt'); + + expect(file).toHaveBeenCalledWith(`${dir}/client/nested/read.txt`); + expect(result.name).toBe(`${dir}/client/nested/read.txt`); +}); + +test('prerendered assets use the base path and preserve their content type', async () => { + const { routes } = await load_routes({ base: '/base' }); + + const [[path, handler]] = routes.prerendered_asset('icon.ico', undefined, meta); + + expect(path).toBe('/base/icon.ico'); + const response = (handler as any).GET(new Request('http://localhost/base/icon.ico')); + expect(response.headers.get('content-type')).toBe('image/x-icon'); +}); + +test.each([ + ['/page/', '/page', '/page/?from=test'], + ['/page', '/page/', '/page?from=test'] +])( + 'prerendered page %s redirects its alternate form %s to the canonical URL', + async (canonical, alternate, location) => { + const { routes } = await load_routes(); + const entries = routes.prerendered_page(canonical, 'page.html', meta); + + expect(entries[0][0]).toBe(canonical); + expect(entries[1][0]).toBe(alternate); + const response = (entries[1][1] as any).GET( + new Request(`http://localhost${alternate}?from=test`) + ); + expect(response.status).toBe(308); + expect(response.headers.get('location')).toBe(location); + } +); + +test('redirects to non-ASCII canonical URLs use a percent-encoded location', async () => { + const { routes } = await load_routes(); + const entries = routes.prerendered_page('/café/', 'cafe.html', meta); + + const response = (entries[1][1] as any).GET(new Request('http://localhost/caf%C3%A9')); + expect(response.headers.get('location')).toBe('/caf%C3%A9/'); +}); + +test('a prerendered root page has no duplicate alternate route', async () => { + const { routes } = await load_routes(); + + expect(routes.prerendered_page('/', 'index.html', meta)).toHaveLength(1); +}); + +test('prerendered redirects retain their status and location', async () => { + const { routes } = await load_routes(); + + const [[path, handler]] = routes.prerendered_redirect('/old path', 307, '/new'); + + expect(path).toBe('/old%20path'); + expect((handler as any).GET.status).toBe(307); + expect((handler as any).GET.headers.get('location')).toBe('/new'); +}); + +async function load_routes({ base = '/', embed = false, app_dir = '_app' } = {}) { + mock_handoff({ app_dir, base, embed, dir }); + // the real Bun.file runs, with the spy recording resolved paths; the files it + // points at need not exist because nothing reads their contents + const file = spyOn(Bun, 'file'); + + const specifier = `../src/routes-util.js?instance=${++instance}`; + return { routes: (await import(specifier)) as typeof import('../src/routes-util.js'), file }; +} diff --git a/packages/adapter-bun/test/start.spec.ts b/packages/adapter-bun/test/start.spec.ts new file mode 100644 index 000000000000..614b237a5dab --- /dev/null +++ b/packages/adapter-bun/test/start.spec.ts @@ -0,0 +1,229 @@ +import fs from 'node:fs'; +import process from 'node:process'; +import { afterAll, afterEach, expect, jest, mock, spyOn, test } from 'bun:test'; +import { mock_handoff } from './mocks.js'; + +// the const captures the real module object before any test swaps the live binding +const real_process = process; +let instance = 0; + +afterEach(() => { + jest.useRealTimers(); + mock.restore(); +}); + +afterAll(() => { + mock.module('node:process', () => ({ default: real_process })); +}); + +test('starts Bun with production defaults and generated request routes', async () => { + const loaded = await load_start(); + + expect(loaded.serve).toHaveBeenCalledWith( + expect.objectContaining({ + development: false, + port: 3000, + maxRequestBodySize: 512 * 1024, + fetch: loaded.handler, + routes: loaded.routes + }) + ); + expect(loaded.log).toHaveBeenCalledWith('Listening on http://localhost:3000/'); +}); + +test('environment variables override TCP server defaults', async () => { + const loaded = await load_start({ + serverOptions: { + hostname: 'default-host', + port: 3000, + reusePort: false, + ipv6Only: false, + idleTimeout: 10, + maxRequestBodySize: 1000, + development: false + }, + env: { + APP_HOST: '127.0.0.1', + APP_PORT: '4000', + APP_REUSE_PORT: 'true', + APP_IPV6_ONLY: 'yes', + APP_CONNECTION_IDLE_TIMEOUT: '30', + APP_BODY_SIZE_LIMIT: '2M', + APP_DEVELOPMENT: 'on' + }, + envPrefix: 'APP_' + }); + + expect(loaded.serve).toHaveBeenCalledWith( + expect.objectContaining({ + hostname: '127.0.0.1', + port: '4000', + reusePort: true, + ipv6Only: true, + idleTimeout: 30, + maxRequestBodySize: 2 * 1024 * 1024, + development: true + }) + ); +}); + +test('a Unix socket takes precedence over TCP-only options', async () => { + const loaded = await load_start({ + serverOptions: { + hostname: 'default-host', + port: 3000, + reusePort: true, + ipv6Only: true + }, + env: { SOCKET_PATH: '/tmp/application.sock' } + }); + + const options = loaded.serve.mock.calls[0][0]; + expect(options.unix).toBe('/tmp/application.sock'); + expect(options).not.toHaveProperty('hostname'); + expect(options).not.toHaveProperty('port'); + expect(options).not.toHaveProperty('reusePort'); + expect(options).not.toHaveProperty('ipv6Only'); + expect(loaded.log).toHaveBeenCalledWith('Listening on /tmp/application.sock'); +}); + +test('removes a stale socket file before listening', async () => { + spyOn(fs, 'statSync').mockReturnValue({ size: 0 } as ReturnType); + const rm = spyOn(fs, 'rmSync').mockImplementation(() => {}); + + await load_start({ env: { SOCKET_PATH: '/tmp/application.sock' } }); + + expect(rm).toHaveBeenCalledWith('/tmp/application.sock'); +}); + +test.each([ + [{ CONNECTION_IDLE_TIMEOUT: '256' }, 'between 0 and 255'], + [{ BODY_SIZE_LIMIT: '1.1' }, 'whole bytes'], + [{ DEVELOPMENT: 'sometimes' }, 'expected a boolean'] +])('rejects invalid startup environment %j', async (env, message) => { + await expect(load_start({ env })).rejects.toThrow(message); +}); + +test('refuses to start on a Bun older than 1.4', async () => { + spyOn(Bun.semver, 'order').mockReturnValue(-1); + await expect(load_start()).rejects.toThrow('requires Bun 1.4'); +}); + +// every other test proves the guard admits the running Bun; these pin the real +// comparator's verdicts for the release shapes the guard must order correctly, +// canaries being the reason it uses order() rather than satisfies() +test.each([ + ['1.3.14', -1], + ['1.4.0', 0], + ['1.4.1', 1], + ['1.5.0-canary.1', 1], + ['2.0.0', 1] +] as const)('Bun.semver orders %s against the 1.4.0 floor as %d', (version, expected) => { + expect(Bun.semver.order(version, '1.4.0')).toBe(expected); +}); + +test.each(['SIGINT', 'SIGTERM'] as const)( + 'gracefully stops the server and emits sveltekit:shutdown for %s', + async (signal) => { + const loaded = await load_start({ pendingRequests: 2 }); + + await loaded.listeners.get(signal)?.(); + + expect(loaded.stop).toHaveBeenCalledTimes(1); + expect(loaded.emit).toHaveBeenCalledWith('sveltekit:shutdown', signal); + expect(loaded.log).toHaveBeenCalledWith( + expect.stringContaining('Waiting for 2 requests to finish before shutting down...') + ); + } +); + +test('force-closes lingering connections after SHUTDOWN_TIMEOUT', async () => { + jest.useFakeTimers(); + try { + let finish_force: (() => void) | undefined; + const loaded = await load_start({ + env: { SHUTDOWN_TIMEOUT: '5' }, + stop: (force) => + force + ? new Promise((resolve) => (finish_force = resolve)) + : new Promise(() => {}) + }); + + const shutdown = loaded.listeners.get('SIGTERM')?.(); + jest.advanceTimersByTime(5000); + await flush_microtasks(); + expect(loaded.stop).toHaveBeenCalledTimes(2); + expect(loaded.stop).toHaveBeenLastCalledWith(true); + expect(loaded.emit).not.toHaveBeenCalled(); + + finish_force?.(); + await shutdown; + expect(loaded.emit).toHaveBeenCalledWith('sveltekit:shutdown', 'SIGTERM'); + } finally { + jest.useRealTimers(); + } +}); + +test('a second shutdown signal forces the process to exit', async () => { + let finish_stop: (() => void) | undefined; + const loaded = await load_start({ + stop: () => new Promise((resolve) => (finish_stop = resolve)) + }); + + const first = loaded.listeners.get('SIGINT')?.(); + await loaded.listeners.get('SIGTERM')?.(); + expect(loaded.exit).toHaveBeenCalledWith(1); + + finish_stop?.(); + await first; +}); + +// bun:test has no async advanceTimersByTime, so drain the promise chains that a +// synchronously-fired timer callback unblocks +async function flush_microtasks() { + for (let i = 0; i < 10; i += 1) await Promise.resolve(); +} + +async function load_start({ + serverOptions = {}, + env = {}, + envPrefix = '', + pendingRequests = 0, + stop: stop_implementation +}: { + serverOptions?: Record; + env?: Record; + envPrefix?: string; + pendingRequests?: number; + stop?: (force?: boolean) => Promise; +} = {}) { + const listeners = new Map Promise | void>(); + const emit = mock((_name: string, _detail: unknown) => {}); + const exit = mock((_code: number) => {}); + const fake_process = { + env, + on: mock((name: string, callback: () => Promise | void) => listeners.set(name, callback)), + emit, + exit + }; + mock.module('node:process', () => ({ default: fake_process })); + mock_handoff({ env_prefix: envPrefix, server_options: serverOptions }); + + const routes = { '/asset': { GET: new Response('asset') } }; + const handler = mock(() => {}); + mock.module('../src/routes.js', () => ({ routes, server_assets: new Map() })); + mock.module('../src/handler.js', () => ({ handler })); + + const stop = mock(stop_implementation ?? (async () => {})); + const server = { + url: new URL('http://localhost:3000'), + pendingRequests, + stop + }; + const serve = spyOn(Bun, 'serve').mockImplementation((_options: any) => server as any); + const log = spyOn(console, 'log').mockImplementation(() => {}); + + await import(`../src/index.js?instance=${++instance}`); + + return { listeners, emit, exit, routes, handler, stop, serve, log }; +} diff --git a/packages/adapter-bun/test/utils.js b/packages/adapter-bun/test/utils.js new file mode 100644 index 000000000000..212bd3745797 --- /dev/null +++ b/packages/adapter-bun/test/utils.js @@ -0,0 +1,30 @@ +import { devices } from '@playwright/test'; +import process from 'node:process'; +import { number_from_env } from '../../../test-utils/index.js'; + +const compiled = process.env.COMPILE === 'true'; +const port = 4174; + +/** @type {import('@playwright/test').PlaywrightTestConfig} */ +export const config = { + forbidOnly: !!process.env.CI, + timeout: process.env.CI ? 45000 : 15000, + webServer: { + command: compiled + ? `bun run --bun build && MY_CUSTOM_PORT=${port} ./build/server` + : 'bun run --bun build && bun run preview', + port + }, + retries: process.env.CI ? 2 : number_from_env('KIT_E2E_RETRIES', 0), + projects: [{ name: 'chromium' }], + use: { + ...devices['Desktop Chrome'], + screenshot: 'only-on-failure', + trace: 'retain-on-failure', + channel: process.env.KIT_E2E_BROWSER ?? 'chromium' + }, + workers: process.env.CI ? 2 : number_from_env('KIT_E2E_WORKERS', undefined), + reporter: 'list', + testDir: 'test', + testMatch: /(.+\.)?(test|spec)\.[jt]s/ +}; diff --git a/packages/adapter-bun/tests/types.ts b/packages/adapter-bun/tests/types.ts new file mode 100644 index 000000000000..f61c45935c0c --- /dev/null +++ b/packages/adapter-bun/tests/types.ts @@ -0,0 +1,45 @@ +import adapter from '../index.js'; + +adapter(); + +adapter({ + out: 'dist', + envPrefix: 'APP_', + serverOptions: { + development: false, + hostname: '127.0.0.1', + port: 4000, + idleTimeout: 30, + maxRequestBodySize: 1024, + reusePort: true, + ipv6Only: false + }, + buildOptions: { + compile: { outfile: 'application', target: 'bun-linux-x64' }, + minify: true, + bytecode: true, + sourcemap: 'linked', + drop: ['debugger'], + external: ['sharp'] + } +}); + +adapter({ + serverOptions: { unix: '/tmp/application.sock' }, + buildOptions: { compile: false } +}); + +adapter({ + buildOptions: { + compile: true, + // @ts-expect-error the adapter reserves the top-level runtime target + target: 'node' + } +}); + +adapter({ + serverOptions: { + // @ts-expect-error the generated server owns its fetch handler + fetch: () => new Response('custom') + } +}); diff --git a/packages/adapter-bun/tsconfig.json b/packages/adapter-bun/tsconfig.json new file mode 100644 index 000000000000..345223b1bc72 --- /dev/null +++ b/packages/adapter-bun/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "noEmit": true, + "target": "es2022", + "module": "node16", + "moduleResolution": "node16", + "skipLibCheck": true, + "paths": { + "@sveltejs/kit": ["../kit/types/index"] + }, + "types": ["bun-types", "node"] + }, + "include": [ + "index.js", + "src/**/*.js", + "test/*.js", + "test/*.ts", + "tests/**/*.js", + "tests/**/*.ts", + "./internal.d.ts" + ] +} diff --git a/packages/adapter-cloudflare/.gitignore b/packages/adapter-cloudflare/.gitignore deleted file mode 100644 index 2c8afebd8eda..000000000000 --- a/packages/adapter-cloudflare/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/files diff --git a/packages/adapter-cloudflare/CHANGELOG.md b/packages/adapter-cloudflare/CHANGELOG.md index 372634fade86..cef5e64eb2ca 100644 --- a/packages/adapter-cloudflare/CHANGELOG.md +++ b/packages/adapter-cloudflare/CHANGELOG.md @@ -1,5 +1,125 @@ # @sveltejs/adapter-cloudflare +## 8.0.0-next.8 + +### Patch Changes + +- fix: dispose the platform proxy when the Vite dev or preview server closes ([#17238](https://github.com/sveltejs/kit/pull/17238)) +- Updated dependencies [[`12ae0b9`](https://github.com/sveltejs/kit/commit/12ae0b9f7b1b92a0356dadcd87d47f8a1707bf3e), [`ee3332f`](https://github.com/sveltejs/kit/commit/ee3332fe58292be9fcfabaef65dc39e61ab6d1f8), [`72832f9`](https://github.com/sveltejs/kit/commit/72832f9f85d94e569b4b4f27799dc6a01564fd01), [`846f3d4`](https://github.com/sveltejs/kit/commit/846f3d42a2cfe60f0501fc80a7ceeaa604324c30)]: + - @sveltejs/kit@3.0.0-next.31 + +## 8.0.0-next.7 + +### Major Changes + +- breaking: populate env vars before `instrumentation.server.js` is evaluated and update the adapter instrumentation API ([#16303](https://github.com/sveltejs/kit/pull/16303)) + +- breaking: remove cloudflare `platform`, emulate the `cloudflare:workers` module instead ([#16754](https://github.com/sveltejs/kit/pull/16754)) + +- breaking: replace the `builder.generateManifest` with `builder.generateServerInstance` and `builder.manifest` ([#16875](https://github.com/sveltejs/kit/pull/16875)) + +### Patch Changes + +- Updated dependencies [[`9a4b343`](https://github.com/sveltejs/kit/commit/9a4b343bd8ab0060dc624fbdd139155fc4a0a1dc), [`ee2f55b`](https://github.com/sveltejs/kit/commit/ee2f55b1e3338d2fa92a25fe53d095129edf1002), [`65673a4`](https://github.com/sveltejs/kit/commit/65673a4dd35054c16f8555eb896dfb773d86a556), [`1b79845`](https://github.com/sveltejs/kit/commit/1b79845e117f417b63fa399149816d73882c27ea), [`b12bb78`](https://github.com/sveltejs/kit/commit/b12bb789c17c30e7ff835de4af004d2f3eb36159), [`03f1687`](https://github.com/sveltejs/kit/commit/03f1687fe612ce3d2d9131139b5b188d9cf90c64), [`49f0808`](https://github.com/sveltejs/kit/commit/49f0808f3e983d0cb5a4d586cf0d1678467431ed), [`e7c74bf`](https://github.com/sveltejs/kit/commit/e7c74bf8227dc228cb36e822eaff5992569b830a), [`0876d63`](https://github.com/sveltejs/kit/commit/0876d633145088c7a5213e9f530cf2fed96ce35d), [`9a2b9ba`](https://github.com/sveltejs/kit/commit/9a2b9bacbb2de0533966bb029ef526b357aac086), [`c9f54b5`](https://github.com/sveltejs/kit/commit/c9f54b5d66acd9443d36b0ae9aa0a656809bd7f9), [`22b8152`](https://github.com/sveltejs/kit/commit/22b8152f74882775ac63e1cf96d3016b92ac8256), [`f6d44cf`](https://github.com/sveltejs/kit/commit/f6d44cf007d6c6b54de57f6ae4ef8e2e7fea2f44), [`3c3a27b`](https://github.com/sveltejs/kit/commit/3c3a27bf67a26164266e132dbb7f9266643cf724), [`7f376b1`](https://github.com/sveltejs/kit/commit/7f376b109cd770638806184cf5d8bf4cf88c5bf2), [`8a587c7`](https://github.com/sveltejs/kit/commit/8a587c7b3919408ec66a362eb249fe5cf43f5316), [`aa4f960`](https://github.com/sveltejs/kit/commit/aa4f9605761e686f6aaf414b9186c68ddd2468dc)]: + - @sveltejs/kit@3.0.0-next.27 + +## 8.0.0-next.6 + +### Patch Changes + +- chore: bump `@cloudflare/worker-types` to 5.20260809.1 ([#16745](https://github.com/sveltejs/kit/pull/16745)) +- Updated dependencies [[`1742811`](https://github.com/sveltejs/kit/commit/1742811562358caf6042bed33c60ce6427a3e831), [`1611c61`](https://github.com/sveltejs/kit/commit/1611c617a1ec1b32e10627c2815fb7e6673f3157), [`b361b81`](https://github.com/sveltejs/kit/commit/b361b815e79828a6b7404d759d0a4c3892814a4e), [`b361b81`](https://github.com/sveltejs/kit/commit/b361b815e79828a6b7404d759d0a4c3892814a4e), [`b361b81`](https://github.com/sveltejs/kit/commit/b361b815e79828a6b7404d759d0a4c3892814a4e), [`b361b81`](https://github.com/sveltejs/kit/commit/b361b815e79828a6b7404d759d0a4c3892814a4e), [`13e7b18`](https://github.com/sveltejs/kit/commit/13e7b186a94b5b8317771d2b1e795b432dcd6faf), [`529346d`](https://github.com/sveltejs/kit/commit/529346d96369f92cdd804be9c4313c598efe34b8), [`b361b81`](https://github.com/sveltejs/kit/commit/b361b815e79828a6b7404d759d0a4c3892814a4e), [`81d6319`](https://github.com/sveltejs/kit/commit/81d6319eef08a765ccd3922790a7aee16e284cc2), [`b361b81`](https://github.com/sveltejs/kit/commit/b361b815e79828a6b7404d759d0a4c3892814a4e), [`69a5bdf`](https://github.com/sveltejs/kit/commit/69a5bdf9f45810eeed2faa089c8d3206463d347e)]: + - @sveltejs/kit@3.0.0-next.20 + +## 8.0.0-next.5 + +### Major Changes + +- breaking: minimum Wrangler version required is `4.118.0` ([#16668](https://github.com/sveltejs/kit/pull/16668)) + +- breaking: upgrade `@cloudflare/workers-types` to 4.20260219.0 + +- breaking: upgrade minimum `wrangler` version to ^4.67.0 + +- breaking: remove `platform.context` in favour of `platform.ctx` + +- breaking: require SvelteKit 3 + +- breaking: remove use of the Cloudflare Workers Cache API in favour of Workers Caching ([#16640](https://github.com/sveltejs/kit/pull/16640)) + +### Patch Changes + +- chore: use `node:fs` instead of deprecated `builder.rimraf` and `builder.mkdirp` ([#16286](https://github.com/sveltejs/kit/pull/16286)) + +- chore: bump `@cloudflare/workers-types` to `4.20260621.1` + +- fix: avoid overriding user's existing `_headers` rules + +- chore: check the `WORKERS_CI` environment variable to determine if we're building for Cloudflare Workers + +- fix: allow prerelease versions of SvelteKit 3 to satisfy the peer dependency range + +- fix: avoid caching immutable asset 404s +- Updated dependencies [[`bbd1e2f`](https://github.com/sveltejs/kit/commit/bbd1e2fcc9af58edb9846393b081be61df4789d5), [`e21d27b`](https://github.com/sveltejs/kit/commit/e21d27ba490c89da98dbad24fca364d0163137c4), [`6044d05`](https://github.com/sveltejs/kit/commit/6044d055031bc17deedc069dc4016565364bd4f4), [`7fb611f`](https://github.com/sveltejs/kit/commit/7fb611fb604e4d74cd889f5fe21ccd255f876382), [`9f06b3a`](https://github.com/sveltejs/kit/commit/9f06b3ac78be30d27b07fc00ea40bda493c573aa), [`8763327`](https://github.com/sveltejs/kit/commit/87633271ce40ac0839be9468598e965bde3cad51), [`7dc2379`](https://github.com/sveltejs/kit/commit/7dc23798cde97b5616049ba8d443a07e8c558bdc), [`a70c3eb`](https://github.com/sveltejs/kit/commit/a70c3eb31fe1e28f1532c84ec569ec7099da3344), [`89f49c7`](https://github.com/sveltejs/kit/commit/89f49c70b7fb4883cf704d2fb57d2d8595790ad2), [`f36b3a4`](https://github.com/sveltejs/kit/commit/f36b3a4cd307bd8c89e03fa191becc175e7b3077), [`1144a63`](https://github.com/sveltejs/kit/commit/1144a63d318c3c3255d06b374bb7a656a469fabf), [`9f06b3a`](https://github.com/sveltejs/kit/commit/9f06b3ac78be30d27b07fc00ea40bda493c573aa)]: + - @sveltejs/kit@3.0.0-next.15 + +## 8.0.0-next.4 + +### Patch Changes + +- fix: avoid caching immutable asset 404s ([#16627](https://github.com/sveltejs/kit/pull/16627)) +- Updated dependencies [[`c3f58bb`](https://github.com/sveltejs/kit/commit/c3f58bb0eb6bde790897f1e8b89019bc39a062e5), [`b655dc7`](https://github.com/sveltejs/kit/commit/b655dc7cf043ff01d53e494e9744e548bb3ad9fd), [`a70cc4f`](https://github.com/sveltejs/kit/commit/a70cc4f2ca24e7099c898456085ffe18d191657e), [`b325afb`](https://github.com/sveltejs/kit/commit/b325afb509b54445f678348e88ea2bf0152f4cec), [`e76b3d7`](https://github.com/sveltejs/kit/commit/e76b3d778bacb429fdb40a41b13ff04b54d97c1f), [`58f47eb`](https://github.com/sveltejs/kit/commit/58f47eb94016c5405829667bbc873e027c7b363c), [`fe9d8d9`](https://github.com/sveltejs/kit/commit/fe9d8d939b6d1a07b2957104fda6a1123f5eebd3), [`0c7bbd7`](https://github.com/sveltejs/kit/commit/0c7bbd7bd4a346c560e7cbf9b39ba8bda7c718f0), [`08d7e2a`](https://github.com/sveltejs/kit/commit/08d7e2a1b327d4e68c217f6e7d6b305732c82b1b), [`f70f679`](https://github.com/sveltejs/kit/commit/f70f679143d4330b6d3b7d0e690145b5487df97f), [`0624b26`](https://github.com/sveltejs/kit/commit/0624b2678efd41223f70972f7bff2d81d4a5c7f9), [`0624b26`](https://github.com/sveltejs/kit/commit/0624b2678efd41223f70972f7bff2d81d4a5c7f9), [`f4222ad`](https://github.com/sveltejs/kit/commit/f4222adc504a7041b50325424a3eb86af02961ed), [`089628b`](https://github.com/sveltejs/kit/commit/089628b0d9637578085ad61b5786cd1c4476e399), [`d8617b3`](https://github.com/sveltejs/kit/commit/d8617b3f2f08144f6acfc7c636b89d62b982a33d), [`c7369a1`](https://github.com/sveltejs/kit/commit/c7369a148169dfd51af150c461ff266977b6befd), [`5e5e592`](https://github.com/sveltejs/kit/commit/5e5e592f093d05c6ea30a41a0ebf543829069c5f), [`60057c1`](https://github.com/sveltejs/kit/commit/60057c1d2a1e6429e5f315d587ebc01baef6dc94), [`f70f679`](https://github.com/sveltejs/kit/commit/f70f679143d4330b6d3b7d0e690145b5487df97f), [`38f1528`](https://github.com/sveltejs/kit/commit/38f15289e249942275eab1281516702de08b7a30), [`40092f3`](https://github.com/sveltejs/kit/commit/40092f34aa063d033fc7a4d1d6fb1c10abd8cbf6), [`ff4247e`](https://github.com/sveltejs/kit/commit/ff4247e2376862207dd9df7317f39d8bb8dd9ee4), [`79a5de5`](https://github.com/sveltejs/kit/commit/79a5de581fc1bb708879678b8295706221ba0f0f), [`a70cc4f`](https://github.com/sveltejs/kit/commit/a70cc4f2ca24e7099c898456085ffe18d191657e)]: + - @sveltejs/kit@3.0.0-next.14 + +## 8.0.0-next.3 + +### Patch Changes + +- chore: bump `@cloudflare/workers-types` to `4.20260621.1` ([#16455](https://github.com/sveltejs/kit/pull/16455)) +- Updated dependencies [[`adc4c5b`](https://github.com/sveltejs/kit/commit/adc4c5be542ecf7da93ee8d0e90e9d13b952998b), [`ecb0701`](https://github.com/sveltejs/kit/commit/ecb0701b8826e41229f84922f37c12099c66d3bc), [`4b4cc60`](https://github.com/sveltejs/kit/commit/4b4cc60990fa940236639a6cecb6c8efe4287bfa), [`7390dbe`](https://github.com/sveltejs/kit/commit/7390dbe6ef8dff0a1cd923f96daef462a3ce68f2), [`2e71340`](https://github.com/sveltejs/kit/commit/2e713407deb22965777afb01cb8215013ef8bfd3), [`c87bc3a`](https://github.com/sveltejs/kit/commit/c87bc3ac7830f8ee4e76254c9315adcc79ff6a99), [`af15c6c`](https://github.com/sveltejs/kit/commit/af15c6cab2fbfe6bae51dc1fa77a9271df996a47), [`c6fa431`](https://github.com/sveltejs/kit/commit/c6fa43157ae0dbaa9fc61b7e8d2db673e1f9f893), [`bfe4dea`](https://github.com/sveltejs/kit/commit/bfe4deae006f61347b5a5dce7747ec96692e97f9), [`df7dc72`](https://github.com/sveltejs/kit/commit/df7dc7219451bcc86a93e93188d78d664973d9ae), [`5ae11a1`](https://github.com/sveltejs/kit/commit/5ae11a119667c1b24e3b6d17f4b538032ddf1ab1), [`781205c`](https://github.com/sveltejs/kit/commit/781205c41cc5696eb862e5c13356c301a0368983), [`c87bc3a`](https://github.com/sveltejs/kit/commit/c87bc3ac7830f8ee4e76254c9315adcc79ff6a99), [`a1bfeb9`](https://github.com/sveltejs/kit/commit/a1bfeb9de906ae140b9cdc7d7eeaf0ee652a7bcc), [`c87bc3a`](https://github.com/sveltejs/kit/commit/c87bc3ac7830f8ee4e76254c9315adcc79ff6a99), [`9f0127d`](https://github.com/sveltejs/kit/commit/9f0127d4eee3f17dc1db1d2b99b1ca5fc80af784), [`4b4cc60`](https://github.com/sveltejs/kit/commit/4b4cc60990fa940236639a6cecb6c8efe4287bfa), [`8f5b9c7`](https://github.com/sveltejs/kit/commit/8f5b9c72ae930b3b93ef9e1b9ecb9e52664e1d74), [`c9b5544`](https://github.com/sveltejs/kit/commit/c9b554448633e1839e8c0f16c374b00a5ce4d1b6), [`fe1d4a4`](https://github.com/sveltejs/kit/commit/fe1d4a4d68d1d7a3925004fe804209ff54091e34), [`4b4cc60`](https://github.com/sveltejs/kit/commit/4b4cc60990fa940236639a6cecb6c8efe4287bfa), [`5f78e95`](https://github.com/sveltejs/kit/commit/5f78e953ae3966c23a01194dd200f5c57f723e40), [`25510fc`](https://github.com/sveltejs/kit/commit/25510fc79484363277d062e01f2b872f17a473f5), [`c99a6cf`](https://github.com/sveltejs/kit/commit/c99a6cfb7fec133aa85a6de59b43fc442aedc41a), [`17a45ca`](https://github.com/sveltejs/kit/commit/17a45cadb9356403b82c170f0d0027a1ba085667), [`2ca20c3`](https://github.com/sveltejs/kit/commit/2ca20c3ee7348836757dc6f3469217e46957766b), [`eb5c973`](https://github.com/sveltejs/kit/commit/eb5c9737b6d74c53f572b3e047faa5947052ced2)]: + - @sveltejs/kit@3.0.0-next.12 + +## 8.0.0-next.2 + +### Patch Changes + +- fix: allow prerelease versions of SvelteKit 3 to satisfy the peer dependency range ([#16286](https://github.com/sveltejs/kit/pull/16286)) +- Updated dependencies [[`737d119`](https://github.com/sveltejs/kit/commit/737d119730ec163832f838b3813c9cbb75dd799f), [`fa78efb`](https://github.com/sveltejs/kit/commit/fa78efb626031497eecc3a88fa7b6f6da36eca0f), [`07c207e`](https://github.com/sveltejs/kit/commit/07c207ed919570d4ed4954f751056b4f838abdea), [`a47071b`](https://github.com/sveltejs/kit/commit/a47071bb6581fe5f6df4cc47b80e9f0f594624e5), [`14d7d5a`](https://github.com/sveltejs/kit/commit/14d7d5abd9cd8462e4c79ed6a5c10bc43258e97a), [`5c38e51`](https://github.com/sveltejs/kit/commit/5c38e515db7fbb92e5ae01db84b4f0040a02f187), [`0702baa`](https://github.com/sveltejs/kit/commit/0702baa58b2a4600e83b55ef4a85e9cbe3391a4f), [`e1938c6`](https://github.com/sveltejs/kit/commit/e1938c62348b8ccc83e186b1acad76a0a10cc9ea), [`8293144`](https://github.com/sveltejs/kit/commit/8293144b6fbfff9f97cdb1d2224a5eaa04c84fc4), [`f76d7d9`](https://github.com/sveltejs/kit/commit/f76d7d91e18a0afd1f6798c095eb8c170c108cff), [`ab5c253`](https://github.com/sveltejs/kit/commit/ab5c253efb2546ead74db4c8c9b4c7639d97e0ed), [`b557b1b`](https://github.com/sveltejs/kit/commit/b557b1ba76903e1fb0fc3bb87bddc5420332137a), [`4a513e2`](https://github.com/sveltejs/kit/commit/4a513e27ab2ad44dae0c6a4a6c8deda840008530)]: + - @sveltejs/kit@3.0.0-next.8 + +## 8.0.0-next.1 +### Patch Changes + + +- fix: avoid overriding user's existing `_headers` rules ([#16183](https://github.com/sveltejs/kit/pull/16183)) + +- Updated dependencies [[`3c434fb`](https://github.com/sveltejs/kit/commit/3c434fbf12822e19bea833a2084b329590ccf5b1), [`a9284e8`](https://github.com/sveltejs/kit/commit/a9284e8e627b1b0a8d3772c53310c41056e556b1), [`3726a7a`](https://github.com/sveltejs/kit/commit/3726a7a67c949a160283df7db6033b440e229171), [`f9d2240`](https://github.com/sveltejs/kit/commit/f9d22409aa0fca0f5035aead811fe6907f63c293), [`a9284e8`](https://github.com/sveltejs/kit/commit/a9284e8e627b1b0a8d3772c53310c41056e556b1), [`7c040ba`](https://github.com/sveltejs/kit/commit/7c040ba28e98ecbf38acbbf93653fc6c33a506ba), [`223eaad`](https://github.com/sveltejs/kit/commit/223eaadca55c6e73096cd605759d6c7366f260a2), [`223eaad`](https://github.com/sveltejs/kit/commit/223eaadca55c6e73096cd605759d6c7366f260a2), [`3b907d4`](https://github.com/sveltejs/kit/commit/3b907d449a42f9d763cf8eaa6ac5a88975137011), [`fd628a5`](https://github.com/sveltejs/kit/commit/fd628a57348d25fb433f63778e0239892ebb1da2), [`223eaad`](https://github.com/sveltejs/kit/commit/223eaadca55c6e73096cd605759d6c7366f260a2), [`6c1d035`](https://github.com/sveltejs/kit/commit/6c1d03535b09f670164eb22f05405c17120149f1), [`178eac0`](https://github.com/sveltejs/kit/commit/178eac007e648f3235bc45d926cfc029ddf8de07), [`c6562a9`](https://github.com/sveltejs/kit/commit/c6562a92cb908058cab3fabad075a94ad9cc6e39), [`8eca2ab`](https://github.com/sveltejs/kit/commit/8eca2ab8b4760456a782d7a29f83f24a15fac177), [`61cf188`](https://github.com/sveltejs/kit/commit/61cf188e38725b041d0411c6074ad2649a7c42b4)]: + - @sveltejs/kit@3.0.0-next.6 + +## 8.0.0-next.0 +### Major Changes + + +- breaking: upgrade `@cloudflare/workers-types` to 4.20260219.0 ([#15347](https://github.com/sveltejs/kit/pull/15347)) + + +- breaking: upgrade minimum `wrangler` version to ^4.67.0 ([#15347](https://github.com/sveltejs/kit/pull/15347)) + + +- breaking: remove `platform.context` in favour of `platform.ctx` ([#15347](https://github.com/sveltejs/kit/pull/15347)) + + +- breaking: require SvelteKit 3 ([#15506](https://github.com/sveltejs/kit/pull/15506)) + + +### Patch Changes + + +- chore: check the `WORKERS_CI` environment variable to determine if we're building for Cloudflare Workers ([#13733](https://github.com/sveltejs/kit/pull/13733)) + +- Updated dependencies [[`fa335bd`](https://github.com/sveltejs/kit/commit/fa335bd8af3a982d68c4d34b813b68887dc7cce8), [`3031d89`](https://github.com/sveltejs/kit/commit/3031d89087d49297e89b6566108a1a1105c38475), [`cb9d416`](https://github.com/sveltejs/kit/commit/cb9d416d05266b6237f2856fb94df5b8a9b04279), [`caf3a18`](https://github.com/sveltejs/kit/commit/caf3a18de6581ab7d454d6fa8ad37ebdbdef95e6), [`4777827`](https://github.com/sveltejs/kit/commit/4777827246727045cf0209399751377564e7951d), [`a2792e2`](https://github.com/sveltejs/kit/commit/a2792e2cdc2dee91c20c7bfd921a104b539abae5), [`ba36148`](https://github.com/sveltejs/kit/commit/ba36148577ed0bc2675034a3d57001d71d328cd1), [`48e8710`](https://github.com/sveltejs/kit/commit/48e8710889b348a039322e05f3d2672327d56184), [`e2f3075`](https://github.com/sveltejs/kit/commit/e2f3075ef299b91175fae116e6a76269002029cf), [`047d6a0`](https://github.com/sveltejs/kit/commit/047d6a0f1c3fd1bc6f90a81177a2834b023a64b3), [`87603d1`](https://github.com/sveltejs/kit/commit/87603d13d009cada171c9b5d5daad5adad4f72de), [`096962c`](https://github.com/sveltejs/kit/commit/096962c47b0b0f63acd0d539d3973797e4867487), [`d545970`](https://github.com/sveltejs/kit/commit/d54597018308068aea5268e6c45b26398cd57607), [`e2f3075`](https://github.com/sveltejs/kit/commit/e2f3075ef299b91175fae116e6a76269002029cf), [`d06affc`](https://github.com/sveltejs/kit/commit/d06affc9246c676fc32b644efc0bbf53b42bc67d), [`8af47eb`](https://github.com/sveltejs/kit/commit/8af47eb06f9a1a883058de059ccf81aab4e85627), [`e2f3075`](https://github.com/sveltejs/kit/commit/e2f3075ef299b91175fae116e6a76269002029cf), [`5c4d130`](https://github.com/sveltejs/kit/commit/5c4d130ea2953464d8b4b462eb5aa4641aa78da1), [`3f11f35`](https://github.com/sveltejs/kit/commit/3f11f35125c5b3625891f0d00d9c7c826e6eaea2), [`caf3a18`](https://github.com/sveltejs/kit/commit/caf3a18de6581ab7d454d6fa8ad37ebdbdef95e6), [`8823037`](https://github.com/sveltejs/kit/commit/8823037b4a341013cc40a9bb688ee23bda3dbb04), [`1d76212`](https://github.com/sveltejs/kit/commit/1d76212740f59ed58839ef569899c653870834fd), [`0dc0548`](https://github.com/sveltejs/kit/commit/0dc0548307a439b6a6f24b14405366ced213f828), [`00d81fa`](https://github.com/sveltejs/kit/commit/00d81fab618435bac95b1bacb5e9734b61ba1af6)]: + - @sveltejs/kit@3.0.0-next.0 + ## 7.2.9 ### Patch Changes diff --git a/packages/adapter-cloudflare/ambient.d.ts b/packages/adapter-cloudflare/ambient.d.ts deleted file mode 100644 index 033b35b56cf0..000000000000 --- a/packages/adapter-cloudflare/ambient.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { - CacheStorage, - IncomingRequestCfProperties, - ExecutionContext -} from '@cloudflare/workers-types'; - -declare global { - namespace App { - export interface Platform { - // we should not type `env` here because it will override any type set by - // the user in `src/app.d.ts` - ctx: ExecutionContext; - /** @deprecated Use `ctx` instead */ - context: ExecutionContext; - caches: CacheStorage; - cf?: IncomingRequestCfProperties; - } - } -} diff --git a/packages/adapter-cloudflare/files/internal.d.ts b/packages/adapter-cloudflare/files/internal.d.ts new file mode 100644 index 000000000000..d3e8d81fe7e8 --- /dev/null +++ b/packages/adapter-cloudflare/files/internal.d.ts @@ -0,0 +1,17 @@ +declare module 'SERVER' { + export const server: import('@sveltejs/kit').Server; +} + +namespace Cloudflare { + interface Env { + ASSETS_BINDING: { + fetch: typeof fetch; + }; + [key: string]: string | undefined; + } +} + +declare const BASE_PATH: string; +declare const APP_PATH: string; +declare const PRERENDERED: Set; +declare const MANIFEST_ASSETS: Set; diff --git a/packages/adapter-cloudflare/files/tsconfig.json b/packages/adapter-cloudflare/files/tsconfig.json new file mode 100644 index 000000000000..076e5e03478a --- /dev/null +++ b/packages/adapter-cloudflare/files/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "types": ["@cloudflare/workers-types"], + // worker types are full of typescript errors we can't fix + "skipLibCheck": true + }, + "include": ["worker.js", "internal.d.ts"] +} diff --git a/packages/adapter-cloudflare/files/worker.js b/packages/adapter-cloudflare/files/worker.js new file mode 100644 index 000000000000..4fef8368d98c --- /dev/null +++ b/packages/adapter-cloudflare/files/worker.js @@ -0,0 +1,96 @@ +import { server } from 'SERVER'; +import { env } from 'cloudflare:workers'; + +const base_path = BASE_PATH; +const assets = MANIFEST_ASSETS; +const prerendered = PRERENDERED; + +const immutable = `/${APP_PATH}/immutable/`; +const version_file = `/${APP_PATH}/version.json`; + +/** + * We don't know the origin until we receive a request, but + * that's guaranteed to happen before we call `read` + * @type {string} + */ +let origin; + +const initialized = server.init({ + env, + read: async (file) => { + const url = `${origin}/${file}`; + const response = await env.ASSETS_BINDING.fetch(url); + + if (!response.ok) { + throw new Error( + `read(...) failed: could not fetch ${url} (${response.status} ${response.statusText})` + ); + } + + return response.body; + } +}); + +/** @type {import('@cloudflare/workers-types').ExportedHandler} */ +export default { + async fetch(req, env) { + if (!origin) { + origin = new URL(req.url).origin; + } + + // always await initialization to prevent race condition with concurrent requests + await initialized; + + let { pathname, search } = new URL(req.url); + try { + pathname = decodeURIComponent(pathname); + } catch { + // ignore invalid URI + } + + const stripped_pathname = pathname.replace(/\/$/, ''); + + // files in /static, the service worker, and Vite imported server assets + let is_static_asset = false; + const filename = stripped_pathname.slice(base_path.length + 1); + if (filename) { + is_static_asset = assets.has(filename) || assets.has(filename + '/index.html'); + } + + let location = pathname.at(-1) === '/' ? stripped_pathname : pathname + '/'; + + if ( + is_static_asset || + prerendered.has(pathname) || + pathname === version_file || + pathname.startsWith(immutable) + ) { + let res = await env.ASSETS_BINDING.fetch(req); + // `_headers` applies cache regardless of status, so we need to ensure an + // error response does not get cached + if (res.status >= 400) { + res = new Response(res.body, res); // headers from ASSETS are immutable. see https://developers.cloudflare.com/workers/examples/alter-headers/ + res.headers.set('cache-control', 'no-store'); + } + return res; + } + + // trailing slash redirect for prerendered pages + if (location && prerendered.has(location)) { + if (search) location += search; + return new Response('', { + status: 308, + headers: { + location + } + }); + } + + // dynamically-generated pages + return await server.respond(req, { + getClientAddress() { + return /** @type {string} */ (req.headers.get('cf-connecting-ip')); + } + }); + } +}; diff --git a/packages/adapter-cloudflare/globals.d.ts b/packages/adapter-cloudflare/globals.d.ts new file mode 100644 index 000000000000..3b93bdcd6f8f --- /dev/null +++ b/packages/adapter-cloudflare/globals.d.ts @@ -0,0 +1,5 @@ +import type { PlatformProxy } from 'wrangler'; + +declare global { + var __sveltekit_cloudflare_platform: PlatformProxy | undefined; +} diff --git a/packages/adapter-cloudflare/index.d.ts b/packages/adapter-cloudflare/index.d.ts index b9062094bd08..9c7266f124e0 100644 --- a/packages/adapter-cloudflare/index.d.ts +++ b/packages/adapter-cloudflare/index.d.ts @@ -1,5 +1,4 @@ import { Adapter } from '@sveltejs/kit'; -import './ambient.js'; import { GetPlatformProxyOptions } from 'wrangler'; export default function plugin(options?: AdapterOptions): Adapter; diff --git a/packages/adapter-cloudflare/index.js b/packages/adapter-cloudflare/index.js index 24e2dd503f07..f50cba55aa80 100644 --- a/packages/adapter-cloudflare/index.js +++ b/packages/adapter-cloudflare/index.js @@ -1,5 +1,7 @@ -import { VERSION } from '@sveltejs/kit'; -import { copyFileSync, existsSync, readFileSync, writeFileSync } from 'node:fs'; +/** @import { Plugin } from 'vite' */ +/** @import { GetPlatformProxyOptions } from 'wrangler' */ + +import fs from 'node:fs'; import path from 'node:path'; import process from 'node:process'; import { fileURLToPath } from 'node:url'; @@ -8,36 +10,40 @@ import { is_building_for_cloudflare_pages, validate_worker_settings, get_routes_json, - parse_redirects + parse_redirects, + append_headers } from './utils.js'; +import { exactRegex } from '@rolldown/pluginutils'; +import { getRequest } from '@sveltejs/kit/node'; const name = '@sveltejs/adapter-cloudflare'; -const [kit_major, kit_minor] = VERSION.split('.'); -/** @type {import('./index.js').default} */ +/** @type {typeof import('./index.js').default} */ export default function (options = {}) { + // Add a random query so we can reliably string-replace the stub + const stub_import = + import.meta.resolve('./src/virtual-cloudflare-workers.js') + '?' + crypto.randomUUID(); return { name, - /** @param {Builder2_0_0} builder */ async adapt(builder) { if ( - existsSync('_routes.json') || - existsSync(`${builder.config.kit.files.assets}/_routes.json`) + fs.existsSync('_routes.json') || + fs.existsSync(`${builder.config.files.assets}/_routes.json`) ) { throw new Error( - "Cloudflare Pages' _routes.json should be configured in svelte.config.js. See https://svelte.dev/docs/kit/adapter-cloudflare#Options-routes" + "Cloudflare Pages' _routes.json should be configured from the adapter option of the SvelteKit plugin in your vite.config.js. See https://svelte.dev/docs/kit/adapter-cloudflare#Options-routes" ); } - if (existsSync(`${builder.config.kit.files.assets}/_headers`)) { + if (fs.existsSync(`${builder.config.files.assets}/_headers`)) { throw new Error( - `The _headers file should be placed in the project root rather than the ${builder.config.kit.files.assets} directory` + `The _headers file should be placed in the project root rather than the ${builder.config.files.assets} directory` ); } - if (existsSync(`${builder.config.kit.files.assets}/_redirects`)) { + if (fs.existsSync(`${builder.config.files.assets}/_redirects`)) { throw new Error( - `The _redirects file should be placed in the project root rather than the ${builder.config.kit.files.assets} directory` + `The _redirects file should be placed in the project root rather than the ${builder.config.files.assets} directory` ); } @@ -74,15 +80,17 @@ export default function (options = {}) { const files = fileURLToPath(new URL('./files', import.meta.url).href); const tmp = builder.getBuildDirectory('cloudflare-tmp'); - builder.rimraf(dest); - builder.rimraf(worker_dest); + fs.rmSync(dest, { force: true, recursive: true }); + fs.rmSync(worker_dest, { force: true, recursive: true }); + + fs.mkdirSync(dest, { recursive: true }); + fs.mkdirSync(tmp, { recursive: true }); - builder.mkdirp(dest); - builder.mkdirp(tmp); + replace_stub(builder.getServerDirectory(), stub_import); // client assets and prerendered pages - const assets_dest = `${dest}${builder.config.kit.paths.base}`; - builder.mkdirp(assets_dest); + const assets_dest = `${dest}${builder.config.paths.base}`; + fs.mkdirSync(assets_dest, { recursive: true }); if ( building_for_cloudflare_pages || wrangler_config.assets?.not_found_handling === '404-page' @@ -94,7 +102,7 @@ export default function (options = {}) { if (options.fallback === 'spa') { await builder.generateFallback(fallback); } else { - writeFileSync(fallback, 'Not Found'); + fs.writeFileSync(fallback, 'Not Found'); } } const client_assets = builder.writeClient(assets_dest); @@ -108,12 +116,7 @@ export default function (options = {}) { // worker const worker_dest_dir = path.dirname(worker_dest); - writeFileSync( - `${tmp}/manifest.js`, - `export const manifest = ${builder.generateManifest({ relativePath: path.posix.relative(tmp, builder.getServerDirectory()) })};\n\n` + - `export const prerendered = new Set(${JSON.stringify(builder.prerendered.paths)});\n\n` + - `export const base_path = ${JSON.stringify(builder.config.kit.paths.base)};\n` - ); + builder.generateServerInstance(`${tmp}/server.js`); builder.copy(`${files}/worker.js`, worker_dest, { replace: { // the paths returned by the Wrangler config might be Windows paths, @@ -121,34 +124,44 @@ export default function (options = {}) { // will be interpreted as escape characters and create an incorrect import path. // We also need to ensure the relative imports start with ./ since Wrangler // errors if a relative import looks like a package import - SERVER: `./${posixify(path.relative(worker_dest_dir, builder.getServerDirectory()))}/index.js`, - MANIFEST: `./${posixify(path.relative(worker_dest_dir, tmp))}/manifest.js`, - ASSETS: assets_binding + SERVER: `./${posixify(path.relative(worker_dest_dir, tmp))}/server.js`, + BASE_PATH: JSON.stringify(builder.config.paths.base), + APP_PATH: JSON.stringify(builder.getAppPath()), + MANIFEST_ASSETS: `new Set(${JSON.stringify(builder.manifest.assets.map((a) => a.path))})`, + PRERENDERED: `new Set(${JSON.stringify(builder.prerendered.paths)})`, + ASSETS_BINDING: assets_binding } }); - if (builder.hasServerInstrumentationFile?.()) { - builder.instrument?.({ + if (builder.hasServerInstrumentationFile()) { + const initializer = builder.createInstrumentationInitializer({ + outputDirectory: worker_dest_dir, + environment: `import { env } from 'cloudflare:workers';\nexport default env;\n` + }); + builder.instrument({ entrypoint: worker_dest, - instrumentation: `${builder.getServerDirectory()}/instrumentation.server.js` + instrumentation: `${builder.getServerDirectory()}/instrumentation.server.js`, + initializer }); } // _headers const headers_src = '_headers'; const headers_dest = `${dest}/_headers`; - if (existsSync(headers_src)) { - copyFileSync(headers_src, headers_dest); + /** @type {string | undefined} */ + let headers; + if (fs.existsSync(headers_src)) { + headers = fs.readFileSync(headers_src, 'utf-8'); } - writeFileSync(headers_dest, generate_headers(builder.getAppPath()), { flag: 'a' }); + fs.writeFileSync(headers_dest, generate_headers(builder.getAppPath(), headers)); // _redirects const redirects_src = '_redirects'; const redirects_dest = `${dest}/_redirects`; - if (existsSync(redirects_src)) { - copyFileSync(redirects_src, redirects_dest); + if (fs.existsSync(redirects_src)) { + fs.copyFileSync(redirects_src, redirects_dest); } if (builder.prerendered.redirects.size > 0) { - writeFileSync(redirects_dest, generate_redirects(builder.prerendered.redirects), { + fs.writeFileSync(redirects_dest, generate_redirects(builder.prerendered.redirects), { flag: 'a' }); } @@ -161,12 +174,12 @@ export default function (options = {}) { // Worker but instead let the rules in the `_redirects` file take over. /** @type {string[]} */ let redirects = []; - if (existsSync(redirects_dest)) { - const redirect_rules = readFileSync(redirects_dest, 'utf8'); + if (fs.existsSync(redirects_dest)) { + const redirect_rules = fs.readFileSync(redirects_dest, 'utf8'); redirects = parse_redirects(redirect_rules); } - writeFileSync( + fs.writeFileSync( `${dest}/_routes.json`, JSON.stringify( get_routes_json(builder, client_assets, redirects, options.routes ?? {}), @@ -175,74 +188,93 @@ export default function (options = {}) { ) ); } else { - writeFileSync(`${dest}/.assetsignore`, generate_assetsignore(), { flag: 'a' }); + fs.writeFileSync(`${dest}/.assetsignore`, generate_assetsignore(), { flag: 'a' }); } }, - 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 - const get_emulated = async () => { - const proxy = await getPlatformProxy(options.platformProxy); - const platform = { - env: proxy.env, - ctx: proxy.ctx, - context: proxy.ctx, // deprecated in favor of ctx - caches: proxy.caches, - cf: proxy.cf - }; - /** @type {Record} */ - const env = {}; - const prerender_platform = /** @type {App.Platform} */ (/** @type {unknown} */ ({ env })); - for (const key in proxy.env) { - Object.defineProperty(env, key, { - get: () => { - throw new Error(`Cannot access platform.env.${key} in a prerenderable route`); - } - }); - } - return { platform, prerender_platform }; - }; - - let emulated; - - return { - platform: async ({ prerender }) => { - emulated ??= await get_emulated(); - return prerender ? emulated.prerender_platform : emulated.platform; - } - }; - }, supports: { - read: ({ route }) => { - // TODO bump peer dep in next adapter major to simplify this - if (kit_major === '2' && kit_minor < '25') { - throw new Error( - `${name}: Cannot use \`read\` from \`$app/server\` in route \`${route.id}\` when using SvelteKit < 2.25.0` - ); - } - - return true; - }, + read: () => true, instrumentation: () => true + }, + vite: { + getRequest(options) { + const request = getRequest(options); + /** @type {import('@cloudflare/workers-types').Request} */ ( + /** @type {unknown} */ (request) + ).cf = globalThis.__sveltekit_cloudflare_platform?.cf; + return request; + }, + plugins: [ + virtual_workers_module( + { + configPath: options.config, + ...options.platformProxy + }, + stub_import + ) + ] + } + }; +} + +/** + * @param {GetPlatformProxyOptions} options + * @param {string} stub_import + * @returns {Plugin} + */ +function virtual_workers_module(options, stub_import) { + const setup = async () => { + if (globalThis.__sveltekit_cloudflare_platform) return; + const proxy = await getPlatformProxy(options); + // We store the platform proxy on globalThis so that our virtual workers module + // can access the same instance that we use here to populate `caches` and `cf` (above). + globalThis.__sveltekit_cloudflare_platform = proxy; + /** @type {any} */ (globalThis).caches = proxy.caches; + }; + const dispose = async () => { + const proxy = globalThis.__sveltekit_cloudflare_platform; + globalThis.__sveltekit_cloudflare_platform = undefined; + await proxy?.dispose(); + }; + return { + name: 'vite-plugin-sveltekit-adapter-cloudflare-virtual-workers-module', + configureServer: setup, + configurePreviewServer: setup, + closeServer({ reason }) { + // a restarting server is created before the old one closes, so it inherits the proxy + if (reason === 'close') return dispose(); + }, + closePreviewServer: dispose, + resolveId: { + filter: { id: exactRegex('cloudflare:workers') }, + handler() { + return { + id: stub_import, + external: true + }; + } } }; } /** * @param {string} app_dir + * @param {string | undefined} content existing `_headers` file content * @returns {string} */ -function generate_headers(app_dir) { - return ` -# === START AUTOGENERATED SVELTE IMMUTABLE HEADERS === -/${app_dir}/* - X-Robots-Tag: noindex - Cache-Control: no-cache -/${app_dir}/immutable/* - ! Cache-Control - Cache-Control: public, immutable, max-age=31536000 -# === END AUTOGENERATED SVELTE IMMUTABLE HEADERS === -`.trimEnd(); +function generate_headers(app_dir, content = '') { + content = append_headers( + `/${app_dir}/*`, + ['X-Robots-Tag: noindex', 'Cache-Control: no-cache'], + content + ); + + content = append_headers( + `/${app_dir}/immutable/*`, + ['! Cache-Control', 'Cache-Control: public, immutable, max-age=31536000'], + content + ); + + return content; } /** @@ -303,3 +335,24 @@ function validate_wrangler_config(config_file = undefined) { function posixify(str) { return str.replace(/\\/g, '/'); } + +/** + * + * @param {string} directory + * @param {string} stub_import + */ +function replace_stub(directory, stub_import) { + // recurse, find stub_import, replace with "cloudflare:workers" + const files = fs.readdirSync(directory); + for (const file of files) { + const file_path = path.join(directory, file); + if (fs.statSync(file_path).isDirectory()) { + replace_stub(file_path, stub_import); + } else { + const contents = fs.readFileSync(file_path, 'utf8'); + if (contents.includes(stub_import)) { + fs.writeFileSync(file_path, contents.replaceAll(stub_import, 'cloudflare:workers')); + } + } + } +} diff --git a/packages/adapter-cloudflare/index.spec.js b/packages/adapter-cloudflare/index.spec.js new file mode 100644 index 000000000000..a3042c517fc6 --- /dev/null +++ b/packages/adapter-cloudflare/index.spec.js @@ -0,0 +1,29 @@ +import { expect, test, vi } from 'vitest'; +import { createServer } from 'vite'; +import adapter from './index.js'; + +const dispose = vi.fn(() => Promise.resolve()); + +vi.mock('wrangler', () => ({ + getPlatformProxy: vi.fn(() => Promise.resolve({ caches: {}, dispose })), + unstable_readConfig: vi.fn() +})); + +test('disposes the platform proxy when the dev server closes, not when it restarts', async () => { + const { plugins } = /** @type {{ plugins: import('vite').Plugin[] }} */ (adapter().vite); + const server = await createServer({ + configFile: false, + logLevel: 'silent', + plugins, + server: { middlewareMode: true } + }); + expect(globalThis.__sveltekit_cloudflare_platform).toBeDefined(); + + await server.restart(); + expect(dispose).not.toHaveBeenCalled(); + expect(globalThis.__sveltekit_cloudflare_platform).toBeDefined(); + + await server.close(); + expect(dispose).toHaveBeenCalledOnce(); + expect(globalThis.__sveltekit_cloudflare_platform).toBeUndefined(); +}); diff --git a/packages/adapter-cloudflare/internal.d.ts b/packages/adapter-cloudflare/internal.d.ts deleted file mode 100644 index 848182720e5d..000000000000 --- a/packages/adapter-cloudflare/internal.d.ts +++ /dev/null @@ -1,41 +0,0 @@ -declare module 'SERVER' { - export { Server } from '@sveltejs/kit'; -} - -declare module 'MANIFEST' { - import { SSRManifest } from '@sveltejs/kit'; - - export const manifest: SSRManifest; - export const prerendered: Set; - export const app_path: string; - export const base_path: string; -} - -type PartialExcept = Partial> & Required>; - -/** - * We use a custom `Builder` type here to ensure compatibility with the minimum version of SvelteKit. - */ -type Builder2_0_0 = PartialExcept< - import('@sveltejs/kit').Builder, - | 'log' - | 'rimraf' - | 'mkdirp' - | 'config' - | 'prerendered' - | 'routes' - | 'createEntries' - | 'generateFallback' - | 'generateEnvModule' - | 'generateManifest' - | 'getBuildDirectory' - | 'getClientDirectory' - | 'getServerDirectory' - | 'getAppPath' - | 'writeClient' - | 'writePrerendered' - | 'writePrerendered' - | 'writeServer' - | 'copy' - | 'compress' ->; diff --git a/packages/adapter-cloudflare/package.json b/packages/adapter-cloudflare/package.json index ef8c0fda45c1..c7e642a3932d 100644 --- a/packages/adapter-cloudflare/package.json +++ b/packages/adapter-cloudflare/package.json @@ -1,6 +1,6 @@ { "name": "@sveltejs/adapter-cloudflare", - "version": "7.2.9", + "version": "8.0.0-next.8", "description": "Adapts SvelteKit applications for Cloudflare Workers or Cloudflare Pages", "keywords": [ "adapter", @@ -27,36 +27,35 @@ }, "types": "index.d.ts", "files": [ - "files", + "src/virtual-cloudflare-workers.js", + "files/worker.js", "index.js", "utils.js", - "index.d.ts", - "ambient.d.ts" + "index.d.ts" ], "scripts": { - "build": "esbuild src/worker.js --bundle --outfile=files/worker.js --external:SERVER --external:MANIFEST --external:cloudflare:workers --format=esm", - "lint": "prettier --check .", - "format": "pnpm lint --write", - "check": "tsc --skipLibCheck", + "lint": "oxfmt --check .", + "format": "oxfmt --write .", + "check": "pnpm tsc-native && pnpm tsc-native --project files/tsconfig.json", "test:unit": "vitest run", - "test:e2e": "pnpm build && pnpm -r --workspace-concurrency 1 --filter=\"./test/**\" test", + "test:e2e": "pnpm -r --workspace-concurrency 1 --filter=\"./test/**\" test", "test": "pnpm test:unit && pnpm test:e2e", - "prepublishOnly": "pnpm build" + "tsc-native": "node node_modules/@typescript/native/bin/tsc" }, "dependencies": { - "@cloudflare/workers-types": "^4.20250507.0", - "worktop": "0.8.0-next.18" + "@cloudflare/workers-types": "^5.20260813.1", + "@rolldown/pluginutils": "^1.0.1" }, "devDependencies": { "@playwright/test": "catalog:", "@sveltejs/kit": "workspace:^", "@types/node": "catalog:", - "esbuild": "catalog:", - "typescript": "catalog:", + "@typescript/native": "catalog:", + "vite": "catalog:", "vitest": "catalog:" }, "peerDependencies": { - "@sveltejs/kit": "^2.0.0", - "wrangler": "^4.0.0" + "@sveltejs/kit": "^3.0.0-next.0", + "wrangler": "^4.118.0" } } diff --git a/packages/adapter-cloudflare/src/virtual-cloudflare-workers.js b/packages/adapter-cloudflare/src/virtual-cloudflare-workers.js new file mode 100644 index 000000000000..c3c86afe4c36 --- /dev/null +++ b/packages/adapter-cloudflare/src/virtual-cloudflare-workers.js @@ -0,0 +1,157 @@ +/** @import { CloudflareWorkersModule } from '@cloudflare/workers-types/experimental/index.js'; */ +import { AsyncLocalStorage } from 'node:async_hooks'; + +const als = new AsyncLocalStorage(); + +const proxy = globalThis.__sveltekit_cloudflare_platform; + +function get_current_env() { + return als.getStore() ?? proxy?.env; +} + +/** @typedef {typeof CloudflareWorkersModule} Module */ + +export const env = new Proxy( + {}, + { + get(_, prop) { + if (!proxy) { + throw new Error(`Cannot access cloudflare:workers in a prerenderable route`); + } + const inner = get_current_env(); + if (inner) { + return Reflect.get(inner, prop); + } + return undefined; + }, + + set(_, prop, newValue) { + if (!proxy) { + throw new Error(`Cannot access cloudflare:workers in a prerenderable route`); + } + const inner = get_current_env(); + if (inner) { + return Reflect.set(inner, prop, newValue); + } + return true; + }, + + has(_, prop) { + if (!proxy) { + throw new Error(`Cannot access cloudflare:workers in a prerenderable route`); + } + const inner = get_current_env(); + if (inner) { + return Reflect.has(inner, prop); + } + return false; + }, + + ownKeys(_) { + if (!proxy) { + throw new Error(`Cannot access cloudflare:workers in a prerenderable route`); + } + const inner = get_current_env(); + if (inner) { + return Reflect.ownKeys(inner); + } + return []; + }, + + deleteProperty(_, prop) { + if (!proxy) { + throw new Error(`Cannot access cloudflare:workers in a prerenderable route`); + } + const inner = get_current_env(); + if (inner) { + return Reflect.deleteProperty(inner, prop); + } + return true; + }, + + defineProperty(_, prop, attr) { + if (!proxy) { + throw new Error(`Cannot access cloudflare:workers in a prerenderable route`); + } + const inner = get_current_env(); + if (inner) { + return Reflect.defineProperty(inner, prop, attr); + } + return true; + }, + + getOwnPropertyDescriptor(_, prop) { + if (!proxy) { + throw new Error(`Cannot access cloudflare:workers in a prerenderable route`); + } + const inner = get_current_env(); + if (inner) { + return Reflect.getOwnPropertyDescriptor(inner, prop); + } + return undefined; + } + } +); + +/** @type {Module['withEnv']} */ +export function withEnv(newEnv, fn) { + if (!proxy) { + throw new Error(`Cannot access cloudflare:workers in a prerenderable route`); + } + return als.run(newEnv, fn); +} +/** @type {Module['withEnvAndExports']} */ +export function withEnvAndExports(newEnv, _, fn) { + if (!proxy) { + throw new Error(`Cannot access cloudflare:workers in a prerenderable route`); + } + return als.run(newEnv, fn); +} + +// no-ops +export const exports = new Proxy( + {}, + { + get() { + throw new Error('exports is not available in dev mode'); + }, + has() { + throw new Error('exports is not available in dev mode'); + } + } +); +/** @type {Module['waitUntil']} */ +export function waitUntil() {} +/** @type {Module['cache']} */ +export const cache = { + purge() { + return Promise.resolve({ success: true, errors: [] }); + } +}; +class Span { + get isTraced() { + return false; + } + setAttribute() { + return this; + } + setAttributes() { + return this; + } + end() {} + recordException() {} +} +/** @type {Module['tracing']} */ +export const tracing = { + enterSpan(_, callback, ...args) { + return callback(new Span(), ...args); + }, + startActiveSpan(_, callback, ...args) { + return callback(new Span(), ...args); + }, + startSpan(_) { + return new Span(); + }, + getActiveSpan() {}, + Span +}; diff --git a/packages/adapter-cloudflare/src/worker.js b/packages/adapter-cloudflare/src/worker.js deleted file mode 100644 index bcee34b882e2..000000000000 --- a/packages/adapter-cloudflare/src/worker.js +++ /dev/null @@ -1,120 +0,0 @@ -import { Server } from 'SERVER'; -import { manifest, prerendered, base_path } from 'MANIFEST'; -import { env } from 'cloudflare:workers'; -import * as Cache from 'worktop/cfw.cache'; - -const server = new Server(manifest); - -const app_path = `/${manifest.appPath}`; - -const immutable = `${app_path}/immutable/`; -const version_file = `${app_path}/version.json`; - -/** - * We don't know the origin until we receive a request, but - * that's guaranteed to happen before we call `read` - * @type {string} - */ -let origin; - -const initialized = server.init({ - // @ts-expect-error env contains environment variables and bindings - env, - read: async (file) => { - const url = `${origin}/${file}`; - const response = await /** @type {{ ASSETS: { fetch: typeof fetch } }} */ (env).ASSETS.fetch( - url - ); - - if (!response.ok) { - throw new Error( - `read(...) failed: could not fetch ${url} (${response.status} ${response.statusText})` - ); - } - - return response.body; - } -}); - -export default { - /** - * @param {Request} req - * @param {{ ASSETS: { fetch: typeof fetch } }} env - * @param {ExecutionContext} ctx - * @returns {Promise} - */ - async fetch(req, env, ctx) { - if (!origin) { - origin = new URL(req.url).origin; - } - - // always await initialization to prevent race condition with concurrent requests - await initialized; - - // skip cache if "cache-control: no-cache" in request - let pragma = req.headers.get('cache-control') || ''; - let res = !pragma.includes('no-cache') && (await Cache.lookup(req)); - if (res) return res; - - let { pathname, search } = new URL(req.url); - try { - pathname = decodeURIComponent(pathname); - } catch { - // ignore invalid URI - } - - const stripped_pathname = pathname.replace(/\/$/, ''); - - // files in /static, the service worker, and Vite imported server assets - let is_static_asset = false; - const filename = stripped_pathname.slice(base_path.length + 1); - if (filename) { - is_static_asset = - manifest.assets.has(filename) || - manifest.assets.has(filename + '/index.html') || - filename in manifest._.server_assets || - filename + '/index.html' in manifest._.server_assets; - } - - let location = pathname.at(-1) === '/' ? stripped_pathname : pathname + '/'; - - if ( - is_static_asset || - prerendered.has(pathname) || - pathname === version_file || - pathname.startsWith(immutable) - ) { - res = await env.ASSETS.fetch(req); - } else if (location && prerendered.has(location)) { - // trailing slash redirect for prerendered pages - if (search) location += search; - res = new Response('', { - status: 308, - headers: { - location - } - }); - } else { - // dynamically-generated pages - res = await server.respond(req, { - platform: { - env, - ctx, - context: ctx, // deprecated in favor of ctx - // @ts-expect-error webworker types from worktop are not compatible with Cloudflare Workers types - caches, - // @ts-expect-error the type is correct but ts is confused because platform.cf uses the type from index.ts while req.cf uses the type from index.d.ts - cf: req.cf - }, - getClientAddress() { - return /** @type {string} */ (req.headers.get('cf-connecting-ip')); - } - }); - } - - // write to `Cache` only if response is not an error, - // let `Cache.save` handle the Cache-Control and Vary headers - pragma = res.headers.get('cache-control') || ''; - return pragma && res.status < 400 ? Cache.save(req, res, ctx) : res; - } -}; diff --git a/packages/adapter-cloudflare/test/apps/pages/package.json b/packages/adapter-cloudflare/test/apps/pages/package.json index 0839ddd0a69f..3e758b7a59e1 100644 --- a/packages/adapter-cloudflare/test/apps/pages/package.json +++ b/packages/adapter-cloudflare/test/apps/pages/package.json @@ -5,8 +5,7 @@ "scripts": { "dev": "vite dev", "build": "vite build", - "preview": "wrangler pages dev .svelte-kit/cloudflare --port 8787", - "prepare": "svelte-kit sync || echo ''", + "preview": "wrangler pages dev .svelte-kit/cloudflare --port 8787 --compatibility-date 2026-06-24", "test": "playwright test" }, "devDependencies": { diff --git a/packages/adapter-cloudflare/test/apps/pages/svelte.config.js b/packages/adapter-cloudflare/test/apps/pages/svelte.config.js deleted file mode 100644 index 20cd2b3ff5b8..000000000000 --- a/packages/adapter-cloudflare/test/apps/pages/svelte.config.js +++ /dev/null @@ -1,10 +0,0 @@ -import adapter from '../../../index.js'; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - kit: { - adapter: adapter() - } -}; - -export default config; diff --git a/packages/adapter-cloudflare/test/apps/pages/tsconfig.json b/packages/adapter-cloudflare/test/apps/pages/tsconfig.json index c2db702f9bb1..0c2092500732 100644 --- a/packages/adapter-cloudflare/test/apps/pages/tsconfig.json +++ b/packages/adapter-cloudflare/test/apps/pages/tsconfig.json @@ -9,5 +9,5 @@ "sourceMap": true, "moduleResolution": "bundler" }, - "extends": "./.svelte-kit/tsconfig.json" + "extends": "$app/tsconfig" } diff --git a/packages/adapter-cloudflare/test/apps/pages/vite.config.js b/packages/adapter-cloudflare/test/apps/pages/vite.config.js index 29ad08debe6a..01b9947e7d2a 100644 --- a/packages/adapter-cloudflare/test/apps/pages/vite.config.js +++ b/packages/adapter-cloudflare/test/apps/pages/vite.config.js @@ -1,11 +1,12 @@ import { sveltekit } from '@sveltejs/kit/vite'; +import adapter from '../../../index.js'; /** @type {import('vite').UserConfig} */ const config = { build: { minify: false }, - plugins: [sveltekit()] + plugins: [sveltekit({ adapter: adapter() })] }; export default config; diff --git a/packages/adapter-cloudflare/test/apps/workers/.gitignore b/packages/adapter-cloudflare/test/apps/workers/.gitignore index f9ee23cc8621..63d9a515f164 100644 --- a/packages/adapter-cloudflare/test/apps/workers/.gitignore +++ b/packages/adapter-cloudflare/test/apps/workers/.gitignore @@ -1,5 +1,8 @@ .DS_Store node_modules /.svelte-kit -/.wrangler -/dist \ No newline at end of file + +# Cloudflare +.wrangler +/dist +worker-configuration.d.ts diff --git a/packages/adapter-cloudflare/test/apps/workers/config/wrangler.jsonc b/packages/adapter-cloudflare/test/apps/workers/config/wrangler.jsonc index 0bf8a9db8c3a..372a381e9b61 100644 --- a/packages/adapter-cloudflare/test/apps/workers/config/wrangler.jsonc +++ b/packages/adapter-cloudflare/test/apps/workers/config/wrangler.jsonc @@ -3,8 +3,12 @@ { "$schema": "../node_modules/wrangler/config-schema.json", "main": "../dist/index.js", + "compatibility_date": "2026-06-24", "assets": { "directory": "../dist/public", "binding": "ASSETS" + }, + "vars": { + "FOO": "from wrangler.jsonc" } } diff --git a/packages/adapter-cloudflare/test/apps/workers/package.json b/packages/adapter-cloudflare/test/apps/workers/package.json index 4032805d1ec6..816115baeaf1 100644 --- a/packages/adapter-cloudflare/test/apps/workers/package.json +++ b/packages/adapter-cloudflare/test/apps/workers/package.json @@ -5,8 +5,8 @@ "scripts": { "dev": "vite dev", "build": "vite build", - "preview": "wrangler dev dist/index.js", - "prepare": "svelte-kit sync || echo ''", + "preview": "wrangler dev dist/index.js --config config/wrangler.jsonc", + "generate:types": "wrangler types --config config/wrangler.jsonc", "test:dev": "DEV=true playwright test", "test:build": "playwright test", "test": "pnpm test:dev && pnpm test:build" diff --git a/packages/adapter-cloudflare/test/apps/workers/src/app.d.ts b/packages/adapter-cloudflare/test/apps/workers/src/app.d.ts deleted file mode 100644 index 101a9db0b8bf..000000000000 --- a/packages/adapter-cloudflare/test/apps/workers/src/app.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// TODO: remove this in 3.0 once svelte.config.js is included by the generated tsconfig.json -// this ensures we get the ambient types from the adapter -import '../../../../index.js'; diff --git a/packages/adapter-cloudflare/test/apps/workers/src/routes/cf/+server.js b/packages/adapter-cloudflare/test/apps/workers/src/routes/cf/+server.js new file mode 100644 index 000000000000..677e42171f0e --- /dev/null +++ b/packages/adapter-cloudflare/test/apps/workers/src/routes/cf/+server.js @@ -0,0 +1,3 @@ +export function GET({ request }) { + return Response.json(request.cf); +} diff --git a/packages/adapter-cloudflare/test/apps/workers/src/routes/ctx/+server.js b/packages/adapter-cloudflare/test/apps/workers/src/routes/ctx/+server.js deleted file mode 100644 index e26ae2cbbd71..000000000000 --- a/packages/adapter-cloudflare/test/apps/workers/src/routes/ctx/+server.js +++ /dev/null @@ -1,3 +0,0 @@ -export function GET({ platform }) { - return new Response(platform?.ctx.waitUntil ? 'ctx works' : 'ctx does not work'); -} diff --git a/packages/adapter-cloudflare/test/apps/workers/src/routes/env/+server.js b/packages/adapter-cloudflare/test/apps/workers/src/routes/env/+server.js new file mode 100644 index 000000000000..b21afacc72d4 --- /dev/null +++ b/packages/adapter-cloudflare/test/apps/workers/src/routes/env/+server.js @@ -0,0 +1,5 @@ +import { env } from 'cloudflare:workers'; + +export function GET() { + return new Response(env.FOO); +} diff --git a/packages/adapter-cloudflare/test/apps/workers/src/routes/prerender/+server.js b/packages/adapter-cloudflare/test/apps/workers/src/routes/prerender/+server.js new file mode 100644 index 000000000000..0bc773d04e72 --- /dev/null +++ b/packages/adapter-cloudflare/test/apps/workers/src/routes/prerender/+server.js @@ -0,0 +1,12 @@ +import { env } from 'cloudflare:workers'; + +export function GET() { + try { + env.FOO; + return new Response('no error'); + } catch (e) { + return new Response(String(e)); + } +} + +export const prerender = true; diff --git a/packages/adapter-cloudflare/test/apps/workers/svelte.config.js b/packages/adapter-cloudflare/test/apps/workers/svelte.config.js deleted file mode 100644 index 26cd6a965908..000000000000 --- a/packages/adapter-cloudflare/test/apps/workers/svelte.config.js +++ /dev/null @@ -1,12 +0,0 @@ -import adapter from '../../../index.js'; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - kit: { - adapter: adapter({ - config: 'config/wrangler.jsonc' - }) - } -}; - -export default config; diff --git a/packages/adapter-cloudflare/test/apps/workers/test/test.js b/packages/adapter-cloudflare/test/apps/workers/test/test.js index 40072953a16f..ed75be3c7df5 100644 --- a/packages/adapter-cloudflare/test/apps/workers/test/test.js +++ b/packages/adapter-cloudflare/test/apps/workers/test/test.js @@ -1,22 +1,34 @@ import fs from 'node:fs'; import path from 'node:path'; -import { fileURLToPath } from 'node:url'; import { expect, test } from '@playwright/test'; -const __dirname = path.dirname(fileURLToPath(import.meta.url)); - test('worker', async ({ page }) => { await page.goto('/'); await expect(page.locator('h1')).toContainText('Sum: 3'); }); -test('ctx', async ({ request }) => { - const res = await request.get('/ctx'); - expect(await res.text()).toBe('ctx works'); +test('cloudflare:workers', async ({ request }) => { + const res = await request.get('/env'); + expect(await res.text()).toBe('from wrangler.jsonc'); +}); + +test('Request.cf', async ({ request }) => { + const res = await request.get('cf'); + const cf = await res.json(); + expect(cf.colo).toBeDefined(); }); test('read from $app/server works', async ({ request }) => { - const content = fs.readFileSync(path.resolve(__dirname, '../src/routes/read/file.txt'), 'utf-8'); + const content = fs.readFileSync( + path.resolve(import.meta.dirname, '../src/routes/read/file.txt'), + 'utf-8' + ); const response = await request.get('/read'); expect(await response.text()).toBe(content); }); + +test('prerendering throws', async ({ request }) => { + test.skip(!!process.env.DEV); + const res = await request.get('/prerender'); + expect(await res.text()).toContain('Cannot access cloudflare:workers in a prerenderable route'); +}); diff --git a/packages/adapter-cloudflare/test/apps/workers/tsconfig.json b/packages/adapter-cloudflare/test/apps/workers/tsconfig.json index c2db702f9bb1..0c2092500732 100644 --- a/packages/adapter-cloudflare/test/apps/workers/tsconfig.json +++ b/packages/adapter-cloudflare/test/apps/workers/tsconfig.json @@ -9,5 +9,5 @@ "sourceMap": true, "moduleResolution": "bundler" }, - "extends": "./.svelte-kit/tsconfig.json" + "extends": "$app/tsconfig" } diff --git a/packages/adapter-cloudflare/test/apps/workers/vite.config.js b/packages/adapter-cloudflare/test/apps/workers/vite.config.js index 29ad08debe6a..195f6c80776e 100644 --- a/packages/adapter-cloudflare/test/apps/workers/vite.config.js +++ b/packages/adapter-cloudflare/test/apps/workers/vite.config.js @@ -1,11 +1,18 @@ import { sveltekit } from '@sveltejs/kit/vite'; +import adapter from '../../../index.js'; /** @type {import('vite').UserConfig} */ const config = { build: { minify: false }, - plugins: [sveltekit()] + plugins: [ + sveltekit({ + adapter: adapter({ + config: 'config/wrangler.jsonc' + }) + }) + ] }; export default config; diff --git a/packages/adapter-cloudflare/test/utils.js b/packages/adapter-cloudflare/test/utils.js index dcb3ce52c9d6..c2ffc070bff8 100644 --- a/packages/adapter-cloudflare/test/utils.js +++ b/packages/adapter-cloudflare/test/utils.js @@ -21,7 +21,7 @@ export const config = { ...devices['Desktop Chrome'], screenshot: 'only-on-failure', trace: 'retain-on-failure', - channel: 'chromium' + channel: 'chrome' }, workers: process.env.CI ? 2 : number_from_env('KIT_E2E_WORKERS', undefined), reporter: 'list', diff --git a/packages/adapter-cloudflare/tsconfig.json b/packages/adapter-cloudflare/tsconfig.json index d1fdac44e7b3..e9e10322ab67 100644 --- a/packages/adapter-cloudflare/tsconfig.json +++ b/packages/adapter-cloudflare/tsconfig.json @@ -3,22 +3,26 @@ "allowJs": true, "checkJs": true, "noEmit": true, - "target": "es2022", - "module": "node16", - "moduleResolution": "node16", + "module": "nodenext", + "moduleResolution": "nodenext", "paths": { "@sveltejs/kit": ["../kit/types/index"] }, + // playwright and worker types are a mess + "skipLibCheck": true, // taken from the Cloudflare Workers TypeScript template https://github.com/cloudflare/workers-sdk/blob/main/packages/create-cloudflare/templates/hello-world/ts/tsconfig.json - "lib": ["es2021"], - "types": ["node", "@cloudflare/workers-types"] + "target": "es2024", + "lib": ["es2024"], + "types": ["node"] }, "include": [ "index.js", + "index.spec.js", + "src", "utils.js", "utils.spec.js", - "test/utils.js", - "internal.d.ts", - "src/worker.js" + "vitest.config.js", + "globals.d.ts", + "test/utils.js" ] } diff --git a/packages/adapter-cloudflare/utils.js b/packages/adapter-cloudflare/utils.js index 28d83691b12a..82f5683c6e07 100644 --- a/packages/adapter-cloudflare/utils.js +++ b/packages/adapter-cloudflare/utils.js @@ -9,7 +9,7 @@ export function is_building_for_cloudflare_pages(wrangler_config) { return true; } - if (wrangler_config.main || wrangler_config.assets) { + if (!!process.env.WORKERS_CI || wrangler_config.main || wrangler_config.assets) { return false; } @@ -81,7 +81,7 @@ export function parse_redirects(file_contents) { /** * Generates the [_routes.json](https://developers.cloudflare.com/pages/functions/routing/#create-a-_routesjson-file) * file that dictates which routes invoke the Cloudflare Worker. - * @param {Builder2_0_0} builder + * @param {import('@sveltejs/kit').Builder} builder * @param {string[]} client_assets * @param {string[]} redirects * @param {import('./index.js').AdapterOptions['routes']} routes @@ -128,8 +128,8 @@ export function get_routes_json(builder, client_assets, redirects, routes) { if (rule === '') { for (const file of client_assets) { - if (file.startsWith(`${builder.config.kit.appDir}/`)) continue; - excluded_routes.add(`${builder.config.kit.paths.base}/${file}`); + if (file.startsWith(`${builder.config.appDir}/`)) continue; + excluded_routes.add(`${builder.config.paths.base}/${file}`); } continue; } @@ -163,3 +163,29 @@ export function get_routes_json(builder, client_assets, redirects, routes) { exclude }; } + +/** + * Adds header rules to the contents of a `_headers` file + * @param {string} url + * @param {string[]} rules + * @param {string} content + * @returns {string} + */ +export function append_headers(url, rules, content) { + const regex = new RegExp(`^(${url.replaceAll('*', '\\*')})$`, 'm'); + const formatted_headers = rules.map((rule) => ` ${rule}`).join('\n'); + + // if the URL already exists, just add header rules to it + if (regex.test(content)) { + return content.replace(regex, `$1\n${formatted_headers}`); + } + + // otherwise, we add the url and header rules + return ` +${content} +# === START AUTOGENERATED SVELTE IMMUTABLE HEADERS === +${url} +${formatted_headers} +# === END AUTOGENERATED SVELTE IMMUTABLE HEADERS === +`.trim(); +} diff --git a/packages/adapter-cloudflare/utils.spec.js b/packages/adapter-cloudflare/utils.spec.js index f6059a96609f..8b0d2af7ce04 100644 --- a/packages/adapter-cloudflare/utils.spec.js +++ b/packages/adapter-cloudflare/utils.spec.js @@ -3,7 +3,8 @@ import { is_building_for_cloudflare_pages, validate_worker_settings, get_routes_json, - parse_redirects + parse_redirects, + append_headers } from './utils.js'; describe('detects Cloudflare Pages project', () => { @@ -68,6 +69,15 @@ describe('detects Cloudflare Workers project', () => { ) ).toBe(false); }); + + test('WORKERS_CI environment variable', () => { + vi.stubEnv('WORKERS_CI', '1'); + const result = is_building_for_cloudflare_pages( + /** @type {import('wrangler').Unstable_Config} */ ({}) + ); + vi.unstubAllEnvs(); + expect(result).toBe(false); + }); }); describe('validates Wrangler config', () => { @@ -148,42 +158,82 @@ describe('validates Wrangler config', () => { }); }); -test('ignores comments in _redirects file', () => { - const redirects = parse_redirects( - ` -# This is a comment -/home301 / 301 - # Indented comment -/blog/* https://blog.my.domain/:splat +describe('_headers', () => { + test('adds URL and its headers', () => { + const content = append_headers( + '/_app/*', + ['X-Robots-Tag: noindex', 'Cache-Control: no-cache'], + '/custom\n X-Custom-Rule: value' + ); + expect(content).toEqual( + ` +/custom + X-Custom-Rule: value +# === START AUTOGENERATED SVELTE IMMUTABLE HEADERS === +/_app/* + X-Robots-Tag: noindex + Cache-Control: no-cache +# === END AUTOGENERATED SVELTE IMMUTABLE HEADERS === `.trim() - ); + ); + }); - expect(redirects).toEqual(['/home301', '/blog/*']); + test('adds headers to existing URLs', () => { + const content = append_headers( + '/_app/*', + ['X-Robots-Tag: noindex', 'Cache-Control: no-cache'], + '/_app/*\n X-Custom-Rule: value' + ); + expect(content).toEqual( + ` +/_app/* + X-Robots-Tag: noindex + Cache-Control: no-cache + X-Custom-Rule: value +`.trim() + ); + }); }); -test('parses _redirects file', () => { - const redirects = parse_redirects( - ` +describe('_redirects', () => { + test('parses sources', () => { + const redirects = parse_redirects( + ` /home301 / 301 /notrailing/ /nottrailing 301 /blog/* https://blog.my.domain/:splat `.trim() - ); + ); + + expect(redirects).toEqual(['/home301', '/notrailing/', '/blog/*']); + }); + + test('ignores comments while parsing', () => { + const redirects = parse_redirects( + ` +# This is a comment +/home301 / 301 + # Indented comment +/blog/* https://blog.my.domain/:splat +`.trim() + ); - expect(redirects).toEqual(['/home301', '/notrailing/', '/blog/*']); + expect(redirects).toEqual(['/home301', '/blog/*']); + }); }); -test('generates a _routes.json file', () => { - const routes = get_routes_json( - { - getAppPath: () => 'base-path/_app', - config: { - kit: { +describe('_routes.json', () => { + test('generates file contents', () => { + const routes = get_routes_json( + { + getAppPath: () => 'base-path/_app', + config: { appDir: '_app', paths: { base: '/base-path', assets: '', + origin: 'http://localhost', relative: true }, alias: {}, @@ -200,7 +250,6 @@ test('generates a _routes.json file', () => { server: 'src/hooks.server.js', universal: 'src/hooks.js' }, - lib: 'src/lib', params: 'src/params', routes: 'src/routes', serviceWorker: 'src/service-worker.js', @@ -217,53 +266,50 @@ test('generates a _routes.json file', () => { reportOnly: {} }, env: { - dir: '.', - publicPrefix: 'PUBLIC_', - privatePrefix: '' + dir: '.' }, outDir: '.svelte-kit' + }, + prerendered: { + paths: ['/base-path/prerendered'], + pages: new Map(), + assets: new Map(), + redirects: new Map() } }, - prerendered: { - paths: ['/base-path/prerendered'], - pages: new Map(), - assets: new Map(), - redirects: new Map() - } - }, - ['_app/immutable/this-should-not-be-excluded.js', 'robots.txt'], - ['/base-path/redirect'], - undefined - ); + ['_app/immutable/this-should-not-be-excluded.js', 'robots.txt'], + ['/base-path/redirect'], + undefined + ); - expect(routes).toEqual({ - version: 1, - description: 'Generated by @sveltejs/adapter-cloudflare', - include: ['/*'], - exclude: [ - '/base-path/_app/version.json', - '/base-path/_app/immutable/*', - '/base-path/robots.txt', - '/base-path/prerendered', - '/base-path/redirect' - ] + expect(routes).toEqual({ + version: 1, + description: 'Generated by @sveltejs/adapter-cloudflare', + include: ['/*'], + exclude: [ + '/base-path/_app/version.json', + '/base-path/_app/immutable/*', + '/base-path/robots.txt', + '/base-path/prerendered', + '/base-path/redirect' + ] + }); }); -}); -test('truncates excess _routes.json exclude rules', () => { - const routes = get_routes_json( - { - // @ts-ignore - log: { - warn: console.warn - }, - getAppPath: () => 'base-path/_app', - config: { - kit: { + test('truncates excess exclude rules', () => { + const routes = get_routes_json( + { + // @ts-ignore + log: { + warn: console.warn + }, + getAppPath: () => 'base-path/_app', + config: { appDir: '_app', paths: { base: '/base-path', assets: '', + origin: 'http://localhost', relative: true }, alias: {}, @@ -280,7 +326,6 @@ test('truncates excess _routes.json exclude rules', () => { server: 'src/hooks.server.js', universal: 'src/hooks.js' }, - lib: 'src/lib', params: 'src/params', routes: 'src/routes', serviceWorker: 'src/service-worker.js', @@ -297,33 +342,31 @@ test('truncates excess _routes.json exclude rules', () => { reportOnly: {} }, env: { - dir: '.', - publicPrefix: 'PUBLIC_', - privatePrefix: '' + dir: '.' }, outDir: '.svelte-kit' + }, + prerendered: { + paths: Array.from({ length: 100 }, (_, i) => `/base-path/blog/post/${i + 1}`), + pages: new Map(), + assets: new Map(), + redirects: new Map() } }, - prerendered: { - paths: Array.from({ length: 100 }, (_, i) => `/base-path/blog/post/${i + 1}`), - pages: new Map(), - assets: new Map(), - redirects: new Map() - } - }, - ['_app/immutable/this-should-not-be-excluded.js', 'robots.txt'], - [], - undefined - ); + ['_app/immutable/this-should-not-be-excluded.js', 'robots.txt'], + [], + undefined + ); - expect(routes).toEqual({ - version: 1, - description: 'Generated by @sveltejs/adapter-cloudflare', - include: ['/*'], - exclude: [ - '/base-path/_app/version.json', - '/base-path/_app/immutable/*', - '/base-path/robots.txt' - ].concat(Array.from({ length: 96 }, (_, i) => `/base-path/blog/post/${i + 1}`)) + expect(routes).toEqual({ + version: 1, + description: 'Generated by @sveltejs/adapter-cloudflare', + include: ['/*'], + exclude: [ + '/base-path/_app/version.json', + '/base-path/_app/immutable/*', + '/base-path/robots.txt' + ].concat(Array.from({ length: 96 }, (_, i) => `/base-path/blog/post/${i + 1}`)) + }); }); }); diff --git a/packages/adapter-netlify/.gitignore b/packages/adapter-netlify/.gitignore index 847a92e0c157..9daa8247da45 100644 --- a/packages/adapter-netlify/.gitignore +++ b/packages/adapter-netlify/.gitignore @@ -1,3 +1,2 @@ .DS_Store node_modules -/files diff --git a/packages/adapter-netlify/CHANGELOG.md b/packages/adapter-netlify/CHANGELOG.md index b88615a71b61..b6bf4f325c93 100644 --- a/packages/adapter-netlify/CHANGELOG.md +++ b/packages/adapter-netlify/CHANGELOG.md @@ -1,5 +1,176 @@ # @sveltejs/adapter-netlify +## 7.0.0-next.12 + +### Major Changes + +- breaking: write static files based on the `publish` adapter option rather than reading the `netlify.toml` file ([#17078](https://github.com/sveltejs/kit/pull/17078)) + +### Minor Changes + +- feat: support `split` and `edge` options combined ([#17045](https://github.com/sveltejs/kit/pull/17045)) + +### Patch Changes + +- chore: include edge function configuration in the function module ([#17058](https://github.com/sveltejs/kit/pull/17058)) +- Updated dependencies [[`abf47df`](https://github.com/sveltejs/kit/commit/abf47df887f914f09c39a7f771d32cdd55fb346d), [`df3f003`](https://github.com/sveltejs/kit/commit/df3f0034b541701bd3319225f4e6003579d4ebd6), [`3c044cd`](https://github.com/sveltejs/kit/commit/3c044cda5c356dede92167dcca549cd58e73e61f), [`509dd14`](https://github.com/sveltejs/kit/commit/509dd146696b2c77ae3ac8ac2435773864520016), [`5d8722c`](https://github.com/sveltejs/kit/commit/5d8722c60977efb6e4606fefd9750c2349af410a), [`462111a`](https://github.com/sveltejs/kit/commit/462111a1411551386c6e1aa6c14f967f4ae875c2), [`9f3342f`](https://github.com/sveltejs/kit/commit/9f3342fb9305d1fd731a49497bec894469073fd0), [`224e791`](https://github.com/sveltejs/kit/commit/224e791cb05f95a5c2d881e5a7d5341352f3c052), [`8614b01`](https://github.com/sveltejs/kit/commit/8614b016e3be4247cb1aae615870f4c23a0622b3), [`65f93c8`](https://github.com/sveltejs/kit/commit/65f93c83b29f47ebc05bfdeac53f8c92c8d522e1), [`147f09a`](https://github.com/sveltejs/kit/commit/147f09ab7397727f17b5f35ff7535eb1a089180f), [`09b8de8`](https://github.com/sveltejs/kit/commit/09b8de849561b5c81112dbe029e9279e8184903c), [`68c6ef9`](https://github.com/sveltejs/kit/commit/68c6ef97fad9baf6c883e203b1ceb686e2ca74a1), [`377fc5d`](https://github.com/sveltejs/kit/commit/377fc5dd93054ba01ec0e9623036735ff7aadfb5), [`84cb5fd`](https://github.com/sveltejs/kit/commit/84cb5fd2173028e29edf87bcbfc551f34dc2e1a9), [`99492ac`](https://github.com/sveltejs/kit/commit/99492ac605a74e066623d8934fa9f662d4350c99), [`4ac8c9b`](https://github.com/sveltejs/kit/commit/4ac8c9b4b9f015769f81adb941c85cf42fc17424), [`8efeeb3`](https://github.com/sveltejs/kit/commit/8efeeb3dea17c6dd3a1cdf55fc25ba0a53e6ee4c), [`4c812c8`](https://github.com/sveltejs/kit/commit/4c812c808c6b62c78b89fb0021dcfe45e06afcc7), [`fbbb4c7`](https://github.com/sveltejs/kit/commit/fbbb4c7d3c4ff3a3792a58db26c6be77996a392d), [`c6ba86b`](https://github.com/sveltejs/kit/commit/c6ba86ba7983a53f0d1e16c0a6462009d5245d4d), [`7847e4e`](https://github.com/sveltejs/kit/commit/7847e4e27f17a0a94b29f88d79050d704436957c), [`68791be`](https://github.com/sveltejs/kit/commit/68791be662e01afd7235e1eb6a2b6b7acf332b17), [`f8258ee`](https://github.com/sveltejs/kit/commit/f8258ee5e042d48d198cca3f35982e90db0077cb), [`4da6320`](https://github.com/sveltejs/kit/commit/4da6320db8d70b73b93e142e4bb6ba9173be3b97), [`b21766f`](https://github.com/sveltejs/kit/commit/b21766f362e39f51cb4ae8f894ea5aca40cf305e), [`a2bfcaf`](https://github.com/sveltejs/kit/commit/a2bfcafbe496e23b1e0bcfce442309bb94783efd), [`680405d`](https://github.com/sveltejs/kit/commit/680405d2d06f02774a8d7df5351df4a0d259c338), [`755024a`](https://github.com/sveltejs/kit/commit/755024a1af3ffcb80cdf939b2cf50ada3a2cb684), [`3a7d329`](https://github.com/sveltejs/kit/commit/3a7d3290e28dffa730d0c71699906dbc6d2fd584)]: + - @sveltejs/kit@3.0.0-next.28 + +## 7.0.0-next.11 + +### Major Changes + +- breaking: populate env vars before `instrumentation.server.js` is evaluated and update the adapter instrumentation API ([#16303](https://github.com/sveltejs/kit/pull/16303)) + +- breaking: replace the `builder.generateManifest` with `builder.generateServerInstance` and `builder.manifest` ([#16875](https://github.com/sveltejs/kit/pull/16875)) + +### Patch Changes + +- fix: route requests with omitted optional parameters to split serverless functions ([#17020](https://github.com/sveltejs/kit/pull/17020)) + +- fix: copy `_redirects` when deploying edge function ([#17047](https://github.com/sveltejs/kit/pull/17047)) + +- chore: replace `@iarna/toml` with `smol-toml` ([#16948](https://github.com/sveltejs/kit/pull/16948)) + +- chore: identify serverless functions with a UUID instead of Netlify's function token ([#17044](https://github.com/sveltejs/kit/pull/17044)) + +- fix: ensure serverless function files have unique names ([#17049](https://github.com/sveltejs/kit/pull/17049)) +- Updated dependencies [[`9a4b343`](https://github.com/sveltejs/kit/commit/9a4b343bd8ab0060dc624fbdd139155fc4a0a1dc), [`ee2f55b`](https://github.com/sveltejs/kit/commit/ee2f55b1e3338d2fa92a25fe53d095129edf1002), [`65673a4`](https://github.com/sveltejs/kit/commit/65673a4dd35054c16f8555eb896dfb773d86a556), [`1b79845`](https://github.com/sveltejs/kit/commit/1b79845e117f417b63fa399149816d73882c27ea), [`b12bb78`](https://github.com/sveltejs/kit/commit/b12bb789c17c30e7ff835de4af004d2f3eb36159), [`03f1687`](https://github.com/sveltejs/kit/commit/03f1687fe612ce3d2d9131139b5b188d9cf90c64), [`49f0808`](https://github.com/sveltejs/kit/commit/49f0808f3e983d0cb5a4d586cf0d1678467431ed), [`e7c74bf`](https://github.com/sveltejs/kit/commit/e7c74bf8227dc228cb36e822eaff5992569b830a), [`0876d63`](https://github.com/sveltejs/kit/commit/0876d633145088c7a5213e9f530cf2fed96ce35d), [`9a2b9ba`](https://github.com/sveltejs/kit/commit/9a2b9bacbb2de0533966bb029ef526b357aac086), [`c9f54b5`](https://github.com/sveltejs/kit/commit/c9f54b5d66acd9443d36b0ae9aa0a656809bd7f9), [`22b8152`](https://github.com/sveltejs/kit/commit/22b8152f74882775ac63e1cf96d3016b92ac8256), [`f6d44cf`](https://github.com/sveltejs/kit/commit/f6d44cf007d6c6b54de57f6ae4ef8e2e7fea2f44), [`3c3a27b`](https://github.com/sveltejs/kit/commit/3c3a27bf67a26164266e132dbb7f9266643cf724), [`7f376b1`](https://github.com/sveltejs/kit/commit/7f376b109cd770638806184cf5d8bf4cf88c5bf2), [`8a587c7`](https://github.com/sveltejs/kit/commit/8a587c7b3919408ec66a362eb249fe5cf43f5316), [`aa4f960`](https://github.com/sveltejs/kit/commit/aa4f9605761e686f6aaf414b9186c68ddd2468dc)]: + - @sveltejs/kit@3.0.0-next.27 + +## 7.0.0-next.10 + +### Patch Changes + +- fix: correctly apply `reroute` results for apps configured with split serverless functions ([#16665](https://github.com/sveltejs/kit/pull/16665)) +- Updated dependencies [[`9b3d195`](https://github.com/sveltejs/kit/commit/9b3d1955cb40042cbc411637dab3064ddfa6b1a5), [`385d378`](https://github.com/sveltejs/kit/commit/385d378dd1281e1b69417bf4980a169d67a49314), [`3782448`](https://github.com/sveltejs/kit/commit/37824483ca6ccf802942740f0da31b149d09e077), [`d0d3a33`](https://github.com/sveltejs/kit/commit/d0d3a33535cd2f3db1ed09680ccb34624b3abb77), [`4b7a483`](https://github.com/sveltejs/kit/commit/4b7a4830e962ee404161197b3b016182c376ed61), [`e325d7d`](https://github.com/sveltejs/kit/commit/e325d7dfa4313c889d3f9ebea9e70552722c427e), [`4f63c79`](https://github.com/sveltejs/kit/commit/4f63c799ab8dd9ae01eb301d7a9f5d712b724d23)]: + - @sveltejs/kit@3.0.0-next.25 + +## 7.0.0-next.9 + +### Patch Changes + +- fix: strip trailing slashes from prerendered paths in the edge function exclude list, so the root page is served statically when using a base path ([#16788](https://github.com/sveltejs/kit/pull/16788)) +- Updated dependencies [[`6d1e9d7`](https://github.com/sveltejs/kit/commit/6d1e9d70587641a8997f9fc806859370da75e845), [`289afbf`](https://github.com/sveltejs/kit/commit/289afbf3314f50d6160ca75bdf99ed8fbf88cc47), [`7f1db04`](https://github.com/sveltejs/kit/commit/7f1db0430b2ad771fff965990ec42d0fccf9528a), [`7570a62`](https://github.com/sveltejs/kit/commit/7570a62238ac24023157c635c2fbfd887bd6e860), [`6833df1`](https://github.com/sveltejs/kit/commit/6833df1bb799ec802cd2f16fc87b234b1533c003), [`d8efec6`](https://github.com/sveltejs/kit/commit/d8efec6d33c4a84bf9d2caade5896bf175a2f83a), [`fb0e21c`](https://github.com/sveltejs/kit/commit/fb0e21cf32a5b8546ee97ddf18a24a900bb71509), [`6e7d44d`](https://github.com/sveltejs/kit/commit/6e7d44d62434b0bf1e01e646f3bb3fcfc2c3715c), [`bd36e00`](https://github.com/sveltejs/kit/commit/bd36e0049ced1c47ec3be0754894af0aeeecf1bf), [`97ad63a`](https://github.com/sveltejs/kit/commit/97ad63a45c65614081de72ac089e92e8d572bfa2), [`17123bf`](https://github.com/sveltejs/kit/commit/17123bfac392e8941ece37278ee35ba347fe9219), [`2c179e9`](https://github.com/sveltejs/kit/commit/2c179e9b19e1f0c23c1374cea404363a7c12acf3), [`e54a37f`](https://github.com/sveltejs/kit/commit/e54a37f52a0546a6143ba604cfb05043e93dd28a), [`a7f32aa`](https://github.com/sveltejs/kit/commit/a7f32aafa90d6ba3ad16565b6296995544d5a347), [`9a910f3`](https://github.com/sveltejs/kit/commit/9a910f3f5297123b2808b74579d558376b78cf86), [`a610618`](https://github.com/sveltejs/kit/commit/a6106186090e72d2b6d11fae4004b95c7579e75d), [`d455628`](https://github.com/sveltejs/kit/commit/d4556285a0fda2655ca62b25be55003086597cd1), [`4682127`](https://github.com/sveltejs/kit/commit/46821277fe201b7fbe151d4343d59f149ca3a89d), [`e4876e9`](https://github.com/sveltejs/kit/commit/e4876e91ebba0007664bbf06e5a9570d0c2e8844), [`7f1db04`](https://github.com/sveltejs/kit/commit/7f1db0430b2ad771fff965990ec42d0fccf9528a), [`b361b81`](https://github.com/sveltejs/kit/commit/b361b815e79828a6b7404d759d0a4c3892814a4e), [`97ad63a`](https://github.com/sveltejs/kit/commit/97ad63a45c65614081de72ac089e92e8d572bfa2), [`ef70fc7`](https://github.com/sveltejs/kit/commit/ef70fc775ff89a8f86eb62cb5b7a5e01884af563), [`6bfccde`](https://github.com/sveltejs/kit/commit/6bfccdedeefc3df216b050001ae118cd2dbfdcc2), [`7d78680`](https://github.com/sveltejs/kit/commit/7d78680a721c3f337812bdbc2868fc326fde7419), [`b361b81`](https://github.com/sveltejs/kit/commit/b361b815e79828a6b7404d759d0a4c3892814a4e)]: + - @sveltejs/kit@3.0.0-next.24 + +## 7.0.0-next.8 + +### Patch Changes + +- chore: bump `rolldown` to 1.2.3 ([#16745](https://github.com/sveltejs/kit/pull/16745)) +- Updated dependencies [[`1742811`](https://github.com/sveltejs/kit/commit/1742811562358caf6042bed33c60ce6427a3e831), [`1611c61`](https://github.com/sveltejs/kit/commit/1611c617a1ec1b32e10627c2815fb7e6673f3157), [`b361b81`](https://github.com/sveltejs/kit/commit/b361b815e79828a6b7404d759d0a4c3892814a4e), [`b361b81`](https://github.com/sveltejs/kit/commit/b361b815e79828a6b7404d759d0a4c3892814a4e), [`b361b81`](https://github.com/sveltejs/kit/commit/b361b815e79828a6b7404d759d0a4c3892814a4e), [`b361b81`](https://github.com/sveltejs/kit/commit/b361b815e79828a6b7404d759d0a4c3892814a4e), [`13e7b18`](https://github.com/sveltejs/kit/commit/13e7b186a94b5b8317771d2b1e795b432dcd6faf), [`529346d`](https://github.com/sveltejs/kit/commit/529346d96369f92cdd804be9c4313c598efe34b8), [`b361b81`](https://github.com/sveltejs/kit/commit/b361b815e79828a6b7404d759d0a4c3892814a4e), [`81d6319`](https://github.com/sveltejs/kit/commit/81d6319eef08a765ccd3922790a7aee16e284cc2), [`b361b81`](https://github.com/sveltejs/kit/commit/b361b815e79828a6b7404d759d0a4c3892814a4e), [`69a5bdf`](https://github.com/sveltejs/kit/commit/69a5bdf9f45810eeed2faa089c8d3206463d347e)]: + - @sveltejs/kit@3.0.0-next.20 + +## 7.0.0-next.7 + +### Patch Changes + +- fix: import the generated manifest via `pathToFileURL`, which handles special characters in the project path ([#16649](https://github.com/sveltejs/kit/pull/16649)) +- Updated dependencies [[`c66e589`](https://github.com/sveltejs/kit/commit/c66e589d9ba2dba7577c2c4857c8989dd65c066d), [`0bee0e8`](https://github.com/sveltejs/kit/commit/0bee0e8f2bed771a92a3d6860363cda7c710839a), [`9703b94`](https://github.com/sveltejs/kit/commit/9703b94b51d71ffd5ebbce8229d35695db790f15)]: + - @sveltejs/kit@3.0.0-next.17 + +## 7.0.0-next.6 + +### Major Changes + +- chore: use `rolldown` for edge function bundling + +- breaking: write output that conforms to the stable [Netlify Frameworks API](https://docs.netlify.com/build/frameworks/frameworks-api/). + + Deploying and previewing with Netlify CLI now requires [v17.31.0](https://github.com/netlify/cli/releases/tag/v17.31.0) or later. Run `npm i -g netlify-cli@latest` to upgrade. +- breaking: require SvelteKit 3 + +- breaking: edge function build target is now `es2022` + +- breaking: require `vite@^8.0.12`, the first Vite 8 release bundling stable `rolldown` 1.0.0 + +### Patch Changes + +- chore: use `node:fs` instead of deprecated `builder.rimraf` and `builder.mkdirp` ([#16286](https://github.com/sveltejs/kit/pull/16286)) + +- fix: ensure types for `platform.context` work + +- fix: include `utils.js` in package.json `files` + +- chore: replace @netlify/functions with @netlify/types for the Context type + +- fix: provide distinct names for each split serverless function ([#16667](https://github.com/sveltejs/kit/pull/16667)) + +- fix: await `init` on every request to prevent race condition + +- chore: bump Rolldown to `1.2.0` +- Updated dependencies [[`bbd1e2f`](https://github.com/sveltejs/kit/commit/bbd1e2fcc9af58edb9846393b081be61df4789d5), [`e21d27b`](https://github.com/sveltejs/kit/commit/e21d27ba490c89da98dbad24fca364d0163137c4), [`6044d05`](https://github.com/sveltejs/kit/commit/6044d055031bc17deedc069dc4016565364bd4f4), [`7fb611f`](https://github.com/sveltejs/kit/commit/7fb611fb604e4d74cd889f5fe21ccd255f876382), [`9f06b3a`](https://github.com/sveltejs/kit/commit/9f06b3ac78be30d27b07fc00ea40bda493c573aa), [`8763327`](https://github.com/sveltejs/kit/commit/87633271ce40ac0839be9468598e965bde3cad51), [`7dc2379`](https://github.com/sveltejs/kit/commit/7dc23798cde97b5616049ba8d443a07e8c558bdc), [`a70c3eb`](https://github.com/sveltejs/kit/commit/a70c3eb31fe1e28f1532c84ec569ec7099da3344), [`89f49c7`](https://github.com/sveltejs/kit/commit/89f49c70b7fb4883cf704d2fb57d2d8595790ad2), [`f36b3a4`](https://github.com/sveltejs/kit/commit/f36b3a4cd307bd8c89e03fa191becc175e7b3077), [`1144a63`](https://github.com/sveltejs/kit/commit/1144a63d318c3c3255d06b374bb7a656a469fabf), [`9f06b3a`](https://github.com/sveltejs/kit/commit/9f06b3ac78be30d27b07fc00ea40bda493c573aa)]: + - @sveltejs/kit@3.0.0-next.15 + +## 7.0.0-next.5 + +### Patch Changes + +- chore: replace @netlify/functions with @netlify/types for the Context type ([#16542](https://github.com/sveltejs/kit/pull/16542)) +- Updated dependencies [[`6942ddb`](https://github.com/sveltejs/kit/commit/6942ddb68b43b5451337a8b420ce05a7a5de9919), [`68137d4`](https://github.com/sveltejs/kit/commit/68137d48cbf921264c6325ffa940165563c0b25a), [`15dc603`](https://github.com/sveltejs/kit/commit/15dc603f5027ee21b261c0c93f7decbf8f9498d8), [`a4f1521`](https://github.com/sveltejs/kit/commit/a4f15213c46f34aac88d05eb9af0905a5f776353), [`629b45d`](https://github.com/sveltejs/kit/commit/629b45d9e68846d862c10006fdabcc1526849811), [`7a7833a`](https://github.com/sveltejs/kit/commit/7a7833af90c0fa70c926cc84d61ca03f9db97bd2), [`e6591a0`](https://github.com/sveltejs/kit/commit/e6591a02743a2b324aa30cd463620450264fabae), [`7a7833a`](https://github.com/sveltejs/kit/commit/7a7833af90c0fa70c926cc84d61ca03f9db97bd2), [`b196a39`](https://github.com/sveltejs/kit/commit/b196a3965dd892a612cc6eae4868e450cc325736), [`66bf343`](https://github.com/sveltejs/kit/commit/66bf343453f8babf719193629a36f084d7bddf75), [`4bde76b`](https://github.com/sveltejs/kit/commit/4bde76b8a7ffa6885878d74eab3e542f1bda9c80), [`d9f05cb`](https://github.com/sveltejs/kit/commit/d9f05cb8efd8682bf91635f517f402849570cb20), [`40efca1`](https://github.com/sveltejs/kit/commit/40efca1b4afd6f0b6329bed652025b99edc8978c), [`8ef5548`](https://github.com/sveltejs/kit/commit/8ef5548f3e63ebb8371099a91acc182f7d094557), [`9efb8f2`](https://github.com/sveltejs/kit/commit/9efb8f2440328a536ebbb70a9e36ce4e30815e8a), [`6a670dc`](https://github.com/sveltejs/kit/commit/6a670dc778561e1fc504c2ccce864a46c2027dcf), [`3416dcc`](https://github.com/sveltejs/kit/commit/3416dccdceddf70ad229b5a1963d0735a7146c01), [`6850fa5`](https://github.com/sveltejs/kit/commit/6850fa56841afc8b1178fe16b6e61ffdfc854ab2), [`e6591a0`](https://github.com/sveltejs/kit/commit/e6591a02743a2b324aa30cd463620450264fabae), [`688bead`](https://github.com/sveltejs/kit/commit/688bead796f60db2384174e4f229c1d558a9ef00), [`b596610`](https://github.com/sveltejs/kit/commit/b596610a0c2e31e7d9adbdfb8d1354a165b5fe23), [`0f26c13`](https://github.com/sveltejs/kit/commit/0f26c133d3c5c19ed375f097b1eb27f6ce1928cd), [`110a3db`](https://github.com/sveltejs/kit/commit/110a3dbdba5f668d3e51b5e9f0d64be5a9ae5b39), [`665d467`](https://github.com/sveltejs/kit/commit/665d46720d26f545cf5ff52c86e44c9b2aa2521b), [`9c8c60c`](https://github.com/sveltejs/kit/commit/9c8c60c26c44e0705091de30a9a485b39d593def)]: + - @sveltejs/kit@3.0.0-next.13 + +## 7.0.0-next.4 + +### Patch Changes + +- fix: await `init` on every request to prevent race condition ([#16467](https://github.com/sveltejs/kit/pull/16467)) + +- chore: bump Rolldown to `1.2.0` ([#16455](https://github.com/sveltejs/kit/pull/16455)) +- Updated dependencies [[`adc4c5b`](https://github.com/sveltejs/kit/commit/adc4c5be542ecf7da93ee8d0e90e9d13b952998b), [`ecb0701`](https://github.com/sveltejs/kit/commit/ecb0701b8826e41229f84922f37c12099c66d3bc), [`4b4cc60`](https://github.com/sveltejs/kit/commit/4b4cc60990fa940236639a6cecb6c8efe4287bfa), [`7390dbe`](https://github.com/sveltejs/kit/commit/7390dbe6ef8dff0a1cd923f96daef462a3ce68f2), [`2e71340`](https://github.com/sveltejs/kit/commit/2e713407deb22965777afb01cb8215013ef8bfd3), [`c87bc3a`](https://github.com/sveltejs/kit/commit/c87bc3ac7830f8ee4e76254c9315adcc79ff6a99), [`af15c6c`](https://github.com/sveltejs/kit/commit/af15c6cab2fbfe6bae51dc1fa77a9271df996a47), [`c6fa431`](https://github.com/sveltejs/kit/commit/c6fa43157ae0dbaa9fc61b7e8d2db673e1f9f893), [`bfe4dea`](https://github.com/sveltejs/kit/commit/bfe4deae006f61347b5a5dce7747ec96692e97f9), [`df7dc72`](https://github.com/sveltejs/kit/commit/df7dc7219451bcc86a93e93188d78d664973d9ae), [`5ae11a1`](https://github.com/sveltejs/kit/commit/5ae11a119667c1b24e3b6d17f4b538032ddf1ab1), [`781205c`](https://github.com/sveltejs/kit/commit/781205c41cc5696eb862e5c13356c301a0368983), [`c87bc3a`](https://github.com/sveltejs/kit/commit/c87bc3ac7830f8ee4e76254c9315adcc79ff6a99), [`a1bfeb9`](https://github.com/sveltejs/kit/commit/a1bfeb9de906ae140b9cdc7d7eeaf0ee652a7bcc), [`c87bc3a`](https://github.com/sveltejs/kit/commit/c87bc3ac7830f8ee4e76254c9315adcc79ff6a99), [`9f0127d`](https://github.com/sveltejs/kit/commit/9f0127d4eee3f17dc1db1d2b99b1ca5fc80af784), [`4b4cc60`](https://github.com/sveltejs/kit/commit/4b4cc60990fa940236639a6cecb6c8efe4287bfa), [`8f5b9c7`](https://github.com/sveltejs/kit/commit/8f5b9c72ae930b3b93ef9e1b9ecb9e52664e1d74), [`c9b5544`](https://github.com/sveltejs/kit/commit/c9b554448633e1839e8c0f16c374b00a5ce4d1b6), [`fe1d4a4`](https://github.com/sveltejs/kit/commit/fe1d4a4d68d1d7a3925004fe804209ff54091e34), [`4b4cc60`](https://github.com/sveltejs/kit/commit/4b4cc60990fa940236639a6cecb6c8efe4287bfa), [`5f78e95`](https://github.com/sveltejs/kit/commit/5f78e953ae3966c23a01194dd200f5c57f723e40), [`25510fc`](https://github.com/sveltejs/kit/commit/25510fc79484363277d062e01f2b872f17a473f5), [`c99a6cf`](https://github.com/sveltejs/kit/commit/c99a6cfb7fec133aa85a6de59b43fc442aedc41a), [`17a45ca`](https://github.com/sveltejs/kit/commit/17a45cadb9356403b82c170f0d0027a1ba085667), [`2ca20c3`](https://github.com/sveltejs/kit/commit/2ca20c3ee7348836757dc6f3469217e46957766b), [`eb5c973`](https://github.com/sveltejs/kit/commit/eb5c9737b6d74c53f572b3e047faa5947052ced2)]: + - @sveltejs/kit@3.0.0-next.12 + +## 7.0.0-next.3 + +### Patch Changes + +- fix: include `utils.js` in package.json `files` ([#16298](https://github.com/sveltejs/kit/pull/16298)) +- Updated dependencies [[`737d119`](https://github.com/sveltejs/kit/commit/737d119730ec163832f838b3813c9cbb75dd799f), [`fa78efb`](https://github.com/sveltejs/kit/commit/fa78efb626031497eecc3a88fa7b6f6da36eca0f), [`07c207e`](https://github.com/sveltejs/kit/commit/07c207ed919570d4ed4954f751056b4f838abdea), [`a47071b`](https://github.com/sveltejs/kit/commit/a47071bb6581fe5f6df4cc47b80e9f0f594624e5), [`14d7d5a`](https://github.com/sveltejs/kit/commit/14d7d5abd9cd8462e4c79ed6a5c10bc43258e97a), [`5c38e51`](https://github.com/sveltejs/kit/commit/5c38e515db7fbb92e5ae01db84b4f0040a02f187), [`0702baa`](https://github.com/sveltejs/kit/commit/0702baa58b2a4600e83b55ef4a85e9cbe3391a4f), [`e1938c6`](https://github.com/sveltejs/kit/commit/e1938c62348b8ccc83e186b1acad76a0a10cc9ea), [`8293144`](https://github.com/sveltejs/kit/commit/8293144b6fbfff9f97cdb1d2224a5eaa04c84fc4), [`f76d7d9`](https://github.com/sveltejs/kit/commit/f76d7d91e18a0afd1f6798c095eb8c170c108cff), [`ab5c253`](https://github.com/sveltejs/kit/commit/ab5c253efb2546ead74db4c8c9b4c7639d97e0ed), [`b557b1b`](https://github.com/sveltejs/kit/commit/b557b1ba76903e1fb0fc3bb87bddc5420332137a), [`4a513e2`](https://github.com/sveltejs/kit/commit/4a513e27ab2ad44dae0c6a4a6c8deda840008530)]: + - @sveltejs/kit@3.0.0-next.8 + +## 7.0.0-next.2 + +### Patch Changes + +- fix: ensure types for `platform.context` work ([#16255](https://github.com/sveltejs/kit/pull/16255)) +- Updated dependencies [[`ba78a0b`](https://github.com/sveltejs/kit/commit/ba78a0bcc4ada3c649cb5d077d0a94c78045b389), [`a248c9b`](https://github.com/sveltejs/kit/commit/a248c9b6bd0593d0d28b7193fc14071944214dc3), [`7596981`](https://github.com/sveltejs/kit/commit/7596981950f534c61a0d23e73ec5dd8d5679d95c), [`be72ed9`](https://github.com/sveltejs/kit/commit/be72ed98cddf240af56d8595e4809cfbd478eec0), [`a5bd7e2`](https://github.com/sveltejs/kit/commit/a5bd7e22e94def2061f3dff9f7ff41f445cdb7a6), [`dec671f`](https://github.com/sveltejs/kit/commit/dec671f6634570053a1439a62440f2edb3044aeb), [`82f3867`](https://github.com/sveltejs/kit/commit/82f38673765e5b561da70fca25e7a4e9e5fec33b), [`97eb324`](https://github.com/sveltejs/kit/commit/97eb3247fc17a8ec6d2acfe854397eb6fe667617), [`e2a9ac0`](https://github.com/sveltejs/kit/commit/e2a9ac01ce9953eb45193f33cf31e203bb2f3656), [`309bfb4`](https://github.com/sveltejs/kit/commit/309bfb4da61b0d97623e17d4e5fdd70dbd4f970e), [`522a86b`](https://github.com/sveltejs/kit/commit/522a86b7b57099b407d213b558a1733583b9811c), [`0ff547f`](https://github.com/sveltejs/kit/commit/0ff547fe596a00b1d67f3b68d57f18a4a8f003da), [`3d4ff91`](https://github.com/sveltejs/kit/commit/3d4ff91cb7c948270ddc3e206413dd6d2da93b16), [`c925f2a`](https://github.com/sveltejs/kit/commit/c925f2a323dca6c7206e8ddd2c516f30bb89e6f7), [`7daf445`](https://github.com/sveltejs/kit/commit/7daf44551b6540244900a281a7bddd31241d65f3)]: + - @sveltejs/kit@3.0.0-next.7 + +## 7.0.0-next.1 +### Major Changes + + +- breaking: require `vite@^8.0.12`, the first Vite 8 release bundling stable `rolldown` 1.0.0 ([#16134](https://github.com/sveltejs/kit/pull/16134)) + + +### Patch Changes + +- Updated dependencies [[`a9629f1`](https://github.com/sveltejs/kit/commit/a9629f136d3256b14a8b3c10114c99832b977f6c), [`53d37f9`](https://github.com/sveltejs/kit/commit/53d37f93cf1f46995264398d4e54bb27bd1e75dd), [`dba6239`](https://github.com/sveltejs/kit/commit/dba62390e96f9f40c55ddc203744138664742026), [`ab12cb6`](https://github.com/sveltejs/kit/commit/ab12cb6a07269e2a18c0a3867e608e9838c592f3), [`f0eab52`](https://github.com/sveltejs/kit/commit/f0eab522bdea2d2a00f9b5473cd917d678b20daa), [`cd884c1`](https://github.com/sveltejs/kit/commit/cd884c1ffa6f96e08c70486c52fcca20501d5cef), [`9ed38a8`](https://github.com/sveltejs/kit/commit/9ed38a886be8c247c42c887c15d53b405693e1a9), [`53aa049`](https://github.com/sveltejs/kit/commit/53aa049a06ce7b053bdbe6484f05eecdb09d874e), [`984e57a`](https://github.com/sveltejs/kit/commit/984e57ac95d6ca7911611e1c297eecd1b1d870db), [`51785be`](https://github.com/sveltejs/kit/commit/51785be17ac706659d7fd7b3fca3fcf58dcc5de0), [`4f6bcbb`](https://github.com/sveltejs/kit/commit/4f6bcbbbb6e104238c41172ce7dbdaba486bbf43), [`78e7137`](https://github.com/sveltejs/kit/commit/78e71372ca284160df0aefe407999d46795a1299)]: + - @sveltejs/kit@3.0.0-next.5 + +## 7.0.0-next.0 +### Major Changes + + +- chore: use `rolldown` for edge function bundling ([#15432](https://github.com/sveltejs/kit/pull/15432)) + + +- breaking: write output that conforms to the stable [Netlify Frameworks API](https://docs.netlify.com/build/frameworks/frameworks-api/). ([#15294](https://github.com/sveltejs/kit/pull/15294)) + + Deploying and previewing with Netlify CLI now requires [v17.31.0](https://github.com/netlify/cli/releases/tag/v17.31.0) or later. Run `npm i -g netlify-cli@latest` to upgrade. + +- breaking: require SvelteKit 3 ([#15506](https://github.com/sveltejs/kit/pull/15506)) + + +- breaking: edge function build target is now `es2022` ([#15432](https://github.com/sveltejs/kit/pull/15432)) + + +### Patch Changes + +- Updated dependencies [[`fa335bd`](https://github.com/sveltejs/kit/commit/fa335bd8af3a982d68c4d34b813b68887dc7cce8), [`3031d89`](https://github.com/sveltejs/kit/commit/3031d89087d49297e89b6566108a1a1105c38475), [`cb9d416`](https://github.com/sveltejs/kit/commit/cb9d416d05266b6237f2856fb94df5b8a9b04279), [`caf3a18`](https://github.com/sveltejs/kit/commit/caf3a18de6581ab7d454d6fa8ad37ebdbdef95e6), [`4777827`](https://github.com/sveltejs/kit/commit/4777827246727045cf0209399751377564e7951d), [`a2792e2`](https://github.com/sveltejs/kit/commit/a2792e2cdc2dee91c20c7bfd921a104b539abae5), [`ba36148`](https://github.com/sveltejs/kit/commit/ba36148577ed0bc2675034a3d57001d71d328cd1), [`48e8710`](https://github.com/sveltejs/kit/commit/48e8710889b348a039322e05f3d2672327d56184), [`e2f3075`](https://github.com/sveltejs/kit/commit/e2f3075ef299b91175fae116e6a76269002029cf), [`047d6a0`](https://github.com/sveltejs/kit/commit/047d6a0f1c3fd1bc6f90a81177a2834b023a64b3), [`87603d1`](https://github.com/sveltejs/kit/commit/87603d13d009cada171c9b5d5daad5adad4f72de), [`096962c`](https://github.com/sveltejs/kit/commit/096962c47b0b0f63acd0d539d3973797e4867487), [`d545970`](https://github.com/sveltejs/kit/commit/d54597018308068aea5268e6c45b26398cd57607), [`e2f3075`](https://github.com/sveltejs/kit/commit/e2f3075ef299b91175fae116e6a76269002029cf), [`d06affc`](https://github.com/sveltejs/kit/commit/d06affc9246c676fc32b644efc0bbf53b42bc67d), [`8af47eb`](https://github.com/sveltejs/kit/commit/8af47eb06f9a1a883058de059ccf81aab4e85627), [`e2f3075`](https://github.com/sveltejs/kit/commit/e2f3075ef299b91175fae116e6a76269002029cf), [`5c4d130`](https://github.com/sveltejs/kit/commit/5c4d130ea2953464d8b4b462eb5aa4641aa78da1), [`3f11f35`](https://github.com/sveltejs/kit/commit/3f11f35125c5b3625891f0d00d9c7c826e6eaea2), [`caf3a18`](https://github.com/sveltejs/kit/commit/caf3a18de6581ab7d454d6fa8ad37ebdbdef95e6), [`8823037`](https://github.com/sveltejs/kit/commit/8823037b4a341013cc40a9bb688ee23bda3dbb04), [`1d76212`](https://github.com/sveltejs/kit/commit/1d76212740f59ed58839ef569899c653870834fd), [`0dc0548`](https://github.com/sveltejs/kit/commit/0dc0548307a439b6a6f24b14405366ced213f828), [`00d81fa`](https://github.com/sveltejs/kit/commit/00d81fab618435bac95b1bacb5e9734b61ba1af6)]: + - @sveltejs/kit@3.0.0-next.0 + ## 6.0.4 ### Patch Changes diff --git a/packages/adapter-netlify/ambient.d.ts b/packages/adapter-netlify/ambient.d.ts index a776cd3568f3..662524608f6d 100644 --- a/packages/adapter-netlify/ambient.d.ts +++ b/packages/adapter-netlify/ambient.d.ts @@ -1,7 +1,7 @@ declare global { namespace App { export interface Platform { - context: import('@netlify/functions').Context; + context: import('@netlify/types').Context; } } } diff --git a/packages/adapter-netlify/files/edge.js b/packages/adapter-netlify/files/edge.js new file mode 100644 index 000000000000..15bb1d040a28 --- /dev/null +++ b/packages/adapter-netlify/files/edge.js @@ -0,0 +1,47 @@ +/** @import { EdgeFunction } from '@netlify/edge-functions' */ +/** @import { Server } from '@sveltejs/kit' */ + +/** + * @param {Server} server + * @returns {EdgeFunction} + */ +export function init(server) { + /** + * We don't know the origin until we receive a request, but + * that's guaranteed to happen before we call `read` + * @type {string} + */ + let origin; + + const initialized = server.init({ + env: Deno.env.toObject(), + read: async (file) => { + const url = `${origin}/${file}`; + const response = await fetch(url); + + if (!response.ok) { + throw new Error( + `read(...) failed: could not fetch ${url} (${response.status} ${response.statusText})` + ); + } + + return response.body; + } + }); + + return async function handler(request, context) { + if (!origin) { + origin = new URL(request.url).origin; + } + + // always await initialization to prevent race condition with concurrent requests + await initialized; + + return server.respond(request, { + platform: { context }, + getClientAddress() { + return context.ip; + } + }); + }; +} diff --git a/packages/adapter-netlify/files/serverless.js b/packages/adapter-netlify/files/serverless.js new file mode 100644 index 000000000000..7397719ef335 --- /dev/null +++ b/packages/adapter-netlify/files/serverless.js @@ -0,0 +1,28 @@ +import { createReadableStream } from '@sveltejs/kit/node'; +import process from 'node:process'; + +/** + * @param {import('@sveltejs/kit').Server} server + * @returns {(request: Request, context: import('@netlify/types').Context) => Promise} + */ +export function init(server) { + /** @type {Promise | null} */ + let init_promise = server.init({ + env: process.env, + read: (file) => createReadableStream(`.netlify/server/${file}`) + }); + + return async (request, context) => { + if (init_promise !== null) { + await init_promise; + init_promise = null; + } + + return server.respond(request, { + platform: { context }, + getClientAddress() { + return context.ip; + } + }); + }; +} diff --git a/packages/adapter-netlify/files/tsconfig.json b/packages/adapter-netlify/files/tsconfig.json new file mode 100644 index 000000000000..bca3c5d6b8f8 --- /dev/null +++ b/packages/adapter-netlify/files/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "noEmit": true, + "target": "es2022", + "module": "node16", + "moduleResolution": "node16", + "paths": { + "@sveltejs/kit": ["../../kit/types/index"] + }, + "types": ["deno"] + }, + "include": ["edge.js"] +} diff --git a/packages/adapter-netlify/index.d.ts b/packages/adapter-netlify/index.d.ts index 580b30cb1060..0dac13fa991e 100644 --- a/packages/adapter-netlify/index.d.ts +++ b/packages/adapter-netlify/index.d.ts @@ -1,4 +1,23 @@ import { Adapter } from '@sveltejs/kit'; import './ambient.d.ts'; -export default function plugin(opts?: { split?: boolean; edge?: boolean }): Adapter; +export interface AdapterOptions { + /** + * If `true`, your app will be deployed as a [Netlify Edge Function](https://docs.netlify.com/build/edge-functions/overview/) rather than the standard Node-based function. + * @default false + */ + edge?: boolean; + /** + * If `true`, your app will be split into multiple functions instead of a single one for the entire app. + * @default false + */ + split?: boolean; + /** + * The directory to write prerendered pages and static assets (the contents of `static`, plus client-side JS and CSS generated by SvelteKit) to. + * @default 'build' + * @since 7.0.0 + */ + publish?: string; +} + +export default function plugin(opts?: AdapterOptions): Adapter; diff --git a/packages/adapter-netlify/index.js b/packages/adapter-netlify/index.js index cd8ff0521324..0057a2b40511 100644 --- a/packages/adapter-netlify/index.js +++ b/packages/adapter-netlify/index.js @@ -1,18 +1,16 @@ -/** @import { BuildOptions } from 'esbuild' */ -import { appendFileSync, existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'; -import { join, resolve, posix } from 'node:path'; +/** @import { IntegrationsConfig } from '@netlify/edge-functions' */ +/** @import { Builder, RouteDefinition } from '@sveltejs/kit' */ +import crypto from 'node:crypto'; +import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { builtinModules } from 'node:module'; import process from 'node:process'; -import esbuild from 'esbuild'; -import toml from '@iarna/toml'; +import { build } from 'rolldown'; +import { matches, s } from './utils.js'; -/** - * @typedef {{ - * build?: { publish?: string } - * functions?: { node_bundler?: 'zisi' | 'esbuild' } - * } & toml.JsonMap} NetlifyConfig - */ +const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf-8')); +const adapter_version = pkg.version; const name = '@sveltejs/adapter-netlify'; const files = fileURLToPath(new URL('./files', import.meta.url).href); @@ -21,13 +19,18 @@ const edge_set_in_env_var = process.env.NETLIFY_SVELTEKIT_USE_EDGE === 'true' || process.env.NETLIFY_SVELTEKIT_USE_EDGE === '1'; +const netlify_framework_config_path = '.netlify/v1/config.json'; +const netlify_framework_serverless_path = '.netlify/v1/functions'; +const netlify_framework_edge_path = '.netlify/v1/edge-functions'; + const FUNCTION_PREFIX = 'sveltekit-'; -/** @type {import('./index.js').default} */ -export default function ({ split = false, edge = edge_set_in_env_var } = {}) { +// "build" is the default publish directory when Netlify detects SvelteKit + +/** @type {typeof import('./index.js').default} */ +export default function ({ split = false, edge = edge_set_in_env_var, publish = 'build' } = {}) { return { name, - /** @param {import('@sveltejs/kit').Builder} builder */ async adapt(builder) { if (!builder.routes) { throw new Error( @@ -36,61 +39,60 @@ export default function ({ split = false, edge = edge_set_in_env_var } = {}) { ); } - if (existsSync(`${builder.config.kit.files.assets}/_headers`)) { + if (existsSync(`${builder.config.files.assets}/_headers`)) { throw new Error( - `The _headers file should be placed in the project root rather than the ${builder.config.kit.files.assets} directory` + `The _headers file should be placed in the project root rather than the ${builder.config.files.assets} directory` ); } - if (existsSync(`${builder.config.kit.files.assets}/_redirects`)) { + if (existsSync(`${builder.config.files.assets}/_redirects`)) { throw new Error( - `The _redirects file should be placed in the project root rather than the ${builder.config.kit.files.assets} directory` + `The _redirects file should be placed in the project root rather than the ${builder.config.files.assets} directory` ); } - const netlify_config = get_netlify_config(); - - // "build" is the default publish directory when Netlify detects SvelteKit - const publish = get_publish_directory(netlify_config, builder) || 'build'; - // empty out existing build directories - builder.rimraf(publish); - builder.rimraf('.netlify/edge-functions'); - builder.rimraf('.netlify/server'); - builder.rimraf('.netlify/package.json'); - builder.rimraf('.netlify/serverless.js'); - + rmSync(publish, { force: true, recursive: true }); + rmSync('.netlify/v1', { force: true, recursive: true }); + + // clean up legacy directories from older adapter versions to avoid + // gnarly edge cases when an existing project is upgraded to this version + rmSync('.netlify/edge-functions', { force: true, recursive: true }); + rmSync('.netlify/server', { force: true, recursive: true }); + rmSync('.netlify/package.json', { force: true, recursive: true }); + rmSync('.netlify/serverless.js', { force: true, recursive: true }); if (existsSync('.netlify/functions-internal')) { for (const file of readdirSync('.netlify/functions-internal')) { if (file.startsWith(FUNCTION_PREFIX)) { - builder.rimraf(join('.netlify/functions-internal', file)); + rmSync(join('.netlify/functions-internal', file), { force: true, recursive: true }); } } } - builder.log.minor(`Publishing to "${publish}"`); - builder.log.minor('Copying assets...'); - const publish_dir = `${publish}${builder.config.kit.paths.base}`; + const publish_dir = `${publish}${builder.config.paths.base}`; builder.writeClient(publish_dir); builder.writePrerendered(publish_dir); - builder.log.minor('Writing custom headers...'); - const headers_file = join(publish, '_headers'); - builder.copy('_headers', headers_file); - appendFileSync( - headers_file, - `\n\n/${builder.getAppPath()}/immutable/*\n cache-control: public\n cache-control: immutable\n cache-control: max-age=31536000\n` - ); + // Copy user's _headers file if it exists + if (existsSync('_headers')) { + builder.log.minor('Copying user custom headers...'); + builder.copy('_headers', join(publish, '_headers')); + } - if (edge) { - if (split) { - throw new Error('Cannot use `split: true` alongside `edge: true`'); - } + // Copy user's _redirects file if it exists + if (existsSync('_redirects')) { + builder.log.minor('Copying user redirects...'); + builder.copy('_redirects', join(publish, '_redirects')); + } - await generate_edge_functions({ builder }); + builder.log.minor('Writing Netlify config...'); + write_frameworks_config({ builder }); + + if (edge) { + await generate_edge_functions({ builder, split }); } else { - generate_lambda_functions({ builder, split, publish }); + generate_serverless_functions(builder, split); } }, @@ -100,358 +102,484 @@ export default function ({ split = false, edge = edge_set_in_env_var } = {}) { } }; } + /** - * @param { object } params - * @param {import('@sveltejs/kit').Builder} params.builder + * @param {Builder} builder + * @param {boolean} split */ -async function generate_edge_functions({ builder }) { - const tmp = builder.getBuildDirectory('netlify-tmp'); - builder.rimraf(tmp); - builder.mkdirp(tmp); +function generate_serverless_functions(builder, split) { + // https://docs.netlify.com/build/frameworks/frameworks-api/#netlifyv1functions + mkdirSync(netlify_framework_serverless_path, { recursive: true }); + + builder.writeServer('.netlify/v1/server'); - builder.mkdirp('.netlify/edge-functions'); + builder.copy(`${files}/serverless.js`, '.netlify/v1/serverless.js'); - builder.log.minor('Generating Edge Function...'); - const relativePath = posix.relative(tmp, builder.getServerDirectory()); + builder.log.minor('Generating serverless functions...'); - builder.copy(`${files}/edge.js`, `${tmp}/entry.js`, { - replace: { - '0SERVER': `${relativePath}/index.js`, - MANIFEST: './manifest.js' + if (split) { + const uuid = crypto.randomUUID(); + for (const fn of get_split_functions(builder)) { + generate_serverless_function(builder, fn, uuid); } - }); + } else { + generate_serverless_function(builder, { + type: 'singular', + routes: undefined, + patterns: ['/*'], + name: `${FUNCTION_PREFIX}render`, + display_name: 'SvelteKit server' + }); + } +} - const manifest = builder.generateManifest({ - relativePath - }); +/** + * Writes the Netlify Frameworks API config file + * https://docs.netlify.com/build/frameworks/frameworks-api/ + * @param {{ builder: import('@sveltejs/kit').Builder }} params + */ +function write_frameworks_config({ builder }) { + // https://docs.netlify.com/build/frameworks/frameworks-api/#headers + /** @type {{ headers: Array<{ for: string, values: Record }> }} */ + const config = { + headers: [ + { + for: `/${builder.getAppPath()}/immutable/*`, + values: { + 'cache-control': 'public, immutable, max-age=31536000' + } + } + ] + }; - writeFileSync(`${tmp}/manifest.js`, `export const manifest = ${manifest};\n`); + mkdirSync('.netlify/v1', { recursive: true }); + writeFileSync(netlify_framework_config_path, s(config)); +} - /** @type {{ assets: Set }} */ - // we have to prepend the file:// protocol because Windows doesn't support absolute path imports - const { assets } = (await import(`file://${tmp}/manifest.js`)).manifest; +/** @typedef {'singular' | 'split' | 'catch-all'} EntrypointType */ - const path = '/*'; - // We only need to specify paths without the trailing slash because - // Netlify will handle the optional trailing slash for us - const excluded = [ - // Contains static files - `/${builder.getAppPath()}/immutable/*`, - `/${builder.getAppPath()}/version.json`, - ...builder.prerendered.paths, - ...Array.from(assets).flatMap((asset) => { - if (asset.endsWith('/index.html')) { - const dir = asset.replace(/\/index\.html$/, ''); - return [ - `${builder.config.kit.paths.base}/${asset}`, - `${builder.config.kit.paths.base}/${dir}` - ]; +/** + * @typedef {object} EntrypointMetadata + * @property {EntrypointType} type + * @property {RouteDefinition[] | undefined} routes + * @property {string[]} patterns + * @property {string} name + * @property {string} display_name + * @property {string[]} [exclude] + */ + +/** + * @param {Builder} builder + * @returns {EntrypointMetadata[]} + */ +function get_split_functions(builder) { + const seen = new Set(); + let index = 0; + + /** @type {EntrypointMetadata[]} */ + const functions = []; + + for (let i = 0; i < builder.routes.length; i++) { + const route = builder.routes[i]; + if (route.prerender === true) continue; + + const routes = [route]; + /** @type {string[]} */ + const parts = []; + + // The parts should conform to URLPattern syntax + // https://docs.netlify.com/build/functions/get-started/?fn-language=ts&data-tab=TypeScript#route-requests + for (const [i, segment] of route.segments.entries()) { + if (segment.rest) { + parts.push('*'); + } else if (segment.dynamic) { + // URLPattern requires params to start with letters + const optional = /^\[\[.+\]\]$/.test(segment.content) ? '?' : ''; + parts.push(`:param${i}${optional}`); + } else { + parts.push(segment.content); } - return `${builder.config.kit.paths.base}/${asset}`; - }), - // Should not be served by SvelteKit at all - '/.netlify/*' - ]; + } - /** @type {import('@netlify/edge-functions').Manifest} */ - const edge_manifest = { - functions: [ - { - function: 'render', - path, - excludedPath: /** @type {`/${string}`[]} */ (excluded) + // Netlify handles trailing slashes for us, so we don't need to include them in the pattern + const pattern = `/${parts.join('/')}`; + + // skip routes with identical patterns, they were already folded into another function + if (seen.has(pattern)) continue; + + const patterns = [pattern, `${pattern === '/' ? '' : pattern}/__data.json`]; + patterns.forEach((pattern) => seen.add(pattern)); + + // figure out which lower priority routes should be considered fallbacks + for (let j = i + 1; j < builder.routes.length; j += 1) { + const other = builder.routes[j]; + if (other.prerender === true) continue; + + if (matches(route.segments, other.segments)) { + routes.push(other); } - ], - version: 1 - }; + } - /** @type {BuildOptions} */ - const esbuild_config = { - bundle: true, - format: 'esm', - platform: 'browser', - sourcemap: 'linked', - target: 'es2020', - loader: { - '.wasm': 'copy', - '.woff': 'copy', - '.woff2': 'copy', - '.ttf': 'copy', - '.eot': 'copy', - '.otf': 'copy' - }, - // Node built-ins are allowed, but must be prefixed with `node:` - // https://docs.netlify.com/edge-functions/api/#runtime-environment - external: builtinModules.map((id) => `node:${id}`), - alias: Object.fromEntries(builtinModules.map((id) => [id, `node:${id}`])) - }; - await Promise.all([ - esbuild.build({ - entryPoints: [`${tmp}/entry.js`], - outfile: '.netlify/edge-functions/render.js', - ...esbuild_config - }), - builder.hasServerInstrumentationFile() && - esbuild.build({ - entryPoints: [`${builder.getServerDirectory()}/instrumentation.server.js`], - outfile: '.netlify/edge/instrumentation.server.js', - ...esbuild_config - }) - ]); + functions.push({ + type: 'split', + routes, + patterns, + name: `${FUNCTION_PREFIX}${index++}`, + display_name: `SvelteKit ${route.id}` + }); + } + + functions.push({ + type: 'catch-all', + routes: [], + patterns: ['/*'], + name: `${FUNCTION_PREFIX}catch-all`, + display_name: 'SvelteKit catch-all', + exclude: Array.from(seen) + }); + + return functions; +} + +/** + * @param {Builder} builder + * @param {EntrypointMetadata} fn + * @param {string} [uuid] + */ +function generate_serverless_function(builder, fn, uuid) { + builder.generateServerInstance(`.netlify/v1/server-${fn.name}.js`, { + routes: fn.routes, + serverDirectory: '.netlify/v1/server' + }); + + const filename = `${netlify_framework_serverless_path}/${fn.name}.mjs`; + const code = generate_function_module( + fn.type, + '../serverless.js', + `../server-${fn.name}.js`, + uuid + ); + const config = create_function_config('serverless', fn); if (builder.hasServerInstrumentationFile()) { + writeFileSync(filename, code); + const initializer = builder.createInstrumentationInitializer({ + outputDirectory: netlify_framework_serverless_path, + serverDirectory: '.netlify/v1/server' + }); builder.instrument({ - entrypoint: '.netlify/edge-functions/render.js', - instrumentation: '.netlify/edge/instrumentation.server.js', - start: '.netlify/edge/start.js' + entrypoint: filename, + instrumentation: '.netlify/v1/server/instrumentation.server.js', + start: `.netlify/v1/server/${fn.name}.start.mjs`, + initializer, + module: { + generateText: generate_traced_module(config) + } }); + } else { + writeFileSync(filename, `${code}\n${config}`); } - - writeFileSync('.netlify/edge-functions/manifest.json', JSON.stringify(edge_manifest)); } + /** - * @param { object } params - * @param {import('@sveltejs/kit').Builder} params.builder - * @param { string } params.publish - * @param { boolean } params.split + * @param {EntrypointType} type + * @param {string} init + * @param {string} server + * @param {string} [uuid] + * @returns {string} */ -function generate_lambda_functions({ builder, publish, split }) { - builder.mkdirp('.netlify/functions-internal/.svelte-kit'); +function generate_function_module(type, init, server, uuid) { + const runtime_imports = [ + `import { init } from '${init}';`, + `import { server } from '${server}';` + ].join('\n'); + const original_pathname_header = `const original_pathname_header = \`x-sveltekit-original-pathname-${uuid}\``; + + if (type === 'catch-all' && uuid) { + // Netlify encodes the response body but `fetch` automatically decodes it. + // So, we need to remove the `content-encoding` header to allow Netlify + // to correctly re-encode it on the way out. + return `\ +import { applyReroute } from '@sveltejs/kit/adapter'; +${runtime_imports} - builder.writeServer('.netlify/server'); +${original_pathname_header} - const replace = { - '0SERVER': './server/index.js' // digit prefix prevents CJS build from using this as a variable name, which would also get replaced - }; +const respond = init(server); - builder.copy(files, '.netlify', { replace, filter: (name) => !name.endsWith('edge.js') }); +export default async (request, context) => { + const catch_all_response = await respond(request, context); - builder.log.minor('Generating serverless functions...'); + return await applyReroute(catch_all_response, async (url) => { + const rerouted_request = new Request(url, request); + rerouted_request.headers.set(original_pathname_header, new URL(request.url).pathname); - if (split) { - const seen = new Set(); - - for (let i = 0; i < builder.routes.length; i++) { - const route = builder.routes[i]; - if (route.prerender === true) continue; - - const routes = [route]; - - /** @type {string[]} */ - const parts = []; - - // The parts should conform to URLPattern syntax - // https://docs.netlify.com/build/functions/get-started/?fn-language=ts&data-tab=TypeScript#route-requests - for (const segment of route.segments) { - if (segment.rest) { - parts.push('*'); - } else if (segment.dynamic) { - // URLPattern requires params to start with letters - parts.push(`:param${parts.length}`); - } else { - parts.push(segment.content); - } - } + const rerouted_response = await fetch(rerouted_request); - // Netlify handles trailing slashes for us, so we don't need to include them in the pattern - const pattern = `/${parts.join('/')}`; - const name = - FUNCTION_PREFIX + (parts.join('-').replace(/[:.]/g, '_').replace('*', '__rest') || 'index'); + const response = new Response(rerouted_response.body, rerouted_response); + if (response.headers.has('content-encoding')) { + response.headers.delete('content-encoding'); + response.headers.delete('content-length'); + } - // skip routes with identical patterns, they were already folded into another function - if (seen.has(pattern)) continue; + return response; + }); +}; +`; + } - const patterns = [pattern, `${pattern === '/' ? '' : pattern}/__data.json`]; - patterns.forEach((p) => seen.add(p)); + if (type === 'split' && uuid) { + return `\ +${runtime_imports} - // figure out which lower priority routes should be considered fallbacks - for (let j = i + 1; j < builder.routes.length; j += 1) { - const other = builder.routes[j]; - if (other.prerender === true) continue; +${original_pathname_header} - if (matches(route.segments, other.segments)) { - routes.push(other); - } - } +const respond = init(server); - generate_serverless_function({ - builder, - routes, - patterns, - name - }); - } - - generate_serverless_function({ - builder, - routes: [], - patterns: ['/*'], - name: `${FUNCTION_PREFIX}catch-all`, - exclude: Array.from(seen) - }); - } else { - generate_serverless_function({ - builder, - routes: undefined, - patterns: ['/*'], - name: `${FUNCTION_PREFIX}render` - }); +export default async (request, context) => { + if (request.headers.has(original_pathname_header)) { + const url = new URL(request.url); + url.pathname = request.headers.get(original_pathname_header); + request = new Request(url, request); + request.headers.delete(original_pathname_header); } - // Copy user's custom _redirects file if it exists - if (existsSync('_redirects')) { - builder.log.minor('Copying user redirects...'); - const redirects_file = join(publish, '_redirects'); - builder.copy('_redirects', redirects_file); + return await respond(request, context); +}; +`; } -} -function get_netlify_config() { - if (!existsSync('netlify.toml')) return null; + return `\ +${runtime_imports} - try { - return /** @type {NetlifyConfig} */ (toml.parse(readFileSync('netlify.toml', 'utf-8'))); - } catch (err) { - if (err instanceof Error) { - err.message = `Error parsing netlify.toml: ${err.message}`; - } - throw err; - } +export default init(server); +`; } +const generator_string = `@sveltejs/adapter-netlify@${adapter_version}`; + /** - * @param {NetlifyConfig | null} netlify_config - * @param {import('@sveltejs/kit').Builder} builder - **/ -function get_publish_directory(netlify_config, builder) { - if (netlify_config) { - if (!netlify_config.build?.publish) { - builder.log.minor('No publish directory specified in netlify.toml, using default'); - return; - } + * @param {'serverless' | 'edge'} runtime + * @param {EntrypointMetadata} fn + * @returns {string} + */ +function create_function_config(runtime, fn) { + /** @type {IntegrationsConfig & { preferStatic?: boolean }} */ + const config = { + name: fn.display_name, + generator: generator_string, + path: /** @type {`/${string}`[]} */ (fn.patterns), + excludedPath: /** @type {`/${string}`[]} */ (['/.netlify/*', ...(fn.exclude ?? [])]) + }; - if (resolve(netlify_config.build.publish) === process.cwd()) { - throw new Error( - 'The publish directory cannot be set to the site root. Please change it to another value such as "build" in netlify.toml.' - ); - } - return netlify_config.build.publish; + if (runtime === 'serverless') { + config.preferStatic = true; } - builder.log.warn( - 'No netlify.toml found. Using default publish directory. Consult https://svelte.dev/docs/kit/adapter-netlify#usage for more details' - ); + return `export const config = ${s(config)};\n`; } /** - * @typedef {{ rest: boolean, dynamic: boolean, content: string }} RouteSegment + * @param {string} config + * @returns {(opts: { instrumentation: string; start: string; initializer: string }) => string} */ +function generate_traced_module(config) { + return ({ instrumentation, start, initializer }) => { + return `\ +import ${JSON.stringify(to_import_specifier(initializer))}; +import ${JSON.stringify(to_import_specifier(instrumentation))}; +const { default: _0 } = await import(${JSON.stringify(to_import_specifier(start))}); +export { _0 as default }; + +${config}`; + }; +} + +/** @param {string} path */ +function to_import_specifier(path) { + return path.startsWith('.') ? path : `./${path}`; +} + +/** @satisfies {import('rolldown').BuildOptions} */ +const rolldown_config = { + platform: 'browser', + output: { + sourcemap: true, + codeSplitting: false + }, + transform: { + target: 'es2022' + }, + // Node built-ins are allowed, but must be prefixed with `node:` + // https://docs.netlify.com/edge-functions/api/#runtime-environment + external: builtinModules.map((id) => `node:${id}`), + resolve: { + alias: Object.fromEntries(builtinModules.map((id) => [id, `node:${id}`])) + } +}; /** - * @param {RouteSegment[]} a - * @param {RouteSegment[]} b - * @returns {boolean} + * @param {object} params + * @param {Builder} params.builder + * @param {boolean} params.split */ -function matches(a, b) { - if (a[0] && b[0]) { - if (b[0].rest) { - if (b.length === 1) return true; +async function generate_edge_functions({ builder, split }) { + const tmp = builder.getBuildDirectory('netlify-tmp'); + rmSync(tmp, { force: true, recursive: true }); + mkdirSync(tmp, { recursive: true }); - const next_b = b.slice(1); + // https://docs.netlify.com/build/frameworks/frameworks-api/#edge-functions + mkdirSync(netlify_framework_edge_path, { recursive: true }); - for (let i = 0; i < a.length; i += 1) { - if (matches(a.slice(i), next_b)) return true; - } + builder.log.minor('Generating edge functions...'); + builder.copy(`${files}/edge.js`, `${tmp}/edge.js`); - return false; - } + // We only need to specify paths without the trailing slash because + // Netlify will handle the optional trailing slash for us + const excluded_paths = [ + // Contains static files + `/${builder.getAppPath()}/immutable/*`, + `/${builder.getAppPath()}/version.json`, + // the base root and `trailingSlash: 'always'` pages are recorded with a trailing slash + ...builder.prerendered.paths.map((path) => (path === '/' ? path : path.replace(/\/$/, ''))), + ...Array.from(builder.manifest.assets).flatMap(({ path: asset }) => { + if (asset.endsWith('/index.html')) { + const dir = asset.replace(/\/index\.html$/, ''); + return [`${builder.config.paths.base}/${asset}`, `${builder.config.paths.base}/${dir}`]; + } + return `${builder.config.paths.base}/${asset}`; + }) + ]; - if (!b[0].dynamic) { - if (!a[0].dynamic && a[0].content !== b[0].content) return false; - } + /** @type {EntrypointMetadata[]} */ + const functions = split + ? get_split_functions(builder) + : [ + { + type: 'singular', + routes: undefined, + patterns: ['/*'], + name: `${FUNCTION_PREFIX}render`, + display_name: 'SvelteKit server' + } + ]; + const reroute_id = split ? crypto.randomUUID() : undefined; - if (a.length === 1 && b.length === 1) return true; - return matches(a.slice(1), b.slice(1)); - } else if (a[0]) { - return a.length === 1 && a[0].rest; - } else { - return b.length === 1 && b[0].rest; + for (const fn of functions) { + await generate_edge_function( + builder, + tmp, + { + ...fn, + exclude: excluded_paths.concat(fn.exclude ?? []) + }, + reroute_id + ); } } /** - * - * @param {{ - * builder: import('@sveltejs/kit').Builder, - * routes: import('@sveltejs/kit').RouteDefinition[] | undefined, - * patterns: string[], - * name: string, - * exclude?: string[] - * }} opts + * @param {Builder} builder + * @param {string} tmp + * @param {EntrypointMetadata} fn + * @param {string | undefined} reroute_id */ -function generate_serverless_function({ builder, routes, patterns, name, exclude }) { - const manifest = builder.generateManifest({ - relativePath: '../server', - routes - }); - - const fn = generate_serverless_function_module(manifest); - const config = generate_config_export(patterns, exclude); +async function generate_edge_function(builder, tmp, fn, reroute_id) { + builder.generateServerInstance(`${tmp}/server-${fn.name}.js`, { routes: fn.routes }); + + const entry = `${tmp}/entry-${fn.name}.js`; + const code = generate_edge_function_module( + fn.type, + './edge.js', + `./server-${fn.name}.js`, + reroute_id + ); + const config = create_function_config('edge', fn); + writeFileSync(entry, `${code}\n${config}`); + let input = entry; if (builder.hasServerInstrumentationFile()) { - writeFileSync(`.netlify/functions-internal/${name}.mjs`, fn); + const initializer = builder.createInstrumentationInitializer({ + outputDirectory: tmp, + environment: 'export default Deno.env.toObject();\n' + }); + input = `${tmp}/instrumented-entry-${fn.name}.js`; + writeFileSync(input, `export { default, config } from './entry-${fn.name}.js';\n`); builder.instrument({ - entrypoint: `.netlify/functions-internal/${name}.mjs`, - instrumentation: '.netlify/server/instrumentation.server.js', - start: `.netlify/functions-start/${name}.start.mjs`, + entrypoint: input, + instrumentation: `${builder.getServerDirectory()}/instrumentation.server.js`, + initializer, module: { generateText: generate_traced_module(config) } }); - } else { - writeFileSync(`.netlify/functions-internal/${name}.mjs`, `${fn}\n${config}`); } + + await build({ + ...rolldown_config, + input, + output: { + ...rolldown_config.output, + file: `${netlify_framework_edge_path}/${fn.name}.js` + } + }); } /** - * @param {string} manifest + * @param {EntrypointType} type + * @param {string} init + * @param {string} server + * @param {string | undefined} reroute_id * @returns {string} */ -function generate_serverless_function_module(manifest) { - return `\ -import { init } from '../serverless.js'; +function generate_edge_function_module(type, init, server, reroute_id) { + if (type === 'singular') { + return generate_function_module(type, init, server); + } -export default init(${manifest}); -`; -} + const runtime_imports = [ + `import { init } from '${init}';`, + `import { server } from '${server}';` + ].join('\n'); + const reroute_parameter = `__sveltekit_original_pathname_${reroute_id}`; -/** - * @param {string[]} patterns - * @param {string[]} [exclude] - * @returns {string} - */ -function generate_config_export(patterns, exclude = []) { - // TODO: add a human friendly name for the function https://docs.netlify.com/build/frameworks/frameworks-api/#configuration-options-2 - return `\ -export const config = { - path: [${patterns.map((s) => JSON.stringify(s)).join(', ')}], - excludedPath: [${['/.netlify/*', ...exclude].map((s) => JSON.stringify(s)).join(', ')}], - preferStatic: true + if (type === 'catch-all') { + return `\ +import { applyReroute } from '@sveltejs/kit/adapter'; +${runtime_imports} + +const respond = init(server); + +export default async (request, context) => { + const response = await respond(request, context); + + let rerouted_url; + applyReroute(response, (url) => { + url.searchParams.set('${reroute_parameter}', new URL(request.url).pathname); + rerouted_url = url; + }); + + return rerouted_url ?? response; }; `; -} + } -/** - * @param {string} config - * @returns {(opts: { instrumentation: string; start: string }) => string} - */ -function generate_traced_module(config) { - return ({ instrumentation, start }) => { - return `\ -import './${instrumentation}'; -const { default: _0 } = await import('./${start}'); -export { _0 as default }; + return `\ +${runtime_imports} -${config}`; - }; +const respond = init(server); + +export default async (request, context) => { + const url = new URL(request.url); + const pathname = url.searchParams.get('${reroute_parameter}'); + + if (pathname) { + url.pathname = pathname; + url.searchParams.delete('${reroute_parameter}'); + request = new Request(url, request); + } + + return await respond(request, context); +}; +`; } diff --git a/packages/adapter-netlify/internal.d.ts b/packages/adapter-netlify/internal.d.ts deleted file mode 100644 index 55da8ba1fbf5..000000000000 --- a/packages/adapter-netlify/internal.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -declare module '0SERVER' { - export { Server } from '@sveltejs/kit'; -} - -declare module 'MANIFEST' { - import { SSRManifest } from '@sveltejs/kit'; - - export const manifest: SSRManifest; -} diff --git a/packages/adapter-netlify/package.json b/packages/adapter-netlify/package.json index fd130d16aee7..9818e7ecff6d 100644 --- a/packages/adapter-netlify/package.json +++ b/packages/adapter-netlify/package.json @@ -1,6 +1,6 @@ { "name": "@sveltejs/adapter-netlify", - "version": "6.0.4", + "version": "7.0.0-next.12", "description": "A SvelteKit adapter that creates a Netlify app", "keywords": [ "adapter", @@ -27,39 +27,34 @@ }, "types": "index.d.ts", "files": [ - "files", + "files/*.js", "index.js", + "utils.js", "index.d.ts", "ambient.d.ts" ], "scripts": { - "dev": "rollup -cw", - "build": "rollup -c", - "check": "tsc", - "lint": "prettier --check .", - "format": "pnpm lint --write", + "check": "pnpm tsc-native && pnpm tsc-native --project files/tsconfig.json", + "lint": "oxfmt --check .", + "format": "oxfmt --write .", "test:unit": "vitest run", - "test:build": "pnpm build && pnpm -r --workspace-concurrency 1 --filter=\"./test/**\" build", - "test": "pnpm test:build", - "prepublishOnly": "pnpm build" + "test:build": "pnpm -r --workspace-concurrency 1 --filter=\"./test/**\" test:build", + "test": "pnpm test:unit && pnpm test:build", + "tsc-native": "node node_modules/@typescript/native/bin/tsc" }, "dependencies": { - "@iarna/toml": "^2.2.5", - "esbuild": "^0.28.1" + "@netlify/types": "^2.8.0", + "rolldown": "^1.2.3" }, "devDependencies": { - "@netlify/edge-functions": "catalog:", - "@netlify/functions": "catalog:", - "@rollup/plugin-commonjs": "catalog:", - "@rollup/plugin-json": "catalog:", - "@rollup/plugin-node-resolve": "catalog:", + "@netlify/edge-functions": "^3.0.8", "@sveltejs/kit": "workspace:^", + "@types/deno": "^2.7.0", "@types/node": "catalog:", - "rollup": "^4.59.0", - "typescript": "catalog:", + "@typescript/native": "catalog:", "vitest": "catalog:" }, "peerDependencies": { - "@sveltejs/kit": "^2.31.0" + "@sveltejs/kit": "^3.0.0-next.0" } } diff --git a/packages/adapter-netlify/rollup.config.js b/packages/adapter-netlify/rollup.config.js deleted file mode 100644 index 82d61b85d29d..000000000000 --- a/packages/adapter-netlify/rollup.config.js +++ /dev/null @@ -1,40 +0,0 @@ -import { nodeResolve } from '@rollup/plugin-node-resolve'; -import commonjs from '@rollup/plugin-commonjs'; -import json from '@rollup/plugin-json'; -import { rmSync } from 'node:fs'; - -/** - * @param {string} filepath - * @returns {import('rollup').Plugin} - */ -function clearOutput(filepath) { - return { - name: 'clear-output', - buildStart: { - order: 'pre', - sequential: true, - handler() { - rmSync(filepath, { recursive: true, force: true }); - } - } - }; -} - -/** @type {import('rollup').RollupOptions} */ -const config = { - input: { - serverless: 'src/serverless.js', - shims: 'src/shims.js', - edge: 'src/edge.js' - }, - output: { - dir: 'files', - format: 'esm' - }, - // @ts-ignore https://github.com/rollup/plugins/issues/1329 - plugins: [clearOutput('files'), nodeResolve({ preferBuiltins: true }), commonjs(), json()], - external: (id) => id === '0SERVER' || id === 'MANIFEST' || id.startsWith('node:'), - preserveEntrySignatures: 'exports-only' -}; - -export default config; diff --git a/packages/adapter-netlify/src/edge.js b/packages/adapter-netlify/src/edge.js deleted file mode 100644 index f0d9944741ca..000000000000 --- a/packages/adapter-netlify/src/edge.js +++ /dev/null @@ -1,43 +0,0 @@ -import { Server } from '0SERVER'; -import { manifest } from 'MANIFEST'; - -const server = new Server(manifest); - -/** - * We don't know the origin until we receive a request, but - * that's guaranteed to happen before we call `read` - * @type {string} - */ -let origin; - -const initialized = server.init({ - // @ts-ignore - env: Deno.env.toObject(), - read: async (file) => { - const url = `${origin}/${file}`; - const response = await fetch(url); - - if (!response.ok) { - throw new Error( - `read(...) failed: could not fetch ${url} (${response.status} ${response.statusText})` - ); - } - - return response.body; - } -}); - -/** @type {import('@netlify/edge-functions').EdgeFunction} */ -export default async function handler(request, context) { - if (!origin) { - origin = new URL(request.url).origin; - await initialized; - } - - return server.respond(request, { - platform: { context }, - getClientAddress() { - return context.ip; - } - }); -} diff --git a/packages/adapter-netlify/src/serverless.js b/packages/adapter-netlify/src/serverless.js deleted file mode 100644 index 592eb081b7b9..000000000000 --- a/packages/adapter-netlify/src/serverless.js +++ /dev/null @@ -1,32 +0,0 @@ -import './shims.js'; -import { Server } from '0SERVER'; -import { createReadableStream } from '@sveltejs/kit/node'; -import process from 'node:process'; - -/** - * @param {import('@sveltejs/kit').SSRManifest} manifest - * @returns {(request: Request, context: import('@netlify/functions').Context) => Promise} - */ -export function init(manifest) { - const server = new Server(manifest); - - /** @type {Promise | null} */ - let init_promise = server.init({ - env: /** @type {Record} */ (process.env), - read: (file) => createReadableStream(`.netlify/server/${file}`) - }); - - return async (request, context) => { - if (init_promise !== null) { - await init_promise; - init_promise = null; - } - - return server.respond(request, { - platform: { context }, - getClientAddress() { - return context.ip; - } - }); - }; -} diff --git a/packages/adapter-netlify/src/shims.js b/packages/adapter-netlify/src/shims.js deleted file mode 100644 index 2490311daa1e..000000000000 --- a/packages/adapter-netlify/src/shims.js +++ /dev/null @@ -1,2 +0,0 @@ -import { installPolyfills } from '@sveltejs/kit/node/polyfills'; -installPolyfills(); diff --git a/packages/adapter-netlify/test/apps/basic/.gitignore b/packages/adapter-netlify/test/apps/basic/.gitignore index 88f661c765c6..4150a3674e3a 100644 --- a/packages/adapter-netlify/test/apps/basic/.gitignore +++ b/packages/adapter-netlify/test/apps/basic/.gitignore @@ -3,4 +3,4 @@ node_modules /.svelte-kit /.netlify /build -deno.lock \ No newline at end of file +deno.lock diff --git a/packages/adapter-netlify/test/apps/basic/_headers b/packages/adapter-netlify/test/apps/basic/_headers new file mode 100644 index 000000000000..01c0aab8e046 --- /dev/null +++ b/packages/adapter-netlify/test/apps/basic/_headers @@ -0,0 +1,2 @@ +/custom-header-path + X-Custom-Header: test-value diff --git a/packages/adapter-netlify/test/apps/basic/_redirects b/packages/adapter-netlify/test/apps/basic/_redirects new file mode 100644 index 000000000000..fd5703a26e6a --- /dev/null +++ b/packages/adapter-netlify/test/apps/basic/_redirects @@ -0,0 +1 @@ +/redirect-me /greeting/redirected 301 diff --git a/packages/adapter-netlify/test/apps/basic/build.test.js b/packages/adapter-netlify/test/apps/basic/build.test.js new file mode 100644 index 000000000000..107f8246134c --- /dev/null +++ b/packages/adapter-netlify/test/apps/basic/build.test.js @@ -0,0 +1,16 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { test, expect } from 'vitest'; + +test('_redirects are copied to publish directory', () => { + const redirects = fs.readFileSync( + path.resolve(import.meta.dirname, './build/_redirects'), + 'utf-8' + ); + expect(redirects).toContain('/redirect-me /greeting/redirected 301'); +}); + +test('_headers are copied to publish directory', () => { + const headers = fs.readFileSync(path.resolve(import.meta.dirname, './build/_headers'), 'utf-8'); + expect(headers).toContain('X-Custom-Header: test-value'); +}); diff --git a/packages/adapter-netlify/test/apps/basic/netlify.toml b/packages/adapter-netlify/test/apps/basic/netlify.toml deleted file mode 100644 index 0183026741cd..000000000000 --- a/packages/adapter-netlify/test/apps/basic/netlify.toml +++ /dev/null @@ -1,2 +0,0 @@ -[build] -publish = "build" diff --git a/packages/adapter-netlify/test/apps/basic/package.json b/packages/adapter-netlify/test/apps/basic/package.json index 16540a0375c0..10a3b68f8176 100644 --- a/packages/adapter-netlify/test/apps/basic/package.json +++ b/packages/adapter-netlify/test/apps/basic/package.json @@ -6,14 +6,15 @@ "dev": "vite dev", "build": "vite build", "preview": "vite preview", - "prepare": "svelte-kit sync || echo ''", + "test:build": "pnpm build && vitest run build.test.js", "test:platform": "playwright test" }, "devDependencies": { "@sveltejs/kit": "workspace:^", "@sveltejs/vite-plugin-svelte": "catalog:", "svelte": "catalog:", - "vite": "catalog:" + "vite": "catalog:", + "vitest": "catalog:" }, "type": "module" } diff --git a/packages/adapter-netlify/test/apps/basic/src/instrumentation.server.js b/packages/adapter-netlify/test/apps/basic/src/instrumentation.server.js deleted file mode 100644 index acc9022e1d64..000000000000 --- a/packages/adapter-netlify/test/apps/basic/src/instrumentation.server.js +++ /dev/null @@ -1 +0,0 @@ -// this is just here to make sure the changes resulting from it work diff --git a/packages/adapter-netlify/test/apps/basic/src/routes/+page.svelte b/packages/adapter-netlify/test/apps/basic/src/routes/+page.svelte new file mode 100644 index 000000000000..03c350d48255 --- /dev/null +++ b/packages/adapter-netlify/test/apps/basic/src/routes/+page.svelte @@ -0,0 +1 @@ +

Hello from SvelteKit

diff --git a/packages/adapter-netlify/test/apps/basic/src/routes/greeting/[name]/+page.server.js b/packages/adapter-netlify/test/apps/basic/src/routes/greeting/[name]/+page.server.js new file mode 100644 index 000000000000..31e005a89bad --- /dev/null +++ b/packages/adapter-netlify/test/apps/basic/src/routes/greeting/[name]/+page.server.js @@ -0,0 +1,4 @@ +/** @type {import('./$types').PageServerLoad} */ +export function load({ params }) { + return { name: params.name }; +} diff --git a/packages/adapter-netlify/test/apps/basic/src/routes/greeting/[name]/+page.svelte b/packages/adapter-netlify/test/apps/basic/src/routes/greeting/[name]/+page.svelte new file mode 100644 index 000000000000..4dcbba568369 --- /dev/null +++ b/packages/adapter-netlify/test/apps/basic/src/routes/greeting/[name]/+page.svelte @@ -0,0 +1,5 @@ + + +

Hello {data.name}

diff --git a/packages/adapter-netlify/test/apps/basic/svelte.config.js b/packages/adapter-netlify/test/apps/basic/svelte.config.js deleted file mode 100644 index 050579db13ba..000000000000 --- a/packages/adapter-netlify/test/apps/basic/svelte.config.js +++ /dev/null @@ -1,15 +0,0 @@ -import adapter from '../../../index.js'; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - kit: { - adapter: adapter(), - experimental: { - instrumentation: { - server: true - } - } - } -}; - -export default config; diff --git a/packages/adapter-netlify/test/apps/basic/test/test.js b/packages/adapter-netlify/test/apps/basic/test/test.js index 50329932eb04..8b60aa827bce 100644 --- a/packages/adapter-netlify/test/apps/basic/test/test.js +++ b/packages/adapter-netlify/test/apps/basic/test/test.js @@ -1,12 +1,24 @@ import fs from 'node:fs'; import path from 'node:path'; -import { fileURLToPath } from 'node:url'; import { expect, test } from '@playwright/test'; -const __dirname = path.dirname(fileURLToPath(import.meta.url)); +test('page renders', async ({ request }) => { + const response = await request.get('/'); + expect(response.status()).toBe(200); + expect(await response.text()).toContain('Hello from SvelteKit'); +}); + +test('dynamic route works', async ({ request }) => { + const response = await request.get('/greeting/world'); + expect(response.status()).toBe(200); + expect(await response.text()).toContain('Hello world'); +}); test('read from $app/server works', async ({ request }) => { - const content = fs.readFileSync(path.resolve(__dirname, '../src/routes/read/file.txt'), 'utf-8'); + const content = fs.readFileSync( + path.resolve(import.meta.dirname, '../src/routes/read/file.txt'), + 'utf-8' + ); const response = await request.get('/read'); expect(await response.text()).toBe(content); }); diff --git a/packages/adapter-netlify/test/apps/basic/tsconfig.json b/packages/adapter-netlify/test/apps/basic/tsconfig.json index c2db702f9bb1..0c2092500732 100644 --- a/packages/adapter-netlify/test/apps/basic/tsconfig.json +++ b/packages/adapter-netlify/test/apps/basic/tsconfig.json @@ -9,5 +9,5 @@ "sourceMap": true, "moduleResolution": "bundler" }, - "extends": "./.svelte-kit/tsconfig.json" + "extends": "$app/tsconfig" } diff --git a/packages/adapter-netlify/test/apps/basic/vite.config.ts b/packages/adapter-netlify/test/apps/basic/vite.config.ts index 72a307cc0e56..8c3bc9c5e4a7 100644 --- a/packages/adapter-netlify/test/apps/basic/vite.config.ts +++ b/packages/adapter-netlify/test/apps/basic/vite.config.ts @@ -1,11 +1,16 @@ import { sveltekit } from '@sveltejs/kit/vite'; import type { UserConfig } from 'vite'; +import adapter from '../../../index.js'; const config: UserConfig = { build: { minify: false }, - plugins: [sveltekit()] + plugins: [ + sveltekit({ + adapter: adapter() + }) + ] }; export default config; diff --git a/packages/adapter-netlify/test/apps/edge/.env b/packages/adapter-netlify/test/apps/edge/.env new file mode 100644 index 000000000000..80bd32f3e8a9 --- /dev/null +++ b/packages/adapter-netlify/test/apps/edge/.env @@ -0,0 +1 @@ +INSTRUMENTATION_ENV= diff --git a/packages/adapter-netlify/test/apps/edge/_headers b/packages/adapter-netlify/test/apps/edge/_headers new file mode 100644 index 000000000000..01c0aab8e046 --- /dev/null +++ b/packages/adapter-netlify/test/apps/edge/_headers @@ -0,0 +1,2 @@ +/custom-header-path + X-Custom-Header: test-value diff --git a/packages/adapter-netlify/test/apps/edge/_redirects b/packages/adapter-netlify/test/apps/edge/_redirects new file mode 100644 index 000000000000..fd5703a26e6a --- /dev/null +++ b/packages/adapter-netlify/test/apps/edge/_redirects @@ -0,0 +1 @@ +/redirect-me /greeting/redirected 301 diff --git a/packages/adapter-netlify/test/apps/edge/build.test.js b/packages/adapter-netlify/test/apps/edge/build.test.js new file mode 100644 index 000000000000..0803ca86a838 --- /dev/null +++ b/packages/adapter-netlify/test/apps/edge/build.test.js @@ -0,0 +1,37 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { test, expect } from 'vitest'; + +const config_path = path.resolve(import.meta.dirname, './.netlify/v1/config.json'); +const edge_function_path = path.resolve( + import.meta.dirname, + './.netlify/v1/edge-functions/sveltekit-render.js' +); + +test('configuration is included in the edge function module', () => { + const config = JSON.parse(fs.readFileSync(config_path, 'utf-8')); + const edge_function = fs.readFileSync(edge_function_path, 'utf-8'); + + expect(config).not.toHaveProperty('edge_functions'); + expect(edge_function).toContain('"name": "SvelteKit server"'); + expect(edge_function).toContain('"path": ["/*"]'); + expect(edge_function).toContain('"excludedPath":'); +}); + +test('_headers are copied to publish directory', () => { + const headers = fs.readFileSync(path.resolve(import.meta.dirname, './build/_headers'), 'utf-8'); + expect(headers).toContain('X-Custom-Header: test-value'); +}); + +test('_redirects are copied to publish directory', () => { + const redirects = fs.readFileSync( + path.resolve(import.meta.dirname, './build/_redirects'), + 'utf-8' + ); + expect(redirects).toContain('/redirect-me /greeting/redirected 301'); +}); + +test('treeshakes component from the server bundle if SSR is turned off', () => { + const edge_function = fs.readFileSync(edge_function_path, 'utf-8'); + expect(edge_function).not.toContain('this should never appear in the server bundle'); +}); diff --git a/packages/adapter-netlify/test/apps/edge/netlify.toml b/packages/adapter-netlify/test/apps/edge/netlify.toml deleted file mode 100644 index 0183026741cd..000000000000 --- a/packages/adapter-netlify/test/apps/edge/netlify.toml +++ /dev/null @@ -1,2 +0,0 @@ -[build] -publish = "build" diff --git a/packages/adapter-netlify/test/apps/edge/package.json b/packages/adapter-netlify/test/apps/edge/package.json index a8bed2c4f7bb..429898785187 100644 --- a/packages/adapter-netlify/test/apps/edge/package.json +++ b/packages/adapter-netlify/test/apps/edge/package.json @@ -6,14 +6,15 @@ "dev": "vite dev", "build": "vite build", "preview": "vite preview", - "prepare": "svelte-kit sync || echo ''", + "test:build": "pnpm build && vitest run build.test.js", "test:platform": "playwright test" }, "devDependencies": { "@sveltejs/kit": "workspace:^", "@sveltejs/vite-plugin-svelte": "catalog:", "svelte": "catalog:", - "vite": "catalog:" + "vite": "catalog:", + "vitest": "catalog:" }, "type": "module" } diff --git a/packages/adapter-netlify/test/apps/edge/src/env.js b/packages/adapter-netlify/test/apps/edge/src/env.js new file mode 100644 index 000000000000..1c80e69a6f89 --- /dev/null +++ b/packages/adapter-netlify/test/apps/edge/src/env.js @@ -0,0 +1,5 @@ +import { defineEnvVars } from '@sveltejs/kit/env'; + +export const variables = defineEnvVars({ + INSTRUMENTATION_ENV: { schema: (value) => value } +}); diff --git a/packages/adapter-netlify/test/apps/edge/src/instrumentation.server.js b/packages/adapter-netlify/test/apps/edge/src/instrumentation.server.js new file mode 100644 index 000000000000..0fc714ba3566 --- /dev/null +++ b/packages/adapter-netlify/test/apps/edge/src/instrumentation.server.js @@ -0,0 +1,3 @@ +import { INSTRUMENTATION_ENV } from '$app/env/private'; + +globalThis.__INSTRUMENTATION_ENV_LOADED__ = INSTRUMENTATION_ENV === 'platform'; diff --git a/packages/adapter-netlify/test/apps/edge/src/routes/+page.svelte b/packages/adapter-netlify/test/apps/edge/src/routes/+page.svelte new file mode 100644 index 000000000000..03c350d48255 --- /dev/null +++ b/packages/adapter-netlify/test/apps/edge/src/routes/+page.svelte @@ -0,0 +1 @@ +

Hello from SvelteKit

diff --git a/packages/adapter-netlify/test/apps/edge/src/routes/greeting/[name]/+page.server.js b/packages/adapter-netlify/test/apps/edge/src/routes/greeting/[name]/+page.server.js new file mode 100644 index 000000000000..31e005a89bad --- /dev/null +++ b/packages/adapter-netlify/test/apps/edge/src/routes/greeting/[name]/+page.server.js @@ -0,0 +1,4 @@ +/** @type {import('./$types').PageServerLoad} */ +export function load({ params }) { + return { name: params.name }; +} diff --git a/packages/adapter-netlify/test/apps/edge/src/routes/greeting/[name]/+page.svelte b/packages/adapter-netlify/test/apps/edge/src/routes/greeting/[name]/+page.svelte new file mode 100644 index 000000000000..4dcbba568369 --- /dev/null +++ b/packages/adapter-netlify/test/apps/edge/src/routes/greeting/[name]/+page.svelte @@ -0,0 +1,5 @@ + + +

Hello {data.name}

diff --git a/packages/adapter-netlify/test/apps/edge/src/routes/instrumentation-env/+server.js b/packages/adapter-netlify/test/apps/edge/src/routes/instrumentation-env/+server.js new file mode 100644 index 000000000000..d374e62053ef --- /dev/null +++ b/packages/adapter-netlify/test/apps/edge/src/routes/instrumentation-env/+server.js @@ -0,0 +1,3 @@ +export function GET() { + return Response.json({ loaded: globalThis.__INSTRUMENTATION_ENV_LOADED__ === true }); +} diff --git a/packages/adapter-netlify/test/apps/edge/svelte.config.js b/packages/adapter-netlify/test/apps/edge/svelte.config.js deleted file mode 100644 index c633196b8ea8..000000000000 --- a/packages/adapter-netlify/test/apps/edge/svelte.config.js +++ /dev/null @@ -1,12 +0,0 @@ -import adapter from '../../../index.js'; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - kit: { - adapter: adapter({ - edge: true - }) - } -}; - -export default config; diff --git a/packages/adapter-netlify/test/apps/edge/test/test.js b/packages/adapter-netlify/test/apps/edge/test/test.js index aca60b059dee..35c80a26e652 100644 --- a/packages/adapter-netlify/test/apps/edge/test/test.js +++ b/packages/adapter-netlify/test/apps/edge/test/test.js @@ -1,24 +1,35 @@ import fs from 'node:fs'; import path from 'node:path'; -import { fileURLToPath } from 'node:url'; import { expect, test } from '@playwright/test'; -// TODO: use import.meta.dirname in Kit 3 -const __dirname = path.dirname(fileURLToPath(import.meta.url)); +test('page renders', async ({ request }) => { + const response = await request.get('/'); + expect(response.status()).toBe(200); + expect(await response.text()).toContain('Hello from SvelteKit'); +}); + +test('dynamic route works', async ({ request }) => { + const response = await request.get('/greeting/world'); + expect(response.status()).toBe(200); + expect(await response.text()).toContain('Hello world'); +}); + +test('dynamic env is available in instrumentation', async ({ request }) => { + const response = await request.get('/instrumentation-env'); + expect(response.status()).toBe(200); + expect(await response.json()).toEqual({ loaded: true }); +}); test('read from $app/server works', async ({ request }) => { - const content = fs.readFileSync(path.resolve(__dirname, '../src/routes/read/file.txt'), 'utf-8'); + const content = fs.readFileSync( + path.resolve(import.meta.dirname, '../src/routes/read/file.txt'), + 'utf-8' + ); const response = await request.get('/read'); expect(await response.text()).toBe(content); }); -test('treeshakes component from the server bundle if SSR is turned off', async ({ page }) => { +test('page renders on the client if SSR is turned off', async ({ page }) => { await page.goto('/treeshake-server'); - const component_text = 'this should never appear in the server bundle'; - const server_bundle = fs.readFileSync( - path.resolve(__dirname, '../.netlify/edge-functions/render.js'), - 'utf-8' - ); - expect(server_bundle).not.toContain(component_text); - await expect(page.locator('p')).toHaveText(component_text); + await expect(page.locator('p')).toHaveText('this should never appear in the server bundle'); }); diff --git a/packages/adapter-netlify/test/apps/edge/tsconfig.json b/packages/adapter-netlify/test/apps/edge/tsconfig.json index c2db702f9bb1..0c2092500732 100644 --- a/packages/adapter-netlify/test/apps/edge/tsconfig.json +++ b/packages/adapter-netlify/test/apps/edge/tsconfig.json @@ -9,5 +9,5 @@ "sourceMap": true, "moduleResolution": "bundler" }, - "extends": "./.svelte-kit/tsconfig.json" + "extends": "$app/tsconfig" } diff --git a/packages/adapter-netlify/test/apps/edge/vite.config.ts b/packages/adapter-netlify/test/apps/edge/vite.config.ts index 72a307cc0e56..8d97a74850b9 100644 --- a/packages/adapter-netlify/test/apps/edge/vite.config.ts +++ b/packages/adapter-netlify/test/apps/edge/vite.config.ts @@ -1,11 +1,18 @@ import { sveltekit } from '@sveltejs/kit/vite'; import type { UserConfig } from 'vite'; +import adapter from '../../../index.js'; const config: UserConfig = { build: { minify: false }, - plugins: [sveltekit()] + plugins: [ + sveltekit({ + adapter: adapter({ + edge: true + }) + }) + ] }; export default config; diff --git a/packages/adapter-netlify/test/apps/instrumentation/.env b/packages/adapter-netlify/test/apps/instrumentation/.env new file mode 100644 index 000000000000..80bd32f3e8a9 --- /dev/null +++ b/packages/adapter-netlify/test/apps/instrumentation/.env @@ -0,0 +1 @@ +INSTRUMENTATION_ENV= diff --git a/packages/adapter-netlify/test/apps/instrumentation/.gitignore b/packages/adapter-netlify/test/apps/instrumentation/.gitignore new file mode 100644 index 000000000000..88f661c765c6 --- /dev/null +++ b/packages/adapter-netlify/test/apps/instrumentation/.gitignore @@ -0,0 +1,6 @@ +.DS_Store +node_modules +/.svelte-kit +/.netlify +/build +deno.lock \ No newline at end of file diff --git a/packages/adapter-netlify/test/apps/instrumentation/package.json b/packages/adapter-netlify/test/apps/instrumentation/package.json new file mode 100644 index 000000000000..f234bc41a823 --- /dev/null +++ b/packages/adapter-netlify/test/apps/instrumentation/package.json @@ -0,0 +1,18 @@ +{ + "name": "test-netlify-instrumentation", + "version": "0.0.1", + "private": true, + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "test:platform": "playwright test" + }, + "devDependencies": { + "@sveltejs/kit": "workspace:^", + "@sveltejs/vite-plugin-svelte": "catalog:", + "svelte": "catalog:", + "vite": "catalog:" + }, + "type": "module" +} diff --git a/packages/adapter-netlify/test/apps/instrumentation/playwright.config.js b/packages/adapter-netlify/test/apps/instrumentation/playwright.config.js new file mode 100644 index 000000000000..33d36b651014 --- /dev/null +++ b/packages/adapter-netlify/test/apps/instrumentation/playwright.config.js @@ -0,0 +1 @@ +export { config as default } from '../../utils.js'; diff --git a/packages/adapter-netlify/test/apps/instrumentation/src/app.html b/packages/adapter-netlify/test/apps/instrumentation/src/app.html new file mode 100644 index 000000000000..26bc25de4e52 --- /dev/null +++ b/packages/adapter-netlify/test/apps/instrumentation/src/app.html @@ -0,0 +1,12 @@ + + + + + + %sveltekit.head% + + + +
%sveltekit.body%
+ + diff --git a/packages/adapter-netlify/test/apps/instrumentation/src/env.js b/packages/adapter-netlify/test/apps/instrumentation/src/env.js new file mode 100644 index 000000000000..13fdd66c636b --- /dev/null +++ b/packages/adapter-netlify/test/apps/instrumentation/src/env.js @@ -0,0 +1,5 @@ +import { defineEnvVars } from '@sveltejs/kit/env'; + +export const variables = defineEnvVars({ + INSTRUMENTATION_ENV: {} +}); diff --git a/packages/adapter-netlify/test/apps/instrumentation/src/instrumentation.server.js b/packages/adapter-netlify/test/apps/instrumentation/src/instrumentation.server.js new file mode 100644 index 000000000000..9f13c110c009 --- /dev/null +++ b/packages/adapter-netlify/test/apps/instrumentation/src/instrumentation.server.js @@ -0,0 +1,4 @@ +import { INSTRUMENTATION_ENV } from '$app/env/private'; + +globalThis.__INSTRUMENTATION_RAN__ = true; +globalThis.__INSTRUMENTATION_ENV_LOADED__ = INSTRUMENTATION_ENV === 'platform'; diff --git a/packages/adapter-netlify/test/apps/instrumentation/src/routes/+page.svelte b/packages/adapter-netlify/test/apps/instrumentation/src/routes/+page.svelte new file mode 100644 index 000000000000..03c350d48255 --- /dev/null +++ b/packages/adapter-netlify/test/apps/instrumentation/src/routes/+page.svelte @@ -0,0 +1 @@ +

Hello from SvelteKit

diff --git a/packages/adapter-netlify/test/apps/instrumentation/src/routes/instrumented/+server.js b/packages/adapter-netlify/test/apps/instrumentation/src/routes/instrumented/+server.js new file mode 100644 index 000000000000..ea9b7e9fc456 --- /dev/null +++ b/packages/adapter-netlify/test/apps/instrumentation/src/routes/instrumented/+server.js @@ -0,0 +1,6 @@ +export function GET() { + return Response.json({ + ran: globalThis.__INSTRUMENTATION_RAN__ === true, + env: globalThis.__INSTRUMENTATION_ENV_LOADED__ === true + }); +} diff --git a/packages/adapter-netlify/test/apps/instrumentation/test/test.js b/packages/adapter-netlify/test/apps/instrumentation/test/test.js new file mode 100644 index 000000000000..c98780855a82 --- /dev/null +++ b/packages/adapter-netlify/test/apps/instrumentation/test/test.js @@ -0,0 +1,13 @@ +import { expect, test } from '@playwright/test'; + +test('page renders', async ({ request }) => { + const response = await request.get('/'); + expect(response.status()).toBe(200); + expect(await response.text()).toContain('Hello from SvelteKit'); +}); + +test('instrumentation.server.js runs at startup', async ({ request }) => { + const response = await request.get('/instrumented'); + expect(response.status()).toBe(200); + expect(await response.json()).toEqual({ ran: true, env: true }); +}); diff --git a/packages/adapter-netlify/test/apps/instrumentation/tsconfig.json b/packages/adapter-netlify/test/apps/instrumentation/tsconfig.json new file mode 100644 index 000000000000..12550521f550 --- /dev/null +++ b/packages/adapter-netlify/test/apps/instrumentation/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + }, + "extends": "$app/tsconfig" +} diff --git a/packages/adapter-netlify/test/apps/instrumentation/vite.config.ts b/packages/adapter-netlify/test/apps/instrumentation/vite.config.ts new file mode 100644 index 000000000000..8c3bc9c5e4a7 --- /dev/null +++ b/packages/adapter-netlify/test/apps/instrumentation/vite.config.ts @@ -0,0 +1,16 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import type { UserConfig } from 'vite'; +import adapter from '../../../index.js'; + +const config: UserConfig = { + build: { + minify: false + }, + plugins: [ + sveltekit({ + adapter: adapter() + }) + ] +}; + +export default config; diff --git a/packages/adapter-netlify/test/apps/split/.gitignore b/packages/adapter-netlify/test/apps/split/.gitignore index 88f661c765c6..4150a3674e3a 100644 --- a/packages/adapter-netlify/test/apps/split/.gitignore +++ b/packages/adapter-netlify/test/apps/split/.gitignore @@ -3,4 +3,4 @@ node_modules /.svelte-kit /.netlify /build -deno.lock \ No newline at end of file +deno.lock diff --git a/packages/adapter-netlify/test/apps/split/_redirects b/packages/adapter-netlify/test/apps/split/_redirects new file mode 100644 index 000000000000..fd5703a26e6a --- /dev/null +++ b/packages/adapter-netlify/test/apps/split/_redirects @@ -0,0 +1 @@ +/redirect-me /greeting/redirected 301 diff --git a/packages/adapter-netlify/test/apps/split/build.test.js b/packages/adapter-netlify/test/apps/split/build.test.js new file mode 100644 index 000000000000..21fdcd24c0fd --- /dev/null +++ b/packages/adapter-netlify/test/apps/split/build.test.js @@ -0,0 +1,62 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import process from 'node:process'; +import { test, expect } from 'vitest'; + +test('_redirects are copied to publish directory', () => { + const redirects = fs.readFileSync( + path.resolve(import.meta.dirname, './build/_redirects'), + 'utf-8' + ); + expect(redirects).toContain('/redirect-me /greeting/redirected 301'); +}); + +const edge = process.env.EDGE === 'true'; +const functions_dir = path.resolve( + import.meta.dirname, + edge ? './.netlify/v1/edge-functions' : './.netlify/v1/functions' +); + +/** @param {(content: string) => boolean} filter */ +function read_functions(filter) { + return fs + .readdirSync(functions_dir) + .filter((f) => f.startsWith('sveltekit-') && f.endsWith(edge ? '.js' : '.mjs')) + .map((f) => fs.readFileSync(path.join(functions_dir, f), 'utf-8')) + .filter(filter); +} + +test('split generates multiple function files', () => { + const functions = read_functions((content) => + content.includes('"name": "SvelteKit /collection/[[optional]]/article"') + ); + expect(functions.length).toBe(1); + expect(functions[0]).toMatch( + /"path": \[\s*"\/collection\/:param1\?\/article",\s*"\/collection\/:param1\?\/article\/__data\.json"\s*\]/ + ); +}); + +test('functions have human friendly display names', () => { + const functions = read_functions((content) => + content.includes('"name": "SvelteKit /collection/[[optional]]/article"') + ); + expect(functions[0]).toContain('"name": "SvelteKit /collection/[[optional]]/article"'); + + const catch_all = read_functions((content) => content.includes('"name": "SvelteKit catch-all"')); + expect(catch_all.length).toBe(1); + expect(catch_all[0]).toContain('"name": "SvelteKit catch-all"'); +}); + +test('routes that sanitize to the same pattern generate separate functions', () => { + const foo_dot = read_functions((content) => + content.includes('"name": "SvelteKit /collision/foo.bar"') + ); + const foo_bar = read_functions((content) => + content.includes('"name": "SvelteKit /collision/foo_bar"') + ); + + expect(foo_dot.length).toBe(1); + expect(foo_bar.length).toBe(1); + expect(foo_dot[0]).toContain('"name": "SvelteKit /collision/foo.bar"'); + expect(foo_bar[0]).toContain('"name": "SvelteKit /collision/foo_bar"'); +}); diff --git a/packages/adapter-netlify/test/apps/split/netlify.toml b/packages/adapter-netlify/test/apps/split/netlify.toml deleted file mode 100644 index 0183026741cd..000000000000 --- a/packages/adapter-netlify/test/apps/split/netlify.toml +++ /dev/null @@ -1,2 +0,0 @@ -[build] -publish = "build" diff --git a/packages/adapter-netlify/test/apps/split/package.json b/packages/adapter-netlify/test/apps/split/package.json index 2af2c95a4b18..fb573e2eaf6c 100644 --- a/packages/adapter-netlify/test/apps/split/package.json +++ b/packages/adapter-netlify/test/apps/split/package.json @@ -6,14 +6,17 @@ "dev": "vite dev", "build": "vite build", "preview": "vite preview", - "prepare": "svelte-kit sync || echo ''", + "test:serverless:build": "pnpm build && vitest run build.test.js", + "test:edge:build": "EDGE=true pnpm build && EDGE=true vitest run build.test.js", + "test:build": "pnpm test:serverless:build && pnpm test:edge:build", "test:platform": "playwright test" }, "devDependencies": { "@sveltejs/kit": "workspace:^", "@sveltejs/vite-plugin-svelte": "catalog:", "svelte": "catalog:", - "vite": "catalog:" + "vite": "catalog:", + "vitest": "catalog:" }, "type": "module" } diff --git a/packages/adapter-netlify/test/apps/split/src/hooks.js b/packages/adapter-netlify/test/apps/split/src/hooks.js new file mode 100644 index 000000000000..cb8d0e6e71e4 --- /dev/null +++ b/packages/adapter-netlify/test/apps/split/src/hooks.js @@ -0,0 +1,5 @@ +export function reroute({ url }) { + if (url.pathname.endsWith('/reroute')) { + return '/reroute'; + } +} diff --git a/packages/adapter-netlify/test/apps/split/src/instrumentation.server.js b/packages/adapter-netlify/test/apps/split/src/instrumentation.server.js deleted file mode 100644 index acc9022e1d64..000000000000 --- a/packages/adapter-netlify/test/apps/split/src/instrumentation.server.js +++ /dev/null @@ -1 +0,0 @@ -// this is just here to make sure the changes resulting from it work diff --git a/packages/adapter-netlify/test/apps/split/src/routes/+page.svelte b/packages/adapter-netlify/test/apps/split/src/routes/+page.svelte new file mode 100644 index 000000000000..03c350d48255 --- /dev/null +++ b/packages/adapter-netlify/test/apps/split/src/routes/+page.svelte @@ -0,0 +1 @@ +

Hello from SvelteKit

diff --git a/packages/adapter-netlify/test/apps/split/src/routes/collection/[[optional]]/article/+page.server.js b/packages/adapter-netlify/test/apps/split/src/routes/collection/[[optional]]/article/+page.server.js new file mode 100644 index 000000000000..0e38a0aa6b4a --- /dev/null +++ b/packages/adapter-netlify/test/apps/split/src/routes/collection/[[optional]]/article/+page.server.js @@ -0,0 +1,4 @@ +/** @type {import('./$types').PageServerLoad} */ +export function load({ params }) { + return { optional: params.optional ?? null }; +} diff --git a/packages/adapter-netlify/test/apps/split/src/routes/collection/[[optional]]/article/+page.svelte b/packages/adapter-netlify/test/apps/split/src/routes/collection/[[optional]]/article/+page.svelte new file mode 100644 index 000000000000..cab0e751c806 --- /dev/null +++ b/packages/adapter-netlify/test/apps/split/src/routes/collection/[[optional]]/article/+page.svelte @@ -0,0 +1,6 @@ + + + +

optional: {data.optional ?? 'none'}

diff --git a/packages/adapter-netlify/test/apps/split/src/routes/collision/foo.bar/+server.js b/packages/adapter-netlify/test/apps/split/src/routes/collision/foo.bar/+server.js new file mode 100644 index 000000000000..df3948a1e79b --- /dev/null +++ b/packages/adapter-netlify/test/apps/split/src/routes/collision/foo.bar/+server.js @@ -0,0 +1,3 @@ +export function GET() { + return new Response('static'); +} diff --git a/packages/adapter-netlify/test/apps/split/src/routes/collision/foo_bar/+server.js b/packages/adapter-netlify/test/apps/split/src/routes/collision/foo_bar/+server.js new file mode 100644 index 000000000000..2bf46d906266 --- /dev/null +++ b/packages/adapter-netlify/test/apps/split/src/routes/collision/foo_bar/+server.js @@ -0,0 +1,3 @@ +export function GET() { + return new Response('underscore'); +} diff --git a/packages/adapter-netlify/test/apps/split/src/routes/greeting/[name]/+page.server.js b/packages/adapter-netlify/test/apps/split/src/routes/greeting/[name]/+page.server.js new file mode 100644 index 000000000000..31e005a89bad --- /dev/null +++ b/packages/adapter-netlify/test/apps/split/src/routes/greeting/[name]/+page.server.js @@ -0,0 +1,4 @@ +/** @type {import('./$types').PageServerLoad} */ +export function load({ params }) { + return { name: params.name }; +} diff --git a/packages/adapter-netlify/test/apps/split/src/routes/greeting/[name]/+page.svelte b/packages/adapter-netlify/test/apps/split/src/routes/greeting/[name]/+page.svelte new file mode 100644 index 000000000000..4dcbba568369 --- /dev/null +++ b/packages/adapter-netlify/test/apps/split/src/routes/greeting/[name]/+page.svelte @@ -0,0 +1,5 @@ + + +

Hello {data.name}

diff --git a/packages/adapter-netlify/test/apps/split/src/routes/reroute/+page.js b/packages/adapter-netlify/test/apps/split/src/routes/reroute/+page.js new file mode 100644 index 000000000000..ffe2c039a6c8 --- /dev/null +++ b/packages/adapter-netlify/test/apps/split/src/routes/reroute/+page.js @@ -0,0 +1,2 @@ +// disable js so we can verify the url as the server rendered it +export const csr = false; diff --git a/packages/adapter-netlify/test/apps/split/src/routes/reroute/+page.svelte b/packages/adapter-netlify/test/apps/split/src/routes/reroute/+page.svelte new file mode 100644 index 000000000000..a728c1562500 --- /dev/null +++ b/packages/adapter-netlify/test/apps/split/src/routes/reroute/+page.svelte @@ -0,0 +1,5 @@ + + +

{page.url.pathname + page.url.search}

diff --git a/packages/adapter-netlify/test/apps/split/svelte.config.js b/packages/adapter-netlify/test/apps/split/svelte.config.js deleted file mode 100644 index 82a386bcfc2e..000000000000 --- a/packages/adapter-netlify/test/apps/split/svelte.config.js +++ /dev/null @@ -1,17 +0,0 @@ -import adapter from '../../../index.js'; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - compilerOptions: { experimental: { async: true } }, - kit: { - adapter: adapter({ split: true }), - experimental: { - instrumentation: { - server: true - }, - remoteFunctions: true - } - } -}; - -export default config; diff --git a/packages/adapter-netlify/test/apps/split/test/test.js b/packages/adapter-netlify/test/apps/split/test/test.js index 1e502c5c559a..0553af31e579 100644 --- a/packages/adapter-netlify/test/apps/split/test/test.js +++ b/packages/adapter-netlify/test/apps/split/test/test.js @@ -1,10 +1,20 @@ +import fs from 'node:fs'; +import path from 'node:path'; import { expect, test } from '@playwright/test'; -test('routes to routes with dynamic params', async ({ page }) => { +test('routes with dynamic params', async ({ page }) => { await page.goto('/dynamic/123'); await expect(page.locator('p')).toHaveText('id: 123'); }); +test('routes with optional params', async ({ page }) => { + await page.goto('/collection/article'); + await expect(page.locator('p')).toHaveText('optional: none'); + + await page.goto('/collection/value/article'); + await expect(page.locator('p')).toHaveText('optional: value'); +}); + test('client-side navigation fetches server load function data', async ({ page }) => { await page.goto('/dynamic'); await page.click('a'); @@ -20,3 +30,15 @@ test('client-side fetch for query remote function data', async ({ page }) => { await page.goto('/remote/query'); await expect(page.locator('p')).toHaveText('a: 1'); }); + +test('reroute works', async ({ page }) => { + await page.goto('/reroute'); + await expect(page.locator('p')).toContainText('/reroute'); + await page.goto('/en/reroute?hello=world'); + await expect(page.locator('p')).toContainText('/en/reroute?hello=world'); + await expect(page.locator('p')).not.toContainText('__sveltekit_original_pathname_'); +}); + +// TODO: test remote function works + +// TODO: test preloadCode works diff --git a/packages/adapter-netlify/test/apps/split/tsconfig.json b/packages/adapter-netlify/test/apps/split/tsconfig.json index c2db702f9bb1..0c2092500732 100644 --- a/packages/adapter-netlify/test/apps/split/tsconfig.json +++ b/packages/adapter-netlify/test/apps/split/tsconfig.json @@ -9,5 +9,5 @@ "sourceMap": true, "moduleResolution": "bundler" }, - "extends": "./.svelte-kit/tsconfig.json" + "extends": "$app/tsconfig" } diff --git a/packages/adapter-netlify/test/apps/split/vite.config.ts b/packages/adapter-netlify/test/apps/split/vite.config.ts index 72a307cc0e56..9920592a3047 100644 --- a/packages/adapter-netlify/test/apps/split/vite.config.ts +++ b/packages/adapter-netlify/test/apps/split/vite.config.ts @@ -1,11 +1,20 @@ import { sveltekit } from '@sveltejs/kit/vite'; import type { UserConfig } from 'vite'; +import adapter from '../../../index.js'; const config: UserConfig = { build: { minify: false }, - plugins: [sveltekit()] + plugins: [ + sveltekit({ + adapter: adapter({ split: true, edge: process.env.EDGE === 'true' }), + compilerOptions: { experimental: { async: true } }, + experimental: { + remoteFunctions: true + } + }) + ] }; export default config; diff --git a/packages/adapter-netlify/test/utils.js b/packages/adapter-netlify/test/utils.js index 18c9c56d89d7..b5bdbb0a952e 100644 --- a/packages/adapter-netlify/test/utils.js +++ b/packages/adapter-netlify/test/utils.js @@ -16,7 +16,7 @@ export const config = { baseURL: process.env.DEPLOYMENT_URL, screenshot: 'only-on-failure', trace: 'retain-on-failure', - channel: 'chromium' + channel: 'chrome' }, workers: process.env.CI ? 2 : undefined, reporter: 'list', diff --git a/packages/adapter-netlify/tsconfig.json b/packages/adapter-netlify/tsconfig.json index 6a110982ab66..738cc889eac6 100644 --- a/packages/adapter-netlify/tsconfig.json +++ b/packages/adapter-netlify/tsconfig.json @@ -11,5 +11,5 @@ }, "types": ["node"] }, - "include": ["*.js", "src/**/*.js", "internal.d.ts", "test/utils.js"] + "include": ["*.js", "files/serverless.js", "test/utils.js"] } diff --git a/packages/adapter-netlify/utils.js b/packages/adapter-netlify/utils.js new file mode 100644 index 000000000000..bd383e790497 --- /dev/null +++ b/packages/adapter-netlify/utils.js @@ -0,0 +1,43 @@ +/** + * @typedef {{ rest: boolean, dynamic: boolean, content: string }} RouteSegment + */ + +/** + * @param {RouteSegment[]} a + * @param {RouteSegment[]} b + * @returns {boolean} + */ +export function matches(a, b) { + if (a[0] && b[0]) { + if (b[0].rest) { + if (b.length === 1) return true; + + const next_b = b.slice(1); + + for (let i = 0; i < a.length; i += 1) { + if (matches(a.slice(i), next_b)) return true; + } + + return false; + } + + if (!b[0].dynamic) { + if (!a[0].dynamic && a[0].content !== b[0].content) return false; + } + + if (a.length === 1 && b.length === 1) return true; + return matches(a.slice(1), b.slice(1)); + } else if (a[0]) { + return a.length === 1 && a[0].rest; + } else { + return b.length === 1 && b[0].rest; + } +} + +/** + * @param {*} value + * @returns {string} + */ +export function s(value) { + return JSON.stringify(value, null, '\t'); +} diff --git a/packages/adapter-netlify/utils.spec.js b/packages/adapter-netlify/utils.spec.js new file mode 100644 index 000000000000..a04bc1415c6b --- /dev/null +++ b/packages/adapter-netlify/utils.spec.js @@ -0,0 +1,120 @@ +import { describe, test, expect } from 'vitest'; +import { matches } from './utils.js'; + +/** + * Helper to create a static route segment + * @param {string} content + * @returns {{ rest: boolean, dynamic: boolean, content: string }} + */ +function static_segment(content) { + return { rest: false, dynamic: false, content }; +} + +/** + * Helper to create a dynamic route segment + * @param {string} content + * @returns {{ rest: boolean, dynamic: boolean, content: string }} + */ +function dynamic_segment(content) { + return { rest: false, dynamic: true, content }; +} + +/** + * Helper to create a rest route segment + * @param {string} content + * @returns {{ rest: boolean, dynamic: boolean, content: string }} + */ +function rest_segment(content) { + return { rest: true, dynamic: true, content }; +} + +describe('matches', () => { + test('two identical static routes match', () => { + expect( + matches( + [static_segment('blog'), static_segment('post')], + [static_segment('blog'), static_segment('post')] + ) + ).toBe(true); + }); + + test('static segment mismatch returns false', () => { + expect(matches([static_segment('blog')], [static_segment('about')])).toBe(false); + }); + + test('dynamic segment matches any static segment', () => { + expect(matches([static_segment('blog')], [dynamic_segment('[slug]')])).toBe(true); + }); + + test('rest segment at end matches everything', () => { + expect( + matches([static_segment('blog'), static_segment('post')], [rest_segment('[...rest]')]) + ).toBe(true); + }); + + test('rest-only route matches any route', () => { + expect( + matches( + [static_segment('a'), static_segment('b'), static_segment('c')], + [rest_segment('[...rest]')] + ) + ).toBe(true); + }); + + test('rest segment matches remaining segments', () => { + expect( + matches( + [static_segment('blog'), static_segment('2024'), static_segment('post')], + [static_segment('blog'), rest_segment('[...rest]')] + ) + ).toBe(true); + }); + + test('empty segments (index routes)', () => { + expect(matches([], [])).toBe(false); + }); + + test('mismatched lengths without rest return false', () => { + expect( + matches([static_segment('blog'), static_segment('post')], [static_segment('blog')]) + ).toBe(false); + }); + + test('rest with following segments', () => { + expect( + matches( + [static_segment('a'), static_segment('b'), static_segment('page')], + [rest_segment('[...rest]'), static_segment('page')] + ) + ).toBe(true); + }); + + test('rest with following segments that do not match', () => { + expect( + matches( + [static_segment('a'), static_segment('b'), static_segment('other')], + [rest_segment('[...rest]'), static_segment('page')] + ) + ).toBe(false); + }); + + test('a has trailing rest and b is shorter', () => { + expect(matches([rest_segment('[...rest]')], [static_segment('blog')])).toBe(true); + }); + + test('a is longer without rest in b', () => { + expect(matches([static_segment('a'), static_segment('b')], [static_segment('a')])).toBe(false); + }); + + test('b is longer without rest in a', () => { + expect(matches([static_segment('a')], [static_segment('a'), static_segment('b')])).toBe(false); + }); + + test('dynamic in a matches static in b', () => { + expect(matches([dynamic_segment('[id]')], [static_segment('blog')])).toBe(true); + }); + + test('both dynamic segments match', () => { + expect(matches([dynamic_segment('[id]')], [dynamic_segment('[slug]')])).toBe(true); + }); +}); diff --git a/packages/adapter-netlify/vitest.config.js b/packages/adapter-netlify/vitest.config.js new file mode 100644 index 000000000000..2d4073740e5d --- /dev/null +++ b/packages/adapter-netlify/vitest.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + include: ['*.spec.{js,ts}'] + } +}); diff --git a/packages/adapter-node/.gitignore b/packages/adapter-node/.gitignore index 22f507db66b4..a96ff58a5388 100644 --- a/packages/adapter-node/.gitignore +++ b/packages/adapter-node/.gitignore @@ -1,4 +1,3 @@ .DS_Store node_modules -/files coverage/ diff --git a/packages/adapter-node/CHANGELOG.md b/packages/adapter-node/CHANGELOG.md index 606f5ab44bbe..7b2bb4e6cb57 100644 --- a/packages/adapter-node/CHANGELOG.md +++ b/packages/adapter-node/CHANGELOG.md @@ -1,5 +1,202 @@ # @sveltejs/adapter-node +## 6.0.0-next.15 + +### Patch Changes + +- fix: bundle Svelte libraries listed in `dependencies` instead of externalising them ([#17234](https://github.com/sveltejs/kit/pull/17234)) +- Updated dependencies [[`f8a90be`](https://github.com/sveltejs/kit/commit/f8a90be73c61dee7d586b26d19dad59637de63b0), [`30d06f5`](https://github.com/sveltejs/kit/commit/30d06f5698528cf540899900366fc0cfeb3dc7f6), [`0107721`](https://github.com/sveltejs/kit/commit/0107721dbdde1310d4c5427f7ad33a2ee5593d3e)]: + - @sveltejs/kit@3.0.0-next.30 + +## 6.0.0-next.14 + +### Patch Changes + +- fix: always bundle dev dependencies ([#17210](https://github.com/sveltejs/kit/pull/17210)) +- Updated dependencies [[`058d29b`](https://github.com/sveltejs/kit/commit/058d29be2cbedb91546b6f6ddf157b881b87360b), [`5e11958`](https://github.com/sveltejs/kit/commit/5e119589bb8784cb43fd631afaa81b78b3ff6f36)]: + - @sveltejs/kit@3.0.0-next.29 + +## 6.0.0-next.13 + +### Major Changes + +- breaking: record the list of static assets at build time, files added to the output afterwards are not served ([#16908](https://github.com/sveltejs/kit/pull/16908)) + +- breaking: validate static assets with content-hash `ETag`s and stop sending `Last-Modified` ([#16908](https://github.com/sveltejs/kit/pull/16908)) + +- breaking: serve static assets only to `GET` and `HEAD` requests ([#16908](https://github.com/sveltejs/kit/pull/16908)) + +### Minor Changes + +- feat: serve static assets from tables recorded at adapt time, instead of using sirv ([#16908](https://github.com/sveltejs/kit/pull/16908)) + +### Patch Changes + +- chore: bundle the adapter's server source directly instead of prebuilding it with rolldown ([#17109](https://github.com/sveltejs/kit/pull/17109)) + +- chore: hand adapt-time values to the server as a module instead of patching built chunks ([#17103](https://github.com/sveltejs/kit/pull/17103)) + +- chore: avoid bundling the server twice ([#17068](https://github.com/sveltejs/kit/pull/17068)) +- Updated dependencies [[`abf47df`](https://github.com/sveltejs/kit/commit/abf47df887f914f09c39a7f771d32cdd55fb346d), [`df3f003`](https://github.com/sveltejs/kit/commit/df3f0034b541701bd3319225f4e6003579d4ebd6), [`3c044cd`](https://github.com/sveltejs/kit/commit/3c044cda5c356dede92167dcca549cd58e73e61f), [`509dd14`](https://github.com/sveltejs/kit/commit/509dd146696b2c77ae3ac8ac2435773864520016), [`5d8722c`](https://github.com/sveltejs/kit/commit/5d8722c60977efb6e4606fefd9750c2349af410a), [`462111a`](https://github.com/sveltejs/kit/commit/462111a1411551386c6e1aa6c14f967f4ae875c2), [`9f3342f`](https://github.com/sveltejs/kit/commit/9f3342fb9305d1fd731a49497bec894469073fd0), [`224e791`](https://github.com/sveltejs/kit/commit/224e791cb05f95a5c2d881e5a7d5341352f3c052), [`8614b01`](https://github.com/sveltejs/kit/commit/8614b016e3be4247cb1aae615870f4c23a0622b3), [`65f93c8`](https://github.com/sveltejs/kit/commit/65f93c83b29f47ebc05bfdeac53f8c92c8d522e1), [`147f09a`](https://github.com/sveltejs/kit/commit/147f09ab7397727f17b5f35ff7535eb1a089180f), [`09b8de8`](https://github.com/sveltejs/kit/commit/09b8de849561b5c81112dbe029e9279e8184903c), [`68c6ef9`](https://github.com/sveltejs/kit/commit/68c6ef97fad9baf6c883e203b1ceb686e2ca74a1), [`377fc5d`](https://github.com/sveltejs/kit/commit/377fc5dd93054ba01ec0e9623036735ff7aadfb5), [`84cb5fd`](https://github.com/sveltejs/kit/commit/84cb5fd2173028e29edf87bcbfc551f34dc2e1a9), [`99492ac`](https://github.com/sveltejs/kit/commit/99492ac605a74e066623d8934fa9f662d4350c99), [`4ac8c9b`](https://github.com/sveltejs/kit/commit/4ac8c9b4b9f015769f81adb941c85cf42fc17424), [`8efeeb3`](https://github.com/sveltejs/kit/commit/8efeeb3dea17c6dd3a1cdf55fc25ba0a53e6ee4c), [`4c812c8`](https://github.com/sveltejs/kit/commit/4c812c808c6b62c78b89fb0021dcfe45e06afcc7), [`fbbb4c7`](https://github.com/sveltejs/kit/commit/fbbb4c7d3c4ff3a3792a58db26c6be77996a392d), [`c6ba86b`](https://github.com/sveltejs/kit/commit/c6ba86ba7983a53f0d1e16c0a6462009d5245d4d), [`7847e4e`](https://github.com/sveltejs/kit/commit/7847e4e27f17a0a94b29f88d79050d704436957c), [`68791be`](https://github.com/sveltejs/kit/commit/68791be662e01afd7235e1eb6a2b6b7acf332b17), [`f8258ee`](https://github.com/sveltejs/kit/commit/f8258ee5e042d48d198cca3f35982e90db0077cb), [`4da6320`](https://github.com/sveltejs/kit/commit/4da6320db8d70b73b93e142e4bb6ba9173be3b97), [`b21766f`](https://github.com/sveltejs/kit/commit/b21766f362e39f51cb4ae8f894ea5aca40cf305e), [`a2bfcaf`](https://github.com/sveltejs/kit/commit/a2bfcafbe496e23b1e0bcfce442309bb94783efd), [`680405d`](https://github.com/sveltejs/kit/commit/680405d2d06f02774a8d7df5351df4a0d259c338), [`755024a`](https://github.com/sveltejs/kit/commit/755024a1af3ffcb80cdf939b2cf50ada3a2cb684), [`3a7d329`](https://github.com/sveltejs/kit/commit/3a7d3290e28dffa730d0c71699906dbc6d2fd584)]: + - @sveltejs/kit@3.0.0-next.28 + +## 6.0.0-next.12 + +### Major Changes + +- breaking: populate env vars before `instrumentation.server.js` is evaluated and update the adapter instrumentation API ([#16303](https://github.com/sveltejs/kit/pull/16303)) + +- breaking: replace the `builder.generateManifest` with `builder.generateServerInstance` and `builder.manifest` ([#16875](https://github.com/sveltejs/kit/pull/16875)) + +### Patch Changes + +- Updated dependencies [[`9a4b343`](https://github.com/sveltejs/kit/commit/9a4b343bd8ab0060dc624fbdd139155fc4a0a1dc), [`ee2f55b`](https://github.com/sveltejs/kit/commit/ee2f55b1e3338d2fa92a25fe53d095129edf1002), [`65673a4`](https://github.com/sveltejs/kit/commit/65673a4dd35054c16f8555eb896dfb773d86a556), [`1b79845`](https://github.com/sveltejs/kit/commit/1b79845e117f417b63fa399149816d73882c27ea), [`b12bb78`](https://github.com/sveltejs/kit/commit/b12bb789c17c30e7ff835de4af004d2f3eb36159), [`03f1687`](https://github.com/sveltejs/kit/commit/03f1687fe612ce3d2d9131139b5b188d9cf90c64), [`49f0808`](https://github.com/sveltejs/kit/commit/49f0808f3e983d0cb5a4d586cf0d1678467431ed), [`e7c74bf`](https://github.com/sveltejs/kit/commit/e7c74bf8227dc228cb36e822eaff5992569b830a), [`0876d63`](https://github.com/sveltejs/kit/commit/0876d633145088c7a5213e9f530cf2fed96ce35d), [`9a2b9ba`](https://github.com/sveltejs/kit/commit/9a2b9bacbb2de0533966bb029ef526b357aac086), [`c9f54b5`](https://github.com/sveltejs/kit/commit/c9f54b5d66acd9443d36b0ae9aa0a656809bd7f9), [`22b8152`](https://github.com/sveltejs/kit/commit/22b8152f74882775ac63e1cf96d3016b92ac8256), [`f6d44cf`](https://github.com/sveltejs/kit/commit/f6d44cf007d6c6b54de57f6ae4ef8e2e7fea2f44), [`3c3a27b`](https://github.com/sveltejs/kit/commit/3c3a27bf67a26164266e132dbb7f9266643cf724), [`7f376b1`](https://github.com/sveltejs/kit/commit/7f376b109cd770638806184cf5d8bf4cf88c5bf2), [`8a587c7`](https://github.com/sveltejs/kit/commit/8a587c7b3919408ec66a362eb249fe5cf43f5316), [`aa4f960`](https://github.com/sveltejs/kit/commit/aa4f9605761e686f6aaf414b9186c68ddd2468dc)]: + - @sveltejs/kit@3.0.0-next.27 + +## 6.0.0-next.11 + +### Patch Changes + +- chore: remove polka, attach the handler to the http server directly ([#16907](https://github.com/sveltejs/kit/pull/16907)) +- Updated dependencies [[`ff8cdd4`](https://github.com/sveltejs/kit/commit/ff8cdd4df8f6a0ee26c25854aa0656f2dc968caf), [`723572c`](https://github.com/sveltejs/kit/commit/723572c76053ebcae02b19166fe7400f45175c70), [`3b8e034`](https://github.com/sveltejs/kit/commit/3b8e034d62f1372f0e450da88c0705bd9828a816), [`f2c5102`](https://github.com/sveltejs/kit/commit/f2c5102079c80c904b3fe165666813023248facc), [`c66a6ed`](https://github.com/sveltejs/kit/commit/c66a6ed5bb6ba4594b0a952744a1d9c7e457b001), [`428e5ef`](https://github.com/sveltejs/kit/commit/428e5efeedba49ccd42d43f85e53707f6704931c)]: + - @sveltejs/kit@3.0.0-next.26 + +## 6.0.0-next.10 + +### Patch Changes + +- chore: bump `rolldown` to 1.2.3 ([#16745](https://github.com/sveltejs/kit/pull/16745)) +- Updated dependencies [[`1742811`](https://github.com/sveltejs/kit/commit/1742811562358caf6042bed33c60ce6427a3e831), [`1611c61`](https://github.com/sveltejs/kit/commit/1611c617a1ec1b32e10627c2815fb7e6673f3157), [`b361b81`](https://github.com/sveltejs/kit/commit/b361b815e79828a6b7404d759d0a4c3892814a4e), [`b361b81`](https://github.com/sveltejs/kit/commit/b361b815e79828a6b7404d759d0a4c3892814a4e), [`b361b81`](https://github.com/sveltejs/kit/commit/b361b815e79828a6b7404d759d0a4c3892814a4e), [`b361b81`](https://github.com/sveltejs/kit/commit/b361b815e79828a6b7404d759d0a4c3892814a4e), [`13e7b18`](https://github.com/sveltejs/kit/commit/13e7b186a94b5b8317771d2b1e795b432dcd6faf), [`529346d`](https://github.com/sveltejs/kit/commit/529346d96369f92cdd804be9c4313c598efe34b8), [`b361b81`](https://github.com/sveltejs/kit/commit/b361b815e79828a6b7404d759d0a4c3892814a4e), [`81d6319`](https://github.com/sveltejs/kit/commit/81d6319eef08a765ccd3922790a7aee16e284cc2), [`b361b81`](https://github.com/sveltejs/kit/commit/b361b815e79828a6b7404d759d0a4c3892814a4e), [`69a5bdf`](https://github.com/sveltejs/kit/commit/69a5bdf9f45810eeed2faa089c8d3206463d347e)]: + - @sveltejs/kit@3.0.0-next.20 + +## 6.0.0-next.9 + +### Patch Changes + +- fix: externalize `@opentelemetry/api` to prevent bundler chunk colocation between `instrumentation.server.js` and application code ([#16302](https://github.com/sveltejs/kit/pull/16302)) +- Updated dependencies [[`04f9ab4`](https://github.com/sveltejs/kit/commit/04f9ab4f64a25c2fecac65ccd97cf4663eb9c1fa), [`1e198bd`](https://github.com/sveltejs/kit/commit/1e198bdbea745b845d9ae59e6a2d3fc62a404b01), [`813726d`](https://github.com/sveltejs/kit/commit/813726dabdc923ce138452dc8cc91d881e329e5d), [`a115a7b`](https://github.com/sveltejs/kit/commit/a115a7beb9b34d1e43609e96100de48010afc3be), [`031ac69`](https://github.com/sveltejs/kit/commit/031ac69d0d8c7d66c9e61aa43a8c447877eb87ff), [`dc7442c`](https://github.com/sveltejs/kit/commit/dc7442cc622c257e89a7acc9253795a449d51df7), [`19c4478`](https://github.com/sveltejs/kit/commit/19c447850e269807eeb089b595267a0616203afa), [`dc7442c`](https://github.com/sveltejs/kit/commit/dc7442cc622c257e89a7acc9253795a449d51df7), [`dc7442c`](https://github.com/sveltejs/kit/commit/dc7442cc622c257e89a7acc9253795a449d51df7), [`0b3e2b3`](https://github.com/sveltejs/kit/commit/0b3e2b386896cf868328138a66670f19931ebea5), [`c5d0ce2`](https://github.com/sveltejs/kit/commit/c5d0ce2a9e47615c245e8ac57e10ee2120142138)]: + - @sveltejs/kit@3.0.0-next.19 + +## 6.0.0-next.8 + +### Major Changes + +- chore: migrate from rollup to rolldown + +- breaking: require SvelteKit 3 + +- breaking: add `kit.paths.origin` config option, remove `kit.prerender.origin` and the `adapter-node` `ORIGIN` environment variable + +- breaking: require `vite@^8.0.12`, the first Vite 8 release bundling stable `rolldown` 1.0.0 + +### Minor Changes + +- feat: better error logging + +### Patch Changes + +- fix: serve static files with the Content-Type recorded in the manifest + +- chore: use `node:fs` instead of deprecated `builder.rimraf` and `builder.mkdirp` ([#16286](https://github.com/sveltejs/kit/pull/16286)) + +- fix: allow prerelease versions of SvelteKit 3 to satisfy the peer dependency range + +- fix: correctly bundle entrypoints on Windows + +- fix: preserve stripped path prefixes by making trailing-slash redirects relative + +- chore: bump Rolldown to `1.2.0` + +- fix: don't send `Vary: Accept-Encoding` for assets that were never precompressed +- Updated dependencies [[`bbd1e2f`](https://github.com/sveltejs/kit/commit/bbd1e2fcc9af58edb9846393b081be61df4789d5), [`e21d27b`](https://github.com/sveltejs/kit/commit/e21d27ba490c89da98dbad24fca364d0163137c4), [`6044d05`](https://github.com/sveltejs/kit/commit/6044d055031bc17deedc069dc4016565364bd4f4), [`7fb611f`](https://github.com/sveltejs/kit/commit/7fb611fb604e4d74cd889f5fe21ccd255f876382), [`9f06b3a`](https://github.com/sveltejs/kit/commit/9f06b3ac78be30d27b07fc00ea40bda493c573aa), [`8763327`](https://github.com/sveltejs/kit/commit/87633271ce40ac0839be9468598e965bde3cad51), [`7dc2379`](https://github.com/sveltejs/kit/commit/7dc23798cde97b5616049ba8d443a07e8c558bdc), [`a70c3eb`](https://github.com/sveltejs/kit/commit/a70c3eb31fe1e28f1532c84ec569ec7099da3344), [`89f49c7`](https://github.com/sveltejs/kit/commit/89f49c70b7fb4883cf704d2fb57d2d8595790ad2), [`f36b3a4`](https://github.com/sveltejs/kit/commit/f36b3a4cd307bd8c89e03fa191becc175e7b3077), [`1144a63`](https://github.com/sveltejs/kit/commit/1144a63d318c3c3255d06b374bb7a656a469fabf), [`9f06b3a`](https://github.com/sveltejs/kit/commit/9f06b3ac78be30d27b07fc00ea40bda493c573aa)]: + - @sveltejs/kit@3.0.0-next.15 + +## 6.0.0-next.7 + +### Patch Changes + +- fix: serve static files with the Content-Type recorded in the manifest ([#16564](https://github.com/sveltejs/kit/pull/16564)) + +- fix: don't send `Vary: Accept-Encoding` for assets that were never precompressed ([#16566](https://github.com/sveltejs/kit/pull/16566)) +- Updated dependencies [[`c3f58bb`](https://github.com/sveltejs/kit/commit/c3f58bb0eb6bde790897f1e8b89019bc39a062e5), [`b655dc7`](https://github.com/sveltejs/kit/commit/b655dc7cf043ff01d53e494e9744e548bb3ad9fd), [`a70cc4f`](https://github.com/sveltejs/kit/commit/a70cc4f2ca24e7099c898456085ffe18d191657e), [`b325afb`](https://github.com/sveltejs/kit/commit/b325afb509b54445f678348e88ea2bf0152f4cec), [`e76b3d7`](https://github.com/sveltejs/kit/commit/e76b3d778bacb429fdb40a41b13ff04b54d97c1f), [`58f47eb`](https://github.com/sveltejs/kit/commit/58f47eb94016c5405829667bbc873e027c7b363c), [`fe9d8d9`](https://github.com/sveltejs/kit/commit/fe9d8d939b6d1a07b2957104fda6a1123f5eebd3), [`0c7bbd7`](https://github.com/sveltejs/kit/commit/0c7bbd7bd4a346c560e7cbf9b39ba8bda7c718f0), [`08d7e2a`](https://github.com/sveltejs/kit/commit/08d7e2a1b327d4e68c217f6e7d6b305732c82b1b), [`f70f679`](https://github.com/sveltejs/kit/commit/f70f679143d4330b6d3b7d0e690145b5487df97f), [`0624b26`](https://github.com/sveltejs/kit/commit/0624b2678efd41223f70972f7bff2d81d4a5c7f9), [`0624b26`](https://github.com/sveltejs/kit/commit/0624b2678efd41223f70972f7bff2d81d4a5c7f9), [`f4222ad`](https://github.com/sveltejs/kit/commit/f4222adc504a7041b50325424a3eb86af02961ed), [`089628b`](https://github.com/sveltejs/kit/commit/089628b0d9637578085ad61b5786cd1c4476e399), [`d8617b3`](https://github.com/sveltejs/kit/commit/d8617b3f2f08144f6acfc7c636b89d62b982a33d), [`c7369a1`](https://github.com/sveltejs/kit/commit/c7369a148169dfd51af150c461ff266977b6befd), [`5e5e592`](https://github.com/sveltejs/kit/commit/5e5e592f093d05c6ea30a41a0ebf543829069c5f), [`60057c1`](https://github.com/sveltejs/kit/commit/60057c1d2a1e6429e5f315d587ebc01baef6dc94), [`f70f679`](https://github.com/sveltejs/kit/commit/f70f679143d4330b6d3b7d0e690145b5487df97f), [`38f1528`](https://github.com/sveltejs/kit/commit/38f15289e249942275eab1281516702de08b7a30), [`40092f3`](https://github.com/sveltejs/kit/commit/40092f34aa063d033fc7a4d1d6fb1c10abd8cbf6), [`ff4247e`](https://github.com/sveltejs/kit/commit/ff4247e2376862207dd9df7317f39d8bb8dd9ee4), [`79a5de5`](https://github.com/sveltejs/kit/commit/79a5de581fc1bb708879678b8295706221ba0f0f), [`a70cc4f`](https://github.com/sveltejs/kit/commit/a70cc4f2ca24e7099c898456085ffe18d191657e)]: + - @sveltejs/kit@3.0.0-next.14 + +## 6.0.0-next.6 + +### Patch Changes + +- fix: preserve stripped path prefixes by making trailing-slash redirects relative ([#16431](https://github.com/sveltejs/kit/pull/16431)) + +- chore: bump Rolldown to `1.2.0` ([#16455](https://github.com/sveltejs/kit/pull/16455)) +- Updated dependencies [[`adc4c5b`](https://github.com/sveltejs/kit/commit/adc4c5be542ecf7da93ee8d0e90e9d13b952998b), [`ecb0701`](https://github.com/sveltejs/kit/commit/ecb0701b8826e41229f84922f37c12099c66d3bc), [`4b4cc60`](https://github.com/sveltejs/kit/commit/4b4cc60990fa940236639a6cecb6c8efe4287bfa), [`7390dbe`](https://github.com/sveltejs/kit/commit/7390dbe6ef8dff0a1cd923f96daef462a3ce68f2), [`2e71340`](https://github.com/sveltejs/kit/commit/2e713407deb22965777afb01cb8215013ef8bfd3), [`c87bc3a`](https://github.com/sveltejs/kit/commit/c87bc3ac7830f8ee4e76254c9315adcc79ff6a99), [`af15c6c`](https://github.com/sveltejs/kit/commit/af15c6cab2fbfe6bae51dc1fa77a9271df996a47), [`c6fa431`](https://github.com/sveltejs/kit/commit/c6fa43157ae0dbaa9fc61b7e8d2db673e1f9f893), [`bfe4dea`](https://github.com/sveltejs/kit/commit/bfe4deae006f61347b5a5dce7747ec96692e97f9), [`df7dc72`](https://github.com/sveltejs/kit/commit/df7dc7219451bcc86a93e93188d78d664973d9ae), [`5ae11a1`](https://github.com/sveltejs/kit/commit/5ae11a119667c1b24e3b6d17f4b538032ddf1ab1), [`781205c`](https://github.com/sveltejs/kit/commit/781205c41cc5696eb862e5c13356c301a0368983), [`c87bc3a`](https://github.com/sveltejs/kit/commit/c87bc3ac7830f8ee4e76254c9315adcc79ff6a99), [`a1bfeb9`](https://github.com/sveltejs/kit/commit/a1bfeb9de906ae140b9cdc7d7eeaf0ee652a7bcc), [`c87bc3a`](https://github.com/sveltejs/kit/commit/c87bc3ac7830f8ee4e76254c9315adcc79ff6a99), [`9f0127d`](https://github.com/sveltejs/kit/commit/9f0127d4eee3f17dc1db1d2b99b1ca5fc80af784), [`4b4cc60`](https://github.com/sveltejs/kit/commit/4b4cc60990fa940236639a6cecb6c8efe4287bfa), [`8f5b9c7`](https://github.com/sveltejs/kit/commit/8f5b9c72ae930b3b93ef9e1b9ecb9e52664e1d74), [`c9b5544`](https://github.com/sveltejs/kit/commit/c9b554448633e1839e8c0f16c374b00a5ce4d1b6), [`fe1d4a4`](https://github.com/sveltejs/kit/commit/fe1d4a4d68d1d7a3925004fe804209ff54091e34), [`4b4cc60`](https://github.com/sveltejs/kit/commit/4b4cc60990fa940236639a6cecb6c8efe4287bfa), [`5f78e95`](https://github.com/sveltejs/kit/commit/5f78e953ae3966c23a01194dd200f5c57f723e40), [`25510fc`](https://github.com/sveltejs/kit/commit/25510fc79484363277d062e01f2b872f17a473f5), [`c99a6cf`](https://github.com/sveltejs/kit/commit/c99a6cfb7fec133aa85a6de59b43fc442aedc41a), [`17a45ca`](https://github.com/sveltejs/kit/commit/17a45cadb9356403b82c170f0d0027a1ba085667), [`2ca20c3`](https://github.com/sveltejs/kit/commit/2ca20c3ee7348836757dc6f3469217e46957766b), [`eb5c973`](https://github.com/sveltejs/kit/commit/eb5c9737b6d74c53f572b3e047faa5947052ced2)]: + - @sveltejs/kit@3.0.0-next.12 + +## 6.0.0-next.5 + +### Minor Changes + +- feat: better error logging ([#16374](https://github.com/sveltejs/kit/pull/16374)) + +### Patch Changes + +- Updated dependencies [[`5220191`](https://github.com/sveltejs/kit/commit/5220191a3bac885fb1474718afee03dafa6b0ce7), [`8cb2f7d`](https://github.com/sveltejs/kit/commit/8cb2f7d007af7e53f780d5b8dbbfe87459befd41), [`b88c7a7`](https://github.com/sveltejs/kit/commit/b88c7a7ba07c0a9309598aebcb86709f8a09c443), [`a6ea113`](https://github.com/sveltejs/kit/commit/a6ea1135fd1a9f0ec5dcc2625215331c55ff8a02), [`6446f64`](https://github.com/sveltejs/kit/commit/6446f64b755ce6489f66c71961e43ad0d89e84eb), [`58f1789`](https://github.com/sveltejs/kit/commit/58f178999e172820bcb278be0c61e7f97b0396ac), [`09774a2`](https://github.com/sveltejs/kit/commit/09774a27004853ecb5140fde9f40356b9bffb077), [`c542fdd`](https://github.com/sveltejs/kit/commit/c542fdda394cff4b3709ddb895417b8a14ab7d30), [`aedaa27`](https://github.com/sveltejs/kit/commit/aedaa27be9df0882e54952b443985197a1e0a16e), [`428ee1a`](https://github.com/sveltejs/kit/commit/428ee1ae27543caa483c9532457b1855dbc10683), [`fefb3ae`](https://github.com/sveltejs/kit/commit/fefb3ae495cc1ec10e35be988b2878bfd880a62e)]: + - @sveltejs/kit@3.0.0-next.11 + +## 6.0.0-next.4 + +### Patch Changes + +- fix: correctly bundle entrypoints on Windows ([#16367](https://github.com/sveltejs/kit/pull/16367)) +- Updated dependencies [[`c1ee782`](https://github.com/sveltejs/kit/commit/c1ee7829b0c7ec9a874c86d7fd1e952f875f5bed), [`1a1b3ea`](https://github.com/sveltejs/kit/commit/1a1b3ea53bdff1d83c4f22e934e825810a088688), [`6423d98`](https://github.com/sveltejs/kit/commit/6423d9827cd31dd7b26c4c611dbabe6d636fc91c), [`6d1f4f0`](https://github.com/sveltejs/kit/commit/6d1f4f0cea991c532cfad49eec8662dac31dfe2b), [`5ca9906`](https://github.com/sveltejs/kit/commit/5ca990646d886d184216995b01d10f5cc649ceed), [`b148d31`](https://github.com/sveltejs/kit/commit/b148d31398f6c0219d553a172d8cf46223d5db53), [`5ca9906`](https://github.com/sveltejs/kit/commit/5ca990646d886d184216995b01d10f5cc649ceed), [`9f3d9bb`](https://github.com/sveltejs/kit/commit/9f3d9bbb0c63e7197c23f7529565542c2d77e592), [`7bfd922`](https://github.com/sveltejs/kit/commit/7bfd922b367571426dcc5c573317bab8beac35f6), [`ffa0e3b`](https://github.com/sveltejs/kit/commit/ffa0e3b06e82d7023dd2189ccfe1fc191c90b941)]: + - @sveltejs/kit@3.0.0-next.9 + +## 6.0.0-next.3 + +### Patch Changes + +- fix: allow prerelease versions of SvelteKit 3 to satisfy the peer dependency range ([#16286](https://github.com/sveltejs/kit/pull/16286)) +- Updated dependencies [[`737d119`](https://github.com/sveltejs/kit/commit/737d119730ec163832f838b3813c9cbb75dd799f), [`fa78efb`](https://github.com/sveltejs/kit/commit/fa78efb626031497eecc3a88fa7b6f6da36eca0f), [`07c207e`](https://github.com/sveltejs/kit/commit/07c207ed919570d4ed4954f751056b4f838abdea), [`a47071b`](https://github.com/sveltejs/kit/commit/a47071bb6581fe5f6df4cc47b80e9f0f594624e5), [`14d7d5a`](https://github.com/sveltejs/kit/commit/14d7d5abd9cd8462e4c79ed6a5c10bc43258e97a), [`5c38e51`](https://github.com/sveltejs/kit/commit/5c38e515db7fbb92e5ae01db84b4f0040a02f187), [`0702baa`](https://github.com/sveltejs/kit/commit/0702baa58b2a4600e83b55ef4a85e9cbe3391a4f), [`e1938c6`](https://github.com/sveltejs/kit/commit/e1938c62348b8ccc83e186b1acad76a0a10cc9ea), [`8293144`](https://github.com/sveltejs/kit/commit/8293144b6fbfff9f97cdb1d2224a5eaa04c84fc4), [`f76d7d9`](https://github.com/sveltejs/kit/commit/f76d7d91e18a0afd1f6798c095eb8c170c108cff), [`ab5c253`](https://github.com/sveltejs/kit/commit/ab5c253efb2546ead74db4c8c9b4c7639d97e0ed), [`b557b1b`](https://github.com/sveltejs/kit/commit/b557b1ba76903e1fb0fc3bb87bddc5420332137a), [`4a513e2`](https://github.com/sveltejs/kit/commit/4a513e27ab2ad44dae0c6a4a6c8deda840008530)]: + - @sveltejs/kit@3.0.0-next.8 + +## 6.0.0-next.2 +### Major Changes + + +- breaking: add `kit.paths.origin` config option, remove `kit.prerender.origin` and the `adapter-node` `ORIGIN` environment variable ([#16161](https://github.com/sveltejs/kit/pull/16161)) + + +### Patch Changes + +- Updated dependencies [[`3c434fb`](https://github.com/sveltejs/kit/commit/3c434fbf12822e19bea833a2084b329590ccf5b1), [`a9284e8`](https://github.com/sveltejs/kit/commit/a9284e8e627b1b0a8d3772c53310c41056e556b1), [`3726a7a`](https://github.com/sveltejs/kit/commit/3726a7a67c949a160283df7db6033b440e229171), [`f9d2240`](https://github.com/sveltejs/kit/commit/f9d22409aa0fca0f5035aead811fe6907f63c293), [`a9284e8`](https://github.com/sveltejs/kit/commit/a9284e8e627b1b0a8d3772c53310c41056e556b1), [`7c040ba`](https://github.com/sveltejs/kit/commit/7c040ba28e98ecbf38acbbf93653fc6c33a506ba), [`223eaad`](https://github.com/sveltejs/kit/commit/223eaadca55c6e73096cd605759d6c7366f260a2), [`223eaad`](https://github.com/sveltejs/kit/commit/223eaadca55c6e73096cd605759d6c7366f260a2), [`3b907d4`](https://github.com/sveltejs/kit/commit/3b907d449a42f9d763cf8eaa6ac5a88975137011), [`fd628a5`](https://github.com/sveltejs/kit/commit/fd628a57348d25fb433f63778e0239892ebb1da2), [`223eaad`](https://github.com/sveltejs/kit/commit/223eaadca55c6e73096cd605759d6c7366f260a2), [`6c1d035`](https://github.com/sveltejs/kit/commit/6c1d03535b09f670164eb22f05405c17120149f1), [`178eac0`](https://github.com/sveltejs/kit/commit/178eac007e648f3235bc45d926cfc029ddf8de07), [`c6562a9`](https://github.com/sveltejs/kit/commit/c6562a92cb908058cab3fabad075a94ad9cc6e39), [`8eca2ab`](https://github.com/sveltejs/kit/commit/8eca2ab8b4760456a782d7a29f83f24a15fac177), [`61cf188`](https://github.com/sveltejs/kit/commit/61cf188e38725b041d0411c6074ad2649a7c42b4)]: + - @sveltejs/kit@3.0.0-next.6 + +## 6.0.0-next.1 +### Major Changes + + +- breaking: require `vite@^8.0.12`, the first Vite 8 release bundling stable `rolldown` 1.0.0 ([#16134](https://github.com/sveltejs/kit/pull/16134)) + + +### Patch Changes + +- Updated dependencies [[`a9629f1`](https://github.com/sveltejs/kit/commit/a9629f136d3256b14a8b3c10114c99832b977f6c), [`53d37f9`](https://github.com/sveltejs/kit/commit/53d37f93cf1f46995264398d4e54bb27bd1e75dd), [`dba6239`](https://github.com/sveltejs/kit/commit/dba62390e96f9f40c55ddc203744138664742026), [`ab12cb6`](https://github.com/sveltejs/kit/commit/ab12cb6a07269e2a18c0a3867e608e9838c592f3), [`f0eab52`](https://github.com/sveltejs/kit/commit/f0eab522bdea2d2a00f9b5473cd917d678b20daa), [`cd884c1`](https://github.com/sveltejs/kit/commit/cd884c1ffa6f96e08c70486c52fcca20501d5cef), [`9ed38a8`](https://github.com/sveltejs/kit/commit/9ed38a886be8c247c42c887c15d53b405693e1a9), [`53aa049`](https://github.com/sveltejs/kit/commit/53aa049a06ce7b053bdbe6484f05eecdb09d874e), [`984e57a`](https://github.com/sveltejs/kit/commit/984e57ac95d6ca7911611e1c297eecd1b1d870db), [`51785be`](https://github.com/sveltejs/kit/commit/51785be17ac706659d7fd7b3fca3fcf58dcc5de0), [`4f6bcbb`](https://github.com/sveltejs/kit/commit/4f6bcbbbb6e104238c41172ce7dbdaba486bbf43), [`78e7137`](https://github.com/sveltejs/kit/commit/78e71372ca284160df0aefe407999d46795a1299)]: + - @sveltejs/kit@3.0.0-next.5 + +## 6.0.0-next.0 +### Major Changes + + +- chore: migrate from rollup to rolldown ([#15297](https://github.com/sveltejs/kit/pull/15297)) + + +- breaking: require SvelteKit 3 ([#15506](https://github.com/sveltejs/kit/pull/15506)) + + +### Patch Changes + +- Updated dependencies [[`fa335bd`](https://github.com/sveltejs/kit/commit/fa335bd8af3a982d68c4d34b813b68887dc7cce8), [`3031d89`](https://github.com/sveltejs/kit/commit/3031d89087d49297e89b6566108a1a1105c38475), [`cb9d416`](https://github.com/sveltejs/kit/commit/cb9d416d05266b6237f2856fb94df5b8a9b04279), [`caf3a18`](https://github.com/sveltejs/kit/commit/caf3a18de6581ab7d454d6fa8ad37ebdbdef95e6), [`4777827`](https://github.com/sveltejs/kit/commit/4777827246727045cf0209399751377564e7951d), [`a2792e2`](https://github.com/sveltejs/kit/commit/a2792e2cdc2dee91c20c7bfd921a104b539abae5), [`ba36148`](https://github.com/sveltejs/kit/commit/ba36148577ed0bc2675034a3d57001d71d328cd1), [`48e8710`](https://github.com/sveltejs/kit/commit/48e8710889b348a039322e05f3d2672327d56184), [`e2f3075`](https://github.com/sveltejs/kit/commit/e2f3075ef299b91175fae116e6a76269002029cf), [`047d6a0`](https://github.com/sveltejs/kit/commit/047d6a0f1c3fd1bc6f90a81177a2834b023a64b3), [`87603d1`](https://github.com/sveltejs/kit/commit/87603d13d009cada171c9b5d5daad5adad4f72de), [`096962c`](https://github.com/sveltejs/kit/commit/096962c47b0b0f63acd0d539d3973797e4867487), [`d545970`](https://github.com/sveltejs/kit/commit/d54597018308068aea5268e6c45b26398cd57607), [`e2f3075`](https://github.com/sveltejs/kit/commit/e2f3075ef299b91175fae116e6a76269002029cf), [`d06affc`](https://github.com/sveltejs/kit/commit/d06affc9246c676fc32b644efc0bbf53b42bc67d), [`8af47eb`](https://github.com/sveltejs/kit/commit/8af47eb06f9a1a883058de059ccf81aab4e85627), [`e2f3075`](https://github.com/sveltejs/kit/commit/e2f3075ef299b91175fae116e6a76269002029cf), [`5c4d130`](https://github.com/sveltejs/kit/commit/5c4d130ea2953464d8b4b462eb5aa4641aa78da1), [`3f11f35`](https://github.com/sveltejs/kit/commit/3f11f35125c5b3625891f0d00d9c7c826e6eaea2), [`caf3a18`](https://github.com/sveltejs/kit/commit/caf3a18de6581ab7d454d6fa8ad37ebdbdef95e6), [`8823037`](https://github.com/sveltejs/kit/commit/8823037b4a341013cc40a9bb688ee23bda3dbb04), [`1d76212`](https://github.com/sveltejs/kit/commit/1d76212740f59ed58839ef569899c653870834fd), [`0dc0548`](https://github.com/sveltejs/kit/commit/0dc0548307a439b6a6f24b14405366ced213f828), [`00d81fa`](https://github.com/sveltejs/kit/commit/00d81fab618435bac95b1bacb5e9734b61ba1af6)]: + - @sveltejs/kit@3.0.0-next.0 + ## 5.5.7 ### Patch Changes diff --git a/packages/adapter-node/index.d.ts b/packages/adapter-node/index.d.ts index 03422f6bfdf3..581e9c9254a4 100644 --- a/packages/adapter-node/index.d.ts +++ b/packages/adapter-node/index.d.ts @@ -25,7 +25,6 @@ interface AdapterOptions { * ```sh * MY_CUSTOM_HOST=127.0.0.1 \ * MY_CUSTOM_PORT=4000 \ - * MY_CUSTOM_ORIGIN=https://my.site \ * node build * ``` */ diff --git a/packages/adapter-node/index.js b/packages/adapter-node/index.js index 3d0c3753e35a..6c0087788398 100644 --- a/packages/adapter-node/index.js +++ b/packages/adapter-node/index.js @@ -1,145 +1,284 @@ -import { readFileSync } from 'node:fs'; +import { createHash } from 'node:crypto'; +import * as fs from 'node:fs'; +import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { rollup } from 'rollup'; -import { nodeResolve } from '@rollup/plugin-node-resolve'; -import commonjs from '@rollup/plugin-commonjs'; -import json from '@rollup/plugin-json'; -import replace from '@rollup/plugin-replace'; -/** - * @template T - * @template {keyof T} K - * @typedef {Partial> & Required>} PartialExcept - */ - -/** - * We use a custom `Builder` type here to support the minimum version of SvelteKit. - * @typedef {PartialExcept} Builder2_4_0 - */ +// posix so it matches the module ids Vite reports on every platform +const src = fileURLToPath(new URL('./src', import.meta.url).href).replaceAll('\\', '/'); +const handoff = '#@sveltejs/adapter-node'; -const files = fileURLToPath(new URL('./files', import.meta.url).href); - -/** @type {import('./index.js').default} */ +/** @type {typeof import('./index.js').default} */ export default function (opts = {}) { const { out = 'build', precompress = true, envPrefix = '' } = opts; return { name: '@sveltejs/adapter-node', - /** @param {Builder2_4_0} builder */ async adapt(builder) { - const tmp = builder.getBuildDirectory('adapter-node'); + fs.rmSync(out, { force: true, recursive: true }); - builder.rimraf(out); - builder.rimraf(tmp); - builder.mkdirp(tmp); + const base = builder.config.paths.base; + const client_dir = `${out}/client${base}`; + const prerendered_dir = `${out}/prerendered${base}`; builder.log.minor('Copying assets'); - builder.writeClient(`${out}/client${builder.config.kit.paths.base}`); - builder.writePrerendered(`${out}/prerendered${builder.config.kit.paths.base}`); - - if (precompress) { - builder.log.minor('Compressing assets'); - await Promise.all([ - builder.compress(`${out}/client`), - builder.compress(`${out}/prerendered`) - ]); - } + const client_files = builder.writeClient(client_dir); + const prerendered_files = builder.writePrerendered(prerendered_dir); - builder.log.minor('Building server'); + builder.log.minor(precompress ? 'Compressing and hashing assets' : 'Hashing assets'); + const [client_compressed, prerendered_compressed] = precompress + ? await Promise.all([builder.compress(client_dir), builder.compress(prerendered_dir)]) + : [[], []]; - // Copy the entrypoints into `.svelte-kit/adapter-node/entries`, - // so that node modules are correctly resolved - const entries = `${tmp}/entries`; - builder.copy(files, entries); + const assets = create_asset_table( + base, + measure_files(client_dir, client_files, client_compressed) + ); + const prerendered_assets = create_prerendered_table( + base, + measure_files(prerendered_dir, prerendered_files, prerendered_compressed), + builder.prerendered.paths + ); - const pkg = JSON.parse(readFileSync('package.json', 'utf8')); const server = builder.getServerDirectory(); - /** @type {Record} */ - const input = { - index: `${entries}/index.js`, - env: `${entries}/env.js`, - handler: `${entries}/handler.js`, - shims: `${entries}/shims.js` - }; - - if (builder.hasServerInstrumentationFile?.()) { - input['instrumentation.server'] = `${server}/instrumentation.server.js`; - } - - // we bundle the Vite output so that deployments only need - // their production dependencies. Anything in devDependencies - // will get included in the bundled code - const bundle = await rollup({ - input, - external: [ - // dependencies could have deep exports, so we need a regex - ...Object.keys(pkg.dependencies || {}).map((d) => new RegExp(`^${d}(\\/.*)?$`)) - ], - plugins: [ - { - name: 'adapter-node:alias', - resolveId(id) { - if (id === 'SERVER') return `${server}/index.js`; - if (id === 'MANIFEST') return `${server}/manifest.js`; - } - }, - nodeResolve({ - preferBuiltins: true, - exportConditions: ['node'] - }), - // @ts-expect-error https://github.com/rollup/plugins/issues/1329 - replace({ - // only replace tokens in the adapter's own entrypoints, so that - // identifiers like `BASE` in the user's app code or bundled - // dependencies aren't accidentally replaced - include: [`${entries}/**`], - values: { - BASE: JSON.stringify(builder.config.kit.paths.base), - ENV_PREFIX: JSON.stringify(envPrefix), - PRECOMPRESS: JSON.stringify(precompress), - PRERENDERED: `new Set(${JSON.stringify(builder.prerendered.paths)})` - }, - preventAssignment: true - }), - // @ts-expect-error https://github.com/rollup/plugins/issues/1329 - commonjs({ strictRequires: true }), - // @ts-expect-error https://github.com/rollup/plugins/issues/1329 - json() - ] - }); - - const server_path_length = server.length + 1; - - await bundle.write({ - dir: out, - format: 'esm', - sourcemap: true, - chunkFileNames: 'server/chunks/[name]-[hash].js', - // force the Vite server output to retain their file structure to avoid - // a circular import chain - // see https://github.com/sveltejs/kit/issues/16092 - manualChunks(id) { - if (id.startsWith(server)) { - return id.slice(server_path_length); - } - } - }); + builder.generateServerInstance(`${server}/server.js`); - if (builder.hasServerInstrumentationFile?.()) { - builder.instrument?.({ - entrypoint: `${out}/index.js`, - instrumentation: `${out}/instrumentation.server.js`, + if (builder.hasServerInstrumentationFile()) { + builder.instrument({ + entrypoint: `${server}/adapter-index.js`, + instrumentation: `${server}/instrumentation.server.js`, + initializer: builder.createInstrumentationInitializer({ outputDirectory: server }), module: { exports: ['path', 'host', 'port', 'server'] } }); } + + builder.copy(server, `${out}/server`); + + // values only known after the build. `dir` needs the output root + fs.writeFileSync( + `${out}/adapter-node.js`, + [ + `import { dirname } from 'node:path';`, + `import { fileURLToPath } from 'node:url';`, + `export { server } from './server/server.js';`, + `export const dir = dirname(fileURLToPath(import.meta.url));`, + `export const base = ${JSON.stringify(base)};`, + `export const app_path = ${JSON.stringify(builder.getAppPath())};`, + `export const origin = ${JSON.stringify(builder.config.paths.origin)};`, + `export const env_prefix = ${JSON.stringify(envPrefix)};`, + `export const mime_types = ${JSON.stringify(builder.mimeTypes)};`, + // JSON.parse of a string loads about twice as fast as an object literal of the same size + `export const assets = JSON.parse(${JSON.stringify(JSON.stringify(assets))});`, + `export const prerendered_assets = JSON.parse(${JSON.stringify(JSON.stringify(prerendered_assets))});` + ].join('\n') + ); + + fs.writeFileSync(`${out}/index.js`, `export * from './server/adapter-index.js';\n`); + fs.writeFileSync(`${out}/handler.js`, `export * from './server/handler.js';\n`); }, supports: { read: () => true, instrumentation: () => true + }, + + vite: { + plugins: { + post: [ + { + name: 'vite-plugin-sveltekit-adapter-node', + apply: 'build', + config(config) { + const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); + // vite-plugin-svelte lists Svelte libraries here so their export conditions resolve at build time + const no_external = Array.isArray(config.ssr?.noExternal) + ? config.ssr.noExternal + : []; + + return { + ssr: { + // Vite doesn't bundle dependencies for SSR by default. Bundle everything + // except production dependencies that don't need the build's export conditions + external: Object.keys(pkg.dependencies || {}).filter( + (dep) => + !no_external.some((rule) => + typeof rule === 'string' ? rule === dep : rule.test(dep) + ) + ), + noExternal: true + }, + environments: { + ssr: { + build: { + rolldownOptions: { + // bundled with the app's server code so shared modules aren't duplicated (#15755) + input: { + 'adapter-index': `${src}/index.js`, + 'adapter-env': `${src}/env.js`, + handler: `${src}/handler.js` + }, + // generated after the Vite build and rewritten to an output-relative path + external: [handoff], + output: { + paths: { [handoff]: '../adapter-node.js' }, + // the hand-off path only holds at the output root, so adapter chunks may not nest + chunkFileNames: (chunk) => + chunk.moduleIds.some((id) => id.startsWith(src)) + ? 'adapter-node-[name].js' + : 'chunks/[name].js' + } + } + } + } + } + }; + } + } + ] + } } }; } + +/** + * Dotfiles are not served, with the customary exception of `.well-known` + * @param {string} file + */ +function is_hidden(file) { + return file.split('/').some((segment) => segment[0] === '.') && !file.startsWith('.well-known/'); +} + +/** + * Size and content hash from one pass over the file, a buffer at a time + * @param {string} file + * @param {Buffer} buffer + */ +function measure(file, buffer) { + const fd = fs.openSync(file, 'r'); + const hash = createHash('sha256'); + let size = 0; + + try { + let read; + while ((read = fs.readSync(fd, buffer)) > 0) { + hash.update(buffer.subarray(0, read)); + size += read; + } + } finally { + fs.closeSync(fd); + } + + return { size, etag: hash.digest('base64url') }; +} + +/** + * Size and content hash of every servable file, plus the sizes of the + * compressed variants where `builder.compress` wrote them. + * Files are read one at a time through a single buffer, so large outputs + * neither exhaust file descriptors nor pile up in memory + * @param {string} root + * @param {string[]} files + * @param {string[]} compressed + * @returns {AssetEntry[]} + */ +function measure_files(root, files, compressed) { + const variants = new Set(compressed); + const buffer = Buffer.allocUnsafe(64 * 1024); + + /** @type {AssetEntry[]} */ + const entries = []; + + for (const file of files) { + if (is_hidden(file)) continue; + + const abs = join(root, file); + + /** @type {AssetEntry} */ + const entry = { file, ...measure(abs, buffer) }; + + // `builder.compress` writes a `.gz` and a `.br` variant of every file it returns + if (variants.has(file)) { + entry.gz = fs.statSync(`${abs}.gz`).size; + entry.br = fs.statSync(`${abs}.br`).size; + } + + entries.push(entry); + } + + return entries; +} + +/** + * Keys the measured files by URL: the exact pathname, plus the `/foo` and + * `/foo/` forms of `foo.html`/`foo/index.html` files + * @param {string} base + * @param {AssetEntry[]} measured + * @returns {AssetTable} + */ +function create_asset_table(base, measured) { + const entries = measured.map( + (entry) => /** @type {[string, AssetEntry]} */ ([`${base}/${entry.file}`, entry]) + ); + + entries.sort(([a], [b]) => (a < b ? -1 : 1)); + + const keys = new Set(entries.map(([key]) => key)); + + /** @type {Array<[string, string]>} */ + const aliases = []; + + /** + * @param {string} alias + * @param {string} key + */ + function alias(alias, key) { + if (!keys.has(alias)) { + keys.add(alias); + aliases.push([alias, key]); + } + } + + // `/foo` and `/foo/` resolve to `foo.html`, or to `foo/index.html` when only that exists. + // `foo.html` sorts first, so it claims the aliases (the resolution order sirv used) + for (const [key, entry] of entries) { + if (!entry.file.endsWith('.html')) continue; + + const is_index = entry.file === 'index.html' || entry.file.endsWith('/index.html'); + const with_slash = is_index ? key.slice(0, -'index.html'.length) : key.slice(0, -5) + '/'; + + alias(with_slash, key); + if (with_slash.length > 1) alias(with_slash.slice(0, -1), key); + } + + return { entries, aliases }; +} + +/** + * Keys the measured files by the exact paths kit prerendered, so a lookup + * hit is precisely a prerendered page, asset or redirect and every other + * pathname (including the non-canonical trailing-slash form) misses + * @param {string} base + * @param {AssetEntry[]} measured + * @param {string[]} paths + * @returns {AssetTable} + */ +function create_prerendered_table(base, measured, paths) { + const by_file = new Map(measured.map((entry) => [entry.file, entry])); + + /** @type {Array<[string, AssetEntry]>} */ + const entries = []; + + for (const path of paths) { + // invert `output_filename` in kit's prerenderer + const file = path.slice(base.length + 1) || 'index.html'; + const entry = + by_file.get(file) ?? by_file.get(file + (file.endsWith('/') ? 'index.html' : '.html')); + if (entry) entries.push([path, entry]); + } + + entries.sort(([a], [b]) => (a < b ? -1 : 1)); + + return { entries, aliases: [] }; +} diff --git a/packages/adapter-node/internal.d.ts b/packages/adapter-node/internal.d.ts index e2e22404e9db..77426e780893 100644 --- a/packages/adapter-node/internal.d.ts +++ b/packages/adapter-node/internal.d.ts @@ -1,13 +1,31 @@ -declare module 'MANIFEST' { - import { SSRManifest } from '@sveltejs/kit'; - export const manifest: SSRManifest; +// generated by `adapt` in the output directory +declare module '#@sveltejs/adapter-node' { + export const server: import('@sveltejs/kit').Server; + /** the output directory */ + export const dir: string; + export const base: string; + export const app_path: string; + export const origin: string | undefined; + export const env_prefix: string; + export const mime_types: Record; + export const assets: AssetTable; + export const prerendered_assets: AssetTable; } -declare module 'SERVER' { - export { Server } from '@sveltejs/kit'; +interface AssetEntry { + /** path on disk, relative to the served directory */ + file: string; + size: number; + /** content hash */ + etag: string; + /** size of the gzip variant, if one was written */ + gz?: number; + /** size of the brotli variant, if one was written */ + br?: number; } -declare const BASE: string; -declare const ENV_PREFIX: string; -declare const PRECOMPRESS: boolean; -declare const PRERENDERED: Set; +interface AssetTable { + entries: Array<[string, AssetEntry]>; + /** `[alias, key]` pairs, e.g. `['/about', '/about.html']` */ + aliases: Array<[string, string]>; +} diff --git a/packages/adapter-node/package.json b/packages/adapter-node/package.json index 407a1e81457d..8304ee554ea6 100644 --- a/packages/adapter-node/package.json +++ b/packages/adapter-node/package.json @@ -1,6 +1,6 @@ { "name": "@sveltejs/adapter-node", - "version": "5.5.7", + "version": "6.0.0-next.15", "description": "Adapter for SvelteKit apps that generates a standalone Node server", "keywords": [ "adapter", @@ -27,37 +27,27 @@ }, "types": "index.d.ts", "files": [ - "files", + "src/*.js", "index.js", "index.d.ts", "ambient.d.ts" ], "scripts": { - "dev": "rollup -cw", - "build": "rollup -c", - "test": "vitest run", - "check": "tsc", - "lint": "prettier --check .", - "format": "pnpm lint --write", - "prepublishOnly": "pnpm build" + "prepublishOnly": "echo TODO: remove this script once .github/workflows/platform-tests-node.yml no longer invokes prepublishOnly, after version-3 is merged into main", + "test": "pnpm test:unit && pnpm test:build", + "test:unit": "vitest run", + "test:build": "pnpm -r --workspace-concurrency 1 --filter=\"./test/**\" test:build", + "check": "node node_modules/@typescript/native/bin/tsc", + "lint": "oxfmt --check .", + "format": "oxfmt --write ." }, "devDependencies": { - "@polka/url": "catalog:", "@sveltejs/kit": "workspace:^", "@types/node": "catalog:", - "polka": "catalog:", - "sirv": "^3.0.2", - "typescript": "catalog:", + "@typescript/native": "catalog:", "vitest": "catalog:" }, - "dependencies": { - "@rollup/plugin-commonjs": "^29.0.0", - "@rollup/plugin-json": "^6.1.0", - "@rollup/plugin-node-resolve": "^16.0.0", - "@rollup/plugin-replace": "^6.0.3", - "rollup": "^4.59.0" - }, "peerDependencies": { - "@sveltejs/kit": "^2.4.0" + "@sveltejs/kit": "^3.0.0-next.0" } } diff --git a/packages/adapter-node/rollup.config.js b/packages/adapter-node/rollup.config.js deleted file mode 100644 index e501635c7186..000000000000 --- a/packages/adapter-node/rollup.config.js +++ /dev/null @@ -1,69 +0,0 @@ -/** @import { Plugin, RollupOptions } from 'rollup' */ -import { nodeResolve } from '@rollup/plugin-node-resolve'; -import commonjs from '@rollup/plugin-commonjs'; -import json from '@rollup/plugin-json'; -import { builtinModules } from 'node:module'; -import { rmSync } from 'node:fs'; - -/** - * @param {string} filepath - * @returns {Plugin} - */ -function clearOutput(filepath) { - return { - name: 'clear-output', - buildStart: { - order: 'pre', - sequential: true, - handler() { - rmSync(filepath, { recursive: true, force: true }); - } - } - }; -} - -/** - * @returns {Plugin} - */ -function prefixBuiltinModules() { - return { - name: 'prefix-built-in-modules', - resolveId(source) { - if (builtinModules.includes(source)) { - return { id: 'node:' + source, external: true }; - } - } - }; -} - -/** @type {RollupOptions} */ -export default { - input: { - index: 'src/index.js', - env: 'src/env.js', - handler: 'src/handler.js', - shims: 'src/shims.js', - utils: 'utils.js' - }, - output: { - dir: 'files', - format: 'esm', - hoistTransitiveImports: false, - chunkFileNames: 'chunks/[name].js', - // prevent the handler code from becoming a shared chunk when both - // handler.js and index.js are input entries - manualChunks(id) { - if (id.includes('node_modules')) return 'vendor'; - } - }, - plugins: [ - clearOutput('files'), - nodeResolve({ preferBuiltins: true }), - // @ts-expect-error https://github.com/rollup/plugins/issues/1329 - commonjs(), - // @ts-expect-error https://github.com/rollup/plugins/issues/1329 - json(), - prefixBuiltinModules() - ], - external: ['MANIFEST', 'SERVER', '@sveltejs/kit/node', '@sveltejs/kit/node/polyfills'] -}; diff --git a/packages/adapter-node/smoke.spec_disabled.js b/packages/adapter-node/smoke.spec_disabled.js deleted file mode 100644 index 58809bf6cb59..000000000000 --- a/packages/adapter-node/smoke.spec_disabled.js +++ /dev/null @@ -1,71 +0,0 @@ -import process from 'node:process'; -import { assert, test } from 'vitest'; -import { create_kit_middleware } from '../src/handler.js'; -import fetch from 'node-fetch'; -import polka from 'polka'; - -const { PORT = 3000 } = process.env; -const DEFAULT_SERVER_OPTS = { render: () => {} }; - -async function startServer(opts = DEFAULT_SERVER_OPTS) { - return new Promise((fulfil, reject) => { - const server = polka().use(create_kit_middleware(opts)); - server.listen(PORT, (err) => { - if (err) { - reject(err); - } - fulfil(server); - }); - }); -} - -test('starts a server', async () => { - const server = await startServer(); - assert.ok('server started'); - server.server.close(); -}); - -test('serves a 404', async () => { - const server = await startServer(); - const res = await fetch(`http://localhost:${PORT}/nothing`); - assert.equal(res.status, 404); - server.server.close(); -}); - -test('responses with the rendered status code', async () => { - const server = await startServer({ - render: () => { - return { - headers: 'wow', - status: 203, - body: 'ok' - }; - } - }); - const res = await fetch(`http://localhost:${PORT}/wow`); - assert.equal(res.status, 203); - server.server.close(); -}); - -test('passes through umlaut as encoded path', async () => { - const server = await startServer({ - render: (incoming) => { - return { - status: 200, - body: incoming.path - }; - } - }); - const res = await fetch(`http://localhost:${PORT}/%C3%BCber-uns`); - assert.equal(await res.text(), '/%C3%BCber-uns'); - server.server.close(); -}); - -test('serve a 400 when we have a malformed url', async () => { - const server = await startServer(); - const res = await fetch(`http://localhost:${PORT}//`); - assert.equal(res.status, 400); - server.server.close(); -}); - -test.run(); diff --git a/packages/adapter-node/src/env.js b/packages/adapter-node/src/env.js index 7b57d9211166..6a093fc11f97 100644 --- a/packages/adapter-node/src/env.js +++ b/packages/adapter-node/src/env.js @@ -1,15 +1,10 @@ -import path from 'node:path'; import process from 'node:process'; -import { fileURLToPath } from 'node:url'; - -// since env.js is an entrypoint, `dir` will point to the output directory -export const dir = path.dirname(fileURLToPath(import.meta.url)); +import { env_prefix } from '#@sveltejs/adapter-node'; const expected = new Set([ 'SOCKET_PATH', 'HOST', 'PORT', - 'ORIGIN', 'XFF_DEPTH', 'ADDRESS_HEADER', 'PROTOCOL_HEADER', @@ -24,8 +19,6 @@ const expected = new Set([ const expected_unprefixed = new Set(['LISTEN_PID', 'LISTEN_FDS']); -export const env_prefix = ENV_PREFIX; - if (env_prefix) { for (const name in process.env) { if (name.startsWith(env_prefix)) { diff --git a/packages/adapter-node/src/env.spec.ts b/packages/adapter-node/src/env.spec.ts index 20fc23a6c2a6..f1fb34827d10 100644 --- a/packages/adapter-node/src/env.spec.ts +++ b/packages/adapter-node/src/env.spec.ts @@ -1,9 +1,7 @@ import { afterEach, expect, test, describe, vi } from 'vitest'; import { timeout_env } from '../src/env.js'; -vi.hoisted(() => { - vi.stubGlobal('ENV_PREFIX', ''); -}); +vi.mock('#@sveltejs/adapter-node', () => ({ env_prefix: '' })); describe('timeout_env', () => { afterEach(() => { diff --git a/packages/adapter-node/src/handler.js b/packages/adapter-node/src/handler.js index 4ee40941f417..649ab5efb58f 100644 --- a/packages/adapter-node/src/handler.js +++ b/packages/adapter-node/src/handler.js @@ -1,21 +1,22 @@ -import './shims.js'; -import fs from 'node:fs'; -import path from 'node:path'; +/** @import { IncomingHttpHeaders } from 'node:http' */ import process from 'node:process'; -import sirv from 'sirv'; -import { parse as polka_url_parser } from '@polka/url'; import { getRequest, setResponse, createReadableStream } from '@sveltejs/kit/node'; -import { Server } from 'SERVER'; -import { manifest } from 'MANIFEST'; -import { dir, env, env_prefix } from './env.js'; -import { parse_as_bytes, parse_origin } from '../utils.js'; - -const prerendered = PRERENDERED; - -const server = new Server(manifest); - -// parse_origin validates ORIGIN and throws descriptive errors for invalid values -const origin = parse_origin(env('ORIGIN', undefined)); +import { + server, + dir, + env_prefix, + base, + app_path, + origin, + mime_types, + assets, + prerendered_assets +} from '#@sveltejs/adapter-node'; +import { env } from './env.js'; +import { parse_as_bytes } from './utils.js'; +import { create_file_map, serve_static } from './static.js'; + +/** @import { Middleware } from './static.js' */ const xff_depth = parseInt(env('XFF_DEPTH', '1')); const address_header = env('ADDRESS_HEADER', '').toLowerCase(); @@ -31,76 +32,37 @@ if (isNaN(body_size_limit)) { ); } -const asset_dir = `${dir}/client${BASE}`; +const asset_dir = `${dir}/client${base}`; await server.init({ - env: /** @type {Record} */ (process.env), + env: process.env, read: (file) => createReadableStream(`${asset_dir}/${file}`) }); -/** - * @param {string} path - * @param {boolean} client - */ -function serve(path, client = false) { - return fs.existsSync(path) - ? sirv(path, { - etag: true, - gzip: PRECOMPRESS, - brotli: PRECOMPRESS, - setHeaders: client - ? (res, pathname) => { - // only apply to build directory, not e.g. version.json - if ( - pathname.startsWith(`/${manifest.appPath}/immutable/`) && - res.statusCode === 200 - ) { - res.setHeader('cache-control', 'public,max-age=31536000,immutable'); - } - } - : undefined - }) - : undefined; -} - -// required because the static file server ignores trailing slashes -/** @returns {import('polka').Middleware} */ -function serve_prerendered() { - const handler = serve(path.join(dir, 'prerendered')); - - return (req, res, next) => { - let { pathname, search, query } = polka_url_parser(req); - - try { - pathname = decodeURIComponent(pathname); - } catch { - // ignore invalid URI - } - - if (prerendered.has(pathname)) { - return handler?.(req, res, next); - } - - // remove or add trailing slash as appropriate - let location = pathname.at(-1) === '/' ? pathname.slice(0, -1) : pathname + '/'; - if (prerendered.has(location)) { - if (query) location += search; - res.writeHead(308, { location }).end(); - } else { - void next(); - } - }; -} - -/** @type {import('polka').Middleware} */ +/** @type {Middleware} */ const ssr = async (req, res) => { /** @type {Request} */ let request; + let request_origin = origin; + + if (!request_origin) { + try { + request_origin = get_origin(req.headers); + } catch (error) { + console.error( + `Could not determine request origin: ${error instanceof Error ? error.message : String(error)}` + ); + res.statusCode = 400; + res.end('Bad Request'); + return; + } + } try { - request = await getRequest({ - base: origin || get_origin(req.headers), + request = getRequest({ + base: request_origin, request: req, + response: res, bodySizeLimit: body_size_limit }); } catch { @@ -162,29 +124,9 @@ const ssr = async (req, res) => { response.headers.set('x-accel-buffering', 'no'); } - await setResponse(res, response); + setResponse(res, response); }; -/** @param {import('polka').Middleware[]} handlers */ -function sequence(handlers) { - /** @type {import('polka').Middleware} */ - return (req, res, next) => { - /** - * @param {number} i - * @returns {ReturnType} - */ - function handle(i) { - if (i < handlers.length) { - return handlers[i](req, res, () => handle(i + 1)); - } else { - return next(); - } - } - - return handle(0); - }; -} - /** * @param {string} name * @param {string | string[] | undefined} value @@ -203,7 +145,7 @@ function normalise_header(name, value) { } /** - * @param {import('http').IncomingHttpHeaders} headers + * @param {IncomingHttpHeaders} headers * @returns {string} */ function get_origin(headers) { @@ -238,7 +180,9 @@ function get_origin(headers) { return port ? `${protocol}://${host}:${port}` : `${protocol}://${host}`; } -export const handler = sequence( - /** @type {(import('sirv').RequestHandler | import('polka').Middleware)[]} */ - ([serve(path.join(dir, 'client'), true), serve_prerendered(), ssr].filter(Boolean)) +const serve = serve_static( + create_file_map({ dir, base, app_path, mime_types, assets, prerendered_assets }) ); + +/** @type {Middleware} */ +export const handler = (req, res, next) => serve(req, res, () => ssr(req, res, next)); diff --git a/packages/adapter-node/src/index.js b/packages/adapter-node/src/index.js index 879060b1bbb8..e3705de7c6ff 100644 --- a/packages/adapter-node/src/index.js +++ b/packages/adapter-node/src/index.js @@ -3,9 +3,8 @@ import fs from 'node:fs'; import process from 'node:process'; import { handler } from './handler.js'; import { env, timeout_env } from './env.js'; -import polka from 'polka'; import { rm } from 'node:fs/promises'; -import { format_listening_address } from '../utils.js'; +import { format_listening_address } from './utils.js'; export const path = env('SOCKET_PATH', false); export const host = env('HOST', '0.0.0.0'); @@ -35,9 +34,6 @@ let shutdown_timeout_id; /** @type {NodeJS.Timeout | void} */ let idle_timeout_id; -// Initialize the HTTP server here so that we can set properties before starting to listen. -// Otherwise, polka delays creating the server until listen() is called. Settings these -// properties after the server has started listening could lead to race conditions. const httpServer = http.createServer(); const keep_alive_timeout = timeout_env('KEEP_ALIVE_TIMEOUT'); @@ -52,10 +48,33 @@ if (headers_timeout !== undefined) { httpServer.headersTimeout = headers_timeout * 1000; } -const server = polka({ server: httpServer }).use(handler); +httpServer.on('request', (req, res) => { + requests++; + + if (socket_activation && idle_timeout_id) { + idle_timeout_id = clearTimeout(idle_timeout_id); + } + + req.on('close', () => { + requests--; + + if (shutdown_timeout_id) { + // close connections as soon as they become idle, so they don't accept new requests + httpServer.closeIdleConnections(); + } + if (requests === 0 && socket_activation && idle_timeout) { + idle_timeout_id = setTimeout(() => graceful_shutdown('IDLE'), idle_timeout * 1000); + } + }); + + return handler(req, res, () => { + res.statusCode = 404; + res.end(); + }); +}); if (socket_activation) { - server.listen({ fd: SD_LISTEN_FDS_START }, () => { + httpServer.listen({ fd: SD_LISTEN_FDS_START }, () => { console.log(`Listening on file descriptor ${SD_LISTEN_FDS_START}`); }); } else { @@ -69,7 +88,7 @@ if (socket_activation) { } } - server.listen({ path, host, port }, () => { + httpServer.listen({ path, host, port }, () => { console.log(`Listening on ${format_listening_address(path, host, port, httpServer.address())}`); }); } @@ -100,31 +119,7 @@ function graceful_shutdown(reason) { shutdown_timeout_id = setTimeout(() => httpServer.closeAllConnections(), shutdown_timeout * 1000); } -httpServer.on( - 'request', - /** @param {import('node:http').IncomingMessage} req */ - (req) => { - requests++; - - if (socket_activation && idle_timeout_id) { - idle_timeout_id = clearTimeout(idle_timeout_id); - } - - req.on('close', () => { - requests--; - - if (shutdown_timeout_id) { - // close connections as soon as they become idle, so they don't accept new requests - httpServer.closeIdleConnections(); - } - if (requests === 0 && socket_activation && idle_timeout) { - idle_timeout_id = setTimeout(() => graceful_shutdown('IDLE'), idle_timeout * 1000); - } - }); - } -); - process.on('SIGTERM', graceful_shutdown); process.on('SIGINT', graceful_shutdown); -export { server }; +export { httpServer as server }; diff --git a/packages/adapter-node/src/shims.js b/packages/adapter-node/src/shims.js deleted file mode 100644 index 2490311daa1e..000000000000 --- a/packages/adapter-node/src/shims.js +++ /dev/null @@ -1,2 +0,0 @@ -import { installPolyfills } from '@sveltejs/kit/node/polyfills'; -installPolyfills(); diff --git a/packages/adapter-node/src/static.js b/packages/adapter-node/src/static.js new file mode 100644 index 000000000000..ed06e0ec799d --- /dev/null +++ b/packages/adapter-node/src/static.js @@ -0,0 +1,260 @@ +/** @import { IncomingMessage, ServerResponse } from 'node:http' */ +import fs from 'node:fs'; +import path from 'node:path'; +import { pipeline } from 'node:stream'; + +/** + * @typedef {(req: IncomingMessage, res: ServerResponse, next: () => void | Promise) => void | Promise} Middleware + * @typedef {AssetEntry & { type?: string, cache_control?: string }} Asset + * @typedef {Asset | { location: string }} Entry + */ + +/** + * Splits `req.url` into a decoded pathname and the search string. + * Decoding follows kit's router: reserved characters such as `%2F` stay + * encoded. An undecodable pathname is returned as-is, so it misses the + * asset table and falls through to SvelteKit's 400 + * @param {import('node:http').IncomingMessage} req + */ +function split_url(req) { + let pathname = /** @type {string} */ (req.url); + let search = ''; + + const query_index = pathname.indexOf('?'); + if (query_index !== -1) { + search = pathname.slice(query_index); + pathname = pathname.slice(0, query_index); + } + + if (pathname.includes('%')) { + try { + pathname = pathname.split('%25').map(decodeURI).join('%25'); + } catch { + // invalid URI + } + } + + return { pathname, search }; +} + +/** + * Relative reference from `from` to `to`, which must differ only by a trailing slash. + * Keep in sync with the copy in `packages/kit/src/utils/url.js` + * @param {string} from + * @param {string} to + * @returns {string} + */ +function relative_pathname(from, to) { + const segment = to.replace(/\/$/, '').split('/').at(-1); + + // The prefix prevents a colon in the segment from being interpreted as a URL scheme. + return from.endsWith('/') ? `../${segment}` : `./${segment}/`; +} + +/** + * Parses `Accept-Encoding` and picks the preferred variant that exists + * @param {string | undefined} header + * @param {Asset} asset + * @returns {'br' | 'gz' | undefined} + */ +function negotiate(header, asset) { + if (!header || !(asset.br || asset.gz)) return; + + /** @type {Map} */ + const weights = new Map(); + + for (const part of header.toLowerCase().split(',')) { + const [coding, ...params] = part.split(';'); + let weight = 1; + + for (const param of params) { + const [name, value] = param.split('='); + if (name.trim() === 'q') weight = parseFloat(value) || 0; + } + + weights.set(coding.trim(), weight); + } + + /** @param {string} coding */ + const weight = (coding) => weights.get(coding) ?? weights.get('*') ?? 0; + + const br = asset.br ? weight('br') : 0; + const gzip = asset.gz ? weight('gzip') : 0; + + if (gzip > br) return 'gz'; + if (br > 0) return 'br'; +} + +/** + * Whether an `If-None-Match` value matches `etag`, using weak comparison + * @param {string | undefined} header + * @param {string} etag + */ +function etag_matches(header, etag) { + if (!header) return false; + if (header.trim() === '*') return true; + + return header.split(',').some((tag) => tag.trim().replace(/^W\//, '') === etag); +} + +/** + * Absolute file paths and content types for one table + * @param {string} dir + * @param {AssetTable} table + * @param {Record} mime_types + * @returns {Map} + */ +function resolve(dir, table, mime_types) { + /** @type {Map} */ + const files = new Map(); + + for (const [key, entry] of table.entries) { + let type = mime_types[entry.file.slice(entry.file.lastIndexOf('.'))]; + if (type === 'text/html') type += ';charset=utf-8'; + files.set(key, { ...entry, file: path.join(dir, entry.file), type }); + } + + for (const [alias, key] of table.aliases) { + files.set(alias, /** @type {Asset} */ (files.get(key))); + } + + return files; +} + +/** + * One lookup for every request, decided at boot: client assets (immutable below + * `app_path`), prerendered pages, and a 308 from the non-canonical trailing-slash + * form of a prerendered path to the canonical one. Client assets win a collision + * @param {{ + * dir: string, + * base: string, + * app_path: string, + * mime_types: Record, + * assets: AssetTable, + * prerendered_assets: AssetTable + * }} opts + * @returns {Map} + */ +export function create_file_map({ dir, base, app_path, mime_types, assets, prerendered_assets }) { + /** @type {Map} */ + const files = resolve(`${dir}/client${base}`, assets, mime_types); + + const immutable = `/${app_path}/immutable/`; + for (const [key, asset] of files) { + if (key.startsWith(immutable)) { + /** @type {Asset} */ (asset).cache_control = 'public,max-age=31536000,immutable'; + } + } + + const prerendered = resolve(`${dir}/prerendered${base}`, prerendered_assets, mime_types); + for (const [key, asset] of prerendered) { + if (!files.has(key)) files.set(key, asset); + } + + for (const key of prerendered.keys()) { + const inverted = key.at(-1) === '/' ? key.slice(0, -1) : key + '/'; + if (inverted && !files.has(inverted)) { + files.set(inverted, { location: relative_pathname(inverted, key) }); + } + } + + return files; +} + +/** + * Serves the closed set of files recorded at adapt time. Everything about a + * response is decided before the first request, so a request is one map + * lookup, header negotiation and a stream + * @param {Map} files + * @returns {Middleware} + */ +export function serve_static(files) { + return (req, res, next) => { + const { pathname, search } = split_url(req); + + const asset = files.get(pathname); + if (!asset) return next(); + + if (req.method !== 'GET' && req.method !== 'HEAD') { + res.writeHead(405, { allow: 'GET, HEAD' }).end(); + return; + } + + if ('location' in asset) { + res.writeHead(308, { location: asset.location + search }).end(); + return; + } + + let file = asset.file; + let size = asset.size; + let etag = `"${asset.etag}"`; + + const variant = negotiate(req.headers['accept-encoding'], asset); + if (variant) { + size = /** @type {number} */ (asset[variant]); + file += `.${variant}`; + etag = `"${asset.etag}.${variant}"`; + } + + /** @type {Record} */ + const headers = { etag }; + + if (asset.br || asset.gz) headers.vary = 'Accept-Encoding'; + if (asset.cache_control) headers['cache-control'] = asset.cache_control; + + if (etag_matches(req.headers['if-none-match'], etag)) { + res.writeHead(304, headers).end(); + return; + } + headers['content-length'] = size; + headers['accept-ranges'] = 'bytes'; + if (asset.type) headers['content-type'] = asset.type; + if (variant) headers['content-encoding'] = variant === 'gz' ? 'gzip' : 'br'; + + /** @type {{ start?: number, end?: number }} */ + const range = {}; + let status = 200; + + // a stale `If-Range` validator means the client's partial copy is of an older + // representation, so it gets the whole current one + const if_range = req.headers['if-range']; + if (req.headers.range && (!if_range || if_range === etag)) { + const match = /^bytes=(\d*)-(\d*)$/.exec(req.headers.range); + + if (match && (match[1] || match[2])) { + let start = match[1] ? parseInt(match[1], 10) : NaN; + let end = match[2] ? parseInt(match[2], 10) : size - 1; + + if (isNaN(start)) { + // suffix range: the last `match[2]` bytes + start = Math.max(size - end, 0); + end = size - 1; + } else { + end = Math.min(end, size - 1); + } + + if (start >= size || start > end) { + res.writeHead(416, { 'content-range': `bytes */${size}` }).end(); + return; + } + + status = 206; + headers['content-range'] = `bytes ${start}-${end}/${size}`; + headers['content-length'] = end - start + 1; + range.start = start; + range.end = end; + } + } + + res.writeHead(status, headers); + + if (req.method === 'HEAD') { + res.end(); + return; + } + + // the headers are already sent, so a failed read can only drop the connection. + // `pipeline` also closes the file when the client goes away mid-transfer + pipeline(fs.createReadStream(file, range), res, () => {}); + }; +} diff --git a/packages/adapter-node/src/static.spec.ts b/packages/adapter-node/src/static.spec.ts new file mode 100644 index 000000000000..d26fff4098f1 --- /dev/null +++ b/packages/adapter-node/src/static.spec.ts @@ -0,0 +1,280 @@ +import { createHash } from 'node:crypto'; +import fs from 'node:fs'; +import http from 'node:http'; +import os from 'node:os'; +import path from 'node:path'; +import { brotliCompressSync, gzipSync } from 'node:zlib'; +import { afterAll, beforeAll, expect, test } from 'vitest'; +import { create_file_map, serve_static } from './static.js'; + +const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'adapter-node-static-')); +const client = path.join(dir, 'client'); +const prerendered = path.join(dir, 'prerendered'); + +/** writes a file the way `builder.writeClient` + `builder.compress` would, and records it like `adapt` does */ +function write(root: string, file: string, content: string, compress = false): AssetEntry { + const abs = path.join(root, file); + fs.mkdirSync(path.dirname(abs), { recursive: true }); + fs.writeFileSync(abs, content); + + const entry: AssetEntry = { + file, + size: Buffer.byteLength(content), + etag: createHash('sha256').update(content).digest('base64url') + }; + + if (compress) { + const gz = gzipSync(content); + const br = brotliCompressSync(content); + fs.writeFileSync(`${abs}.gz`, gz); + fs.writeFileSync(`${abs}.br`, br); + entry.gz = gz.length; + entry.br = br.length; + } + + return entry; +} + +const range = write(client, 'range.txt', '0123456789', true); +const plain = write(client, 'plain.txt', 'plain'); +const plus = write(client, 'a+b.txt', 'plus'); +const page = write(client, 'page.html', '

page

', true); +const sub = write(client, 'sub/index.html', '

sub

'); +const chunk = write(client, '_app/immutable/chunks/x.js', 'export {}'); +const version = write(client, '_app/version.json', '{}'); +const about = write(prerendered, 'about.html', '

about

'); + +const files = create_file_map({ + dir, + base: '', + app_path: '_app', + mime_types: { + '.txt': 'text/plain', + '.html': 'text/html', + '.js': 'text/javascript', + '.json': 'application/json' + }, + assets: { + entries: [ + ['/range.txt', range], + ['/plain.txt', plain], + ['/a+b.txt', plus], + ['/page.html', page], + ['/sub/index.html', sub], + ['/_app/immutable/chunks/x.js', chunk], + ['/_app/version.json', version] + ], + aliases: [ + ['/page', '/page.html'], + ['/sub/', '/sub/index.html'] + ] + }, + prerendered_assets: { + entries: [ + ['/about', about], + ['/always/http:example.com/', about], + ['/always/https:example.com/', about], + ['/never/http:example.com', about], + ['/never/https:example.com', about] + ], + aliases: [] + } +}); + +const middleware = serve_static(files); +const server = http.createServer((req, res) => + middleware(req, res, () => { + res.statusCode = 404; + res.end('next'); + }) +); + +let origin: string; + +beforeAll(async () => { + await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); + const address = server.address() as { port: number }; + origin = `http://127.0.0.1:${address.port}`; +}); + +afterAll(async () => { + await new Promise((resolve) => server.close(resolve)); + fs.rmSync(dir, { recursive: true, force: true }); +}); + +function get(pathname: string, { method = 'GET', headers = {} as Record } = {}) { + return new Promise<{ status: number; headers: http.IncomingHttpHeaders; body: string }>( + (resolve, reject) => { + http + .request(origin + pathname, { method, headers }, (res) => { + const chunks: Buffer[] = []; + res.on('data', (chunk) => chunks.push(chunk)); + res.on('end', () => + resolve({ + status: res.statusCode ?? 0, + headers: res.headers, + body: Buffer.concat(chunks).toString() + }) + ); + }) + .on('error', reject) + .end(); + } + ); +} + +test('serves a recorded file with its precomputed headers', async () => { + const response = await get('/plain.txt'); + expect(response.status).toBe(200); + expect(response.body).toBe('plain'); + expect(response.headers['content-type']).toBe('text/plain'); + expect(response.headers['content-length']).toBe('5'); + expect(response.headers['accept-ranges']).toBe('bytes'); + expect(response.headers['etag']).toBe(`"${plain.etag}"`); + expect(response.headers['vary']).toBeUndefined(); + + const head = await get('/plain.txt', { method: 'HEAD' }); + expect(head.headers['content-length']).toBe('5'); + expect(head.body).toBe(''); +}); + +test('passes unknown paths to the next handler', async () => { + expect((await get('/missing.txt')).status).toBe(404); +}); + +test('disallows non-GET/HEAD methods on recorded paths only', async () => { + for (const pathname of ['/plain.txt', '/about/']) { + const post = await get(pathname, { method: 'POST' }); + expect(post.status).toBe(405); + expect(post.headers['allow']).toBe('GET, HEAD'); + } + + expect((await get('/missing.txt', { method: 'POST' })).body).toBe('next'); +}); + +test('decodes percent-encoding but not reserved characters or +', async () => { + // https://github.com/sveltejs/kit/issues/11766 + expect((await get('/pl%61in.txt')).body).toBe('plain'); + expect((await get('/a+b.txt')).body).toBe('plus'); + expect((await get('/a%2Bb.txt')).status).toBe(404); + expect((await get('/sub%2Findex.html')).status).toBe(404); +}); + +test('resolves aliases to their html file', async () => { + const page = await get('/page'); + expect(page.body).toBe('

page

'); + expect(page.headers['content-type']).toBe('text/html;charset=utf-8'); + expect((await get('/sub/')).body).toBe('

sub

'); +}); + +test('marks assets below the immutable directory only', async () => { + const immutable = 'public,max-age=31536000,immutable'; + expect((await get('/_app/immutable/chunks/x.js')).headers['cache-control']).toBe(immutable); + expect((await get('/_app/version.json')).headers['cache-control']).toBeUndefined(); +}); + +test('serves prerendered pages and redirects their non-canonical trailing-slash form', async () => { + expect((await get('/about')).body).toBe('

about

'); + + const redirect = await get('/about/?x=1'); + expect(redirect.status).toBe(308); + expect(redirect.headers['location']).toBe('../about?x=1'); +}); + +test('keeps scheme-like prerendered paths on the original origin', async () => { + for (const segment of ['http:example.com', 'https:example.com']) { + for (const trailing_slash of ['always', 'never']) { + const pathname = `/${trailing_slash}/${segment}`; + const from = trailing_slash === 'always' ? pathname : `${pathname}/`; + const to = trailing_slash === 'always' ? `${pathname}/` : pathname; + const response = await get(`${from}?ref=test`); + const location = response.headers['location']; + + expect(response.status).toBe(308); + for (const protocol of ['https:', 'http:']) { + const base = new URL(`${protocol}//internal/mount${from}?ref=test`); + const target = new URL(location!, base); + expect(target.origin).toBe(base.origin); + expect(target.pathname).toBe(`/mount${to}`); + expect(target.search).toBe(base.search); + } + expect(location).toBe( + `${trailing_slash === 'always' ? `./${segment}/` : `../${segment}`}?ref=test` + ); + } + } +}); + +test('answers a matching if-none-match with a 304 that carries the validators', async () => { + const etag = `"${range.etag}"`; + + for (const header of [etag, `"stale", ${etag}`, `W/${etag}`, '*']) { + const cached = await get('/range.txt', { headers: { 'if-none-match': header } }); + expect(cached.status, header).toBe(304); + expect(cached.body).toBe(''); + expect(cached.headers['etag']).toBe(etag); + expect(cached.headers['vary']).toBe('Accept-Encoding'); + } + + expect((await get('/range.txt', { headers: { 'if-none-match': '"stale"' } })).status).toBe(200); +}); + +test('serves byte ranges', async () => { + const single = await get('/range.txt', { headers: { range: 'bytes=0-0' } }); + expect(single.status).toBe(206); + expect(single.headers['content-range']).toBe('bytes 0-0/10'); + expect(single.headers['content-length']).toBe('1'); + expect(single.body).toBe('0'); + + const suffix = await get('/range.txt', { headers: { range: 'bytes=-3' } }); + expect(suffix.headers['content-range']).toBe('bytes 7-9/10'); + expect(suffix.body).toBe('789'); + + const open = await get('/range.txt', { headers: { range: 'bytes=4-' } }); + expect(open.body).toBe('456789'); + + const clamped = await get('/range.txt', { headers: { range: 'bytes=8-100' } }); + expect(clamped.headers['content-range']).toBe('bytes 8-9/10'); + + const unsatisfiable = await get('/range.txt', { headers: { range: 'bytes=10-' } }); + expect(unsatisfiable.status).toBe(416); + expect(unsatisfiable.headers['content-range']).toBe('bytes */10'); + + // a stale `If-Range` validator gets the whole current representation + const etag = `"${range.etag}"`; + const fresh = await get('/range.txt', { headers: { range: 'bytes=0-1', 'if-range': etag } }); + expect(fresh.status).toBe(206); + const stale = await get('/range.txt', { headers: { range: 'bytes=0-1', 'if-range': '"stale"' } }); + expect(stale.status).toBe(200); + expect(stale.body).toBe('0123456789'); +}); + +test('negotiates the compressed variant', async () => { + const gzip = await get('/range.txt', { headers: { 'accept-encoding': 'gzip' } }); + expect(gzip.headers['content-encoding']).toBe('gzip'); + expect(gzip.headers['content-length']).toBe(String(range.gz)); + expect(gzip.headers['vary']).toBe('Accept-Encoding'); + + const preferred = await get('/range.txt', { + headers: { 'accept-encoding': 'gzip;q=1, br;q=0.5' } + }); + expect(preferred.headers['content-encoding']).toBe('gzip'); + + const upper = await get('/range.txt', { headers: { 'accept-encoding': 'BR' } }); + expect(upper.headers['content-encoding']).toBe('br'); + + const wildcard = await get('/range.txt', { headers: { 'accept-encoding': '*' } }); + expect(wildcard.headers['content-encoding']).toBe('br'); + + const rejected = await get('/range.txt', { headers: { 'accept-encoding': 'br;q=0, gzip;q=0' } }); + expect(rejected.headers['content-encoding']).toBeUndefined(); + expect(rejected.body).toBe('0123456789'); + + // each representation has its own validator and its own range base + const br = await get('/range.txt', { headers: { 'accept-encoding': 'br' } }); + expect(br.headers['etag']).toBe(`"${range.etag}.br"`); + const partial = await get('/range.txt', { + headers: { 'accept-encoding': 'br', range: 'bytes=0-0' } + }); + expect(partial.headers['content-range']).toBe(`bytes 0-0/${range.br}`); +}); diff --git a/packages/adapter-node/src/utils.js b/packages/adapter-node/src/utils.js new file mode 100644 index 000000000000..95b78986cc4a --- /dev/null +++ b/packages/adapter-node/src/utils.js @@ -0,0 +1,46 @@ +import { format } from 'node:url'; + +/** + * Parses the given value into number of bytes. + * + * @param {string} value - Size in bytes. Can also be specified with a unit suffix kilobytes (K), megabytes (M), or gigabytes (G). + * @returns {number} + */ +export function parse_as_bytes(value) { + const multiplier = + { + K: 1024, + M: 1024 * 1024, + G: 1024 * 1024 * 1024 + }[value[value.length - 1]?.toUpperCase()] ?? 1; + return Number(multiplier != 1 ? value.substring(0, value.length - 1) : value) * multiplier; +} + +/** + * Formats the address the server is listening on. + * + * @param {string | false} path + * @param {string} host + * @param {string | false} port + * @param {import('node:net').AddressInfo | string | null} address + * @returns {string} + */ +export function format_listening_address(path, host, port, address) { + if (path) { + return path; + } + + if (address && typeof address === 'object') { + return format({ + protocol: 'http:', + hostname: address.address, + port: address.port + }); + } + + return format({ + protocol: 'http:', + hostname: host, + port: String(port) + }); +} diff --git a/packages/adapter-node/src/utils.spec.ts b/packages/adapter-node/src/utils.spec.ts new file mode 100644 index 000000000000..6af2f2612f86 --- /dev/null +++ b/packages/adapter-node/src/utils.spec.ts @@ -0,0 +1,50 @@ +import { expect, test, describe } from 'vitest'; +import { format_listening_address, parse_as_bytes } from './utils.js'; + +describe('parse_as_bytes', () => { + test.each([ + ['200', 200], + ['512K', 512 * 1024], + ['200M', 200 * 1024 * 1024], + ['1G', 1024 * 1024 * 1024], + ['Infinity', Infinity], + ['asdf', NaN] + ] as const)('parses correctly', (input, expected) => { + const actual = parse_as_bytes(input); + expect(actual, `Testing input '${input}'`).toBe(expected); + }); +}); + +describe('format_listening_address', () => { + test('uses the actual listening port assigned by the server', () => { + expect( + format_listening_address(false, '0.0.0.0', '0', { + address: '0.0.0.0', + family: 'IPv4', + port: 43521 + }) + ).toBe('http://0.0.0.0:43521'); + }); + + test('formats IPv6 addresses as valid URLs', () => { + expect( + format_listening_address(false, '::1', '3000', { + address: '::1', + family: 'IPv6', + port: 3000 + }) + ).toBe('http://[::1]:3000'); + }); + + test('falls back to configured host and port when the address is unavailable', () => { + expect(format_listening_address(false, 'localhost', '3000', null)).toBe( + 'http://localhost:3000' + ); + }); + + test('returns the socket path unchanged', () => { + expect(format_listening_address('/tmp/sveltekit.sock', '0.0.0.0', '3000', null)).toBe( + '/tmp/sveltekit.sock' + ); + }); +}); diff --git a/packages/adapter-node/test/apps/basic/build.test.js b/packages/adapter-node/test/apps/basic/build.test.js new file mode 100644 index 000000000000..deb6023a098d --- /dev/null +++ b/packages/adapter-node/test/apps/basic/build.test.js @@ -0,0 +1,26 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { afterEach, expect, test, vi } from 'vitest'; + +afterEach(() => vi.unstubAllEnvs()); + +test('exports the handler', async () => { + vi.stubEnv('MY_CUSTOM_PORT', '5173'); + vi.stubEnv('INSTRUMENTATION_ENV', 'available'); + const { handler } = await import('./build/handler.js'); + expect(handler).toBeDefined(); +}); + +const build = path.resolve(import.meta.dirname, 'build'); + +function server_source() { + return fs + .readdirSync(build, { encoding: 'utf8', recursive: true }) + .filter((file) => file.endsWith('.js')) + .map((file) => fs.readFileSync(path.join(build, file), 'utf8')) + .join('\n'); +} + +test('dependencies are not bundled', () => { + expect(server_source()).not.toContain('server-side-dep implementation'); +}); diff --git a/packages/adapter-node/test/apps/basic/package.json b/packages/adapter-node/test/apps/basic/package.json index 5686e13c1011..9868553af83a 100644 --- a/packages/adapter-node/test/apps/basic/package.json +++ b/packages/adapter-node/test/apps/basic/package.json @@ -4,9 +4,9 @@ "private": true, "scripts": { "dev": "vite dev", - "build": "vite build", - "preview": "MY_CUSTOM_PORT=5173 node build", - "prepare": "svelte-kit sync || echo ''", + "build": "MY_CUSTOM_PORT=5173 INSTRUMENTATION_ENV=available vite build", + "preview": "MY_CUSTOM_PORT=5173 INSTRUMENTATION_ENV=available node build", + "test:build": "pnpm build && vitest run build.test.js", "test:platform": "playwright test" }, "devDependencies": { @@ -14,7 +14,12 @@ "@sveltejs/kit": "workspace:^", "@sveltejs/vite-plugin-svelte": "catalog:", "svelte": "catalog:", - "vite": "catalog:" + "vite": "catalog:", + "vitest": "catalog:" + }, + "dependencies": { + "server-side-dep": "file:server-side-dep", + "server-side-svelte-dep": "file:server-side-svelte-dep" }, "type": "module" } diff --git a/packages/adapter-node/test/apps/basic/server-side-dep/index.d.ts b/packages/adapter-node/test/apps/basic/server-side-dep/index.d.ts new file mode 100644 index 000000000000..8b5127f912f1 --- /dev/null +++ b/packages/adapter-node/test/apps/basic/server-side-dep/index.d.ts @@ -0,0 +1 @@ +export function message(): string; diff --git a/packages/adapter-node/test/apps/basic/server-side-dep/index.js b/packages/adapter-node/test/apps/basic/server-side-dep/index.js new file mode 100644 index 000000000000..c282a9e45189 --- /dev/null +++ b/packages/adapter-node/test/apps/basic/server-side-dep/index.js @@ -0,0 +1,4 @@ +/** @type {import('./index.js').message} */ +export function message() { + return 'server-side-dep implementation'; +} diff --git a/packages/adapter-node/test/apps/basic/server-side-dep/package.json b/packages/adapter-node/test/apps/basic/server-side-dep/package.json new file mode 100644 index 000000000000..5b26c9d1855a --- /dev/null +++ b/packages/adapter-node/test/apps/basic/server-side-dep/package.json @@ -0,0 +1,11 @@ +{ + "name": "server-side-dep", + "version": "0.0.1", + "type": "module", + "exports": { + ".": { + "default": "./index.js", + "types": "./index.d.ts" + } + } +} diff --git a/packages/adapter-node/test/apps/basic/server-side-svelte-dep/Hello.svelte b/packages/adapter-node/test/apps/basic/server-side-svelte-dep/Hello.svelte new file mode 100644 index 000000000000..6b4423dce78f --- /dev/null +++ b/packages/adapter-node/test/apps/basic/server-side-svelte-dep/Hello.svelte @@ -0,0 +1 @@ +

Hello from a Svelte dependency

diff --git a/packages/adapter-node/test/apps/basic/server-side-svelte-dep/component.js b/packages/adapter-node/test/apps/basic/server-side-svelte-dep/component.js new file mode 100644 index 000000000000..a6718db49cbc --- /dev/null +++ b/packages/adapter-node/test/apps/basic/server-side-svelte-dep/component.js @@ -0,0 +1 @@ +export { default as Hello } from './Hello.svelte'; diff --git a/packages/adapter-node/test/apps/basic/server-side-svelte-dep/index.d.ts b/packages/adapter-node/test/apps/basic/server-side-svelte-dep/index.d.ts new file mode 100644 index 000000000000..8b5127f912f1 --- /dev/null +++ b/packages/adapter-node/test/apps/basic/server-side-svelte-dep/index.d.ts @@ -0,0 +1 @@ +export function message(): string; diff --git a/packages/adapter-node/test/apps/basic/server-side-svelte-dep/index.js b/packages/adapter-node/test/apps/basic/server-side-svelte-dep/index.js new file mode 100644 index 000000000000..7e1ba0caa2d2 --- /dev/null +++ b/packages/adapter-node/test/apps/basic/server-side-svelte-dep/index.js @@ -0,0 +1,3 @@ +export function message() { + return 'server-side Svelte dependency'; +} diff --git a/packages/adapter-node/test/apps/basic/server-side-svelte-dep/package.json b/packages/adapter-node/test/apps/basic/server-side-svelte-dep/package.json new file mode 100644 index 000000000000..88c52eba1c59 --- /dev/null +++ b/packages/adapter-node/test/apps/basic/server-side-svelte-dep/package.json @@ -0,0 +1,15 @@ +{ + "name": "server-side-svelte-dep", + "version": "0.0.1", + "type": "module", + "exports": { + ".": { + "types": "./index.d.ts", + "svelte": "./index.js" + }, + "./component": { + "svelte": "./component.js", + "default": "./component.js" + } + } +} diff --git a/packages/adapter-node/test/apps/basic/src/env.js b/packages/adapter-node/test/apps/basic/src/env.js new file mode 100644 index 000000000000..bc73178bd163 --- /dev/null +++ b/packages/adapter-node/test/apps/basic/src/env.js @@ -0,0 +1,6 @@ +import { defineEnvVars } from '@sveltejs/kit/env'; + +export const variables = defineEnvVars({ + MY_CUSTOM_PORT: {}, + INSTRUMENTATION_ENV: {} +}); diff --git a/packages/adapter-node/test/apps/basic/src/hooks.server.js b/packages/adapter-node/test/apps/basic/src/hooks.server.js index fda64966449e..008bd4bd68a4 100644 --- a/packages/adapter-node/test/apps/basic/src/hooks.server.js +++ b/packages/adapter-node/test/apps/basic/src/hooks.server.js @@ -1,9 +1,9 @@ -// importing $env in the server hook can cause a cyclical import that crashes the app start up +// importing $app/env/* in the server hook can cause a cyclical import that crashes the app start up // see https://github.com/sveltejs/kit/issues/16092 -import { env } from '$env/dynamic/private'; +import { MY_CUSTOM_PORT } from '$app/env/private'; -/** @type {import('@sveltejs/kit').Handle} */ +/** @type {import('@sveltejs/kit/hooks').Handle} */ export function handle({ event, resolve }) { - if (!env) throw new Error('env should be defined'); + if (!MY_CUSTOM_PORT) throw new Error('MY_CUSTOM_PORT should be defined'); return resolve(event); } diff --git a/packages/adapter-node/test/apps/basic/src/instrumentation.server.js b/packages/adapter-node/test/apps/basic/src/instrumentation.server.js new file mode 100644 index 000000000000..7972f12f8feb --- /dev/null +++ b/packages/adapter-node/test/apps/basic/src/instrumentation.server.js @@ -0,0 +1,4 @@ +import { INSTRUMENTATION_ENV } from '$app/env/private'; + +// @ts-expect-error test-only state shared with the endpoint +globalThis.instrumentation_env = INSTRUMENTATION_ENV; diff --git a/packages/adapter-node/test/apps/basic/src/routes/external-dependency/+server.js b/packages/adapter-node/test/apps/basic/src/routes/external-dependency/+server.js new file mode 100644 index 000000000000..52df90bf8861 --- /dev/null +++ b/packages/adapter-node/test/apps/basic/src/routes/external-dependency/+server.js @@ -0,0 +1,5 @@ +import { message } from 'server-side-dep'; + +export function GET() { + return new Response(message()); +} diff --git a/packages/adapter-node/test/apps/basic/src/routes/instrumentation-env/+server.js b/packages/adapter-node/test/apps/basic/src/routes/instrumentation-env/+server.js new file mode 100644 index 000000000000..1db4adbea1d5 --- /dev/null +++ b/packages/adapter-node/test/apps/basic/src/routes/instrumentation-env/+server.js @@ -0,0 +1,6 @@ +import { json } from '@sveltejs/kit'; + +export function GET() { + // @ts-expect-error test-only state set by instrumentation + return json({ value: globalThis.instrumentation_env }); +} diff --git a/packages/adapter-node/test/apps/basic/src/routes/prerendered-page/+page.js b/packages/adapter-node/test/apps/basic/src/routes/prerendered-page/+page.js new file mode 100644 index 000000000000..189f71e2e1b3 --- /dev/null +++ b/packages/adapter-node/test/apps/basic/src/routes/prerendered-page/+page.js @@ -0,0 +1 @@ +export const prerender = true; diff --git a/packages/adapter-node/test/apps/basic/src/routes/prerendered-page/+page.svelte b/packages/adapter-node/test/apps/basic/src/routes/prerendered-page/+page.svelte new file mode 100644 index 000000000000..e0c562081ba2 --- /dev/null +++ b/packages/adapter-node/test/apps/basic/src/routes/prerendered-page/+page.svelte @@ -0,0 +1 @@ +

prerendered

diff --git a/packages/adapter-node/test/apps/basic/src/routes/prerendered.ico/+server.js b/packages/adapter-node/test/apps/basic/src/routes/prerendered.ico/+server.js new file mode 100644 index 000000000000..ea5bb293b791 --- /dev/null +++ b/packages/adapter-node/test/apps/basic/src/routes/prerendered.ico/+server.js @@ -0,0 +1,7 @@ +export const prerender = true; + +export function GET() { + return new Response(new Uint8Array([0, 0, 1, 0]), { + headers: { 'content-type': 'image/x-icon' } + }); +} diff --git a/packages/adapter-node/test/apps/basic/src/routes/svelte-dependency/+server.js b/packages/adapter-node/test/apps/basic/src/routes/svelte-dependency/+server.js new file mode 100644 index 000000000000..e526142613a2 --- /dev/null +++ b/packages/adapter-node/test/apps/basic/src/routes/svelte-dependency/+server.js @@ -0,0 +1,6 @@ +// this file helps test that svelte packages are correctly bundled +import { message } from 'server-side-svelte-dep'; + +export function GET() { + return new Response(message()); +} diff --git a/packages/adapter-node/test/apps/basic/src/routes/svelte-dependency/component/+page.svelte b/packages/adapter-node/test/apps/basic/src/routes/svelte-dependency/component/+page.svelte new file mode 100644 index 000000000000..9bf3f09d5cd9 --- /dev/null +++ b/packages/adapter-node/test/apps/basic/src/routes/svelte-dependency/component/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/packages/adapter-node/test/apps/basic/static/.hidden b/packages/adapter-node/test/apps/basic/static/.hidden new file mode 100644 index 000000000000..f525718fdb02 --- /dev/null +++ b/packages/adapter-node/test/apps/basic/static/.hidden @@ -0,0 +1 @@ +shh \ No newline at end of file diff --git a/packages/adapter-node/test/apps/basic/static/.well-known/thing.txt b/packages/adapter-node/test/apps/basic/static/.well-known/thing.txt new file mode 100644 index 000000000000..68cfd84c3359 --- /dev/null +++ b/packages/adapter-node/test/apps/basic/static/.well-known/thing.txt @@ -0,0 +1 @@ +wk \ No newline at end of file diff --git a/packages/adapter-node/test/apps/basic/static/a+b.txt b/packages/adapter-node/test/apps/basic/static/a+b.txt new file mode 100644 index 000000000000..b070a6a41f99 --- /dev/null +++ b/packages/adapter-node/test/apps/basic/static/a+b.txt @@ -0,0 +1 @@ +plus \ No newline at end of file diff --git a/packages/adapter-node/test/apps/basic/static/data.json b/packages/adapter-node/test/apps/basic/static/data.json new file mode 100644 index 000000000000..5de601c012b8 --- /dev/null +++ b/packages/adapter-node/test/apps/basic/static/data.json @@ -0,0 +1 @@ +{ "compressible": true } diff --git a/packages/adapter-node/test/apps/basic/static/page.html b/packages/adapter-node/test/apps/basic/static/page.html new file mode 100644 index 000000000000..3ec9ee860d0f --- /dev/null +++ b/packages/adapter-node/test/apps/basic/static/page.html @@ -0,0 +1,2 @@ + +

hi

diff --git a/packages/adapter-node/test/apps/basic/static/range.txt b/packages/adapter-node/test/apps/basic/static/range.txt new file mode 100644 index 000000000000..ad471007bd7f --- /dev/null +++ b/packages/adapter-node/test/apps/basic/static/range.txt @@ -0,0 +1 @@ +0123456789 \ No newline at end of file diff --git a/packages/adapter-node/test/apps/basic/static/sub/index.html b/packages/adapter-node/test/apps/basic/static/sub/index.html new file mode 100644 index 000000000000..838a7c81782a --- /dev/null +++ b/packages/adapter-node/test/apps/basic/static/sub/index.html @@ -0,0 +1,2 @@ + +

index

diff --git a/packages/adapter-node/test/apps/basic/static/test.ico b/packages/adapter-node/test/apps/basic/static/test.ico new file mode 100644 index 000000000000..5b94bb90acc5 Binary files /dev/null and b/packages/adapter-node/test/apps/basic/static/test.ico differ diff --git a/packages/adapter-node/test/apps/basic/static/v1.0/index.html b/packages/adapter-node/test/apps/basic/static/v1.0/index.html new file mode 100644 index 000000000000..88833dd9fc63 --- /dev/null +++ b/packages/adapter-node/test/apps/basic/static/v1.0/index.html @@ -0,0 +1,2 @@ + +

dotted segment

diff --git a/packages/adapter-node/test/apps/basic/test/test.js b/packages/adapter-node/test/apps/basic/test/test.js index a3bae54d9c1f..e2d32367b999 100644 --- a/packages/adapter-node/test/apps/basic/test/test.js +++ b/packages/adapter-node/test/apps/basic/test/test.js @@ -12,6 +12,21 @@ test('CSR', async ({ page }) => { await expect(page.locator('button')).toContainText('Toggle: true'); }); +test('loads external dependencies', async ({ request }) => { + const response = await request.get('/external-dependency'); + expect(await response.text()).toBe('server-side-dep implementation'); +}); + +test('loads bundled Svelte dependencies', async ({ request }) => { + const response = await request.get('/svelte-dependency'); + expect(await response.text()).toBe('server-side Svelte dependency'); +}); + +test('renders components from bundled Svelte dependencies', async ({ page }) => { + await page.goto('/svelte-dependency/component'); + await expect(page.locator('p')).toHaveText('Hello from a Svelte dependency'); +}); + test('sets X-Accel-Buffering header on text/event-stream responses', async ({ request }) => { const response = await request.get('/event-stream'); expect(response.headers()['content-type']).toContain('text/event-stream'); @@ -22,3 +37,64 @@ test('does not set X-Accel-Buffering header on other responses', async ({ reques const response = await request.get('/'); expect(response.headers()['x-accel-buffering']).toBeUndefined(); }); + +test('initializes dynamic env before instrumentation', async ({ request }) => { + const response = await request.get('/instrumentation-env'); + expect(await response.json()).toEqual({ value: 'available' }); +}); + +test('records the size, content hash and compressed variants of each file', async ({ request }) => { + expect(await (await request.get('/a+b.txt')).text()).toBe('plus'); + + const identity = await request.get('/range.txt', { headers: { 'accept-encoding': 'identity' } }); + expect(identity.headers()['content-length']).toBe('10'); + const etag = identity.headers()['etag']; + expect(etag).toBeTruthy(); + const cached = await request.get('/range.txt', { + headers: { 'accept-encoding': 'identity', 'if-none-match': etag } + }); + expect(cached.status()).toBe(304); + + const gzip = await request.get('/range.txt', { headers: { 'accept-encoding': 'gzip' } }); + expect(gzip.headers()['content-encoding']).toBe('gzip'); + expect(await gzip.text()).toBe('0123456789'); + expect(gzip.headers()['etag']).not.toBe(etag); +}); + +test('records aliases for html files', async ({ request }) => { + for (const path of ['/page', '/sub/']) { + const response = await request.get(path); + expect(response.status(), path).toBe(200); + expect(response.headers()['content-type']).toBe('text/html;charset=utf-8'); + } +}); + +test('uses the content types from the manifest', async ({ request }) => { + // https://github.com/sveltejs/kit/issues/13753 + expect((await request.get('/test.ico')).headers()['content-type']).toBe('image/x-icon'); + expect((await request.get('/prerendered.ico')).headers()['content-type']).toBe('image/x-icon'); +}); + +test('does not record dotfiles, except .well-known', async ({ request }) => { + expect((await request.get('/.hidden')).status()).toBe(404); + expect(await (await request.get('/.well-known/thing.txt')).text()).toBe('wk'); +}); + +test('serves prerendered pages and redirects to their canonical path', async ({ request }) => { + const page = await request.get('/prerendered-page'); + expect(page.headers()['content-type']).toBe('text/html;charset=utf-8'); + expect(await page.text()).toContain('prerendered'); + + const redirect = await request.get('/prerendered-page/', { maxRedirects: 0 }); + expect(redirect.status()).toBe(308); + expect(redirect.headers()['location']).toBe('../prerendered-page'); +}); + +test('serves immutable assets with an immutable cache header', async ({ request }) => { + const html = await (await request.get('/')).text(); + const [asset] = /** @type {RegExpMatchArray} */ (html.match(/\/_app\/immutable\/[^"']+\.js/)); + + const response = await request.get(asset); + expect(response.status()).toBe(200); + expect(response.headers()['cache-control']).toBe('public,max-age=31536000,immutable'); +}); diff --git a/packages/adapter-node/test/apps/basic/tsconfig.json b/packages/adapter-node/test/apps/basic/tsconfig.json index c2db702f9bb1..8e210f5f6f5d 100644 --- a/packages/adapter-node/test/apps/basic/tsconfig.json +++ b/packages/adapter-node/test/apps/basic/tsconfig.json @@ -1,13 +1,4 @@ { - "compilerOptions": { - "allowJs": true, - "checkJs": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "skipLibCheck": true, - "sourceMap": true, - "moduleResolution": "bundler" - }, - "extends": "./.svelte-kit/tsconfig.json" + "extends": "$app/tsconfig", + "include": ["src", "test", "*.js"] } diff --git a/packages/adapter-node/test/apps/basic/vite.config.js b/packages/adapter-node/test/apps/basic/vite.config.js index ab8fc581e9d4..71f440a54514 100644 --- a/packages/adapter-node/test/apps/basic/vite.config.js +++ b/packages/adapter-node/test/apps/basic/vite.config.js @@ -10,12 +10,7 @@ const config = { sveltekit({ adapter: adapter({ envPrefix: 'MY_CUSTOM_' - }), - typescript: { - config(config) { - config.include.push('../playwright.config.js'); - } - } + }) }) ] }; diff --git a/packages/adapter-node/test/utils.js b/packages/adapter-node/test/utils.js index c553b42b7534..d4830c2f1a13 100644 --- a/packages/adapter-node/test/utils.js +++ b/packages/adapter-node/test/utils.js @@ -21,7 +21,7 @@ export const config = { ...devices['Desktop Chrome'], screenshot: 'only-on-failure', trace: 'retain-on-failure', - channel: 'chromium' + channel: 'chrome' }, workers: process.env.CI ? 2 : number_from_env('KIT_E2E_WORKERS', undefined), reporter: 'list', diff --git a/packages/adapter-node/tsconfig.json b/packages/adapter-node/tsconfig.json index 12ae3813e896..7d101719d1af 100644 --- a/packages/adapter-node/tsconfig.json +++ b/packages/adapter-node/tsconfig.json @@ -13,15 +13,12 @@ }, "include": [ "index.js", + "vitest.config.js", "src/**/*.js", "src/**/*.ts", "test/utils.js", "tests/**/*.js", "tests/**/*.ts", - "utils.js", - "./internal.d.ts", - "./*.spec.ts", - "rollup.config.js" - ], - "exclude": ["smoke.spec_disabled.js"] + "./internal.d.ts" + ] } diff --git a/packages/adapter-node/utils.js b/packages/adapter-node/utils.js deleted file mode 100644 index 38c543ef39c8..000000000000 --- a/packages/adapter-node/utils.js +++ /dev/null @@ -1,83 +0,0 @@ -import { format } from 'node:url'; - -/** - * Parses the given value into number of bytes. - * - * @param {string} value - Size in bytes. Can also be specified with a unit suffix kilobytes (K), megabytes (M), or gigabytes (G). - * @returns {number} - */ -export function parse_as_bytes(value) { - const multiplier = - { - K: 1024, - M: 1024 * 1024, - G: 1024 * 1024 * 1024 - }[value[value.length - 1]?.toUpperCase()] ?? 1; - return Number(multiplier != 1 ? value.substring(0, value.length - 1) : value) * multiplier; -} - -/** - * Parses and validates an origin URL. - * - * @param {string | undefined} value - Origin URL with http:// or https:// protocol - * @returns {string | undefined} The validated origin, or undefined if value is undefined - * @throws {Error} If value is provided but invalid - */ -export function parse_origin(value) { - if (value === undefined) { - return undefined; - } - - const trimmed = value.trim(); - - let url; - try { - url = new URL(trimmed); - } catch (error) { - throw new Error( - `Invalid ORIGIN: '${trimmed}'. ` + - `ORIGIN must be a valid URL with http:// or https:// protocol. ` + - `For example: 'http://localhost:3000' or 'https://my.site'`, - { cause: error } - ); - } - - if (url.protocol !== 'http:' && url.protocol !== 'https:') { - throw new Error( - `Invalid ORIGIN: '${trimmed}'. ` + - `Only http:// and https:// protocols are supported. ` + - `Received protocol: ${url.protocol}` - ); - } - - return url.origin; -} - -/** - * Formats the address the server is listening on. - * - * @param {string | false} path - * @param {string} host - * @param {string | false} port - * @param {import('node:net').AddressInfo | string | null} address - * @returns {string} - */ -export function format_listening_address(path, host, port, address) { - if (path) { - return path; - } - - if (address && typeof address === 'object') { - return format({ - protocol: 'http:', - hostname: address.address, - port: address.port - }); - } - - return format({ - protocol: 'http:', - hostname: host, - port: String(port) - }); -} diff --git a/packages/adapter-node/utils.spec.ts b/packages/adapter-node/utils.spec.ts deleted file mode 100644 index 09938918d871..000000000000 --- a/packages/adapter-node/utils.spec.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { expect, test, describe } from 'vitest'; -import { format_listening_address, parse_as_bytes, parse_origin } from './utils.js'; - -describe('parse_as_bytes', () => { - test.each([ - ['200', 200], - ['512K', 512 * 1024], - ['200M', 200 * 1024 * 1024], - ['1G', 1024 * 1024 * 1024], - ['Infinity', Infinity], - ['asdf', NaN] - ] as const)('parses correctly', (input, expected) => { - const actual = parse_as_bytes(input); - expect(actual, `Testing input '${input}'`).toBe(expected); - }); -}); - -describe('parse_origin', () => { - test.each([ - ['http://localhost:3000', 'http://localhost:3000'], - ['https://example.com', 'https://example.com'], - ['http://192.168.1.1:8080', 'http://192.168.1.1:8080'], - ['https://my-site.com', 'https://my-site.com'], - ['http://localhost', 'http://localhost'], - // Default ports are normalized by URL.origin per WHATWG URL standard - ['https://example.com:443', 'https://example.com'], - ['http://example.com:80', 'http://example.com'], - [undefined, undefined] - ] as const)('normalizes %s to %s', (input, expected) => { - expect(parse_origin(input)).toBe(expected); - }); - - test.each([ - ['http://localhost:3000/path', 'http://localhost:3000'], - ['http://localhost:3000?query=1', 'http://localhost:3000'], - ['http://localhost:3000#hash', 'http://localhost:3000'], - ['https://example.com/path/to/page', 'https://example.com'], - ['https://example.com:443/path?query=1#hash', 'https://example.com'] - ] as const)('strips path/query/hash from %s to get %s', (input, expected) => { - expect(parse_origin(input)).toBe(expected); - }); - - test.each(['localhost:3000', 'example.com', '', ' ', 'ftp://localhost:3000'] as const)( - 'throws error for invalid origin: %s', - (input) => { - expect(() => parse_origin(input)).toThrow('Invalid ORIGIN'); - } - ); -}); - -describe('format_listening_address', () => { - test('uses the actual listening port assigned by the server', () => { - expect( - format_listening_address(false, '0.0.0.0', '0', { - address: '0.0.0.0', - family: 'IPv4', - port: 43521 - }) - ).toBe('http://0.0.0.0:43521'); - }); - - test('formats IPv6 addresses as valid URLs', () => { - expect( - format_listening_address(false, '::1', '3000', { - address: '::1', - family: 'IPv6', - port: 3000 - }) - ).toBe('http://[::1]:3000'); - }); - - test('falls back to configured host and port when the address is unavailable', () => { - expect(format_listening_address(false, 'localhost', '3000', null)).toBe( - 'http://localhost:3000' - ); - }); - - test('returns the socket path unchanged', () => { - expect(format_listening_address('/tmp/sveltekit.sock', '0.0.0.0', '3000', null)).toBe( - '/tmp/sveltekit.sock' - ); - }); -}); diff --git a/packages/adapter-node/vitest.config.js b/packages/adapter-node/vitest.config.js new file mode 100644 index 000000000000..78b77fe7fa86 --- /dev/null +++ b/packages/adapter-node/vitest.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + include: ['src/**/*.spec.{js,ts}'] + } +}); diff --git a/packages/adapter-static/CHANGELOG.md b/packages/adapter-static/CHANGELOG.md index 9202756132d2..e7e953f27f5b 100644 --- a/packages/adapter-static/CHANGELOG.md +++ b/packages/adapter-static/CHANGELOG.md @@ -1,5 +1,59 @@ # @sveltejs/adapter-static +## 4.0.0-next.4 + +### Major Changes + +- breaking: require SvelteKit 3 + +### Patch Changes + +- chore: use `node:fs` instead of deprecated `builder.rimraf` and `builder.mkdirp` ([#16286](https://github.com/sveltejs/kit/pull/16286)) + +- fix: avoid caching immutable asset 404s when configuring for Vercel + +- fix: match adapter-vercel's prerendered redirect handling when deploying to Vercel + +- fix: allow prerelease versions of SvelteKit 3 to satisfy the peer dependency range +- Updated dependencies [[`bbd1e2f`](https://github.com/sveltejs/kit/commit/bbd1e2fcc9af58edb9846393b081be61df4789d5), [`e21d27b`](https://github.com/sveltejs/kit/commit/e21d27ba490c89da98dbad24fca364d0163137c4), [`6044d05`](https://github.com/sveltejs/kit/commit/6044d055031bc17deedc069dc4016565364bd4f4), [`7fb611f`](https://github.com/sveltejs/kit/commit/7fb611fb604e4d74cd889f5fe21ccd255f876382), [`9f06b3a`](https://github.com/sveltejs/kit/commit/9f06b3ac78be30d27b07fc00ea40bda493c573aa), [`8763327`](https://github.com/sveltejs/kit/commit/87633271ce40ac0839be9468598e965bde3cad51), [`7dc2379`](https://github.com/sveltejs/kit/commit/7dc23798cde97b5616049ba8d443a07e8c558bdc), [`a70c3eb`](https://github.com/sveltejs/kit/commit/a70c3eb31fe1e28f1532c84ec569ec7099da3344), [`89f49c7`](https://github.com/sveltejs/kit/commit/89f49c70b7fb4883cf704d2fb57d2d8595790ad2), [`f36b3a4`](https://github.com/sveltejs/kit/commit/f36b3a4cd307bd8c89e03fa191becc175e7b3077), [`1144a63`](https://github.com/sveltejs/kit/commit/1144a63d318c3c3255d06b374bb7a656a469fabf), [`9f06b3a`](https://github.com/sveltejs/kit/commit/9f06b3ac78be30d27b07fc00ea40bda493c573aa)]: + - @sveltejs/kit@3.0.0-next.15 + +## 4.0.0-next.3 + +### Patch Changes + +- fix: avoid caching immutable asset 404s when configuring for Vercel ([#16626](https://github.com/sveltejs/kit/pull/16626)) +- Updated dependencies [[`c3f58bb`](https://github.com/sveltejs/kit/commit/c3f58bb0eb6bde790897f1e8b89019bc39a062e5), [`b655dc7`](https://github.com/sveltejs/kit/commit/b655dc7cf043ff01d53e494e9744e548bb3ad9fd), [`a70cc4f`](https://github.com/sveltejs/kit/commit/a70cc4f2ca24e7099c898456085ffe18d191657e), [`b325afb`](https://github.com/sveltejs/kit/commit/b325afb509b54445f678348e88ea2bf0152f4cec), [`e76b3d7`](https://github.com/sveltejs/kit/commit/e76b3d778bacb429fdb40a41b13ff04b54d97c1f), [`58f47eb`](https://github.com/sveltejs/kit/commit/58f47eb94016c5405829667bbc873e027c7b363c), [`fe9d8d9`](https://github.com/sveltejs/kit/commit/fe9d8d939b6d1a07b2957104fda6a1123f5eebd3), [`0c7bbd7`](https://github.com/sveltejs/kit/commit/0c7bbd7bd4a346c560e7cbf9b39ba8bda7c718f0), [`08d7e2a`](https://github.com/sveltejs/kit/commit/08d7e2a1b327d4e68c217f6e7d6b305732c82b1b), [`f70f679`](https://github.com/sveltejs/kit/commit/f70f679143d4330b6d3b7d0e690145b5487df97f), [`0624b26`](https://github.com/sveltejs/kit/commit/0624b2678efd41223f70972f7bff2d81d4a5c7f9), [`0624b26`](https://github.com/sveltejs/kit/commit/0624b2678efd41223f70972f7bff2d81d4a5c7f9), [`f4222ad`](https://github.com/sveltejs/kit/commit/f4222adc504a7041b50325424a3eb86af02961ed), [`089628b`](https://github.com/sveltejs/kit/commit/089628b0d9637578085ad61b5786cd1c4476e399), [`d8617b3`](https://github.com/sveltejs/kit/commit/d8617b3f2f08144f6acfc7c636b89d62b982a33d), [`c7369a1`](https://github.com/sveltejs/kit/commit/c7369a148169dfd51af150c461ff266977b6befd), [`5e5e592`](https://github.com/sveltejs/kit/commit/5e5e592f093d05c6ea30a41a0ebf543829069c5f), [`60057c1`](https://github.com/sveltejs/kit/commit/60057c1d2a1e6429e5f315d587ebc01baef6dc94), [`f70f679`](https://github.com/sveltejs/kit/commit/f70f679143d4330b6d3b7d0e690145b5487df97f), [`38f1528`](https://github.com/sveltejs/kit/commit/38f15289e249942275eab1281516702de08b7a30), [`40092f3`](https://github.com/sveltejs/kit/commit/40092f34aa063d033fc7a4d1d6fb1c10abd8cbf6), [`ff4247e`](https://github.com/sveltejs/kit/commit/ff4247e2376862207dd9df7317f39d8bb8dd9ee4), [`79a5de5`](https://github.com/sveltejs/kit/commit/79a5de581fc1bb708879678b8295706221ba0f0f), [`a70cc4f`](https://github.com/sveltejs/kit/commit/a70cc4f2ca24e7099c898456085ffe18d191657e)]: + - @sveltejs/kit@3.0.0-next.14 + +## 4.0.0-next.2 + +### Patch Changes + +- fix: match adapter-vercel's prerendered redirect handling when deploying to Vercel ([#16497](https://github.com/sveltejs/kit/pull/16497)) +- Updated dependencies [[`6942ddb`](https://github.com/sveltejs/kit/commit/6942ddb68b43b5451337a8b420ce05a7a5de9919), [`68137d4`](https://github.com/sveltejs/kit/commit/68137d48cbf921264c6325ffa940165563c0b25a), [`15dc603`](https://github.com/sveltejs/kit/commit/15dc603f5027ee21b261c0c93f7decbf8f9498d8), [`a4f1521`](https://github.com/sveltejs/kit/commit/a4f15213c46f34aac88d05eb9af0905a5f776353), [`629b45d`](https://github.com/sveltejs/kit/commit/629b45d9e68846d862c10006fdabcc1526849811), [`7a7833a`](https://github.com/sveltejs/kit/commit/7a7833af90c0fa70c926cc84d61ca03f9db97bd2), [`e6591a0`](https://github.com/sveltejs/kit/commit/e6591a02743a2b324aa30cd463620450264fabae), [`7a7833a`](https://github.com/sveltejs/kit/commit/7a7833af90c0fa70c926cc84d61ca03f9db97bd2), [`b196a39`](https://github.com/sveltejs/kit/commit/b196a3965dd892a612cc6eae4868e450cc325736), [`66bf343`](https://github.com/sveltejs/kit/commit/66bf343453f8babf719193629a36f084d7bddf75), [`4bde76b`](https://github.com/sveltejs/kit/commit/4bde76b8a7ffa6885878d74eab3e542f1bda9c80), [`d9f05cb`](https://github.com/sveltejs/kit/commit/d9f05cb8efd8682bf91635f517f402849570cb20), [`40efca1`](https://github.com/sveltejs/kit/commit/40efca1b4afd6f0b6329bed652025b99edc8978c), [`8ef5548`](https://github.com/sveltejs/kit/commit/8ef5548f3e63ebb8371099a91acc182f7d094557), [`9efb8f2`](https://github.com/sveltejs/kit/commit/9efb8f2440328a536ebbb70a9e36ce4e30815e8a), [`6a670dc`](https://github.com/sveltejs/kit/commit/6a670dc778561e1fc504c2ccce864a46c2027dcf), [`3416dcc`](https://github.com/sveltejs/kit/commit/3416dccdceddf70ad229b5a1963d0735a7146c01), [`6850fa5`](https://github.com/sveltejs/kit/commit/6850fa56841afc8b1178fe16b6e61ffdfc854ab2), [`e6591a0`](https://github.com/sveltejs/kit/commit/e6591a02743a2b324aa30cd463620450264fabae), [`688bead`](https://github.com/sveltejs/kit/commit/688bead796f60db2384174e4f229c1d558a9ef00), [`b596610`](https://github.com/sveltejs/kit/commit/b596610a0c2e31e7d9adbdfb8d1354a165b5fe23), [`0f26c13`](https://github.com/sveltejs/kit/commit/0f26c133d3c5c19ed375f097b1eb27f6ce1928cd), [`110a3db`](https://github.com/sveltejs/kit/commit/110a3dbdba5f668d3e51b5e9f0d64be5a9ae5b39), [`665d467`](https://github.com/sveltejs/kit/commit/665d46720d26f545cf5ff52c86e44c9b2aa2521b), [`9c8c60c`](https://github.com/sveltejs/kit/commit/9c8c60c26c44e0705091de30a9a485b39d593def)]: + - @sveltejs/kit@3.0.0-next.13 + +## 4.0.0-next.1 + +### Patch Changes + +- fix: allow prerelease versions of SvelteKit 3 to satisfy the peer dependency range ([#16286](https://github.com/sveltejs/kit/pull/16286)) +- Updated dependencies [[`737d119`](https://github.com/sveltejs/kit/commit/737d119730ec163832f838b3813c9cbb75dd799f), [`fa78efb`](https://github.com/sveltejs/kit/commit/fa78efb626031497eecc3a88fa7b6f6da36eca0f), [`07c207e`](https://github.com/sveltejs/kit/commit/07c207ed919570d4ed4954f751056b4f838abdea), [`a47071b`](https://github.com/sveltejs/kit/commit/a47071bb6581fe5f6df4cc47b80e9f0f594624e5), [`14d7d5a`](https://github.com/sveltejs/kit/commit/14d7d5abd9cd8462e4c79ed6a5c10bc43258e97a), [`5c38e51`](https://github.com/sveltejs/kit/commit/5c38e515db7fbb92e5ae01db84b4f0040a02f187), [`0702baa`](https://github.com/sveltejs/kit/commit/0702baa58b2a4600e83b55ef4a85e9cbe3391a4f), [`e1938c6`](https://github.com/sveltejs/kit/commit/e1938c62348b8ccc83e186b1acad76a0a10cc9ea), [`8293144`](https://github.com/sveltejs/kit/commit/8293144b6fbfff9f97cdb1d2224a5eaa04c84fc4), [`f76d7d9`](https://github.com/sveltejs/kit/commit/f76d7d91e18a0afd1f6798c095eb8c170c108cff), [`ab5c253`](https://github.com/sveltejs/kit/commit/ab5c253efb2546ead74db4c8c9b4c7639d97e0ed), [`b557b1b`](https://github.com/sveltejs/kit/commit/b557b1ba76903e1fb0fc3bb87bddc5420332137a), [`4a513e2`](https://github.com/sveltejs/kit/commit/4a513e27ab2ad44dae0c6a4a6c8deda840008530)]: + - @sveltejs/kit@3.0.0-next.8 + +## 4.0.0-next.0 +### Major Changes + + +- breaking: require SvelteKit 3 ([#15506](https://github.com/sveltejs/kit/pull/15506)) + + +### Patch Changes + +- Updated dependencies [[`fa335bd`](https://github.com/sveltejs/kit/commit/fa335bd8af3a982d68c4d34b813b68887dc7cce8), [`3031d89`](https://github.com/sveltejs/kit/commit/3031d89087d49297e89b6566108a1a1105c38475), [`cb9d416`](https://github.com/sveltejs/kit/commit/cb9d416d05266b6237f2856fb94df5b8a9b04279), [`caf3a18`](https://github.com/sveltejs/kit/commit/caf3a18de6581ab7d454d6fa8ad37ebdbdef95e6), [`4777827`](https://github.com/sveltejs/kit/commit/4777827246727045cf0209399751377564e7951d), [`a2792e2`](https://github.com/sveltejs/kit/commit/a2792e2cdc2dee91c20c7bfd921a104b539abae5), [`ba36148`](https://github.com/sveltejs/kit/commit/ba36148577ed0bc2675034a3d57001d71d328cd1), [`48e8710`](https://github.com/sveltejs/kit/commit/48e8710889b348a039322e05f3d2672327d56184), [`e2f3075`](https://github.com/sveltejs/kit/commit/e2f3075ef299b91175fae116e6a76269002029cf), [`047d6a0`](https://github.com/sveltejs/kit/commit/047d6a0f1c3fd1bc6f90a81177a2834b023a64b3), [`87603d1`](https://github.com/sveltejs/kit/commit/87603d13d009cada171c9b5d5daad5adad4f72de), [`096962c`](https://github.com/sveltejs/kit/commit/096962c47b0b0f63acd0d539d3973797e4867487), [`d545970`](https://github.com/sveltejs/kit/commit/d54597018308068aea5268e6c45b26398cd57607), [`e2f3075`](https://github.com/sveltejs/kit/commit/e2f3075ef299b91175fae116e6a76269002029cf), [`d06affc`](https://github.com/sveltejs/kit/commit/d06affc9246c676fc32b644efc0bbf53b42bc67d), [`8af47eb`](https://github.com/sveltejs/kit/commit/8af47eb06f9a1a883058de059ccf81aab4e85627), [`e2f3075`](https://github.com/sveltejs/kit/commit/e2f3075ef299b91175fae116e6a76269002029cf), [`5c4d130`](https://github.com/sveltejs/kit/commit/5c4d130ea2953464d8b4b462eb5aa4641aa78da1), [`3f11f35`](https://github.com/sveltejs/kit/commit/3f11f35125c5b3625891f0d00d9c7c826e6eaea2), [`caf3a18`](https://github.com/sveltejs/kit/commit/caf3a18de6581ab7d454d6fa8ad37ebdbdef95e6), [`8823037`](https://github.com/sveltejs/kit/commit/8823037b4a341013cc40a9bb688ee23bda3dbb04), [`1d76212`](https://github.com/sveltejs/kit/commit/1d76212740f59ed58839ef569899c653870834fd), [`0dc0548`](https://github.com/sveltejs/kit/commit/0dc0548307a439b6a6f24b14405366ced213f828), [`00d81fa`](https://github.com/sveltejs/kit/commit/00d81fab618435bac95b1bacb5e9734b61ba1af6)]: + - @sveltejs/kit@3.0.0-next.0 + ## 3.0.10 ### Patch Changes diff --git a/packages/adapter-static/index.d.ts b/packages/adapter-static/index.d.ts index 101e02fdef12..d0f9b1f4c52c 100644 --- a/packages/adapter-static/index.d.ts +++ b/packages/adapter-static/index.d.ts @@ -1,10 +1,32 @@ import { Adapter } from '@sveltejs/kit'; export interface AdapterOptions { + /** + * The directory to write prerendered pages to. + * @default 'build' + */ pages?: string; + /** + * The directory to write static assets (the contents of `static`, plus client-side JS and CSS generated by SvelteKit) to. Ordinarily this should be the same as `pages`, and it will default to whatever the value of `pages` is, but in rare circumstances you might need to output pages and assets to separate locations. + * @default same as `pages` + */ assets?: string; + /** + * To create a [single page app (SPA)](https://svelte.dev/docs/kit/single-page-apps) you must specify the name of the fallback page to be generated by SvelteKit, which is used as the entry point for URLs that have not been prerendered. This is commonly `200.html`, but can vary depending on your deployment platform. You should avoid `index.html` where possible to avoid conflicting with a prerendered homepage. + * + * > [!NOTE] This option has large negative performance and SEO impacts. It is only recommended in certain circumstances such as wrapping the site in a mobile app. See the [single page apps](https://svelte.dev/docs/kit/single-page-apps) documentation for more details and alternatives. + * @default undefined + */ fallback?: string; + /** + * If `true`, precompresses files with brotli and gzip. This will generate `.br` and `.gz` files. + * @default false + */ precompress?: boolean; + /** + * By default, `adapter-static` checks that either all pages and endpoints (if any) of your app were prerendered, or you have the `fallback` option set. This check exists to prevent you from accidentally publishing an app where some parts of it are not accessible, because they are not contained in the final output. If you know this is ok (for example when a certain page only exists conditionally), you can set `strict` to `false` to turn off this check. + * @default true + */ strict?: boolean; } diff --git a/packages/adapter-static/index.js b/packages/adapter-static/index.js index c2b87301c8a8..559b9fcef1bb 100644 --- a/packages/adapter-static/index.js +++ b/packages/adapter-static/index.js @@ -1,41 +1,53 @@ +import { rmSync } from 'node:fs'; import path from 'node:path'; import { platforms } from './platforms.js'; -/** @type {import('./index.js').default} */ +/** @type {typeof import('./index.js').default} */ export default function (options) { return { name: '@sveltejs/adapter-static', - /** @param {import('./internal.js').Builder2_0_0} builder */ async adapt(builder) { - if (!options?.fallback && builder.config.kit.router?.type !== 'hash') { + if (!options?.fallback && builder.config.router?.type !== 'hash') { const dynamic_routes = builder.routes.filter((route) => route.prerender !== true); if (dynamic_routes.length > 0 && options?.strict !== false) { - const prefix = path.relative('.', builder.config.kit.files.routes); + const prefix = path.relative('.', builder.config.files.routes); const has_param_routes = builder.routes.some((route) => route.id.includes('[')); - const config_option = - has_param_routes || JSON.stringify(builder.config.kit.prerender.entries) !== '["*"]' - ? ` - adjust the \`prerender.entries\` config option ${ - has_param_routes - ? '(routes with parameters are not part of entry points by default)' - : '' - } — see https://svelte.dev/docs/kit/configuration#prerender for more info.` - : ''; builder.log.error( - `@sveltejs/adapter-static: all routes must be fully prerenderable, but found the following routes that are dynamic: -${dynamic_routes.map((route) => ` - ${path.posix.join(prefix, route.id)}`).join('\n')} - -You have the following options: - - set the \`fallback\` option — see https://svelte.dev/docs/kit/single-page-apps#usage for more info. - - add \`export const prerender = true\` to your root \`+layout.js/.ts\` or \`+layout.server.js/.ts\` file. This will try to prerender all pages. - - add \`export const prerender = true\` to any \`+server.js/ts\` files that are not fetched by page \`load\` functions. -${config_option} - - pass \`strict: false\` to \`adapter-static\` to ignore this error. Only do this if you are sure you don't need the routes in question in your final app, as they will be unavailable. See https://github.com/sveltejs/kit/tree/main/packages/adapter-static#strict for more info. - -If this doesn't help, you may need to use a different adapter. @sveltejs/adapter-static can only be used for sites that don't need a server for dynamic rendering, and can run on just a static file server. -See https://svelte.dev/docs/kit/page-options#prerender for more details` + `\n@sveltejs/adapter-static: all routes must be fully prerenderable, but found the following routes that are dynamic: +${dynamic_routes.map((route) => ` - ${path.posix.join(prefix, route.id)}`).join('\n')}\n` ); - throw new Error('Encountered dynamic routes'); + + const options = [ + 'set the `fallback` option — see https://svelte.dev/docs/kit/single-page-apps#usage for more info.', + 'add `export const prerender = true` to your root `+layout.js/.ts` or `+layout.server.js/.ts` file. This will try to prerender all pages.', + 'add `export const prerender = true` to any `+server.js/ts` files that are not fetched by page `load` functions.' + ]; + + if (has_param_routes || JSON.stringify(builder.config.prerender.entries) !== '["*"]') { + let option = 'adjust the `prerender.entries` config option'; + if (has_param_routes) + option += ' (routes with parameters are not part of entry points by default)'; + options.push(option); + } + + options.push( + "pass `strict: false` to `adapter-static` to ignore this error. Only do this if you are sure you don't need the routes in question in your final app, as they will be unavailable. See https://github.com/sveltejs/kit/tree/main/packages/adapter-static#strict for more info." + ); + + builder.log( + `You have the following options:${options.map((o) => `\n - ${o}`).join('')}\n` + ); + + builder.log( + `If this doesn't help, you may need to use a different adapter. @sveltejs/adapter-static can only be used for sites that don't need a server for dynamic rendering, and can run on just a static file server.\n` + ); + + builder.log(`See https://svelte.dev/docs/kit/page-options#prerender for more details\n`); + + const error = new Error('Encountered dynamic routes'); + error.stack = ''; + throw error; } } @@ -52,15 +64,14 @@ See https://svelte.dev/docs/kit/page-options#prerender for more details` } const { - // @ts-ignore pages = 'build', assets = pages, fallback, precompress } = options ?? platform?.defaults ?? /** @type {import('./index.js').AdapterOptions} */ ({}); - builder.rimraf(assets); - builder.rimraf(pages); + rmSync(assets, { force: true, recursive: true }); + rmSync(pages, { force: true, recursive: true }); builder.generateEnvModule(); builder.writeClient(assets); diff --git a/packages/adapter-static/internal.d.ts b/packages/adapter-static/internal.d.ts deleted file mode 100644 index 9857dbae43cd..000000000000 --- a/packages/adapter-static/internal.d.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Utility type that makes all properties optional except for the specified keys - */ -export type PartialExcept = Partial> & Required>; - -/** - * We use a custom `Builder` type here to support the minimum version of SvelteKit. - */ -export type Builder2_0_0 = PartialExcept< - import('@sveltejs/kit').Builder, - | 'log' - | 'rimraf' - | 'mkdirp' - | 'config' - | 'prerendered' - | 'routes' - | 'createEntries' - | 'generateFallback' - | 'generateEnvModule' - | 'generateManifest' - | 'getBuildDirectory' - | 'getClientDirectory' - | 'getServerDirectory' - | 'getAppPath' - | 'writeClient' - | 'writePrerendered' - | 'writeServer' - | 'copy' - | 'compress' ->; diff --git a/packages/adapter-static/package.json b/packages/adapter-static/package.json index 488ef1d88c09..6446f766f06b 100644 --- a/packages/adapter-static/package.json +++ b/packages/adapter-static/package.json @@ -1,6 +1,6 @@ { "name": "@sveltejs/adapter-static", - "version": "3.0.10", + "version": "4.0.0-next.4", "description": "Adapter for SvelteKit apps that prerenders your entire site as a collection of static files", "keywords": [ "adapter", @@ -33,21 +33,20 @@ "platforms.js" ], "scripts": { - "lint": "prettier --check .", - "check": "tsc", - "format": "pnpm lint --write", + "lint": "oxfmt --check .", + "check": "node node_modules/@typescript/native/bin/tsc", + "format": "oxfmt --write .", "test": "pnpm -r --workspace-concurrency 1 --filter=\"./test/**\" test" }, "devDependencies": { "@playwright/test": "catalog:", "@sveltejs/kit": "workspace:^", "@types/node": "catalog:", - "sirv": "^3.0.0", "svelte": "catalog:", - "typescript": "catalog:", + "@typescript/native": "catalog:", "vite": "catalog:" }, "peerDependencies": { - "@sveltejs/kit": "^2.0.0" + "@sveltejs/kit": "^3.0.0-next.0" } } diff --git a/packages/adapter-static/platforms.js b/packages/adapter-static/platforms.js index 6ed421625668..47c59903e0ab 100644 --- a/packages/adapter-static/platforms.js +++ b/packages/adapter-static/platforms.js @@ -6,12 +6,12 @@ import process from 'node:process'; * name: string; * test: () => boolean; * defaults: import('./index.js').AdapterOptions; - * done: (builder: import('./internal.js').Builder2_0_0) => void; + * done: (builder: import('@sveltejs/kit').Builder) => void; * }} * Platform */ // This function is duplicated in adapter-vercel -/** @param {import('./internal.js').Builder2_0_0} builder */ +/** @param {import('@sveltejs/kit').Builder} builder */ function static_vercel_config(builder) { /** @type {any[]} */ const prerendered_redirects = []; @@ -19,7 +19,17 @@ function static_vercel_config(builder) { /** @type {Record} */ const overrides = {}; - for (const [src, redirect] of builder.prerendered.redirects) { + for (let [src, redirect] of builder.prerendered.redirects) { + if (src.replace(/\/$/, '') === redirect.location.replace(/\/$/, '')) { + // ignore the extreme edge case of a `/foo` -> `/foo/` redirect, + // which would only arise if the response was generated by a + // `handle` hook or outside the app altogether (since you + // can't declaratively create both routes) + } else { + // redirect both `/foo` and `/foo/` to `redirect.location` + src = src.replace(/\/?$/, '/?'); + } + prerendered_redirects.push({ src, headers: { @@ -30,16 +40,24 @@ function static_vercel_config(builder) { } for (const [path, page] of builder.prerendered.pages) { - if (path.endsWith('/') && path !== '/') { + let overrides_path = path.slice(1); + + if (path !== '/') { + /** @type {string | undefined} */ + let counterpart_route = path + '/'; + + if (path.endsWith('/')) { + counterpart_route = path.slice(0, -1); + overrides_path = path.slice(1, -1); + } + prerendered_redirects.push( - { src: path, dest: path.slice(0, -1) }, - { src: path.slice(0, -1), status: 308, headers: { Location: path } } + { src: path, dest: counterpart_route }, + { src: counterpart_route, status: 308, headers: { Location: path } } ); - - overrides[page.file] = { path: path.slice(1, -1) }; - } else { - overrides[page.file] = { path: path.slice(1) }; } + + overrides[page.file] = { path: overrides_path }; } return { @@ -54,6 +72,19 @@ function static_vercel_config(builder) { }, { handle: 'filesystem' + }, + // Prevent incorrect caching: if a request to /_app/immutable/* doesn't match + // a static file, return 404 instead of falling through to dynamic routes. + // Otherwise, we could accidentally immutably cache dynamic content served + // by the fallback function. `no-store` stops the earlier immutable header + // from sticking to this 404, so a missing asset isn't cached for a year. + { + src: `/${builder.getAppPath()}/immutable/.+`, + status: 404, + headers: { + 'cache-control': 'no-store' + }, + continue: false } ], overrides diff --git a/packages/adapter-static/test/apps/prerendered/.npmrc b/packages/adapter-static/test/apps/prerendered/.npmrc deleted file mode 100644 index b6f27f135954..000000000000 --- a/packages/adapter-static/test/apps/prerendered/.npmrc +++ /dev/null @@ -1 +0,0 @@ -engine-strict=true diff --git a/packages/adapter-static/test/apps/prerendered/package.json b/packages/adapter-static/test/apps/prerendered/package.json index e2b887afe266..ecca2b33ad32 100644 --- a/packages/adapter-static/test/apps/prerendered/package.json +++ b/packages/adapter-static/test/apps/prerendered/package.json @@ -5,7 +5,7 @@ "scripts": { "dev": "vite dev", "build": "vite build", - "preview": "sirv -p 5173 -s 200.html build", + "preview": "sirv -p 5174 -s 200.html build", "test": "playwright test" }, "devDependencies": { diff --git a/packages/adapter-static/test/apps/prerendered/src/env.ts b/packages/adapter-static/test/apps/prerendered/src/env.ts new file mode 100644 index 000000000000..fa43d644dac4 --- /dev/null +++ b/packages/adapter-static/test/apps/prerendered/src/env.ts @@ -0,0 +1,8 @@ +import { defineEnvVars } from '@sveltejs/kit/env'; + +export const variables = defineEnvVars({ + PUBLIC_ANSWER: { + public: true, + static: true + } +}); diff --git a/packages/adapter-static/test/apps/prerendered/src/routes/public-env/+page.svelte b/packages/adapter-static/test/apps/prerendered/src/routes/public-env/+page.svelte index a36f01afaa54..4be4878c2a01 100644 --- a/packages/adapter-static/test/apps/prerendered/src/routes/public-env/+page.svelte +++ b/packages/adapter-static/test/apps/prerendered/src/routes/public-env/+page.svelte @@ -1,11 +1,10 @@

The answer is {PUBLIC_ANSWER}

{#if browser} -

The dynamic answer is {env.PUBLIC_ANSWER}

+

The dynamic answer is {PUBLIC_ANSWER}

{/if} diff --git a/packages/adapter-static/test/apps/prerendered/svelte.config.js b/packages/adapter-static/test/apps/prerendered/svelte.config.js deleted file mode 100644 index 20cd2b3ff5b8..000000000000 --- a/packages/adapter-static/test/apps/prerendered/svelte.config.js +++ /dev/null @@ -1,10 +0,0 @@ -import adapter from '../../../index.js'; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - kit: { - adapter: adapter() - } -}; - -export default config; diff --git a/packages/adapter-static/test/apps/prerendered/vite.config.js b/packages/adapter-static/test/apps/prerendered/vite.config.js index 29ad08debe6a..dc6532d9578d 100644 --- a/packages/adapter-static/test/apps/prerendered/vite.config.js +++ b/packages/adapter-static/test/apps/prerendered/vite.config.js @@ -1,11 +1,16 @@ import { sveltekit } from '@sveltejs/kit/vite'; +import adapter from '../../../index.js'; /** @type {import('vite').UserConfig} */ const config = { build: { minify: false }, - plugins: [sveltekit()] + plugins: [ + sveltekit({ + adapter: adapter() + }) + ] }; export default config; diff --git a/packages/adapter-static/test/apps/spa/.npmrc b/packages/adapter-static/test/apps/spa/.npmrc deleted file mode 100644 index b6f27f135954..000000000000 --- a/packages/adapter-static/test/apps/spa/.npmrc +++ /dev/null @@ -1 +0,0 @@ -engine-strict=true diff --git a/packages/adapter-static/test/apps/spa/jsconfig.json b/packages/adapter-static/test/apps/spa/jsconfig.json index e0733f11157e..681df462f63f 100644 --- a/packages/adapter-static/test/apps/spa/jsconfig.json +++ b/packages/adapter-static/test/apps/spa/jsconfig.json @@ -1,4 +1,4 @@ { - "extends": "./.svelte-kit/tsconfig.json", + "extends": "$app/tsconfig", "include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"] } diff --git a/packages/adapter-static/test/apps/spa/package.json b/packages/adapter-static/test/apps/spa/package.json index 7860e3591bd1..eee0847712e2 100644 --- a/packages/adapter-static/test/apps/spa/package.json +++ b/packages/adapter-static/test/apps/spa/package.json @@ -5,7 +5,7 @@ "scripts": { "dev": "vite dev", "build": "vite build --mode staging", - "preview": "sirv -p 5173 -s 200.html build", + "preview": "sirv -p 5174 -s 200.html build", "test": "playwright test" }, "devDependencies": { diff --git a/packages/adapter-static/test/apps/spa/src/env.ts b/packages/adapter-static/test/apps/spa/src/env.ts new file mode 100644 index 000000000000..fd970cbbea21 --- /dev/null +++ b/packages/adapter-static/test/apps/spa/src/env.ts @@ -0,0 +1,8 @@ +import { defineEnvVars } from '@sveltejs/kit/env'; + +export const variables = defineEnvVars({ + PUBLIC_VALUE: { + public: true, + static: true + } +}); diff --git a/packages/adapter-static/test/apps/spa/src/routes/fallback/[...rest]/+page.svelte b/packages/adapter-static/test/apps/spa/src/routes/fallback/[...rest]/+page.svelte index 55b4798e99c5..70ce9f5a8682 100644 --- a/packages/adapter-static/test/apps/spa/src/routes/fallback/[...rest]/+page.svelte +++ b/packages/adapter-static/test/apps/spa/src/routes/fallback/[...rest]/+page.svelte @@ -1,5 +1,5 @@

the fallback page was rendered

diff --git a/packages/adapter-static/test/apps/spa/svelte.config.js b/packages/adapter-static/test/apps/spa/svelte.config.js deleted file mode 100644 index da388bfc49d9..000000000000 --- a/packages/adapter-static/test/apps/spa/svelte.config.js +++ /dev/null @@ -1,12 +0,0 @@ -import adapter from '../../../index.js'; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - kit: { - adapter: adapter({ - fallback: '200.html' - }) - } -}; - -export default config; diff --git a/packages/adapter-static/test/apps/spa/vite.config.js b/packages/adapter-static/test/apps/spa/vite.config.js index 29ad08debe6a..7e81282e251e 100644 --- a/packages/adapter-static/test/apps/spa/vite.config.js +++ b/packages/adapter-static/test/apps/spa/vite.config.js @@ -1,11 +1,18 @@ import { sveltekit } from '@sveltejs/kit/vite'; +import adapter from '../../../index.js'; /** @type {import('vite').UserConfig} */ const config = { build: { minify: false }, - plugins: [sveltekit()] + plugins: [ + sveltekit({ + adapter: adapter({ + fallback: '200.html' + }) + }) + ] }; export default config; diff --git a/packages/adapter-static/test/utils.js b/packages/adapter-static/test/utils.js index c553b42b7534..2c438de70a46 100644 --- a/packages/adapter-static/test/utils.js +++ b/packages/adapter-static/test/utils.js @@ -9,7 +9,7 @@ export const config = { timeout: process.env.CI ? 45000 : 15000, webServer: { command: 'pnpm build && pnpm preview', - port: 5173 + port: 5174 }, retries: process.env.CI ? 2 : number_from_env('KIT_E2E_RETRIES', 0), projects: [ @@ -21,7 +21,7 @@ export const config = { ...devices['Desktop Chrome'], screenshot: 'only-on-failure', trace: 'retain-on-failure', - channel: 'chromium' + channel: 'chrome' }, workers: process.env.CI ? 2 : number_from_env('KIT_E2E_WORKERS', undefined), reporter: 'list', diff --git a/packages/adapter-static/tsconfig.json b/packages/adapter-static/tsconfig.json index 7395ffa36b2e..22ea3b1d6c08 100644 --- a/packages/adapter-static/tsconfig.json +++ b/packages/adapter-static/tsconfig.json @@ -11,5 +11,5 @@ }, "types": ["node"] }, - "include": ["index.js", "internal.d.ts", "test/utils.js"] + "include": ["index.js", "test/utils.js"] } diff --git a/packages/adapter-vercel/CHANGELOG.md b/packages/adapter-vercel/CHANGELOG.md index b8d36ea06184..caa6b1f63e4f 100644 --- a/packages/adapter-vercel/CHANGELOG.md +++ b/packages/adapter-vercel/CHANGELOG.md @@ -1,5 +1,130 @@ # @sveltejs/adapter-vercel +## 7.0.0-next.9 + +### Major Changes + +- breaking: populate env vars before `instrumentation.server.js` is evaluated and update the adapter instrumentation API ([#16303](https://github.com/sveltejs/kit/pull/16303)) + +- breaking: replace the `builder.generateManifest` with `builder.generateServerInstance` and `builder.manifest` ([#16875](https://github.com/sveltejs/kit/pull/16875)) + +### Patch Changes + +- fix: pass the requested pathname, including any trailing slash, to ISR functions ([#16903](https://github.com/sveltejs/kit/pull/16903)) + +- fix: trace `process.cwd()`-relative files from the project directory and never glob from the filesystem root ([#16972](https://github.com/sveltejs/kit/pull/16972)) +- Updated dependencies [[`9a4b343`](https://github.com/sveltejs/kit/commit/9a4b343bd8ab0060dc624fbdd139155fc4a0a1dc), [`ee2f55b`](https://github.com/sveltejs/kit/commit/ee2f55b1e3338d2fa92a25fe53d095129edf1002), [`65673a4`](https://github.com/sveltejs/kit/commit/65673a4dd35054c16f8555eb896dfb773d86a556), [`1b79845`](https://github.com/sveltejs/kit/commit/1b79845e117f417b63fa399149816d73882c27ea), [`b12bb78`](https://github.com/sveltejs/kit/commit/b12bb789c17c30e7ff835de4af004d2f3eb36159), [`03f1687`](https://github.com/sveltejs/kit/commit/03f1687fe612ce3d2d9131139b5b188d9cf90c64), [`49f0808`](https://github.com/sveltejs/kit/commit/49f0808f3e983d0cb5a4d586cf0d1678467431ed), [`e7c74bf`](https://github.com/sveltejs/kit/commit/e7c74bf8227dc228cb36e822eaff5992569b830a), [`0876d63`](https://github.com/sveltejs/kit/commit/0876d633145088c7a5213e9f530cf2fed96ce35d), [`9a2b9ba`](https://github.com/sveltejs/kit/commit/9a2b9bacbb2de0533966bb029ef526b357aac086), [`c9f54b5`](https://github.com/sveltejs/kit/commit/c9f54b5d66acd9443d36b0ae9aa0a656809bd7f9), [`22b8152`](https://github.com/sveltejs/kit/commit/22b8152f74882775ac63e1cf96d3016b92ac8256), [`f6d44cf`](https://github.com/sveltejs/kit/commit/f6d44cf007d6c6b54de57f6ae4ef8e2e7fea2f44), [`3c3a27b`](https://github.com/sveltejs/kit/commit/3c3a27bf67a26164266e132dbb7f9266643cf724), [`7f376b1`](https://github.com/sveltejs/kit/commit/7f376b109cd770638806184cf5d8bf4cf88c5bf2), [`8a587c7`](https://github.com/sveltejs/kit/commit/8a587c7b3919408ec66a362eb249fe5cf43f5316), [`aa4f960`](https://github.com/sveltejs/kit/commit/aa4f9605761e686f6aaf414b9186c68ddd2468dc)]: + - @sveltejs/kit@3.0.0-next.27 + +## 7.0.0-next.8 + +### Patch Changes + +- fix: omit ISR data endpoints for server-only routes ([#16731](https://github.com/sveltejs/kit/pull/16731)) + +- fix: correctly apply `reroute` results for apps configured with split serverless functions ([#16665](https://github.com/sveltejs/kit/pull/16665)) +- Updated dependencies [[`9b3d195`](https://github.com/sveltejs/kit/commit/9b3d1955cb40042cbc411637dab3064ddfa6b1a5), [`385d378`](https://github.com/sveltejs/kit/commit/385d378dd1281e1b69417bf4980a169d67a49314), [`3782448`](https://github.com/sveltejs/kit/commit/37824483ca6ccf802942740f0da31b149d09e077), [`d0d3a33`](https://github.com/sveltejs/kit/commit/d0d3a33535cd2f3db1ed09680ccb34624b3abb77), [`4b7a483`](https://github.com/sveltejs/kit/commit/4b7a4830e962ee404161197b3b016182c376ed61), [`e325d7d`](https://github.com/sveltejs/kit/commit/e325d7dfa4313c889d3f9ebea9e70552722c427e), [`4f63c79`](https://github.com/sveltejs/kit/commit/4f63c799ab8dd9ae01eb301d7a9f5d712b724d23)]: + - @sveltejs/kit@3.0.0-next.25 + +## 7.0.0-next.7 + +### Patch Changes + +- fix: ignore `EEXIST` errors when symlinking traced files that resolve to the same destination ([#16809](https://github.com/sveltejs/kit/pull/16809)) +- Updated dependencies [[`6d1e9d7`](https://github.com/sveltejs/kit/commit/6d1e9d70587641a8997f9fc806859370da75e845), [`289afbf`](https://github.com/sveltejs/kit/commit/289afbf3314f50d6160ca75bdf99ed8fbf88cc47), [`7f1db04`](https://github.com/sveltejs/kit/commit/7f1db0430b2ad771fff965990ec42d0fccf9528a), [`7570a62`](https://github.com/sveltejs/kit/commit/7570a62238ac24023157c635c2fbfd887bd6e860), [`6833df1`](https://github.com/sveltejs/kit/commit/6833df1bb799ec802cd2f16fc87b234b1533c003), [`d8efec6`](https://github.com/sveltejs/kit/commit/d8efec6d33c4a84bf9d2caade5896bf175a2f83a), [`fb0e21c`](https://github.com/sveltejs/kit/commit/fb0e21cf32a5b8546ee97ddf18a24a900bb71509), [`6e7d44d`](https://github.com/sveltejs/kit/commit/6e7d44d62434b0bf1e01e646f3bb3fcfc2c3715c), [`bd36e00`](https://github.com/sveltejs/kit/commit/bd36e0049ced1c47ec3be0754894af0aeeecf1bf), [`97ad63a`](https://github.com/sveltejs/kit/commit/97ad63a45c65614081de72ac089e92e8d572bfa2), [`17123bf`](https://github.com/sveltejs/kit/commit/17123bfac392e8941ece37278ee35ba347fe9219), [`2c179e9`](https://github.com/sveltejs/kit/commit/2c179e9b19e1f0c23c1374cea404363a7c12acf3), [`e54a37f`](https://github.com/sveltejs/kit/commit/e54a37f52a0546a6143ba604cfb05043e93dd28a), [`a7f32aa`](https://github.com/sveltejs/kit/commit/a7f32aafa90d6ba3ad16565b6296995544d5a347), [`9a910f3`](https://github.com/sveltejs/kit/commit/9a910f3f5297123b2808b74579d558376b78cf86), [`a610618`](https://github.com/sveltejs/kit/commit/a6106186090e72d2b6d11fae4004b95c7579e75d), [`d455628`](https://github.com/sveltejs/kit/commit/d4556285a0fda2655ca62b25be55003086597cd1), [`4682127`](https://github.com/sveltejs/kit/commit/46821277fe201b7fbe151d4343d59f149ca3a89d), [`e4876e9`](https://github.com/sveltejs/kit/commit/e4876e91ebba0007664bbf06e5a9570d0c2e8844), [`7f1db04`](https://github.com/sveltejs/kit/commit/7f1db0430b2ad771fff965990ec42d0fccf9528a), [`b361b81`](https://github.com/sveltejs/kit/commit/b361b815e79828a6b7404d759d0a4c3892814a4e), [`97ad63a`](https://github.com/sveltejs/kit/commit/97ad63a45c65614081de72ac089e92e8d572bfa2), [`ef70fc7`](https://github.com/sveltejs/kit/commit/ef70fc775ff89a8f86eb62cb5b7a5e01884af563), [`6bfccde`](https://github.com/sveltejs/kit/commit/6bfccdedeefc3df216b050001ae118cd2dbfdcc2), [`7d78680`](https://github.com/sveltejs/kit/commit/7d78680a721c3f337812bdbc2868fc326fde7419), [`b361b81`](https://github.com/sveltejs/kit/commit/b361b815e79828a6b7404d759d0a4c3892814a4e)]: + - @sveltejs/kit@3.0.0-next.24 + +## 7.0.0-next.6 + +### Patch Changes + +- fix: match sibling `.html__route.js` route resolution requests ([#16674](https://github.com/sveltejs/kit/pull/16674)) +- Updated dependencies [[`10b2c64`](https://github.com/sveltejs/kit/commit/10b2c6419378b0340728f66d60c3fd824ae64c56)]: + - @sveltejs/kit@3.0.0-next.16 + +## 7.0.0-next.5 + +### Major Changes + +- chore: use `rolldown` for edge function bundling + +- breaking: edge function build target is now `es2022` + +- breaking: require `vite@^8.0.12`, the first Vite 8 release bundling stable `rolldown` 1.0.0 + +- breaking: remove support for edge and Node 20 runtimes + +### Patch Changes + +- chore: bump `@vercel/nft` dependency + +- chore: use `node:fs` instead of deprecated `builder.rimraf` and `builder.mkdirp` ([#16286](https://github.com/sveltejs/kit/pull/16286)) + +- fix: allow prerelease versions of SvelteKit 3 to satisfy the peer dependency range + +- fix: await `init` on every request to prevent race condition + +- chore: bump Rolldown to `1.2.0` +- Updated dependencies [[`bbd1e2f`](https://github.com/sveltejs/kit/commit/bbd1e2fcc9af58edb9846393b081be61df4789d5), [`e21d27b`](https://github.com/sveltejs/kit/commit/e21d27ba490c89da98dbad24fca364d0163137c4), [`6044d05`](https://github.com/sveltejs/kit/commit/6044d055031bc17deedc069dc4016565364bd4f4), [`7fb611f`](https://github.com/sveltejs/kit/commit/7fb611fb604e4d74cd889f5fe21ccd255f876382), [`9f06b3a`](https://github.com/sveltejs/kit/commit/9f06b3ac78be30d27b07fc00ea40bda493c573aa), [`8763327`](https://github.com/sveltejs/kit/commit/87633271ce40ac0839be9468598e965bde3cad51), [`7dc2379`](https://github.com/sveltejs/kit/commit/7dc23798cde97b5616049ba8d443a07e8c558bdc), [`a70c3eb`](https://github.com/sveltejs/kit/commit/a70c3eb31fe1e28f1532c84ec569ec7099da3344), [`89f49c7`](https://github.com/sveltejs/kit/commit/89f49c70b7fb4883cf704d2fb57d2d8595790ad2), [`f36b3a4`](https://github.com/sveltejs/kit/commit/f36b3a4cd307bd8c89e03fa191becc175e7b3077), [`1144a63`](https://github.com/sveltejs/kit/commit/1144a63d318c3c3255d06b374bb7a656a469fabf), [`9f06b3a`](https://github.com/sveltejs/kit/commit/9f06b3ac78be30d27b07fc00ea40bda493c573aa)]: + - @sveltejs/kit@3.0.0-next.15 + +## 7.0.0-next.4 + +### Major Changes + +- breaking: remove support for edge and Node 20 runtimes ([#16520](https://github.com/sveltejs/kit/pull/16520)) + +### Patch Changes + +- chore: bump `@vercel/nft` dependency ([#16522](https://github.com/sveltejs/kit/pull/16522)) +- Updated dependencies [[`6942ddb`](https://github.com/sveltejs/kit/commit/6942ddb68b43b5451337a8b420ce05a7a5de9919), [`68137d4`](https://github.com/sveltejs/kit/commit/68137d48cbf921264c6325ffa940165563c0b25a), [`15dc603`](https://github.com/sveltejs/kit/commit/15dc603f5027ee21b261c0c93f7decbf8f9498d8), [`a4f1521`](https://github.com/sveltejs/kit/commit/a4f15213c46f34aac88d05eb9af0905a5f776353), [`629b45d`](https://github.com/sveltejs/kit/commit/629b45d9e68846d862c10006fdabcc1526849811), [`7a7833a`](https://github.com/sveltejs/kit/commit/7a7833af90c0fa70c926cc84d61ca03f9db97bd2), [`e6591a0`](https://github.com/sveltejs/kit/commit/e6591a02743a2b324aa30cd463620450264fabae), [`7a7833a`](https://github.com/sveltejs/kit/commit/7a7833af90c0fa70c926cc84d61ca03f9db97bd2), [`b196a39`](https://github.com/sveltejs/kit/commit/b196a3965dd892a612cc6eae4868e450cc325736), [`66bf343`](https://github.com/sveltejs/kit/commit/66bf343453f8babf719193629a36f084d7bddf75), [`4bde76b`](https://github.com/sveltejs/kit/commit/4bde76b8a7ffa6885878d74eab3e542f1bda9c80), [`d9f05cb`](https://github.com/sveltejs/kit/commit/d9f05cb8efd8682bf91635f517f402849570cb20), [`40efca1`](https://github.com/sveltejs/kit/commit/40efca1b4afd6f0b6329bed652025b99edc8978c), [`8ef5548`](https://github.com/sveltejs/kit/commit/8ef5548f3e63ebb8371099a91acc182f7d094557), [`9efb8f2`](https://github.com/sveltejs/kit/commit/9efb8f2440328a536ebbb70a9e36ce4e30815e8a), [`6a670dc`](https://github.com/sveltejs/kit/commit/6a670dc778561e1fc504c2ccce864a46c2027dcf), [`3416dcc`](https://github.com/sveltejs/kit/commit/3416dccdceddf70ad229b5a1963d0735a7146c01), [`6850fa5`](https://github.com/sveltejs/kit/commit/6850fa56841afc8b1178fe16b6e61ffdfc854ab2), [`e6591a0`](https://github.com/sveltejs/kit/commit/e6591a02743a2b324aa30cd463620450264fabae), [`688bead`](https://github.com/sveltejs/kit/commit/688bead796f60db2384174e4f229c1d558a9ef00), [`b596610`](https://github.com/sveltejs/kit/commit/b596610a0c2e31e7d9adbdfb8d1354a165b5fe23), [`0f26c13`](https://github.com/sveltejs/kit/commit/0f26c133d3c5c19ed375f097b1eb27f6ce1928cd), [`110a3db`](https://github.com/sveltejs/kit/commit/110a3dbdba5f668d3e51b5e9f0d64be5a9ae5b39), [`665d467`](https://github.com/sveltejs/kit/commit/665d46720d26f545cf5ff52c86e44c9b2aa2521b), [`9c8c60c`](https://github.com/sveltejs/kit/commit/9c8c60c26c44e0705091de30a9a485b39d593def)]: + - @sveltejs/kit@3.0.0-next.13 + +## 7.0.0-next.3 + +### Patch Changes + +- fix: await `init` on every request to prevent race condition ([#16467](https://github.com/sveltejs/kit/pull/16467)) + +- chore: bump Rolldown to `1.2.0` ([#16455](https://github.com/sveltejs/kit/pull/16455)) +- Updated dependencies [[`adc4c5b`](https://github.com/sveltejs/kit/commit/adc4c5be542ecf7da93ee8d0e90e9d13b952998b), [`ecb0701`](https://github.com/sveltejs/kit/commit/ecb0701b8826e41229f84922f37c12099c66d3bc), [`4b4cc60`](https://github.com/sveltejs/kit/commit/4b4cc60990fa940236639a6cecb6c8efe4287bfa), [`7390dbe`](https://github.com/sveltejs/kit/commit/7390dbe6ef8dff0a1cd923f96daef462a3ce68f2), [`2e71340`](https://github.com/sveltejs/kit/commit/2e713407deb22965777afb01cb8215013ef8bfd3), [`c87bc3a`](https://github.com/sveltejs/kit/commit/c87bc3ac7830f8ee4e76254c9315adcc79ff6a99), [`af15c6c`](https://github.com/sveltejs/kit/commit/af15c6cab2fbfe6bae51dc1fa77a9271df996a47), [`c6fa431`](https://github.com/sveltejs/kit/commit/c6fa43157ae0dbaa9fc61b7e8d2db673e1f9f893), [`bfe4dea`](https://github.com/sveltejs/kit/commit/bfe4deae006f61347b5a5dce7747ec96692e97f9), [`df7dc72`](https://github.com/sveltejs/kit/commit/df7dc7219451bcc86a93e93188d78d664973d9ae), [`5ae11a1`](https://github.com/sveltejs/kit/commit/5ae11a119667c1b24e3b6d17f4b538032ddf1ab1), [`781205c`](https://github.com/sveltejs/kit/commit/781205c41cc5696eb862e5c13356c301a0368983), [`c87bc3a`](https://github.com/sveltejs/kit/commit/c87bc3ac7830f8ee4e76254c9315adcc79ff6a99), [`a1bfeb9`](https://github.com/sveltejs/kit/commit/a1bfeb9de906ae140b9cdc7d7eeaf0ee652a7bcc), [`c87bc3a`](https://github.com/sveltejs/kit/commit/c87bc3ac7830f8ee4e76254c9315adcc79ff6a99), [`9f0127d`](https://github.com/sveltejs/kit/commit/9f0127d4eee3f17dc1db1d2b99b1ca5fc80af784), [`4b4cc60`](https://github.com/sveltejs/kit/commit/4b4cc60990fa940236639a6cecb6c8efe4287bfa), [`8f5b9c7`](https://github.com/sveltejs/kit/commit/8f5b9c72ae930b3b93ef9e1b9ecb9e52664e1d74), [`c9b5544`](https://github.com/sveltejs/kit/commit/c9b554448633e1839e8c0f16c374b00a5ce4d1b6), [`fe1d4a4`](https://github.com/sveltejs/kit/commit/fe1d4a4d68d1d7a3925004fe804209ff54091e34), [`4b4cc60`](https://github.com/sveltejs/kit/commit/4b4cc60990fa940236639a6cecb6c8efe4287bfa), [`5f78e95`](https://github.com/sveltejs/kit/commit/5f78e953ae3966c23a01194dd200f5c57f723e40), [`25510fc`](https://github.com/sveltejs/kit/commit/25510fc79484363277d062e01f2b872f17a473f5), [`c99a6cf`](https://github.com/sveltejs/kit/commit/c99a6cfb7fec133aa85a6de59b43fc442aedc41a), [`17a45ca`](https://github.com/sveltejs/kit/commit/17a45cadb9356403b82c170f0d0027a1ba085667), [`2ca20c3`](https://github.com/sveltejs/kit/commit/2ca20c3ee7348836757dc6f3469217e46957766b), [`eb5c973`](https://github.com/sveltejs/kit/commit/eb5c9737b6d74c53f572b3e047faa5947052ced2)]: + - @sveltejs/kit@3.0.0-next.12 + +## 7.0.0-next.2 + +### Patch Changes + +- fix: allow prerelease versions of SvelteKit 3 to satisfy the peer dependency range ([#16286](https://github.com/sveltejs/kit/pull/16286)) +- Updated dependencies [[`737d119`](https://github.com/sveltejs/kit/commit/737d119730ec163832f838b3813c9cbb75dd799f), [`fa78efb`](https://github.com/sveltejs/kit/commit/fa78efb626031497eecc3a88fa7b6f6da36eca0f), [`07c207e`](https://github.com/sveltejs/kit/commit/07c207ed919570d4ed4954f751056b4f838abdea), [`a47071b`](https://github.com/sveltejs/kit/commit/a47071bb6581fe5f6df4cc47b80e9f0f594624e5), [`14d7d5a`](https://github.com/sveltejs/kit/commit/14d7d5abd9cd8462e4c79ed6a5c10bc43258e97a), [`5c38e51`](https://github.com/sveltejs/kit/commit/5c38e515db7fbb92e5ae01db84b4f0040a02f187), [`0702baa`](https://github.com/sveltejs/kit/commit/0702baa58b2a4600e83b55ef4a85e9cbe3391a4f), [`e1938c6`](https://github.com/sveltejs/kit/commit/e1938c62348b8ccc83e186b1acad76a0a10cc9ea), [`8293144`](https://github.com/sveltejs/kit/commit/8293144b6fbfff9f97cdb1d2224a5eaa04c84fc4), [`f76d7d9`](https://github.com/sveltejs/kit/commit/f76d7d91e18a0afd1f6798c095eb8c170c108cff), [`ab5c253`](https://github.com/sveltejs/kit/commit/ab5c253efb2546ead74db4c8c9b4c7639d97e0ed), [`b557b1b`](https://github.com/sveltejs/kit/commit/b557b1ba76903e1fb0fc3bb87bddc5420332137a), [`4a513e2`](https://github.com/sveltejs/kit/commit/4a513e27ab2ad44dae0c6a4a6c8deda840008530)]: + - @sveltejs/kit@3.0.0-next.8 + +## 7.0.0-next.1 +### Major Changes + + +- breaking: require `vite@^8.0.12`, the first Vite 8 release bundling stable `rolldown` 1.0.0 ([#16134](https://github.com/sveltejs/kit/pull/16134)) + + +### Patch Changes + +- Updated dependencies [[`a9629f1`](https://github.com/sveltejs/kit/commit/a9629f136d3256b14a8b3c10114c99832b977f6c), [`53d37f9`](https://github.com/sveltejs/kit/commit/53d37f93cf1f46995264398d4e54bb27bd1e75dd), [`dba6239`](https://github.com/sveltejs/kit/commit/dba62390e96f9f40c55ddc203744138664742026), [`ab12cb6`](https://github.com/sveltejs/kit/commit/ab12cb6a07269e2a18c0a3867e608e9838c592f3), [`f0eab52`](https://github.com/sveltejs/kit/commit/f0eab522bdea2d2a00f9b5473cd917d678b20daa), [`cd884c1`](https://github.com/sveltejs/kit/commit/cd884c1ffa6f96e08c70486c52fcca20501d5cef), [`9ed38a8`](https://github.com/sveltejs/kit/commit/9ed38a886be8c247c42c887c15d53b405693e1a9), [`53aa049`](https://github.com/sveltejs/kit/commit/53aa049a06ce7b053bdbe6484f05eecdb09d874e), [`984e57a`](https://github.com/sveltejs/kit/commit/984e57ac95d6ca7911611e1c297eecd1b1d870db), [`51785be`](https://github.com/sveltejs/kit/commit/51785be17ac706659d7fd7b3fca3fcf58dcc5de0), [`4f6bcbb`](https://github.com/sveltejs/kit/commit/4f6bcbbbb6e104238c41172ce7dbdaba486bbf43), [`78e7137`](https://github.com/sveltejs/kit/commit/78e71372ca284160df0aefe407999d46795a1299)]: + - @sveltejs/kit@3.0.0-next.5 + +## 7.0.0-next.0 +### Major Changes + + +- chore: use `rolldown` for edge function bundling ([#15432](https://github.com/sveltejs/kit/pull/15432)) + + +- breaking: edge function build target is now `es2022` ([#15432](https://github.com/sveltejs/kit/pull/15432)) + + +### Patch Changes + +- Updated dependencies [[`fa335bd`](https://github.com/sveltejs/kit/commit/fa335bd8af3a982d68c4d34b813b68887dc7cce8), [`3031d89`](https://github.com/sveltejs/kit/commit/3031d89087d49297e89b6566108a1a1105c38475), [`cb9d416`](https://github.com/sveltejs/kit/commit/cb9d416d05266b6237f2856fb94df5b8a9b04279), [`caf3a18`](https://github.com/sveltejs/kit/commit/caf3a18de6581ab7d454d6fa8ad37ebdbdef95e6), [`4777827`](https://github.com/sveltejs/kit/commit/4777827246727045cf0209399751377564e7951d), [`a2792e2`](https://github.com/sveltejs/kit/commit/a2792e2cdc2dee91c20c7bfd921a104b539abae5), [`ba36148`](https://github.com/sveltejs/kit/commit/ba36148577ed0bc2675034a3d57001d71d328cd1), [`48e8710`](https://github.com/sveltejs/kit/commit/48e8710889b348a039322e05f3d2672327d56184), [`e2f3075`](https://github.com/sveltejs/kit/commit/e2f3075ef299b91175fae116e6a76269002029cf), [`047d6a0`](https://github.com/sveltejs/kit/commit/047d6a0f1c3fd1bc6f90a81177a2834b023a64b3), [`87603d1`](https://github.com/sveltejs/kit/commit/87603d13d009cada171c9b5d5daad5adad4f72de), [`096962c`](https://github.com/sveltejs/kit/commit/096962c47b0b0f63acd0d539d3973797e4867487), [`d545970`](https://github.com/sveltejs/kit/commit/d54597018308068aea5268e6c45b26398cd57607), [`e2f3075`](https://github.com/sveltejs/kit/commit/e2f3075ef299b91175fae116e6a76269002029cf), [`d06affc`](https://github.com/sveltejs/kit/commit/d06affc9246c676fc32b644efc0bbf53b42bc67d), [`8af47eb`](https://github.com/sveltejs/kit/commit/8af47eb06f9a1a883058de059ccf81aab4e85627), [`e2f3075`](https://github.com/sveltejs/kit/commit/e2f3075ef299b91175fae116e6a76269002029cf), [`5c4d130`](https://github.com/sveltejs/kit/commit/5c4d130ea2953464d8b4b462eb5aa4641aa78da1), [`3f11f35`](https://github.com/sveltejs/kit/commit/3f11f35125c5b3625891f0d00d9c7c826e6eaea2), [`caf3a18`](https://github.com/sveltejs/kit/commit/caf3a18de6581ab7d454d6fa8ad37ebdbdef95e6), [`8823037`](https://github.com/sveltejs/kit/commit/8823037b4a341013cc40a9bb688ee23bda3dbb04), [`1d76212`](https://github.com/sveltejs/kit/commit/1d76212740f59ed58839ef569899c653870834fd), [`0dc0548`](https://github.com/sveltejs/kit/commit/0dc0548307a439b6a6f24b14405366ced213f828), [`00d81fa`](https://github.com/sveltejs/kit/commit/00d81fab618435bac95b1bacb5e9734b61ba1af6)]: + - @sveltejs/kit@3.0.0-next.0 + ## 6.3.4 ### Patch Changes diff --git a/packages/adapter-vercel/ambient.d.ts b/packages/adapter-vercel/ambient.d.ts deleted file mode 100644 index 67464d3365d9..000000000000 --- a/packages/adapter-vercel/ambient.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { RequestContext } from './index.js'; - -declare global { - namespace App { - export interface Platform { - /** - * `context` is only available in Edge Functions - * - * @deprecated Vercel's context is deprecated. Use [`@vercel/functions`](https://vercel.com/docs/functions/functions-api-reference/vercel-functions-package) instead. - */ - context?: RequestContext; - } - } -} diff --git a/packages/adapter-vercel/files/catch-all.js b/packages/adapter-vercel/files/catch-all.js new file mode 100644 index 000000000000..c7abb536c50b --- /dev/null +++ b/packages/adapter-vercel/files/catch-all.js @@ -0,0 +1,26 @@ +import { applyReroute } from '@sveltejs/kit/adapter'; +import entry from './serverless.js'; + +export default { + /** + * @param {Request} request + * @returns {Promise} + */ + fetch: async (request) => { + const response = await entry.fetch(request); + return applyReroute(response, rewrite); + } +}; + +/** + * @param {URL} url + * @returns {Response} + */ +function rewrite(url) { + // taken from https://github.com/vercel/vercel/blob/main/packages/functions/src/middleware.ts#L106 + return new Response(null, { + headers: { + 'x-middleware-rewrite': String(url) + } + }); +} diff --git a/packages/adapter-vercel/files/edge.js b/packages/adapter-vercel/files/edge.js deleted file mode 100644 index f87bb6e64a98..000000000000 --- a/packages/adapter-vercel/files/edge.js +++ /dev/null @@ -1,68 +0,0 @@ -/* eslint-disable n/prefer-global/process -- - Vercel Edge Runtime does not support node:process */ -import { Server } from 'SERVER'; -import { manifest } from 'MANIFEST'; - -const server = new Server(manifest); - -/** @type {HeadersInit | undefined} */ -let read_headers; -if (process.env.VERCEL_AUTOMATION_BYPASS_SECRET) { - read_headers = { - 'x-vercel-protection-bypass': process.env.VERCEL_AUTOMATION_BYPASS_SECRET - }; -} - -/** - * We don't know the origin until we receive a request, but - * that's guaranteed to happen before we call `read` - * @type {string} - */ -let origin; - -const initialized = server.init({ - env: /** @type {Record} */ (process.env), - read: async (file) => { - const url = `${origin}/${file}`; - const response = await fetch(url, { - // we need to add a bypass header if the user has deployment protection enabled - // see https://vercel.com/docs/deployment-protection/methods-to-bypass-deployment-protection/protection-bypass-automation - headers: read_headers - }); - - if (!response.ok) { - if (response.status === 401) { - throw new Error( - `Please enable Protection Bypass for Automation: https://svelte.dev/docs/kit/adapter-vercel#Troubleshooting-Deployment-protection` - ); - } - - // belt and braces — not sure how we could end up here - throw new Error( - `read(...) failed: could not fetch ${url} (${response.status} ${response.statusText})` - ); - } - - return response.body; - } -}); - -/** - * @param {Request} request - * @param {import('../index.js').RequestContext} context - */ -export default async (request, context) => { - if (!origin) { - origin = new URL(request.url).origin; - await initialized; - } - - return server.respond(request, { - getClientAddress() { - return /** @type {string} */ (request.headers.get('x-forwarded-for')); - }, - platform: { - context - } - }); -}; diff --git a/packages/adapter-vercel/files/serverless.js b/packages/adapter-vercel/files/serverless.js index fb818ba067a5..2db701c77b67 100644 --- a/packages/adapter-vercel/files/serverless.js +++ b/packages/adapter-vercel/files/serverless.js @@ -1,17 +1,12 @@ import { createReadableStream } from '@sveltejs/kit/node'; -import { Server } from 'SERVER'; -import { manifest } from 'MANIFEST'; +import { server } from 'SERVER'; import process from 'node:process'; -const server = new Server(manifest); - await server.init({ - env: /** @type {Record} */ (process.env), + env: process.env, read: createReadableStream }); -const DATA_SUFFIX = '/__data.json'; - export default { /** * @param {Request} request @@ -21,13 +16,10 @@ export default { // If this is an ISR request, the requested pathname is encoded // as a search parameter, so we need to extract it const url = new URL(request.url); - let pathname = url.searchParams.get('__pathname'); + const pathname = url.searchParams.get('__pathname'); if (pathname) { - // Optional routes' pathname replacements look like `/foo/$1/bar` which means we could end up with an url like /foo//bar - pathname = pathname.replace(/\/+/g, '/'); - - url.pathname = pathname + (url.pathname.endsWith(DATA_SUFFIX) ? DATA_SUFFIX : ''); + url.pathname = pathname; url.searchParams.delete('__pathname'); request = new Request(url, request); diff --git a/packages/adapter-vercel/index.d.ts b/packages/adapter-vercel/index.d.ts index e3333d04a88e..8cbdafd1b070 100644 --- a/packages/adapter-vercel/index.d.ts +++ b/packages/adapter-vercel/index.d.ts @@ -1,28 +1,25 @@ import { Adapter } from '@sveltejs/kit'; -import './ambient.js'; -import { RuntimeConfigKey } from './utils.js'; +import { RuntimeKey } from './utils.js'; export default function plugin(config?: Config): Adapter; export interface ServerlessConfig { /** - * Whether to use [Edge Functions](https://vercel.com/docs/concepts/functions/edge-functions) (`'edge'`) or [Serverless Functions](https://vercel.com/docs/concepts/functions/serverless-functions) (`'nodejs22.x'`, `'nodejs24.x'` etc). + * Which [Serverless Function](https://vercel.com/docs/concepts/functions/serverless-functions) runtime to use (`'nodejs22.x'`, `'nodejs24.x'` etc). * @default Same as the build environment */ - runtime?: Exclude; + runtime?: RuntimeKey; /** - * To which regions to deploy the app. A list of regions. + * A list of regions to deploy the app to * More info: https://vercel.com/docs/concepts/edge-network/regions */ regions?: string[]; /** * Maximum execution duration (in seconds) that will be allowed for the Serverless Function. - * Serverless only. */ maxDuration?: number; /** * Amount of memory (RAM in MB) that will be allocated to the Serverless Function. - * Serverless only. */ memory?: number; /** @@ -32,7 +29,6 @@ export interface ServerlessConfig { /** * [Incremental Static Regeneration](https://vercel.com/docs/concepts/incremental-static-regeneration/overview) configuration. - * Serverless only. */ isr?: | { @@ -75,89 +71,9 @@ type ImagesConfig = { contentDispositionType?: string; }; -/** @deprecated */ -export interface EdgeConfig { - /** - * Whether to use [Edge Functions](https://vercel.com/docs/concepts/functions/edge-functions) (`'edge'`) or [Serverless Functions](https://vercel.com/docs/concepts/functions/serverless-functions) (`'nodejs22.x'`, `'nodejs24.x'` etc). - */ - runtime?: 'edge'; - /** - * To which regions to deploy the app. A list of regions or `'all'`. - * More info: https://vercel.com/docs/concepts/edge-network/regions - */ - regions?: string[] | 'all'; - /** - * List of packages that should not be bundled into the Edge Function. - * Edge only. - */ - external?: string[]; - /** - * If `true`, this route will always be deployed as its own separate function - */ - split?: boolean; -} - -export type Config = (EdgeConfig | ServerlessConfig) & { +export type Config = ServerlessConfig & { /** * https://vercel.com/docs/build-output-api/v3/configuration#images */ images?: ImagesConfig; }; - -// we copy the RequestContext interface from `@vercel/edge` because that package can't co-exist with `@types/node`. -// see https://github.com/sveltejs/kit/pull/9280#issuecomment-1452110035 - -/** - * An extension to the standard `Request` object that is passed to every Edge Function. - * - * @deprecated - use [`@vercel/functions`](https://vercel.com/docs/functions/functions-api-reference/vercel-functions-package) instead. - * - * @example - * ```ts - * import type { RequestContext } from '@vercel/edge'; - * - * export default async function handler(request: Request, ctx: RequestContext): Promise { - * // ctx is the RequestContext - * } - * ``` - */ -export interface RequestContext { - /** - * A method that can be used to keep the function running after a response has been sent. - * This is useful when you have an async task that you want to keep running even after the - * response has been sent and the request has ended. - * - * @example - * - * Sending an internal error to an error tracking service - * - * ```ts - * import type { RequestContext } from '@vercel/edge'; - * - * export async function handleRequest(request: Request, ctx: RequestContext): Promise { - * try { - * return await myFunctionThatReturnsResponse(); - * } catch (e) { - * ctx.waitUntil((async () => { - * // report this error to your error tracking service - * await fetch('https://my-error-tracking-service.com', { - * method: 'POST', - * body: JSON.stringify({ - * stack: e.stack, - * message: e.message, - * name: e.name, - * url: request.url, - * }), - * }); - * })()); - * return new Response('Internal Server Error', { status: 500 }); - * } - * } - * ``` - */ - waitUntil( - /** - * A promise that will be kept alive until it resolves or rejects. - */ promise: Promise - ): void; -} diff --git a/packages/adapter-vercel/index.js b/packages/adapter-vercel/index.js index 9369f8cbb026..7cec5f02109a 100644 --- a/packages/adapter-vercel/index.js +++ b/packages/adapter-vercel/index.js @@ -1,54 +1,29 @@ -/** @import { BuildOptions } from 'esbuild' */ import fs from 'node:fs'; import path from 'node:path'; import process from 'node:process'; import { fileURLToPath } from 'node:url'; -import { nodeFileTrace } from '@vercel/nft'; -import esbuild from 'esbuild'; -import { get_pathname, parse_isr_expiration, pattern_to_src, resolve_runtime } from './utils.js'; import { VERSION } from '@sveltejs/kit'; +import { nodeFileTrace } from '@vercel/nft'; +import { parse_isr_expiration, pattern_to_src, resolve_runtime } from './utils.js'; -/** - * @template T - * @template {keyof T} K - * @typedef {Partial> & Required>} PartialExcept - */ - -/** - * We use a custom `Builder` type here to support the minimum version of SvelteKit. - * @typedef {PartialExcept} Builder2_4_0 - */ - -const name = '@sveltejs/adapter-vercel'; const INTERNAL = '![-]'; // this name is guaranteed not to conflict with user routes -const [kit_major, kit_minor] = VERSION.split('.'); - -// https://vercel.com/docs/functions/edge-functions/edge-runtime#compatible-node.js-modules -const compatible_node_modules = ['async_hooks', 'events', 'buffer', 'assert', 'util']; - -/** @type {import('./index.js').default} **/ +/** @type {typeof import('./index.js').default} **/ const plugin = function (defaults = {}) { - if ('edge' in defaults) { - throw new Error("{ edge: true } has been removed in favour of { runtime: 'edge' }"); + // @ts-ignore TODO remove this in a future version + if ('edge' in defaults || defaults.runtime === 'edge') { + throw new Error('The `edge` runtime is no longer supported'); } return { - name, - /** @param {Builder2_4_0} builder */ + name: '@sveltejs/adapter-vercel', + /** @param {import('@sveltejs/kit').Builder} builder */ async adapt(builder) { - if (!builder.routes) { - throw new Error( - '@sveltejs/adapter-vercel >=2.x (possibly installed through @sveltejs/adapter-auto) requires @sveltejs/kit version 1.5 or higher. ' + - 'Either downgrade the adapter or upgrade @sveltejs/kit' - ); - } - const dir = '.vercel/output'; const tmp = builder.getBuildDirectory('vercel-tmp'); - builder.rimraf(dir); - builder.rimraf(tmp); + fs.rmSync(dir, { force: true, recursive: true }); + fs.rmSync(tmp, { force: true, recursive: true }); if (fs.existsSync('vercel.json')) { const vercel_file = fs.readFileSync('vercel.json', 'utf-8'); @@ -59,7 +34,7 @@ const plugin = function (defaults = {}) { const files = fileURLToPath(new URL('./files', import.meta.url).href); const dirs = { - static: `${dir}/static${builder.config.kit.paths.base}`, + static: `${dir}/static${builder.config.paths.base}`, functions: `${dir}/functions` }; @@ -76,30 +51,32 @@ const plugin = function (defaults = {}) { * @param {string} name * @param {import('./index.js').ServerlessConfig} config * @param {import('@sveltejs/kit').RouteDefinition[]} routes + * @param {string} [proxy] */ - async function generate_serverless_function(name, config, routes) { + async function generate_serverless_function(name, config, routes, proxy) { const dir = `${dirs.functions}/${name}.func`; + const entrypoint = `${tmp}/index.js`; + + if (proxy) { + builder.copy(proxy, entrypoint); + } - const relativePath = path.posix.relative(tmp, builder.getServerDirectory()); - builder.copy(`${files}/serverless.js`, `${tmp}/index.js`, { + builder.copy(`${files}/serverless.js`, proxy ? `${tmp}/serverless.js` : entrypoint, { replace: { - SERVER: `${relativePath}/index.js`, - MANIFEST: './manifest.js' + SERVER: `./server.js` } }); - if (builder.hasServerInstrumentationFile?.()) { - builder.instrument?.({ - entrypoint: `${tmp}/index.js`, - instrumentation: `${builder.getServerDirectory()}/instrumentation.server.js` + if (builder.hasServerInstrumentationFile()) { + const initializer = builder.createInstrumentationInitializer({ outputDirectory: tmp }); + builder.instrument({ + entrypoint, + instrumentation: `${builder.getServerDirectory()}/instrumentation.server.js`, + initializer }); } + builder.generateServerInstance(`${tmp}/server.js`, { routes }); - write( - `${tmp}/manifest.js`, - `export const manifest = ${builder.generateManifest({ relativePath, routes })};\n` - ); - - await create_function_bundle(builder, `${tmp}/index.js`, dir, config); + await create_function_bundle(builder, entrypoint, dir, config); for (const asset of builder.findServerAssets(routes)) { // TODO use symlinks, once Build Output API supports doing so @@ -107,153 +84,6 @@ const plugin = function (defaults = {}) { } } - let warned = false; - - /** - * @param {string} name - * @param {import('./index.js').EdgeConfig} config - * @param {import('@sveltejs/kit').RouteDefinition[]} routes - */ - async function generate_edge_function(name, config, routes) { - if (!warned) { - warned = true; - builder.log.warn( - `The \`runtime: 'edge'\` option is deprecated, and will be removed in a future version of adapter-vercel` - ); - } - - const tmp = builder.getBuildDirectory(`vercel-tmp/${name}`); - const relativePath = path.posix.relative(tmp, builder.getServerDirectory()); - - builder.copy(`${files}/edge.js`, `${tmp}/edge.js`, { - replace: { - SERVER: `${relativePath}/index.js`, - MANIFEST: './manifest.js' - } - }); - - write( - `${tmp}/manifest.js`, - `export const manifest = ${builder.generateManifest({ relativePath, routes })};\n` - ); - - try { - const outdir = `${dirs.functions}/${name}.func`; - /** @type {BuildOptions} */ - const esbuild_config = { - // minimum Node.js version supported is v14.6.0 that is mapped to ES2019 - // https://edge-runtime.vercel.app/features/polyfills - // TODO verify the latest ES version the edge runtime supports - target: 'es2020', - bundle: true, - platform: 'browser', - conditions: [ - // Vercel's Edge runtime key https://runtime-keys.proposal.wintercg.org/#edge-light - 'edge-light', - // re-include these since they are included by default when no conditions are specified - // https://esbuild.github.io/api/#conditions - 'module' - ], - format: 'esm', - external: [ - ...compatible_node_modules, - ...compatible_node_modules.map((id) => `node:${id}`), - ...(config.external || []) - ], - sourcemap: 'linked', - banner: { js: 'globalThis.global = globalThis;' }, - loader: { - '.wasm': 'copy', - '.woff': 'copy', - '.woff2': 'copy', - '.ttf': 'copy', - '.eot': 'copy', - '.otf': 'copy' - } - }; - const result = await esbuild.build({ - entryPoints: [`${tmp}/edge.js`], - outfile: `${outdir}/index.js`, - ...esbuild_config - }); - - let instrumentation_result; - if (builder.hasServerInstrumentationFile?.()) { - instrumentation_result = await esbuild.build({ - entryPoints: [`${builder.getServerDirectory()}/instrumentation.server.js`], - outfile: `${outdir}/instrumentation.server.js`, - ...esbuild_config - }); - - builder.instrument?.({ - entrypoint: `${outdir}/index.js`, - instrumentation: `${outdir}/instrumentation.server.js`, - module: { - generateText: generate_traced_edge_module - } - }); - } - - const warnings = instrumentation_result - ? [...result.warnings, ...instrumentation_result.warnings] - : result.warnings; - - if (warnings.length > 0) { - const formatted = await esbuild.formatMessages(warnings, { - kind: 'warning', - color: true - }); - - console.error(formatted.join('\n')); - } - } catch (err) { - const error = /** @type {import('esbuild').BuildFailure} */ (err); - for (const e of error.errors) { - for (const node of e.notes) { - const match = - /The package "(.+)" wasn't found on the file system but is built into node/.exec( - node.text - ); - - if (match) { - node.text = `Cannot use "${match[1]}" when deploying to Vercel Edge Functions.`; - } - } - } - - const formatted = await esbuild.formatMessages(error.errors, { - kind: 'error', - color: true - }); - - console.error(formatted.join('\n')); - - throw new Error( - `Bundling with esbuild failed with ${error.errors.length} ${ - error.errors.length === 1 ? 'error' : 'errors' - }`, - { cause: err } - ); - } - - write( - `${dirs.functions}/${name}.func/.vc-config.json`, - JSON.stringify( - { - runtime: config.runtime, - regions: config.regions, - entrypoint: 'index.js', - framework: { - slug: 'sveltekit', - version: VERSION - } - }, - null, - '\t' - ) - ); - } - /** @type {Map[] }>} */ const groups = new Map(); @@ -271,7 +101,12 @@ const plugin = function (defaults = {}) { // group routes by config for (const route of builder.routes) { + if (route.config.runtime === 'edge') { + throw new Error('The `edge` runtime is no longer supported'); + } + const runtime = resolve_runtime(defaults.runtime, route.config.runtime); + const config = { ...defaults, ...route.config, runtime }; if (is_prerendered(route)) { @@ -282,13 +117,7 @@ const plugin = function (defaults = {}) { } if (config.isr) { - const directory = path.relative('.', builder.config.kit.files.routes + route.id); - - if (runtime === 'edge') { - throw new Error( - `${directory}: Routes using \`isr\` must use a Node.js or Bun runtime (for example 'nodejs24.x' or 'experimental_bun1.x')` - ); - } + const directory = path.relative('.', builder.config.files.routes + route.id); if (config.isr.allowQuery?.includes('__pathname')) { throw new Error( @@ -348,17 +177,10 @@ const plugin = function (defaults = {}) { const singular = groups.size === 1; for (const group of groups.values()) { - const generate_function = - group.config.runtime === 'edge' ? generate_edge_function : generate_serverless_function; - // generate one function for the group const name = singular ? `${INTERNAL}/catchall` : `${INTERNAL}/${group.i}`; - await generate_function( - name, - /** @type {any} */ (group.config), - /** @type {import('@sveltejs/kit').RouteDefinition[]} */ (group.routes) - ); + await generate_serverless_function(name, group.config, group.routes); for (const route of group.routes) { functions.set(route.pattern.toString(), name); @@ -370,17 +192,16 @@ const plugin = function (defaults = {}) { // by SvelteKit rather than Vercel const runtime = resolve_runtime(defaults.runtime); - const generate_function = - runtime === 'edge' ? generate_edge_function : generate_serverless_function; - await generate_function( + await generate_serverless_function( `${INTERNAL}/catchall`, - /** @type {any} */ ({ ...defaults, runtime }), - [] + { ...defaults, runtime }, + [], + `${files}/catch-all.js` ); } - if (builder.config.kit.experimental.remoteFunctions) { + if (builder.config.experimental.remoteFunctions) { // Ensure remote functions are always handled by the catchall route, which will be symlinked to /_app/remote. // This stops them from being affected by ISR config from other routes that match /[...rest] (ref: #15085) // and also makes them show as handled by `/_app/remote` in Vercel's observability. @@ -393,7 +214,7 @@ const plugin = function (defaults = {}) { const target = path.join(dirs.functions, INTERNAL, 'catchall.func'); // Ensure the parent directory exists before symlinking - builder.mkdirp(path.join(dirs.functions, app_path)); + fs.mkdirSync(path.join(dirs.functions, app_path), { recursive: true }); const relative = path.relative(path.dirname(remote_symlink_path), target); @@ -405,6 +226,12 @@ const plugin = function (defaults = {}) { }); } + // Vercel's filesystem phase serves a function at its own path, with or without a + // trailing slash, before the routes below are consulted. Static ISR routes live at + // their own path, so they must be routed before it to arrive with `__pathname` + /** @type {any[]} */ + const static_isr_routes = []; + for (const route of builder.routes) { if (is_prerendered(route)) continue; @@ -416,7 +243,8 @@ const plugin = function (defaults = {}) { if (isr) { const isr_name = route.id.slice(1) || '__root__'; // should we check that __root__ isn't a route? const base = `${dirs.functions}/${isr_name}`; - builder.mkdirp(base); + const has_page = route.page.methods.length > 0; + fs.mkdirSync(base, { recursive: true }); const target = `${dirs.functions}/${name}.func`; const relative = path.relative(path.dirname(base), target); @@ -424,9 +252,10 @@ const plugin = function (defaults = {}) { // create a symlink to the actual function, but use the // route name so that we can derive the correct URL fs.symlinkSync(relative, `${base}.func`); - fs.symlinkSync(`../${relative}`, `${base}/__data.json.func`); + if (has_page) { + fs.symlinkSync(`../${relative}`, `${base}/__data.json.func`); + } - const pathname = get_pathname(route); const json = JSON.stringify( { ...isr, expiration: parse_isr_expiration(isr.expiration, route.id) }, null, @@ -434,19 +263,29 @@ const plugin = function (defaults = {}) { ); write(`${base}.prerender-config.json`, json); - write(`${base}/__data.json.prerender-config.json`, json); + if (has_page) { + write(`${base}/__data.json.prerender-config.json`, json); + } - const q = `?__pathname=/${pathname}`; + const routes = route.segments.some((segment) => segment.dynamic) + ? static_config.routes + : static_isr_routes; - static_config.routes.push({ - src: src + '$', - dest: `/${isr_name}${q}` - }); + // capture the requested pathname (minus the `^` anchor) as `__pathname`, + // since the function otherwise only sees its own path + const pathname = src.slice(1); - static_config.routes.push({ - src: src + '/__data.json$', - dest: `/${isr_name}/__data.json${q}` + routes.push({ + src: `^(${pathname})$`, + dest: `/${isr_name}?__pathname=$1` }); + + if (has_page) { + routes.push({ + src: `^(${pathname}/__data.json)$`, + dest: `/${isr_name}/__data.json?__pathname=$1` + }); + } } else { // Create a symlink for each route to the main function for better observability // (without this, every request appears to go through `/![-]`) @@ -467,7 +306,7 @@ const plugin = function (defaults = {}) { const target = path.join(dirs.functions, `${name}.func`); // The actual function directory e.g., .vercel/output/functions/![-].func // Ensure the directory for the data endpoint symlink exists (e.g., functions/index/) - builder.mkdirp(base_dir); + fs.mkdirSync(base_dir, { recursive: true }); // Calculate relative paths FROM the directory containing the symlink TO the target const relative_for_main = path.relative(path.dirname(main_symlink_path), target); @@ -486,23 +325,24 @@ const plugin = function (defaults = {}) { } } - // optional chaining to support older versions that don't have this setting yet - if (builder.config.kit.router?.resolution === 'server') { - // Create a separate edge function just for server-side route resolution. + const filesystem = static_config.routes.findIndex((route) => route.handle === 'filesystem'); + static_config.routes.splice(filesystem, 0, ...static_isr_routes); + + if (builder.config.router.resolution === 'server') { + // Create a separate serverless function just for server-side route resolution. // By omitting all routes we're ensuring it's small (the routes will still be available // to the route resolution, because it does not rely on the server routing manifest) - await generate_edge_function( - `${builder.config.kit.appDir}/route`, - { - external: 'external' in defaults ? defaults.external : undefined, - runtime: 'edge' - }, + const runtime = resolve_runtime(defaults.runtime); + + await generate_serverless_function( + `${builder.config.appDir}/route`, + /** @type {any} */ ({ ...defaults, runtime }), [] ); static_config.routes.push({ - src: `${builder.config.kit.paths.base}/(|.+/)__route\\.js`, - dest: `${builder.config.kit.paths.base}/${builder.config.kit.appDir}/route` + src: `${builder.config.paths.base}/(?:.+/|.+\\.html)?__route\\.js`, + dest: `${builder.config.paths.base}/${builder.config.appDir}/route` }); } @@ -516,28 +356,16 @@ const plugin = function (defaults = {}) { }, supports: { - read: ({ config, route }) => { - const runtime = config.runtime ?? defaults.runtime; - - // TODO bump peer dep in next adapter major to simplify this - if (runtime === 'edge' && kit_major === '2' && kit_minor < '25') { - throw new Error( - `${name}: Cannot use \`read\` from \`$app/server\` in route \`${route.id}\` configured with \`runtime: 'edge'\` and SvelteKit < 2.25.0` - ); - } - - return true; - }, + read: () => true, instrumentation: () => true } }; }; -/** @param {import('./index.js').EdgeConfig & import('./index.js').ServerlessConfig} config */ +/** @param {import('./index.js').ServerlessConfig} config */ function hash_config(config) { return [ config.runtime ?? '', - config.external ?? '', config.regions ?? '', config.memory ?? '', config.maxDuration ?? '', @@ -561,7 +389,7 @@ function write(file, data) { // This function is duplicated in adapter-static /** - * @param {Builder2_4_0} builder + * @param {import('@sveltejs/kit').Builder} builder * @param {import('./index.js').Config} config * @param {string} dir */ @@ -651,7 +479,7 @@ function static_vercel_config(builder, config, dir) { } ], headers: { - 'Set-Cookie': `__vdpl=${process.env.VERCEL_DEPLOYMENT_ID}; Path=${builder.config.kit.paths.base}/; SameSite=Strict; Secure; HttpOnly` + 'Set-Cookie': `__vdpl=${process.env.VERCEL_DEPLOYMENT_ID}; Path=${builder.config.paths.base}/; SameSite=Strict; Secure; HttpOnly` }, continue: true }); @@ -660,7 +488,7 @@ function static_vercel_config(builder, config, dir) { // allows you to set multiple cookies for a single route. essentially, since we // know that the entry file will be requested immediately, we can set the second // cookie in _that_ response rather than the document response - const base = `${dir}/${builder.config.kit.appDir}/immutable/entry`; + const base = `${dir}/${builder.config.appDir}/immutable/entry`; const entry = fs.readdirSync(base).find((file) => file.startsWith('start.')); if (!entry) { @@ -703,7 +531,7 @@ function static_vercel_config(builder, config, dir) { } /** - * @param {Builder2_4_0} builder + * @param {import('@sveltejs/kit').Builder} builder * @param {string} entry * @param {string} dir * @param {import('./index.js').ServerlessConfig} config @@ -714,7 +542,12 @@ async function create_function_bundle(builder, entry, dir, config) { let base = entry; while (base !== (base = path.dirname(base))); - const traced = await nodeFileTrace([entry], { base }); + const traced = await nodeFileTrace([entry], { + base, + processCwd: process.cwd(), + // a wildcard directly under `base` would glob the entire filesystem + ignore: (file) => file.startsWith('**') + }); /** @type {Map} */ const resolution_failures = new Map(); @@ -793,7 +626,13 @@ async function create_function_bundle(builder, entry, dir, config) { if (source !== realpath) { const realdest = path.join(dir, path.relative(ancestor, realpath)); - fs.symlinkSync(path.relative(path.dirname(dest), realdest), dest, is_dir ? 'dir' : 'file'); + try { + fs.symlinkSync(path.relative(path.dirname(dest), realdest), dest, is_dir ? 'dir' : 'file'); + } catch (error) { + // different traced paths can resolve to the same destination + // (e.g. multiple pnpm symlink chains pointing at the same real file) + if (/** @type {NodeJS.ErrnoException} */ (error).code !== 'EEXIST') throw error; + } } else if (!is_dir) { fs.copyFileSync(source, dest); } @@ -824,18 +663,10 @@ async function create_function_bundle(builder, entry, dir, config) { } /** - * - * @param {Builder2_4_0} builder - * @param {any} vercel_config + * @param {import('@sveltejs/kit').Builder} builder + * @param {any} vercel_config see https://vercel.com/docs/project-configuration/vercel-json */ function validate_vercel_json(builder, vercel_config) { - if (builder.routes.length > 0 && !builder.routes[0].api) { - // bail — we're on an older SvelteKit version that doesn't - // populate `route.api.methods`, so we can't check - // to see if cron paths are valid - return; - } - const crons = /** @type {Array} */ ( Array.isArray(vercel_config?.crons) ? vercel_config.crons : [] ); @@ -882,23 +713,4 @@ function is_prerendered(route) { ); } -/** - * @param {{ instrumentation: string; start: string }} opts - */ -function generate_traced_edge_module({ instrumentation, start }) { - return `\ -import './${instrumentation}'; -const promise = import('./${start}'); - -/** - * @param {import('http').IncomingMessage} req - * @param {import('http').ServerResponse} res - */ -export default async (req, res) => { - const { default: handler } = await promise; - return handler(req, res); -} -`; -} - export default plugin; diff --git a/packages/adapter-vercel/internal.d.ts b/packages/adapter-vercel/internal.d.ts index 537f7cc041d1..8897747bd7c3 100644 --- a/packages/adapter-vercel/internal.d.ts +++ b/packages/adapter-vercel/internal.d.ts @@ -1,8 +1,3 @@ declare module 'SERVER' { - export { Server } from '@sveltejs/kit'; -} - -declare module 'MANIFEST' { - import { SSRManifest } from '@sveltejs/kit'; - export const manifest: SSRManifest; + export const server: import('@sveltejs/kit').Server; } diff --git a/packages/adapter-vercel/package.json b/packages/adapter-vercel/package.json index f79963ba1fa6..b8308a950ea9 100644 --- a/packages/adapter-vercel/package.json +++ b/packages/adapter-vercel/package.json @@ -1,6 +1,6 @@ { "name": "@sveltejs/adapter-vercel", - "version": "6.3.4", + "version": "7.0.0-next.9", "description": "A SvelteKit adapter that creates a Vercel app", "keywords": [ "adapter", @@ -30,29 +30,25 @@ "files", "index.js", "utils.js", - "index.d.ts", - "ambient.d.ts" + "index.d.ts" ], "scripts": { - "lint": "prettier --check .", - "format": "pnpm lint --write", - "check": "tsc", - "test": "vitest run" + "lint": "oxfmt --check .", + "format": "oxfmt --write .", + "check": "node node_modules/@typescript/native/bin/tsc", + "test:unit": "vitest run *.spec.js", + "test": "pnpm test:unit && pnpm -r --filter='./test/apps/**' test:build" }, "dependencies": { - "@vercel/nft": "^1.3.2", - "esbuild": "^0.28.1" + "@vercel/nft": "^1.10.2" }, "devDependencies": { "@sveltejs/kit": "workspace:^", "@types/node": "catalog:", - "typescript": "catalog:", + "@typescript/native": "catalog:", "vitest": "catalog:" }, "peerDependencies": { - "@sveltejs/kit": "^2.4.0" - }, - "engines": { - "node": ">=20.0" + "@sveltejs/kit": "^3.0.0-next.0" } } diff --git a/packages/adapter-vercel/test/apps/basic/.env b/packages/adapter-vercel/test/apps/basic/.env new file mode 100644 index 000000000000..80bd32f3e8a9 --- /dev/null +++ b/packages/adapter-vercel/test/apps/basic/.env @@ -0,0 +1 @@ +INSTRUMENTATION_ENV= diff --git a/packages/adapter-vercel/test/apps/basic/asset.txt b/packages/adapter-vercel/test/apps/basic/asset.txt new file mode 100644 index 000000000000..eef26100dc5a --- /dev/null +++ b/packages/adapter-vercel/test/apps/basic/asset.txt @@ -0,0 +1 @@ +adapter trace fixture diff --git a/packages/adapter-vercel/test/apps/basic/build.test.js b/packages/adapter-vercel/test/apps/basic/build.test.js new file mode 100644 index 000000000000..63f7c3c3f383 --- /dev/null +++ b/packages/adapter-vercel/test/apps/basic/build.test.js @@ -0,0 +1,68 @@ +import fs from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { test, assert } from 'vitest'; + +const output = fileURLToPath(new URL('./.vercel/output', import.meta.url)); +const functions = `${output}/functions/api/json`; + +test('API route function is generated', () => { + assert.ok(fs.existsSync(`${functions}.func`)); +}); + +test('API route ISR configuration is generated', () => { + assert.ok(fs.existsSync(`${functions}.prerender-config.json`)); +}); + +test('__data.json function is not generated for server-only route', () => { + assert.ok(!fs.existsSync(`${functions}/__data.json.func`)); +}); + +test('__data.json prerender config is not generated for server-only route', () => { + assert.ok(!fs.existsSync(`${functions}/__data.json.prerender-config.json`)); +}); + +/** @type {{ routes: Array<{ src?: string, dest?: string, handle?: string }> }} */ +const config = JSON.parse(fs.readFileSync(`${output}/config.json`, 'utf8')); +const route_sources = config.routes.flatMap((route) => + typeof route.src === 'string' ? [route.src] : [] +); +const api_route_sources = route_sources.filter((src) => src.includes('/api/json')); +const isr_page_route_sources = route_sources.filter((src) => src.includes('/isr')); + +test('API route is present in Vercel routing configuration', () => { + assert.ok(api_route_sources.some((src) => !src.includes('__data.json'))); +}); + +test('__data.json route is not present in Vercel routing configuration for server-only route', () => { + assert.ok(!api_route_sources.some((src) => src.includes('__data.json'))); +}); + +test('__data.json function exists for ISR page route', () => { + assert.ok(fs.existsSync(`${output}/functions/isr/__data.json.func`)); +}); + +test('__data.json function exists in Vercel routing configuration', () => { + assert.ok(isr_page_route_sources.some((src) => src.includes('__data.json'))); +}); + +const filesystem = config.routes.findIndex((route) => route.handle === 'filesystem'); + +test('ISR routes capture the requested pathname', () => { + const route = config.routes.find((route) => route.src === '^(/isr-trailing-slash/?)$'); + assert.equal(route?.dest, '/isr-trailing-slash?__pathname=$1'); +}); + +test('static ISR routes are matched before the filesystem', () => { + const index = config.routes.findIndex((route) => route.src === '^(/isr-trailing-slash/?)$'); + assert.ok(index < filesystem); +}); + +test('dynamic ISR routes are matched after the filesystem', () => { + const index = config.routes.findIndex((route) => route.src === '^(/isr/([^/]+?)/?)$'); + assert.ok(index > filesystem); +}); + +test('process.cwd() is traced from the project directory', () => { + const function_dir = fs.realpathSync(`${output}/functions/process-cwd.func`); + assert.ok(fs.existsSync(`${function_dir}/adapter-vercel/test/apps/basic/asset.txt`)); +}); diff --git a/packages/adapter-vercel/test/apps/basic/package.json b/packages/adapter-vercel/test/apps/basic/package.json index 821ffb4d56c1..a194a3ece909 100644 --- a/packages/adapter-vercel/test/apps/basic/package.json +++ b/packages/adapter-vercel/test/apps/basic/package.json @@ -4,8 +4,9 @@ "private": true, "scripts": { "dev": "vite dev", + "preview": "vite preview", "build": "vite build", - "prepare": "svelte-kit sync || echo ''", + "test:build": "pnpm build && vitest run build.test.js", "test:platform": "playwright test" }, "devDependencies": { @@ -13,7 +14,8 @@ "@sveltejs/vite-plugin-svelte": "catalog:", "svelte": "catalog:", "typescript": "catalog:", - "vite": "catalog:" + "vite": "catalog:", + "vitest": "catalog:" }, "type": "module" } diff --git a/packages/adapter-vercel/test/apps/basic/src/app.d.ts b/packages/adapter-vercel/test/apps/basic/src/app.d.ts index da08e6da592d..62adba4e89cf 100644 --- a/packages/adapter-vercel/test/apps/basic/src/app.d.ts +++ b/packages/adapter-vercel/test/apps/basic/src/app.d.ts @@ -1,6 +1,8 @@ // See https://svelte.dev/docs/kit/types#app.d.ts // for information about these interfaces declare global { + var __INSTRUMENTATION_ENV_LOADED__: boolean | undefined; + namespace App { // interface Error {} // interface Locals {} diff --git a/packages/adapter-vercel/test/apps/basic/src/env.js b/packages/adapter-vercel/test/apps/basic/src/env.js new file mode 100644 index 000000000000..13fdd66c636b --- /dev/null +++ b/packages/adapter-vercel/test/apps/basic/src/env.js @@ -0,0 +1,5 @@ +import { defineEnvVars } from '@sveltejs/kit/env'; + +export const variables = defineEnvVars({ + INSTRUMENTATION_ENV: {} +}); diff --git a/packages/adapter-vercel/test/apps/basic/src/instrumentation.server.js b/packages/adapter-vercel/test/apps/basic/src/instrumentation.server.js new file mode 100644 index 000000000000..0fc714ba3566 --- /dev/null +++ b/packages/adapter-vercel/test/apps/basic/src/instrumentation.server.js @@ -0,0 +1,3 @@ +import { INSTRUMENTATION_ENV } from '$app/env/private'; + +globalThis.__INSTRUMENTATION_ENV_LOADED__ = INSTRUMENTATION_ENV === 'platform'; diff --git a/packages/adapter-vercel/test/apps/basic/src/routes/api/json/+server.ts b/packages/adapter-vercel/test/apps/basic/src/routes/api/json/+server.ts index 60261a6585da..4697fbdcdfcc 100644 --- a/packages/adapter-vercel/test/apps/basic/src/routes/api/json/+server.ts +++ b/packages/adapter-vercel/test/apps/basic/src/routes/api/json/+server.ts @@ -1,5 +1,11 @@ import { json } from '@sveltejs/kit'; +export const config = { + isr: { + expiration: 60 + } +}; + export function GET() { return json({ ok: true }); } diff --git a/packages/adapter-vercel/test/apps/basic/src/routes/instrumentation-env/+server.ts b/packages/adapter-vercel/test/apps/basic/src/routes/instrumentation-env/+server.ts new file mode 100644 index 000000000000..d374e62053ef --- /dev/null +++ b/packages/adapter-vercel/test/apps/basic/src/routes/instrumentation-env/+server.ts @@ -0,0 +1,3 @@ +export function GET() { + return Response.json({ loaded: globalThis.__INSTRUMENTATION_ENV_LOADED__ === true }); +} diff --git a/packages/adapter-vercel/test/apps/basic/src/routes/isr-trailing-slash/+page.server.ts b/packages/adapter-vercel/test/apps/basic/src/routes/isr-trailing-slash/+page.server.ts new file mode 100644 index 000000000000..955c0d986b74 --- /dev/null +++ b/packages/adapter-vercel/test/apps/basic/src/routes/isr-trailing-slash/+page.server.ts @@ -0,0 +1,13 @@ +export const trailingSlash = 'always'; + +export const config = { + isr: { + expiration: 60 + } +}; + +export function load() { + return { + rendered_at: Date.now() + }; +} diff --git a/packages/adapter-vercel/test/apps/basic/src/routes/isr-trailing-slash/+page.svelte b/packages/adapter-vercel/test/apps/basic/src/routes/isr-trailing-slash/+page.svelte new file mode 100644 index 000000000000..01714bc82a70 --- /dev/null +++ b/packages/adapter-vercel/test/apps/basic/src/routes/isr-trailing-slash/+page.svelte @@ -0,0 +1,6 @@ + + +

ISR Trailing Slash Page

+

{data.rendered_at}

diff --git a/packages/adapter-vercel/test/apps/basic/src/routes/max-duration/+server.ts b/packages/adapter-vercel/test/apps/basic/src/routes/max-duration/+server.ts new file mode 100644 index 000000000000..4954bda3ecf9 --- /dev/null +++ b/packages/adapter-vercel/test/apps/basic/src/routes/max-duration/+server.ts @@ -0,0 +1,8 @@ +export const config = { + maxDuration: 1 +}; + +export async function GET() { + await new Promise((resolve) => setTimeout(resolve, 3_000)); + return new Response('completed'); +} diff --git a/packages/adapter-vercel/test/apps/basic/src/routes/process-cwd/+server.js b/packages/adapter-vercel/test/apps/basic/src/routes/process-cwd/+server.js new file mode 100644 index 000000000000..acf017b8db28 --- /dev/null +++ b/packages/adapter-vercel/test/apps/basic/src/routes/process-cwd/+server.js @@ -0,0 +1,6 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +export function GET() { + return new Response(fs.readFileSync(path.join(process.cwd(), 'asset.txt'), 'utf8')); +} diff --git a/packages/adapter-vercel/test/apps/basic/svelte.config.js b/packages/adapter-vercel/test/apps/basic/svelte.config.js deleted file mode 100644 index 20cd2b3ff5b8..000000000000 --- a/packages/adapter-vercel/test/apps/basic/svelte.config.js +++ /dev/null @@ -1,10 +0,0 @@ -import adapter from '../../../index.js'; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - kit: { - adapter: adapter() - } -}; - -export default config; diff --git a/packages/adapter-vercel/test/apps/basic/test/test.ts b/packages/adapter-vercel/test/apps/basic/test/test.ts index 008bd735358f..a631c4a26cea 100644 --- a/packages/adapter-vercel/test/apps/basic/test/test.ts +++ b/packages/adapter-vercel/test/apps/basic/test/test.ts @@ -19,6 +19,18 @@ test('API routes work', async ({ request }) => { expect(data.ok).toBe(true); }); +test('route-level maxDuration is applied', async ({ request }) => { + const response = await request.get('/max-duration'); + expect(response.status()).toBe(504); + expect(response.headers()['x-vercel-error']).toBe('FUNCTION_INVOCATION_TIMEOUT'); +}); + +test('dynamic env is available in instrumentation', async ({ request }) => { + const response = await request.get('/instrumentation-env'); + expect(response.ok()).toBe(true); + expect(await response.json()).toEqual({ loaded: true }); +}); + test('$app/server read works', async ({ request }) => { const response = await request.get('/read'); expect(response.ok()).toBe(true); @@ -46,6 +58,20 @@ test('ISR route serves cached response', async ({ request }) => { expect(first_rendered_at).toBe(second_rendered_at); }); +test('ISR page with trailingSlash always loads without errors', async ({ page, request }) => { + await page.goto('/isr-trailing-slash/'); + + expect(new URL(page.url()).pathname).toBe('/isr-trailing-slash/'); + await expect(page.locator('h1')).toContainText('ISR Trailing Slash Page'); + + const rendered_at = await page.locator('#rendered-at').textContent(); + await page.reload(); + await expect(page.locator('#rendered-at')).toHaveText(String(rendered_at)); + + const response = await request.get('/isr-trailing-slash', { maxRedirects: 0 }); + expect(response.status()).toBe(308); +}); + test('ISR dynamic route serves cached response per slug', async ({ request }) => { // warm the cache for /isr/alpha const first = await request.get('/isr/alpha'); @@ -67,6 +93,10 @@ test('ISR dynamic route serves cached response per slug', async ({ request }) => expect(beta.ok()).toBe(true); const beta_html = await beta.text(); expect(beta_html).toContain('ISR: beta'); + + // trailing slash is normalized rather than silently served + const slashed = await request.get('/isr/alpha/', { maxRedirects: 0 }); + expect(slashed.status()).toBe(308); }); test('prerendered page works', async ({ page }) => { @@ -116,3 +146,24 @@ test('client-side navigation to prerendered routes works', async ({ page }) => { await page.click('a[href="/prerendered"]'); await expect(page.locator('h1')).toContainText('this page is prerendered'); }); + +test('missing immutable asset returns 404 with no-store cache-control', async ({ request }) => { + // https://github.com/sveltejs/kit/pull/16077 — the 404 must not inherit the + // immutable `max-age=31536000` header, otherwise it's cached for a year + const response = await request.get('/_app/immutable/chunks/nonexistent.js'); + expect(response.status()).toBe(404); + expect(response.headers()['cache-control']).toBe('no-store'); +}); + +test('valid immutable asset is still cached immutably', async ({ page, request }) => { + await page.goto('/'); + // immutable assets are preloaded via , not + +

{page.url.pathname + page.url.search}

diff --git a/packages/adapter-vercel/test/apps/split/test/test.ts b/packages/adapter-vercel/test/apps/split/test/test.ts index 95250e9e1d71..2f4b2d21d3c7 100644 --- a/packages/adapter-vercel/test/apps/split/test/test.ts +++ b/packages/adapter-vercel/test/apps/split/test/test.ts @@ -6,3 +6,14 @@ test('split functions work', async ({ page }) => { await page.goto('/a'); await expect(page.locator('p')).toContainText(/split config/); }); + +test('reroute works', async ({ page }) => { + await page.goto('/reroute'); + await expect(page.locator('p')).toContainText('/reroute'); + await page.goto('/en/reroute?hello=world'); + await expect(page.locator('p')).toContainText('/en/reroute?hello=world'); +}); + +// TODO: test remote function works + +// TODO: test preloadCode works diff --git a/packages/adapter-vercel/test/utils.js b/packages/adapter-vercel/test/utils.js index 18c9c56d89d7..b5bdbb0a952e 100644 --- a/packages/adapter-vercel/test/utils.js +++ b/packages/adapter-vercel/test/utils.js @@ -16,7 +16,7 @@ export const config = { baseURL: process.env.DEPLOYMENT_URL, screenshot: 'only-on-failure', trace: 'retain-on-failure', - channel: 'chromium' + channel: 'chrome' }, workers: process.env.CI ? 2 : undefined, reporter: 'list', diff --git a/packages/adapter-vercel/test/utils.spec.js b/packages/adapter-vercel/test/utils.spec.js deleted file mode 100644 index 2ee4ca2030c0..000000000000 --- a/packages/adapter-vercel/test/utils.spec.js +++ /dev/null @@ -1,191 +0,0 @@ -import { assert, test, describe } from 'vitest'; -import { get_pathname, parse_isr_expiration, pattern_to_src, resolve_runtime } from '../utils.js'; - -// workaround so that TypeScript doesn't follow that import which makes it pick up that file and then error on missing import aliases -const { parse_route_id } = await import( - new URL('../../kit/src/' + 'utils/routing.js', import.meta.url).href -); - -/** - * @param {import('@sveltejs/kit').RouteDefinition['segments']} segments - * @param {string} expected - */ -function run_get_pathname_test(segments, expected) { - const route = /** @type {import('@sveltejs/kit').RouteDefinition} */ ({ segments }); - assert.equal(get_pathname(route), expected); -} - -test('get_pathname for simple route', () => { - run_get_pathname_test([{ content: 'foo', dynamic: false, rest: false }], 'foo'); -}); - -test('get_pathname for simple route with multiple segments', () => { - run_get_pathname_test( - [ - { content: 'foo', dynamic: false, rest: false }, - { content: 'bar', dynamic: false, rest: false } - ], - 'foo/bar' - ); -}); - -test('get_pathname for route with parameters', () => { - run_get_pathname_test( - [ - { content: 'foo', dynamic: false, rest: false }, - { content: '[bar]', dynamic: true, rest: false } - ], - 'foo/$1' - ); -}); - -test('get_pathname for route with parameters within segment', () => { - run_get_pathname_test( - [ - { content: 'foo-[bar]', dynamic: true, rest: false }, - { content: '[baz]-buz', dynamic: true, rest: false } - ], - 'foo-$1/$2-buz' - ); -}); - -test('get_pathname for route with optional parameters within segment', () => { - run_get_pathname_test( - [ - { content: 'foo-[[bar]]', dynamic: true, rest: false }, - { content: '[[baz]]-buz', dynamic: true, rest: false } - ], - 'foo-$1/$2-buz' - ); -}); - -test('get_pathname for route with rest parameter', () => { - run_get_pathname_test( - [ - { content: 'foo', dynamic: false, rest: false }, - { content: '[[...rest]]', dynamic: true, rest: true } - ], - 'foo$1' - ); -}); - -test('get_pathname for route with required and rest parameter', () => { - run_get_pathname_test( - [ - { content: '[foo]', dynamic: true, rest: false }, - { content: '[...rest]', dynamic: true, rest: true } - ], - '$1$2' - ); -}); - -test('get_pathname for route with required and optional parameter', () => { - run_get_pathname_test( - [ - { content: '[foo]', dynamic: true, rest: false }, - { content: '[[optional]]', dynamic: true, rest: true } - ], - '$1$2' - ); -}); - -test('get_pathname for route with required and optional parameter', () => { - run_get_pathname_test( - [ - { content: '[foo]', dynamic: true, rest: false }, - { content: '[[...rest]]', dynamic: true, rest: true }, - { content: 'bar', dynamic: false, rest: false } - ], - '$1$2/bar' - ); -}); - -/** - * @param {string} route_id - * @param {string} expected - */ -function run_pattern_to_src_test(route_id, expected) { - const { pattern } = parse_route_id(route_id); - assert.equal(pattern_to_src(pattern.toString()), expected); -} - -test('pattern_to_src for simple route', () => { - run_pattern_to_src_test('/', '^/?'); -}); - -test('pattern_to_src for route with parameters', () => { - run_pattern_to_src_test('/foo/[bar]', '^/foo/([^/]+?)/?'); -}); - -test('pattern_to_src for route with optional parameters', () => { - run_pattern_to_src_test('/foo/[[bar]]', '^/foo(/[^/]+)?/?'); -}); - -test('pattern_to_src for route with optional parameter in the middle', () => { - run_pattern_to_src_test('/foo/[[bar]]/baz', '^/foo(/[^/]+)?/baz/?'); -}); - -test('pattern_to_src for route with rest parameter', () => { - run_pattern_to_src_test('/foo/[...bar]', '^/foo(/[^]*)?/?'); -}); - -test('pattern_to_src for route with rest parameter in the middle', () => { - run_pattern_to_src_test('/foo/[...bar]/baz', '^/foo(/[^]*)?/baz/?'); -}); - -describe('parse_isr_expiration', () => { - test.each( - /** @type {const} */ ([ - [1, 1], - ['1', 1], - [false, false], - ['false', false] - ]) - )('works for valid inputs ($0)', (input, output) => { - const result = parse_isr_expiration(input, '/isr'); - assert.equal(result, output); - }); - - test('does not allow floats', () => { - assert.throws(() => parse_isr_expiration(1.5, '/isr'), /should be an integer, in \/isr/); - }); - - test('does not allow `true`', () => { - const val = /** @type {false} */ (true); - assert.throws(() => parse_isr_expiration(val, '/isr'), /should be an integer, in \/isr/); - }); - - test('does not allow negative numbers', () => { - assert.throws(() => parse_isr_expiration(-1, '/isr'), /should be non-negative, in \/isr/); - }); - - test('does not allow strings that do not parse to valid numbers', () => { - assert.throws( - () => parse_isr_expiration('foo', '/isr'), - /value was a string but could not be parsed as an integer, in \/isr/ - ); - }); - - test('does not allow strings that parse to floats', () => { - assert.throws( - () => parse_isr_expiration('1.1', '/isr'), - /value was a string but could not be parsed as an integer, in \/isr/ - ); - }); -}); - -describe('resolve_runtime', () => { - test('prefers override_key over default_key', () => { - const result = resolve_runtime('nodejs20.x', 'experimental_bun1.x'); - assert.equal(result, 'bun1.x'); - }); - - test('uses default_key when override_key is undefined', () => { - const result = resolve_runtime('experimental_bun1.x'); - assert.equal(result, 'bun1.x'); - }); - - test('throws an error when resolving to an invalid runtime', () => { - assert.throws(() => resolve_runtime('node18.x', undefined), /Unsupported runtime: node18.x/); - }); -}); diff --git a/packages/adapter-vercel/tsconfig.json b/packages/adapter-vercel/tsconfig.json index 25c169acf112..3c5824b87ed2 100644 --- a/packages/adapter-vercel/tsconfig.json +++ b/packages/adapter-vercel/tsconfig.json @@ -11,5 +11,6 @@ }, "types": ["node"] }, - "include": ["*.js", "files/**/*.js", "internal.d.ts", "test/**/*.js"] + "include": ["*.js", "files/**/*.js", "internal.d.ts", "test/**/*.js"], + "exclude": ["test/apps/**"] } diff --git a/packages/adapter-vercel/utils.js b/packages/adapter-vercel/utils.js index d35ceea65198..ed9253c8474a 100644 --- a/packages/adapter-vercel/utils.js +++ b/packages/adapter-vercel/utils.js @@ -1,46 +1,5 @@ import process from 'node:process'; -/** @param {import("@sveltejs/kit").RouteDefinition} route */ -export function get_pathname(route) { - let i = 1; - - const pathname = route.segments - .map((segment) => { - if (!segment.dynamic) { - return '/' + segment.content; - } - - const parts = segment.content.split(/\[(.+?)\](?!\])/); - - if ( - parts.length === 3 && - !parts[0] && - !parts[2] && - (parts[1].startsWith('...') || parts[1][0] === '[') - ) { - // Special case: segment is a single optional or rest parameter. - // In that case we don't prepend a slash (also see comment in pattern_to_src). - return `$${i++}`; - } else { - return ( - '/' + - parts - .map((content, j) => { - if (j % 2) { - return `$${i++}`; - } else { - return content; - } - }) - .join('') - ); - } - }) - .join(''); - - return pathname[0] === '/' ? pathname.slice(1) : pathname; -} - /** * Adjusts the stringified route regex for Vercel's routing system * @param {string} pattern stringified route regex @@ -57,14 +16,6 @@ export function pattern_to_src(pattern) { src = '^/?'; } - // Move non-capturing groups that swallow slashes into their following capturing groups. - // This is necessary because during ISR we're using the regex to construct the __pathname - // query parameter: In case of a route like [required]/[...rest] we need to turn them - // into $1$2 and not $1/$2, because if [...rest] is empty, we don't want to have a trailing - // slash in the __pathname query parameter which wasn't there in the original URL, as that - // could result in a false trailing slash redirect in the SvelteKit runtime, leading to infinite redirects. - src = src.replace(/\(\?:\/\((.+?)\)\)/g, '(/$1)'); - return src; } @@ -113,37 +64,47 @@ export function parse_isr_expiration(value, route_id) { * @returns {RuntimeKey} */ export function resolve_runtime(default_key, override_key) { - const key = (override_key ?? default_key ?? get_default_runtime()).replace('experimental_', ''); + const key = override_key ?? default_key ?? get_default_runtime(); assert_is_valid_runtime(key); return key; } -const valid_node_versions = [20, 22, 24]; -const formatter = new Intl.ListFormat('en', { type: 'disjunction' }); +const valid_node_versions = [22, 24]; +const formatter = new Intl.ListFormat('en-gb', { type: 'disjunction' }); /** @returns {RuntimeKey} */ function get_default_runtime() { - // TODO may someday need to auto-detect Bun, but this will be complicated because you may want to run your build - // with Bun but not have your serverless runtime be in Bun. Vercel will likely have to attach something to `globalThis` or similar - // to tell us what the bun configuration is. - const major = Number(process.version.slice(1).split('.')[0]); + // if the user ran e.g. `bunx --bun vite build`, infer that they want to run the app in Bun + if (process.versions.bun) { + const major = process.versions.bun.split('.')[0]; + if (major !== '1') { + throw new Error( + `Unsupported Bun version: ${major}. Please use Bun 1.x to build your project, or explicitly specify a runtime in your adapter configuration.` + ); + } - if (!valid_node_versions.includes(major)) { - throw new Error( - `Unsupported Node.js version: ${process.version}. Please use Node ${formatter.format(valid_node_versions.map((v) => `${v}`))} to build your project, or explicitly specify a runtime in your adapter configuration.` - ); + return `bun${major}.x`; + } + + // otherwise, default to the version of Node specified in the project config + if (process.versions.node) { + const major = Number(process.versions.node.split('.')[0]); + + if (!valid_node_versions.includes(major)) { + throw new Error( + `Unsupported Node.js version: ${process.version}. Please use Node ${formatter.format(valid_node_versions.map((v) => `${v}`))} to build your project, or explicitly specify a runtime in your adapter configuration.` + ); + } + + return `nodejs${/** @type {22 | 24} */ (major)}.x`; } - return `nodejs${/** @type {20 | 22 | 24} */ (major)}.x`; + throw new Error( + 'Could not auto-detect a runtime. Please explicitly specify a runtime in your adapter configuration.' + ); } -const valid_runtimes = /** @type {const} */ ([ - 'nodejs20.x', - 'nodejs22.x', - 'nodejs24.x', - 'bun1.x', - 'edge' -]); +const valid_runtimes = /** @type {const} */ (['nodejs22.x', 'nodejs24.x', 'bun1.x']); /** * @param {string} key @@ -157,5 +118,4 @@ function assert_is_valid_runtime(key) { } } -/** @typedef {Exclude | 'experimental_bun1.x'} RuntimeConfigKey */ /** @typedef {typeof valid_runtimes[number]} RuntimeKey */ diff --git a/packages/adapter-vercel/utils.spec.js b/packages/adapter-vercel/utils.spec.js new file mode 100644 index 000000000000..d382bd5f6d00 --- /dev/null +++ b/packages/adapter-vercel/utils.spec.js @@ -0,0 +1,97 @@ +import { assert, test, describe } from 'vitest'; +import { parse_isr_expiration, pattern_to_src, resolve_runtime } from './utils.js'; + +// workaround so that TypeScript doesn't follow that import which makes it pick up that file and then error on missing import aliases +const { parse_route_id } = await import( + new URL('../kit/src/' + 'utils/routing.js', import.meta.url).href +); + +/** + * @param {string} route_id + * @param {string} expected + */ +function run_pattern_to_src_test(route_id, expected) { + const { pattern } = parse_route_id(route_id); + assert.equal(pattern_to_src(pattern.toString()), expected); +} + +test('pattern_to_src for simple route', () => { + run_pattern_to_src_test('/', '^/?'); +}); + +test('pattern_to_src for route with parameters', () => { + run_pattern_to_src_test('/foo/[bar]', '^/foo/([^/]+?)/?'); +}); + +test('pattern_to_src for route with optional parameters', () => { + run_pattern_to_src_test('/foo/[[bar]]', '^/foo(?:/([^/]+))?/?'); +}); + +test('pattern_to_src for route with optional parameter in the middle', () => { + run_pattern_to_src_test('/foo/[[bar]]/baz', '^/foo(?:/([^/]+))?/baz/?'); +}); + +test('pattern_to_src for route with rest parameter', () => { + run_pattern_to_src_test('/foo/[...bar]', '^/foo(?:/([^]*))?/?'); +}); + +test('pattern_to_src for route with rest parameter in the middle', () => { + run_pattern_to_src_test('/foo/[...bar]/baz', '^/foo(?:/([^]*))?/baz/?'); +}); + +describe('parse_isr_expiration', () => { + test.each( + /** @type {const} */ ([ + [1, 1], + ['1', 1], + [false, false], + ['false', false] + ]) + )('works for valid inputs ($0)', (input, output) => { + const result = parse_isr_expiration(input, '/isr'); + assert.equal(result, output); + }); + + test('does not allow floats', () => { + assert.throws(() => parse_isr_expiration(1.5, '/isr'), /should be an integer, in \/isr/); + }); + + test('does not allow `true`', () => { + const val = /** @type {false} */ (true); + assert.throws(() => parse_isr_expiration(val, '/isr'), /should be an integer, in \/isr/); + }); + + test('does not allow negative numbers', () => { + assert.throws(() => parse_isr_expiration(-1, '/isr'), /should be non-negative, in \/isr/); + }); + + test('does not allow strings that do not parse to valid numbers', () => { + assert.throws( + () => parse_isr_expiration('foo', '/isr'), + /value was a string but could not be parsed as an integer, in \/isr/ + ); + }); + + test('does not allow strings that parse to floats', () => { + assert.throws( + () => parse_isr_expiration('1.1', '/isr'), + /value was a string but could not be parsed as an integer, in \/isr/ + ); + }); +}); + +describe('resolve_runtime', () => { + test('prefers override_key over default_key', () => { + const result = resolve_runtime('nodejs20.x', 'bun1.x'); + assert.equal(result, 'bun1.x'); + }); + + test('uses default_key when override_key is undefined', () => { + const result = resolve_runtime('bun1.x'); + assert.equal(result, 'bun1.x'); + }); + + test('throws an error when resolving to an invalid runtime', () => { + assert.throws(() => resolve_runtime('node18.x', undefined), /Unsupported runtime: node18.x/); + }); +}); diff --git a/packages/amp/CHANGELOG.md b/packages/amp/CHANGELOG.md deleted file mode 100644 index 336e7c6fe82b..000000000000 --- a/packages/amp/CHANGELOG.md +++ /dev/null @@ -1,92 +0,0 @@ -# @sveltejs/amp - -## 1.1.5 -### Patch Changes - - -- chore: add `.git` to the end of `package.json` repository url ([#14134](https://github.com/sveltejs/kit/pull/14134)) - -- Updated dependencies [[`c968aef`](https://github.com/sveltejs/kit/commit/c968aef5727f978244d5160657b4a7ac651384ae)]: - - @sveltejs/kit@2.27.3 - -## 1.1.4 -### Patch Changes - - -- docs: update URLs for new svelte.dev site ([#12857](https://github.com/sveltejs/kit/pull/12857)) - -- Updated dependencies [[`dcbe4222a194c5f90cfc0fc020cf065f7a4e4c46`](https://github.com/sveltejs/kit/commit/dcbe4222a194c5f90cfc0fc020cf065f7a4e4c46), [`4cdbf76fbbf0c0ce7f574ef69c8daddcf954d39d`](https://github.com/sveltejs/kit/commit/4cdbf76fbbf0c0ce7f574ef69c8daddcf954d39d), [`3a9b78f04786898ca93f6d4b75ab18d26bc45192`](https://github.com/sveltejs/kit/commit/3a9b78f04786898ca93f6d4b75ab18d26bc45192), [`723eb8b31e6a22c82f730c30e485386c8676b746`](https://github.com/sveltejs/kit/commit/723eb8b31e6a22c82f730c30e485386c8676b746), [`8ec471c875345b751344e67580ff1b772ef2735b`](https://github.com/sveltejs/kit/commit/8ec471c875345b751344e67580ff1b772ef2735b)]: - - @sveltejs/kit@2.7.3 - -## 1.1.3 -### Patch Changes - - -- chore: configure provenance in a simpler manner ([#12570](https://github.com/sveltejs/kit/pull/12570)) - -- Updated dependencies [[`087a43d391fc38b8c008fb39a804dc6988974101`](https://github.com/sveltejs/kit/commit/087a43d391fc38b8c008fb39a804dc6988974101)]: - - @sveltejs/kit@2.5.22 - -## 1.1.2 -### Patch Changes - - -- chore: package provenance ([#12567](https://github.com/sveltejs/kit/pull/12567)) - -- Updated dependencies [[`4930a8443caa53bcecee7b690cd28e429b1c8a20`](https://github.com/sveltejs/kit/commit/4930a8443caa53bcecee7b690cd28e429b1c8a20)]: - - @sveltejs/kit@2.5.21 - -## 1.1.1 - -### Patch Changes - -- chore: add keywords for discovery in npm search ([#12330](https://github.com/sveltejs/kit/pull/12330)) - -- Updated dependencies [[`25acb1d9fce998dccd8050b93cf4142c2b082611`](https://github.com/sveltejs/kit/commit/25acb1d9fce998dccd8050b93cf4142c2b082611), [`642c4a4aff4351b786fe6274aa2f0bf7d905faf9`](https://github.com/sveltejs/kit/commit/642c4a4aff4351b786fe6274aa2f0bf7d905faf9), [`0a0e9aa897123ebec50af08e9385b2ca4fc5bb28`](https://github.com/sveltejs/kit/commit/0a0e9aa897123ebec50af08e9385b2ca4fc5bb28)]: - - @sveltejs/kit@2.5.11 - -## 1.1.0 - -### Minor Changes - -- feat: allow SvelteKit 2 as peer dependency ([#11233](https://github.com/sveltejs/kit/pull/11233)) - -## 1.0.2 - -### Patch Changes - -- fix: publish missing files ([#8532](https://github.com/sveltejs/kit/pull/8532)) - -## 1.0.1 - -### Patch Changes - -- chore: remove superfluous main field from package.json ([#8519](https://github.com/sveltejs/kit/pull/8519)) - -- Updated dependencies [[`7e2d3405`](https://github.com/sveltejs/kit/commit/7e2d34056e99f371e22406d941b764df365a2649)]: - - @sveltejs/kit@1.1.1 - -## 1.0.0 - -### Major Changes - -First major release, see below for the history of changes that lead up to this. -Starting from now all releases follow semver and changes will be listed as Major/Minor/Patch - -## 1.0.0-next.3 - -### Patch Changes - -- chore: add peerDependencies, add more specific next version ([#8141](https://github.com/sveltejs/kit/pull/8141)) - -## 1.0.0-next.2 - -### Patch Changes - -- Amend boilerplate to match current recommendations ([#7184](https://github.com/sveltejs/kit/pull/7184)) - -## 1.0.0-next.1 - -### Patch Changes - -- Add types to pkg.exports ([#5045](https://github.com/sveltejs/kit/pull/5045)) diff --git a/packages/amp/index.d.ts b/packages/amp/index.d.ts deleted file mode 100644 index 475cec2b75c3..000000000000 --- a/packages/amp/index.d.ts +++ /dev/null @@ -1 +0,0 @@ -export function transform(html: string): string; diff --git a/packages/amp/index.js b/packages/amp/index.js deleted file mode 100644 index 0c1baedb1838..000000000000 --- a/packages/amp/index.js +++ /dev/null @@ -1,28 +0,0 @@ -// https://amp.dev/documentation/guides-and-tutorials/learn/spec/amp-boilerplate/ -const boilerplate = ` - - - -`; - -/** @param {string} html */ -export function transform(html) { - return html - .replace(//, (_match, $1) => ``) - ).css; - }; - const SKIP_PARSING_REGEX = /url\(/i; /** Capture a single url(...) so we can process them one at a time */ @@ -31,8 +22,6 @@ const HASH_OR_QUERY_REGEX = /[#?]/; */ const VITE_ASSET_PREFIX = './'; -const AST_OFFSET = '`; - }); - - css = dropcss({ css, html: markup }).css; - return markup.replace('', `${css}`); - } - } - }); -} diff --git a/packages/kit/test/apps/amp/src/routes/assets/+page.svelte b/packages/kit/test/apps/amp/src/routes/assets/+page.svelte deleted file mode 100644 index de9a86eb3e24..000000000000 --- a/packages/kit/test/apps/amp/src/routes/assets/+page.svelte +++ /dev/null @@ -1,8 +0,0 @@ - - -svelte logo - - -
includes public assets diff --git a/packages/kit/test/apps/amp/src/routes/http-equiv/cache-control/+page.js b/packages/kit/test/apps/amp/src/routes/http-equiv/cache-control/+page.js deleted file mode 100644 index feb67e3e4c07..000000000000 --- a/packages/kit/test/apps/amp/src/routes/http-equiv/cache-control/+page.js +++ /dev/null @@ -1,8 +0,0 @@ -export const prerender = true; - -/** @type {import('./$types').PageLoad} */ -export function load({ setHeaders }) { - setHeaders({ - 'cache-control': 'public, max-age=300' - }); -} diff --git a/packages/kit/test/apps/amp/src/routes/http-equiv/cache-control/+page.svelte b/packages/kit/test/apps/amp/src/routes/http-equiv/cache-control/+page.svelte deleted file mode 100644 index 63edda10a55f..000000000000 --- a/packages/kit/test/apps/amp/src/routes/http-equiv/cache-control/+page.svelte +++ /dev/null @@ -1 +0,0 @@ -

the cache-control headers should be removed from this page

diff --git a/packages/kit/test/apps/amp/src/routes/invalid/+page.svelte b/packages/kit/test/apps/amp/src/routes/invalid/+page.svelte deleted file mode 100644 index cfb5e5dd32a5..000000000000 --- a/packages/kit/test/apps/amp/src/routes/invalid/+page.svelte +++ /dev/null @@ -1,2 +0,0 @@ - -invalid diff --git a/packages/kit/test/apps/amp/src/routes/invalid/has-stylesheet/+page.svelte b/packages/kit/test/apps/amp/src/routes/invalid/has-stylesheet/+page.svelte deleted file mode 100644 index e6c9b2bdb3c2..000000000000 --- a/packages/kit/test/apps/amp/src/routes/invalid/has-stylesheet/+page.svelte +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/kit/test/apps/amp/src/routes/origin.json/+server.js b/packages/kit/test/apps/amp/src/routes/origin.json/+server.js deleted file mode 100644 index 142be28ffbca..000000000000 --- a/packages/kit/test/apps/amp/src/routes/origin.json/+server.js +++ /dev/null @@ -1,6 +0,0 @@ -import { json } from '@sveltejs/kit'; - -/** @type {import('@sveltejs/kit').RequestHandler} */ -export function GET({ url }) { - return json({ origin: url.origin }); -} diff --git a/packages/kit/test/apps/amp/src/routes/origin/+page.js b/packages/kit/test/apps/amp/src/routes/origin/+page.js deleted file mode 100644 index 298a00a667d7..000000000000 --- a/packages/kit/test/apps/amp/src/routes/origin/+page.js +++ /dev/null @@ -1,10 +0,0 @@ -/** @type {import('@sveltejs/kit').Load} */ -export async function load({ url, fetch }) { - const res = await fetch('/origin.json'); - const data = await res.json(); - - return { - origin: url.origin, - data - }; -} diff --git a/packages/kit/test/apps/amp/src/routes/origin/+page.svelte b/packages/kit/test/apps/amp/src/routes/origin/+page.svelte deleted file mode 100644 index ad574575c080..000000000000 --- a/packages/kit/test/apps/amp/src/routes/origin/+page.svelte +++ /dev/null @@ -1,10 +0,0 @@ - - -

{data.origin}

-

{page.url.origin}

-

{data.data.origin}

diff --git a/packages/kit/test/apps/amp/src/routes/styles/+page.svelte b/packages/kit/test/apps/amp/src/routes/styles/+page.svelte deleted file mode 100644 index 08db544b1211..000000000000 --- a/packages/kit/test/apps/amp/src/routes/styles/+page.svelte +++ /dev/null @@ -1,16 +0,0 @@ - - -{#if Math.random() <= 1} -

this text is rendered

-{:else} - -{/if} - - diff --git a/packages/kit/test/apps/amp/src/routes/styles/Unused.svelte b/packages/kit/test/apps/amp/src/routes/styles/Unused.svelte deleted file mode 100644 index 76039659e1e2..000000000000 --- a/packages/kit/test/apps/amp/src/routes/styles/Unused.svelte +++ /dev/null @@ -1,7 +0,0 @@ -

this component will never be rendered

- - diff --git a/packages/kit/test/apps/amp/src/routes/styles/imported.css b/packages/kit/test/apps/amp/src/routes/styles/imported.css deleted file mode 100644 index 9a9f3c1f3279..000000000000 --- a/packages/kit/test/apps/amp/src/routes/styles/imported.css +++ /dev/null @@ -1,3 +0,0 @@ -.shouty { - text-transform: uppercase; -} diff --git a/packages/kit/test/apps/amp/src/routes/valid.json/+server.js b/packages/kit/test/apps/amp/src/routes/valid.json/+server.js deleted file mode 100644 index 714189a71ae4..000000000000 --- a/packages/kit/test/apps/amp/src/routes/valid.json/+server.js +++ /dev/null @@ -1,5 +0,0 @@ -import { json } from '@sveltejs/kit'; - -export function GET() { - return json({ answer: 42 }); -} diff --git a/packages/kit/test/apps/amp/src/routes/valid/+page.js b/packages/kit/test/apps/amp/src/routes/valid/+page.js deleted file mode 100644 index aaf112e513dc..000000000000 --- a/packages/kit/test/apps/amp/src/routes/valid/+page.js +++ /dev/null @@ -1,6 +0,0 @@ -/** @type {import('@sveltejs/kit').Load} */ -export async function load({ fetch }) { - const res = await fetch('/valid.json'); - const { answer } = await res.json(); - return { answer }; -} diff --git a/packages/kit/test/apps/amp/src/routes/valid/+page.svelte b/packages/kit/test/apps/amp/src/routes/valid/+page.svelte deleted file mode 100644 index 17bca5973e70..000000000000 --- a/packages/kit/test/apps/amp/src/routes/valid/+page.svelte +++ /dev/null @@ -1,16 +0,0 @@ - - -

Hello from the {browser ? 'client' : 'server'} in {dev ? 'dev' : 'prod'} mode!

-

The answer is {data.answer}

-

This text is red

- - diff --git a/packages/kit/test/apps/amp/svelte.config.js b/packages/kit/test/apps/amp/svelte.config.js deleted file mode 100644 index 18aec126f11f..000000000000 --- a/packages/kit/test/apps/amp/svelte.config.js +++ /dev/null @@ -1,8 +0,0 @@ -/** @type {import('@sveltejs/kit').Config} */ -const config = { - kit: { - inlineStyleThreshold: Infinity - } -}; - -export default config; diff --git a/packages/kit/test/apps/amp/test/test.js b/packages/kit/test/apps/amp/test/test.js deleted file mode 100644 index 06e0b8977c8f..000000000000 --- a/packages/kit/test/apps/amp/test/test.js +++ /dev/null @@ -1,91 +0,0 @@ -import fs from 'node:fs'; -import process from 'node:process'; -import { expect } from '@playwright/test'; -import { test } from '../../../utils.js'; - -test.describe.configure({ mode: 'parallel' }); - -test('renders an AMP page', async ({ page, baseURL }) => { - await page.goto(`${baseURL}/valid`); - - await expect(page.locator('h1')).toHaveText( - `Hello from the server in ${process.env.DEV ? 'dev' : 'prod'} mode!` - ); - - await expect(page.locator('h2')).toHaveText('The answer is 42'); - - // should not include serialized data - expect(await page.$('script[sveltekit\\:data-type="data"]')).toBeNull(); -}); - -test('styles are applied', async ({ page, baseURL, get_computed_style }) => { - await page.goto(`${baseURL}/valid`); - - expect(await get_computed_style('p', 'color')).toEqual('rgb(255, 0, 0)'); - expect(await get_computed_style('footer', 'color')).toEqual('rgb(128, 0, 128)'); -}); - -test('sets origin', async ({ baseURL, page }) => { - const { origin } = new URL(/** @type {string} */ (baseURL)); - - await page.goto(`${baseURL}/origin`); - - await expect(page.locator('[data-source="load"]')).toHaveText(origin); - await expect(page.locator('[data-source="store"]')).toHaveText(origin); - await expect(page.locator('[data-source="endpoint"]')).toHaveText(origin); -}); - -test('only includes CSS for rendered components', async ({ page, baseURL }) => { - await page.goto(`${baseURL}/styles`); - - const style = await page.innerHTML('style[amp-custom]'); - - expect(style).toContain('#ff3e00'); // rendered styles - expect(style).toContain('uppercase'); // imported styles - expect(style).not.toContain('#40b3ff'); // unrendered styles -}); - -test('http-equiv tags are removed', async ({ page }) => { - await page.goto('/http-equiv/cache-control'); - - expect(await page.textContent('h1')).toBe( - 'the cache-control headers should be removed from this page' - ); - expect(await page.innerHTML('head')).not.toContain('http-equiv="cache-control"'); -}); - -// validation tests are skipped because amphtml-validator doesn't -// play nicely with CI, and is also abominably slow, because -// everything AMP-related is awful -test.skip('prints validation errors', async ({ page, baseURL }) => { - await page.goto(`${baseURL}/invalid`); - - const body = await page.innerHTML('body'); - - expect(body).toContain("Invalid URL protocol 'javascript:' for attribute 'href' in tag 'a'"); -}); - -test.skip('throws error on encountering stylesheet links', async ({ page }) => { - await page.goto('/invalid/has-stylesheet'); - - expect(await page.textContent('body')).toContain( - 'An AMP document cannot contain — ensure that inlineStyleThreshold is set to Infinity, and remove links from your page template and elements' - ); -}); - -test('skips client build if every node has CSR disabled', async ({ page, request }) => { - test.skip(!!process.env.DEV); - - const files = fs.existsSync('.svelte-kit/output/client/_app/immutable/nodes'); - expect(files).toBe(false); - - await page.goto('/assets'); - const src = await page.locator('img').getAttribute('src'); - if (!src) throw new Error('Image src not found on /assets page'); - - const img_response = await request.get(src); - expect(img_response.status()).toBe(200); - - const public_asset_response = await request.get('/asset.json'); - expect(public_asset_response.status()).toBe(200); -}); diff --git a/packages/kit/test/apps/amp/tsconfig.json b/packages/kit/test/apps/amp/tsconfig.json deleted file mode 100644 index b1096bf168cd..000000000000 --- a/packages/kit/test/apps/amp/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "compilerOptions": { - "allowJs": true, - "checkJs": true, - "noEmit": true - }, - "extends": "./.svelte-kit/tsconfig.json" -} diff --git a/packages/kit/test/apps/amp/vite.config.js b/packages/kit/test/apps/amp/vite.config.js deleted file mode 100644 index 755ebab97929..000000000000 --- a/packages/kit/test/apps/amp/vite.config.js +++ /dev/null @@ -1,20 +0,0 @@ -import * as path from 'node:path'; -import { sveltekit } from '@sveltejs/kit/vite'; - -/** @type {import('vite').UserConfig} */ -const config = { - build: { - minify: false, - // TODO: remove when we stop testing for vite on node 18 - assetsInlineLimit: 0 - }, - clearScreen: false, - plugins: [sveltekit()], - server: { - fs: { - allow: [path.resolve('../../../src')] - } - } -}; - -export default config; diff --git a/packages/kit/test/apps/async/_test_dependencies/plain-lib/index.js b/packages/kit/test/apps/async/_test_dependencies/plain-lib/index.js new file mode 100644 index 000000000000..9776a321c5f1 --- /dev/null +++ b/packages/kit/test/apps/async/_test_dependencies/plain-lib/index.js @@ -0,0 +1 @@ +export { create_key_set } from './jwks/remote.js'; diff --git a/packages/kit/test/apps/async/_test_dependencies/plain-lib/jwks/remote.js b/packages/kit/test/apps/async/_test_dependencies/plain-lib/jwks/remote.js new file mode 100644 index 000000000000..401a09256b98 --- /dev/null +++ b/packages/kit/test/apps/async/_test_dependencies/plain-lib/jwks/remote.js @@ -0,0 +1,3 @@ +export function create_key_set(url) { + return `key set for ${url}`; +} diff --git a/packages/kit/test/apps/async/_test_dependencies/plain-lib/package.json b/packages/kit/test/apps/async/_test_dependencies/plain-lib/package.json new file mode 100644 index 000000000000..2f53775ab3a1 --- /dev/null +++ b/packages/kit/test/apps/async/_test_dependencies/plain-lib/package.json @@ -0,0 +1,8 @@ +{ + "name": "e2e-test-dep-plain", + "version": "1.0.0", + "private": true, + "type": "module", + "main": "index.js", + "svelte": "./index.js" +} diff --git a/packages/kit/test/apps/async/_test_dependencies/remote-lib/data.remote.js b/packages/kit/test/apps/async/_test_dependencies/remote-lib/data.remote.js new file mode 100644 index 000000000000..80f2cf9816bf --- /dev/null +++ b/packages/kit/test/apps/async/_test_dependencies/remote-lib/data.remote.js @@ -0,0 +1,3 @@ +import { query } from '$app/server'; + +export const lib_echo = query('unchecked', (value) => `lib says ${value}`); diff --git a/packages/kit/test/apps/async/_test_dependencies/remote-lib/index.js b/packages/kit/test/apps/async/_test_dependencies/remote-lib/index.js new file mode 100644 index 000000000000..f4bb682cc0e8 --- /dev/null +++ b/packages/kit/test/apps/async/_test_dependencies/remote-lib/index.js @@ -0,0 +1 @@ +export { lib_echo } from './data.remote.js'; diff --git a/packages/kit/test/apps/async/_test_dependencies/remote-lib/package.json b/packages/kit/test/apps/async/_test_dependencies/remote-lib/package.json new file mode 100644 index 000000000000..f5832a14d750 --- /dev/null +++ b/packages/kit/test/apps/async/_test_dependencies/remote-lib/package.json @@ -0,0 +1,11 @@ +{ + "name": "e2e-test-dep-remote", + "version": "1.0.0", + "private": true, + "type": "module", + "main": "index.js", + "svelte": "./index.js", + "peerDependencies": { + "@sveltejs/kit": "*" + } +} diff --git a/packages/kit/test/apps/async/package.json b/packages/kit/test/apps/async/package.json index 0a2094fca12e..bd0acfb01cf1 100644 --- a/packages/kit/test/apps/async/package.json +++ b/packages/kit/test/apps/async/package.json @@ -4,22 +4,29 @@ "version": "0.0.1", "type": "module", "scripts": { - "dev": "vite dev", + "dev": "svelte-kit sync && vite dev", "build": "vite build", "preview": "vite preview", - "prepare": "svelte-kit sync || echo ''", - "check": "svelte-kit sync && tsc && svelte-check", + "check": "svelte-kit sync && node node_modules/@typescript/native/bin/tsc && svelte-check --tsgo --incremental", "test": "pnpm test:dev && pnpm test:build", "test:dev": "DEV=true playwright test", - "test:build": "playwright test" + "test:build": "svelte-kit sync && vitest run && playwright test" }, "devDependencies": { + "@standard-schema/spec": "catalog:", "@sveltejs/kit": "workspace:^", "@sveltejs/vite-plugin-svelte": "catalog:", "svelte": "catalog:", "svelte-check": "catalog:", + "@typescript/native": "catalog:", "typescript": "catalog:", "valibot": "catalog:", - "vite": "catalog:" + "vite": "catalog:", + "e2e-test-dep-plain": "file:./_test_dependencies/plain-lib", + "e2e-test-dep-remote": "file:./_test_dependencies/remote-lib" + }, + "imports": { + "#lib": "./src/lib/index.js", + "#lib/*": "./src/lib/*" } } diff --git a/packages/kit/test/apps/async/playwright.config.js b/packages/kit/test/apps/async/playwright.config.js index f3ef088b1b6f..26e5ff5d744c 100644 --- a/packages/kit/test/apps/async/playwright.config.js +++ b/packages/kit/test/apps/async/playwright.config.js @@ -1,11 +1,13 @@ import process from 'node:process'; -import { config } from '../../utils.js'; +import { config, port } from '../../utils.js'; import { defineConfig } from '@playwright/test'; export default defineConfig({ ...config, webServer: { - command: process.env.DEV ? `pnpm dev` : `pnpm build && pnpm preview`, - port: process.env.DEV ? 5173 : 4173 + command: process.env.DEV + ? `pnpm dev --port ${port} --strictPort` + : `pnpm build && pnpm preview --port ${port} --strictPort`, + port } }); diff --git a/packages/kit/test/apps/async/src/app.d.ts b/packages/kit/test/apps/async/src/app.d.ts new file mode 100644 index 000000000000..61c54b6b6028 --- /dev/null +++ b/packages/kit/test/apps/async/src/app.d.ts @@ -0,0 +1,7 @@ +declare global { + interface Window { + __recomputations: number; + } +} + +export {}; diff --git a/packages/kit/test/apps/async/src/env.ts b/packages/kit/test/apps/async/src/env.ts new file mode 100644 index 000000000000..82f110e72262 --- /dev/null +++ b/packages/kit/test/apps/async/src/env.ts @@ -0,0 +1,20 @@ +import { defineEnvVars } from '@sveltejs/kit/env'; + +export const variables = defineEnvVars({ + PRIVATE_STATIC: { + public: false, + static: true + }, + PRIVATE_DYNAMIC: { + public: false, + static: false + }, + PUBLIC_STATIC: { + public: true, + static: true + }, + PUBLIC_DYNAMIC: { + public: true, + static: false + } +}); diff --git a/packages/kit/test/apps/async/src/hooks.client.js b/packages/kit/test/apps/async/src/hooks.client.js index af343e2d1022..598e971f83fd 100644 --- a/packages/kit/test/apps/async/src/hooks.client.js +++ b/packages/kit/test/apps/async/src/hooks.client.js @@ -1,13 +1,17 @@ import { isRedirect } from '@sveltejs/kit'; -/** @type {import('@sveltejs/kit').HandleClientError} */ -export const handleError = ({ error: e, event, status, message }) => { +/** @type {import('@sveltejs/kit/hooks').HandleClientError} */ +export const handleError = (input) => { // helps us catch sveltekit redirects thrown in component code - if (isRedirect(e)) { + if (isRedirect(input.error)) { throw new Error("Redirects shouldn't trigger the handleError hook"); } - const error = /** @type {Error} */ (e); + if (input.kind !== 'unknown') return input.error; - return { message: `${error.message} (${status} ${message}, on ${event.url.pathname})` }; + const error = /** @type {Error} */ (input.error); + + return { + message: `${error.message} (500 Internal Error, on ${input.event.url.pathname})` + }; }; diff --git a/packages/kit/test/apps/async/src/hooks.js b/packages/kit/test/apps/async/src/hooks.js index 238cc7744f2c..96240b3cc14f 100644 --- a/packages/kit/test/apps/async/src/hooks.js +++ b/packages/kit/test/apps/async/src/hooks.js @@ -1,6 +1,6 @@ -import { Foo } from '$lib'; +import { Foo } from '#lib'; -/** @type {import("@sveltejs/kit").Transport} */ +/** @type {import("@sveltejs/kit/hooks").Transport} */ export const transport = { Foo: { encode: (value) => value instanceof Foo && [value.message], diff --git a/packages/kit/test/apps/async/src/hooks.server.js b/packages/kit/test/apps/async/src/hooks.server.js index 3e9c141e2f37..cca0ba034c0d 100644 --- a/packages/kit/test/apps/async/src/hooks.server.js +++ b/packages/kit/test/apps/async/src/hooks.server.js @@ -1,15 +1,14 @@ import { isRedirect } from '@sveltejs/kit'; import { do_something } from './routes/remote/server-action/action.remote'; -/** @type {import('@sveltejs/kit').Handle} */ +/** @type {import('@sveltejs/kit/hooks').Handle} */ export async function handle({ event, resolve }) { // Assign each browser session a unique id so that the in-memory `count` // state in `routes/remote/query-command.remote.js` is isolated per test. // Without this, tests running in parallel (different Playwright workers) // against the same server clobber each other's state and flake. - if (!event.cookies.get('count_session')) { - event.cookies.set('count_session', crypto.randomUUID(), { - path: '/', + if (!event.cookies.get('session')) { + event.cookies.set('session', crypto.randomUUID(), { httpOnly: true, sameSite: 'lax' }); @@ -38,31 +37,24 @@ export async function handle({ event, resolve }) { return resolve(event); } -/** @type {import('@sveltejs/kit').HandleValidationError} */ -export const handleValidationError = ({ issues }) => { - return { message: issues[0].message }; -}; - -/** @type {import('@sveltejs/kit').HandleServerError} */ -export const handleError = ({ error: e, event, status, message }) => { +/** @type {import('@sveltejs/kit/hooks').HandleServerError} */ +export const handleError = (input) => { // helps us catch sveltekit redirects thrown in component code - if (isRedirect(e)) { + if (isRedirect(input.error)) { throw new Error("Redirects shouldn't trigger the handleError hook"); } - const error = /** @type {Error} */ (e); + if (input.kind === 'validation') { + // must not throw, even when validation failed inside a query + void input.event.url.pathname; + return { message: input.issues[0].message }; + } - return { message: `${error.message} (${status} ${message}, on ${event.url.pathname})` }; -}; + if (input.kind !== 'unknown') return input.error; + + const error = /** @type {Error} */ (input.error); -// TODO: remove in SvelteKit 3.0 -// @ts-ignore this doesn't exist in old Node -Promise.withResolvers ??= () => { - /** @type {{ promise: Promise, resolve: (value: any) => void, reject: (reason?: any) => void }} */ - const d = {}; - d.promise = new Promise((resolve, reject) => { - d.resolve = resolve; - d.reject = reject; - }); - return d; + return { + message: `${error.message} (500 Internal Error, on ${input.event.url.pathname})` + }; }; diff --git a/packages/kit/test/apps/async/src/routes/+error.svelte b/packages/kit/test/apps/async/src/routes/+error.svelte index 31f3cb324e47..0d3cab0c999b 100644 --- a/packages/kit/test/apps/async/src/routes/+error.svelte +++ b/packages/kit/test/apps/async/src/routes/+error.svelte @@ -12,6 +12,8 @@

This is your custom error page saying: "{error.message}"

+home + diff --git a/packages/kit/test/apps/options-2/src/routes/asset-preload/prerendered/+page.server.js b/packages/kit/test/apps/options-2/src/routes/asset-preload/prerendered/+page.server.js new file mode 100644 index 000000000000..189f71e2e1b3 --- /dev/null +++ b/packages/kit/test/apps/options-2/src/routes/asset-preload/prerendered/+page.server.js @@ -0,0 +1 @@ +export const prerender = true; diff --git a/packages/kit/test/apps/basics/src/routes/asset-preload/prerendered/+page.svelte b/packages/kit/test/apps/options-2/src/routes/asset-preload/prerendered/+page.svelte similarity index 100% rename from packages/kit/test/apps/basics/src/routes/asset-preload/prerendered/+page.svelte rename to packages/kit/test/apps/options-2/src/routes/asset-preload/prerendered/+page.svelte diff --git a/packages/kit/test/apps/options-2/src/routes/asset-preload/shlop.woff2 b/packages/kit/test/apps/options-2/src/routes/asset-preload/shlop.woff2 new file mode 100644 index 000000000000..6e1af4e75189 Binary files /dev/null and b/packages/kit/test/apps/options-2/src/routes/asset-preload/shlop.woff2 differ diff --git a/packages/kit/test/apps/options-2/src/routes/asset-preload/styles.css b/packages/kit/test/apps/options-2/src/routes/asset-preload/styles.css new file mode 100644 index 000000000000..fef546dd088d --- /dev/null +++ b/packages/kit/test/apps/options-2/src/routes/asset-preload/styles.css @@ -0,0 +1,5 @@ +@font-face { + /* we need a font file that exceeds assetsInlineLimit */ + font-family: 'Shlop'; + src: url(./shlop.woff2); +} diff --git a/packages/kit/test/apps/options-2/src/routes/deeply/nested/page/+page.svelte b/packages/kit/test/apps/options-2/src/routes/deeply/nested/page/+page.svelte index f645edd52e3d..6c2701fa0f46 100644 --- a/packages/kit/test/apps/options-2/src/routes/deeply/nested/page/+page.svelte +++ b/packages/kit/test/apps/options-2/src/routes/deeply/nested/page/+page.svelte @@ -4,8 +4,8 @@

Hello

-

base: {resolve('/')}

-

assets: {asset('/')}

+

base: {resolve('')}

+

assets: {asset('answer.txt').replace('answer.txt', '')}