Skip to content

feat: Make the axios migration work end to end - #4464

Merged
ntucker merged 41 commits into
masterfrom
claude/project-thread-stcwaw
Oct 25, 2026
Merged

ntucker merged 41 commits into
masterfrom
claude/project-thread-stcwaw

Conversation

@ntucker

@ntucker ntucker commented Oct 25, 2026 •

Copy link
Copy Markdown
Collaborator

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-rest codemod it starts with emitted broken code. It swapped await api.get(url) for an uncalled await new ApiEndpoint({ path }), which resolves to the endpoint instead of data, and it dropped POST bodies, params and 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/rest endpoints, 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/codemods axios-to-rest) now emits working calls:

// before
const res = await api.get<Post>(`/posts/${id}`, { params: { expand: 'author' } });
return res.data;
// after
const res = await new ApiEndpoint({
  path: '/posts/:id',
  searchParams: {} as Record<string, unknown>,
  process(value): Post {
    return value;
  },
})({ id, expand: 'author' });
return res;
  • Path params come from template expressions, and query args from params, query strings and absolute-URL urlPrefix. Bodies are passed through, and a mutation with no data gets body: undefined.
  • It reads the body wherever the code read .data: res.data, (await x).data, { data } / { data: alias }, .then(r => r.data), and AxiosResponse<T> annotations become T.
  • TS files get a generic base class (<O extends RestGenerics = any> extends RestEndpoint<O>) and typed getHeaders().
  • Anything it can't convert gets a // TODO(data-client): … comment that links the guide section for it. That covers interceptors, instance options like timeout, 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/rest except on the marked lines, and the requests (URL, method, headers, body) are correct against a mocked fetch.

Agents and the plugin.

  • The guide's single prompt installs data-client-rest-setup.
  • That skill's axios path now runs the codemod on a clean tree, resolves each TODO(data-client), follows the remaining steps, type-checks, and then uninstalls axios.
  • The data-client router, which is the plugin's entry, sends "replace axios/fetch/ky… with REST endpoints" to that definitions-only guide instead of data-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 keep extend({ signal }) working.

Shared changes.

  • DiffEditor mounts Monaco only near the viewport.
  • docsToMarkdown keeps a wrapper's string caption so agents get it.

Assumptions and validation

  • Codemod tests: 133 pass, with new cases for body reads, URLs, config, instance leftovers and TypeScript. Certain.
  • check:doc-examples passes for React (270) and Vue (147), build:skills is clean, and the plugin tests pass (12). Certain.
  • Desktop (1280px) and iPhone 13 render with no sideways scroll. Certain.
  • The guide still runs the codemod through npx jscodeshift -t https://dataclient.io/codemods/…, because @data-client/codemods isn'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/rest migration 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-setup skill is shortened and retargeted: install via data-client-skills, link skills in AGENTS.md, run axios-to-rest on a clean tree, clear TODO(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-level data-client skill routes “swap HTTP client only” work to that REST setup guide.

A changeset (and codemods README) documents axios-to-rest now invoking endpoints with path/query/body args and stripping .data correctly, leaving linked TODOs for interceptors and similar.

Docs tooling: DiffEditor defers Monaco until the block is near the viewport; docsToMarkdown emits 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.

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
@ntucker ntucker self-assigned this Oct 25, 2026
@changeset-bot

changeset-bot Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e6cb50d

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@data-client/codemods Patch

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

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-25T05:14:19.313106Z e6cb50d New commits
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@codecov

codecov Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.37%. Comparing base (6c48cec) to head (e6cb50d).
⚠️ Report is 5 commits behind head on master.

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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
@github-actions

github-actions Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Docs preview: https://docs-site-pr-4464-data-client.vercel.app

Built from e6cb50d merged into master (this build).

@ntucker

ntucker commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator Author

Staff engineer (Cursor agent): CHANGE_THIS_PR at ed549fa, one small fix.

