Skip to content

feat(bestax-migrate): convert .file inside a Field, building its tree from props - #830

Merged
allxsmith merged 4 commits into
mainfrom
feat/bulma-classes-file
Sep 30, 2026
Merged

allxsmith merged 4 commits into
mainfrom
feat/bulma-classes-file

Conversation

@allxsmith

Copy link
Copy Markdown
Owner

Closes #809. This is its last item, .file. .pagination-link/.pagination-ellipsis landed in #819 and .icon-text in #823. The ul/li inside .tabs is dropped, as I proposed on the issue: the only part that could match a bare <li> there is the deprecated Tabs.Item, and converting to it would just hand people another migration.

File renders the whole .file tree from props, and a .field of its own around it outside a Field, so until now the codemod left .file as markup. It now converts when the tree is exactly the one File renders and it sits inside a Field:

<div className="field">
  <div className="file has-name">
    <label className="file-label">
      <input className="file-input" type="file" name="cv" />
      <span className="file-cta">
        <span className="file-icon"><i className="fas fa-upload"></i></span>
        <span className="file-label">Upload</span>
      </span>
      <span className="file-name">cv.pdf</span>
    </label>
  </div>
</div>
// becomes
<Field>
  <File hasName name="cv" buttonLabel="Upload" iconLeft={<i className="fas fa-upload"></i>} fileName="cv.pdf" />
</Field>
  • The Field around it is a bestax one already in the file, or a .field that converts in the same run. The planner can't see the second case, since children are planned first, so a pass after planning checks it. When the .field stays markup, the .file stays too, with a context:File TODO.
  • The input gives its attributes to File, which puts the ones it doesn't read back on the <input>. Its other classes become inputClassName. An attribute File reads as a prop of its own (size, color, label, a helper name) refuses.
  • The parts: the button text becomes buttonLabel, left out when it's File's own default Choose a file…. Each .file-icon's content becomes iconLeft or iconRight, and the .file-name's text becomes fileName beside hasName. Mixed content is written as a fragment, with each text as the string it renders.
  • What stays a class: a color, since color renders has-text-<color> too, and is-centered, which renders nothing beside isRight. A has-name under a condition also stays in the joiner call. File renders {hasName && fileName && …}, and the truth test caught a 0 condition rendering as text.
  • What refuses:
    • any attribute on the .file but key, since File would move it to the <input>;
    • any class or attribute on a part of the tree;
    • text or a comment between the parts.

Tests:

  • The render truth test compares each tree shape inside a .field against the component inside a Field: icons, default text, input attributes and classes, a name, and the modifiers together. Swapping the icons in the planner on purpose fails it.
  • The props test typechecks what File is given.
  • Planner and transform tests, a fixture pair, and a .field with a .file in the kitchen sink.
  • A transform test caught comments on the parts' className attributes being lost. They now move to the name that stays.

Also in this PR:

  • MCP: lookup_bulma_classes describes what File is written from.
  • ESLint plugin: no-bulma-component-class now reports .file as a class the codemod converts.
  • Docs: the skill's maps and unmappables, and the guide.

pnpm all passes locally.

… from props

File renders the whole .file tree from props, and a .field of its own
around it outside a Field, so the family stayed markup. It now converts
when its tree is exactly the one File renders and it sits inside a Field:
a bestax one already in the file, or a .field converting in the same run,
which a pass after planning checks.

The <input>'s attributes become File's own, since File puts the ones it
doesn't read there, and its other classes become inputClassName. The
button text becomes buttonLabel (left out when it's File's own default),
each .file-icon's content iconLeft or iconRight, and the .file-name's text
fileName beside hasName. The .file itself carries no attribute but a key,
since that would move to the <input> too. A color stays a class, since
color renders has-text-<color> as well, and so does is-centered beside
isRight. A has-name under a condition stays in the call: File renders
{hasName && ...}, and the truth test caught a 0 condition rendering as
text.

The render truth test wraps the markup in a .field and the component in a
Field for each tree shape; the props test typechecks the <input>
attributes File takes.

Closes #809
…rom props

lookup_bulma_classes now tells an agent that File is written from the
<input>'s attributes, the button text, the icons and the file name, and
that outside a Field it renders a .field of its own. The index carries the
table's new buildsFile, and the agree test hands the planner the tree and
the .field around it, as the codemod sees them.
The regenerated metadata moves file from the families the codemod leaves
as markup to the classes it converts, so no-bulma-component-class points
at bestax-migrate for it.
@coderabbitai

coderabbitai Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 15 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Repository: allxsmith/bestax/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 49ac9441-a472-4fa9-a91b-b67854409bfa

