fix(ts-dsl): cache LazyTsDsl result to preserve planner import alias redirects#3855
fix(ts-dsl): cache LazyTsDsl result to preserve planner import alias redirects#3855awdr74100 wants to merge 3 commits intohey-api:mainfrom
Conversation
|
|
|
@awdr74100 is attempting to deploy a commit to the Hey API Team on Vercel. A member of the Team first needs to authorize it. |
|
|
TL;DR — Key changes
Summary | 22 files | 3 commits | base: Shared node instance across
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #3855 +/- ##
==========================================
- Coverage 39.58% 39.55% -0.03%
==========================================
Files 532 532
Lines 19581 19582 +1
Branches 5835 5829 -6
==========================================
- Hits 7751 7746 -5
- Misses 9582 9585 +3
- Partials 2248 2251 +3
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Important
Fix is correct and minimal — verified end-to-end that the planner mutates dependency['~ref'] on the exact node handed back by toResult() during analyze(), and that pre-fix the thunk ran twice per lazy (once under the analyzer, once under the renderer), discarding that mutation. Two follow-ups before merge:
LazyPyDslhas the same bug.packages/openapi-python/src/py-dsl/utils/lazy.tsis a structural mirror ofLazyTsDslwithout the cache — itstoResult()still returnsthis._thunk(ctx)fresh on every call. If the Python codegen hits the same planner~refrewrite path (which it does via the shared@hey-api/codegen-coreplanner), it will reproduce the same wrong-identifier symptom. Please apply the same two-line fix there.- No regression coverage. The existing
internal-name-conflictfixture only exercises intra-SDK collisions (create,create_,create__) and does not trigger the bug — no snapshot in the repo currently contains the broken output, which means reverting this fix leaves every test passing. A minimal spec with two operations whose IDs produce theop+{op}Optionscollision (e.g.getMembers+getMembersOptions) wired throughpackages/openapi-ts-tests/tanstack-query/v5/test/3.1.x.test.tswould pin the user-visible behavior, and a tiny unit test inpackages/openapi-ts/src/ts-dsl/utils/__tests__/assertinglazy.toResult() === lazy.toResult()and thunk-invocation count would pin the caching contract itself.
Neither is strictly blocking; both would meaningfully harden the fix against future refactors.
|
|
||
| toResult(): TsDsl<T> { | ||
| return this._thunk(ctx); | ||
| this._result ??= this._thunk(ctx); |
There was a problem hiding this comment.
The Python sibling packages/openapi-python/src/py-dsl/utils/lazy.ts:25-27 has the identical shape without the cache and will reproduce the same wrong-identifier symptom whenever the shared planner performs a ~ref rewrite on a LazyPyDsl-produced node. Consider applying the same two-line fix there in this PR.
| toResult(): TsDsl<T> { | ||
| return this._thunk(ctx); | ||
| this._result ??= this._thunk(ctx); | ||
| return this._result; |
There was a problem hiding this comment.
The caching contract introduced here has no unit test, and no existing snapshot exercises the bug the PR fixes (the internal-name-conflict fixture only covers intra-SDK collisions). A ~15-line test in packages/openapi-ts/src/ts-dsl/utils/__tests__/ that asserts lazy.toResult() === lazy.toResult() and counts thunk invocations would prevent a future refactor from silently reintroducing the bug — the contract is purely local to this 35-line file.
@hey-api/codegen-core
@hey-api/json-schema-ref-parser
@hey-api/nuxt
@hey-api/openapi-ts
@hey-api/shared
@hey-api/spec-types
@hey-api/types
@hey-api/vite-plugin
commit: |
|
@awdr74100 Can you add tests so I can reproduce? |
|
Added, please confirm again, thank you. |
|
@pullfrog review now |
There was a problem hiding this comment.
Reviewed — no issues found.
Correctness, planner pass counts, clone() interaction, ctx capture, and every existing $.lazy(...) call site were verified. The unit tests pin the exact instance identity the planner's ~ref mutation relies on (first test + third test together would have failed pre-fix), and the new query-options-name-conflict snapshot is a genuine regression guard — @tanstack/react-query.gen.ts line 6 imports getMembersOptions as getMembersOptions2 and line 61 threads that alias through getMembersOptionsOptions.
One optional observation, not blocking: LazyPyDsl has no consumers in packages/openapi-python yet (it's only exposed via $.lazy in the factory), so shipping the mirror fix without a parallel unit test is defensible — but once py-dsl gains a real $.lazy call site, adding a unit test under the existing packages/openapi-python/src/py-dsl/utils/__tests__/ directory would keep the two DSLs symmetric.

Summary
This discarded the mutations applied by the planner during the analyze() phase — specifically, the
~ref redirects that resolve cross-file symbol naming conflicts to their correct import aliases.
locally-defined symbol would call the wrong identifier in the function body (the local symbol), even
though the import statement correctly used an alias.
node instance.
Root cause
The planner resolves cross-file naming conflicts by mutating the Ref object held inside a node's
_exprInput:
LazyTsDsl calls toResult() (which invokes the thunk) in both analyze() and toAst(). Because the
thunk constructs a new DSL node each time, the mutation applied during analyze() is made on an
instance that is immediately thrown away. toAst() then creates a brand-new instance — with the
original, un-mutated ~ref — and renders the wrong identifier.
Example
Given an OpenAPI spec with two endpoints:
GET /membersGET /members/optionsWhen a TanStack Query plugin generates a query options wrapper for getMembers, it names it
getMembersOptions (pattern: {operationId}Options). This collides with the SDK function also named
getMembersOptions.
The planner resolves this by importing the SDK function under an alias:
Before this fix — toAst() re-creates the node and loses the alias redirect:
After this fix — the cached node retains the planner's mutation:
Affected plugins
All plugins that use $.lazy to reference cross-file SDK symbols are affected. This includes:
@tanstack/preact-query, @tanstack/angular-query-experimental (all share query-core logic)
Changes
Test plan
getMembersOptionsOptions, not the local getMembersOptions wrapper