Skip to content

feat(parser): build React fiber trees from source without running it - #116

Open
aidenybai wants to merge 31 commits into
mainfrom
aiden/parser-static-fiber-b46e
Open

feat(parser): build React fiber trees from source without running it#116
aidenybai wants to merge 31 commits into
mainfrom
aiden/parser-static-fiber-b46e

Conversation

@aidenybai

@aidenybai aidenybai commented Sep 6, 2026

Copy link
Copy Markdown
Owner

Summary

Adds @bippy/parser, a private workspace package that constructs a React fiber tree directly from a project's source. It parses with oxc-parser, links modules with oxc-resolver, abstractly interprets component render bodies, and reconciles the resulting elements the way ReactChildFiber / ReactFiberConfigDOM do. Every tree can be checked against what React actually commits, captured through Bippy's hook, and the package ships the harness and corpus tooling that does so.

source files ─▶ module ─▶ project ─▶ link ─▶ analyze ─▶ fiber ─▶ snapshot
                parse      resolve    symbols  interpret   build    compare

packages/parser/README.md documents the pipeline, the static value model, the harness, the corpus and the known limits.

What is in the package

  • src/module, src/project, src/link — parsing, tsconfig-aware resolution (paths, extension aliases, unresolvable extends, workspace symlinks, CommonJS interop, JSON modules), cross-module symbol linking, React API identification regardless of import style, and module-level compound-component statics.
  • src/analyze — the abstract interpreter over StaticValues: conditionals keyed by test text with collapse/normalization, path-sensitive narrowing (truthiness, nullish, equality, switch cases, and every local value that branched on a decided test), undecided side effects, bounded loop unrolling, lazy module bindings, function/class/memo/forwardRef/lazy/context components (including Babel/TS-lowered classes), hooks and contexts, React's element shape and $$typeof brands per React version, standard globals as values (Object.assign, Array.prototype.slice.call, Symbol, process.env), array/string/regexp builtins, in, typeof, optional chaining, TS enums, closure escape invalidation, recursion cutoff and a wall-clock budget.
  • src/fiber — static reconciliation into StaticFibers: fragments, textContent vs HostText, Suspense fallbacks, hoistables/singletons, Children key algorithm, render/recursion/fiber budgets.
  • src/snapshot — a serializable FiberSnapshot shared by static and runtime sides, a tree printer, and an automaton-based matcher: unknown nodes are wildcards, branches are alternations, lists are repetitions; among accepting paths the one explaining the most runtime fibers wins, giving a coverage metric so a match cannot be bought with wildcards.
  • src/harness (@bippy/parser/harness) — renders for real under happy-dom with Bippy observing commits and verifies static against runtime.
  • src/corpus (@bippy/parser/corpus) — 30 real React repositories with framework/app-directory/entry metadata, a static scanner, and live verification (dev server boot, Playwright capture with Bippy's hook injected before React loads, comparison against the largest committed root).
  • scripts/inspect.ts, scripts/corpus.ts — CLIs.

Verification

  • Unit tests: 101 in tests/unit (interpreter semantics via describeValue, linker, resolver, matcher, JSX text, host rules, mount discovery, tree rendering).
  • Fixture conformance: 34 fixtures in tests/fixtures (conditionals, lists, context, class components, error boundaries, hooks, HOCs, Suspense, compiled classic/automatic/class output, path aliases, i18n, router, forms, …) render both statically and for real; the runtime tree must be one the static tree describes and, unless a fixture opts out with minCoverage, every runtime fiber must be explained by a concrete static fiber.
  • Corpus static scan (all 30 repos, latest analyzer): 21,000+ exported components rendered, 0 crashes, 0 timeouts. Examples: shadcn/ui 4,380 components / 133k fibers; payload 972 / 137k; supabase 1,848 / 98k; trigger.dev 908 / 111k; dub 1,348 / 55k.
  • Live verification (dev server vs static tree), all matching:
target static fibers unknown live match coverage
@bippy/e2e-vite 81 14 yes 98.6%
@bippy/e2e-rsbuild 81 14 yes 98.6%
@bippy/e2e-kitchen-sink (MUI, Radix, react-query, redux, zustand, i18next, …) 984 94 yes 60.3%
alan2207/bulletproof-react 705 347 yes 54.4%
@bippy/e2e-react-router 794 474 yes 16.7%
@bippy/e2e-tanstack 50,020 24,701 yes 10.8%

Low coverage on the router apps is honest: their trees are dominated by router internals whose state (matches, loaders, transitions) is unknowable statically; the match still holds. Remaining kitchen-sink unknowns are by design (forgotten hook/class state, Proxy-built motion.*/styled.* factories, Map-held contexts).

  • Monorepo pnpm typecheck, pnpm check, and pnpm test (1,870 tests) pass. A knip pass removed the only dead export.

Root changes

  • vite.config.ts: registers the parser test project and excludes compiled fixtures from formatting.
  • package.json: typecheck includes the parser.
  • .changeset/config.json: ignores @bippy/parser (private).

Known limits

  • Externals are opaque unless followed or linked; React itself is modelled, not parsed.
  • Module-level statements other than declarations, X.m = … and Object.assign(X, {…}) never run; bindings they mention read unknown statics rather than a wrong undefined.
  • Hook/class state, refs read during render, and effect-driven updates are unknown by design: the tree describes what could render, not one commit.
  • Proxy, Map/Set contents, Object.create prototypes and most host APIs are unknown.

How to try it

pnpm --filter @bippy/parser test
pnpm --filter @bippy/parser inspect packages/parser/tests/fixtures/conditionals.tsx --diagnostics
pnpm --filter @bippy/parser corpus --workspace --live --no-scan @bippy/e2e-kitchen-sink
pnpm --filter @bippy/parser corpus shadcn-ui/ui
Open in Web Open in Cursor 

cursoragent and others added 30 commits September 6, 2026 01:53
…d linker

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
…ormance harness

Adds the abstract interpreter (values, scopes, statements, calls, JSX,
hooks, contexts), the static reconciler mirroring ReactChildFiber and
ReactFiberBeginWork shapes, DOM host-config rules, serializable
snapshots with an NFA-based consistency matcher, a bippy-backed runtime
capture harness and a fixture conformance suite comparing both.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
…unrolling and undecided side effects

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
…tance, HOC display names and 14 hard fixtures

- model regex literals as a static value kind; run pure String/RegExp
  methods for real when receiver and arguments are fully known, including
  replace() callbacks routed through the interpreter
- class components: inherit render/lifecycle members, static defaultProps
  and contextType through project-local base classes
- HOCs: displayName assignment renames wrapped functions/components,
  reduceRight composition, rest parameters
- conditional array/object mutations wrap in conditional values via
  undecided control-flow frames; pop/shift/splice poison arrays when the
  outcome is undecided
- fixtures: hocs, conditional-pushes, i18n, tagged-templates, router,
  forms, error-boundaries, compiled-automatic/classic, recursion,
  typescript-syntax, class-advanced, suspending-data, path-aliases
- tsconfig paths + vite aliases for fixture path aliases; lint ignores
  compiled fixture output

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
…er and tree renderer

- matcher: drop redundant same-level attribute mismatches, list expected
  siblings in source order
- interpreter: module bindings without an initializer are unknown, not
  undefined (declare const, later-assigned let)
- statements: distinct continue completion so loops with continue still
  unroll; only a break that targets the loop itself disables unrolling
- values: conditional() collapses nested conditionals on the same test and
  identical arms
- fiber: Offscreen fibers have no owner, matching createFiberFromOffscreen

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
…s from them

findMountPoints locates createRoot(el).render(x), hydrateRoot(el, x) and the
legacy ReactDOM.render(x, el), following roots stored in module bindings.
Only callee chains rooted in module-level bindings are resolved, so locals of
nested functions are never evaluated in module scope. The inspect CLI gains
--entry to render an entry module the way the browser mounts it.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
…n and model throw

- Renders run under a wall-clock budget; exceeding it throws
  AnalysisTimeoutError instead of pegging the CPU on pathological inputs.
- Recursion is only followed while every argument is fully known, and at
  most MAX_RECURSION_DEPTH activations deep. A recursive string algorithm
  (fractional-indexing's midpoint) previously fanned out to 2^24 calls.
- throw is a completion of its own: throwing arms leave the render path and
  are dropped from branch merges, a catch handler becomes the only arm when
  the block is known to throw, and a function that always throws yields
  unknown.
- Diagnostics are deduplicated per code, message and location.
- Free identifiers are checked against the host's globals plus browser-only
  names before being reported as unresolved.
- isNullish replaces loose == null comparisons.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
… apps

Adds the corpus module: 30 real-world repositories plus this monorepo's
fixture apps are shallow-cloned, every exported component is rendered with
unknown props, and the results are aggregated (fiber tags, unknown reasons,
opaque packages, crashes, timeouts, unresolved imports, diagnostics) into
JSON and markdown reports with rendered entry trees.

With --live the target's dev server is started, a bippy capture script is
injected into headless Chromium before the page loads, the committed fiber
tree is snapshotted once React goes quiet, and it is matched against the
tree derived statically from the entry module's mount point.

Runtime snapshots now name built-in fibers the way React DevTools does.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
…ions and recursion cutoff by argument knowledge

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
…ain is unavailable, link workspace packages for corpus checkouts, model TS enums

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
…every/includes, optional chaining short-circuit

- OptionalValue models items a filter may drop; map/forEach/slice/reduce/
  at and children reconciliation treat them as presence branches
- selectItem reads filtered[k] as the k-th present item instead of unknown
- find/findLast fall through undecided matches; some/every/includes decide
  from per-item verdicts
- ?. short-circuits on nullish objects and callees along the access spine;
  logical operators distribute over branching left operands
- callsHooksOrCreatesJsx filters PascalCase non-components (API handlers)

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
- narrowing.ts: if/else arms, switch cases, ternaries, && / || and early
  returns refine variables and property paths (x, x.y, x?.y === "a")
  by dropping arms the path rules out; optional chains keep nullish
  prefixes where they would have yielded undefined
- scopes gain a kind; narrowing scopes never receive writes
- logical operators stay linear: the kept arm is filtered instead of
  distributing the right side over every arm (exponential on || chains)
- find/findLast stop branching past SELECTION_LIMIT undecided candidates

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
…ot as unresolved

A checkout without its dependencies installed resolved bare specifiers into
this monorepo's own node_modules and analyzed a different version of the
package.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
…s static tree

Also pass per-target environment variables to the dev server and scan
vercel/ai-chatbot from its root, where its components live beside app/.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
…eference equality, branch memo on a conditional inner type

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
…ss definitions, as NamedEvaluation does

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
…l that cannot be followed

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
…itional on not having returned, store negated tests as the positive one with swapped arms

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
… name when matching

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
…elopment build a bundler substitutes

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
…ctor functions, define class components from a member list, fold module-level class statics

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
…earch known arrays with indexOf and findIndex, unroll for loops whose header assigns outer counters

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
…, prototype methods via call/apply and Boolean/String callbacks are followed

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
…typeof of React's wrappers, apply binary operators per conditional arm, fold every module-level static once a binding is declared

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
… a path decides it

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
…ctions and wrappers rules out, keep named members written onto arrays

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
…e analysis never runs, or a computed key, may have written

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
@changeset-bot

changeset-bot Bot commented Sep 6, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 52caaf1

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel

vercel Bot commented Sep 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
bippy Ready Ready Preview Sep 6, 2026 9:36am UTC

@pkg-pr-new

pkg-pr-new Bot commented Sep 6, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/bippy@116

commit: 52caaf1

@aidenybai
aidenybai marked this pull request as ready for review September 6, 2026 09:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants