Skip to content

Commit a0c19f7

Browse files
committed
fix: enforce zero-ui postcss plugin ordering
1 parent eb1b992 commit a0c19f7

3 files changed

Lines changed: 87 additions & 14 deletions

File tree

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,15 @@ test("parseAndUpdatePostcssConfig is idempotent for array plugin configs", () =>
3535
assert.equal((updated?.split(zeroUiPlugin).length ?? 1) - 1, 1);
3636
});
3737

38+
test("parseAndUpdatePostcssConfig reorders array plugin configs so Zero-UI comes before Tailwind", () => {
39+
const source = `module.exports = { plugins: ["@tailwindcss/postcss", "@react-zero-ui/core/postcss"] };`;
40+
const updated = parseAndUpdatePostcssConfig(source, zeroUiPlugin, false);
41+
42+
assert.ok(updated);
43+
assert.equal((updated?.split(zeroUiPlugin).length ?? 1) - 1, 1);
44+
assert.ok(updated!.indexOf(zeroUiPlugin) < updated!.indexOf("@tailwindcss/postcss"));
45+
});
46+
3847
test("parseAndUpdatePostcssConfig is idempotent for object plugin configs", () => {
3948
const source = `module.exports = { plugins: { "@react-zero-ui/core/postcss": {}, "@tailwindcss/postcss": {} } };`;
4049
const updated = parseAndUpdatePostcssConfig(source, zeroUiPlugin, false);
@@ -43,6 +52,15 @@ test("parseAndUpdatePostcssConfig is idempotent for object plugin configs", () =
4352
assert.equal((updated?.split(zeroUiPlugin).length ?? 1) - 1, 1);
4453
});
4554

55+
test("parseAndUpdatePostcssConfig reorders object plugin configs so Zero-UI comes before Tailwind", () => {
56+
const source = `module.exports = { plugins: { "@tailwindcss/postcss": {}, "@react-zero-ui/core/postcss": {} } };`;
57+
const updated = parseAndUpdatePostcssConfig(source, zeroUiPlugin, false);
58+
59+
assert.ok(updated);
60+
assert.equal((updated?.split(zeroUiPlugin).length ?? 1) - 1, 1);
61+
assert.ok(updated!.indexOf(zeroUiPlugin) < updated!.indexOf("@tailwindcss/postcss"));
62+
});
63+
4664
const FIXTURES = {
4765
// 1. ESM object
4866
"vite.config.js": `

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

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export function parseAndUpdatePostcssConfig(source: string, zeroUiPlugin: string
5555
if (pluginsProperty && t.isExpression(pluginsProperty.value)) {
5656
const result = addZeroUiToPlugins(pluginsProperty.value, zeroUiPlugin);
5757
handled ||= result !== "unsupported";
58-
modified ||= result === "added";
58+
modified ||= result === "added" || result === "reordered";
5959
}
6060
}
6161
},
@@ -70,7 +70,7 @@ export function parseAndUpdatePostcssConfig(source: string, zeroUiPlugin: string
7070
if (pluginsProperty && t.isExpression(pluginsProperty.value)) {
7171
const result = addZeroUiToPlugins(pluginsProperty.value, zeroUiPlugin);
7272
handled ||= result !== "unsupported";
73-
modified ||= result === "added";
73+
modified ||= result === "added" || result === "reordered";
7474
}
7575
}
7676
},
@@ -85,7 +85,7 @@ export function parseAndUpdatePostcssConfig(source: string, zeroUiPlugin: string
8585
if (pluginsProperty && t.isExpression(pluginsProperty.value)) {
8686
const result = addZeroUiToPlugins(pluginsProperty.value, zeroUiPlugin);
8787
handled ||= result !== "unsupported";
88-
modified ||= result === "added";
88+
modified ||= result === "added" || result === "reordered";
8989
}
9090
}
9191
},
@@ -110,41 +110,80 @@ export function parseAndUpdatePostcssConfig(source: string, zeroUiPlugin: string
110110
* Helper function to add Zero-UI plugin to plugins configuration
111111
* Handles both object format {plugin: {}} and array format [plugin]
112112
*/
113-
function addZeroUiToPlugins(pluginsNode: t.Expression, zeroUiPlugin: string): "added" | "present" | "unsupported" {
113+
function addZeroUiToPlugins(pluginsNode: t.Expression, zeroUiPlugin: string): "added" | "reordered" | "present" | "unsupported" {
114114
if (t.isObjectExpression(pluginsNode)) {
115-
const alreadyPresent = pluginsNode.properties.some(
116-
(prop) => t.isObjectProperty(prop) && t.isStringLiteral(prop.key) && prop.key.value === zeroUiPlugin
115+
const zeroIndex = pluginsNode.properties.findIndex(
116+
(prop) => t.isObjectProperty(prop) && getPluginObjectKey(prop) === zeroUiPlugin
117+
);
118+
const tailwindIndex = pluginsNode.properties.findIndex(
119+
(prop) => t.isObjectProperty(prop) && getPluginObjectKey(prop) === "@tailwindcss/postcss"
117120
);
118-
if (alreadyPresent) return "present";
121+
122+
if (zeroIndex !== -1) {
123+
if (tailwindIndex !== -1 && zeroIndex > tailwindIndex) {
124+
const [zeroProp] = pluginsNode.properties.splice(zeroIndex, 1);
125+
const nextTailwindIndex = pluginsNode.properties.findIndex(
126+
(prop) => t.isObjectProperty(prop) && getPluginObjectKey(prop) === "@tailwindcss/postcss"
127+
);
128+
pluginsNode.properties.splice(nextTailwindIndex, 0, zeroProp);
129+
return "reordered";
130+
}
131+
return "present";
132+
}
119133

120134
// Object format: { 'plugin': {} }
121-
pluginsNode.properties.unshift(t.objectProperty(t.stringLiteral(zeroUiPlugin), t.objectExpression([])));
135+
const newProp = t.objectProperty(t.stringLiteral(zeroUiPlugin), t.objectExpression([]));
136+
if (tailwindIndex !== -1) {
137+
pluginsNode.properties.splice(tailwindIndex, 0, newProp);
138+
} else {
139+
pluginsNode.properties.unshift(newProp);
140+
}
122141
return "added";
123142
} else if (t.isArrayExpression(pluginsNode)) {
124-
const alreadyPresent = pluginsNode.elements.some((el) => isZeroUiPostcssEntry(el, zeroUiPlugin));
125-
if (alreadyPresent) return "present";
143+
const zeroIndex = pluginsNode.elements.findIndex((el) => isPluginArrayEntry(el, zeroUiPlugin));
144+
const tailwindIndex = pluginsNode.elements.findIndex((el) => isPluginArrayEntry(el, "@tailwindcss/postcss"));
145+
146+
if (zeroIndex !== -1) {
147+
if (tailwindIndex !== -1 && zeroIndex > tailwindIndex) {
148+
const [zeroEntry] = pluginsNode.elements.splice(zeroIndex, 1);
149+
const nextTailwindIndex = pluginsNode.elements.findIndex((el) => isPluginArrayEntry(el, "@tailwindcss/postcss"));
150+
pluginsNode.elements.splice(nextTailwindIndex, 0, zeroEntry);
151+
return "reordered";
152+
}
153+
return "present";
154+
}
126155

127156
// Array format: ['plugin']
128-
pluginsNode.elements.unshift(t.stringLiteral(zeroUiPlugin));
157+
if (tailwindIndex !== -1) {
158+
pluginsNode.elements.splice(tailwindIndex, 0, t.stringLiteral(zeroUiPlugin));
159+
} else {
160+
pluginsNode.elements.unshift(t.stringLiteral(zeroUiPlugin));
161+
}
129162
return "added";
130163
}
131164
return "unsupported";
132165
}
133166

134-
function isZeroUiPostcssEntry(node: t.ArrayExpression["elements"][number], zeroUiPlugin: string): boolean {
167+
function isPluginArrayEntry(node: t.ArrayExpression["elements"][number], pluginName: string): boolean {
135168
if (!node) return false;
136-
if (t.isStringLiteral(node)) return node.value === zeroUiPlugin;
169+
if (t.isStringLiteral(node)) return node.value === pluginName;
137170
if (
138171
t.isCallExpression(node) &&
139172
t.isIdentifier(node.callee, { name: "require" }) &&
140173
node.arguments.length === 1 &&
141174
t.isStringLiteral(node.arguments[0])
142175
) {
143-
return node.arguments[0].value === zeroUiPlugin;
176+
return node.arguments[0].value === pluginName;
144177
}
145178
return false;
146179
}
147180

181+
function getPluginObjectKey(node: t.ObjectProperty): string | null {
182+
if (t.isStringLiteral(node.key)) return node.key.value;
183+
if (t.isIdentifier(node.key)) return node.key.name;
184+
return null;
185+
}
186+
148187
/**
149188
* Parse Vite config TypeScript/JavaScript file and add Zero-UI plugin
150189
* Removes tailwindcss plugin if present, and adds zeroUI plugin if missing

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,22 @@ test("patchPostcssConfig does not duplicate Zero-UI in array plugin configs", as
242242
});
243243
});
244244

245+
test("patchPostcssConfig reorders array plugin configs when Tailwind comes first", async () => {
246+
const original = `module.exports = {
247+
plugins: ["${TAIL}", "${ZERO}"],
248+
};`;
249+
250+
await runTest({ "postcss.config.js": original }, async () => {
251+
await patchPostcssConfig();
252+
253+
const updated = readFile("postcss.config.js");
254+
const zeroCount = updated.split(ZERO).length - 1;
255+
256+
assert.equal(zeroCount, 1, "Zero-UI plugin must not be duplicated");
257+
assert.ok(zeroBeforeTail(updated), "Zero-UI must precede Tailwind");
258+
});
259+
});
260+
245261
test("patchViteConfig creates defineConfig setup when no plugin array exists", async () => {
246262
await runTest(
247263
{

0 commit comments

Comments
 (0)