zfb
GitHub repository

Type to search...

to open search from anywhere

Sources and candidates

See how the source plan finds complete candidates and how explicit inputs are validated.

Make candidates discoverable

Source discovery and generated rules here belong to zudo-wind. The extractor reads files without running the app, so keep each possible class complete in source:

function ActionButton({ active }: { active: boolean }) {
  const buttonClass = active
    ? "bg-accent text-white"
    : "bg-muted text-white";

  return <button className={buttonClass}>Continue</button>;
}

A finite conditional exposes both complete alternatives. A runtime prefix joined to a value does not form a candidate. Put a complete app-owned candidate that cannot appear in source in an owner-keyed wind.safelist.

NeedUse
App classes written in sourceBuild or standalone source plan
Finite app candidate absent from scanned fileswind.safelist
Classes shipped by a packageVersioned manifest in wind.manifests
Paths that should stay outwind.sources.exclude
Package directory containing class-bearing fileswind.sources.packageRoots

Generate a package stylesheet and candidate manifest from the package directory:

zfb css --input src/styles.css --output dist/ui.css \
  --config wind.config.json --no-auto-source --source 'src/**/*.tsx'
zfb wind manifest --producer ui --output dist/wind.json \
  --config wind.config.json --no-auto-source --source 'src/**/*.tsx'
zfb wind audit --plan build

The consumer declares producer ui and supplies every token used by the manifest. This sample compiles two complete candidates with configured colors and spacing:

See complete candidates resolve
HTML
<div class="wind-demo-status"><p class="bg-accent text-white p-2">Ready</p><p class="bg-muted text-white p-2">Queued</p></div>
CSS
@layer zw-reset, zw-tokens, zfb-hi, base, components;
@layer zw-tokens {
  :root {
    --zw-color-accent: #155eef;
    --zw-color-muted: #475467;
    --zw-color-white: #fff;
    --zw-spacing-unit: .25rem;
  }
}
.wind-demo-status {
  max-width: 18rem;
}

.wind-demo-status p {
  margin: 0 0 .5rem;
}
.p-2 {
  padding-top: 0.5rem;
  padding-right: 0.5rem;
  padding-bottom: 0.5rem;
  padding-left: 0.5rem;
}
.text-white {
  color: var(--zw-color-white);
}
.bg-accent {
  background-color: var(--zw-color-accent);
}
.bg-muted {
  background-color: var(--zw-color-muted);
}
Configuration for this example
{
  "wind": {
    "spec": 1,
    "reset": "none",
    "strict": true,
    "tokens": {
      "spacingUnit": "0.25rem",
      "colors": {
        "accent": "#155eef",
        "muted": "#475467",
        "white": "#ffffff"
      }
    },
    "breakpoints": {},
    "defaultTransitionTimingFunction": "ease",
    "authoredClasses": {
      "wind-demo-status": true
    },
    "safelist": {
      "docs-preview": [
        "bg-accent",
        "bg-muted",
        "p-2",
        "text-white"
      ]
    }
  }
}
Authored demonstration CSS
.wind-demo-status { max-width: 18rem; }
.wind-demo-status p { margin: 0 0 0.5rem; }

An explicit glob whose every match is excluded fails with ZW010. If some matches remain, zfb warns and scans those files. These records show the exact all-excluded and partial-exclusion source plans:

<p class="p-4">Excluded source</p>

sample.html and excluded.html contain the HTML above. The configuration excludes excluded.html.

zfb css --input input.css --output compiled.css --config zfb.config.json --project-root . --source 'excluded.html' --no-auto-source --no-default-highlight-styles

Expected diagnostics: ZW010 (error)

Configuration for this example
{
  "wind": {
    "spec": 1,
    "reset": "none",
    "strict": true,
    "tokens": {
      "spacingUnit": "0.25rem"
    },
    "breakpoints": {},
    "defaultTransitionTimingFunction": "ease",
    "authoredClasses": {},
    "safelist": {},
    "sources": {
      "exclude": [
        "excluded.html"
      ]
    }
  }
}
Authored demonstration CSS
<p class="p-4">Accepted source</p>

sample.html and excluded.html contain the HTML above. The configuration excludes excluded.html.

zfb css --input input.css --output compiled.css --config zfb.config.json --project-root . --source '*.html' --no-auto-source --no-default-highlight-styles

Expected diagnostics: ZW010 (warning)

Configuration for this example
{
  "wind": {
    "spec": 1,
    "reset": "none",
    "strict": true,
    "tokens": {
      "spacingUnit": "0.25rem"
    },
    "breakpoints": {},
    "defaultTransitionTimingFunction": "ease",
    "authoredClasses": {},
    "safelist": {},
    "sources": {
      "exclude": [
        "excluded.html"
      ]
    }
  }
}
Authored demonstration CSS

The extractor does not execute application code or infer classes from runtime values.

Technical reference

The source plan

