1/**
2 * Support for projects with their own `vite.config.ts`.
3 *
4 * A normal SvelteKit v3 project carries all of its configuration in the Vite
5 * config file: `svelte.config.js` is gone (kit's own `plugin_root` throws if
6 * one exists on disk), and kit options — adapter, aliases, preprocess,
7 * prerender entries — are passed directly to the `sveltekit(...)` plugin call
8 * inside `vite.config.ts`.
9 *
10 * This package must honor that file natively (the user's Vite plugins, kit
11 * options, and Vite settings all apply) while still injecting the deploy
12 * target's adapter — WITHOUT constructing a second `sveltekit()` plugin
13 * instance next to the user's.
14 *
15 * Mechanism: `sveltekit(config)` validates the config eagerly and exposes it
16 * as `api.options` on the `vite-plugin-sveltekit-setup` plugin, but only
17 * *processes* it (deriving the `kit` config every later hook closes over)
18 * inside that plugin's `config: { order: "pre" }` hook. Vite runs config
19 * hooks in hook-`order` groups, and within a group in `enforce`-sorted plugin
20 * order — so an `enforce: "pre"` inline plugin whose config hook is also
21 * `order: "pre"` runs BEFORE kit's setup hook (inline plugins append after
22 * file plugins, and the file's only other `pre`-enforced kit plugin,
23 * `vite-plugin-sveltekit-resolve-svelte-config`, merely asserts no
24 * `svelte.config.js` exists). Mutating `api.options` at that point is
25 * therefore exactly as authoritative as having passed the options to
26 * `sveltekit(...)` directly.
27 *
28 * The injected plugin:
29 *
30 * 1. flattens the config's plugin option (awaiting the promises `sveltekit()`
31 * returns) and finds `vite-plugin-sveltekit-setup`;
32 * 2. fails with a descriptive error when it is absent (a `vite.config.ts`
33 * without `sveltekit()` is not a SvelteKit project — we cannot add the
34 * plugin from a config hook, Vite ignores plugins injected there);
35 * 3. merges the integration-level kit options (`SvelteKitOptions.kit`) over
36 * the user's — integration options win, mirroring Vite's inline-over-file
37 * precedence;
38 * 4. replaces the adapter with the deploy target's, warning when the user
39 * had declared their own: the adapter is the deploy-packaging seam (its
40 * output is what the target's finishing pass re-bundles and what alchemy
41 * uploads), so a foreign adapter's output would simply never be deployed.
42 * The user's file itself is untouched — running plain `vite build`
43 * outside this integration still uses their adapter.
44 */
45import type { Adapter } from "@sveltejs/kit";
46import type * as ViteModule from "vite";
47
48/** The kit plugin that exposes the validated svelte config as `api.options`. */
49export const SVELTEKIT_SETUP_PLUGIN_NAME = "vite-plugin-sveltekit-setup";
50
51/** The name of the injected adapter/config plugin. */
52export const CONFIG_PLUGIN_NAME = "distilled-sveltekit-config";
53
54/**
55 * The Vite config file names this package probes for (mirrors Vite's
56 * `DEFAULT_CONFIG_FILES`) to decide between honoring a user config natively
57 * and the fully-programmatic fallback.
58 */
59export const DEFAULT_VITE_CONFIG_FILES: ReadonlyArray<string> = [
60 "vite.config.js",
61 "vite.config.mjs",
62 "vite.config.ts",
63 "vite.config.cjs",
64 "vite.config.mts",
65 "vite.config.cts",
66];
67
68/** The shape of kit's setup-plugin `api` (structural — kit doesn't export it). */
69interface SetupPluginApi {
70 readonly options?: Record<string, unknown>;
71}
72
73/**
74 * Flatten Vite's `PluginOption` tree (nested arrays, promises, falsy holes)
75 * into a plain plugin list — the same normalization Vite itself performs
76 * before running config hooks.
77 */
78export const flattenPluginOption = async (
79 option: ViteModule.PluginOption,
80): Promise<Array<ViteModule.Plugin>> => {
81 const resolved = await option;
82 if (resolved === null || resolved === undefined || resolved === false) {
83 return [];
84 }
85 if (Array.isArray(resolved)) {
86 const nested = await Promise.all(resolved.map(flattenPluginOption));
87 return nested.flat();
88 }
89 return [resolved as ViteModule.Plugin];
90};
91
92/**
93 * The top-level svelte options `sveltekit(config)` consumes at construction
94 * time (before any Vite hook runs). They cannot be applied by mutating
95 * `api.options` afterwards — `vite-plugin-svelte` was already instantiated
96 * with them — so the injector warns instead of silently dropping them.
97 */
98const CONSTRUCTION_TIME_KEYS = [
99 "extensions",
100 "compilerOptions",
101 "vitePlugin",
102 "preprocess",
103];
104
105const isPlainObject = (value: unknown): value is Record<string, unknown> =>
106 value !== null && typeof value === "object" && !Array.isArray(value);
107
108/**
109 * Merge integration-level kit overrides over the user's validated kit config
110 * (two levels deep: nested plain objects merge key-by-key so validated
111 * defaults survive, everything else replaces).
112 */
113export const mergeKitOptions = (
114 target: Record<string, unknown>,
115 overrides: Record<string, unknown>,
116): void => {
117 for (const [key, value] of Object.entries(overrides)) {
118 if (value === undefined) {
119 continue;
120 }
121 const existing = target[key];
122 if (isPlainObject(existing) && isPlainObject(value)) {
123 target[key] = { ...existing, ...value };
124 } else {
125 target[key] = value;
126 }
127 }
128};
129
130export interface SvelteKitConfigPluginOptions {
131 /** The deploy target's adapter — always wins over a user-declared adapter. */
132 readonly adapter: Adapter;
133 /**
134 * Integration-level kit options (`SvelteKitOptions.kit`) merged over the
135 * user's `sveltekit(...)` options (integration wins).
136 */
137 readonly kit?: Record<string, unknown> | undefined;
138 /** Warning sink. @default console.warn */
139 readonly warn?: ((message: string) => void) | undefined;
140}
141
142/**
143 * The inline Vite plugin injected when the project has its own
144 * `vite.config.ts`: locates the user's `sveltekit()` plugin and installs the
145 * deploy target's adapter (plus integration-level kit options) into its
146 * config before kit processes it. See the module doc for why the ordering is
147 * sound.
148 */
149export const makeSvelteKitConfigPlugin = (
150 options: SvelteKitConfigPluginOptions,
151): ViteModule.Plugin => {
152 const warn =
153 options.warn ??
154 ((message: string) => console.warn(`[${CONFIG_PLUGIN_NAME}] ${message}`));
155 return {
156 name: CONFIG_PLUGIN_NAME,
157 enforce: "pre",
158 config: {
159 order: "pre",
160 handler: async (config) => {
161 const plugins = await flattenPluginOption(config.plugins);
162 const setup = plugins.find(
163 (plugin) => plugin.name === SVELTEKIT_SETUP_PLUGIN_NAME,
164 );
165 if (setup === undefined) {
166 throw new Error(
167 "The project's Vite config does not register the SvelteKit plugin. " +
168 "Add `sveltekit()` (from `@sveltejs/kit/vite`) to the `plugins` array of " +
169 "your Vite config file, or remove the config file to let the integration " +
170 "configure Vite programmatically.",
171 );
172 }
173 const svelteConfig = (setup.api as SetupPluginApi | undefined)?.options;
174 const nestedKit = svelteConfig?.["kit"];
175 const kit = isPlainObject(nestedKit) ? nestedKit : svelteConfig;
176
177 if (!isPlainObject(kit)) {
178 throw new Error(
179 `The "${SVELTEKIT_SETUP_PLUGIN_NAME}" plugin does not expose its config as ` +
180 "`api.options` or `api.options.kit` — the installed @sveltejs/kit version " +
181 "is incompatible with this integration's adapter injection.",
182 );
183 }
184 if (options.kit !== undefined) {
185 const { applicable, constructionTime } = Object.entries(
186 options.kit,
187 ).reduce<{
188 applicable: Record<string, unknown>;
189 constructionTime: Array<string>;
190 }>(
191 (acc, [key, value]) => {
192 if (CONSTRUCTION_TIME_KEYS.includes(key)) {
193 acc.constructionTime.push(key);
194 } else {
195 acc.applicable[key] = value;
196 }
197 return acc;
198 },
199 { applicable: {}, constructionTime: [] },
200 );
201 if (constructionTime.length > 0) {
202 warn(
203 `The kit option(s) ${constructionTime.map((key) => `"${key}"`).join(", ")} ` +
204 "cannot be injected when the project has its own Vite config file — " +
205 "pass them to the `sveltekit(...)` call in the config file instead.",
206 );
207 }
208 mergeKitOptions(kit, applicable);
209 }
210 const userAdapter = kit["adapter"];
211 if (userAdapter !== null && userAdapter !== undefined) {
212 const name =
213 isPlainObject(userAdapter) &&
214 typeof userAdapter["name"] === "string"
215 ? userAdapter["name"]
216 : "unknown";
217 warn(
218 `Replacing the adapter declared in the project's Vite config ("${name}") with ` +
219 "the deploy target's adapter: the deploy target owns build packaging, so a " +
220 "user-declared adapter's output would never be deployed. Remove the " +
221 "`adapter` option from `sveltekit(...)` to silence this warning " +
222 "(plain `vite build` outside this integration still uses it).",
223 );
224 }
225 kit["adapter"] = options.adapter;
226 },
227 },
228 };
229};