Flex to Stack codemod

Replaces Pluma's Flex / PlumaFlex with Stack / InlineStack (PlumaStack / PlumaInlineStack), remapping the alignment props onto the axis they belong to and updating the import.

Flex exposes the raw flexbox model, so alignment is named after the main and cross axes — which swap meaning when the direction changes. Stack and InlineStack fix the direction and name the axes after the writing mode, so alignInline and alignBlock mean the same thing everywhere.

This is the bulk counterpart to the pluma/prefer-stack-over-flex lint rule. The rule keeps new Flex out of a codebase; this clears the existing ones.

Usage

Dry run first — both transforms report every element they skip, and that list is the manual-review queue.

pnx @customerio/pluma-cli@latest upgrade --codemod flex-to-stack react --dry -p ./src
pnx @customerio/pluma-cli@latest upgrade --codemod flex-to-stack ember --dry ./app

Then apply:

pnx @customerio/pluma-cli@latest upgrade --codemod flex-to-stack react ./src
pnx @customerio/pluma-cli@latest upgrade --codemod flex-to-stack ember ./app ./addon

What it does

table
Flexdirection="column" → Stackdirection="row" or absent → InlineStack
alignItemsalignInlinealignBlock
justifyContentalignBlockalignInline

direction is removed. gap, columnGap, rowGap, and every other Box sprinkle carry over unchanged — Stack and InlineStack forward them to Box exactly as Flex does.

A converted column also gains an explicit alignInline="start" when it had no alignItems, because Flex defaults its cross axis to start while Stack defaults alignInline to stretch. Without it the conversion would silently change the layout.

The import is updated in place: the replacement is added to the existing Pluma import (or as a sibling subpath import, matching how the file already imports Pluma), and Flex is removed once nothing else references it.

What it leaves alone

Each of these is skipped with a warning naming the file and the reason. Other elements in the same file still convert.

  • A direction that is not a literal "row" or "column" — a dynamic value, or row-reverse / column-reverse, which neither component supports.
  • wrap on a column: Stack has no wrap. A wrapping column usually wants Grid.
  • flexWrap on a column: Flex overwrites it with its own wrap, so it is inert today, and Stack would let it take effect.
  • alignBlock or alignInline written on the Flex itself: they do nothing there, but the replacement would honour them.
  • flexDirection: ignored by both replacements.
  • Spread props, and in Ember @unsafe_propspropsProxy falls back to it for any argument not passed explicitly, so it can carry direction unseen.
  • A column whose alignItems value cannot be read statically: Flex drops an undefined prop and falls back to start, where Stack would fall back to stretch, so the default cannot be carried over blind.
  • A prop set more than once, where the value the codemod reads is not the one in effect.
  • An aliased import (import { PlumaFlex as Row } in Ember): the template tag no longer names the component, so the file is reported for a manual change.
  • A replacement name already bound in the file — by another import, a local, a parameter, or a Glimmer block param such as {{#let x as |PlumaStack|}}. Adding the import would clash with that binding, or the converted element would resolve to it instead of the Pluma component.
  • Every element in a file where Flex itself is shadowed, block params included. Elements are matched by tag name, so there is no way to tell Pluma's component from the other one.
  • Every element in a file that imports Flex from Pluma more than once. Only one of those imports would be rewritten, leaving the other converted at the use site but still importing Flex.
  • Every element in a .gts/.gjs file whose JavaScript cannot be parsed, since the import cannot be read or updated. In practice this means files that are not really JavaScript, such as an Ember blueprint holding <%= %> placeholders.

Verifying a run

Run with --dry first. Both transforms report every skipped element, so the warning list is the manual-review queue. After applying, pluma/prefer-stack-over-flex will still flag whatever was skipped, so lint output and the warning list should agree.

Run your formatter afterwards. The Ember transform preserves template formatting exactly, but the React one reprints the enclosing JSX through recast, which reindents it — the same behaviour as the other React codemods here. Formatting the touched files keeps the diff to the change itself.