DataTable TanStack Table v9 codemodDataTable is built on TanStack Table v9 in v3. This codemod migrates the four
mechanical parts of that move. Two of them are Ember-only, because Pluma's
hand-rolled Ember integration was replaced by TanStack's own
@tanstack/ember-table.
flexRenderWrapper → flexRenderComponentThe @customerio/pluma-components/ember/tanstack/table entry point is deleted
in v3. Three things change together:
@tanstack/ember-tableflexRenderWrapper(…) becomes flexRenderComponent(…)@props, and now gets the cell/header context as @ctx plus the custom
object as @optionsStep 3 is the one that needs judgement, so it's kept narrow. For every component
passed as the first argument to a flexRenderWrapper/flexRenderComponent
call, if that component is declared in the same file the codemod rewrites,
inside that declaration only:
@props → @options (in the <template>)this.args.props → this.args.options (in the JS region)props key under Args in the component's signature type — both the
inline (TOC<{ Args: { props: … } }>) and named
(Component<NameCellSignature>, declared in the same file) shapes@props elsewhere in the file is never touched, so a cell component sitting next
to unrelated components is safe.
sortingFn → sortFnv9 renamed the column-definition option. Leaving it silently disables sorting for that column rather than erroring, which is why this one is worth automating.
It's only renamed when the object literal is provably a column definition: it
carries one of accessorKey, accessorFn, header, cell or id, or it's
an argument to columnHelper.accessor(…) / .display(…) / .group(…). Anything
else is left alone and listed for manual review — including a sortingFn nested
in meta.
table.getState() → table.store.stategetState() is gone in v9. To avoid rewriting an unrelated getState() (a
zustand store, a state machine, your own class), only receivers that read like a
table are touched: the identifier table, dataTable or tableInstance, or any
identifier/member ending in table (case-insensitive) — usersTable,
this.args.dataTable, peopleTable. Anything else is left alone and listed.
Every rewrite is reported, because store.state is a live snapshot object rather
than a call: code that captured const state = table.getState() and expected it
to stay frozen behaves differently.
createColumnHelper features type parameterv9 added a leading features type parameter. The codemod only fires when the call
has exactly one type argument and createColumnHelper is imported from
@tanstack/table-core, @tanstack/react-table or @tanstack/ember-table (the
adapters re-export table-core wholesale). Two type arguments means it's already
migrated.
DataTableFeatures is folded into an existing
@customerio/pluma-components/{react,ember} import when the file has one
(import { DataTable, type DataTableFeatures } from …), otherwise a dedicated
import type line is added.
meta.getCellColSpan. Deprecated, but still works: it's mapped onto v9's
spanColumns for you. The callback's signature changed (it took the cell, and
spanColumns takes { row, column, table }) and "no span" changed from
undefined to 1, so a mechanical rewrite would be wrong. Migrate it when you
touch the column, and you also get Infinity support in exchange.Table,
Row, Cell, Column and ColumnDef all take it now
(Table<DataTableFeatures, Person>). Only createColumnHelper is automated,
because it's the one that's always called the same way; the types appear in
aliases, generics and prop signatures where an insertion isn't safely
mechanical. TypeScript will point at every one of them.FlexRender (Ember). It split into FlexRenderCell, FlexRenderHeader
and FlexRenderFooter, each taking the cell/header/footer directly. There's no
1:1 replacement, so the codemod reports it instead of guessing.Run it through the Pluma CLI from inside your consuming app. Always start with a dry run to review the diff, the warnings and the review notes before applying:
Both frameworks accept --dry / -d. The React path also forwards standard
jscodeshift flags.
The Ember transform walks .gts, .gjs, .ts and .js. .hbs is skipped —
none of these APIs appear in a template-only file, and a classic component's
@props can't be migrated without its backing class (see below).
Everything below is left exactly as it was and printed in the "needs manual
review" summary with a file:line:
| Situation | Why |
|---|---|
The deleted entry point is imported more than once, or exports something other than flexRenderWrapper | The flexRender migration is skipped for that file - import and call sites alike - so it stays consistent. The independent renames still apply |
| The cell component is imported from another module | Its @props are in a file we can't tie back to this call |
The cell component is declared in a .ts/.js file | Its template is a separate .hbs — only half the rename would land |
The first argument isn't a plain identifier (flexRenderWrapper(row.cell, …)) | We can't tell which component to rewrite |
The component is also invoked directly with @props= | Renaming its argument would break that call site |
The component destructures let { props } = this.args | The argument name isn't spelled out at the rewrite site |
| A signature shared with a component that isn't being migrated | Renaming Args.props for one of them would break the other's @props |
The component reads .props off something else (a constructor's args) | Only @props and this.args.props are rewritten; the rest is by hand |
The deleted entry point is re-exported (export … from …) | A re-export isn't rewritten — repoint the barrel by hand |
flexRenderWrapper is also bound locally (a parameter, a variable) | Not every call by that name is the import's |
sortingFn in an object we can't prove is a column def, or next to an existing sortFn | Guessing here would either miss sorting or duplicate a key |
getState() on a receiver that doesn't read like a table | It's more likely someone else's getState |
createColumnHelper() with no type argument | There's nothing to put the features parameter in front of |
createColumnHelper in a file that declares its own DataTableFeatures | The import the migration needs would redeclare that name |
| A file that fails to parse | Never worth risking a mangled file |
Sites that were changed but deserve a look (each migrated cell component, and
every getState() rewrite) are printed in the review-notes section instead.
Both transforms compute text edits against the original source rather than
reprinting an AST. That's what keeps a 700-line column-definition file's diff
down to the lines that actually changed, and it's the only way to edit a .gts
file, whose <template> regions no JS parser can read.
For .gts/.gjs, content-tag locates the template regions and each one is
blanked in place — same character count, same newlines, with a valid stand-in
(_pt0; for a class-member template, 0 for an expression one) at the front. The
remainder parses as ordinary TS, and because nothing shifted, an AST offset in the
masked source is the same offset in the real file.
One parser gotcha: jscodeshift hands transforms a recast-wrapped AST whose
start/end/loc.column are measured in tab-expanded columns, so a
tab-indented file would put every edit in the wrong place. Both transforms parse
with jscodeshift's underlying babel parser directly (jscodeshift/parser/tsx) and
walk the tree with a small type index in lib/table-edits.cjs.
Tests use Node's built-in test runner (node --test).