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:
| Check | Example | Default |
|---|---|---|
| Missing token or unknown name on a token-backed property | color: 'brand.foreground' | error |
A token() reference naming no token, anywhere in a value | border: '1px solid token(colors.nope)' | error |
A token() or token.value() call naming no token | color: token('colors.nope') | error |
| Bare name rejected by CSS grammar on a property without a token category | display: '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.prunedecides which reset rules to keep from a textual scan of your own source, so an element rendered by a dependency, bydangerouslySetInnerHTMLor 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 whatincludecovers, which scopes style extraction rather than everything that may import — a script, a config, or a sibling workspace package that callstoken()is not covered. Name what they reach withprune.keepTokens. - A binding renamed away from
token.const t = tokenis invisible to the scan that decides whether a token is reachable from JavaScript. - An entry template outside
include.includeconventionally covers components rather than markup, andindex.htmlis where<table>,<noscript>and a page's static markup usually live. The scans read any fileincludecovers, 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.