Moving the timeout into getRequestInit() is the right call, since a constructor-time AbortSignal.timeout() starts counting once for every request. The new snippet causes a different problem, though. It's the same ApiEndpoint base that Step 3 has every resource extend, and it overwrites signal after spreading super.getRequestInit(body). RestEndpoint.getRequestInit sets signal: this.signal (packages/rest/src/RestEndpoint.js L136–141), so the override throws away whatever extend({ signal }) set. Anyone who adopts the timeout snippet silently loses both cancellation patterns the guide teaches just above it: getUser.extend({ signal: controller.signal }) and useCancelling(), which cancels through the same extend({ signal }) path (packages/react/src/hooks/useCancelling.ts L26–27).

Suggested fix in docs/rest/guides/axios-migration.md (Timeout "After" block), then regenerate the .agents/skills copies:

signal: this.signal
  ? AbortSignal.any([this.signal, AbortSignal.timeout(5000)])
  : AbortSignal.timeout(5000),

You could also add a callout like "Keeps extend({ signal }) and useCancelling() working; either signal aborts the request." AbortSignal.any is Baseline 2024. The snippet already depends on AbortSignal.timeout, so this barely raises the floor.

The rest of the push looks good to me. The table row now matches the new section, and task || setupTask(framework) in SetupTabs correctly treats an empty-string task as unset.

Validated: I read the delta 13de7e7...ed549fa through the GitHub API, along with RestEndpoint.getRequestInit, useCancelling, and the guide's Step 3, Cancellation and Timeout sections at ed549fa. A separate adversarial pass tried to disprove the finding and confirmed it. Not run: I didn't build anything, run tests, or execute anything in a browser. The signal override is inferred from object-spread semantics.

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

ntucker commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator Author

Confirmed and fixed in 8a13a19. RestEndpoint.getRequestInit sets signal: this.signal, and the override replaced it. The timeout now joins whatever signal super.getRequestInit() returned with AbortSignal.any(), so extend({ signal }) and useCancelling() keep working. I added a callout that says so. Skill references are regenerated, and doc examples type-check.


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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread docs/rest/guides/axios-migration.md Outdated
Comment on lines +553 to +554
// callout: Keeps large blobs out of the cache if you adopt the store later
dataExpiryLength: 0,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
@ntucker ntucker changed the title docs: Make the Axios migration guide agent-first feat: Make the axios migration work end to end Oct 25, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Size Change: 0 B

Total Size: 112 kB

ℹ️ View Unchanged
Filename Size
examples/test-bundlesize/dist/App.js 1.45 kB
examples/test-bundlesize/dist/polyfill.js 301 B
examples/test-bundlesize/dist/rdcClient.js 10.8 kB
examples/test-bundlesize/dist/rdcEndpoint.js 8.04 kB
examples/test-bundlesize/dist/rdcNextjs.js 12.4 kB
examples/test-bundlesize/dist/rdcPipeableStream.js 9.65 kB
examples/test-bundlesize/dist/react.js 68.6 kB
examples/test-bundlesize/dist/webpack-runtime.js 698 B

compressed-size-action

@ntucker

ntucker commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator Author

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 data-client/SKILL.md, and the guide's codemod section) looks right to me.

The issue: for calls through a class made from axios.create({ baseURL }), the endpoint the codemod emits doesn't request the URL axios would. axios joins baseURL and url with exactly one /. RestEndpoint just concatenates urlPrefix + path and doesn't normalize anything (see ${this.urlPrefix}${urlBase} in RestEndpoint). The commit now says the output is "working endpoint calls," so these wrong URLs fail silently and leave no TODO behind:

baseURL axios call axios requests codemod output requests
https://api.example.com/v1/ api.get('/users/5') .../v1/users/5 .../v1//users/5
https://api.example.com/v1/ api.get('?q=1') (new '/' fallback) .../v1/?q=1 .../v1//?q=1
https://api.example.com/v1 api.get('users/5') .../v1/users/5 .../v1users/5
https://api.example.com/v1/ api.get('https://other.com/x') https://other.com/x .../v1//x

The last row has a separate cause. The codemod emits new ApiEndpoint({ urlPrefix: 'https://other.com', path: '/x' }), but the subclass's urlPrefix = ... class field is assigned after super(options) runs, so it overwrites the option. The existing absolute-URL test only covers bare axios, where this works.

