concepts
build diagnostics

Build Diagnostics

What Bamboo refuses to ship broken, and what it can only warn you about.

Static extraction has one characteristic failure, and it is silence.

Bamboo reads your source, finds the styles, and emits a rule for each one. When it cannot — a file it could not parse, a call to a pattern that no longer exists, a value naming a token with a typo in it — it emits nothing. The stylesheet that results is still a valid stylesheet. Nothing downstream objects, because there is nothing to object to: a missing rule looks exactly like a rule you never asked for.

What ships is a component asking for a class with nothing behind it. It surfaces as "this colour never applied", weeks later, a long way from the edit that caused it, and the only reliable way to find it is to diff the selector sets of two builds.

So the checks below exist. Most of them fail the build outright; a few can only warn, and the difference is always the same question — can this be decided, or is it inferred from the shape of your code.

What fails the build

These are read off something the build knows for certain, so there is no setting to grade them. There is no configuration under which the answer is what you meant.

A file the build could not read

ERR_BAMBOO_EXTRACT_FAILED

Extraction threw. The file's styles never reach the encoder, so every rule it would have contributed is absent and every class its components ask for has nothing behind it.

ERR_BAMBOO_EXTRACT_FAILED: 2 file(s) could not be extracted:

src/checkout/summary.tsx
  Unexpected token (14:22)

src/checkout/total.tsx
  `colors.brand.500` was renamed to `colors.brand.primary`

One pass names every broken file rather than stopping at the first, so a config change that breaks six components is one fix rather than six builds. The original errors are on the thrown error's cause as an AggregateError — the codes underneath are what tells a retired token spelling from a syntax error.

Deleting the file, or taking it out of include, is a fix and is treated as one.

A call to a binding that does not exist

ERR_BAMBOO_DEAD_IMPORT

The patterns and recipes entrypoints are generated from your config, so what they export moves when the config does — a pattern dropped from a preset, a recipe renamed. The import survives that as a binding to nothing: nothing extracts the call, and every rule it would have contributed is gone.

ERR_BAMBOO_DEAD_IMPORT: 400 call(s) name a binding that does not exist:

`stack` is not a pattern — `../styled-system/patterns` does not export it.
  400 file(s): src/comp-0.tsx, src/comp-1.tsx, src/comp-10.tsx, … and 397 more

Removing one pattern took eleven selectors out of a release this way, along with every modal's spacing and width. Codegen printed four ticks and exited 0.

Grouped by the binding, because that is the unit of the mistake — a pattern dropped from a preset and called across an app is one thing to fix, however many call sites reach it. Two distinct dead bindings stay two findings.

Reported per call, not per import. Both entrypoints export types beside their functions — FlexProperties, ButtonVariantProps — and neither is a pattern or a recipe, so an import-only test would report every file that types a prop. A type is never called, which is the difference. A binding nobody calls is left alone for the same reason: nothing asked it for a class, so no rule is missing.

A nested key that is neither a property nor a condition

Reported by the Vite compiler as unknown-condition

A key nested in a style object is either the property a declaration sets or a condition it applies under: one from your config, a selector with & in it, or an at-rule. A key that is neither — _hovr, or svg written without its & — has no rule anywhere. The class the element gets keeps the key, while the stylesheet drops it and writes the declaration with no condition at all, so the two never meet.

bamboocss: 2 call(s) could not be compiled.

  src/button.tsx
    12: css() — unknown-condition: `_hovr` is not a condition; did you mean `_hover`?
    18: css() — unknown-condition: `has` is not a condition; did you mean `'&:has(svg)'`?

A compiled class with no rule

Reported by the Vite compiler as it writes the stylesheet

The compiler and the stylesheet pass read a module separately: the compiler reads what Vite hands it, the stylesheet pass reads the source — and a single-file component through a conversion of it. A class the compiler emitted for a call the stylesheet pass never saw has no rule, and an element carrying it renders unstyled. The build names the class and the file whose call produced it:

bamboocss: 1 compiled class(es) still named by live output have no rule in the candidate stylesheet. Elements carrying them would render unstyled.

  c_red.300
      (NOT extracted, though src/Card.svelte compiled a call to it; no rule in the sheet)

The two reading a file differently is a Bamboo bug to report with the file. Until it is fixed, moving the call into the component's <script> works around it. A class passed through cx() as written is somebody else's and is not checked.

A class name the build and the browser would spell differently

ERR_BAMBOO_NAMING_DISAGREEMENT

Bamboo checks that the encoder and class-name generation agree for the resolved config. The Vite compiler writes those names into application code; generated style calls do not run in the browser. A mismatch would leave compiled classes without matching CSS rules, so this check fails the build.

Native extraction is unavailable

ERR_BAMBOO_NATIVE_EXTRACTION

Stylesheet extraction requires the Rust/Oxc binary bundled with @bamboocss/node. Check the build platform requirements against the machine running Bamboo. Alpine Linux/musl and Windows arm64 do not have bundled binaries; use a supported build environment.

On a supported platform, a load error can mean the installed package is incomplete or its binary cannot load. Reinstall from your lockfile on the build machine and inspect the original error in cause. In a Bamboo source checkout, build @bamboocss/native-extractor with Cargo through pnpm --filter=@bamboocss/native-extractor build-fast.

