Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ coverage
*.tsbuildinfo

.turbo

# AI generated plans from developer loop
docs/plans
103 changes: 103 additions & 0 deletions packages/appkit/src/utils/tests/vite-config-merge.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import type { Plugin } from "vite";
import { describe, expect, test } from "vitest";
import { mergeConfigDedup } from "../vite-config-merge";

const plugin = (name: string): Plugin => ({ name, enforce: "pre" });

const simpleMerge = (a: Plugin, b: Plugin) => ({ ...a, ...b });

describe("mergeConfigDedup", () => {
test("deduplicates plugins by name, keeping the first occurrence", () => {
const base = { plugins: [plugin("a"), plugin("b")] };
const override = { plugins: [plugin("b"), plugin("c")] };

const result = mergeConfigDedup(base, override, simpleMerge);

expect(result.plugins.map((p: Plugin) => p.name)).toEqual(["a", "b", "c"]);
});

test("returns merged config when no plugins on either side", () => {
const result = mergeConfigDedup({ x: 1 }, { y: 2 }, simpleMerge);
expect(result).toEqual({ x: 1, y: 2 });
expect(result.plugins).toBeUndefined();
});

test("preserves plugins when only base has them", () => {
const base = { plugins: [plugin("a")] };
const override = { other: true };

const result = mergeConfigDedup(base, override, simpleMerge);
// mergeFn merges everything; dedup branch only runs when both have plugins
expect(result.plugins).toBeDefined();
});

test("flattens array-returning plugins (e.g. @tailwindcss/vite)", () => {
const tailwindPreset = [
plugin("tw:base"),
plugin("tw:scan"),
plugin("tw:generate"),
];
const base = { plugins: [tailwindPreset] };
const override = { plugins: [plugin("react")] };

const result = mergeConfigDedup(base, override, simpleMerge);

expect(result.plugins.map((p: Plugin) => p.name)).toEqual([
"tw:base",
"tw:scan",
"tw:generate",
"react",
]);
});

test("deduplicates across flattened array plugins and single plugins", () => {
const preset = [plugin("shared"), plugin("unique-a")];
const base = { plugins: [preset] };
const override = { plugins: [plugin("shared"), plugin("unique-b")] };

const result = mergeConfigDedup(base, override, simpleMerge);

expect(result.plugins.map((p: Plugin) => p.name)).toEqual([
"shared",
"unique-a",
"unique-b",
]);
});

test("handles deeply nested plugin arrays", () => {
const deep = [[plugin("deep-a")], [[plugin("deep-b")]]];
const base = { plugins: [deep] };
const override = { plugins: [plugin("top")] };

const result = mergeConfigDedup(base, override, simpleMerge);

expect(result.plugins.map((p: Plugin) => p.name)).toEqual([
"deep-a",
"deep-b",
"top",
]);
});

test("filters out false/null/undefined plugin entries", () => {
const base = { plugins: [plugin("a"), false, null] };
const override = { plugins: [undefined, plugin("b")] };

const result = mergeConfigDedup(base, override, simpleMerge);

expect(result.plugins.map((p: Plugin) => p.name)).toEqual(["a", "b"]);
});

test("handles mixed falsy and array plugins", () => {
const preset = [plugin("tw:base"), plugin("tw:scan")];
const base = { plugins: [false, preset, null] };
const override = { plugins: [undefined, plugin("react"), false] };

const result = mergeConfigDedup(base, override, simpleMerge);

expect(result.plugins.map((p: Plugin) => p.name)).toEqual([
"tw:base",
"tw:scan",
"react",
]);
});
});
19 changes: 11 additions & 8 deletions packages/appkit/src/utils/vite-config-merge.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import type { Plugin } from "vite";

function flattenPlugins(plugins: any[]): Plugin[] {
return plugins.flat(Infinity).filter(Boolean);
}

export function mergeConfigDedup(
base: any,
override: any,
Expand All @@ -8,14 +12,13 @@ export function mergeConfigDedup(
const merged = mergeFn(base, override);
if (base.plugins && override.plugins) {
const seen = new Set<string>();
merged.plugins = [...base.plugins, ...override.plugins].filter(
(p: Plugin) => {
const name = p.name;
if (seen.has(name)) return false;
seen.add(name);
return true;
},
);
const allPlugins = flattenPlugins([...base.plugins, ...override.plugins]);
merged.plugins = allPlugins.filter((p) => {
const name = p.name;
if (seen.has(name)) return false;
seen.add(name);
return true;
});
}
return merged;
}