📥 Commits

Reviewing files that changed from the base of the PR and between 9740ee9 and 212831e.

⛔ Files ignored due to path filters (1)
  • eslint-plugin/src/generated/metadata.ts is excluded by !**/generated/**
📒 Files selected for processing (21)
  • bestax-mcp/data/bulma-classes.json
  • bestax-mcp/data/skills.json
  • bestax-mcp/src/__tests__/bulma-classes-agree.test.ts
  • bestax-mcp/src/bulma-classes.ts
  • bestax-migrate/CLAUDE.md
  • bestax-migrate/e2e/bulma-classes-props.test.ts
  • bestax-migrate/e2e/bulma-classes-truth.test.ts
  • bestax-migrate/e2e/support/file-tree.ts
  • bestax-migrate/fixtures/bulma-classes-kitchen-sink/src/elements.tsx
  • bestax-migrate/src/sources/bulma-classes/__testfixtures__/file.input.tsx
  • bestax-migrate/src/sources/bulma-classes/__testfixtures__/file.output.tsx
  • bestax-migrate/src/sources/bulma-classes/__tests__/plan.test.ts
  • bestax-migrate/src/sources/bulma-classes/__tests__/transform.test.ts
  • bestax-migrate/src/sources/bulma-classes/class-map.ts
  • bestax-migrate/src/sources/bulma-classes/plan.ts
  • bestax-migrate/src/sources/bulma-classes/transform.ts
  • docs/docs/guides/getting-started/migration/bulma-classes.md
  • scripts/gen-mcp-index.mjs
  • skills/bestax-migrate/references/bulma-classes/component-map.md
  • skills/bestax-migrate/references/bulma-classes/prop-map.md
  • skills/bestax-migrate/references/bulma-classes/unmappables.md

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

❤️ Share

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

@github-actions

Copy link
Copy Markdown
Contributor

Preview Deployment

Preview URL: https://93a22a59.bestax.pages.dev

Comment thread bestax-migrate/src/sources/bulma-classes/transform.ts Outdated

@claude claude 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.

Deep review — 1 blocking · 2 advisory

# Severity Area Finding Location
1 🟡 Minor Correctness textValue guards ", & and newlines but not \; recast's StringLiteral printer doubles a backslash, and a JSX attribute string has no escapes — so fileName/inputClassName/buttonLabel can render text the markup didn't bestax-migrate/src/sources/bulma-classes/transform.ts:878
2 🔵 Advisory Performance readChild now recurses the whole subtree, and childElementsOf(element, readChild) runs once per element, so fact-gathering is O(Σ subtree size) rather than one level deep bestax-migrate/src/sources/bulma-classes/transform.ts:1181
3 🔵 Advisory Coverage The has-name-under-a-condition path (onlyTrue, the 0-renders-as-text case the PR body cites) has no test pairing a conditional has-name with a .file-name in the tree bestax-migrate/src/sources/bulma-classes/__tests__/plan.test.ts:1050

Overall: Sound, and unusually well fenced for a whole-tree conversion — the shape check is exact-match on every node, the <input>-attribute hand-off refuses anything File reads as a prop of its own, and the truth test renders markup-in-.field against component-in-Field for each tree shape, which is the only thing that could really prove this. I verified it empirically: pnpm --filter bestax-migrate test is 53 suites / 2123 tests green (the e2e suites need bulma-ui built first), and bestax-mcp and the ESLint plugin are green too. The riskiest part is the two-phase Field gate — the planner sees only classesAround, and the post-planning pass at transform.ts:1367 is what actually decides — so read that pass first; I traced the demotion orders and it is safe (the field entry has no folds, and the file-level block() nulls every conversion together, so there is no way to land a File whose Field went away). The one blocking item is a narrow printing bug in new code that the repo's own jsxStringValue helper already solves.

Residual risk:

  • A File converted outside a runtime Field, rendering a nested .field. Refuted. The post-pass walks syntactic ancestors for a bestax Field or a .field planned to become one; Field.tsx:286 returns <FieldProvider value={true}> wrapping everything it renders, including renderedLabel and the horizontal Field.Body path, so any syntactic Field ancestor — children, label prop, message prop — really does set useInsideField(). The other direction (a runtime Field the walk cannot see, e.g. <Field>{renderFile()}</Field>) only refuses, which is the safe side. Confirmed in the fixture: the .file with no .field around it stays markup with context:File.
  • Index drift between the planner's parts/children and the transform's inside(...). Refuted. childElementsOf returns undefined unless every non-dropped child is a JSXElement, and buildFile refuses on undefined at each level, so by the time transform.ts:1553 re-derives the same lists with !dropped, the two filters are provably the same set.
  • Comments lost with the tree. Refuted by reading moved (transform.ts:1577) against every node it deletes: tags, tag names, all parts' attributes, the <input>'s dropped className/type, and the comment children of element/label/cta. Comments inside a part that becomes a prop ride along in contentValue's fragment — and a comment child makes textOf return undefined, so the buttonLabel-equals-default omission can never silently swallow one.
  • hasName rendering a falsy number as text. Refuted: has-name carries onlyTrue, so a conditional stays in the joiner and hasName is only ever written bare. The sibling flags (isBoxed, isFullwidth, isRight) do not need it — File consumes them in a classNames object or an if, never as a child. Open, untested: the conditional-has-name-with-a-.file-name combination (advisory 3).
  • Attribute-type drift moving the <input>'s attributes onto File. Refuted for the two props whose types actually differ (size, color are Omited and are in ownProps, giving an attr: refusal, test-covered) and for type (skipped, hardcoded). numberAttrsOf(File) is just tabIndex, and the truth test feeds it. ref correctly passes through — File is in FORWARDS_REF and ref-roster.test.ts holds that to the real component.
  • textValue's other escape hazards beyond finding 1. <, >, {, } are literal inside a JSX attribute string and need no guard; ' never triggers an escape because recast's "auto" quote picks the shorter rendering, which is always the double-quoted one here. Control characters cannot reach it — jsxTextRenders maps tabs to spaces and the regex already rejects \r\n. So \ is the whole gap.
  • Generated-artefact staleness (MCP index, ESLint metadata, skill rosters). Not re-run locally: the gen:*:check scripts need approval in this sandbox. CI's own steps cover it; the buildsFile field is present in all three outputs and bestax-mcp's agree test passes against the committed index.

🏄 Whole gnarly .file tree collapsing into one clean <File /> — that's a proper barrel, dude, and the truth test riding markup and component side by side is exactly the wax job this deserved. Just one grain of sand on the board: a backslash slips past textValue and comes out doubled. Sand that off and paddle out. 🤙

A JSX attribute string has no escapes, and recast prints a string literal
with its backslash doubled, so a fileName, inputClassName or buttonLabel
holding one rendered two. It now goes in as an expression, as a quote or an
& already did.

Each element's tree is read once now: every element around it read it
again, which made gathering the facts grow with the depth of the tree. A
plan that changes after it's been read forgets the reading. And a test pins
that a has-name under a condition doesn't count for a .file-name.
@allxsmith

Copy link
Copy Markdown
Owner Author

On the advisories: both are done in 212831e. Each element's tree is now read once and cached, and a plan that changes after it's read forgets the reading. A planner test now pins that a has-name under a condition doesn't count for a .file-name.

@allxsmith

Copy link
Copy Markdown
Owner Author

deep-review: fresh

212831e answers the first review: the backslash fix on its thread, plus both advisories, a cache so each element's tree is read once and a test for a conditional has-name. The cache is new code this review hasn't seen. This review is of the whole PR as it now stands.

@github-actions

Copy link
Copy Markdown
Contributor

Preview Deployment

Preview URL: https://f865cd58.bestax.pages.dev

@allxsmith

Copy link
Copy Markdown
Owner Author

deep-review: verify the open thread

212831e fixes the backslash thread and adds the two advisories' changes (a reading cache and a conditional has-name test). Three fresh runs of this PR were cut short by the runner before posting, and the first full review already covered the rest of the diff.

@claude claude 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.

Deep review (verify) — 1 resolved · 0 open

# Severity Area Finding Location
1 🟡 Minor Correctness textValue let a backslash through and the printer doubled it — verified fixed, resolved bestax-migrate/src/sources/bulma-classes/transform.ts:880

What I checked

  • textValue now reads /^[^"&\\\r\n]*$/, so a text holding a backslash takes the
    jsxExpressionContainer branch — the same branch " and & already took. All three
    call sites share the helper: buttonLabel (:913), inputClassName (:1575),
    fileName (:1582).
  • The regression test at __tests__/transform.test.ts:1312 feeds C:\Uploads as a
    .file-label and asserts the printed buttonLabel={"C:\\Uploads"}. It passes. With the
    old character class that text matched, so it printed as a JSX attribute string and the
    assertion could not hold — the test is a real guard, not a restatement.
  • Keeping the local helper rather than _shared/jsx-utils.ts's jsxStringValue is right:
    the local one also guards &, which jsxStringValue does not.
  • Suite state: the bulma-classes unit set is green, 269 passed. The one red suite,
    __tests__/value-unions.test.ts, fails on Cannot find module '@allxsmith/bestax-bulma/constants' — an unbuilt sibling in this checkout, unrelated to
    the diff.

Overall: This pass settled threads and reviewed no commits. The backslash fix is
correct at the single choke point every affected prop flows through, and it ships with a
test that fails against the old guard. Nothing is left open from the first review; the two
advisory-driven changes in 212831e2 (the reading cache and the conditional has-name test)
were not re-reviewed here — a fresh run is what covers those.

Residual risk: Out of scope for a verify pass — this pass judged only the single open
thread, not the failure class or the rest of the diff.

🏄 Paddled back out for just the one wave and it closed out clean — backslash bug's
handled at the one spot all three props funnel through, with a test that actually wipes
out on the old code. All threads cleared, nothing left bobbing in the lineup.

@claude claude 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.

Deep review (verify) — 1 resolved · 0 open

No. Severity Area Finding Location Status
1 🟡 Minor Correctness textValue let a backslash through and the printer doubled it bestax-migrate/src/sources/bulma-classes/transform.ts:880 ✅ Resolved

Overall: This pass settled threads and reviewed no commits. The single open thread was already answered by 212831e, and had been replied to and resolved at this same SHA by the previous verify run; this pass re-confirmed the fix independently rather than taking that on trust, and found nothing still wrong. Nothing is left open, so a human can go straight to the change itself.

What I checked on the one thread
  • Guard: the negated class in textValue now covers a backslash as well as the quote, the ampersand and the line breaks (transform.ts:880), so a text holding a backslash falls to the jsxExpressionContainer branch. Keeping the local helper over the shared jsxStringValue is the right call: it also guards the ampersand, which jsxStringValue does not, and an attribute holding an entity would otherwise be re-read as the character it names.

  • Blast radius: all three consumers share the one helper — buttonLabel (913), inputClassName (1575), fileName (1582) — so the fix covers every site the finding named.

  • Printer behaviour, exercised directly against the repo copy of jscodeshift/recast rather than reasoned about:

    bare literal   : <File buttonLabel="C:\\Uploads" />   old branch: a JSX attribute string has no escapes, so this renders TWO backslashes
    expr container : <File buttonLabel={"C:\\Uploads"} />  new branch: a JS string, so ONE backslash
    
  • The regression test bites, and is not tautological: the new case at transform.test.ts:1312 feeds a Windows path as a .file-label and asserts the printed buttonLabel takes the braced, JS-string form. Under the old class, which had no backslash in it, that text matches, so it prints as the bare attribute form above, which lacks the brace the assertion requires. The test could not pass before the fix.

  • Suite: the bulma-classes transform suite is green, 151 passed. The one red suite in the wider package run is the render harness failing to resolve an unbuilt bestax-bulma dist, which is pre-existing and unrelated.

Residual risk: none identified for the addressed class.

  • The sibling emitter textChild (transform.ts:867) needs no matching change: it emits j.jsxText, which recast prints raw, and JSX text has no backslash escapes, as the original finding said, and that code is unchanged.
  • The ampersand case that the local helper is kept for lives in the same character class, so hardening for the backslash did not trade away the entity-decoding guard.

🏄 Paddled back out to check the one wave that was still breaking, and yeah, that backslash fix is clean, the test behind it actually bites, and the lineup is empty. All green, go surf it.

@allxsmith allxsmith added the needs-human-review Loop converged (or contested): awaiting owner review + manual merge label Sep 29, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📸 Story screenshots at handoff — 212831e

No Storybook stories map to this PR's changed files — nothing to screenshot. (workflow run)

@allxsmith
allxsmith merged commit dea0857 into main Sep 30, 2026
91 of 94 checks passed
@allxsmith
allxsmith deleted the feat/bulma-classes-file branch September 30, 2026 01:26
@bestax-release-bot

Copy link
Copy Markdown

🎉 This PR is included in version 5.18.1 🎉

The release is available on:

Your semantic-release bot 📦🚀

@bestax-release-bot

Copy link
Copy Markdown

🎉 This PR is included in version 2.24.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

@bestax-release-bot

Copy link
Copy Markdown

🎉 This PR is included in version 1.13.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

@bestax-release-bot

Copy link
Copy Markdown

🎉 This PR is included in version 4.2.13 🎉

The release is available on:

Your semantic-release bot 📦🚀

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

Labels

deep-review needs-human-review Loop converged (or contested): awaiting owner review + manual merge released

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] bestax-migrate: the Bulma families that still need a new conversion shape

1 participant