Skip to content

safelist cannot express a family: patterns are rejected, while blocklist accepts globs in adjacent code #26

Description

@glennmichael123

safelist accepts string literals only. Patterns are explicitly rejected — while blocklist, in adjacent code operating on the same config object, accepts glob patterns. The machinery exists on the exclusion side and was never built for the inclusion side.

The asymmetry

dist/chunk-jbn8cyfb.js:470 — safelist:

for (let G of $.safelist)
  if (typeof G === "string") q.add(G)
  else console.warn(`⚠️  Ignoring non-string safelist entry (patterns are not supported): ${JSON.stringify(G)}`)

dist/chunk-jbn8cyfb.js:452 — blocklist, same file, same config:

for (let U of $.blocklist)
  if (U.includes("*")) Z.push(new RegExp(`^${U.replace(/\*/g, ".*")}$`))
  else q.add(U)

Type surface confirms it: dist/types.d.ts:41safelist: string[].

Through the stx pipeline it degrades further: @stacksjs/stx/src/dev-server/crosswind.ts:705-707 does for (const cls of safelist) generator.generate(cls), and generate() string-parses its argument — so a RegExp there produces nothing at all, without even the warning above.

To be clear about what does work

Literal-string safelist entries work fine end-to-end. I verified safelist: ['z-[100]', 'max-w-[22rem]', 'flex-col-reverse', 'rotate-180'] all emit with zero matching markup, and stx explicitly merges user safelist with the base (dev-server/crosswind.ts:664-668). So this isn't "safelist is broken" — it's that safelist cannot express a family.

Why it matters

The case that needs a pattern is a colour family driven by data. Our app has status chips whose tone is computed at runtime across four colours and several shades. Written as a safelist that's ~40 literal entries covering the cross product of (bg|text|border) × (emerald|amber|red|blue) × (50|200|700|800), which then has to be maintained by hand every time a status is added.

We avoided it by moving the whole thing to data attributes plus hand-written preflight CSS — which works, but means those surfaces are no longer expressed in utilities at all, and their dark-mode variants had to be hand-written too. One pattern entry would have kept them as ordinary utility classes.

Ask

Accept string | RegExp | { pattern: RegExp, variants?: string[] }, matched against the generatable utility space rather than against scanned source:

safelist: [
  { pattern: /^(bg|text|border)-(emerald|amber|red|blue)-(50|200|700|800)$/, variants: ['dark', 'hover'] },
  'rotate-180',
]

This is Tailwind's safelist: [{ pattern, variants }] shape, which is what most people arriving here will already expect — and given the warning message says "patterns are not supported", the shape was clearly anticipated.

Whatever the outcome, the stx path (dev-server/crosswind.ts:705) should at least not swallow a non-string entry more quietly than crosswind itself does.

Environment

crosswind 0.2.15, consumed through @stacksjs/stx 0.2.153, Bun 1.3.1.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions