feat: add pnpm probe for inspecting rendered HTML & CSS - #1324
Conversation
Answering "what CSS does this actually produce?" required hand-writing a
throwaway vitest spec, running it, reading the output and deleting the file.
Cube Cloud grew a `yarn probe` for exactly this (cube-2 #14024) and it already
imports its DOM helpers from this repo's `@cube-dev/ui-kit/probe` entry, so the
tool belongs here too — and this is where the config, the palette and the
components it inspects actually live. Orphaned `__screenshots__` dirs from
deleted `describe('probe')` specs are the local evidence of the old habit.
Same modes and flags as Cloud's, so a recipe carries between the two repos:
styles, tokens, render, globals. `render` reports the markup plus only the CSS
that snippet caused, by rendering `<Root>` empty, capturing, mounting and
subtracting. Measuring a wrapper div directly would have been simpler and
wrong: `<Root>` is the portal target, so a Dialog renders as its sibling —
measured that way its inline markup is 0 chars while the subtraction still
captures all 169 of its rules, reported separately under PORTALS.
`pnpm probe:browser` adds the four things jsdom cannot do — computed values,
geometry, pointer input, screenshots. Same Card, both tiers:
`var(--surface-2-color)` / `calc(3 * var(--gap))` under jsdom,
`rgb(248, 248, 249)` / `24px` in Chromium. It shares its Chromium and setup
with `pnpm test:browser`, which unlike Cloud's equivalent is wired into CI.
`@cube-dev/ui-kit` is aliased to `src`, so a snippet reads like consumer code
and the same snippet runs unchanged through Cloud's probe.
Two deliberate divergences from Cloud's implementation:
- `globals` reads `getCSSText()`, not the per-node baseline. Console-ui hands
its palette to `<Root>` through a tasty `tokens` prop, so the token block is
attributed to that node and a per-node dump carries all ~119KB of it. Here
`GlobalStyles` injects it through `useGlobalStyles` instead, so it lives on a
global sheet no per-node dump can see: the node-scoped answer was 3 rules and
995 bytes, which reads as "`<Root>` barely styles anything". It now reports
all 82 rules and prints how many of them `render` actually subtracts.
- The browser tier imports the snippet by URL, not by filesystem path.
Chromium can only fetch what Vite serves, so an absolute path is requested as
`http://localhost:PORT/Users/…` and 404s — reported as "Snippet failed to
compile" for a snippet that compiles fine. The input carries both addresses.
Notes:
- Harness files are named `*.probe.tsx` so they cannot match vitest's default
include; `vitest list` confirms neither `pnpm test` nor `pnpm test:browser`
collects them, and `dist/` carries no `test/probe` after a build.
- The browser project DELETES the base project's `include` before merging.
`mergeConfig` concatenates arrays, so merging over it leaves both globs in
place and every browser spec in the repo would run on each probe.
- `assertConfigApplied()` fails the run if `<Root>`'s module body did not take
effect. `configure()` no-ops once any style has been generated, and that
failure has no other symptom — units, recipes and presets silently go
unresolved while the output still looks authoritative. The check is
end-to-end (`1x` must resolve to `var(--gap)`) rather than a config-key
lookup, so it asserts the thing the probe promises.
- `--computed` / `--rect` / `--screenshot` are rejected on the jsdom tier
rather than silently ignored: asking for computed values and getting none
reads as "no styles applied", the opposite of the truth. `probe:browser`
likewise refuses every mode but `render`, instead of passing an undefined
snippet path into a dynamic import.
- `--hc` is new relative to Cloud. This is where palette work happens, and the
four-variant sweep the Glaze docs require needs the high-contrast variant
resolved flat, not only as a state map.
- The harness reads its input and writes its result through a file. Stdout is
not a usable channel: React warnings can quote the probed markup, so no
delimiter is safe against arbitrary snippet content.
- No changeset. Internal tooling and docs only — `src/probe/` is untouched and
the published output is byte-identical.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
📦 NPM canary releaseDeployed canary version 0.0.0-canary-fabe7b5. |
🧪 Storybook is successfully deployed!
|
🏋️ Size limit report
Compared against main at 8261652 — run 32034201743, 2026-08-17T13:16:27Z.Click here if you want to find out what is changed in this build |
Review round on #1324. Two findings from Cursor Bugbot, plus two more of the same shape found while checking them — all four are the failure mode this PR already rejects for `--computed`: a flag accepted, recorded, and then quietly dropped, so the answer looks like the answer to the question that was asked. - `--canonical` was applied by the jsdom harness and ignored by the browser one, which returned raw tasty hashes and `useId` counters. That defeats exactly the comparison the flag exists for, and a browser run is where you would diff one scheme or viewport against another. - The browser harness never ran `assertConfigApplied`. It matters more there, not less: an unresolved unit surfaces as a confident `rgb(...)` and a real pixel geometry on the tier trusted for real numbers, rather than as visibly missing output. Moved to `config-guard.ts` and shared, rather than copied. - `--scheme hc` could not express dark + high contrast, so the fourth palette variant was unreachable in a browser even though `tokens --scheme dark --hc` reports it. Contrast is a separate axis from schema, so it is now a separate parameter and composes: `--scheme dark --hc` computes rgb(0,0,0) on `#surface` and rgb(255,255,255) on `#surface-text`, matching the `@dark & @hc` entries in the state map. `--scheme hc` stays accepted as Cloud's spelling for light + high contrast. - An unknown `--scheme` reached `renderColorTokens`, missed the variant lookup and threw inside the token renderer — arriving as a vitest stack trace that reads like a harness bug rather than a typo in a flag. Validated in the CLI now, per tier. `--scheme` / `--hc` are also refused on modes with no scheme instead of being dropped: `styles` and `globals` report every scheme at once, and their state maps and `@media` blocks already ARE the per-scheme answer. Verified: `--canonical` now yields `tcls0 tcls1` in Chromium; dark+hc differs from dark (rgb(0,0,0) vs rgb(31,32,34)); every rejection path prints its own message and exits 1; and the shared guard was proved to actually fire by inverting its assertion and watching the browser run fail with it, not merely by checking the call was added.
|
Review round addressed in 932bed6. Both Bugbot findings were real and are fixed, and checking them turned up two more instances of the same shape — a flag accepted, recorded into the probe input, then quietly dropped, so the output looks like the answer to the question that was asked. That is precisely the failure this PR argues against for
The third one mattered more than it looks: Verification, rather than assertion:
Docs updated alongside ( Still no changeset, for the same reason as before: |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 3 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit b7b1828. Configure here.
… runs Second review round on #1324. Three more Bugbot findings, all real. **Hooks in a snippet threw (High).** Both harnesses called `module.default()` and mounted what came back, which runs the body outside React's render phase — so any hook threw "invalid hook call", including in the `export default function Snippet() { … }` form the CLI's own --help documents. `useState` in a snippet is not exotic: it is what probing a controlled input or a disclosure takes. React now renders `<Snippet />`. That also fixes a mislabel the same code caused. A throw from the snippet body was caught by the import's try/catch and reported as "Snippet failed to compile", sending you to look for a syntax error in code that had parsed perfectly well. The render is now its own try/catch reporting `kind: 'render'` — a branch the CLI's printer already had and could never reach. **Concurrent probes clobbered each other (Medium).** Only the result file was per-run; `input.json` and `snippet.tsx` were fixed paths and every run began by `rmSync`-ing the whole `.probe` tree. Two probes at once therefore deleted each other's input mid-flight or answered with the wrong snippet — and the docs explicitly invite probing freely, so this was reachable by following them. Everything is now under `.probe/<runId>/`. Tidiness comes from pruning directories older than an hour instead: a run takes seconds, so age cannot misfire on a live sibling, whereas the blanket delete was guaranteed to. **A flag missing its value read as "flag absent" (Medium).** `--computed` with no selector stored `undefined`, which every check downstream treats as not passed, so the run succeeded and looked like an answer to a computed-values question nobody had answered. `--computed --scheme dark` was worse: it took `--scheme` as the selector and silently dropped the scheme too. Values are now required, and a value that is itself a flag is refused by name. Verified: a `useState` snippet reports `count 3` on jsdom and `browser hook 7` plus real computed values in Chromium; a throwing snippet now says "failed to render"; bare JSX still works; two concurrent probes each got their own answer with zero cross-talk (previously impossible); a backdated run dir is pruned while live ones survive; and every new rejection prints its own message.
|
Second review round addressed in f1b1ff3. All three findings were real; the High one was the best catch so far.
Two things worth pulling out, because they made the findings bigger than they looked: The hooks bug broke a form the tool documents. The concurrency bug was reachable by following the docs, which is what moved it off my "unlikely in practice" pile: this doc tells you to probe as freely as you like, so parallel runs are the expected usage, not an edge case. Verified rather than reasoned about:
Full CI was green on |

Describe changes
Answering "what CSS does this actually produce?" required hand-writing a throwaway vitest spec, running it, reading the output and deleting the file. Cube Cloud grew a
yarn probefor exactly this (cube-2 #14024) and it already imports its DOM helpers from this repo's@cube-dev/ui-kit/probeentry (#1315) — so the tool belongs here too, where the config, the palette and the components it inspects actually live. The orphaned__screenshots__dirs left behind by deleteddescribe('probe')specs are the local evidence of the old habit.Same modes and flags as Cloud's, so a recipe carries between the two repos:
renderreports the markup plus only the CSS that snippet caused, by rendering<Root>empty, capturing, mounting and subtracting. Measuring a wrapper div directly would have been simpler and wrong:<Root>is the portal target, so aDialogrenders as its sibling — measured that way its inline markup is 0 chars, while the subtraction still captures all 169 of its rules, reported separately underPORTALS.pnpm probe:browseradds the four things jsdom cannot do — computed values, geometry, pointer input, screenshots. SameCard, both tiers:var(--surface-2-color)/calc(3 * var(--gap))under jsdom,rgb(248, 248, 249)/24pxin Chromium. It shares its Chromium andsetup.browser.tswithpnpm test:browser, which — unlike Cloud's equivalent — is already wired into CI.@cube-dev/ui-kitis aliased tosrc, so a snippet reads like consumer code and the same snippet runs unchanged through Cloud's probe.Two deliberate divergences from Cloud's implementation
globalsreadsgetCSSText(), not the per-node baseline. Console-ui hands its palette to<Root>through a tastytokensprop, so the token block is attributed to that node and a per-node dump carries all ~119KB of it. HereGlobalStylesinjects it throughuseGlobalStyles, so it lives on a global sheet no per-node dump can see: the node-scoped answer was 3 rules and 995 bytes, which reads as "<Root>barely styles anything". It now reports all 82 rules and prints how many of themrenderactually subtracts.http://localhost:PORT/Users/…and 404s — surfacing as "Snippet failed to compile" for a snippet that compiles fine. The input now carries both addresses. Cloud'sprobe:browser renderlikely hits this too; I did not test it there, so I would rather flag it than assert it.Reviewer notes
*.probe.tsxso they cannot match vitest's default include.vitest listconfirms neitherpnpm testnorpnpm test:browsercollects them, anddist/carries notest/probeafter a build.includebefore merging.mergeConfigconcatenates arrays, so merging over it leaves both globs in place and every browser spec in the repo would run on each probe.assertConfigApplied()fails the run if<Root>'s module body did not take effect.configure()no-ops once any style has been generated, and that failure has no other symptom — units, recipes and presets silently go unresolved while the output still looks authoritative. The check is end-to-end (1xmust resolve tovar(--gap)) rather than a config-key lookup, so it asserts the thing the probe promises.--computed/--rect/--screenshotare rejected on the jsdom tier rather than silently ignored: asking for computed values and getting none reads as "no styles applied", the opposite of the truth.probe:browserlikewise refuses every mode butrender, instead of passing an undefined snippet path into a dynamic import.--hcis new relative to Cloud. This is where palette work happens, and the four-variant sweep the Glaze docs require needs the high-contrast variant resolved flat, not only as a state map. Verified against the state map:tokens --scheme dark --hcgives#surface-text: oklch(1 0 0), matching its@dark & @hcentry.Verified by hand
Both tiers end to end: all four modes;
renderon<Button type="primary">(57 rules, baseline subtracted) and on aDialogContainer(2 portals, 169 rules, 0 inline chars);--computed/--rect/--screenshotin Chromium including a dark-scheme PNG; and every rejection path —--computedon jsdom, non-rendermodes on browser, unknown mode, no trailing JSX, and a syntax error (reports Vite's code frame pointing at the snippet, not at the wrapper).Checklist
.claude/worktrees/. Note for whoever runs this locally: two stale git worktrees are checked out inside the repo, andvitest.config.tssetstest.excludeexplicitly (replacing vitest's defaults) without listing them, so they get collected with their ownnode_modulesand fail ~390 tests. That is pre-existing and unrelated to this PR, but it does trip thepre-pushhook — worth its own fix.src/probe/is untouched and the published output is byte-identical, whichAGENTS.mdsays to skip a changeset for.Closes: N/A
Other information
The DOM helpers this depends on (
captureCss,diffRules,canonicalize) already ship as@cube-dev/ui-kit/probeand are unchanged here — Cloud imports them from there rather than keeping a copy, so the two repos cannot drift on exactly the thing a probe exists to answer.Docs:
docs/rules/probe.md, reachable fromAGENTS.mdand fromdocs/rules/tests.md, which now carries the rule that a look is not a test.Note
Low Risk
Internal dev tooling and documentation only; published
src/probe/and package build output are unchanged per the PR description.Overview
Adds
pnpm probeandpnpm probe:browseras a one-shot alternative to throwaway Vitest specs for styling questions, aligned with Cube Cloud’syarn probeand loading real<Root>config (tastyconfigure(), Glaze palette).The CLI (
scripts/probe.mjs) drives dedicated Vitest harnesses (*.probe.tsx, excluded frompnpm test) with four modes:styles,tokens,render(stdin JSX, baseline-subtracted CSS, separatePORTALSmarkup), andglobals(fullgetCSSText()here vs Cloud’s per-node dump). Runs use per-invocation.probe/<runId>/scratch dirs; jsdom is default; Chromium adds--computed,--rect,--screenshot, and scheme flags onrenderonly.assertConfigApplied()runs before any answer; invalid tier/mode/flag combos error instead of no-oping.Docs land in
docs/rules/probe.mdwithAGENTS.md/tests.mdupdates (probe before writing a spec; don’t verify styles via bare@tenphi/tasty). Existing@cube-dev/ui-kit/probehelpers are reused, not modified.Reviewed by Cursor Bugbot for commit f1b1ff3. Bugbot is set up for automated code reviews on this repo. Configure here.