Files
cloudflare__vinext/tests/postcss-resolve.test.ts
T
2026-02-24 09:29:39 -06:00

311 lines
10 KiB
TypeScript

import { describe, it, expect, beforeAll, afterAll } from "vitest";
import path from "node:path";
import os from "node:os";
describe("resolvePostcssStringPlugins", () => {
let resolvePostcssStringPlugins: typeof import("../packages/vinext/src/index.js")["_resolvePostcssStringPlugins"];
beforeAll(async () => {
const mod = await import("../packages/vinext/src/index.js");
resolvePostcssStringPlugins = mod._resolvePostcssStringPlugins;
});
/**
* Creates a temporary project directory with a mock PostCSS plugin
* and a postcss config file. Returns the temp dir path.
*/
async function createTmpProject(
configFileName: string,
configContent: string,
opts?: { mockPluginContent?: string },
): Promise<string> {
const fsp = await import("node:fs/promises");
const dir = await fsp.mkdtemp(path.join(os.tmpdir(), "vinext-postcss-"));
// Create a mock PostCSS plugin that the config can reference
const pluginDir = path.join(dir, "node_modules", "mock-postcss-plugin");
await fsp.mkdir(pluginDir, { recursive: true });
const pluginContent = opts?.mockPluginContent ?? `
module.exports = function mockPlugin(opts) {
return {
postcssPlugin: "mock-postcss-plugin",
Once(root) {},
};
};
module.exports.postcss = true;
`;
await fsp.writeFile(path.join(pluginDir, "index.js"), pluginContent);
await fsp.writeFile(
path.join(pluginDir, "package.json"),
JSON.stringify({ name: "mock-postcss-plugin", version: "1.0.0", main: "index.js" }),
);
// Write the PostCSS config file
await fsp.writeFile(path.join(dir, configFileName), configContent);
return dir;
}
async function cleanupDir(dir: string) {
const fsp = await import("node:fs/promises");
await fsp.rm(dir, { recursive: true, force: true }).catch(() => {});
}
// --- No config file ---
it("returns undefined when no PostCSS config exists", async () => {
const fsp = await import("node:fs/promises");
const dir = await fsp.mkdtemp(path.join(os.tmpdir(), "vinext-postcss-none-"));
try {
const result = await resolvePostcssStringPlugins(dir);
expect(result).toBeUndefined();
} finally {
await cleanupDir(dir);
}
});
// --- Array form with string plugin ---
it("resolves string plugin names in array-form postcss.config.cjs", async () => {
const dir = await createTmpProject(
"postcss.config.cjs",
`module.exports = { plugins: ["mock-postcss-plugin"] };`,
);
try {
const result = await resolvePostcssStringPlugins(dir);
expect(result).toBeDefined();
expect(result!.plugins).toHaveLength(1);
// The resolved plugin should be an object (PostCSS plugin instance)
const plugin = result!.plugins[0];
expect(plugin).toBeDefined();
expect(typeof plugin === "object" || typeof plugin === "function").toBe(true);
} finally {
await cleanupDir(dir);
}
});
it("resolves string plugin names in array-form postcss.config.mjs", async () => {
const dir = await createTmpProject(
"postcss.config.mjs",
`export default { plugins: ["mock-postcss-plugin"] };`,
);
try {
const result = await resolvePostcssStringPlugins(dir);
expect(result).toBeDefined();
expect(result!.plugins).toHaveLength(1);
} finally {
await cleanupDir(dir);
}
});
// --- Object form (should be skipped) ---
it("returns undefined for object-form plugins (postcss-load-config handles these)", async () => {
const dir = await createTmpProject(
"postcss.config.cjs",
`module.exports = { plugins: { "mock-postcss-plugin": {} } };`,
);
try {
const result = await resolvePostcssStringPlugins(dir);
expect(result).toBeUndefined();
} finally {
await cleanupDir(dir);
}
});
// --- Array form without string entries (already resolved) ---
it("returns undefined when array plugins contain no strings", async () => {
const dir = await createTmpProject(
"postcss.config.cjs",
`const plugin = require("mock-postcss-plugin");
module.exports = { plugins: [plugin()] };`,
);
try {
const result = await resolvePostcssStringPlugins(dir);
expect(result).toBeUndefined();
} finally {
await cleanupDir(dir);
}
});
// --- Array tuple form: ["plugin-name", { options }] ---
it("resolves array tuple form [name, options]", async () => {
const dir = await createTmpProject(
"postcss.config.cjs",
`module.exports = { plugins: [["mock-postcss-plugin", { foo: "bar" }]] };`,
);
try {
const result = await resolvePostcssStringPlugins(dir);
expect(result).toBeDefined();
expect(result!.plugins).toHaveLength(1);
} finally {
await cleanupDir(dir);
}
});
// --- Mixed array with strings and already-resolved plugins ---
it("handles mixed array with strings and pre-resolved plugins", async () => {
const dir = await createTmpProject(
"postcss.config.cjs",
`const plugin = require("mock-postcss-plugin");
module.exports = { plugins: ["mock-postcss-plugin", plugin()] };`,
);
try {
const result = await resolvePostcssStringPlugins(dir);
expect(result).toBeDefined();
expect(result!.plugins).toHaveLength(2);
} finally {
await cleanupDir(dir);
}
});
// --- Plugin that exports a default function ---
it("calls default export function for string plugin", async () => {
const dir = await createTmpProject(
"postcss.config.cjs",
`module.exports = { plugins: ["mock-postcss-plugin"] };`,
{
mockPluginContent: `
module.exports = function(opts) {
return {
postcssPlugin: "mock-plugin",
Once(root) {},
};
};
module.exports.postcss = true;
`,
},
);
try {
const result = await resolvePostcssStringPlugins(dir);
expect(result).toBeDefined();
expect(result!.plugins).toHaveLength(1);
// Should be the result of calling the function (a plugin object)
expect(result!.plugins[0]).toHaveProperty("postcssPlugin", "mock-plugin");
} finally {
await cleanupDir(dir);
}
});
// --- JSON/YAML configs are skipped ---
it("returns undefined for .postcssrc.json (postcss-load-config handles these)", async () => {
const fsp = await import("node:fs/promises");
const dir = await fsp.mkdtemp(path.join(os.tmpdir(), "vinext-postcss-json-"));
await fsp.writeFile(
path.join(dir, ".postcssrc.json"),
JSON.stringify({ plugins: { autoprefixer: {} } }),
);
try {
const result = await resolvePostcssStringPlugins(dir);
expect(result).toBeUndefined();
} finally {
await cleanupDir(dir);
}
});
// --- Config file priority ---
it("picks postcss.config.js over .postcssrc", async () => {
const fsp = await import("node:fs/promises");
const dir = await createTmpProject(
"postcss.config.js",
`module.exports = { plugins: ["mock-postcss-plugin"] };`,
);
// Also create a .postcssrc with object form
await fsp.writeFile(
path.join(dir, ".postcssrc"),
JSON.stringify({ plugins: { autoprefixer: {} } }),
);
try {
const result = await resolvePostcssStringPlugins(dir);
// postcss.config.js has array-form with string — should resolve
expect(result).toBeDefined();
expect(result!.plugins).toHaveLength(1);
} finally {
await cleanupDir(dir);
}
});
});
// ---------------------------------------------------------------------------
// Integration test: Vite config hook injects resolved PostCSS
// ---------------------------------------------------------------------------
describe("PostCSS string plugin resolution in Vite config", () => {
let tmpDir: string;
afterAll(async () => {
if (tmpDir) {
const fsp = await import("node:fs/promises");
await fsp.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
}
});
it("injects resolved PostCSS plugins into Vite css.postcss config", async () => {
const fsp = await import("node:fs/promises");
tmpDir = await fsp.mkdtemp(path.join(os.tmpdir(), "vinext-postcss-int-"));
// Symlink node_modules from project root
const rootNodeModules = path.resolve(import.meta.dirname, "../node_modules");
await fsp.symlink(rootNodeModules, path.join(tmpDir, "node_modules"), "junction");
// Create a mock PostCSS plugin in node_modules
const pluginDir = path.join(tmpDir, "node_modules", "mock-postcss-plugin");
await fsp.mkdir(pluginDir, { recursive: true });
await fsp.writeFile(
path.join(pluginDir, "index.js"),
`module.exports = function(opts) {
return { postcssPlugin: "mock-postcss-plugin", Once(root) {} };
};
module.exports.postcss = true;`,
);
await fsp.writeFile(
path.join(pluginDir, "package.json"),
JSON.stringify({ name: "mock-postcss-plugin", version: "1.0.0", main: "index.js" }),
);
// PostCSS config with array-form string plugin
await fsp.writeFile(
path.join(tmpDir, "postcss.config.cjs"),
`module.exports = { plugins: ["mock-postcss-plugin"] };`,
);
// Minimal pages directory
await fsp.mkdir(path.join(tmpDir, "pages"), { recursive: true });
await fsp.writeFile(
path.join(tmpDir, "pages", "index.tsx"),
`export default function Home() { return <h1>Home</h1>; }`,
);
// Create a Vite server to test the resolved config
const { createServer } = await import("vite");
const vinext = (await import("../packages/vinext/src/index.js")).default;
const server = await createServer({
root: tmpDir,
configFile: false,
plugins: [vinext()],
server: { port: 0 },
logLevel: "silent",
});
try {
// Check the resolved config has css.postcss set
const postcssConfig = server.config.css?.postcss;
expect(postcssConfig).toBeDefined();
expect(typeof postcssConfig).toBe("object");
const postcssObj = postcssConfig as { plugins: any[] };
expect(postcssObj.plugins).toHaveLength(1);
expect(postcssObj.plugins[0]).toHaveProperty("postcssPlugin", "mock-postcss-plugin");
} finally {
await server.close();
}
}, 30000);
});