diff --git a/documentation/docs/60-appendix/10-faq.md b/documentation/docs/60-appendix/10-faq.md index 5caa0f5b7b9f..fdbb85f5ba37 100644 --- a/documentation/docs/60-appendix/10-faq.md +++ b/documentation/docs/60-appendix/10-faq.md @@ -183,6 +183,12 @@ export default config; See [Vite's `configureServer` docs](https://vitejs.dev/guide/api-plugin.html#configureserver) for more details including how to control ordering. +## Why is SvelteKit being loaded twice? + +Sometimes, due to incorrect configuration, SvelteKit and other libraries can end up being loaded twice. The most common cause for this is package misconfiguration -- either when a package depends on a specific version of `@sveltejs/kit` rather than listing it as a `peerDependency` or when a package is using an incorrect combination of `main`/`module`/`exports` configuration that's causing your code to load both an ESM and CJS copy of SvelteKit. + +SvelteKit will try to figure out when this is happening and warn you by logging to the console during build. If you see one of these logs, try using `pnpm why` or `npm ls` to determine which package is loading SvelteKit, then file an issue with that package. + ## How do I use Yarn? ### Does it work with Yarn 2? diff --git a/packages/kit/package.json b/packages/kit/package.json index a9a782332ca2..c4a5cf4dfe94 100644 --- a/packages/kit/package.json +++ b/packages/kit/package.json @@ -29,8 +29,7 @@ "mrmime": "^2.0.0", "sade": "^1.8.1", "set-cookie-parser": "^2.6.0", - "sirv": "^3.0.0", - "vitefu": "^1.0.6" + "sirv": "^3.0.0" }, "devDependencies": { "@playwright/test": "catalog:", diff --git a/packages/kit/src/exports/hooks/index.js b/packages/kit/src/exports/hooks/index.js index 98e1e53a5ac2..336129dfbf37 100644 --- a/packages/kit/src/exports/hooks/index.js +++ b/packages/kit/src/exports/hooks/index.js @@ -1 +1,3 @@ +import '../multi-load-test.js'; + export { sequence } from './sequence.js'; diff --git a/packages/kit/src/exports/index.js b/packages/kit/src/exports/index.js index e587ede88581..08dc264bd118 100644 --- a/packages/kit/src/exports/index.js +++ b/packages/kit/src/exports/index.js @@ -8,6 +8,7 @@ import { strip_data_suffix, strip_resolution_suffix } from '../runtime/pathname.js'; +import './multi-load-test.js'; export { VERSION } from '../version.js'; @@ -85,8 +86,14 @@ export function error(status, body) { * @return {e is (HttpError & { status: T extends undefined ? never : T })} */ export function isHttpError(e, status) { - if (!(e instanceof HttpError)) return false; - return !status || e.status === status; + return ( + typeof e === 'object' && + e !== null && + Object.hasOwn(e, '_tag') && + Object.hasOwn(e, 'status') && + /** @type {{ _tag: string; }} */ (e)._tag === 'SvelteKitHttpError' && + (!status || /** @type {{ status: number }} */ (e).status === status) + ); } /** @@ -124,7 +131,12 @@ export function redirect(status, location) { * @return {e is Redirect} */ export function isRedirect(e) { - return e instanceof Redirect; + return ( + typeof e === 'object' && + e !== null && + Object.hasOwn(e, '_tag') && + /** @type {{ _tag: string }} */ (e)._tag === 'SvelteKitRedirect' + ); } /** @@ -213,7 +225,12 @@ export function fail(status, data) { * @return {e is import('./public.js').ActionFailure} */ export function isActionFailure(e) { - return e instanceof ActionFailure; + return ( + typeof e === 'object' && + e !== null && + Object.hasOwn(e, '_tag') && + /** @type {{ _tag: string }} */ (e)._tag === 'SvelteKitActionFailure' + ); } /** diff --git a/packages/kit/src/exports/multi-load-test.js b/packages/kit/src/exports/multi-load-test.js new file mode 100644 index 000000000000..de50c8ad3129 --- /dev/null +++ b/packages/kit/src/exports/multi-load-test.js @@ -0,0 +1,9 @@ +const key = '__sveltekit_module_loaded'; +// @ts-ignore +if (globalThis[key]) { + console.warn( + 'SvelteKit has been loaded twice. This is likely a misconfiguration, and could cause subtle bugs. For more information, visit https://svelte.dev/docs/kit/faq#Why-is-SvelteKit-being-loaded-twice' + ); +} +// @ts-ignore +globalThis[key] = true; diff --git a/packages/kit/src/exports/node/index.js b/packages/kit/src/exports/node/index.js index a69b7ae6d906..bf1ca0dc5015 100644 --- a/packages/kit/src/exports/node/index.js +++ b/packages/kit/src/exports/node/index.js @@ -2,6 +2,7 @@ import { createReadStream } from 'node:fs'; import { Readable } from 'node:stream'; import * as set_cookie_parser from 'set-cookie-parser'; import { SvelteKitError } from '../../runtime/control.js'; +import '../multi-load-test.js'; /** * @param {import('http').IncomingMessage} req diff --git a/packages/kit/src/exports/node/polyfills.js b/packages/kit/src/exports/node/polyfills.js index 347c68cea2e9..555378784ed0 100644 --- a/packages/kit/src/exports/node/polyfills.js +++ b/packages/kit/src/exports/node/polyfills.js @@ -1,5 +1,6 @@ import buffer from 'node:buffer'; import { webcrypto as crypto } from 'node:crypto'; +import '../multi-load-test.js'; // `buffer.File` was added in Node 18.13.0 while the `File` global was added in Node 20.0.0 const File = /** @type {import('node:buffer') & { File?: File}} */ (buffer).File; diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index 13a9776005cc..6ebd9a712748 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -36,7 +36,7 @@ import { } from './module_ids.js'; import { import_peer } from '../../utils/import.js'; import { compact } from '../../utils/array.js'; -import { crawlFrameworkPkgs } from 'vitefu'; +import '../multi-load-test.js'; const cwd = process.cwd(); @@ -229,7 +229,7 @@ async function kit({ svelte_config }) { * Build the SvelteKit-provided Vite config to be merged with the user's vite.config.js file. * @see https://vitejs.dev/guide/api-plugin.html#config */ - async config(config, config_env) { + config(config, config_env) { initial_config = config; vite_config_env = config_env; is_build = config_env.command === 'build'; @@ -252,20 +252,6 @@ async function kit({ svelte_config }) { const generated = path.posix.join(kit.outDir, 'generated'); - const packages_depending_on_svelte_kit = ( - await crawlFrameworkPkgs({ - root: cwd, - isBuild: is_build, - viteUserConfig: config, - isSemiFrameworkPkgByJson: (pkg_json) => { - return ( - !!pkg_json.dependencies?.['@sveltejs/kit'] || - !!pkg_json.peerDependencies?.['@sveltejs/kit'] - ); - } - }) - ).ssr.noExternal; - // dev and preview config can be shared /** @type {import('vite').UserConfig} */ const new_config = { @@ -322,11 +308,9 @@ async function kit({ svelte_config }) { // when it is detected to keep our virtual modules working. // See https://github.com/sveltejs/kit/pull/9172 // and https://vitest.dev/config/#deps-registernodeloader - '@sveltejs/kit', - // We need to bundle any packages depending on @sveltejs/kit so that - // everyone uses the same instances of classes such as `Redirect` - // which we use in `instanceof` checks - ...packages_depending_on_svelte_kit + '@sveltejs/kit' + // Don't bundle packages that depend on `@sveltejs/kit` because they + // may include CJS syntax/dependencies ] } }; diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index 4b8eb5831e4a..627c9e3cefdd 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -35,12 +35,13 @@ import { } from './constants.js'; import { validate_page_exports } from '../../utils/exports.js'; import { compact } from '../../utils/array.js'; -import { HttpError, Redirect, SvelteKitError } from '../control.js'; +import { HttpError, SvelteKitError } from '../control.js'; import { INVALIDATED_PARAM, TRAILING_SLASH_PARAM, validate_depends } from '../shared.js'; import { get_message, get_status } from '../../utils/error.js'; import { writable } from 'svelte/store'; import { page, update, navigating } from './state.svelte.js'; import { add_data_suffix, add_resolution_suffix } from '../pathname.js'; +import { isRedirect } from '../../exports/index.js'; export { load_css }; @@ -1049,7 +1050,7 @@ async function load_route({ id, invalidating, url, params, route, preload }) { try { branch.push(await branch_promises[i]); } catch (err) { - if (err instanceof Redirect) { + if (isRedirect(err)) { return { type: 'redirect', location: err.location @@ -1219,7 +1220,7 @@ async function load_root_error_page({ status, error, url, route }) { route: null }); } catch (error) { - if (error instanceof Redirect) { + if (isRedirect(error)) { return _goto(new URL(error.location, location.href), {}, 0); } @@ -2641,7 +2642,7 @@ async function _hydrate( route: parsed_route ?? null }); } catch (error) { - if (error instanceof Redirect) { + if (isRedirect(error)) { // this is a real edge case — `load` would need to return // a redirect but only in the browser await native_navigation(new URL(error.location, location.href)); diff --git a/packages/kit/src/runtime/control.js b/packages/kit/src/runtime/control.js index aa0b93f8965b..6fbf57f961e1 100644 --- a/packages/kit/src/runtime/control.js +++ b/packages/kit/src/runtime/control.js @@ -1,4 +1,8 @@ export class HttpError { + /** @private */ + // @ts-ignore this property is checked by the `isHttpError` helper + _tag = 'SvelteKitHttpError'; + /** * @param {number} status * @param {{message: string} extends App.Error ? (App.Error | string | undefined) : App.Error} body @@ -20,6 +24,10 @@ export class HttpError { } export class Redirect { + /** @private */ + // @ts-ignore this property is checked by the `isRedirect` helper + _tag = 'SvelteKitRedirect'; + /** * @param {300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308} status * @param {string} location @@ -52,6 +60,10 @@ export class SvelteKitError extends Error { * @template {Record | undefined} [T=undefined] */ export class ActionFailure { + /** @private */ + // @ts-ignore this property is checked by the `isActionFailure` helper + _tag = 'SvelteKitActionFailure'; + /** * @param {number} status * @param {T} data diff --git a/packages/kit/src/runtime/server/data/index.js b/packages/kit/src/runtime/server/data/index.js index 46150a987ae9..aad8f996772f 100644 --- a/packages/kit/src/runtime/server/data/index.js +++ b/packages/kit/src/runtime/server/data/index.js @@ -1,10 +1,11 @@ -import { HttpError, SvelteKitError, Redirect } from '../../control.js'; +/** @import { Redirect } from '../../control.js'; */ +import { HttpError, SvelteKitError } from '../../control.js'; import { normalize_error } from '../../../utils/error.js'; import { once } from '../../../utils/functions.js'; import { load_server_data } from '../page/load_data.js'; import { clarify_devalue_error, handle_error_and_jsonify, serialize_uses } from '../utils.js'; import { normalize_path } from '../../../utils/url.js'; -import { text } from '../../../exports/index.js'; +import { isRedirect, text } from '../../../exports/index.js'; import * as devalue from 'devalue'; import { create_async_iterator } from '../../../utils/streaming.js'; @@ -99,7 +100,7 @@ export async function render_data( const nodes = await Promise.all( promises.map((p, i) => p.catch(async (error) => { - if (error instanceof Redirect) { + if (isRedirect(error)) { throw error; } @@ -150,7 +151,7 @@ export async function render_data( } catch (e) { const error = normalize_error(e); - if (error instanceof Redirect) { + if (isRedirect(error)) { return redirect_json_response(error); } else { return json_response(await handle_error_and_jsonify(event, options, error), 500); diff --git a/packages/kit/src/runtime/server/endpoint.js b/packages/kit/src/runtime/server/endpoint.js index d480d344036b..5b37189cccc9 100644 --- a/packages/kit/src/runtime/server/endpoint.js +++ b/packages/kit/src/runtime/server/endpoint.js @@ -1,7 +1,7 @@ import { ENDPOINT_METHODS, PAGE_METHODS } from '../../constants.js'; +import { isRedirect } from '../../exports/index.js'; import { negotiate } from '../../utils/http.js'; import { with_event } from '../app/server/event.js'; -import { Redirect } from '../control.js'; import { method_not_allowed } from './utils.js'; /** @@ -77,7 +77,7 @@ export async function render_endpoint(event, mod, state) { return response; } catch (e) { - if (e instanceof Redirect) { + if (isRedirect(e)) { return new Response(undefined, { status: e.status, headers: { location: e.location } diff --git a/packages/kit/src/runtime/server/page/actions.js b/packages/kit/src/runtime/server/page/actions.js index e7703359234e..777e3ad35e89 100644 --- a/packages/kit/src/runtime/server/page/actions.js +++ b/packages/kit/src/runtime/server/page/actions.js @@ -1,9 +1,10 @@ +/** @import { HttpError } from '../../control.js'; */ import * as devalue from 'devalue'; import { DEV } from 'esm-env'; -import { json } from '../../../exports/index.js'; +import { isActionFailure, isRedirect, json } from '../../../exports/index.js'; import { get_status, normalize_error } from '../../../utils/error.js'; import { is_form_content_type, negotiate } from '../../../utils/http.js'; -import { HttpError, Redirect, ActionFailure, SvelteKitError } from '../../control.js'; +import { SvelteKitError } from '../../control.js'; import { handle_error_and_jsonify } from '../utils.js'; import { with_event } from '../../app/server/event.js'; @@ -53,11 +54,7 @@ export async function handle_action_json_request(event, options, server) { try { const data = await call_action(event, actions); - if (__SVELTEKIT_DEV__) { - validate_action_return(data); - } - - if (data instanceof ActionFailure) { + if (isActionFailure(data)) { return action_json({ type: 'failure', status: data.status, @@ -85,7 +82,7 @@ export async function handle_action_json_request(event, options, server) { } catch (e) { const err = normalize_error(e); - if (err instanceof Redirect) { + if (isRedirect(err)) { return action_json_redirect(err); } @@ -105,9 +102,7 @@ export async function handle_action_json_request(event, options, server) { * @param {HttpError | Error} error */ function check_incorrect_fail_use(error) { - return error instanceof ActionFailure - ? new Error('Cannot "throw fail()". Use "return fail()"') - : error; + return isActionFailure(error) ? new Error('Cannot "throw fail()". Use "return fail()"') : error; } /** @@ -166,11 +161,7 @@ export async function handle_action_request(event, server) { try { const data = await call_action(event, actions); - if (__SVELTEKIT_DEV__) { - validate_action_return(data); - } - - if (data instanceof ActionFailure) { + if (isActionFailure(data)) { return { type: 'failure', status: data.status, @@ -187,7 +178,7 @@ export async function handle_action_request(event, server) { } catch (e) { const err = normalize_error(e); - if (err instanceof Redirect) { + if (isRedirect(err)) { return { type: 'redirect', status: err.status, @@ -250,17 +241,6 @@ async function call_action(event, actions) { return with_event(event, () => action(event)); } -/** @param {any} data */ -function validate_action_return(data) { - if (data instanceof Redirect) { - throw new Error('Cannot `return redirect(...)` — use `redirect(...)` instead'); - } - - if (data instanceof HttpError) { - throw new Error('Cannot `return error(...)` — use `error(...)` or `return fail(...)` instead'); - } -} - /** * Try to `devalue.uneval` the data object, and if it fails, return a proper Error with context * @param {any} data diff --git a/packages/kit/src/runtime/server/page/index.js b/packages/kit/src/runtime/server/page/index.js index e7b462a74dda..ba59df7e2bd0 100644 --- a/packages/kit/src/runtime/server/page/index.js +++ b/packages/kit/src/runtime/server/page/index.js @@ -1,8 +1,7 @@ -import { text } from '../../../exports/index.js'; +import { isRedirect, text } from '../../../exports/index.js'; import { compact } from '../../../utils/array.js'; import { get_status, normalize_error } from '../../../utils/error.js'; import { add_data_suffix } from '../../pathname.js'; -import { Redirect } from '../../control.js'; import { redirect_response, static_error_page, handle_error_and_jsonify } from '../utils.js'; import { handle_action_json_request, @@ -219,7 +218,7 @@ export async function render_page(event, page, options, manifest, state, nodes, } catch (e) { const err = normalize_error(e); - if (err instanceof Redirect) { + if (isRedirect(err)) { if (state.prerendering && should_prerender_data) { const body = JSON.stringify({ type: 'redirect', diff --git a/packages/kit/src/runtime/server/page/respond_with_error.js b/packages/kit/src/runtime/server/page/respond_with_error.js index f29e91329183..6a734020cdf1 100644 --- a/packages/kit/src/runtime/server/page/respond_with_error.js +++ b/packages/kit/src/runtime/server/page/respond_with_error.js @@ -1,9 +1,9 @@ import { render_response } from './render.js'; import { load_data, load_server_data } from './load_data.js'; import { handle_error_and_jsonify, static_error_page, redirect_response } from '../utils.js'; -import { Redirect } from '../../control.js'; import { get_status } from '../../../utils/error.js'; import { PageNodes } from '../../../utils/page_nodes.js'; +import { isRedirect } from '../../../exports/index.js'; /** * @typedef {import('./types.js').Loaded} Loaded @@ -101,7 +101,7 @@ export async function respond_with_error({ } catch (e) { // Edge case: If route is a 404 and the user redirects to somewhere from the root layout, // we end up here. - if (e instanceof Redirect) { + if (isRedirect(e)) { return redirect_response(e.status, e.location); } diff --git a/packages/kit/src/runtime/server/respond.js b/packages/kit/src/runtime/server/respond.js index 783c7d0ee65a..42f92f520c97 100644 --- a/packages/kit/src/runtime/server/respond.js +++ b/packages/kit/src/runtime/server/respond.js @@ -19,7 +19,7 @@ import { create_fetch } from './fetch.js'; import { PageNodes } from '../../utils/page_nodes.js'; import { HttpError, Redirect, SvelteKitError } from '../control.js'; import { validate_server_exports } from '../../utils/exports.js'; -import { json, text } from '../../exports/index.js'; +import { isRedirect, json, text } from '../../exports/index.js'; import { action_json_redirect, is_action_json_request } from './page/actions.js'; import { INVALIDATED_PARAM, TRAILING_SLASH_PARAM } from '../shared.js'; import { get_public_env } from './env_module.js'; @@ -434,7 +434,7 @@ export async function respond(request, options, manifest, state) { return response; } catch (e) { - if (e instanceof Redirect) { + if (isRedirect(e)) { const response = is_data_request ? redirect_json_response(e) : route?.page && is_action_json_request(event) diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 74d438a6f5cc..911a787d60ae 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -2042,6 +2042,8 @@ declare module '@sveltejs/kit' { constructor(status: number, body: { message: string; } extends App.Error ? (App.Error | string | undefined) : App.Error); + + private _tag; status: number; body: App.Error; toString(): string; @@ -2049,6 +2051,8 @@ declare module '@sveltejs/kit' { class Redirect_1 { constructor(status: 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308, location: string); + + private _tag; status: 301 | 302 | 303 | 307 | 308 | 300 | 304 | 305 | 306; location: string; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 484df1fd429e..a32d59ba5ce3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -317,7 +317,7 @@ importers: dependencies: '@sveltejs/kit': specifier: ^1.0.0 || ^2.0.0 - version: link:../kit + version: 2.22.0(@sveltejs/vite-plugin-svelte@6.0.0-next.0(svelte@5.23.1)(vite@6.3.5(@types/node@18.19.50)(lightningcss@1.30.1)))(svelte@5.23.1)(vite@6.3.5(@types/node@18.19.50)(lightningcss@1.30.1)) devDependencies: typescript: specifier: ^5.3.3 @@ -406,9 +406,6 @@ importers: sirv: specifier: ^3.0.0 version: 3.0.0 - vitefu: - specifier: ^1.0.6 - version: 1.0.6(vite@6.3.5(@types/node@18.19.50)(lightningcss@1.30.1)) devDependencies: '@playwright/test': specifier: 'catalog:' @@ -2202,6 +2199,15 @@ packages: typescript: '>= 5' typescript-eslint: '>= 7.5' + '@sveltejs/kit@2.22.0': + resolution: {integrity: sha512-DJm0UxVgzXq+1MUfiJK4Ridk7oIQsIets6JwHiEl97sI6nXScfXe+BeqNhzB7jQIVBb3BM51U4hNk8qQxRXBAA==} + engines: {node: '>=18.13'} + hasBin: true + peerDependencies: + '@sveltejs/vite-plugin-svelte': ^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 + svelte: ^4.0.0 || ^5.0.0-next.0 + vite: ^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 + '@sveltejs/vite-plugin-svelte-inspector@5.0.0-next.0': resolution: {integrity: sha512-G++kR34xZSd3cT6VVOB781Pa2KOS756/ZKK7urSyXmrhK/D/mPiUvjZwWKNVTDOXkwrvVt/Y3cLecbR6Qm66Kw==} engines: {node: ^20.19 || ^22.12 || >=24} @@ -5467,6 +5473,25 @@ snapshots: typescript: 5.8.3 typescript-eslint: 8.26.0(eslint@9.6.0)(typescript@5.8.3) + '@sveltejs/kit@2.22.0(@sveltejs/vite-plugin-svelte@6.0.0-next.0(svelte@5.23.1)(vite@6.3.5(@types/node@18.19.50)(lightningcss@1.30.1)))(svelte@5.23.1)(vite@6.3.5(@types/node@18.19.50)(lightningcss@1.30.1))': + dependencies: + '@sveltejs/acorn-typescript': 1.0.5(acorn@8.14.1) + '@sveltejs/vite-plugin-svelte': 6.0.0-next.0(svelte@5.23.1)(vite@6.3.5(@types/node@18.19.50)(lightningcss@1.30.1)) + '@types/cookie': 0.6.0 + acorn: 8.14.1 + cookie: 0.6.0 + devalue: 5.1.0 + esm-env: 1.2.2 + kleur: 4.1.5 + magic-string: 0.30.17 + mrmime: 2.0.0 + sade: 1.8.1 + set-cookie-parser: 2.6.0 + sirv: 3.0.0 + svelte: 5.23.1 + vite: 6.3.5(@types/node@18.19.50)(lightningcss@1.30.1) + vitefu: 1.0.6(vite@6.3.5(@types/node@18.19.50)(lightningcss@1.30.1)) + '@sveltejs/vite-plugin-svelte-inspector@5.0.0-next.0(@sveltejs/vite-plugin-svelte@6.0.0-next.0(svelte@5.23.1)(vite@6.3.5(@types/node@18.19.50)(lightningcss@1.30.1)))(svelte@5.23.1)(vite@6.3.5(@types/node@18.19.50)(lightningcss@1.30.1))': dependencies: '@sveltejs/vite-plugin-svelte': 6.0.0-next.0(svelte@5.23.1)(vite@6.3.5(@types/node@18.19.50)(lightningcss@1.30.1))