1import type { Adapter } from "@sveltejs/kit";
2import type * as ViteModule from "vite";
3import { describe, expect, it } from "vitest";
4import {
5 CONFIG_PLUGIN_NAME,
6 DEFAULT_VITE_CONFIG_FILES,
7 flattenPluginOption,
8 makeSvelteKitConfigPlugin,
9 mergeKitOptions,
10 SVELTEKIT_SETUP_PLUGIN_NAME,
11} from "../UserConfig.ts";
12
13const makeAdapter = (name: string): Adapter => ({
14 name,
15 adapt: async () => {},
16});
17
18/**
19 * A stand-in for the current SvelteKit `vite-plugin-sveltekit-setup` shape,
20 * which exposes the validated Kit config directly through `api.options`.
21 */
22const makeSetupPlugin = (kit: Record<string, unknown>) =>
23 ({
24 name: SVELTEKIT_SETUP_PLUGIN_NAME,
25 api: { options: kit },
26 }) as ViteModule.Plugin;
27
28/**
29 * A stand-in for the legacy setup-plugin shape, where the validated Kit
30 * config was nested under `api.options.kit`.
31 */
32const makeLegacySetupPlugin = (kit: Record<string, unknown>) =>
33 ({
34 name: SVELTEKIT_SETUP_PLUGIN_NAME,
35 api: { options: { kit } },
36 }) as ViteModule.Plugin;
37
38const configEnv: ViteModule.ConfigEnv = {
39 command: "build",
40 mode: "production",
41};
42
43/** Run the injector's config hook the way Vite would. */
44const runConfigHook = async (
45 plugin: ViteModule.Plugin,
46 config: ViteModule.UserConfig,
47): Promise<void> => {
48 const hook = plugin.config as {
49 order?: string;
50 handler: (
51 config: ViteModule.UserConfig,
52 env: ViteModule.ConfigEnv,
53 ) => Promise<unknown>;
54 };
55 await hook.handler(config, configEnv);
56};
57
58describe("flattenPluginOption", () => {
59 it("flattens nested arrays, promises, and falsy holes", async () => {
60 const a = { name: "a" } as ViteModule.Plugin;
61 const b = { name: "b" } as ViteModule.Plugin;
62 const c = { name: "c" } as ViteModule.Plugin;
63 const flattened = await flattenPluginOption([
64 a,
65 false,
66 Promise.resolve([b, undefined, [null, Promise.resolve(c)]]),
67 ]);
68 expect(flattened.map((plugin) => plugin.name)).toEqual(["a", "b", "c"]);
69 });
70
71 it("returns an empty list for an absent option", async () => {
72 expect(await flattenPluginOption(undefined)).toEqual([]);
73 });
74});
75
76describe("mergeKitOptions", () => {
77 it("merges nested plain objects key-by-key and replaces everything else", () => {
78 const target: Record<string, unknown> = {
79 alias: { $lib: "src/lib" },
80 prerender: { entries: ["*"], concurrency: 1 },
81 appDir: "_app",
82 };
83 mergeKitOptions(target, {
84 alias: { $extra: "src/extra" },
85 prerender: { entries: ["/about"] },
86 appDir: "custom",
87 ignored: undefined,
88 });
89 expect(target["alias"]).toEqual({
90 $lib: "src/lib",
91 $extra: "src/extra",
92 });
93 expect(target["prerender"]).toEqual({
94 entries: ["/about"],
95 concurrency: 1,
96 });
97 expect(target["appDir"]).toBe("custom");
98 expect("ignored" in target).toBe(false);
99 });
100});
101
102describe("makeSvelteKitConfigPlugin", () => {
103 it("is pre-enforced with a pre-ordered config hook (must run before kit's setup hook)", () => {
104 const plugin = makeSvelteKitConfigPlugin({
105 adapter: makeAdapter("target-adapter"),
106 });
107 expect(plugin.name).toBe(CONFIG_PLUGIN_NAME);
108 expect(plugin.enforce).toBe("pre");
109 expect((plugin.config as { order?: string }).order).toBe("pre");
110 });
111
112 it("injects the deploy target's adapter into the user's sveltekit() config", async () => {
113 const adapter = makeAdapter("target-adapter");
114 const kit: Record<string, unknown> = {};
115 const warnings: Array<string> = [];
116 const plugin = makeSvelteKitConfigPlugin({
117 adapter,
118 warn: (m) => warnings.push(m),
119 });
120
121 // sveltekit() returns Promise<Plugin[]> — mirror that shape
122 await runConfigHook(plugin, {
123 plugins: [Promise.resolve([makeSetupPlugin(kit)])],
124 });
125
126 expect(kit["adapter"]).toBe(adapter);
127 expect(warnings).toEqual([]);
128 });
129
130 it("supports the legacy nested api.options.kit shape", async () => {
131 const adapter = makeAdapter("target-adapter");
132 const kit: Record<string, unknown> = {};
133 const plugin = makeSvelteKitConfigPlugin({ adapter });
134
135 await runConfigHook(plugin, {
136 plugins: [makeLegacySetupPlugin(kit)],
137 });
138
139 expect(kit["adapter"]).toBe(adapter);
140 });
141
142 it("replaces a user-declared adapter and warns", async () => {
143 const adapter = makeAdapter("target-adapter");
144 const kit: Record<string, unknown> = {
145 adapter: makeAdapter("user-adapter"),
146 };
147 const warnings: Array<string> = [];
148 const plugin = makeSvelteKitConfigPlugin({
149 adapter,
150 warn: (m) => warnings.push(m),
151 });
152 await runConfigHook(plugin, {
153 plugins: [[makeSetupPlugin(kit)]],
154 });
155 expect(kit["adapter"]).toBe(adapter);
156 expect(warnings).toHaveLength(1);
157 expect(warnings[0]).toContain('"user-adapter"');
158 expect(warnings[0]).toContain("deploy target");
159 });
160
161 it("merges integration kit options over the user's (integration wins)", async () => {
162 const adapter = makeAdapter("target-adapter");
163 const kit: Record<string, unknown> = {
164 alias: { $lib: "src/lib" },
165 appDir: "_app",
166 };
167 const plugin = makeSvelteKitConfigPlugin({
168 adapter,
169 kit: {
170 alias: { $fixture: "src/fixture" },
171 appDir: "custom",
172 },
173 });
174 await runConfigHook(plugin, {
175 plugins: [makeSetupPlugin(kit)],
176 });
177 expect(kit["alias"]).toEqual({
178 $lib: "src/lib",
179 $fixture: "src/fixture",
180 });
181 expect(kit["appDir"]).toBe("custom");
182 expect(kit["adapter"]).toBe(adapter);
183 });
184
185 it("warns for construction-time options it cannot apply late", async () => {
186 const adapter = makeAdapter("target-adapter");
187 const kit: Record<string, unknown> = {};
188 const warnings: Array<string> = [];
189 const plugin = makeSvelteKitConfigPlugin({
190 adapter,
191 kit: {
192 preprocess: [],
193 alias: { $x: "src/x" },
194 },
195 warn: (m) => warnings.push(m),
196 });
197 await runConfigHook(plugin, {
198 plugins: [makeSetupPlugin(kit)],
199 });
200 expect(warnings).toHaveLength(1);
201 expect(warnings[0]).toContain('"preprocess"');
202 expect(kit["alias"]).toEqual({ $x: "src/x" });
203 });
204
205 it("fails clearly when the Vite config has no sveltekit() plugin", async () => {
206 const plugin = makeSvelteKitConfigPlugin({
207 adapter: makeAdapter("target-adapter"),
208 });
209 await expect(
210 runConfigHook(plugin, {
211 plugins: [{ name: "some-other-plugin" }],
212 }),
213 ).rejects.toThrow(/does not register the SvelteKit plugin/);
214 });
215
216 it("fails clearly when the setup plugin does not expose its options", async () => {
217 const plugin = makeSvelteKitConfigPlugin({
218 adapter: makeAdapter("target-adapter"),
219 });
220 await expect(
221 runConfigHook(plugin, {
222 plugins: [{ name: SVELTEKIT_SETUP_PLUGIN_NAME }],
223 }),
224 ).rejects.toThrow(/api\.options/);
225 });
226});
227
228describe("DEFAULT_VITE_CONFIG_FILES", () => {
229 it("mirrors Vite's default config discovery list", () => {
230 expect(DEFAULT_VITE_CONFIG_FILES).toEqual([
231 "vite.config.js",
232 "vite.config.mjs",
233 "vite.config.ts",
234 "vite.config.cjs",
235 "vite.config.mts",
236 "vite.config.cts",
237 ]);
238 });
239});