Skip to content

Commit eb1b992

Browse files
committed
fix: make postcss plugin patching idempotent
1 parent e6638f1 commit eb1b992

3 files changed

Lines changed: 71 additions & 8 deletions

File tree

packages/core/src/postcss/ast-generating.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,22 @@ export default config;
2727
);
2828
});
2929

30+
test("parseAndUpdatePostcssConfig is idempotent for array plugin configs", () => {
31+
const source = `module.exports = { plugins: ["@react-zero-ui/core/postcss", "@tailwindcss/postcss"] };`;
32+
const updated = parseAndUpdatePostcssConfig(source, zeroUiPlugin, false);
33+
34+
assert.equal(updated, source);
35+
assert.equal((updated?.split(zeroUiPlugin).length ?? 1) - 1, 1);
36+
});
37+
38+
test("parseAndUpdatePostcssConfig is idempotent for object plugin configs", () => {
39+
const source = `module.exports = { plugins: { "@react-zero-ui/core/postcss": {}, "@tailwindcss/postcss": {} } };`;
40+
const updated = parseAndUpdatePostcssConfig(source, zeroUiPlugin, false);
41+
42+
assert.equal(updated, source);
43+
assert.equal((updated?.split(zeroUiPlugin).length ?? 1) - 1, 1);
44+
});
45+
3046
const FIXTURES = {
3147
// 1. ESM object
3248
"vite.config.js": `

packages/core/src/postcss/ast-generating.ts

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export function parseAndUpdatePostcssConfig(source: string, zeroUiPlugin: string
3535
const ast = parse(source, AST_CONFIG_OPTS);
3636

3737
let modified = false;
38+
let handled = false;
3839

3940
traverse(ast, {
4041
// Handle CommonJS: module.exports = { ... } and exports = { ... }
@@ -52,7 +53,9 @@ export function parseAndUpdatePostcssConfig(source: string, zeroUiPlugin: string
5253
);
5354

5455
if (pluginsProperty && t.isExpression(pluginsProperty.value)) {
55-
modified = addZeroUiToPlugins(pluginsProperty.value, zeroUiPlugin);
56+
const result = addZeroUiToPlugins(pluginsProperty.value, zeroUiPlugin);
57+
handled ||= result !== "unsupported";
58+
modified ||= result === "added";
5659
}
5760
}
5861
},
@@ -65,7 +68,9 @@ export function parseAndUpdatePostcssConfig(source: string, zeroUiPlugin: string
6568
);
6669

6770
if (pluginsProperty && t.isExpression(pluginsProperty.value)) {
68-
modified = addZeroUiToPlugins(pluginsProperty.value, zeroUiPlugin);
71+
const result = addZeroUiToPlugins(pluginsProperty.value, zeroUiPlugin);
72+
handled ||= result !== "unsupported";
73+
modified ||= result === "added";
6974
}
7075
}
7176
},
@@ -78,14 +83,18 @@ export function parseAndUpdatePostcssConfig(source: string, zeroUiPlugin: string
7883
);
7984

8085
if (pluginsProperty && t.isExpression(pluginsProperty.value)) {
81-
modified = addZeroUiToPlugins(pluginsProperty.value, zeroUiPlugin);
86+
const result = addZeroUiToPlugins(pluginsProperty.value, zeroUiPlugin);
87+
handled ||= result !== "unsupported";
88+
modified ||= result === "added";
8289
}
8390
}
8491
},
8592
});
8693

8794
if (modified) {
8895
return generate(ast).code;
96+
} else if (handled) {
97+
return source;
8998
} else {
9099
console.warn(`[Zero-UI] Failed to automatically modify PostCSS config: ${source}`);
91100
return null; // Could not automatically modify
@@ -101,15 +110,37 @@ export function parseAndUpdatePostcssConfig(source: string, zeroUiPlugin: string
101110
* Helper function to add Zero-UI plugin to plugins configuration
102111
* Handles both object format {plugin: {}} and array format [plugin]
103112
*/
104-
function addZeroUiToPlugins(pluginsNode: t.Expression, zeroUiPlugin: string): boolean {
113+
function addZeroUiToPlugins(pluginsNode: t.Expression, zeroUiPlugin: string): "added" | "present" | "unsupported" {
105114
if (t.isObjectExpression(pluginsNode)) {
115+
const alreadyPresent = pluginsNode.properties.some(
116+
(prop) => t.isObjectProperty(prop) && t.isStringLiteral(prop.key) && prop.key.value === zeroUiPlugin
117+
);
118+
if (alreadyPresent) return "present";
119+
106120
// Object format: { 'plugin': {} }
107121
pluginsNode.properties.unshift(t.objectProperty(t.stringLiteral(zeroUiPlugin), t.objectExpression([])));
108-
return true;
122+
return "added";
109123
} else if (t.isArrayExpression(pluginsNode)) {
124+
const alreadyPresent = pluginsNode.elements.some((el) => isZeroUiPostcssEntry(el, zeroUiPlugin));
125+
if (alreadyPresent) return "present";
126+
110127
// Array format: ['plugin']
111128
pluginsNode.elements.unshift(t.stringLiteral(zeroUiPlugin));
112-
return true;
129+
return "added";
130+
}
131+
return "unsupported";
132+
}
133+
134+
function isZeroUiPostcssEntry(node: t.ArrayExpression["elements"][number], zeroUiPlugin: string): boolean {
135+
if (!node) return false;
136+
if (t.isStringLiteral(node)) return node.value === zeroUiPlugin;
137+
if (
138+
t.isCallExpression(node) &&
139+
t.isIdentifier(node.callee, { name: "require" }) &&
140+
node.arguments.length === 1 &&
141+
t.isStringLiteral(node.arguments[0])
142+
) {
143+
return node.arguments[0].value === zeroUiPlugin;
113144
}
114145
return false;
115146
}

packages/core/src/postcss/helpers.test.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,8 +215,8 @@ test("patchPostcssConfig inserts Zero-UI before Tailwind in existing config", as
215215
test("patchPostcssConfig is idempotent when Zero-UI plugin already present", async () => {
216216
const original = `module.exports = {
217217
plugins: {
218-
${ZERO}: {},
219-
${TAIL}: {},
218+
"${ZERO}": {},
219+
"${TAIL}": {},
220220
},
221221
};`;
222222

@@ -226,6 +226,22 @@ test("patchPostcssConfig is idempotent when Zero-UI plugin already present", asy
226226
});
227227
});
228228

229+
test("patchPostcssConfig does not duplicate Zero-UI in array plugin configs", async () => {
230+
const original = `module.exports = {
231+
plugins: ["${ZERO}", "${TAIL}"],
232+
};`;
233+
234+
await runTest({ "postcss.config.js": original }, async () => {
235+
await patchPostcssConfig();
236+
237+
const updated = readFile("postcss.config.js");
238+
const zeroCount = updated.split(ZERO).length - 1;
239+
240+
assert.equal(zeroCount, 1, "Zero-UI plugin must not be duplicated");
241+
assert.ok(zeroBeforeTail(updated), "Zero-UI must precede Tailwind");
242+
});
243+
});
244+
229245
test("patchViteConfig creates defineConfig setup when no plugin array exists", async () => {
230246
await runTest(
231247
{

0 commit comments

Comments
 (0)