From eb1b992e40b860619a57c0b7a2920a76b34008af Mon Sep 17 00:00:00 2001 From: Austin1serb Date: Mon, 6 Apr 2026 16:33:28 -0600 Subject: [PATCH 1/4] fix: make postcss plugin patching idempotent --- .../core/src/postcss/ast-generating.test.ts | 16 +++++++ packages/core/src/postcss/ast-generating.ts | 43 ++++++++++++++++--- packages/core/src/postcss/helpers.test.ts | 20 ++++++++- 3 files changed, 71 insertions(+), 8 deletions(-) diff --git a/packages/core/src/postcss/ast-generating.test.ts b/packages/core/src/postcss/ast-generating.test.ts index b29697a..8573d0c 100644 --- a/packages/core/src/postcss/ast-generating.test.ts +++ b/packages/core/src/postcss/ast-generating.test.ts @@ -27,6 +27,22 @@ export default config; ); }); +test("parseAndUpdatePostcssConfig is idempotent for array plugin configs", () => { + const source = `module.exports = { plugins: ["@react-zero-ui/core/postcss", "@tailwindcss/postcss"] };`; + const updated = parseAndUpdatePostcssConfig(source, zeroUiPlugin, false); + + assert.equal(updated, source); + assert.equal((updated?.split(zeroUiPlugin).length ?? 1) - 1, 1); +}); + +test("parseAndUpdatePostcssConfig is idempotent for object plugin configs", () => { + const source = `module.exports = { plugins: { "@react-zero-ui/core/postcss": {}, "@tailwindcss/postcss": {} } };`; + const updated = parseAndUpdatePostcssConfig(source, zeroUiPlugin, false); + + assert.equal(updated, source); + assert.equal((updated?.split(zeroUiPlugin).length ?? 1) - 1, 1); +}); + const FIXTURES = { // 1. ESM object "vite.config.js": ` diff --git a/packages/core/src/postcss/ast-generating.ts b/packages/core/src/postcss/ast-generating.ts index 3df6840..4e0a374 100644 --- a/packages/core/src/postcss/ast-generating.ts +++ b/packages/core/src/postcss/ast-generating.ts @@ -35,6 +35,7 @@ export function parseAndUpdatePostcssConfig(source: string, zeroUiPlugin: string const ast = parse(source, AST_CONFIG_OPTS); let modified = false; + let handled = false; traverse(ast, { // Handle CommonJS: module.exports = { ... } and exports = { ... } @@ -52,7 +53,9 @@ export function parseAndUpdatePostcssConfig(source: string, zeroUiPlugin: string ); if (pluginsProperty && t.isExpression(pluginsProperty.value)) { - modified = addZeroUiToPlugins(pluginsProperty.value, zeroUiPlugin); + const result = addZeroUiToPlugins(pluginsProperty.value, zeroUiPlugin); + handled ||= result !== "unsupported"; + modified ||= result === "added"; } } }, @@ -65,7 +68,9 @@ export function parseAndUpdatePostcssConfig(source: string, zeroUiPlugin: string ); if (pluginsProperty && t.isExpression(pluginsProperty.value)) { - modified = addZeroUiToPlugins(pluginsProperty.value, zeroUiPlugin); + const result = addZeroUiToPlugins(pluginsProperty.value, zeroUiPlugin); + handled ||= result !== "unsupported"; + modified ||= result === "added"; } } }, @@ -78,7 +83,9 @@ export function parseAndUpdatePostcssConfig(source: string, zeroUiPlugin: string ); if (pluginsProperty && t.isExpression(pluginsProperty.value)) { - modified = addZeroUiToPlugins(pluginsProperty.value, zeroUiPlugin); + const result = addZeroUiToPlugins(pluginsProperty.value, zeroUiPlugin); + handled ||= result !== "unsupported"; + modified ||= result === "added"; } } }, @@ -86,6 +93,8 @@ export function parseAndUpdatePostcssConfig(source: string, zeroUiPlugin: string if (modified) { return generate(ast).code; + } else if (handled) { + return source; } else { console.warn(`[Zero-UI] Failed to automatically modify PostCSS config: ${source}`); return null; // Could not automatically modify @@ -101,15 +110,37 @@ export function parseAndUpdatePostcssConfig(source: string, zeroUiPlugin: string * Helper function to add Zero-UI plugin to plugins configuration * Handles both object format {plugin: {}} and array format [plugin] */ -function addZeroUiToPlugins(pluginsNode: t.Expression, zeroUiPlugin: string): boolean { +function addZeroUiToPlugins(pluginsNode: t.Expression, zeroUiPlugin: string): "added" | "present" | "unsupported" { if (t.isObjectExpression(pluginsNode)) { + const alreadyPresent = pluginsNode.properties.some( + (prop) => t.isObjectProperty(prop) && t.isStringLiteral(prop.key) && prop.key.value === zeroUiPlugin + ); + if (alreadyPresent) return "present"; + // Object format: { 'plugin': {} } pluginsNode.properties.unshift(t.objectProperty(t.stringLiteral(zeroUiPlugin), t.objectExpression([]))); - return true; + return "added"; } else if (t.isArrayExpression(pluginsNode)) { + const alreadyPresent = pluginsNode.elements.some((el) => isZeroUiPostcssEntry(el, zeroUiPlugin)); + if (alreadyPresent) return "present"; + // Array format: ['plugin'] pluginsNode.elements.unshift(t.stringLiteral(zeroUiPlugin)); - return true; + return "added"; + } + return "unsupported"; +} + +function isZeroUiPostcssEntry(node: t.ArrayExpression["elements"][number], zeroUiPlugin: string): boolean { + if (!node) return false; + if (t.isStringLiteral(node)) return node.value === zeroUiPlugin; + if ( + t.isCallExpression(node) && + t.isIdentifier(node.callee, { name: "require" }) && + node.arguments.length === 1 && + t.isStringLiteral(node.arguments[0]) + ) { + return node.arguments[0].value === zeroUiPlugin; } return false; } diff --git a/packages/core/src/postcss/helpers.test.ts b/packages/core/src/postcss/helpers.test.ts index 0aeb11f..0148a80 100644 --- a/packages/core/src/postcss/helpers.test.ts +++ b/packages/core/src/postcss/helpers.test.ts @@ -215,8 +215,8 @@ test("patchPostcssConfig inserts Zero-UI before Tailwind in existing config", as test("patchPostcssConfig is idempotent when Zero-UI plugin already present", async () => { const original = `module.exports = { plugins: { - ${ZERO}: {}, - ${TAIL}: {}, + "${ZERO}": {}, + "${TAIL}": {}, }, };`; @@ -226,6 +226,22 @@ test("patchPostcssConfig is idempotent when Zero-UI plugin already present", asy }); }); +test("patchPostcssConfig does not duplicate Zero-UI in array plugin configs", async () => { + const original = `module.exports = { + plugins: ["${ZERO}", "${TAIL}"], +};`; + + await runTest({ "postcss.config.js": original }, async () => { + await patchPostcssConfig(); + + const updated = readFile("postcss.config.js"); + const zeroCount = updated.split(ZERO).length - 1; + + assert.equal(zeroCount, 1, "Zero-UI plugin must not be duplicated"); + assert.ok(zeroBeforeTail(updated), "Zero-UI must precede Tailwind"); + }); +}); + test("patchViteConfig creates defineConfig setup when no plugin array exists", async () => { await runTest( { From a0c19f76dfc52ee7b6bd4b1288b71fe3c9c57353 Mon Sep 17 00:00:00 2001 From: Austin1serb Date: Mon, 6 Apr 2026 16:46:34 -0600 Subject: [PATCH 2/4] fix: enforce zero-ui postcss plugin ordering --- .../core/src/postcss/ast-generating.test.ts | 18 +++++ packages/core/src/postcss/ast-generating.ts | 67 +++++++++++++++---- packages/core/src/postcss/helpers.test.ts | 16 +++++ 3 files changed, 87 insertions(+), 14 deletions(-) diff --git a/packages/core/src/postcss/ast-generating.test.ts b/packages/core/src/postcss/ast-generating.test.ts index 8573d0c..efb1631 100644 --- a/packages/core/src/postcss/ast-generating.test.ts +++ b/packages/core/src/postcss/ast-generating.test.ts @@ -35,6 +35,15 @@ test("parseAndUpdatePostcssConfig is idempotent for array plugin configs", () => assert.equal((updated?.split(zeroUiPlugin).length ?? 1) - 1, 1); }); +test("parseAndUpdatePostcssConfig reorders array plugin configs so Zero-UI comes before Tailwind", () => { + const source = `module.exports = { plugins: ["@tailwindcss/postcss", "@react-zero-ui/core/postcss"] };`; + const updated = parseAndUpdatePostcssConfig(source, zeroUiPlugin, false); + + assert.ok(updated); + assert.equal((updated?.split(zeroUiPlugin).length ?? 1) - 1, 1); + assert.ok(updated!.indexOf(zeroUiPlugin) < updated!.indexOf("@tailwindcss/postcss")); +}); + test("parseAndUpdatePostcssConfig is idempotent for object plugin configs", () => { const source = `module.exports = { plugins: { "@react-zero-ui/core/postcss": {}, "@tailwindcss/postcss": {} } };`; const updated = parseAndUpdatePostcssConfig(source, zeroUiPlugin, false); @@ -43,6 +52,15 @@ test("parseAndUpdatePostcssConfig is idempotent for object plugin configs", () = assert.equal((updated?.split(zeroUiPlugin).length ?? 1) - 1, 1); }); +test("parseAndUpdatePostcssConfig reorders object plugin configs so Zero-UI comes before Tailwind", () => { + const source = `module.exports = { plugins: { "@tailwindcss/postcss": {}, "@react-zero-ui/core/postcss": {} } };`; + const updated = parseAndUpdatePostcssConfig(source, zeroUiPlugin, false); + + assert.ok(updated); + assert.equal((updated?.split(zeroUiPlugin).length ?? 1) - 1, 1); + assert.ok(updated!.indexOf(zeroUiPlugin) < updated!.indexOf("@tailwindcss/postcss")); +}); + const FIXTURES = { // 1. ESM object "vite.config.js": ` diff --git a/packages/core/src/postcss/ast-generating.ts b/packages/core/src/postcss/ast-generating.ts index 4e0a374..0ce919a 100644 --- a/packages/core/src/postcss/ast-generating.ts +++ b/packages/core/src/postcss/ast-generating.ts @@ -55,7 +55,7 @@ export function parseAndUpdatePostcssConfig(source: string, zeroUiPlugin: string if (pluginsProperty && t.isExpression(pluginsProperty.value)) { const result = addZeroUiToPlugins(pluginsProperty.value, zeroUiPlugin); handled ||= result !== "unsupported"; - modified ||= result === "added"; + modified ||= result === "added" || result === "reordered"; } } }, @@ -70,7 +70,7 @@ export function parseAndUpdatePostcssConfig(source: string, zeroUiPlugin: string if (pluginsProperty && t.isExpression(pluginsProperty.value)) { const result = addZeroUiToPlugins(pluginsProperty.value, zeroUiPlugin); handled ||= result !== "unsupported"; - modified ||= result === "added"; + modified ||= result === "added" || result === "reordered"; } } }, @@ -85,7 +85,7 @@ export function parseAndUpdatePostcssConfig(source: string, zeroUiPlugin: string if (pluginsProperty && t.isExpression(pluginsProperty.value)) { const result = addZeroUiToPlugins(pluginsProperty.value, zeroUiPlugin); handled ||= result !== "unsupported"; - modified ||= result === "added"; + modified ||= result === "added" || result === "reordered"; } } }, @@ -110,41 +110,80 @@ export function parseAndUpdatePostcssConfig(source: string, zeroUiPlugin: string * Helper function to add Zero-UI plugin to plugins configuration * Handles both object format {plugin: {}} and array format [plugin] */ -function addZeroUiToPlugins(pluginsNode: t.Expression, zeroUiPlugin: string): "added" | "present" | "unsupported" { +function addZeroUiToPlugins(pluginsNode: t.Expression, zeroUiPlugin: string): "added" | "reordered" | "present" | "unsupported" { if (t.isObjectExpression(pluginsNode)) { - const alreadyPresent = pluginsNode.properties.some( - (prop) => t.isObjectProperty(prop) && t.isStringLiteral(prop.key) && prop.key.value === zeroUiPlugin + const zeroIndex = pluginsNode.properties.findIndex( + (prop) => t.isObjectProperty(prop) && getPluginObjectKey(prop) === zeroUiPlugin + ); + const tailwindIndex = pluginsNode.properties.findIndex( + (prop) => t.isObjectProperty(prop) && getPluginObjectKey(prop) === "@tailwindcss/postcss" ); - if (alreadyPresent) return "present"; + + if (zeroIndex !== -1) { + if (tailwindIndex !== -1 && zeroIndex > tailwindIndex) { + const [zeroProp] = pluginsNode.properties.splice(zeroIndex, 1); + const nextTailwindIndex = pluginsNode.properties.findIndex( + (prop) => t.isObjectProperty(prop) && getPluginObjectKey(prop) === "@tailwindcss/postcss" + ); + pluginsNode.properties.splice(nextTailwindIndex, 0, zeroProp); + return "reordered"; + } + return "present"; + } // Object format: { 'plugin': {} } - pluginsNode.properties.unshift(t.objectProperty(t.stringLiteral(zeroUiPlugin), t.objectExpression([]))); + const newProp = t.objectProperty(t.stringLiteral(zeroUiPlugin), t.objectExpression([])); + if (tailwindIndex !== -1) { + pluginsNode.properties.splice(tailwindIndex, 0, newProp); + } else { + pluginsNode.properties.unshift(newProp); + } return "added"; } else if (t.isArrayExpression(pluginsNode)) { - const alreadyPresent = pluginsNode.elements.some((el) => isZeroUiPostcssEntry(el, zeroUiPlugin)); - if (alreadyPresent) return "present"; + const zeroIndex = pluginsNode.elements.findIndex((el) => isPluginArrayEntry(el, zeroUiPlugin)); + const tailwindIndex = pluginsNode.elements.findIndex((el) => isPluginArrayEntry(el, "@tailwindcss/postcss")); + + if (zeroIndex !== -1) { + if (tailwindIndex !== -1 && zeroIndex > tailwindIndex) { + const [zeroEntry] = pluginsNode.elements.splice(zeroIndex, 1); + const nextTailwindIndex = pluginsNode.elements.findIndex((el) => isPluginArrayEntry(el, "@tailwindcss/postcss")); + pluginsNode.elements.splice(nextTailwindIndex, 0, zeroEntry); + return "reordered"; + } + return "present"; + } // Array format: ['plugin'] - pluginsNode.elements.unshift(t.stringLiteral(zeroUiPlugin)); + if (tailwindIndex !== -1) { + pluginsNode.elements.splice(tailwindIndex, 0, t.stringLiteral(zeroUiPlugin)); + } else { + pluginsNode.elements.unshift(t.stringLiteral(zeroUiPlugin)); + } return "added"; } return "unsupported"; } -function isZeroUiPostcssEntry(node: t.ArrayExpression["elements"][number], zeroUiPlugin: string): boolean { +function isPluginArrayEntry(node: t.ArrayExpression["elements"][number], pluginName: string): boolean { if (!node) return false; - if (t.isStringLiteral(node)) return node.value === zeroUiPlugin; + if (t.isStringLiteral(node)) return node.value === pluginName; if ( t.isCallExpression(node) && t.isIdentifier(node.callee, { name: "require" }) && node.arguments.length === 1 && t.isStringLiteral(node.arguments[0]) ) { - return node.arguments[0].value === zeroUiPlugin; + return node.arguments[0].value === pluginName; } return false; } +function getPluginObjectKey(node: t.ObjectProperty): string | null { + if (t.isStringLiteral(node.key)) return node.key.value; + if (t.isIdentifier(node.key)) return node.key.name; + return null; +} + /** * Parse Vite config TypeScript/JavaScript file and add Zero-UI plugin * Removes tailwindcss plugin if present, and adds zeroUI plugin if missing diff --git a/packages/core/src/postcss/helpers.test.ts b/packages/core/src/postcss/helpers.test.ts index 0148a80..a39038d 100644 --- a/packages/core/src/postcss/helpers.test.ts +++ b/packages/core/src/postcss/helpers.test.ts @@ -242,6 +242,22 @@ test("patchPostcssConfig does not duplicate Zero-UI in array plugin configs", as }); }); +test("patchPostcssConfig reorders array plugin configs when Tailwind comes first", async () => { + const original = `module.exports = { + plugins: ["${TAIL}", "${ZERO}"], +};`; + + await runTest({ "postcss.config.js": original }, async () => { + await patchPostcssConfig(); + + const updated = readFile("postcss.config.js"); + const zeroCount = updated.split(ZERO).length - 1; + + assert.equal(zeroCount, 1, "Zero-UI plugin must not be duplicated"); + assert.ok(zeroBeforeTail(updated), "Zero-UI must precede Tailwind"); + }); +}); + test("patchViteConfig creates defineConfig setup when no plugin array exists", async () => { await runTest( { From 01392bf2b1f22cf637bf74cf7daf629d2ea30fd0 Mon Sep 17 00:00:00 2001 From: Austin1serb Date: Mon, 6 Apr 2026 16:48:46 -0600 Subject: [PATCH 3/4] refactor: simplify postcss plugin normalization --- packages/core/src/postcss/ast-generating.ts | 81 +++++++++------------ 1 file changed, 36 insertions(+), 45 deletions(-) diff --git a/packages/core/src/postcss/ast-generating.ts b/packages/core/src/postcss/ast-generating.ts index 0ce919a..c5a565e 100644 --- a/packages/core/src/postcss/ast-generating.ts +++ b/packages/core/src/postcss/ast-generating.ts @@ -112,56 +112,47 @@ export function parseAndUpdatePostcssConfig(source: string, zeroUiPlugin: string */ function addZeroUiToPlugins(pluginsNode: t.Expression, zeroUiPlugin: string): "added" | "reordered" | "present" | "unsupported" { if (t.isObjectExpression(pluginsNode)) { - const zeroIndex = pluginsNode.properties.findIndex( - (prop) => t.isObjectProperty(prop) && getPluginObjectKey(prop) === zeroUiPlugin + return normalizePluginEntries( + pluginsNode.properties, + (prop): prop is t.ObjectProperty => t.isObjectProperty(prop) && getPluginObjectKey(prop) === zeroUiPlugin, + (prop): prop is t.ObjectProperty => t.isObjectProperty(prop) && getPluginObjectKey(prop) === "@tailwindcss/postcss", + () => t.objectProperty(t.stringLiteral(zeroUiPlugin), t.objectExpression([])) ); - const tailwindIndex = pluginsNode.properties.findIndex( - (prop) => t.isObjectProperty(prop) && getPluginObjectKey(prop) === "@tailwindcss/postcss" + } else if (t.isArrayExpression(pluginsNode)) { + return normalizePluginEntries( + pluginsNode.elements, + (el) => isPluginArrayEntry(el, zeroUiPlugin), + (el) => isPluginArrayEntry(el, "@tailwindcss/postcss"), + () => t.stringLiteral(zeroUiPlugin) ); + } + return "unsupported"; +} - if (zeroIndex !== -1) { - if (tailwindIndex !== -1 && zeroIndex > tailwindIndex) { - const [zeroProp] = pluginsNode.properties.splice(zeroIndex, 1); - const nextTailwindIndex = pluginsNode.properties.findIndex( - (prop) => t.isObjectProperty(prop) && getPluginObjectKey(prop) === "@tailwindcss/postcss" - ); - pluginsNode.properties.splice(nextTailwindIndex, 0, zeroProp); - return "reordered"; - } - return "present"; - } +function normalizePluginEntries( + items: T[], + isZeroUi: (item: T) => boolean, + isTailwind: (item: T) => boolean, + createZeroUi: () => T +): "added" | "reordered" | "present" { + const firstZeroUi = items.findIndex(isZeroUi); + const existingZeroUi = firstZeroUi === -1 ? null : items[firstZeroUi]; + const normalized = items.filter((item) => !isZeroUi(item)); + const tailwindIndex = normalized.findIndex(isTailwind); + const targetIndex = tailwindIndex !== -1 ? tailwindIndex : firstZeroUi === -1 ? 0 : firstZeroUi; + + normalized.splice(targetIndex, 0, existingZeroUi ?? createZeroUi()); + + if (sameOrder(items, normalized)) { + return "present"; + } - // Object format: { 'plugin': {} } - const newProp = t.objectProperty(t.stringLiteral(zeroUiPlugin), t.objectExpression([])); - if (tailwindIndex !== -1) { - pluginsNode.properties.splice(tailwindIndex, 0, newProp); - } else { - pluginsNode.properties.unshift(newProp); - } - return "added"; - } else if (t.isArrayExpression(pluginsNode)) { - const zeroIndex = pluginsNode.elements.findIndex((el) => isPluginArrayEntry(el, zeroUiPlugin)); - const tailwindIndex = pluginsNode.elements.findIndex((el) => isPluginArrayEntry(el, "@tailwindcss/postcss")); - - if (zeroIndex !== -1) { - if (tailwindIndex !== -1 && zeroIndex > tailwindIndex) { - const [zeroEntry] = pluginsNode.elements.splice(zeroIndex, 1); - const nextTailwindIndex = pluginsNode.elements.findIndex((el) => isPluginArrayEntry(el, "@tailwindcss/postcss")); - pluginsNode.elements.splice(nextTailwindIndex, 0, zeroEntry); - return "reordered"; - } - return "present"; - } + items.splice(0, items.length, ...normalized); + return existingZeroUi ? "reordered" : "added"; +} - // Array format: ['plugin'] - if (tailwindIndex !== -1) { - pluginsNode.elements.splice(tailwindIndex, 0, t.stringLiteral(zeroUiPlugin)); - } else { - pluginsNode.elements.unshift(t.stringLiteral(zeroUiPlugin)); - } - return "added"; - } - return "unsupported"; +function sameOrder(left: T[], right: T[]): boolean { + return left.length === right.length && left.every((item, index) => item === right[index]); } function isPluginArrayEntry(node: t.ArrayExpression["elements"][number], pluginName: string): boolean { From 52e901282b62bc41eb5e86a160cf54da3e816249 Mon Sep 17 00:00:00 2001 From: Austin1serb Date: Mon, 6 Apr 2026 18:14:51 -0600 Subject: [PATCH 4/4] refactor: reduce postcss plugin ordering logic --- packages/core/src/postcss/ast-generating.ts | 45 ++++++++++----------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/packages/core/src/postcss/ast-generating.ts b/packages/core/src/postcss/ast-generating.ts index c5a565e..60762e9 100644 --- a/packages/core/src/postcss/ast-generating.ts +++ b/packages/core/src/postcss/ast-generating.ts @@ -54,8 +54,8 @@ export function parseAndUpdatePostcssConfig(source: string, zeroUiPlugin: string if (pluginsProperty && t.isExpression(pluginsProperty.value)) { const result = addZeroUiToPlugins(pluginsProperty.value, zeroUiPlugin); - handled ||= result !== "unsupported"; - modified ||= result === "added" || result === "reordered"; + handled ||= result !== null; + modified ||= result === true; } } }, @@ -69,8 +69,8 @@ export function parseAndUpdatePostcssConfig(source: string, zeroUiPlugin: string if (pluginsProperty && t.isExpression(pluginsProperty.value)) { const result = addZeroUiToPlugins(pluginsProperty.value, zeroUiPlugin); - handled ||= result !== "unsupported"; - modified ||= result === "added" || result === "reordered"; + handled ||= result !== null; + modified ||= result === true; } } }, @@ -84,8 +84,8 @@ export function parseAndUpdatePostcssConfig(source: string, zeroUiPlugin: string if (pluginsProperty && t.isExpression(pluginsProperty.value)) { const result = addZeroUiToPlugins(pluginsProperty.value, zeroUiPlugin); - handled ||= result !== "unsupported"; - modified ||= result === "added" || result === "reordered"; + handled ||= result !== null; + modified ||= result === true; } } }, @@ -110,7 +110,7 @@ export function parseAndUpdatePostcssConfig(source: string, zeroUiPlugin: string * Helper function to add Zero-UI plugin to plugins configuration * Handles both object format {plugin: {}} and array format [plugin] */ -function addZeroUiToPlugins(pluginsNode: t.Expression, zeroUiPlugin: string): "added" | "reordered" | "present" | "unsupported" { +function addZeroUiToPlugins(pluginsNode: t.Expression, zeroUiPlugin: string): boolean | null { if (t.isObjectExpression(pluginsNode)) { return normalizePluginEntries( pluginsNode.properties, @@ -126,7 +126,7 @@ function addZeroUiToPlugins(pluginsNode: t.Expression, zeroUiPlugin: string): "a () => t.stringLiteral(zeroUiPlugin) ); } - return "unsupported"; + return null; } function normalizePluginEntries( @@ -134,25 +134,22 @@ function normalizePluginEntries( isZeroUi: (item: T) => boolean, isTailwind: (item: T) => boolean, createZeroUi: () => T -): "added" | "reordered" | "present" { - const firstZeroUi = items.findIndex(isZeroUi); - const existingZeroUi = firstZeroUi === -1 ? null : items[firstZeroUi]; - const normalized = items.filter((item) => !isZeroUi(item)); - const tailwindIndex = normalized.findIndex(isTailwind); - const targetIndex = tailwindIndex !== -1 ? tailwindIndex : firstZeroUi === -1 ? 0 : firstZeroUi; - - normalized.splice(targetIndex, 0, existingZeroUi ?? createZeroUi()); - - if (sameOrder(items, normalized)) { - return "present"; +): boolean { + const zeroEntries = items.filter(isZeroUi); + const zeroIndex = items.findIndex(isZeroUi); + const tailwindIndex = items.findIndex(isTailwind); + + // Already correct: exactly one Zero-UI entry and it already comes before Tailwind. + if (zeroEntries.length === 1 && (tailwindIndex === -1 || zeroIndex < tailwindIndex)) { + return false; } - items.splice(0, items.length, ...normalized); - return existingZeroUi ? "reordered" : "added"; -} + const normalized = items.filter((item) => !isZeroUi(item)); + const insertIndex = normalized.findIndex(isTailwind); -function sameOrder(left: T[], right: T[]): boolean { - return left.length === right.length && left.every((item, index) => item === right[index]); + normalized.splice(insertIndex === -1 ? 0 : insertIndex, 0, zeroEntries[0] ?? createZeroUi()); + items.splice(0, items.length, ...normalized); + return true; } function isPluginArrayEntry(node: t.ArrayExpression["elements"][number], pluginName: string): boolean {