Skip to content

feat(tooling): add antd-button-to-core codemod - #30754

Merged
chirag-madlani merged 2 commits into
mainfrom
antd-migration/button-codemod
Aug 1, 2026
Merged

feat(tooling): add antd-button-to-core codemod#30754
chirag-madlani merged 2 commits into
mainfrom
antd-migration/button-codemod

Conversation

@chirag-madlani

@chirag-madlani chirag-madlani commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Summary

The Button sweep's mechanical transform (Wave 1, #30565 / epic #30570), implementing the approved mapping in docs/antd-migration/button.md (landed in #30721):

  • type→color for all literals, including the subtle default inversion: antd's no-type default is default, core's is primary, so a bare <Button> gets explicit color="secondary" to preserve appearance
  • danger folds into -destructive colors branching on the base type; ghost/type="ghost" converts to color="tertiary" with a per-site ghost-remap warning for reviewer eyeballs
  • Approved size map small/middle/large → xs/sm/md; disabled→isDisabled, loading→isLoading, icon→iconLeading, htmlType→type (visual-type rewrite ordered first so the two never collide); block merges tw:w-full into className; ref passes through (forwardRef landed in feat(ui-core): forward refs on Button (+ vitest bootstrap) #30664)
  • Skip-and-warn (element stays antd, warning lists file + reason): shapes, Button.Group (incl. subpath imports), dynamic type/size, loading objects, dynamic classNames on block
  • Same partial-conversion safety as the Typography transform: unconverted elements keep antd, converted siblings use a CoreButton alias, imports managed with license-header preservation

Tests

44 new (37 inline transforms + 7 warning-behavior); full codemod suite 81/81 (move-named-imports 10 + typography 27 + button 44).

Fixes #30753

🤖 Generated with Claude Code

Greptile Summary

Adds an antd Button-to-core codemod and its documentation.

  • Maps supported Button visual variants, sizes, state props, icons, native button types, and full-width styling.
  • Preserves or aliases imports for files containing unsupported Button usages.
  • Adds inline transformation and warning-behavior tests.

Confidence Score: 4/5

The PR is not yet safe to merge because conditional and false-valued block props are still converted into unconditional full-width styling.

The transform checks only whether a block attribute exists and always appends tw:w-full, so block={false} and runtime conditions lose their original width behavior.

Files Needing Attention: tooling/antd-codemods/transforms/antd-button-to-core.js; tooling/antd-codemods/tests/antd-button-to-core.test.js

Important Files Changed

Filename Overview
tooling/antd-codemods/transforms/antd-button-to-core.js Implements Button classification, supported prop rewrites, skip warnings, and partial/full import conversion.
tooling/antd-codemods/tests/antd-button-to-core.test.js Adds inline coverage for supported mappings, unsupported-form skips, import handling, and warning output.
tooling/antd-codemods/README.md Documents the Button mapping, deliberate skips, partial-conversion behavior, and known limitations.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Find antd Button imports] --> B[Classify each Button usage]
  B -->|Unsupported shape or dynamic prop| C[Keep antd Button and warn]
  B -->|Supported props| D[Rewrite props for core Button]
  D --> E{Any skipped antd Buttons?}
  E -->|Yes| F[Keep antd import and add CoreButton alias]
  E -->|No| G[Replace antd import with core Button]
  C --> E
Loading

Reviews (2): Last reviewed commit: "Merge branch 'main' into antd-migration/..." | Re-trigger Greptile

