Skip to content

feat(bestax-migrate): convert an .image around anything but a bare <img> - #804

Merged
allxsmith merged 4 commits into
mainfrom
feat/bulma-classes-image-children
Sep 28, 2026
Merged

allxsmith merged 4 commits into
mainfrom
feat/bulma-classes-image-children

Conversation

@allxsmith

Copy link
Copy Markdown
Owner

Closes #801. An .image converted only together with a bare <img> (#797), so these kept both as markup:

<figure className="image is-16by9">
  <iframe className="has-ratio" src="https://www.youtube.com/embed/x" allowFullScreen />
</figure>
<figure className="image is-48x48">
  <img src="/lazy.png" alt="Lazy" loading="lazy" />
</figure>
// now
<Image as="figure" className="is-16by9">
  <iframe className="has-ratio" src="https://www.youtube.com/embed/x" allowFullScreen />
</Image>
<Image as="figure" size="48x48">
  <img src="/lazy.png" alt="Lazy" loading="lazy" />
</Image>

How it decides

  • Image renders any children it's given as they are, in place of its own <img>. So the absorb spec gains elseWraps: when the child can't be absorbed, the planner converts the element around its children like any other element, and they stay as written.
  • The bare-<img> form still comes first, since it gives <Image src alt />.
  • An empty .image still refuses, now with a children:Image message saying Image would render an <img> of its own there.

The other surfaces

  • MCP (feat(bestax-mcp)): the lookup adds that other children go inside Image as they are.
  • Skill and docs: the component map's .image bullet and the migration guide say so. The unmappables drop the <img> from the spread, attr and dynamic-class cases it no longer blocks, and say an .image gets children:Image only when it's empty.

Checked

  • The truth test's absorb pass now expects an elseWraps entry to convert around every child it can't absorb, rendering the same markup, instead of refusing, and still to refuse with nothing inside. Its render helper gives a non-absorbing conversion the raw child as its children.
  • Planner tests for the bare, fallback and empty cases. The absorbed fixture adds an <iframe> and an empty .image, and its <img loading> case now converts. All render the same before and after.
  • pnpm all passes locally.

An .image converted only together with a bare <img>, so an <iframe
class="has-ratio"> or an <img> with loading or width kept both as markup.
Image renders any children it's given as they are, in place of its own
<img>, so the absorb spec gains elseWraps: when the child can't be absorbed,
the element converts around its children instead. The bare-<img> form still
comes first, and an empty .image still refuses, with a message saying Image
would render an <img> of its own.

Closes #801
…img>

lookup_bulma_classes now adds that Image renders other children as given,
so they go inside it as they are.
@coderabbitai

coderabbitai Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 30 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: 4389d435-e8d8-4a56-9fa7-d293eee1d9d5

📥 Commits

Reviewing files that changed from the base of the PR and between a44b310 and aeebfdb.

📒 Files selected for processing (13)
  • bestax-mcp/data/bulma-classes.json
  • bestax-mcp/data/skills.json
  • bestax-mcp/src/__tests__/server.test.ts
  • bestax-mcp/src/bulma-classes.ts
  • bestax-migrate/e2e/bulma-classes-truth.test.ts
  • bestax-migrate/src/sources/bulma-classes/__testfixtures__/absorbed.input.tsx
  • bestax-migrate/src/sources/bulma-classes/__testfixtures__/absorbed.output.tsx
  • bestax-migrate/src/sources/bulma-classes/__tests__/plan.test.ts
  • bestax-migrate/src/sources/bulma-classes/class-map.ts
  • bestax-migrate/src/sources/bulma-classes/plan.ts
  • docs/docs/guides/getting-started/migration/bulma-classes.md
  • skills/bestax-migrate/references/bulma-classes/component-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.

@allxsmith

Copy link
Copy Markdown
Owner Author

deep-review: fresh

