Skip to content

fix(router-utils): cache a descriptor instead of the initializer AST node - #8296

Open
sverrejoh wants to merge 1 commit into
TanStack:mainfrom
sverrejoh:fix/start-compiler-module-cache-retention
Open

fix(router-utils): cache a descriptor instead of the initializer AST node#8296
sverrejoh wants to merge 1 commit into
TanStack:mainfrom
sverrejoh:fix/start-compiler-module-cache-retention

Conversation

@sverrejoh

@sverrejoh sverrejoh commented Sep 8, 2026

Copy link
Copy Markdown

🎯 Changes

Alternative to #8215, which stops the same leak by cloning the node instead.

@babel/traverse keys its NodePath and Scope caches on node identity, and
the Start compiler keeps the initializer node in its module cache for the life
of the dev server, so the file's traversal graph is never collected.

Rather than cloning the node, store only what the resolvers actually read. They
follow callee, object, an identifier property, and name, never call
arguments or source positions, so the cached shape can be a small descriptor:

export type ExpressionSummary =
  | { type: 'identifier'; name: string }
  | { type: 'member'; object: ExpressionSummary; property: string }
  | { type: 'call'; callee: ExpressionSummary }

✅ Checklist

  • I have followed the steps in the Contributing guide (https://github.com/TanStack/router/blob/main/CONTRIBUTING.md).
  • I have tested code changes locally with the relevant test commands, or tests do not apply to this pull request.
  • I fully understand the code in this pull request, including any code generated with AI assistance.

🚀 Release Impact

Summary by CodeRabbit

  • Bug Fixes
    • Improved compiler cache behavior by preventing cached module information from retaining source-file parsing data.
    • Preserved compiler handling for variable bindings, member access, function calls, wrapped expressions, and computed properties.
  • Tests
    • Expanded coverage for expression summarization and updated compiler snapshots to reflect the revised representation.
  • Chores
    • Published patch release updates for the affected Router and Start compiler packages.

@coderabbitai

coderabbitai Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: e136ec96-71e0-4356-b05d-0c2dfe0d7484

📥 Commits

Reviewing files that changed from the base of the PR and between 9871c06 and eb9628e.

📒 Files selected for processing (5)
  • .changeset/start-compiler-module-info-ast-retention.md
  • packages/router-utils/src/compiler-helpers.ts
  • packages/router-utils/src/index.ts
  • packages/router-utils/tests/compiler-helpers.test.ts
  • packages/start-plugin-core/src/start-compiler/compiler.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.


📝 Walkthrough

Walkthrough

The compiler now stores compact expression summaries instead of Babel expression nodes. Router utilities expose the summary type and helper. Start compiler kind resolution consumes summarized identifiers, members, and calls.

Changes

Expression summary retention

Layer / File(s) Summary
Expression summary contract and storage
packages/router-utils/src/compiler-helpers.ts, packages/router-utils/src/index.ts, packages/router-utils/tests/compiler-helpers.test.ts, .changeset/start-compiler-module-info-ast-retention.md
Adds ExpressionSummary and summarizeExpression. Variable and default-export bindings store summarized expressions. Exports and tests cover supported expression shapes and omitted fields.
Compiler kind resolution integration
packages/start-plugin-core/src/start-compiler/compiler.ts
Updates expression and callee resolution to use ExpressionSummary, including namespace-import handling and direct-call checks.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to f37c2

This change replaces retained initializer ASTs with compact summaries while preserving supported compiler kind resolution, reducing development-server memory retention without an identified current-head merge risk.

Suggested reviewers: schiller-manuel

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 5 functions across 4 files. (1 skipped: 1… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes the primary change: caching a descriptor instead of retaining the initializer AST node.
Description check ✅ Passed The description explains the motivation, implementation, testing checklist, and release impact. It includes the required sections and a generated changeset.
Full details: Docstring Coverage

Explanation

Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 5 functions across 4 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…AST nodes

`extractModuleInfoFromAst` stored each `var` binding's initializer as the
`t.Expression` node from the parsed file. `StartCompiler.moduleCache` keeps
that module info for as long as the module is known, so in a dev server the
node stays reachable for the process lifetime.

`@babel/traverse` keys its `NodePath` and `Scope` caches on node identity
(`WeakMap<node, ...>`), which is what normally lets a file's traversal state be
collected once the AST is dropped. Holding one node from a file defeats that
for the whole file: the cache entry chains through `NodePath.parentPath` up to
the Program path and through `NodePath.scope` into every binding in the file.
In a heap snapshot of our dev server these retained graphs survived a full GC.

The only consumers, `resolveBindingKind` and `resolveExprKind`, read whether
the expression is an identifier, a member access or a call, plus the names
involved. They never read call arguments, source positions, scope or parent
links. Store that projection instead of the node:

- add `ExpressionSummary` and `summarizeExpression` to `@tanstack/router-utils`
- change `ModuleInfoBinding['init']` to `ExpressionSummary | null`
- resolve kinds from the summary in `StartCompiler`

The summary is built with the same shape checks the resolvers used, so results
are unchanged, and expressions the resolvers cannot act on summarize to `null`,
which they already treat like an absent initializer. Resolving from a
three-variant union also lets `resolveExprKind` drop its accumulator and its
duplicated callee branches.

Measured on our app: 11.6% less peak dev-server memory over four paired cold
runs, with byte-identical build output.
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.

1 participant