From ab2a449012da15b27adba1a53d2d51a0bd96dfd4 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 13 Aug 2026 20:07:52 -0400 Subject: [PATCH 01/12] chore: tidy up $app/env modules --- packages/kit/package.json | 11 +++++++++++ packages/kit/src/core/env.js | 4 ++-- packages/kit/src/core/sync/write_server.js | 2 +- packages/kit/src/runtime/app/env/client.js | 5 +++++ packages/kit/src/runtime/app/env/index.js | 2 +- .../runtime/app/env/{private.js => private/index.js} | 0 .../src/runtime/app/env/{internal.js => server.js} | 5 +---- packages/kit/src/runtime/app/env/standard-schema.d.ts | 0 packages/kit/src/runtime/app/server/remote/query.js | 2 +- packages/kit/src/runtime/env/dynamic/private.js | 2 +- packages/kit/src/runtime/env/static/private.js | 2 +- packages/kit/src/runtime/invalid-import.js | 1 + 12 files changed, 25 insertions(+), 11 deletions(-) create mode 100644 packages/kit/src/runtime/app/env/client.js rename packages/kit/src/runtime/app/env/{private.js => private/index.js} (100%) rename packages/kit/src/runtime/app/env/{internal.js => server.js} (51%) delete mode 100644 packages/kit/src/runtime/app/env/standard-schema.d.ts create mode 100644 packages/kit/src/runtime/invalid-import.js diff --git a/packages/kit/package.json b/packages/kit/package.json index ddae5c52ed29..ce0310c4e40a 100644 --- a/packages/kit/package.json +++ b/packages/kit/package.json @@ -93,6 +93,17 @@ "generate:types": "node scripts/generate-dts.js" }, "imports": { + "#app/env": { + "types": "./src/runtime/app/env/internal.d.ts", + "workerd": "./src/runtime/app/env/server.js", + "browser": "./src/runtime/app/env/client.js", + "default": "./src/runtime/app/env/server.js" + }, + "#app/env/server": { + "workerd": "./src/runtime/app/env/server.js", + "browser": "./src/runtime/invalid-import.js", + "default": "./src/runtime/app/env/server.js" + }, "#app/internal/transport": { "default": "./src/runtime/app/internal/transport.js" }, diff --git a/packages/kit/src/core/env.js b/packages/kit/src/core/env.js index 8b27400b4ab1..362517bb208c 100644 --- a/packages/kit/src/core/env.js +++ b/packages/kit/src/core/env.js @@ -55,8 +55,8 @@ export async function load_explicit_env(vite, kit, file, root, mode) { const runner = get_runner(vite, server); - /** @type {typeof import('../runtime/app/env/internal.js')} */ ( - await runner.import(`${runtime_directory}/app/env/internal.js`) + /** @type {typeof import('../runtime/app/env/server.js')} */ ( + await runner.import(`${runtime_directory}/app/env/server.js`) ).set_building(); try { diff --git a/packages/kit/src/core/sync/write_server.js b/packages/kit/src/core/sync/write_server.js index 5ae7105a6a26..8c49b4408b37 100644 --- a/packages/kit/src/core/sync/write_server.js +++ b/packages/kit/src/core/sync/write_server.js @@ -23,7 +23,7 @@ const server_template = ({ has_service_worker, template }) => ` -import { set_building, set_prerendering } from '$app/env/internal'; +import { set_building, set_prerendering } from '$app/env/server'; import { set_assets } from '$app/paths/internal/server'; import { set_fix_stack_trace, set_manifest, set_read_implementation, log_response } from '__sveltekit/server'; import error from '../shared/error-template.js'; diff --git a/packages/kit/src/runtime/app/env/client.js b/packages/kit/src/runtime/app/env/client.js new file mode 100644 index 000000000000..e37c0e84c7c9 --- /dev/null +++ b/packages/kit/src/runtime/app/env/client.js @@ -0,0 +1,5 @@ +import { payload } from '../../client/payload.js'; + +export const version = payload.version; +export const building = false; +export const prerendering = false; diff --git a/packages/kit/src/runtime/app/env/index.js b/packages/kit/src/runtime/app/env/index.js index 87a7b3521a10..fd92ec99ded1 100644 --- a/packages/kit/src/runtime/app/env/index.js +++ b/packages/kit/src/runtime/app/env/index.js @@ -1,2 +1,2 @@ export { BROWSER as browser, DEV as dev } from 'esm-env'; -export { building, version } from './internal.js'; +export { building, version } from '#app/env'; diff --git a/packages/kit/src/runtime/app/env/private.js b/packages/kit/src/runtime/app/env/private/index.js similarity index 100% rename from packages/kit/src/runtime/app/env/private.js rename to packages/kit/src/runtime/app/env/private/index.js diff --git a/packages/kit/src/runtime/app/env/internal.js b/packages/kit/src/runtime/app/env/server.js similarity index 51% rename from packages/kit/src/runtime/app/env/internal.js rename to packages/kit/src/runtime/app/env/server.js index ec8fa432a666..30a47983df52 100644 --- a/packages/kit/src/runtime/app/env/internal.js +++ b/packages/kit/src/runtime/app/env/server.js @@ -1,7 +1,4 @@ -import { BROWSER } from 'esm-env'; -import { payload } from '../../client/payload.js'; - -export const version = BROWSER ? payload.version : __SVELTEKIT_APP_VERSION__; +export const version = __SVELTEKIT_APP_VERSION__; export let building = false; export let prerendering = false; diff --git a/packages/kit/src/runtime/app/env/standard-schema.d.ts b/packages/kit/src/runtime/app/env/standard-schema.d.ts deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/packages/kit/src/runtime/app/server/remote/query.js b/packages/kit/src/runtime/app/server/remote/query.js index f69f4e1c3338..1a0da5bf84e3 100644 --- a/packages/kit/src/runtime/app/server/remote/query.js +++ b/packages/kit/src/runtime/app/server/remote/query.js @@ -4,7 +4,7 @@ /** @import { StandardSchemaV1 } from '@standard-schema/spec' */ import { get_request_store } from '@sveltejs/kit/internal/server'; import { create_remote_key, stringify_remote_arg } from '../../../shared.js'; -import { prerendering } from '$app/env/internal'; +import { prerendering } from '#app/env/server'; import { create_validator, get_cache, diff --git a/packages/kit/src/runtime/env/dynamic/private.js b/packages/kit/src/runtime/env/dynamic/private.js index d1875571ac66..836f84c8eb0c 100644 --- a/packages/kit/src/runtime/env/dynamic/private.js +++ b/packages/kit/src/runtime/env/dynamic/private.js @@ -1,5 +1,5 @@ import { DEV } from 'esm-env'; -import * as env from '../../app/env/private.js'; +import * as env from '../../app/env/private/index.js'; export { env }; if (DEV) { diff --git a/packages/kit/src/runtime/env/static/private.js b/packages/kit/src/runtime/env/static/private.js index 27beb05e7236..70293809df44 100644 --- a/packages/kit/src/runtime/env/static/private.js +++ b/packages/kit/src/runtime/env/static/private.js @@ -1,5 +1,5 @@ import { DEV } from 'esm-env'; -export * from '../../app/env/private.js'; +export * from '../../app/env/private/index.js'; if (DEV) { console.warn('`$env/static/private` is deprecated, use `$app/env/private` instead'); diff --git a/packages/kit/src/runtime/invalid-import.js b/packages/kit/src/runtime/invalid-import.js new file mode 100644 index 000000000000..99ca89eff783 --- /dev/null +++ b/packages/kit/src/runtime/invalid-import.js @@ -0,0 +1 @@ +throw new Error('invalid import'); From 3739d11d553783807a9e1d8cd6645fbc94af708c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 13 Aug 2026 20:13:48 -0400 Subject: [PATCH 02/12] move --- packages/kit/package.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/kit/package.json b/packages/kit/package.json index ce0310c4e40a..8b19e49c7f69 100644 --- a/packages/kit/package.json +++ b/packages/kit/package.json @@ -99,6 +99,11 @@ "browser": "./src/runtime/app/env/client.js", "default": "./src/runtime/app/env/server.js" }, + "#app/env/public": { + "workerd": "./src/runtime/app/env/public/server.js", + "browser": "./src/runtime/app/env/public/client.js", + "default": "./src/runtime/app/env/public/server.js" + }, "#app/env/server": { "workerd": "./src/runtime/app/env/server.js", "browser": "./src/runtime/invalid-import.js", @@ -113,11 +118,6 @@ "browser": "./src/runtime/app/paths/client.js", "default": "./src/runtime/app/paths/server.js" }, - "#app/env/public": { - "workerd": "./src/runtime/app/env/public/server.js", - "browser": "./src/runtime/app/env/public/client.js", - "default": "./src/runtime/app/env/public/server.js" - }, "#internal": { "workerd": "./src/exports/internal/server/index.js", "browser": "./src/exports/internal/client.js", From d97f3df17dd924e72977a03fde8d57092dd0d3a9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 13 Aug 2026 20:26:24 -0400 Subject: [PATCH 03/12] use client.js as the source of truth --- packages/kit/package.json | 2 +- packages/kit/scripts/generate-dts.js | 2 +- packages/kit/src/runtime/app/env/client.js | 23 +++++++++++++++++++-- packages/kit/src/runtime/app/env/index.js | 3 +-- packages/kit/src/runtime/app/env/server.js | 3 +++ packages/kit/src/runtime/app/env/types.d.ts | 19 ----------------- packages/kit/tsconfig.json | 2 +- packages/kit/types/index.d.ts | 9 +++----- 8 files changed, 31 insertions(+), 32 deletions(-) delete mode 100644 packages/kit/src/runtime/app/env/types.d.ts diff --git a/packages/kit/package.json b/packages/kit/package.json index 8b19e49c7f69..a868720a09dc 100644 --- a/packages/kit/package.json +++ b/packages/kit/package.json @@ -94,7 +94,7 @@ }, "imports": { "#app/env": { - "types": "./src/runtime/app/env/internal.d.ts", + "types": "./src/runtime/app/env/client.js", "workerd": "./src/runtime/app/env/server.js", "browser": "./src/runtime/app/env/client.js", "default": "./src/runtime/app/env/server.js" diff --git a/packages/kit/scripts/generate-dts.js b/packages/kit/scripts/generate-dts.js index 89f6214ee465..c12113a94a40 100644 --- a/packages/kit/scripts/generate-dts.js +++ b/packages/kit/scripts/generate-dts.js @@ -10,7 +10,7 @@ await createBundle({ '@sveltejs/kit/node': 'src/exports/node/index.js', '@sveltejs/kit/params': 'src/exports/params/public.d.ts', '@sveltejs/kit/vite': 'src/exports/vite/public.d.ts', - '$app/env': 'src/runtime/app/env/types.d.ts', + '$app/env': 'src/runtime/app/env/client.js', '$app/forms': 'src/runtime/app/forms/public.d.ts', '$app/navigation': 'src/runtime/app/navigation/public.d.ts', '$app/paths': 'src/runtime/app/paths/public.d.ts', diff --git a/packages/kit/src/runtime/app/env/client.js b/packages/kit/src/runtime/app/env/client.js index e37c0e84c7c9..b3aca49b0ef5 100644 --- a/packages/kit/src/runtime/app/env/client.js +++ b/packages/kit/src/runtime/app/env/client.js @@ -1,5 +1,24 @@ import { payload } from '../../client/payload.js'; -export const version = payload.version; +/** + * `true` if the app is running in the browser. + * @type {boolean} + */ +export const browser = true; + +/** + * Whether the dev server is running. This is not guaranteed to correspond to `NODE_ENV` or `MODE`. + * @type {boolean} + */ +export const dev = __SVELTEKIT_DEV__; + +/** + * SvelteKit analyses your app during the `build` step by running it. During this process, `building` is `true`. This also applies during prerendering. + * @type {boolean} + */ export const building = false; -export const prerendering = false; + +/** + * The value of `config.version.name`. + */ +export const version = payload.version; diff --git a/packages/kit/src/runtime/app/env/index.js b/packages/kit/src/runtime/app/env/index.js index fd92ec99ded1..8392a0e4932f 100644 --- a/packages/kit/src/runtime/app/env/index.js +++ b/packages/kit/src/runtime/app/env/index.js @@ -1,2 +1 @@ -export { BROWSER as browser, DEV as dev } from 'esm-env'; -export { building, version } from '#app/env'; +export { browser, dev, building, version } from '#app/env'; diff --git a/packages/kit/src/runtime/app/env/server.js b/packages/kit/src/runtime/app/env/server.js index 30a47983df52..62f7ff4385c9 100644 --- a/packages/kit/src/runtime/app/env/server.js +++ b/packages/kit/src/runtime/app/env/server.js @@ -1,5 +1,8 @@ +export const browser = false; +export const dev = __SVELTEKIT_DEV__; export const version = __SVELTEKIT_APP_VERSION__; export let building = false; + export let prerendering = false; export function set_building() { diff --git a/packages/kit/src/runtime/app/env/types.d.ts b/packages/kit/src/runtime/app/env/types.d.ts deleted file mode 100644 index fb52b3170080..000000000000 --- a/packages/kit/src/runtime/app/env/types.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * `true` if the app is running in the browser. - */ -export const browser: boolean; - -/** - * Whether the dev server is running. This is not guaranteed to correspond to `NODE_ENV` or `MODE`. - */ -export const dev: boolean; - -/** - * SvelteKit analyses your app during the `build` step by running it. During this process, `building` is `true`. This also applies during prerendering. - */ -export const building: boolean; - -/** - * The value of `config.version.name`. - */ -export const version: string; diff --git a/packages/kit/tsconfig.json b/packages/kit/tsconfig.json index 36d123cc24e4..34fdf085d835 100644 --- a/packages/kit/tsconfig.json +++ b/packages/kit/tsconfig.json @@ -15,7 +15,7 @@ "@sveltejs/kit/node": ["./src/exports/node/index.js"], "@sveltejs/kit/internal": ["./src/exports/internal/index.js"], "@sveltejs/kit/internal/server": ["./src/exports/internal/server/index.js"], - "$app/env": ["./src/runtime/app/env/types.d.ts"], + "$app/env": ["./src/runtime/app/env/client.js"], "$app/env/internal": ["./src/runtime/app/env/internal.js"], "$app/forms": ["./src/runtime/app/forms/public.d.ts"], "$app/navigation": ["./src/runtime/app/navigation/public.d.ts"], diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index b8a0c2cf11e7..2443df9631a8 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -2289,19 +2289,16 @@ declare module '@sveltejs/kit/vite' { declare module '$app/env' { /** * `true` if the app is running in the browser. - */ + * */ export const browser: boolean; - /** * Whether the dev server is running. This is not guaranteed to correspond to `NODE_ENV` or `MODE`. - */ + * */ export const dev: boolean; - /** * SvelteKit analyses your app during the `build` step by running it. During this process, `building` is `true`. This also applies during prerendering. - */ + * */ export const building: boolean; - /** * The value of `config.version.name`. */ From 97a63c8a35b6dee4063e301864b82278457c4412 Mon Sep 17 00:00:00 2001 From: "vercel[bot]" <35613825+vercel[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 00:33:04 +0000 Subject: [PATCH 04/12] Fix: `app_env_private` in `module_ids.js` points to the renamed-away path `runtime/app/env/private.js`, breaking server-only import enforcement for `$app/env/private`. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit fixes the issue reported at packages/kit/src/exports/vite/module_ids.js:19 ## Bug This PR renamed `packages/kit/src/runtime/app/env/private.js` → `packages/kit/src/runtime/app/env/private/index.js` (verified: only `private/index.js` exists now). But `packages/kit/src/exports/vite/module_ids.js` still computed: ```js export const app_env_private = posixify( fileURLToPath(new URL('../../runtime/app/env/private.js', import.meta.url)) ); ``` which resolves to a non-existent path `.../runtime/app/env/private.js`. `$app/env/private` is resolved at build time via the `$app` alias (`replacement:` ), producing the real module id `.../runtime/app/env/private/index.js`. Since `app_env_private` no longer equals that id: * In `src/exports/vite/index.js` (~line 830), `exactRegex(app_env_private)` in the `load.filter.id` no longer matches the actual module id, so the server-only enforcement handler is never invoked for `$app/env/private`. * In `src/exports/vite/utils.js` (~line 122), `normalize_id` no longer maps the id back to `$app/env/private`, so the `is_server_only === '$app/env/private'` check fails. **Failure mode / trigger:** importing `$app/env/private` from client-side code. Previously this produced an illegal-import error blocking private env vars from reaching client bundles; with the stale path, that enforcement silently no longer fires, risking private environment variables leaking into client output. Note that the sibling constant `app_server` already correctly points to `server/index.js`, confirming the intended pattern. ## Fix Updated the path to `../../runtime/app/env/private/index.js`: ```js export const app_env_private = posixify( fileURLToPath(new URL('../../runtime/app/env/private/index.js', import.meta.url)) ); ``` The `utils.spec.js` expectation (`normalize_id(app_env_private, [], cwd)` → `'$app/env/private'`) still holds, since it compares against the same constant. Co-authored-by: Vercel Co-authored-by: Rich-Harris --- packages/kit/src/exports/vite/module_ids.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kit/src/exports/vite/module_ids.js b/packages/kit/src/exports/vite/module_ids.js index a297a0b220be..065df4df902e 100644 --- a/packages/kit/src/exports/vite/module_ids.js +++ b/packages/kit/src/exports/vite/module_ids.js @@ -16,5 +16,5 @@ export const app_server = posixify( ); export const app_env_private = posixify( - fileURLToPath(new URL('../../runtime/app/env/private.js', import.meta.url)) + fileURLToPath(new URL('../../runtime/app/env/private/index.js', import.meta.url)) ); From a65422c9d7237b4914184dca2de7b3c3d59c4caf Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 13 Aug 2026 20:37:28 -0400 Subject: [PATCH 05/12] split $app/navigation --- packages/kit/package.json | 6 +++ .../kit/src/runtime/app/navigation/client.js | 15 ++++++++ .../kit/src/runtime/app/navigation/index.js | 16 +------- .../src/runtime/app/navigation/public.d.ts | 2 +- .../kit/src/runtime/app/navigation/server.js | 25 ++++++++++++ packages/kit/src/runtime/client/client.js | 38 +------------------ 6 files changed, 49 insertions(+), 53 deletions(-) create mode 100644 packages/kit/src/runtime/app/navigation/client.js create mode 100644 packages/kit/src/runtime/app/navigation/server.js diff --git a/packages/kit/package.json b/packages/kit/package.json index a868720a09dc..44213bd7c625 100644 --- a/packages/kit/package.json +++ b/packages/kit/package.json @@ -112,6 +112,12 @@ "#app/internal/transport": { "default": "./src/runtime/app/internal/transport.js" }, + "#app/navigation": { + "types": "./src/runtime/app/navigation/client.js", + "workerd": "./src/runtime/app/navigation/server.js", + "browser": "./src/runtime/app/navigation/client.js", + "default": "./src/runtime/app/navigation/server.js" + }, "#app/paths": { "types": "./src/runtime/app/paths/internal.d.ts", "workerd": "./src/runtime/app/paths/server.js", diff --git a/packages/kit/src/runtime/app/navigation/client.js b/packages/kit/src/runtime/app/navigation/client.js new file mode 100644 index 000000000000..f62d582d114a --- /dev/null +++ b/packages/kit/src/runtime/app/navigation/client.js @@ -0,0 +1,15 @@ +export { + afterNavigate, + beforeNavigate, + disableScrollHandling, + goto, + invalidate, + invalidateAll, + refreshAll, + onNavigate, + preloadCode, + preloadData, + pushState, + replaceState +} from '../../client/client.js'; +export { snapshot } from '../../client/snapshots.js'; diff --git a/packages/kit/src/runtime/app/navigation/index.js b/packages/kit/src/runtime/app/navigation/index.js index f62d582d114a..e2e4f2648993 100644 --- a/packages/kit/src/runtime/app/navigation/index.js +++ b/packages/kit/src/runtime/app/navigation/index.js @@ -1,15 +1 @@ -export { - afterNavigate, - beforeNavigate, - disableScrollHandling, - goto, - invalidate, - invalidateAll, - refreshAll, - onNavigate, - preloadCode, - preloadData, - pushState, - replaceState -} from '../../client/client.js'; -export { snapshot } from '../../client/snapshots.js'; +export * from '#app/navigation'; diff --git a/packages/kit/src/runtime/app/navigation/public.d.ts b/packages/kit/src/runtime/app/navigation/public.d.ts index 3788abc29eff..325612ede306 100644 --- a/packages/kit/src/runtime/app/navigation/public.d.ts +++ b/packages/kit/src/runtime/app/navigation/public.d.ts @@ -1,6 +1,6 @@ import type { LayoutParams as AppLayoutParams, RouteId as AppRouteId } from '$app/types'; -export * from './index.js'; +export * from './client.js'; /** * Information about the target of a specific navigation. diff --git a/packages/kit/src/runtime/app/navigation/server.js b/packages/kit/src/runtime/app/navigation/server.js new file mode 100644 index 000000000000..9fb3ba2e72d1 --- /dev/null +++ b/packages/kit/src/runtime/app/navigation/server.js @@ -0,0 +1,25 @@ +import { noop } from '../../../utils/functions.js'; + +export const afterNavigate = noop; +export const beforeNavigate = noop; +export const disableScrollHandling = disallow('disableScrollHandling', '()'); +export const goto = disallow('goto'); +export const invalidate = disallow('invalidate'); +export const invalidateAll = disallow('invalidateAll', '()'); +export const onNavigate = noop; +export const refreshAll = disallow('refreshAll', '()'); +export const preloadCode = disallow('preloadCode'); +export const preloadData = disallow('preloadData'); +export const pushState = disallow('pushState'); +export const replaceState = disallow('replaceState'); +export const snapshot = noop; + +/** + * @param {string} name + * @param {string} [parens] + */ +function disallow(name, parens = '(...)') { + return () => { + throw new Error(`Cannot call \`${name}${parens}\` on the server`); + }; +} diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index 5d2765a133fa..102429807633 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -2605,10 +2605,6 @@ export function onNavigate(callback) { * @returns {void} */ export function disableScrollHandling() { - if (!BROWSER) { - throw new Error('Cannot call disableScrollHandling() on the server'); - } - if (DEV && started && !updating) { throw new Error('Can only disable scroll handling during navigation'); } @@ -2665,10 +2661,6 @@ async function resolve_intent(url, caller) { * @returns {Promise} */ export async function goto(url, opts = {}) { - if (!BROWSER) { - throw new Error('Cannot call goto(...) on the server'); - } - if (DEV) { if ('replaceState' in opts && !warned_on_replace_state) { warned_on_replace_state = true; @@ -2737,12 +2729,7 @@ export async function goto(url, opts = {}) { * @returns {Promise} */ export function invalidate(resource, keepState = false) { - if (!BROWSER) { - throw new Error('Cannot call invalidate(...) on the server'); - } - push_invalidated(resource); - return _invalidate(!keepState); } @@ -2767,10 +2754,6 @@ function push_invalidated(resource) { * @returns {Promise} */ export function invalidateAll() { - if (!BROWSER) { - throw new Error('Cannot call invalidateAll() on the server'); - } - force_invalidation = true; return _invalidate(); } @@ -2781,10 +2764,6 @@ export function invalidateAll() { * @returns {Promise} */ export function refreshAll() { - if (!BROWSER) { - throw new Error('Cannot call refreshAll() on the server'); - } - force_invalidation = true; return _invalidate(false); } @@ -2802,10 +2781,6 @@ export function refreshAll() { * @returns {Promise<({ type: 'loaded'; data: Record } | { type: 'redirect'; location: string } | { type: 'error'; error: App.Error }) & { status: number; }>} */ export async function preloadData(href) { - if (!BROWSER) { - throw new Error('Cannot call preloadData(...) on the server'); - } - const url = resolve_url(href); const intent = await get_navigation_intent(url, false); @@ -2866,10 +2841,6 @@ export async function preloadData(href) { * @returns {Promise} */ export async function preloadCode(id) { - if (!BROWSER) { - throw new Error('Cannot call preloadCode(...) on the server'); - } - if (DEV && id[0] !== '/') { throw new Error( `argument passed to preloadCode must be a route ID (i.e. "/blog/[slug]" rather than "blog/[slug]")` @@ -2927,10 +2898,6 @@ export async function preloadCode(id) { * @returns {Promise} */ export async function pushState(url, state) { - if (!BROWSER) { - throw new Error('Cannot call pushState(...) on the server'); - } - if (DEV && !warned_on_push_state) { warned_on_push_state = true; console.warn( @@ -2957,10 +2924,6 @@ export async function pushState(url, state) { * @returns {Promise} */ export async function replaceState(url, state) { - if (!BROWSER) { - throw new Error('Cannot call replaceState(...) on the server'); - } - if (DEV && !warned_on_replace_state_function) { warned_on_replace_state_function = true; console.warn( @@ -3107,6 +3070,7 @@ async function update_state(intent, state, { replace, persist_state, reset }, ca * @returns {Promise} */ export async function applyAction(result) { + // TODO get rid of this if (!BROWSER) { throw new Error('Cannot call applyAction(...) on the server'); } From 7500a7c208f1698b26528d6b569078070532a72e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 13 Aug 2026 20:43:14 -0400 Subject: [PATCH 06/12] fix --- packages/kit/src/core/env.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/kit/src/core/env.js b/packages/kit/src/core/env.js index 362517bb208c..372a1aeb525d 100644 --- a/packages/kit/src/core/env.js +++ b/packages/kit/src/core/env.js @@ -39,8 +39,10 @@ export async function load_explicit_env(vite, kit, file, root, mode) { logLevel: 'silent', mode, define: { - __SVELTEKIT_PAYLOAD__: 'undefined', // coming in through static import in env/internal.js but will end up unused - __SVELTEKIT_APP_VERSION__: JSON.stringify(kit.version.name) // needed by $app/env + // these are needed by $app/env + __SVELTEKIT_APP_VERSION__: JSON.stringify(kit.version.name), + __SVELTEKIT_DEV__: mode === 'development', + __SVELTEKIT_PAYLOAD__: 'undefined' // coming in through static import in env/internal.js but will end up unused }, resolve: { alias: [ From 836a263288f962d639a310bff8a6425138ca128d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 13 Aug 2026 21:33:00 -0400 Subject: [PATCH 07/12] split up $app/forms --- packages/kit/package.json | 6 + packages/kit/src/runtime/app/forms/client.js | 263 +++++++++++++++++ packages/kit/src/runtime/app/forms/index.js | 264 +----------------- .../kit/src/runtime/app/forms/public.d.ts | 2 +- packages/kit/src/runtime/app/forms/server.js | 5 + .../kit/src/runtime/app/navigation/server.js | 30 +- packages/kit/src/runtime/client/client.js | 9 +- packages/kit/src/utils/functions.js | 10 + playgrounds/basic/src/env.ts | 5 + 9 files changed, 303 insertions(+), 291 deletions(-) create mode 100644 packages/kit/src/runtime/app/forms/client.js create mode 100644 packages/kit/src/runtime/app/forms/server.js create mode 100644 playgrounds/basic/src/env.ts diff --git a/packages/kit/package.json b/packages/kit/package.json index 44213bd7c625..7e3e64b53321 100644 --- a/packages/kit/package.json +++ b/packages/kit/package.json @@ -109,6 +109,12 @@ "browser": "./src/runtime/invalid-import.js", "default": "./src/runtime/app/env/server.js" }, + "#app/forms": { + "types": "./src/runtime/app/forms/client.js", + "workerd": "./src/runtime/app/forms/server.js", + "browser": "./src/runtime/app/forms/client.js", + "default": "./src/runtime/app/forms/server.js" + }, "#app/internal/transport": { "default": "./src/runtime/app/internal/transport.js" }, diff --git a/packages/kit/src/runtime/app/forms/client.js b/packages/kit/src/runtime/app/forms/client.js new file mode 100644 index 000000000000..631331a34528 --- /dev/null +++ b/packages/kit/src/runtime/app/forms/client.js @@ -0,0 +1,263 @@ +/** @import { ActionResult, SubmitFunction } from './types.js' */ +import { DEV } from 'esm-env'; +import { noop } from '../../../utils/functions.js'; +import { refreshAll } from '../navigation/index.js'; +import { + applyAction, + apply_action_navigation, + handle_error, + is_current_location +} from '../../client/client.js'; +import { notify_version } from '../../client/state.svelte.js'; +import { parse } from '#app/internal/transport'; + +export { applyAction }; + +/** + * Use this function to deserialize the response from a form submission. + * Usage: + * + * ```js + * import { deserialize } from '$app/forms'; + * + * async function handleSubmit(event) { + * const response = await fetch('/form?/action', { + * method: 'POST', + * body: new FormData(event.target) + * }); + * + * const result = deserialize(await response.text()); + * // ... + * } + * ``` + * @template {Record | undefined} Success + * @template {Record | undefined} Failure + * @param {string} result + * @returns {ActionResult} + */ +export function deserialize(result) { + const parsed = JSON.parse(result); + + if (parsed.data) { + parsed.data = parse(parsed.data); + } + + return parsed; +} + +/** + * Shallow clone an element, so that we can access e.g. `form.action` without worrying + * that someone has added an `` (https://github.com/sveltejs/kit/issues/7593) + * @template {HTMLElement} T + * @param {T} element + * @returns {T} + */ +function clone(element) { + return /** @type {T} */ (HTMLElement.prototype.cloneNode.call(element)); +} + +/** + * This action enhances a `
` element that otherwise would work without JavaScript. + * + * The `submit` function is called upon submission with the given FormData and the `action` that should be triggered. + * If `cancel` is called, the form will not be submitted. + * You can use the abort `controller` to cancel the submission in case another one starts. + * If a function is returned, that function is called with the response from the server. + * If nothing is returned, the fallback will be used. + * + * If this function or its return value isn't set, it emulates the browser-native behaviour, just without the full-page reload. It + * - resets the `` element and refreshes all data in case of a successful submission with no redirect response + * - updates the `form` prop, `page.form` and `page.status` if the action is on the same page as the form + * - navigates to the page the submission lands on — populating that page's `form` prop and `page.status` — on success and failure if that isn't the current page, just as a native form submission would, but with the `?/actionName` param stripped from the destination URL + * - redirects in case of a redirect response + * - renders the nearest error page in case of an unexpected error — the one nearest the action's route, if the action is on a different page + * + * If you provide a custom function with a callback and want to use the default behavior, invoke `update` in your callback. + * It accepts an options object + * - `reset: false` if you don't want the `` values to be reset after a successful submission + * - `refreshAll` to control whether all data is refreshed after submission; it defaults to `true` for successes and `false` for failures + * - `navigate: false` to apply non-redirect results to the current page rather than navigating to `result.location`; redirects are always followed + * @template {Record | undefined} Success + * @template {Record | undefined} Failure + * @param {HTMLFormElement} form_element The form element + * @param {SubmitFunction} submit Submit callback + */ +export function enhance(form_element, submit = noop) { + if (DEV && clone(form_element).method !== 'post') { + throw new Error('use:enhance can only be used on fields with method="POST"'); + } + + /** + * @param {{ + * result: ActionResult; + * reset?: boolean; + * refreshAll?: boolean; + * invalidateAll?: boolean; + * navigate?: boolean; + * }} opts + */ + const fallback_callback = async ({ + result, + reset = true, + refreshAll: should_refresh_all, + invalidateAll: deprecated_invalidate_all, + navigate = true + }) => { + if (DEV && deprecated_invalidate_all !== undefined) { + console.warn( + 'The `update({ invalidateAll })` option has been deprecated in favour of `update({ refreshAll })`' + ); + } + + should_refresh_all ??= deprecated_invalidate_all ?? result.type === 'success'; + + if (result.type === 'success' && reset) { + // We call reset from the prototype to avoid DOM clobbering + HTMLFormElement.prototype.reset.call(form_element); + } + + const destination = + navigate && result.type !== 'redirect' && !is_current_location(result.location) + ? result.location + : undefined; + + if (destination === undefined) { + if (should_refresh_all && result.type !== 'redirect') { + await refreshAll(); + } + + await applyAction(result); + return; + } + + // emulate the browser: navigate to where the submission lands, rendering that + // page with this result + await apply_action_navigation(destination, result, should_refresh_all); + }; + + /** @param {SubmitEvent} event */ + async function handle_submit(event) { + const method = event.submitter?.hasAttribute('formmethod') + ? /** @type {HTMLButtonElement | HTMLInputElement} */ (event.submitter).formMethod + : clone(form_element).method; + if (method !== 'post') return; + + event.preventDefault(); + + const action = new URL( + // We can't do submitter.formAction directly because that property is always set + event.submitter?.hasAttribute('formaction') + ? /** @type {HTMLButtonElement | HTMLInputElement} */ (event.submitter).formAction + : clone(form_element).action + ); + + const enctype = event.submitter?.hasAttribute('formenctype') + ? /** @type {HTMLButtonElement | HTMLInputElement} */ (event.submitter).formEnctype + : clone(form_element).enctype; + + const form_data = new FormData(form_element, event.submitter); + + if (DEV && enctype !== 'multipart/form-data') { + for (const value of form_data.values()) { + if (value instanceof File) { + throw new Error( + 'Your form contains fields, but is missing the necessary `enctype="multipart/form-data"` attribute. This will lead to inconsistent behavior between enhanced and native forms. For more details, see https://github.com/sveltejs/kit/issues/9819.' + ); + } + } + } + + const controller = new AbortController(); + + let cancelled = false; + const cancel = () => (cancelled = true); + + const callback = + (await submit({ + action, + cancel, + controller, + formData: form_data, + formElement: form_element, + submitter: event.submitter + })) ?? fallback_callback; + if (cancelled) return; + + /** @type {ActionResult} */ + let result; + + try { + const headers = new Headers({ + accept: 'application/json', + 'x-sveltekit-action': 'true' + }); + + // do not explicitly set the `Content-Type` header when sending `FormData` + // or else it will interfere with the browser's header setting + // see https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest_API/Using_FormData_Objects#sect4 + if (enctype !== 'multipart/form-data') { + headers.set( + 'Content-Type', + /^(:?application\/x-www-form-urlencoded|text\/plain)$/.test(enctype) + ? enctype + : 'application/x-www-form-urlencoded' + ); + } + + // @ts-expect-error `URLSearchParams(form_data)` is kosher, but typescript doesn't know that + const body = enctype === 'multipart/form-data' ? form_data : new URLSearchParams(form_data); + + const response = await fetch(action, { + method: 'POST', + headers, + cache: 'no-store', + body, + signal: controller.signal + }); + + // detect new deployments from the response header + notify_version(response.headers.get('x-sveltekit-version')); + + result = deserialize(await response.text()); + if (result.type === 'error' || result.type === 'failure') { + result.status = response.status; + } + } catch (error) { + if (/** @type {any} */ (error)?.name === 'AbortError') return; + result = { + type: 'error', + error: await handle_error(error, { + params: {}, + route: { id: null }, + url: new URL(location.href) + }) + }; + } + + await callback({ + action, + formData: form_data, + formElement: form_element, + update: (opts) => + fallback_callback({ + result, + reset: opts?.reset, + refreshAll: opts?.refreshAll, + invalidateAll: opts?.invalidateAll, + navigate: opts?.navigate + }), + // @ts-expect-error generic constraints stuff we don't care about + result + }); + } + + // @ts-expect-error + HTMLFormElement.prototype.addEventListener.call(form_element, 'submit', handle_submit); + + return { + destroy() { + // @ts-expect-error + HTMLFormElement.prototype.removeEventListener.call(form_element, 'submit', handle_submit); + } + }; +} diff --git a/packages/kit/src/runtime/app/forms/index.js b/packages/kit/src/runtime/app/forms/index.js index 631331a34528..438cfec0ce98 100644 --- a/packages/kit/src/runtime/app/forms/index.js +++ b/packages/kit/src/runtime/app/forms/index.js @@ -1,263 +1 @@ -/** @import { ActionResult, SubmitFunction } from './types.js' */ -import { DEV } from 'esm-env'; -import { noop } from '../../../utils/functions.js'; -import { refreshAll } from '../navigation/index.js'; -import { - applyAction, - apply_action_navigation, - handle_error, - is_current_location -} from '../../client/client.js'; -import { notify_version } from '../../client/state.svelte.js'; -import { parse } from '#app/internal/transport'; - -export { applyAction }; - -/** - * Use this function to deserialize the response from a form submission. - * Usage: - * - * ```js - * import { deserialize } from '$app/forms'; - * - * async function handleSubmit(event) { - * const response = await fetch('/form?/action', { - * method: 'POST', - * body: new FormData(event.target) - * }); - * - * const result = deserialize(await response.text()); - * // ... - * } - * ``` - * @template {Record | undefined} Success - * @template {Record | undefined} Failure - * @param {string} result - * @returns {ActionResult} - */ -export function deserialize(result) { - const parsed = JSON.parse(result); - - if (parsed.data) { - parsed.data = parse(parsed.data); - } - - return parsed; -} - -/** - * Shallow clone an element, so that we can access e.g. `form.action` without worrying - * that someone has added an `` (https://github.com/sveltejs/kit/issues/7593) - * @template {HTMLElement} T - * @param {T} element - * @returns {T} - */ -function clone(element) { - return /** @type {T} */ (HTMLElement.prototype.cloneNode.call(element)); -} - -/** - * This action enhances a `` element that otherwise would work without JavaScript. - * - * The `submit` function is called upon submission with the given FormData and the `action` that should be triggered. - * If `cancel` is called, the form will not be submitted. - * You can use the abort `controller` to cancel the submission in case another one starts. - * If a function is returned, that function is called with the response from the server. - * If nothing is returned, the fallback will be used. - * - * If this function or its return value isn't set, it emulates the browser-native behaviour, just without the full-page reload. It - * - resets the `` element and refreshes all data in case of a successful submission with no redirect response - * - updates the `form` prop, `page.form` and `page.status` if the action is on the same page as the form - * - navigates to the page the submission lands on — populating that page's `form` prop and `page.status` — on success and failure if that isn't the current page, just as a native form submission would, but with the `?/actionName` param stripped from the destination URL - * - redirects in case of a redirect response - * - renders the nearest error page in case of an unexpected error — the one nearest the action's route, if the action is on a different page - * - * If you provide a custom function with a callback and want to use the default behavior, invoke `update` in your callback. - * It accepts an options object - * - `reset: false` if you don't want the `` values to be reset after a successful submission - * - `refreshAll` to control whether all data is refreshed after submission; it defaults to `true` for successes and `false` for failures - * - `navigate: false` to apply non-redirect results to the current page rather than navigating to `result.location`; redirects are always followed - * @template {Record | undefined} Success - * @template {Record | undefined} Failure - * @param {HTMLFormElement} form_element The form element - * @param {SubmitFunction} submit Submit callback - */ -export function enhance(form_element, submit = noop) { - if (DEV && clone(form_element).method !== 'post') { - throw new Error('use:enhance can only be used on fields with method="POST"'); - } - - /** - * @param {{ - * result: ActionResult; - * reset?: boolean; - * refreshAll?: boolean; - * invalidateAll?: boolean; - * navigate?: boolean; - * }} opts - */ - const fallback_callback = async ({ - result, - reset = true, - refreshAll: should_refresh_all, - invalidateAll: deprecated_invalidate_all, - navigate = true - }) => { - if (DEV && deprecated_invalidate_all !== undefined) { - console.warn( - 'The `update({ invalidateAll })` option has been deprecated in favour of `update({ refreshAll })`' - ); - } - - should_refresh_all ??= deprecated_invalidate_all ?? result.type === 'success'; - - if (result.type === 'success' && reset) { - // We call reset from the prototype to avoid DOM clobbering - HTMLFormElement.prototype.reset.call(form_element); - } - - const destination = - navigate && result.type !== 'redirect' && !is_current_location(result.location) - ? result.location - : undefined; - - if (destination === undefined) { - if (should_refresh_all && result.type !== 'redirect') { - await refreshAll(); - } - - await applyAction(result); - return; - } - - // emulate the browser: navigate to where the submission lands, rendering that - // page with this result - await apply_action_navigation(destination, result, should_refresh_all); - }; - - /** @param {SubmitEvent} event */ - async function handle_submit(event) { - const method = event.submitter?.hasAttribute('formmethod') - ? /** @type {HTMLButtonElement | HTMLInputElement} */ (event.submitter).formMethod - : clone(form_element).method; - if (method !== 'post') return; - - event.preventDefault(); - - const action = new URL( - // We can't do submitter.formAction directly because that property is always set - event.submitter?.hasAttribute('formaction') - ? /** @type {HTMLButtonElement | HTMLInputElement} */ (event.submitter).formAction - : clone(form_element).action - ); - - const enctype = event.submitter?.hasAttribute('formenctype') - ? /** @type {HTMLButtonElement | HTMLInputElement} */ (event.submitter).formEnctype - : clone(form_element).enctype; - - const form_data = new FormData(form_element, event.submitter); - - if (DEV && enctype !== 'multipart/form-data') { - for (const value of form_data.values()) { - if (value instanceof File) { - throw new Error( - 'Your form contains fields, but is missing the necessary `enctype="multipart/form-data"` attribute. This will lead to inconsistent behavior between enhanced and native forms. For more details, see https://github.com/sveltejs/kit/issues/9819.' - ); - } - } - } - - const controller = new AbortController(); - - let cancelled = false; - const cancel = () => (cancelled = true); - - const callback = - (await submit({ - action, - cancel, - controller, - formData: form_data, - formElement: form_element, - submitter: event.submitter - })) ?? fallback_callback; - if (cancelled) return; - - /** @type {ActionResult} */ - let result; - - try { - const headers = new Headers({ - accept: 'application/json', - 'x-sveltekit-action': 'true' - }); - - // do not explicitly set the `Content-Type` header when sending `FormData` - // or else it will interfere with the browser's header setting - // see https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest_API/Using_FormData_Objects#sect4 - if (enctype !== 'multipart/form-data') { - headers.set( - 'Content-Type', - /^(:?application\/x-www-form-urlencoded|text\/plain)$/.test(enctype) - ? enctype - : 'application/x-www-form-urlencoded' - ); - } - - // @ts-expect-error `URLSearchParams(form_data)` is kosher, but typescript doesn't know that - const body = enctype === 'multipart/form-data' ? form_data : new URLSearchParams(form_data); - - const response = await fetch(action, { - method: 'POST', - headers, - cache: 'no-store', - body, - signal: controller.signal - }); - - // detect new deployments from the response header - notify_version(response.headers.get('x-sveltekit-version')); - - result = deserialize(await response.text()); - if (result.type === 'error' || result.type === 'failure') { - result.status = response.status; - } - } catch (error) { - if (/** @type {any} */ (error)?.name === 'AbortError') return; - result = { - type: 'error', - error: await handle_error(error, { - params: {}, - route: { id: null }, - url: new URL(location.href) - }) - }; - } - - await callback({ - action, - formData: form_data, - formElement: form_element, - update: (opts) => - fallback_callback({ - result, - reset: opts?.reset, - refreshAll: opts?.refreshAll, - invalidateAll: opts?.invalidateAll, - navigate: opts?.navigate - }), - // @ts-expect-error generic constraints stuff we don't care about - result - }); - } - - // @ts-expect-error - HTMLFormElement.prototype.addEventListener.call(form_element, 'submit', handle_submit); - - return { - destroy() { - // @ts-expect-error - HTMLFormElement.prototype.removeEventListener.call(form_element, 'submit', handle_submit); - } - }; -} +export { applyAction, deserialize, enhance } from '#app/forms'; diff --git a/packages/kit/src/runtime/app/forms/public.d.ts b/packages/kit/src/runtime/app/forms/public.d.ts index c9ab684780bb..1a62be53cc2d 100644 --- a/packages/kit/src/runtime/app/forms/public.d.ts +++ b/packages/kit/src/runtime/app/forms/public.d.ts @@ -1,2 +1,2 @@ -export * from './index.js'; +export * from './client.js'; export type { ActionResult, SubmitFunction } from './types.js'; diff --git a/packages/kit/src/runtime/app/forms/server.js b/packages/kit/src/runtime/app/forms/server.js new file mode 100644 index 000000000000..e3c42c1921c8 --- /dev/null +++ b/packages/kit/src/runtime/app/forms/server.js @@ -0,0 +1,5 @@ +import { noop, disallow_on_server } from '../../../utils/functions.js'; + +export const applyAction = disallow_on_server('applyAction'); +export const deserialize = disallow_on_server('deserialize'); +export const enhance = noop; diff --git a/packages/kit/src/runtime/app/navigation/server.js b/packages/kit/src/runtime/app/navigation/server.js index 9fb3ba2e72d1..6a9186725f17 100644 --- a/packages/kit/src/runtime/app/navigation/server.js +++ b/packages/kit/src/runtime/app/navigation/server.js @@ -1,25 +1,15 @@ -import { noop } from '../../../utils/functions.js'; +import { noop, disallow_on_server } from '../../../utils/functions.js'; export const afterNavigate = noop; export const beforeNavigate = noop; -export const disableScrollHandling = disallow('disableScrollHandling', '()'); -export const goto = disallow('goto'); -export const invalidate = disallow('invalidate'); -export const invalidateAll = disallow('invalidateAll', '()'); +export const disableScrollHandling = disallow_on_server('disableScrollHandling', '()'); +export const goto = disallow_on_server('goto'); +export const invalidate = disallow_on_server('invalidate'); +export const invalidateAll = disallow_on_server('invalidateAll', '()'); export const onNavigate = noop; -export const refreshAll = disallow('refreshAll', '()'); -export const preloadCode = disallow('preloadCode'); -export const preloadData = disallow('preloadData'); -export const pushState = disallow('pushState'); -export const replaceState = disallow('replaceState'); +export const refreshAll = disallow_on_server('refreshAll', '()'); +export const preloadCode = disallow_on_server('preloadCode'); +export const preloadData = disallow_on_server('preloadData'); +export const pushState = disallow_on_server('pushState'); +export const replaceState = disallow_on_server('replaceState'); export const snapshot = noop; - -/** - * @param {string} name - * @param {string} [parens] - */ -function disallow(name, parens = '(...)') { - return () => { - throw new Error(`Cannot call \`${name}${parens}\` on the server`); - }; -} diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index 102429807633..719659279097 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -6,7 +6,7 @@ /** @import { CacheEntry } from './remote-functions/cache.svelte.js' */ /** @import { Query } from './remote-functions/query/instance.svelte.js' */ /** @import { LiveQuery } from './remote-functions/query-live/instance.svelte.js' */ -import { BROWSER, DEV } from 'esm-env'; +import { DEV } from 'esm-env'; import { settled, tick, fork, onMount, hydrate, mount } from 'svelte'; import { HttpError, Redirect, SvelteKitError, HandledHttpError } from '@sveltejs/kit/internal'; import { decode_pathname, strip_hash, make_trackable, normalize_path } from '../../utils/url.js'; @@ -112,7 +112,7 @@ init_snapshots(() => (started && !is_navigating && !updating ? current_history_i /** @type {Props} */ let props; -if (DEV && BROWSER) { +if (DEV) { let warned = false; const current_module_url = import.meta.url.split('?')[0]; // remove query params that vite adds to the URL when it is loaded from node_modules @@ -3070,11 +3070,6 @@ async function update_state(intent, state, { replace, persist_state, reset }, ca * @returns {Promise} */ export async function applyAction(result) { - // TODO get rid of this - if (!BROWSER) { - throw new Error('Cannot call applyAction(...) on the server'); - } - if (result.type === 'redirect') { await _goto(result.location, { refreshAll: true }); return; diff --git a/packages/kit/src/utils/functions.js b/packages/kit/src/utils/functions.js index 43b248583996..62b7c3baf995 100644 --- a/packages/kit/src/utils/functions.js +++ b/packages/kit/src/utils/functions.js @@ -16,3 +16,13 @@ export function once(fn) { return (result = fn()); }; } + +/** + * @param {string} name + * @param {string} [parens] + */ +export function disallow_on_server(name, parens = '(...)') { + return () => { + throw new Error(`Cannot call \`${name}${parens}\` on the server`); + }; +} diff --git a/playgrounds/basic/src/env.ts b/playgrounds/basic/src/env.ts new file mode 100644 index 000000000000..80e7c93a5e62 --- /dev/null +++ b/playgrounds/basic/src/env.ts @@ -0,0 +1,5 @@ +import { defineEnvVars } from '@sveltejs/kit/env'; + +export const variables = defineEnvVars({ + MESSAGE: {} +}); From 12a143f6e898c4f0769f1f80aba1e743fe75398e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 13 Aug 2026 21:38:30 -0400 Subject: [PATCH 08/12] remove redundant BROWSER checks --- packages/kit/src/runtime/client/fetcher.js | 10 ++++------ packages/kit/src/runtime/client/snapshots.js | 4 +--- packages/kit/src/runtime/client/utils.js | 4 ++-- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/packages/kit/src/runtime/client/fetcher.js b/packages/kit/src/runtime/client/fetcher.js index 3a138030f1bf..6cc41360ef8d 100644 --- a/packages/kit/src/runtime/client/fetcher.js +++ b/packages/kit/src/runtime/client/fetcher.js @@ -1,12 +1,10 @@ -import { BROWSER, DEV } from 'esm-env'; -import { noop } from '../../utils/functions.js'; +import { DEV } from 'esm-env'; import { hash_request } from '../../utils/hash.js'; import { base64_decode } from '../utils.js'; let loading = 0; -/** @type {typeof fetch} */ -const native_fetch = BROWSER ? window.fetch : /** @type {any} */ (noop); +const native_fetch = window.fetch; export function lock_fetch() { loading += 1; @@ -16,7 +14,7 @@ export function unlock_fetch() { loading -= 1; } -if (DEV && BROWSER) { +if (DEV) { let can_inspect_stack_trace = false; // detect whether async stack traces work @@ -66,7 +64,7 @@ if (DEV && BROWSER) { return native_fetch(input, init); }; -} else if (BROWSER) { +} else { window.fetch = (input, init) => { const method = input instanceof Request ? input.method : init?.method || 'GET'; diff --git a/packages/kit/src/runtime/client/snapshots.js b/packages/kit/src/runtime/client/snapshots.js index 54d9476c4217..d7bad374e7e6 100644 --- a/packages/kit/src/runtime/client/snapshots.js +++ b/packages/kit/src/runtime/client/snapshots.js @@ -1,4 +1,4 @@ -import { BROWSER, DEV } from 'esm-env'; +import { DEV } from 'esm-env'; import { onMount } from 'svelte'; import * as storage from './session-storage.js'; import { NAVIGATION_SNAPSHOT_KEY } from './constants.js'; @@ -105,8 +105,6 @@ function callsite_id(stack) { * @returns {void} */ export function snapshot(options) { - if (!BROWSER) return; - let id = options.id; if (id === undefined) { // restore any lowered third-party limit, else every callsite collapses to one id diff --git a/packages/kit/src/runtime/client/utils.js b/packages/kit/src/runtime/client/utils.js index fc590088ae18..0e280d36505d 100644 --- a/packages/kit/src/runtime/client/utils.js +++ b/packages/kit/src/runtime/client/utils.js @@ -1,7 +1,7 @@ -import { BROWSER, DEV } from 'esm-env'; +import { DEV } from 'esm-env'; import { PRELOAD_PRIORITIES } from './constants.js'; -export const origin = BROWSER ? location.origin : ''; +export const origin = location.origin; /** @param {string | URL} url */ export function resolve_url(url) { From b222c78921c571a7578a131e493b51644370579d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 13 Aug 2026 21:58:50 -0400 Subject: [PATCH 09/12] fix --- packages/kit/src/runtime/app/forms/client.js | 34 +------------------- packages/kit/src/runtime/app/forms/server.js | 3 +- packages/kit/src/runtime/app/forms/shared.js | 34 ++++++++++++++++++++ 3 files changed, 37 insertions(+), 34 deletions(-) create mode 100644 packages/kit/src/runtime/app/forms/shared.js diff --git a/packages/kit/src/runtime/app/forms/client.js b/packages/kit/src/runtime/app/forms/client.js index 631331a34528..5908886ebbf2 100644 --- a/packages/kit/src/runtime/app/forms/client.js +++ b/packages/kit/src/runtime/app/forms/client.js @@ -9,41 +9,9 @@ import { is_current_location } from '../../client/client.js'; import { notify_version } from '../../client/state.svelte.js'; -import { parse } from '#app/internal/transport'; export { applyAction }; - -/** - * Use this function to deserialize the response from a form submission. - * Usage: - * - * ```js - * import { deserialize } from '$app/forms'; - * - * async function handleSubmit(event) { - * const response = await fetch('/form?/action', { - * method: 'POST', - * body: new FormData(event.target) - * }); - * - * const result = deserialize(await response.text()); - * // ... - * } - * ``` - * @template {Record | undefined} Success - * @template {Record | undefined} Failure - * @param {string} result - * @returns {ActionResult} - */ -export function deserialize(result) { - const parsed = JSON.parse(result); - - if (parsed.data) { - parsed.data = parse(parsed.data); - } - - return parsed; -} +export { deserialize } from './shared.js'; /** * Shallow clone an element, so that we can access e.g. `form.action` without worrying diff --git a/packages/kit/src/runtime/app/forms/server.js b/packages/kit/src/runtime/app/forms/server.js index e3c42c1921c8..4f4b1513f0f6 100644 --- a/packages/kit/src/runtime/app/forms/server.js +++ b/packages/kit/src/runtime/app/forms/server.js @@ -1,5 +1,6 @@ import { noop, disallow_on_server } from '../../../utils/functions.js'; +export { deserialize } from './shared.js'; + export const applyAction = disallow_on_server('applyAction'); -export const deserialize = disallow_on_server('deserialize'); export const enhance = noop; diff --git a/packages/kit/src/runtime/app/forms/shared.js b/packages/kit/src/runtime/app/forms/shared.js new file mode 100644 index 000000000000..a598b357b16e --- /dev/null +++ b/packages/kit/src/runtime/app/forms/shared.js @@ -0,0 +1,34 @@ +/** @import { ActionResult } from './types.js' */ +import { parse } from '#app/internal/transport'; + +/** + * Use this function to deserialize the response from a form submission. + * Usage: + * + * ```js + * import { deserialize } from '$app/forms'; + * + * async function handleSubmit(event) { + * const response = await fetch('/form?/action', { + * method: 'POST', + * body: new FormData(event.target) + * }); + * + * const result = deserialize(await response.text()); + * // ... + * } + * ``` + * @template {Record | undefined} Success + * @template {Record | undefined} Failure + * @param {string} result + * @returns {ActionResult} + */ +export function deserialize(result) { + const parsed = JSON.parse(result); + + if (parsed.data) { + parsed.data = parse(parsed.data); + } + + return parsed; +} From da44db29966785db64f252a438a91fd413737732 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 13 Aug 2026 22:06:38 -0400 Subject: [PATCH 10/12] doh --- packages/kit/src/runtime/app/forms/client.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/kit/src/runtime/app/forms/client.js b/packages/kit/src/runtime/app/forms/client.js index 5908886ebbf2..b8e7b14f110c 100644 --- a/packages/kit/src/runtime/app/forms/client.js +++ b/packages/kit/src/runtime/app/forms/client.js @@ -9,9 +9,9 @@ import { is_current_location } from '../../client/client.js'; import { notify_version } from '../../client/state.svelte.js'; +import { deserialize } from './shared.js'; -export { applyAction }; -export { deserialize } from './shared.js'; +export { applyAction, deserialize }; /** * Shallow clone an element, so that we can access e.g. `form.action` without worrying From c44ea35fce4b94a7f1c93669e124c357f35ada7d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 13 Aug 2026 22:08:45 -0400 Subject: [PATCH 11/12] didnt mean to commit this --- playgrounds/basic/src/env.ts | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 playgrounds/basic/src/env.ts diff --git a/playgrounds/basic/src/env.ts b/playgrounds/basic/src/env.ts deleted file mode 100644 index 80e7c93a5e62..000000000000 --- a/playgrounds/basic/src/env.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { defineEnvVars } from '@sveltejs/kit/env'; - -export const variables = defineEnvVars({ - MESSAGE: {} -}); From 82bb5794a9ecf30738eb72e8949a9ec7bffc15c9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 14 Aug 2026 12:50:35 +0000 Subject: [PATCH 12/12] chore: autofix lint --- packages/kit/types/index.d.ts | 38 +++++++++++++++++------------------ 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 2443df9631a8..ae5e59c1868d 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -2308,25 +2308,6 @@ declare module '$app/env' { } declare module '$app/forms' { - /** - * Use this function to deserialize the response from a form submission. - * Usage: - * - * ```js - * import { deserialize } from '$app/forms'; - * - * async function handleSubmit(event) { - * const response = await fetch('/form?/action', { - * method: 'POST', - * body: new FormData(event.target) - * }); - * - * const result = deserialize(await response.text()); - * // ... - * } - * ``` - * */ - export function deserialize | undefined, Failure extends Record | undefined>(result: string): ActionResult; /** * This action enhances a `` element that otherwise would work without JavaScript. * @@ -2414,6 +2395,25 @@ declare module '$app/forms' { * the redirect location. * */ export function applyAction | undefined, Failure extends Record | undefined>(result: ActionResult): Promise; + /** + * Use this function to deserialize the response from a form submission. + * Usage: + * + * ```js + * import { deserialize } from '$app/forms'; + * + * async function handleSubmit(event) { + * const response = await fetch('/form?/action', { + * method: 'POST', + * body: new FormData(event.target) + * }); + * + * const result = deserialize(await response.text()); + * // ... + * } + * ``` + * */ + export function deserialize | undefined, Failure extends Record | undefined>(result: string): ActionResult; type MaybePromise = T | Promise; export {};