Repository navigation
feat: Make the axios migration work end to end - #4464
Conversation
One prompt hands the migration to a coding agent through the setup skill (SetupTabs takes a task), agents reading the page markdown get the steps directly, and the guide is now seven ordered steps with DiffEditor before/after, including a Vue call-site variant. DiffEditor mounts Monaco only near the viewport, and page markdown keeps component captions. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PnnGtvQRyLnFcGsqygBK4c
🦋 Changeset detectedLatest commit: e6cb50d The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #4464 +/- ##
=======================================
Coverage 98.37% 98.37%
=======================================
Files 161 161
Lines 3080 3080
Branches 605 605
=======================================
Hits 3030 3030
Misses 10 10
Partials 40 40 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
A signal set when the endpoint is created is shared by all of its requests, so the timeout started counting at import and one cancel aborted every later call. Set the timeout per request in getRequestInit() and cancel through extend(). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PnnGtvQRyLnFcGsqygBK4c
|
Docs preview: https://docs-site-pr-4464-data-client.vercel.app Built from e6cb50d merged into |
|
Staff engineer (Cursor agent): CHANGE_THIS_PR at ed549fa, one small fix. Moving the timeout into Suggested fix in signal: this.signal
? AbortSignal.any([this.signal, AbortSignal.timeout(5000)])
: AbortSignal.timeout(5000),You could also add a callout like "Keeps The rest of the push looks good to me. The table row now matches the new section, and Validated: I read the delta 13de7e7...ed549fa through the GitHub API, along with |
The timeout override replaced the endpoint's own signal, so endpoints
using it lost extend({ signal }) and useCancelling(). Combine both with
AbortSignal.any().
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PnnGtvQRyLnFcGsqygBK4c
|
Confirmed and fixed in 8a13a19. Generated by Claude Code |
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PnnGtvQRyLnFcGsqygBK4c
The guide moves axios calls to @data-client/rest endpoints and schemas, and leaves how components load and store data alone, as it did on master. Its agent prompt installs the REST setup skill instead of the setup skill, which also adds the provider, so SetupTabs and the prompt builders go back to master. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PnnGtvQRyLnFcGsqygBK4c
The setup skill's next steps told agents to move components onto hooks, which the axios guide now leaves to the user. Also restore the blob endpoint's dataExpiryLength so it stays out of the cache once the store is adopted, and say plainly that only the call shape changes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PnnGtvQRyLnFcGsqygBK4c
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 62c32b3bf9
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // callout: Keeps large blobs out of the cache if you adopt the store later | ||
| dataExpiryLength: 0, |
There was a problem hiding this comment.
Don't describe zero expiry as disabling blob caching
When readers later fetch this endpoint through the store, dataExpiryLength: 0 does not keep the blob out of the cache: createSetResponse() only uses it to calculate freshness, and setResponseReducer() still stores the response. The blob can therefore remain retained while becoming immediately stale, causing additional downloads when freshness-aware consumers read it. Remove this option and the cache-exclusion claim, or document a download path that actually bypasses the store.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
You're right. setResponseReducer() stores the response either way, and dataExpiryLength only changes expiresAt. I removed the option and the claim in 15a8396.
Generated by Claude Code
dataExpiryLength: 0 only marks the response stale; the store still keeps it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PnnGtvQRyLnFcGsqygBK4c
The axios-to-rest codemod replaced `api.get(url)` with an uncalled
`new ApiEndpoint({ path })`, so awaiting it returned the endpoint, and
it dropped bodies and params. It now calls the endpoint with path params,
params, query strings and the body, reads the body where code read
`.data`, keeps types, and marks anything left with a TODO(data-client)
comment linking the guide.
The rest-setup skill now runs the codemod, resolves those comments and
removes axios, and the data-client router (the plugin's entry) sends
"replace axios" to that definitions-only guide instead of provider setup.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PnnGtvQRyLnFcGsqygBK4c
|
Size Change: 0 B Total Size: 112 kB ℹ️ View Unchanged
|
|
Staff engineer (Cursor agent): CHANGE_THIS_PR at a17086e, about the codemod. The rest of the delta (the rest-setup skill steps, the router line in The issue: for calls through a class made from
The last row has a separate cause. The codemod emits Ask: this only applies to calls through a created class that has a
Leave paths alone when there's no What I validated, and how: I ran this PR's Optional, not blocking: the router line in |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a17086e6c0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (key === 'params') { | ||
| if (prop.value.type === 'ObjectExpression') { | ||
| callArgs.push(...prop.value.properties); | ||
| } else { | ||
| callArgs.push(j.spreadElement(prop.value)); |
There was a problem hiding this comment.
Keep query parameters from overwriting path parameters
When config.params contains a key also used by a URL interpolation, merging both into one object changes the requested resource. For example, axios.get(/users/${id}, { params: filters }) becomes an endpoint with path /users/:id invoked with { id, ...filters }. If filters.id exists, it replaces the original path ID, and RestEndpoint.url() consumes it as a path parameter instead of sending the query parameter. Generate nonconflicting path parameter names, or leave a TODO when the query keys cannot be determined safely.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in ba2982e.
- A literal
paramskey or query key that matches a path param leaves the call unconverted with a TODO. - Non-literal
paramsare spread before the path args, so they can't replace a path param, and the call gets a query-serialization TODO.
Generated by Claude Code
| if (value === '\0') { | ||
| const expr = expressions[exprIndex++]; | ||
| props.push( | ||
| j.objectProperty.from({ | ||
| key: j.identifier(key), | ||
| value: expr, | ||
| shorthand: expr.type === 'Identifier' && expr.name === key, |
There was a problem hiding this comment.
Preserve encoding when extracting interpolated query values
For URLs such as axios.get(/search?q=${encodeURIComponent(q)}), this branch moves the already encoded expression directly into endpoint arguments. RestEndpoint.searchToString() subsequently encodes it again through URLSearchParams: a value of a b previously reached the server as a b, but now arrives as a%20b. Preserve the original query semantics or leave these calls unchanged with a TODO rather than silently double-encoding their values.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in b06f300. An encodeURIComponent(x) interpolation now passes x as the arg, in both path segments and the query, because RestEndpoint encodes path params (path-to-regexp compile) and query values (URLSearchParams) itself. There's a new test for /users/${encodeURIComponent(user)}/search?q=${encodeURIComponent(q)}.
Generated by Claude Code
| if (key === 'params') { | ||
| if (prop.value.type === 'ObjectExpression') { | ||
| callArgs.push(...prop.value.properties); | ||
| } else { | ||
| callArgs.push(j.spreadElement(prop.value)); |
There was a problem hiding this comment.
Preserve Axios serialization for non-scalar params
When an Axios call supplies array or nested-object params, copying those values into endpoint arguments does not preserve the request. For example, Axios's default serialization of { ids: [1, 2] } produces bracketed array parameters, whereas RestEndpoint.searchToString() uses URLSearchParams and produces ids=1%2C2; nested objects likewise become [object Object]. These calls are converted without a query-serialization TODO, so resolving the generated comments can still leave broken API filters. Supply compatible serialization or explicitly mark these cases for manual migration.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in ba2982e. A call gets a TODO pointing at searchToString() (#query-serialization) when its params has an array or object literal value, or when params isn't a literal.
Generated by Claude Code
| if (url.urlPrefix) { | ||
| options.push( | ||
| j.objectProperty( | ||
| j.identifier('urlPrefix'), | ||
| j.stringLiteral(url.urlPrefix), |
There was a problem hiding this comment.
Preserve absolute URL overrides on created Axios instances
When an instance created with axios.create({ baseURL: 'https://primary.example' }) requests an absolute URL on another origin, this constructor option does not preserve that override. The generated ApiEndpoint declares urlPrefix as a class field, which runs after Endpoint assigns the constructor options and overwrites the absolute URL's origin. Consequently, api.get('https://secondary.example/users') requests https://primary.example/users after conversion. Use an endpoint construction strategy that retains the explicit origin, or leave this case unchanged with a TODO.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Already fixed in 556d973. An absolute url through a class made from axios.create({ baseURL }) is left as is with a TODO, because the class's urlPrefix field would override the option.
Generated by Claude Code
| const source = j(expr).toSource(); | ||
| if (names.has(name) && names.get(name) !== source) { | ||
| let n = 2; | ||
| while (names.has(name + n)) n++; | ||
| name = name + n; |
There was a problem hiding this comment.
Preserve each evaluation of repeated URL expressions
When two URL interpolations have identical source text but produce different values or have side effects, deduplicating their arguments changes the request and subsequent program state. For example, a URL template containing /pairs/${ids.shift()}/${ids.shift()} becomes /pairs/:param/:param with only one ids.shift() argument: given ['first', 'second'], it requests /pairs/first/first instead of /pairs/first/second and leaves the second item in the array. Give potentially effectful expressions separate parameters, or leave these calls unchanged with a TODO instead of assuming identical source means identical values.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in ba2982e. Only identifiers and plain member chains are deduplicated now. Any other expression gets its own param, so ${ids.shift()}/${ids.shift()} becomes :param/:param2, with two calls.
Generated by Claude Code
RestEndpoint joins urlPrefix and path as is, while axios puts exactly one '/' between baseURL and a relative url. Strip a literal baseURL's trailing slash, start every path through such a class with '/', mark non-literal baseURLs, and mark absolute urls through an instance, since the class's urlPrefix field would override a urlPrefix option. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PnnGtvQRyLnFcGsqygBK4c
|
Confirmed, and fixed in this PR in 556d973. Changes, for classes made from
Bare On the optional note: rest-setup does ship a got reference ( Generated by Claude Code |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 556d973f10
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| props.push( | ||
| j.objectProperty( | ||
| j.identifier(key), | ||
| j.stringLiteral(decodeURIComponent(value)), |
There was a problem hiding this comment.
Preserve repeated keys in literal query strings
For a call such as axios.get('/search?tag=red&tag=blue'), this parser generates two tag properties in the endpoint argument object. JavaScript keeps only the last property, so RestEndpoint.searchToString() sends only tag=blue, silently changing the API filter. Preserve repeated query values with compatible serialization, or leave these URLs unchanged with a migration TODO.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in ba2982e. A repeated query key now leaves the call unconverted with a TODO.
Generated by Claude Code
| props.push( | ||
| j.objectProperty( | ||
| j.identifier(key), | ||
| j.stringLiteral(decodeURIComponent(value)), |
There was a problem hiding this comment.
Decode plus signs as spaces in literal query values
For a URL such as axios.get('/search?q=hello+world'), decodeURIComponent() leaves the + intact instead of decoding the query's space. The generated endpoint subsequently serializes that value through URLSearchParams as q=hello%2Bworld, so the server receives hello+world instead of hello world. Use query-string decoding semantics before moving literal values into endpoint arguments.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in ba2982e: + is decoded as a space before decodeURIComponent(). c6eba13 adds a test for ?q=hello+world.
Generated by Claude Code
| if (!dropDataReads(j, scope.path, id.name, scope, id)) return false; | ||
| if (bodyType) id.typeAnnotation = j.tsTypeAnnotation(bodyType); | ||
| else if (bodyType === undefined) id.typeAnnotation = null; | ||
| return true; |
There was a problem hiding this comment.
Mark exported response bindings for manual migration
When a JavaScript module declares export const response = await axios.get('/config'), this branch treats the conversion as complete because dropDataReads() only searches the current scope and finds no local reads. Consumers in other files that import response and read response.data are left unchanged, but the export now contains the body rather than the Axios response, and no migration TODO is emitted. Detect directly exported response bindings and mark their external consumers for manual migration instead of considering local reference rewriting sufficient.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in c6eba13. An export const response keeps its value as is and gets the "drop .data" TODO, since other modules may read its .data. export { response } and export default response were already marked, because those count as non-.data reads.
Generated by Claude Code
String.replace expanded $ patterns in the matched module path. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PnnGtvQRyLnFcGsqygBK4c
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: df8ac4b443
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // the same variable can fill two segments; anything else is evaluated once each | ||
| const source = isPure(expr) ? j(expr).toSource() : {}; | ||
| if (names.has(name) && names.get(name) !== source) { |
There was a problem hiding this comment.
Preserve repeated path reads across intervening mutations
When an interpolation changes state read by a later interpolation, deduplicating identifiers still silently changes the requested resource. For example, with id = 1, /pairs/${id}/${id++}/${id} originally requests /pairs/1/1/2, but this produces /pairs/:id/:param/:id with { id, param: id++ }, requesting /pairs/1/1/1 without a TODO. The fresh evidence beyond the earlier repeated-expression finding is that effectful expressions now receive separate parameters, but their mutations do not invalidate previously deduplicated reads. Preserve each read across potentially effectful interpolations, or leave these calls unconverted with a migration TODO.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 91bd240: repeated variables are only merged into one param when every path interpolation is pure, so /pairs/${id}/${id++}/${id} now becomes /pairs/:id/:param/:id2 with each read kept in order (test added).
Generated by Claude Code
…nterpolation Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PnnGtvQRyLnFcGsqygBK4c
Haiku evals on a 27-call shop app and bulletproof-react scored the same with this version as with the full one (81/81 and 14/14 hidden request checks), at a third of the length. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PnnGtvQRyLnFcGsqygBK4c
…esponse transforms Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PnnGtvQRyLnFcGsqygBK4c
…een its reads Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PnnGtvQRyLnFcGsqygBK4c
Merge the two process() rows, point non-JSON responses at the content option, and link the fetchResponse error example. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PnnGtvQRyLnFcGsqygBK4c
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PnnGtvQRyLnFcGsqygBK4c
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PnnGtvQRyLnFcGsqygBK4c
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PnnGtvQRyLnFcGsqygBK4c
Share the response-interceptor query, cache module resolution per folder, add memberOf/specName helpers, drop a dead export walk in markTodo, and use jscodeshift's applyTransform in the multi-file tests. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PnnGtvQRyLnFcGsqygBK4c
Haiku evals on a ky app: 3 of 6 skill runs called a bodyless POST endpoint with only its path params, which were sent as the body. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PnnGtvQRyLnFcGsqygBK4c
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PnnGtvQRyLnFcGsqygBK4c
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PnnGtvQRyLnFcGsqygBK4c
Requested by Nathaniel · project thread
Motivation
Migrating from axios needed more than a docs rewrite. The Axios migration guide came before the agent-first delivery, and the
axios-to-restcodemod it starts with emitted broken code. It swappedawait api.get(url)for an uncalledawait new ApiEndpoint({ path }), which resolves to the endpoint instead of data, and it dropped POST bodies,paramsand template path params. Agents that ran it then had to find and repair every call by hand.Solution
The scope is data definitions only. axios calls become
@data-client/restendpoints,resource()and Entity schemas, and how components load or store data doesn't change. Master's "Convert call sites to hooks" and "Gradual migration" sections are gone for that reason, and adopting the store is linked as a separate, later step.Codemod (
@data-client/codemodsaxios-to-rest) now emits working calls:params, query strings and absolute-URLurlPrefix. Bodies are passed through, and a mutation with no data getsbody: undefined..data:res.data,(await x).data,{ data }/{ data: alias },.then(r => r.data), andAxiosResponse<T>annotations becomeT.<O extends RestGenerics = any> extends RestEndpoint<O>) and typedgetHeaders().// TODO(data-client): …comment that links the guide section for it. That covers interceptors, instance options liketimeout, per-request config,isAxiosError/AxiosError,CancelToken,axios.all, dynamic URLs, and responses returned to callers. It never silently rewrites those.I verified it on a sample app. The output type-checks against
@data-client/restexcept on the marked lines, and the requests (URL, method, headers, body) are correct against a mockedfetch.Agents and the plugin.
data-client-rest-setup.TODO(data-client), follows the remaining steps, type-checks, and then uninstalls axios.data-clientrouter, which is the plugin's entry, sends "replace axios/fetch/ky… with REST endpoints" to that definitions-only guide instead ofdata-client-setup, which adds a provider.Humans. The guide is seven ordered steps: install, codemod, base endpoint, entities and resources, call sites, errors, remove axios. Then comes an "Axios options reference". Every before/after is a
<DiffEditor>, and the codemod step shows what the codemod outputs. The timeout and cancel examples now use per-request signals and keepextend({ signal })working.Shared changes.
DiffEditormounts Monaco only near the viewport.docsToMarkdownkeeps a wrapper's stringcaptionso agents get it.Assumptions and validation
check:doc-examplespasses for React (270) and Vue (147),build:skillsis clean, and the plugin tests pass (12). Certain.npx jscodeshift -t https://dataclient.io/codemods/…, because@data-client/codemodsisn't on npm until the release. Likely right for now.🤖 Generated with Claude Code
https://claude.ai/code/session_01PnnGtvQRyLnFcGsqygBK4c
Note
Low Risk
Mostly agent skills, generated migration markdown, and docs UI lazy-loading; codemod output changes affect migration automation but not runtime app behavior.
Overview
End-to-end axios →
@data-client/restmigration is reframed as endpoint/schema work only: agents and docs no longer push a Data Client provider or converting call sites to hooks unless the user asks.The
data-client-rest-setupskill is shortened and retargeted: install viadata-client-skills, link skills inAGENTS.md, runaxios-to-reston a clean tree, clearTODO(data-client)comments, then base endpoint +resource()patterns. The axios migration references are reorganized into seven steps (codemod → base endpoint → entities → call sites → errors → uninstall axios) with expanded option mapping; Vue copies (axios-migration.vue.md) are added. The top-leveldata-clientskill routes “swap HTTP client only” work to that REST setup guide.A changeset (and codemods README) documents
axios-to-restnow invoking endpoints with path/query/body args and stripping.datacorrectly, leaving linked TODOs for interceptors and similar.Docs tooling:
DiffEditordefers Monaco until the block is near the viewport;docsToMarkdownemits string **caption**s from diff wrappers for agent-readable guides.Reviewed by Cursor Bugbot for commit e6cb50d. Bugbot is set up for automated code reviews on this repo. Configure here.