Runs here get stopped by the runner at 15 to 20 minutes (#800), so post your review by about your 30th tool call, even if short, and list anything you didn't reach under Residual risk. Don't run builds or whole suites; CI runs them on this head.

Review only the core:

  • The elseWraps fallback in plan() (bestax-migrate/src/sources/bulma-classes/plan.ts), against bulma-ui's Image.tsx. When the child can't be absorbed, does converting around the children render exactly the same markup, and does an empty .image still refuse?
  • The image entry in class-map.ts.

@github-actions

Copy link
Copy Markdown
Contributor

Preview Deployment

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

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

@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 · 1 advisory

No Severity Area Finding Location
1 🟠 Major Correctness elseWraps converts an .image whose sole rendered child is a JSX expression; Image picks its content with a truthiness test on children, so a falsy expression makes it render an <img> of its own where the <figure> rendered nothing bestax-migrate/src/sources/bulma-classes/plan.ts:293
2 🔵 Advisory Coverage The truth test elseWraps branch renders only truthy children, and its two class-shaped refusal rows (empty child class, computed child class) render the child with no className in both halves, since renderBoth sets one only when sole.tokens?.length — so those rows prove the container, not the child bestax-migrate/e2e/bulma-classes-truth.test.ts:628

Overall: The mechanism is the right one, and the container half is genuinely equal: Image renders image + is-<size> + has-ratio (ratio sizes only) + helpers + className, the ratio tokens stay in omits so they land in className, and the wrap path leaves the child JSX completely untouched — which is why dropping the <img> from the spread, attr and dynamic-class unmappables is correct rather than optimistic. The riskiest part is the one fact the planner reads statically: hasChildren means "there is JSX inside", while Image decides with children ? … : <img/>, and the two disagree for exactly the single-expression child that formatted JSX produces. That is finding 1, and where the human should look first.

Residual risk:

  • A discarded refusal leaking a TODO — refuted: refuse builds a new array (plan.ts:256) and never mutates the outer one, so throwing the absorb outcome away leaves no phantom entry; the new plan test pins the todos to exactly children:Image for the empty case.
  • The child losing something in the wrap path — refuted: absorbed stays undefined, so no absorbs.props are emitted and the codemod rewrites only the element. A child loading, width, key or ref, a child spread, an empty or computed child className, and is-rounded on the child all survive verbatim, which is what makes the unmappables edits accurate.
  • Container class drift — refuted: the new plan.test.ts case pins the leftover className to is-16by9 for a ratio, and Image adds has-ratio only from a size containing by, so a ratio kept as a class renders image is-16by9 exactly.
  • Empty .image — refuted: hasChildren: false refuses with children:Image, asserted in both plan.test.ts and the new elseWraps tail of the truth test.
  • Open: the empty-element gate now sits ahead of the dangerouslySetInnerHTML gate, so an .image carrying dangerouslySetInnerHTML and no JSX children refuses with the children:Image message about an empty element rather than the attr:dangerouslySetInnerHTML one. It still refuses, so no markup is at risk — only which rule and wording the report shows.
  • Open (not reached): I did not run the suites or the bulma-classes corpus script on this head (runner time cap). CI covers the suites; the corpus is deliberately out of CI, so a breadth pass over real .image markup is still worth doing before release.

🏄 Clean little swell, this one — Image already rides whatever children you hand it, so letting the .image wrap instead of bail is the right line. Just watch that one rip current: a falsy expression child, and Image paddles out an empty <img> of its own. Patch that and it is all glass.

…they're elements

Image tests its children for truth, so an .image around an expression
({src && <img />}) that came out falsy would render an <img> of its own
where the markup rendered nothing. The fallback now converts only around
HTML elements written out, and refuses anything else with a
children:Image TODO.

The truth test also renders a child's empty or computed className in both
halves, so those rows test the child as well as the container.
@allxsmith

Copy link
Copy Markdown
Owner Author

deep-review: verify

Check the fix for the open thread (an expression child) in 570c6fd: the new condition on the elseWraps fallback in plan().

Keep it short: runs here get stopped by the runner at 15 to 20 minutes (#800), so read just that commit and don't run builds or whole suites (CI runs them on this head).

@github-actions

Copy link
Copy Markdown
Contributor

Preview Deployment

Preview URL: https://2544f9d7.bestax.pages.dev

@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 Thread Location Outcome
1 🟠 Major Correctness elseWraps converted an .image whose sole child is an expression, and Image renders its own <img> when that expression is falsy bestax-migrate/src/sources/bulma-classes/plan.ts:293 ✅ Verified fixed — resolved

Overall: This pass settled the one open thread from the earlier review and reviewed no commits. The fallback in plan() now converts only when the children are HTML elements written out — soleChild, or a non-empty childElements — both of which make props.children a React element or an array, so Image can never fall through to rendering its own <img>; anything else refuses with a children:Image TODO. Nothing here needs a human beyond the usual squash-merge read; CI owns the builds and the e2e truth suite.

Residual risk:

  • A fact path that still admits a falsy child — refuted: transform.ts is the only producer of these facts, and soleChild (872) requires a single non-dropped JSXElement while childElementsOf (712) bails to undefined on any non-JSXElement or non-intrinsic child, so no expression, text, or component child reaches the convert path.
  • Regression in what still converts — refuted empirically: plan.test.ts 58/58 and transform.test.ts 130/130 pass on this head, the absorbed fixture pinning the new refusal alongside the existing .image conversions.
  • Over-refusal — open, and harmless: an .image holding an element plus a comment sibling, or one wrapping only text, now refuses where it would have converted safely. That direction only leaves markup as markup with a TODO.
  • The e2e truth suite needs bulma-ui/dist, so its new plan-level expression assertion was read rather than run; CI executes it on this head.

🏄 Clean little set on this one, dude — the codemod stopped dropping a phantom <img> into the falsy branch and just leaves the markup chillin where it cannot read the wave. Only thread paddled in, good to go.

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

Copy link
Copy Markdown
Contributor

📸 Story screenshots at handoff — aeebfdb

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

@allxsmith
allxsmith merged commit 49bb27b into main Sep 28, 2026
43 checks passed
@allxsmith
allxsmith deleted the feat/bulma-classes-image-children branch September 28, 2026 11:23
@bestax-release-bot

Copy link
Copy Markdown

🎉 This PR is included in version 2.16.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

@bestax-release-bot

Copy link
Copy Markdown

🎉 This PR is included in version 1.9.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

@bestax-release-bot

Copy link
Copy Markdown

🎉 This PR is included in version 5.18.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: convert an .image around anything but a bare <img>, keeping its children

1 participant