PostCSS custom plugin plus autoprefixer with pinned browsers: working versions (tested 2026-10-15)
Write a PostCSS 8 plugin that resolves design tokens, converts px to rem and strips @debug rules, chain it with autoprefixer using a fixed browser list, and check prefixes, source maps and CssSyntaxError positions.
passed4 passed · 0 failed2026-10-15 20:09 UTC- Status
- passed
- Last passed
- 2026-10-15 20:09 UTC
- Tests
- 4 / 4
- Language
- TypeScript
- Runtime
- node v22.23.3
pinned versions4
postcss@8.5.30autoprefixer@10.6.1@types/node@26.6.5tsx@4.23.15
run it
npm install --no-audit --no-fund --ignore-scripts=false npm test
test environment
node v22.23.3 linux 6.17.0-1022-azure x64 runner: github-actions 38799498972
The code
Exactly the files that passed. Your agent can pull them with get_recipe("postcss-custom-plugin-autoprefixer") (install), or see them on GitHub.
package.json
{
"name": "postcss-custom-plugin-autoprefixer",
"private": true,
"type": "module",
"scripts": {
"test": "node --import tsx --test test/*.test.ts"
},
"dependencies": {
"postcss": "8.5.30",
"autoprefixer": "10.6.1"
},
"devDependencies": {
"@types/node": "26.6.5",
"tsx": "4.23.15"
}
}
src/css.ts
import postcss, { type PluginCreator } from "postcss";
import autoprefixer from "autoprefixer";
/** Custom plugin: replace var-like tokens `theme(name)` with design tokens and convert px to rem. */
export const tokens: PluginCreator<{ tokens: Record<string, string>; rootPx?: number }> = (opts = { tokens: {} }) => {
const root = opts.rootPx ?? 16;
return {
postcssPlugin: "provenpin-tokens",
Declaration(decl) {
decl.value = decl.value.replace(/theme\(([\w-]+)\)/g, (_, name: string) => {
const v = opts.tokens[name];
if (v === undefined) throw decl.error(`Unknown token ${name}`, { word: name });
return v;
});
if (decl.prop.startsWith("font-size") || decl.prop.startsWith("padding") || decl.prop.startsWith("margin")) {
decl.value = decl.value.replace(/(\d*\.?\d+)px/g, (_, n: string) => `${+(Number(n) / root).toFixed(4)}rem`);
}
},
AtRule: {
// @debug rules are stripped from production output
debug(at) { at.remove(); },
},
};
};
tokens.postcss = true;
// Pin browsers explicitly so output never depends on the machine's browserslist or caniuse-lite age guesses.
export const BROWSERS = ["Safari 12", "Firefox 60", "Chrome 60", "ie 11"];
export async function build(css: string, t: Record<string, string>) {
const result = await postcss([tokens({ tokens: t }), autoprefixer({ overrideBrowserslist: BROWSERS, grid: "autoplace" })]).process(css, { from: "in.css", to: "out.css", map: { inline: false } });
return { css: result.css, map: result.map.toJSON(), warnings: result.warnings().map((w) => w.text) };
}
test/css.test.ts
import { test } from "node:test";
import assert from "node:assert/strict";
import { build } from "../src/css.ts";
const t = { brand: "#0b3d91", radius: "6px" };
test("custom plugin resolves tokens and converts px to rem", async () => {
const { css } = await build(".btn { color: theme(brand); border-radius: theme(radius); padding: 8px 12px; font-size: 14px; }", t);
assert.match(css, /color: #0b3d91/);
assert.match(css, /border-radius: 6px/);
assert.match(css, /padding: 0\.5rem 0\.75rem/);
assert.match(css, /font-size: 0\.875rem/);
});
test("autoprefixer adds prefixes for pinned browsers", async () => {
const { css } = await build(".a { user-select: none; } .g { display: grid; grid-template-columns: 1fr 1fr; } .s { position: sticky; }", t);
assert.match(css, /-webkit-user-select: none/);
assert.match(css, /-moz-user-select: none/);
assert.match(css, /-ms-user-select: none/);
assert.match(css, /display: -ms-grid/);
assert.match(css, /-ms-grid-columns: 1fr 1fr/);
assert.match(css, /position: -webkit-sticky/);
});
test("removes @debug at-rules and emits a source map", async () => {
const { css, map } = await build("@debug 'x';\n.a { margin: 4px; }", t);
assert.doesNotMatch(css, /@debug/);
assert.equal(map.version, 3);
assert.deepEqual(map.sources, ["in.css"]);
});
test("unknown token raises a CssSyntaxError with position", async () => {
await assert.rejects(build(".a {\n color: theme(nope);\n}", t), (e: Error & { line?: number; name: string }) => {
assert.equal(e.name, "CssSyntaxError");
assert.equal(e.line, 2);
assert.match(e.message, /Unknown token nope/);
return true;
});
});
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
~/.cursor/mcp.json
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }.vscode/mcp.json
{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }~/.codeium/windsurf/mcp_config.json
{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }MCP settings
{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.