There is no TypeScript fallback or environment-variable opt-out. This code also reports native analysis failures; use the error message and cause to distinguish a load failure from an extraction failure.

What you can grade

These are inferred rather than decided, so each has a setting. The default is the one that is right when the inference might be wrong about your code; escalating is a choice you make once you know your own source is clean.

A value naming a token that does not exist

unresolvedToken defaults to { token: 'error', grammar: 'warn' }. The two checks have independent severities:

CheckExampleDefault
Missing token or unknown name on a token-backed propertycolor: 'brand.foreground'error
A token() reference naming no token, anywhere in a valueborder: '1px solid token(colors.nope)'error
A token() or token.value() call naming no tokencolor: token('colors.nope')error
Bare name rejected by CSS grammar on a property without a token categorydisplay: 'flexx'warn

A missing token fails the build:

ERR_BAMBOO_UNRESOLVED_TOKEN: 2 style value(s) name a token that does not exist:

- `color: brand.foreground` — no such `colors` token. It is emitted as written, and the browser will drop it. Write `[brand.foreground]` to mean it literally.
- `border: token(colors.nope)` — no such token. It reaches the css as text instead of a variable, so the declaration never takes a token's value. Check the path, or add the token to your theme.

Set either half to off, warn, or error, or pass one severity to apply it to both. At warn the build carries on; at error the reported list is capped at 25. Both inspect styles from calls, staticCss, global CSS, config recipes, and mixins. Lowering the token severity can emit the unresolved value as written, leaving a declaration the browser drops.

A call naming no token is listed in the same error, by the path it names and where it is called. It returns undefined, so the property it was written into is dropped rather than emitted: nothing in the stylesheet shows it, and the style simply never applies. A call whose result is tested or defaulted, token('colors.brand') ?? '#fff', is not reported.

1 `token()` call(s) name a token that does not exist:

- `token('colors.nope')` names no token. It returns `undefined`, so whatever reads it gets nothing: in a style, the property is dropped. Check the path, or add the token to your theme. At src/card.tsx:12:19.

The grammar check defaults to warn because its data can lag CSS support. [accent.default] explicitly marks a literal and bypasses token checking; the finished-sheet invalidDeclaration check can still report invalid CSS.

A declaration the browser would drop

invalidDeclaration — off | warn | error, default warn

The other end of the pipeline from the token check. That one reads the values your source asked for; this one reads what the finished sheet contains, after every utility transform, mixin and recipe has had its say — which is the only place a transform that handed a value through unchanged, or a […] literal that was never valid CSS, can be seen at all.

1 declaration(s) in the stylesheet are not valid CSS for their property:

- `background-image: 65deg` in `.bg-linear_65deg`, `@layer utilities`

Every declaration is matched against its property's value grammar. One the grammar knows and rejects is reported; one it has never heard of is left alone, and so is any value reading var(), env(), attr() or if(), because what the declaration will hold is not in the text.

The default is warn because the grammar's data lags the spec — display: masonry is valid CSS it has not caught up with — so a build is never failed by how fresh a grammar is until you say so.

A call the build could not fully see

The extraction pass reports unresolved values on the css and recipe warning channels, with a file, line, and property when available. These warnings describe what extraction could recover; they do not permit runtime styling. The Vite compiler separately rejects style-producing calls it cannot resolve to a finite set of classes.

Make the value statically recoverable, express finite choices as recipe variants, or put an open dynamic value in an inline style or CSS variable. Setting a recipe's className or requesting staticCss does not make an open runtime style call compilable. See dynamic styling.

Generated files under outdir are not reported as authored unresolved-style warnings.

A token path the prune scan could not follow

prune.unresolvedPath — off | warn | error, default off

Token pruning accounts for each token path individually. A path assembled at runtime cannot be followed, and an unfollowable one forces the keep set back onto every declaration — so this is what tells you the token layer is larger than it needs to be, and error guarantees you are shipping the exact set rather than the fallback.

Silent by default, because the fallback is an inference the build makes on its own rather than a claim you asked it to check. Turn it to warn when the token layer is bigger than you expect.

A config that does not validate

validation — off | warn | error, default warn

This grades opinions about a config that still builds. Two checks are not that and answer to nothing here: a retired token spelling, which is output that is already broken, and a removed option, which is proof the config predates the version reading it. Both throw at any setting.

What nothing can check

The honest limits, all of which have the same shape — the evidence is outside what the build can see.

  • preflight.prune decides which reset rules to keep from a textual scan of your own source, so an element rendered by a dependency, by dangerouslySetInnerHTML or by markdown is invisible to it. This is why it is the one pruning option that is off by default and cannot be made safe by default.
  • A caller outside include. The token scans read what include covers, which scopes style extraction rather than everything that may import — a script, a config, or a sibling workspace package that calls token() is not covered. Name what they reach with prune.keepTokens.
  • A binding renamed away from token. const t = token is invisible to the scan that decides whether a token is reachable from JavaScript.
  • An entry template outside include. include conventionally covers components rather than markup, and index.html is where <table>, <noscript> and a page's static markup usually live. The scans read any file include covers, not only ones the parser understands, so listing it is the fix.

A prune.keepTokens pattern that matches no token in your theme is reported, because keeping nothing is otherwise silent in a pass whose whole job is dropping things.