Second Wave 1 transform (guide: docs/antd-migration/button.md). Maps
type->color incl. the no-type default inversion (antd default 'default' ->
explicit color=secondary since core defaults to primary), folds danger into
-destructive colors, remaps ghost->tertiary with a per-site review warning,
applies the approved size map (small/middle/large -> xs/sm/md), renames
disabled/loading/icon/htmlType with type-collision ordering handled, moves
block to tw:w-full, passes refs through (forwardRef landed in #30664), and
skips-with-warnings the hand-finish punch list (shapes, Button.Group,
dynamic type/size, loading objects). 44 tests; suite 81/81.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 31, 2026 09:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

❌ PR checklist incomplete

This PR cannot be merged until the following are addressed on its linked issue:

The fields live on the linked issue in the Shipping project (open the issue → right sidebar → Projects). After you set them, re-run this check (or push a commit) — issue/project changes do not re-trigger it automatically.

Maintainers can bypass this check by adding the skip-pr-checks label.

@github-actions github-actions Bot added safe to test Add this label to run secure Github workflows on PRs UI UI specific issues labels Jul 31, 2026
Comment on lines +323 to +337
root.find(j.JSXElement).forEach((elPath) => {
const classification = classifyElement(elPath.node);
if (!classification) {
return;
}
if (classification.kind === 'ButtonGroup') {
buttonSkips.push({ kind: 'Button.Group', reason: 'button-group' });
return;
}
if (classification.kind === 'ButtonGroupSubpath') {
subpathSkips.push({ kind: 'ButtonGroup', reason: 'button-group-subpath' });
return;
}
const result = convertElement(elPath.node);
if (result.skip) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Edge Case: Non-JSX references to Button break when antd import is removed

Full-conversion detection (fullyConvertedFile = buttonSkips.length === 0) and element matching only inspect j.JSXElement nodes. Any non-JSX reference to the imported Button identifier — e.g. styled(Button), Button.defaultProps, or passing Button as a prop/argument — is invisible to the transform, so the antd Button import is removed while the reference remains, producing code that no longer resolves Button. During a bulk sweep this silently emits broken files (caught only later at build time). Consider scanning for remaining Identifier references to buttonLocalName outside the converted JSX (mirroring the typography transform's bareUsageFound guard) and forcing partial-conversion/keeping the import when any exist.

Was this helpful? React with 👍 / 👎

Comment on lines +166 to +180
const ghostAttr = findAttr(attrs, 'ghost');
let ghostApplies = false;
if (ghostAttr) {
const lit = readAttrLiteral(ghostAttr);
if (!lit.isLiteral || typeof lit.value !== 'boolean') {
return { skip: true, reason: 'dynamic-ghost' };
}
ghostApplies = lit.value === true;
}

const typeAttr = findAttr(attrs, 'type');
let typeValue = null;
if (typeAttr) {
const lit = readAttrLiteral(typeAttr);
if (!lit.isLiteral || typeof lit.value !== 'string') {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Bug: ghost + danger silently drops destructive styling

When both ghost (or type="ghost") and danger are present, ghostApplies short-circuits color to 'tertiary' and the danger attribute is dropped (it's in EXCLUDE), losing the destructive intent that antd renders for a danger ghost button. The element still converts and emits only a generic ghost-remap warning that doesn't mention the discarded danger. Consider either skipping (skip: true) or emitting a distinct warning when ghostApplies && dangerApplies so reviewers know the destructive styling was dropped.

Was this helpful? React with 👍 / 👎

Comment on lines +271 to +285
const newAttrs = [];
attrs.forEach((attr) => {
if (attr.type !== 'JSXAttribute') {
newAttrs.push(attr); // JSXSpreadAttribute — pass through
return;
}
if (EXCLUDE.has(attr.name.name)) {
return; // handled below
}
newAttrs.push(attr);
});

newAttrs.push(j.jsxAttribute(j.jsxIdentifier('color'), j.stringLiteral(color)));
if (sizeValue) {
newAttrs.push(j.jsxAttribute(j.jsxIdentifier('size'), j.stringLiteral(sizeValue)));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Edge Case: Appended color/renamed props can override a trailing spread

Converted props (color, renamed size/isDisabled/etc., and merged className) are always appended at the end of the attribute list. For <Button type="primary" {...props}>, antd lets a later spread override the explicit prop, but after transform color is appended after the spread and now wins — a behavior change if props intended to override. This is a narrow edge case (spread-after-explicit); if worth handling, insert generated attributes at the original attribute's position rather than at the end.

Was this helpful? React with 👍 / 👎

Comment on lines +309 to +319
j.jsxAttribute(j.jsxIdentifier('className'), j.stringLiteral('tw:w-full'))
);
}
}

opening.attributes = newAttrs;
return { skip: false, warnings };
}

const buttonSkips = []; // ties up the antd Button import; forces partial conversion
const subpathSkips = []; // unrelated import; warn-only

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Conditional block becomes unconditional

When an antd button uses block={false} or block={isFullWidth}, the transform treats the attribute's presence as true and always appends tw:w-full, causing the converted button to remain full width even when block evaluates to false.

@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

✅ Playwright Results — workflow succeeded

Validated commit c2729cbe642f0ce3a9e319969212d51855d598b5 in Playwright run 30687837860, attempt 1.

✅ 549 passed · ❌ 0 failed · 🟡 1 flaky · ⏭️ 3 skipped · 🧰 0 lifecycle flaky

Performance

Blocking targets: ✅ met · Optimization targets: 🟡 in progress

Shard-job maxima below are not the full workflow wall time; the linked run includes build, fixture, planning, and reporting.

🕒 Full workflow signal wall (to summary) 48m 43s

⏱️ Max setup 2m 56s · max shard execution 15m 12s · max shard-job elapsed before upload 18m 10s · reporting 6s

🌐 201.74 requests/attempt · 2.83 app boots/UI scenario · 12.39% common-shard skew

Optimization targets still in progress:

  • Browser traffic was 201.74 requests per attempt (convergence target: fewer than 200).
  • Application boot ratio was 2.83 per UI scenario (1620 boots / 572 scenarios; convergence target: at most 1).
Shard Passed Failed Flaky Skipped Lifecycle failed Lifecycle flaky
✅ Shard chromium-01 85 0 0 0 0 0
✅ Shard chromium-02 115 0 0 3 0 0
🟡 Shard chromium-03 98 0 1 0 0 0
✅ Shard chromium-04 100 0 0 0 0 0
✅ Shard data-asset-rules-01 61 0 0 0 0 0
✅ Shard domain-isolation-01 14 0 0 0 0 0
✅ Shard global-state-01 34 0 0 0 0 0
✅ Shard ingestion-01 1 0 0 0 0 0
✅ Shard reindex-01 2 0 0 0 0 0
✅ Shard search-01 10 0 0 0 0 0
✅ Shard search-rbac-01 29 0 0 0 0 0
🟡 1 flaky test(s) (passed on retry)
  • Pages/Entity.spec.tsDomain Propagation (shard chromium-03, 1 retry)

📦 Download artifacts

How to debug locally
# Download playwright-test-results-<shard> artifact and unzip
npx playwright show-trace path/to/trace.zip    # view trace

@chirag-madlani
chirag-madlani added this pull request to the merge queue Jul 31, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🚦 Removed from the merge queue — failed_checks (2026-07-31T22:31:33Z)

These checks failed on merge-queue commit f5b2e0a:

@chirag-madlani
chirag-madlani added this pull request to the merge queue Aug 1, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Aug 1, 2026
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

🚦 Removed from the merge queue — failed_checks (2026-08-01T06:04:03Z)

These checks failed on merge-queue commit 5bb34fc:

Copilot AI review requested due to automatic review settings August 1, 2026 06:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

Suppressed comments (7)

tooling/antd-codemods/transforms/antd-button-to-core.js:269

  • After introducing blockApplies, className should only be excluded (and later re-added) when block actually applies. Otherwise block={false} will still drop the original className.
    if (blockAttr) {
      EXCLUDE.add('className');
    }

tooling/antd-codemods/transforms/antd-button-to-core.js:301

  • The full-width className merge should be gated on blockApplies (literal true) rather than the mere presence of a block attribute, to avoid turning block={false} into full width.
    if (blockAttr) {

tooling/antd-codemods/transforms/antd-button-to-core.js:237

  • block is treated as truthy based on attribute presence only. This will incorrectly add tw:w-full for block={false} and cannot safely represent block={expr} (should be skipped + warned). Parse block as a literal boolean and skip when it's dynamic; only apply full-width behavior when it resolves to true.

This issue also appears in the following locations of the same file:

  • line 267
  • line 301
    const blockAttr = findAttr(attrs, 'block');
    const classNameAttr = findAttr(attrs, 'className');
    if (blockAttr && classNameAttr) {
      const isPlainString =
        classNameAttr.value &&
        (classNameAttr.value.type === 'StringLiteral' ||
          classNameAttr.value.type === 'Literal');
      if (!isPlainString) {
        return { skip: true, reason: 'dynamic-classname' };
      }
    }

tooling/antd-codemods/tests/antd-button-to-core.test.js:229

  • The block mapping tests don’t cover block={false} (should not become full-width) or block={expr} (should be skipped + warned). Adding these cases will prevent regressions in the full-width conversion logic.
// -- `block` into existing/absent className + dynamic-className skip --

defineInlineTest(
  transform,
  OPTS,

tooling/antd-codemods/tests/antd-button-to-core.test.js:364

  • There’s no assertion that the skip warning includes a distinct reason for dynamic block={expr}. Adding a focused warning test helps ensure reviewers see the correct hand-finish category.
  it('warns with the file path and skip reason for a dynamic size', () => {
    transform(
      {
        path: 'src/components/Foo.tsx',
        source: `import { Button } from 'antd';\nconst App = ({ sz }) => <Button size={sz}>Click</Button>;`,

tooling/antd-codemods/README.md:44

  • README references .context/wave1-prep/button-gap-check.md, but that path isn’t present in the repo. This makes the guidance hard to follow for contributors.
Mapping rules approved 2026-07-30 — see
`.context/wave1-prep/button-gap-check.md` and the mapping guide
(`docs/antd-migration/button.md`) for the full survey data behind them.

tooling/antd-codemods/README.md:99

  • Once the transform skips block={expr} (dynamic) with a dedicated reason, the README should document that skip category alongside dynamic-classname so sweep reviewers know what to hand-finish.
- **`block` combined with a non-literal `className`** (e.g.
  `className={someVar}`) — `dynamic-classname`. The transform can only
  safely merge `tw:w-full` into a string literal.

@chirag-madlani
chirag-madlani added this pull request to the merge queue Aug 1, 2026
chirag-madlani pushed a commit that referenced this pull request Aug 1, 2026
…ock the merge queue (#30784)

* ci(playwright): raise the chromium shard budget to 21 minutes

The chromium lane outgrew a 19-minute shard. At the COMMON_MAX_SHARDS
ceiling of 24 the heaviest shard is predicted at 19.2m, so
assign_lane_within_budget() raises SystemExit and full-mode planning
aborts before a single test runs. Every merge_group run today failed
this way (PRs #30705, #30768, #30458, #30725, #30754), while
pull_request_target runs pass because targeted selection is far smaller.

Raise COMMON_SHARD_BUDGET_MS from 19m to 21m. At 24 shards the heaviest
is 19.2m, so the loop is guaranteed to converge at or before the
ceiling. 21m stays inside the 25m `timeout` wrapper around
`npx playwright test` and the 35m playwright-ci-postgresql job clock,
leaving ~4m of headroom.

Note the common lane now sits 1m above the dedicated lanes rather than
1m below. The strict 20-minute TARGET_MS ceiling is unaffected: it
bounds a single atomic unit, not a shard, so a 21m shard built from
units each under 20m does not trip it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(playwright): cover full-mode allocator convergence at the shard ceiling

Addresses review on #30784.

assign_lane_within_budget() was only exercised in "targeted" mode, so
neither the full-mode convergence path nor the SystemExit at
COMMON_MAX_SHARDS had coverage -- the exact code path that took the
merge queue down. Add both:

- test_full_mode_chromium_converges_at_the_shard_ceiling builds a lane
  that needs the window above 19m and asserts the allocator converges
  at or before the ceiling. Verified as a real guard: with the budget
  reverted to 19m it fails with "needs more than 24 shards ... heaviest
  shard is predicted at 20.4m".
- test_full_mode_chromium_reports_a_lane_the_ceiling_cannot_hold pins
  the SystemExit path, which had no coverage at all.

Also reword the budget comment: ~4m of headroom is relative to the 25m
playwright timeout wrapper specifically, not to the 35m job clock, which
is looser and additionally absorbs setup/teardown.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Merged via the queue into main with commit 36dfefd Aug 1, 2026
75 of 77 checks passed
@chirag-madlani
chirag-madlani deleted the antd-migration/button-codemod branch August 1, 2026 09:25
@gitar-bot

gitar-bot Bot commented Aug 1, 2026

Copy link
Copy Markdown
Code Review ⚠️ Changes requested 0 resolved / 3 findings

Adds the antd-button-to-core codemod with comprehensive test coverage, but non-JSX references break when antd imports are removed, ghost and danger combinations silently drop destructive styling, and appended converted props can override trailing spreads.

⚠️ Edge Case: Non-JSX references to Button break when antd import is removed

📄 tooling/antd-codemods/transforms/antd-button-to-core.js:323-337 📄 tooling/antd-codemods/transforms/antd-button-to-core.js:367-370 📄 tooling/antd-codemods/transforms/antd-button-to-core.js:401-415

Full-conversion detection (fullyConvertedFile = buttonSkips.length === 0) and element matching only inspect j.JSXElement nodes. Any non-JSX reference to the imported Button identifier — e.g. styled(Button), Button.defaultProps, or passing Button as a prop/argument — is invisible to the transform, so the antd Button import is removed while the reference remains, producing code that no longer resolves Button. During a bulk sweep this silently emits broken files (caught only later at build time). Consider scanning for remaining Identifier references to buttonLocalName outside the converted JSX (mirroring the typography transform's bareUsageFound guard) and forcing partial-conversion/keeping the import when any exist.

💡 Bug: ghost + danger silently drops destructive styling

📄 tooling/antd-codemods/transforms/antd-button-to-core.js:166-180 📄 tooling/antd-codemods/transforms/antd-button-to-core.js:242-250 📄 tooling/antd-codemods/transforms/antd-button-to-core.js:256-266

When both ghost (or type="ghost") and danger are present, ghostApplies short-circuits color to 'tertiary' and the danger attribute is dropped (it's in EXCLUDE), losing the destructive intent that antd renders for a danger ghost button. The element still converts and emits only a generic ghost-remap warning that doesn't mention the discarded danger. Consider either skipping (skip: true) or emitting a distinct warning when ghostApplies && dangerApplies so reviewers know the destructive styling was dropped.

💡 Edge Case: Appended color/renamed props can override a trailing spread

📄 tooling/antd-codemods/transforms/antd-button-to-core.js:271-285

Converted props (color, renamed size/isDisabled/etc., and merged className) are always appended at the end of the attribute list. For <Button type="primary" {...props}>, antd lets a later spread override the explicit prop, but after transform color is appended after the spread and now wins — a behavior change if props intended to override. This is a narrow edge case (spread-after-explicit); if worth handling, insert generated attributes at the original attribute's position rather than at the end.

🤖 Prompt for agents
Code Review: Adds the antd-button-to-core codemod with comprehensive test coverage, but non-JSX references break when antd imports are removed, ghost and danger combinations silently drop destructive styling, and appended converted props can override trailing spreads.

1. ⚠️ Edge Case: Non-JSX references to Button break when antd import is removed
   Files: tooling/antd-codemods/transforms/antd-button-to-core.js:323-337, tooling/antd-codemods/transforms/antd-button-to-core.js:367-370, tooling/antd-codemods/transforms/antd-button-to-core.js:401-415

   Full-conversion detection (`fullyConvertedFile = buttonSkips.length === 0`) and element matching only inspect `j.JSXElement` nodes. Any non-JSX reference to the imported `Button` identifier — e.g. `styled(Button)`, `Button.defaultProps`, or passing `Button` as a prop/argument — is invisible to the transform, so the antd `Button` import is removed while the reference remains, producing code that no longer resolves `Button`. During a bulk sweep this silently emits broken files (caught only later at build time). Consider scanning for remaining `Identifier` references to `buttonLocalName` outside the converted JSX (mirroring the typography transform's `bareUsageFound` guard) and forcing partial-conversion/keeping the import when any exist.

2. 💡 Bug: ghost + danger silently drops destructive styling
   Files: tooling/antd-codemods/transforms/antd-button-to-core.js:166-180, tooling/antd-codemods/transforms/antd-button-to-core.js:242-250, tooling/antd-codemods/transforms/antd-button-to-core.js:256-266

   When both `ghost` (or `type="ghost"`) and `danger` are present, `ghostApplies` short-circuits `color` to `'tertiary'` and the `danger` attribute is dropped (it's in EXCLUDE), losing the destructive intent that antd renders for a danger ghost button. The element still converts and emits only a generic `ghost-remap` warning that doesn't mention the discarded danger. Consider either skipping (`skip: true`) or emitting a distinct warning when `ghostApplies && dangerApplies` so reviewers know the destructive styling was dropped.

3. 💡 Edge Case: Appended color/renamed props can override a trailing spread
   Files: tooling/antd-codemods/transforms/antd-button-to-core.js:271-285

   Converted props (`color`, renamed `size`/`isDisabled`/etc., and merged `className`) are always appended at the end of the attribute list. For `<Button type="primary" {...props}>`, antd lets a later spread override the explicit prop, but after transform `color` is appended after the spread and now wins — a behavior change if `props` intended to override. This is a narrow edge case (spread-after-explicit); if worth handling, insert generated attributes at the original attribute's position rather than at the end.

Options

Display: compact → Showing less information.

Comment with these commands to change the behavior for this request:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar | Powered by Gitar — free for open source

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

safe to test Add this label to run secure Github workflows on PRs UI UI specific issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Wave 1: antd-button-to-core codemod

3 participants