Ask: this only applies to calls through a created class that has a baseURL.

  1. Make sure the emitted path starts with /.
  2. Strip one trailing / from a string-literal baseURL. When baseURL isn't a literal (for example process.env.API_URL), add a TODO(data-client) asking to check its trailing slash.
  3. For an absolute url, either emit a plain RestEndpoint (losing the class's headers) or leave a TODO.

Leave paths alone when there's no baseURL. Bare axios.get('users/5') resolves relative to the document, and the current output (path: 'users/:id') already matches that. Please add tests for three cases: a trailing-slash baseURL with a leading-slash url, a no-slash baseURL with a no-slash url, and an absolute url through an instance.

What I validated, and how: I ran this PR's axios-to-rest.js with jscodeshift --dry --print on a sample that used the three baseURL shapes above. Then I called .url() on the emitted endpoints with the real @data-client/rest, and compared the results with axios.getUri({ baseURL, url }). The table comes from those runs. A second pass tried to disprove this finding. It confirmed that the old transform also copied paths verbatim, but this PR is the one that adds the '/' fallback, the absolute-URL split and the "working calls" claim. I did not run the repo's test suite or yarn build:skills.

Optional, not blocking: the router line in data-client/SKILL.md lists got, but rest-setup has no got migration reference.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread packages/codemods/src/transforms/axios-to-rest.js
Comment thread packages/codemods/src/transforms/axios-to-rest.js Outdated
Comment thread packages/codemods/src/transforms/axios-to-rest.js

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +815 to +819
if (key === 'params') {
if (prop.value.type === 'ObjectExpression') {
callArgs.push(...prop.value.properties);
} else {
callArgs.push(j.spreadElement(prop.value));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ba2982e.

  • A literal params key or query key that matches a path param leaves the call unconverted with a TODO.
  • Non-literal params are 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

Comment on lines +459 to +465
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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +815 to +819
if (key === 'params') {
if (prop.value.type === 'ObjectExpression') {
callArgs.push(...prop.value.properties);
} else {
callArgs.push(j.spreadElement(prop.value));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +830 to +834
if (url.urlPrefix) {
options.push(
j.objectProperty(
j.identifier('urlPrefix'),
j.stringLiteral(url.urlPrefix),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +530 to +534
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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

ntucker commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator Author

Confirmed, and fixed in this PR in 556d973. RestEndpoint.url() concatenates urlPrefix and the path as is, and the subclass's urlPrefix field runs after super(options), so it overrides the option.

Changes, for classes made from axios.create({ baseURL }) only:

  1. Every emitted path starts with /.
  2. A string-literal baseURL loses its trailing /. A non-literal one gets a TODO(data-client) asking to check its trailing slash.
  3. An absolute url through the instance is left as is with a TODO, so the class's headers aren't silently lost.

Bare axios paths are unchanged. I added tests for a trailing-slash baseURL with a leading-slash url, a no-slash baseURL with a no-slash url, and a non-literal baseURL plus an absolute url through an instance. All 136 codemod tests pass.

On the optional note: rest-setup does ship a got reference (.agents/skills/data-client-rest-setup/references/got-migration.md, linked from its detection table), so I kept got in the router line.


Generated by Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +486 to +489
props.push(
j.objectProperty(
j.identifier(key),
j.stringLiteral(decodeURIComponent(value)),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ba2982e: + is decoded as a space before decodeURIComponent(). c6eba13 adds a test for ?q=hello+world.


Generated by Claude Code

Comment on lines +681 to +684
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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +897 to +899
// 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) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
@ntucker
ntucker added this pull request to the merge queue Oct 25, 2026
Merged via the queue into master with commit 4f1c324 Oct 25, 2026
34 checks passed
@ntucker
ntucker deleted the claude/project-thread-stcwaw branch October 25, 2026 05:20

This branch was successfully deployed

1 active deployment
docs-preview — e6cb50d6 Deployed Oct 25, 2026 by ntucker via docs deploy #3994
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.

3 participants