ProvenPin Install

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

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.