The source plan determines every input scanned by build and dev. It can include project conventional roots, declared source roots, declared package roots, mirror roots, explicit command-line sources, generated sources, and candidate manifests. Paths are resolved relative to the project, package, preset, or declaring config that supplied them, not the process working directory.

When pnpm-workspace.yaml explicitly claims ., build and dev include the workspace root package as an implicit Wind source by default. Set wind.sources.rootPackage: false to skip only that implicit Wind source; project roots, imported sources, explicit roots, package roots, manifests, and safelists keep their own discovery. true restores the implicit source. Omission inherits a preset value when one is configured, and otherwise defaults to true. This setting does not change separate CSS module/import discovery or its watches. The standalone plan stays project-scoped.

zfb css and the default zfb wind audit use the standalone plan: the conventional roots, configured wind.sources.roots, package roots and exclusions, manifests, and explicit --source declarations. Build and dev add package-owned routes, sibling mirror roots, and plugin virtual modules. zfb wind audit --plan build runs that same discovery, so its inventory matches what zfb build compiles. Both audit modes print the roots and exclusions they cover before the report.

Build and dev scan these case-sensitive extensions: .tsx, .ts, .jsx, .js, .mdx, and .md. The standalone zfb css --source path can also process .html and .mjs; these two extensions are not automatically scanned in build or dev. There is no public extension override.

wind.sources.roots adds directories strictly below the declaring project or preset. They obey gitignore, standard skipped directories, and wind.sources.exclude; packageRoots remains a separate package traversal channel. Ordinary .ts and .test.ts files are candidates; .d.ts is always excluded and audit names excluded **/*.d.ts (declaration files).

What the extractor reads

The extractor looks for complete candidate strings in JSX and HTML class or className attributes, literal maps, script string and template literals, and raw HTML or JSX attributes in Markdown and MDX. It also scans nested literals inside interpolations and literal strings passed to class-list APIs. It decodes source-language escapes and HTML entities while retaining source positions for diagnostics.

Comments, Markdown prose, inline code, and fenced code are ignored. A script string containing HTML is inspected for embedded class attributes. Reading a string does not execute the application.

Candidates must be complete tokens. A finite map provides known alternatives:

const stateClasses = ["text-muted", "text-accent"];
const className = stateClasses[active ? 1 : 0];

Both text-muted and text-accent are present as complete literals and can be extracted independently; whether they resolve as utilities still depends on configuration. Concatenating a prefix such as bg- with a runtime value does not form a candidate. A partial fragment such as bg- or -500 is never expanded, so a dynamically constructed class is never covered. Use a literal map or an explicit candidate source for a finite set of possibilities.

At an interpolation boundary, a static token remains eligible only if it is complete on its own. For example, a full token followed by an interpolation may be retained and marked as adjacent to dynamic text; text on either side is never joined to create a token.

Configure wind.sources.ignoreAttributes for nonclass prop names such as css, html, js, displayJs, and title. It augments built-in href, src, content, name, rel, and type, excluding the entire prop value, even nested markup, interpolations, and class-helper calls. Class attributes elsewhere remain scanned. Speculative literals discard punctuation noise outside balanced arbitrary CSS forms but keep Unicode and punctuation inside those forms; embedded markup contributes its class attributes once. No query-selector or script-text heuristic is applied.

Class positions and confidence

A candidate's position decides how strictly it is checked. Text written directly in a class or className attribute is a class position. So is a complete string whose value provably reaches such an attribute:

  • a branch of a conditional, or the operand after &&, ||, or ??;

  • a static part of a template literal used as a class value, including strings inside its interpolations;

  • an argument, array element, or object key of the class helpers clsx, cn, cx, classNames, and classnames;

  • the initializer of a same-module const referenced from a class expression, through aliases too.

const linkClass = "px-3 text-ink"; // class position: referenced below
const label = "ring-2"; // literal: never reaches a class attribute

export const Link = ({ active }) => (
  <a class={`flex ${active ? "border-accent" : "border-muted"}`}>
    <span class={linkClass}>{label}</span>
  </a>
);

A constant is traced only when it is declared exactly once with const and is never imported, redeclared, or reassigned. Comparison operands, arguments to other functions, member and index access, and concatenation are not followed. An eligible complete string in an unrelated constant or test title is a lower-confidence literal. Module specifiers and pure URL or relative-path literals are skipped rather than classified as candidates. An invalid or unsupported utility at a class position fails the build like a direct class; the same text in a literal is audit-only information.

wind.sources.classHelpers declares additional local class-list helper identifiers. Preset and project names merge deterministically without duplicates, alongside built-ins. Supported string/template, array, and object arguments contribute classes; arbitrary cva or twMerge semantics are not inferred. A value import of h from @takazudo/zfb/zudo-react, including a verified named-import alias, recognizes bare or quoted class and className keys only in its direct second-argument props object. Supported conditionals, templates, same-module constants, and configured helpers can feed those values. className recognition is for migration scanning only and does not make it renderer-valid. Arbitrary data objects, unrelated factories, type-only imports, shadowed bindings, labels, ternaries in unrelated positions, and props objects outside the second argument remain lower confidence. Invalid recognized candidates keep class-position errors in normal build and wind audit --fail-on error.

Adding explicit candidates

A safelist entry is an owner ID mapped to an array of full candidates. There is no regular-expression, brace-expansion, or interpolation syntax. Each listed entry must be a utility candidate or a supported marker, unless the exact full string appears in authoredClasses.

A package can publish a versioned JSON manifest. The consumer declares its path under a producer ID in wind.manifests; the manifest's producer must match that ID. Its shape is:

{
  "schemaVersion": 1,
  "specVersion": 1,
  "producer": "widgets",
  "candidates": ["group", "p-0", "hover:bg-panel"]
}

The schema version is separate from the language version. A manifest contains candidates only: it cannot define tokens, run code, or be found through ambient dependency discovery. The consumer must configure any tokens used by manifest candidates. Manifest paths resolve from their declaring config or package. Missing files, invalid JSON or schema, and unsupported source extensions are errors. Unknown or invalid explicit entries retain their candidate diagnostics and manifest index. Duplicate strings are deduplicated. If two declared origins claim the same producer, the conflict is an error rather than a traversal-order choice; replacing or removing a producer retracts its previous candidates.

Publishing candidates from a package

A package needs no site config or pages to ship styles. Keep the token configuration its classes are written against in an ordinary zfb config file, such as wind.config.json beside package.json, and pass it with --config; without a zfb.config.* file the package never looks like a site. The config file's directory is the default project root, so these commands run from the package directory:

zfb css --input src/styles.css --output dist/ui.css \
  --config wind.config.json --no-auto-source --source 'src/**/*.tsx'
zfb wind manifest --producer ui --output dist/wind.json \
  --config wind.config.json --no-auto-source --source 'src/**/*.tsx'

zfb wind manifest runs the build's extractor and compiler over every source in one process, writes only resolved utilities and markers, and fails without touching dist/wind.json when a class-position candidate does not resolve. The configured outDir and the --output path stay out of the scan, so a previous dist/ build never feeds the next manifest. Export both files from package.json, for example "./ui.css": "./dist/ui.css" and "./wind.json": "./dist/wind.json".

A consumer declares the manifest in wind.manifests and configures every token its candidates use. A preset is the simplest way to ship both: definePreset("@example/ui", { wind: { tokens, manifests: { ui: { path: "./dist/wind.json" } } } }) resolves ./dist/wind.json from the installed preset package, wherever the consumer installed it. A consumer without those tokens fails its own build on the first unresolved manifest entry, so treat the token set as part of the package's public contract. See zfb wind manifest.

Authored classes

Use authoredClasses to reserve exact complete candidate spellings for rules you write in CSS. Each key maps to true; include a variant chain when the whole variant-bearing candidate should stay authored. A match suppresses utility validation, emission, generated CSS, and candidate diagnostics at every origin. This lets a class remain in markup for an authored selector without being interpreted as a utility.

The allowlist is exact: it does not accept prefixes, patterns, or partial names. It cannot suppress invalid configuration or a leftover directive in a stylesheet.

Exclusions and updates

Previous output is never scanned. Three kinds of exclusion apply, with different reach:

  • Output and scratch paths. The configured outDir (dist by default), the zfb css --output file, and zfb's own scratch and cache directories are excluded from every root, including explicit --source files and package roots. Nothing can re-include them. With a different outDir, explicit --source files under a dist that is not the project's output are scanned.

  • Skipped directory names. While walking a directory root, subdirectories named node_modules, dist, build, target, .turbo, .next, .vercel, .zfb-build, or .zfb are skipped, as are hidden directories such as .git and .cache. This prunes the walk only: a root that is itself such a directory, or a file an explicit --source glob matched, is still scanned, so --source 'build/**/*.tsx' reads those files. A declared package root also walks its own node_modules and dist.

  • Ignore files. Gitignore rules apply under each root except a declared package root, which is walked without them because projects commonly ignore exactly the node_modules and dist it must reach.

Directory symlinks are not followed. Author exclusions from wind.sources.exclude remove matching files under their declaring root from every root, including explicit --source files; they cannot re-include anything excluded above. Package roots are declared with wind.sources.packageRoots; see Configuration. An explicit --source declaration whose matches are all excluded fails with ZW010; one with both excluded and accepted matches warns and scans the accepted files.

The source index tracks which owners supplied each candidate. Additions, removals, renames, manifest changes, and source-plan changes replace the relevant owner's set, so removing a file or declaration removes its candidates. No removed candidate is kept as a safety net. A missing conventional root is skipped; a required root, missing manifest, or explicit CLI glob with no matches is an error.

See also

Revision History

CreatedUpdated