bestax-migrate 2.10.0 → 2.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -29,6 +29,12 @@ export interface Modifier {
29
29
  readonly writes: readonly PropWrite[];
30
30
  /** The raw tags the writes are exact on (Title's size picks its heading). */
31
31
  readonly tagIn?: readonly string[];
32
+ /**
33
+ * The prop renders the class for `true` alone, not for any truthy value
34
+ * (`Field`'s `grouped`), so a condition a joiner adds it under can't be
35
+ * handed to the prop as it is.
36
+ */
37
+ readonly onlyTrue?: boolean;
32
38
  }
33
39
  /**
34
40
  * - `mapped`: converts to `target`.
@@ -131,6 +137,17 @@ export interface RootEntry {
131
137
  readonly folds?: readonly PropWrite[];
132
138
  /** The target renders the element's only child itself. */
133
139
  readonly absorbs?: Absorbs;
140
+ /** Props the target needs to render this class at all (`variant="lines"`). */
141
+ readonly writes?: readonly PropWrite[];
142
+ /**
143
+ * The target renders the element's children itself, `prop` of them, each
144
+ * a bare, empty `<tag>`. The element converts only when its children are
145
+ * just that, and they go (`.skeleton-lines`' `<div>`s, as `lines`).
146
+ */
147
+ readonly countsChildren?: {
148
+ readonly tag: string;
149
+ readonly prop: string;
150
+ };
134
151
  /** The target takes no helper props, so every helper class stays a class. */
135
152
  readonly noHelpers?: boolean;
136
153
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"class-map.d.ts","sourceRoot":"","sources":["../../../src/sources/bulma-classes/class-map.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"class-map.d.ts","sourceRoot":"","sources":["../../../src/sources/bulma-classes/class-map.ts"],"names":[],"mappings":"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"}
@@ -621,12 +621,16 @@ export const ROOTS = {
621
621
  tag: 'div',
622
622
  // `has-addons-centered`, `is-grouped-right` and the like stay classes:
623
623
  // the prop values that render them render `has-addons` too.
624
- modifiers: flags({
625
- 'is-horizontal': 'horizontal',
626
- 'is-grouped': 'grouped',
627
- 'has-addons': 'hasAddons',
628
- 'is-narrow': 'narrow',
629
- }),
624
+ modifiers: {
625
+ ...flags({
626
+ 'is-horizontal': 'horizontal',
627
+ 'has-addons': 'hasAddons',
628
+ 'is-narrow': 'narrow',
629
+ }),
630
+ // `grouped` renders `is-grouped` for `true` and its three layouts, and
631
+ // for nothing else truthy.
632
+ 'is-grouped': { writes: [{ prop: 'grouped' }], onlyTrue: true },
633
+ },
630
634
  wrapsChildren: {
631
635
  in: 'field-body',
632
636
  unless: ['Field.Label', 'Field.Body'],
@@ -1155,6 +1159,28 @@ export const ROOTS = {
1155
1159
  absorbs: { tag: 'ul', attributesOn: 'element' },
1156
1160
  ownProps: ['alignment', 'separator', 'size'],
1157
1161
  },
1162
+ // `Skeleton` takes no helper props: what it isn't given as its own reaches
1163
+ // the DOM as an attribute, so every helper class stays a class.
1164
+ 'skeleton-block': {
1165
+ status: 'mapped',
1166
+ target: 'Skeleton',
1167
+ tag: 'div',
1168
+ textColor: null,
1169
+ bgColor: null,
1170
+ noHelpers: true,
1171
+ ownProps: ['variant', 'lines'],
1172
+ },
1173
+ 'skeleton-lines': {
1174
+ status: 'mapped',
1175
+ target: 'Skeleton',
1176
+ tag: 'div',
1177
+ textColor: null,
1178
+ bgColor: null,
1179
+ noHelpers: true,
1180
+ writes: [{ prop: 'variant', value: 'lines' }],
1181
+ countsChildren: { tag: 'div', prop: 'lines' },
1182
+ ownProps: ['variant', 'lines'],
1183
+ },
1158
1184
  // ---- Families bestax has but that do not map element by element yet ------
1159
1185
  checkbox: todo('Checkbox', "bestax `Checkbox` renders its own styled markup, not Bulma's"),
1160
1186
  checkboxes: todo('Checkboxes', 'bestax `Checkboxes` renders its own `.field` and `.control` wrappers'),
@@ -1205,8 +1231,6 @@ export const ROOTS = {
1205
1231
  'panel-icon': part(),
1206
1232
  'panel-list': part(),
1207
1233
  tabs: todo('Tabs', "bestax `Tabs` renders each tab's `<li>` and `<a>` together, with tab roles"),
1208
- 'skeleton-block': todo('Skeleton', 'bestax `Skeleton` renders its own markup'),
1209
- 'skeleton-lines': todo('Skeleton', 'bestax `Skeleton` renders its own markup'),
1210
1234
  // ---- Wrappers a component renders from a prop ---------------------------------
1211
1235
  // Each renders the wrapper with its own class and nothing else, so the
1212
1236
  // element folds only when it carries no attribute and no other class.
@@ -13,6 +13,19 @@ export interface ElementFacts {
13
13
  tag: string;
14
14
  /** The static className, split on whitespace, in order. */
15
15
  tokens: readonly string[];
16
+ /**
17
+ * For a `className` a class joiner builds (`clsx('button', busy &&
18
+ * 'is-loading')`), the classes each condition adds, one list per condition;
19
+ * `tokens` then holds the ones it always adds. Set only when the codemod
20
+ * can read every argument.
21
+ */
22
+ conditional?: readonly (readonly string[])[];
23
+ /**
24
+ * Indexes into `conditional` of conditions that may have side effects (a
25
+ * call, an assignment). A condition that becomes a prop is evaluated before
26
+ * the ones left in the call, so it passes them only when neither is one.
27
+ */
28
+ impure?: readonly number[];
16
29
  /**
17
30
  * The element's other attributes: a string value, `true` for a bare
18
31
  * attribute, or null for an expression.
@@ -38,6 +51,12 @@ export interface ElementFacts {
38
51
  * entry that `absorbs` it reads.
39
52
  */
40
53
  soleChild?: ChildFacts;
54
+ /**
55
+ * The element's children, when every one React renders is a plain HTML
56
+ * element; left out when one is anything else (text, an expression, a
57
+ * component). What an entry that `countsChildren` reads.
58
+ */
59
+ childElements?: readonly ChildFacts[];
41
60
  /** The bestax components already in the file inside this element. */
42
61
  bestaxInside?: readonly string[];
43
62
  /** The bestax components already in the file around this element. */
@@ -67,6 +86,11 @@ export interface ChildFacts {
67
86
  */
68
87
  attributes: ReadonlyMap<string, string | number | true | null>;
69
88
  hasSpread: boolean;
89
+ /**
90
+ * Nothing inside it but whitespace React drops. A comment is something:
91
+ * an element that goes would take it with it.
92
+ */
93
+ isEmpty: boolean;
70
94
  }
71
95
  export interface Todo {
72
96
  rule: string;
@@ -99,6 +123,16 @@ export interface Conversion {
99
123
  /** Child attributes a prop above renders instead. */
100
124
  drop: string[];
101
125
  };
126
+ /**
127
+ * The target renders the element's children itself, from a prop above:
128
+ * they go, and the element closes itself.
129
+ */
130
+ rendersChildren?: true;
131
+ /**
132
+ * Conditional classes (`ElementFacts.conditional`) that become a boolean
133
+ * prop set to their condition, each once. The rest stay in the joiner call.
134
+ */
135
+ conditional?: Array<[prop: string, token: string]>;
102
136
  }
103
137
  export interface Plan {
104
138
  conversion: Conversion | null;
@@ -1 +1 @@
1
- {"version":3,"file":"plan.d.ts","sourceRoot":"","sources":["../../../src/sources/bulma-classes/plan.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAiBH,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,2DAA2D;IAC3D,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1B;;;OAGG;IACH,UAAU,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC;IACtD,SAAS,EAAE,OAAO,CAAC;IACnB,MAAM,EAAE,OAAO,CAAC;IAChB,4EAA4E;IAC5E,WAAW,EAAE,OAAO,CAAC;IACrB;;;;OAIG;IACH,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,SAAS,CAAC,EAAE,UAAU,CAAC;IACvB,qEAAqE;IACrE,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,qEAAqE;IACrE,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC;;;OAGG;IACH,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ;;;OAGG;IACH,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAAC;IAClC;;;OAGG;IACH,UAAU,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC;IAC/D,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,IAAI;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,UAAU;IACzB,0CAA0C;IAC1C,MAAM,EAAE,MAAM,CAAC;IACf,6DAA6D;IAC7D,KAAK,EAAE,KAAK,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC;IACnD,4DAA4D;IAC5D,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,mEAAmE;IACnE,IAAI,EAAE,MAAM,EAAE,CAAC;IACf;;;OAGG;IACH,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB;;;;OAIG;IACH,OAAO,CAAC,EAAE;QACR,oEAAoE;QACpE,KAAK,EAAE,KAAK,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC;QACnD,4DAA4D;QAC5D,OAAO,EAAE,KAAK,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC;QAC3C,qDAAqD;QACrD,IAAI,EAAE,MAAM,EAAE,CAAC;KAChB,CAAC;CACH;AAED,MAAM,WAAW,IAAI;IACnB,UAAU,EAAE,UAAU,GAAG,IAAI,CAAC;IAC9B;;;OAGG;IACH,IAAI,CAAC,EAAE;QACL,MAAM,EAAE,MAAM,CAAC;QACf,KAAK,EAAE,KAAK,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC;QACnD,OAAO,EAAE,MAAM,EAAE,CAAC;KACnB,CAAC;IACF,KAAK,EAAE,IAAI,EAAE,CAAC;CACf;AA+BD,wBAAgB,IAAI,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI,CA0S9C"}
1
+ {"version":3,"file":"plan.d.ts","sourceRoot":"","sources":["../../../src/sources/bulma-classes/plan.ts"],"names":[],"mappings":"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"}
@@ -42,7 +42,13 @@ function tagsItReaches(entry) {
42
42
  export function plan(facts) {
43
43
  const todos = [];
44
44
  const { tag, tokens } = facts;
45
- for (const token of tokens) {
45
+ // Every class the element may carry: its fixed ones, and any a condition
46
+ // adds, which the element has to be right with as well as without.
47
+ const carried = [
48
+ ...new Set([...tokens, ...(facts.conditional ?? []).flat()]),
49
+ ];
50
+ const mayCarry = (token) => carried.includes(token);
51
+ for (const token of carried) {
46
52
  const hint = legacyHint(token);
47
53
  if (hint)
48
54
  todos.push({ rule: ruleId('legacy', token), message: hint });
@@ -50,7 +56,7 @@ export function plan(facts) {
50
56
  // A family this source leaves as markup keeps its element as markup, even
51
57
  // beside a class it would convert (`navbar box`): the family's parts carry no
52
58
  // TODO of their own, so its outermost class is the one place it is flagged.
53
- const family = tokens.find(token => {
59
+ const family = carried.find(token => {
54
60
  const found = rootFor(token);
55
61
  return found?.status === 'todo' && !found.part;
56
62
  });
@@ -74,7 +80,7 @@ export function plan(facts) {
74
80
  else {
75
81
  // A root the table does not convert keeps the element as markup, so a
76
82
  // later pass (or a person) still finds it as the class it is.
77
- if (tokens.some(token => rootFor(token))) {
83
+ if (carried.some(token => rootFor(token))) {
78
84
  return { conversion: null, todos };
79
85
  }
80
86
  entry = wrapperEntry(tag);
@@ -110,6 +116,11 @@ export function plan(facts) {
110
116
  if (attributes.has('dangerouslySetInnerHTML')) {
111
117
  return refuse('attr', 'dangerouslySetInnerHTML', `\`dangerouslySetInnerHTML\` sets the element's content directly, and some bestax components render content of their own beside \`children\`, which React rejects; keep this element as markup`);
112
118
  }
119
+ // Every fact about the element's children comes from the JSX inside it, so
120
+ // children passed as an attribute are content none of them sees.
121
+ if (attributes.has('children')) {
122
+ return refuse('attr', 'children', `this element sets \`children\` as an attribute, and the codemod reads an element's children from the JSX inside it, so it can't tell what bestax \`${target}\` would render from these; move them inside the element, then re-run`);
123
+ }
113
124
  for (const name of attributes.keys()) {
114
125
  if (renamed.has(name))
115
126
  continue;
@@ -158,11 +169,20 @@ export function plan(facts) {
158
169
  }
159
170
  const wraps = entry.wrapsChildren;
160
171
  if (wraps &&
161
- (!wraps.when || tokens.includes(wraps.when)) &&
172
+ (!wraps.when || mayCarry(wraps.when)) &&
162
173
  (facts.hasChildren || wraps.whenEmpty) &&
163
174
  !facts.childTargets?.some(child => wraps.unless.includes(child))) {
164
175
  return refuse('children', target, `bestax \`${target}\` renders its children inside a \`.${wraps.in}\` of its own unless one of them is a ${orList(wraps.unless)}, so this element stays markup`);
165
176
  }
177
+ const counts = entry.countsChildren;
178
+ const counted = facts.childElements?.every(child => child.tag === counts?.tag &&
179
+ child.tokens === undefined &&
180
+ child.attributes.size === 0 &&
181
+ !child.hasSpread &&
182
+ child.isEmpty);
183
+ if (counts && !counted) {
184
+ return refuse('children', target, `bestax \`${target}\` renders this element's children itself, \`${counts.prop}\` bare, empty <${counts.tag}>s, so it converts only when its children are just that; keep it as markup`);
185
+ }
166
186
  if (missing.length > 0) {
167
187
  const list = missing.map(([name, value]) => `\`${name}="${value}"\``);
168
188
  return refuse('defaults', target, `bestax \`${target}\` renders ${list.join(' and ')} when the element does not set ${missing.length === 1 ? 'it' : 'them'}; add ${missing.length === 1 ? 'it' : 'them'} here if that is what you want, then re-run`);
@@ -178,7 +198,8 @@ export function plan(facts) {
178
198
  }
179
199
  }
180
200
  // ---- Tokens → props ---------------------------------------------------------
181
- const writes = new Map();
201
+ // What the root itself needs comes first, so no class can take its prop.
202
+ const writes = new Map((entry.writes ?? []).map(write => [write.prop, write.value ?? true]));
182
203
  /** The tokens each written prop came from, so a group rule can undo it. */
183
204
  const sourceOf = new Map();
184
205
  const groupOf = new Map();
@@ -254,8 +275,49 @@ export function plan(facts) {
254
275
  if (as)
255
276
  props.push(['as', as]);
256
277
  props.push(...writes);
278
+ if (counts) {
279
+ props.push([counts.prop, String(facts.childElements.length)]);
280
+ numbers.push(counts.prop);
281
+ }
282
+ // A class a condition adds becomes the boolean prop that renders it, set to
283
+ // that condition, when it is one of the root's flags on its own: the
284
+ // component renders it exactly when the condition is truthy, as the joiner
285
+ // does. Anything else a condition adds stays in the joiner call.
286
+ const parts = facts.conditional ?? [];
287
+ const conditional = [];
288
+ let conditionalStays = false;
289
+ const places = (token) => parts.filter(part => part.includes(token)).length +
290
+ (tokens.includes(token) ? 1 : 0);
291
+ const paired = new Set(Object.values(entry.absorbs?.pairs ?? {}));
292
+ const impure = new Set(facts.impure ?? []);
293
+ let impureStays = false;
294
+ parts.forEach((part, index) => {
295
+ const [token] = part;
296
+ const modifier = part.length === 1 && places(token) === 1 && !paired.has(token)
297
+ ? modifierFor(entry, token)
298
+ : undefined;
299
+ const [write, ...more] = modifier?.writes ?? [];
300
+ const flag = write !== undefined &&
301
+ more.length === 0 &&
302
+ write.value === undefined &&
303
+ !modifier.onlyTrue &&
304
+ (!modifier.tagIn || modifier.tagIn.includes(tag)) &&
305
+ !writes.has(write.prop) &&
306
+ !conditional.some(([prop]) => prop === write.prop);
307
+ // Written as a prop, the condition is evaluated before any left in the
308
+ // call ahead of it, which is safe only when neither can have side effects.
309
+ const reorders = conditionalStays && (impureStays || impure.has(index));
310
+ if (flag && !reorders) {
311
+ conditional.push([write.prop, token]);
312
+ }
313
+ else {
314
+ conditionalStays = true;
315
+ if (impure.has(index))
316
+ impureStays = true;
317
+ }
318
+ });
257
319
  const rest = tokens.filter(token => !converted.has(token));
258
- if (entry.ownClassOnly && rest.length > 0) {
320
+ if (entry.ownClassOnly && (rest.length > 0 || conditionalStays)) {
259
321
  return refuse('attr', 'className', `bestax \`${target}\` drops its own class when it is given a \`className\`, so this element's other classes would take the place of \`.${root}\`; keep it as markup`);
260
322
  }
261
323
  return {
@@ -266,6 +328,8 @@ export function plan(facts) {
266
328
  drop,
267
329
  numbers,
268
330
  ...(absorbed ? { absorbs: absorbed } : {}),
331
+ ...(counts ? { rendersChildren: true } : {}),
332
+ ...(conditional.length > 0 ? { conditional } : {}),
269
333
  },
270
334
  todos,
271
335
  };
@@ -294,7 +358,7 @@ function absorb(facts, root, entry, target, refuse) {
294
358
  return refuse('spread', target, `the <${spec.tag}> inside spreads props, which bestax \`${target}\` may read differently than the <${spec.tag}> did; convert the two to \`${target}\` by hand`);
295
359
  }
296
360
  if (child.tokens === null) {
297
- return refuse('dynamic-class', target, `the <${spec.tag}> inside has a computed \`className\`, and the codemod converts static class strings only; convert the two to bestax \`${target}\` by hand, turning each condition into its prop`);
361
+ return refuse('dynamic-class', target, `the <${spec.tag}> inside has a computed \`className\`, which the codemod does not read there; convert the two to bestax \`${target}\` by hand, turning each condition into its prop`);
298
362
  }
299
363
  if (child.tokens?.length === 0) {
300
364
  return refuse('attr', 'className', `the <${spec.tag}> inside has an empty \`className\`, which renders \`class=""\`, and bestax \`${target}\` renders it with no class attribute; drop the empty \`className\`, then re-run`);
@@ -1 +1 @@
1
- {"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../../../src/sources/bulma-classes/rules.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAUH,eAAO,MAAM,KAAK,EAAE,WAAW,CAAC,MAAM,CAapC,CAAC;AAyBH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEnD;AAED,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAK1D"}
1
+ {"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../../../src/sources/bulma-classes/rules.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAUH,eAAO,MAAM,KAAK,EAAE,WAAW,CAAC,MAAM,CAapC,CAAC;AA0BH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEnD;AAED,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAK1D"}
@@ -40,8 +40,9 @@ const VOCABULARY = new Set([
40
40
  ...Object.values(WRAPPERS),
41
41
  ...Object.values(WRAPPER_OWN_PROPS).flat(),
42
42
  ...HELPER_PROPS,
43
- // Refused on every target, so no entry lists it.
43
+ // Refused on every target, so no entry lists them.
44
44
  'dangerouslySetInnerHTML',
45
+ 'children',
45
46
  // Refused where the target drops its own class for one it is given.
46
47
  'className',
47
48
  // Refused on the child a target renders itself.
@@ -1 +1 @@
1
- {"version":3,"file":"transform.d.ts","sourceRoot":"","sources":["../../../src/sources/bulma-classes/transform.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAW,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE1D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AA8YvD,MAAM,CAAC,OAAO,UAAU,SAAS,CAC/B,QAAQ,EAAE,QAAQ,EAClB,GAAG,EAAE,GAAG,EACR,OAAO,GAAE,gBAAqB,GAC7B,MAAM,GAAG,SAAS,CAwZpB"}
1
+ {"version":3,"file":"transform.d.ts","sourceRoot":"","sources":["../../../src/sources/bulma-classes/transform.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAW,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE1D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAmtBvD,MAAM,CAAC,OAAO,UAAU,SAAS,CAC/B,QAAQ,EAAE,QAAQ,EAClB,GAAG,EAAE,GAAG,EACR,OAAO,GAAE,gBAAqB,GAC7B,MAAM,GAAG,SAAS,CAygBpB"}
@@ -21,8 +21,9 @@ import { addTodo, findAttr, forgetJsxParens, jsxNameParts, literalValueOf, makeA
21
21
  import { collectBoundNames, makeReserve, prefersTabs, resolvesToBinding, } from '../_shared/imports.js';
22
22
  import { rewriteStylesheetImports } from '../_shared/css-imports.js';
23
23
  import { BESTAX, buildBestaxImport, placeBestaxImport, seedBestaxImport, } from '../_shared/bestax-import.js';
24
- import { plan } from './plan.js';
24
+ import { plan, } from './plan.js';
25
25
  import { REACT_RUNTIMES, } from './project.js';
26
+ import { rootFor } from './class-map.js';
26
27
  import { ruleId } from './rules.js';
27
28
  /* eslint-disable @typescript-eslint/no-explicit-any */
28
29
  /**
@@ -48,48 +49,293 @@ const HEADER_PRAGMA = /^[\s*]*(?:@ts-(?:no)?check|@flow|@noflow|eslint-disable(?
48
49
  /** Directives that act on the line after them, so must stay above it. */
49
50
  const NEXT_LINE_DIRECTIVE = /^[\s*]*(?:eslint-disable-next-line|@ts-expect-error|@ts-ignore|prettier-ignore)\b/;
50
51
  /**
51
- * A comment moved to sit after a JSX tag name. It has to be a block comment
52
- * there: recast prints a line comment as `<// …`, which TypeScript reads as a
53
- * closing tag.
52
+ * A comment moved into a JSX tag, before or after a node there. It has to be
53
+ * a block comment: after a tag name recast prints a line comment as `<// …`,
54
+ * which TypeScript reads as a closing tag, and before an attribute it would
55
+ * run on into it.
54
56
  */
55
- function afterTagName(comment) {
56
- if (comment.type !== 'CommentLine') {
57
- return { ...comment, leading: false, trailing: true };
58
- }
57
+ function asBlock(comment, trailing) {
58
+ const placed = { leading: !trailing, trailing };
59
+ if (comment.type !== 'CommentLine')
60
+ return { ...comment, ...placed };
59
61
  return {
60
62
  type: 'CommentBlock',
61
63
  value: ` ${comment.value.trim().replace(/\*\//g, '*\\/')} `,
62
- leading: false,
63
- trailing: true,
64
+ ...placed,
64
65
  };
65
66
  }
67
+ /** A comment moved to sit after a JSX tag name. */
68
+ function afterTagName(comment) {
69
+ return asBlock(comment, true);
70
+ }
66
71
  /**
67
- * Put `props`, then what stays in `className`, where an element's
68
- * `className` was. A comment on the className goes with whatever takes its
69
- * place: the first new attribute, else the next one along, else the tag name.
72
+ * Put `replacement` where an element's `className` was. A comment on the
73
+ * className, and any `carried` from inside it, goes with whatever takes its
74
+ * place: the first new attribute, else the next one along, else the tag
75
+ * name. A className kept in `replacement` keeps its own.
70
76
  */
71
- function writeClassName(j, element, props, className) {
77
+ function spliceClassName(element, replacement, carried = []) {
72
78
  const attrs = element.openingElement.attributes;
73
79
  const classAttr = findAttr(element, 'className');
74
- const replacement = props.map(([name, value]) => makeAttr(j, name, value === true ? undefined : value));
75
- if (className)
76
- replacement.push(makeAttr(j, 'className', className));
77
- if (classAttr.comments?.length) {
80
+ const kept = replacement.includes(classAttr);
81
+ const moving = [...(kept ? [] : (classAttr.comments ?? [])), ...carried];
82
+ if (moving.length > 0) {
78
83
  const name = element.openingElement.name;
79
84
  const carrier = replacement[0] ?? attrs.find((attr) => attr !== classAttr) ?? name;
80
85
  carrier.comments = [
81
86
  ...(carrier.comments ?? []),
82
- ...classAttr.comments.map((comment) => carrier === name ? afterTagName(comment) : comment),
87
+ ...moving.map((comment) => carrier === name ? afterTagName(comment) : comment),
83
88
  ];
84
89
  }
85
90
  attrs.splice(attrs.indexOf(classAttr), 1, ...replacement);
86
91
  }
92
+ /** Put `props`, then what stays in `className`, where `className` was. */
93
+ function writeClassName(j, element, props, className) {
94
+ const replacement = props.map(([name, value]) => makeAttr(j, name, value === true ? undefined : value));
95
+ if (className)
96
+ replacement.push(makeAttr(j, 'className', className));
97
+ spliceClassName(element, replacement);
98
+ }
99
+ /**
100
+ * The same, for a `className` a joiner builds: the classes a prop now
101
+ * renders leave the call, each condition that became a prop moves onto it,
102
+ * and what is left stays in the call (a plain string when nothing in it is
103
+ * conditional, no `className` when nothing is left). Returns whether the
104
+ * call is gone.
105
+ */
106
+ function writeJoined(j, element, conversion, joined) {
107
+ const { call, statics, parts } = joined;
108
+ const kept = new Set(conversion.className?.split(' ') ?? []);
109
+ const carried = [];
110
+ const remove = (list, node) => list.splice(list.indexOf(node), 1);
111
+ for (const node of statics) {
112
+ const classes = classesIn(staticText(node));
113
+ const remaining = classes.filter(token => kept.has(token));
114
+ if (remaining.length === classes.length)
115
+ continue;
116
+ if (remaining.length === 0) {
117
+ carried.push(...commentsWithin(node));
118
+ remove(call.arguments, node);
119
+ continue;
120
+ }
121
+ const literal = j.stringLiteral(remaining.join(' '));
122
+ literal.comments = node.comments;
123
+ call.arguments[call.arguments.indexOf(node)] = literal;
124
+ }
125
+ const conditions = (conversion.conditional ?? []).map(([prop, token]) => {
126
+ const part = parts.find(found => found.tokens.length === 1 && found.tokens[0] === token);
127
+ carried.push(...commentsWithin(part.node, part.condition));
128
+ if (part.object) {
129
+ remove(part.object.properties, part.node);
130
+ if (part.object.properties.length === 0) {
131
+ carried.push(...commentsWithin(part.object));
132
+ remove(call.arguments, part.object);
133
+ }
134
+ }
135
+ else {
136
+ remove(call.arguments, part.node);
137
+ }
138
+ const value = part.negate
139
+ ? j.unaryExpression('!', part.condition)
140
+ : part.condition;
141
+ return j.jsxAttribute(j.jsxIdentifier(prop), j.jsxExpressionContainer(value));
142
+ });
143
+ const classAttr = findAttr(element, 'className');
144
+ let rest = [classAttr];
145
+ const gone = call.arguments.every((arg) => staticText(arg) !== null);
146
+ if (gone) {
147
+ carried.push(...commentsWithin(classAttr.value));
148
+ const classes = call.arguments.flatMap((arg) => classesIn(staticText(arg)));
149
+ rest =
150
+ classes.length > 0 ? [makeAttr(j, 'className', classes.join(' '))] : [];
151
+ }
152
+ spliceClassName(element, [
153
+ ...conversion.props.map(([name, value]) => makeAttr(j, name, value === true ? undefined : value)),
154
+ ...conditions,
155
+ ...rest,
156
+ ], carried.map(comment => asBlock(comment, false)));
157
+ return gone;
158
+ }
87
159
  /** A lowercase intrinsic tag: not a component, not a custom element. */
88
160
  const INTRINSIC = /^[a-z][a-z0-9]*$/;
89
161
  /** Subtrees whose tags are not HTML. */
90
162
  const FOREIGN_ROOTS = new Set(['svg', 'math']);
91
163
  /** Parents that render their only child as it is. */
92
164
  const PASS_THROUGH_PARENTS = new Set(['Fragment', 'React.Fragment']);
165
+ /**
166
+ * Class joiners whose arguments the codemod can read exactly, by package and
167
+ * export: a string adds its classes, and an object key, `a && 'x'` or
168
+ * `a ? 'x' : ''` adds them when the condition is truthy. Not `clsx/lite`,
169
+ * which ignores objects, `classnames/dedupe`, where a later key can take a
170
+ * class back, or `classnames/bind`, which looks names up in a CSS module.
171
+ */
172
+ const JOINERS = {
173
+ clsx: ['default', 'clsx'],
174
+ classnames: ['default'],
175
+ };
176
+ /** A string literal's or an expression-free template's text, else null. */
177
+ function staticText(node) {
178
+ if (node?.type === 'StringLiteral')
179
+ return node.value;
180
+ if (node?.type === 'TemplateLiteral' && node.expressions.length === 0) {
181
+ return node.quasis[0].value.cooked ?? null;
182
+ }
183
+ return null;
184
+ }
185
+ function classesIn(text) {
186
+ return text.split(/\s+/).filter(Boolean);
187
+ }
188
+ /** A joiner argument that adds nothing: `''`, `null`, `undefined`, `false`. */
189
+ function addsNothing(node) {
190
+ if (staticText(node)?.trim() === '')
191
+ return true;
192
+ if (node.type === 'NullLiteral')
193
+ return true;
194
+ if (node.type === 'BooleanLiteral')
195
+ return node.value === false;
196
+ return node.type === 'Identifier' && node.name === 'undefined';
197
+ }
198
+ /**
199
+ * Whether evaluating an expression can't change anything: names, literals,
200
+ * property reads, and operators over those. A call, an assignment, `new` or
201
+ * `delete` can. (So can a getter; a property read is taken as a read.)
202
+ */
203
+ function sideEffectFree(node) {
204
+ switch (node?.type) {
205
+ case 'Identifier':
206
+ case 'StringLiteral':
207
+ case 'NumericLiteral':
208
+ case 'BooleanLiteral':
209
+ case 'NullLiteral':
210
+ case 'BigIntLiteral':
211
+ case 'ThisExpression':
212
+ return true;
213
+ case 'TemplateLiteral':
214
+ return node.expressions.every(sideEffectFree);
215
+ case 'MemberExpression':
216
+ case 'OptionalMemberExpression':
217
+ return (sideEffectFree(node.object) &&
218
+ (!node.computed || sideEffectFree(node.property)));
219
+ case 'UnaryExpression':
220
+ return node.operator !== 'delete' && sideEffectFree(node.argument);
221
+ case 'BinaryExpression':
222
+ case 'LogicalExpression':
223
+ return sideEffectFree(node.left) && sideEffectFree(node.right);
224
+ case 'ConditionalExpression':
225
+ return [node.test, node.consequent, node.alternate].every(sideEffectFree);
226
+ case 'TSAsExpression':
227
+ case 'TSSatisfiesExpression':
228
+ case 'TSNonNullExpression':
229
+ case 'TSTypeAssertion':
230
+ case 'ParenthesizedExpression':
231
+ return sideEffectFree(node.expression);
232
+ default:
233
+ return false;
234
+ }
235
+ }
236
+ /**
237
+ * A `className={joiner(…)}` whose every argument the codemod can read, or
238
+ * undefined: an argument it can't (a variable, a call, a spread, an array)
239
+ * could add any class.
240
+ */
241
+ function readJoiner(value, isJoiner) {
242
+ const call = value?.type === 'JSXExpressionContainer' ? value.expression : null;
243
+ if (call?.type !== 'CallExpression' || !isJoiner(call.callee)) {
244
+ return undefined;
245
+ }
246
+ const statics = [];
247
+ const parts = [];
248
+ for (const arg of call.arguments) {
249
+ if (staticText(arg) !== null) {
250
+ statics.push(arg);
251
+ }
252
+ else if (arg.type === 'ObjectExpression') {
253
+ for (const property of arg.properties) {
254
+ const plain = (property.type === 'ObjectProperty' ||
255
+ property.type === 'Property') &&
256
+ !property.computed &&
257
+ !property.method &&
258
+ (property.kind === undefined || property.kind === 'init');
259
+ const key = !plain
260
+ ? undefined
261
+ : property.key.type === 'Identifier'
262
+ ? property.key.name
263
+ : staticText(property.key);
264
+ if (key == null)
265
+ return undefined;
266
+ parts.push({
267
+ tokens: classesIn(key),
268
+ condition: property.value,
269
+ negate: false,
270
+ node: property,
271
+ object: arg,
272
+ });
273
+ }
274
+ }
275
+ else if (arg.type === 'LogicalExpression' &&
276
+ arg.operator === '&&' &&
277
+ staticText(arg.right) !== null) {
278
+ parts.push({
279
+ tokens: classesIn(staticText(arg.right)),
280
+ condition: arg.left,
281
+ negate: false,
282
+ node: arg,
283
+ });
284
+ }
285
+ else if (arg.type === 'ConditionalExpression' &&
286
+ staticText(arg.consequent) !== null &&
287
+ addsNothing(arg.alternate)) {
288
+ parts.push({
289
+ tokens: classesIn(staticText(arg.consequent)),
290
+ condition: arg.test,
291
+ negate: false,
292
+ node: arg,
293
+ });
294
+ }
295
+ else if (arg.type === 'ConditionalExpression' &&
296
+ staticText(arg.alternate) !== null &&
297
+ addsNothing(arg.consequent)) {
298
+ parts.push({
299
+ tokens: classesIn(staticText(arg.alternate)),
300
+ condition: arg.test,
301
+ negate: true,
302
+ node: arg,
303
+ });
304
+ }
305
+ else {
306
+ return undefined;
307
+ }
308
+ }
309
+ const tokens = [
310
+ ...new Set(statics.flatMap(node => classesIn(staticText(node)))),
311
+ ];
312
+ return { call, tokens, statics, parts };
313
+ }
314
+ /**
315
+ * Every comment on a node and inside it, except inside `keep` (a condition
316
+ * that moves, taking its comments with it).
317
+ */
318
+ function commentsWithin(node, keep) {
319
+ const found = [];
320
+ const visit = (current) => {
321
+ if (!current || typeof current !== 'object' || current === keep)
322
+ return;
323
+ if (Array.isArray(current)) {
324
+ current.forEach(visit);
325
+ return;
326
+ }
327
+ if (typeof current.type !== 'string')
328
+ return;
329
+ found.push(...(current.comments ?? []));
330
+ for (const [key, child] of Object.entries(current)) {
331
+ if (key !== 'comments' && key !== 'loc' && key !== 'original') {
332
+ visit(child);
333
+ }
334
+ }
335
+ };
336
+ visit(node);
337
+ return found;
338
+ }
93
339
  /**
94
340
  * The static text of a `className`, or null when it is computed. A string
95
341
  * literal, `{'…'}`, and a template literal with no expressions are static.
@@ -294,6 +540,10 @@ function reachesReact(child) {
294
540
  return isContent(child);
295
541
  return /[^ \t\r\n]/.test(child.value) || !/[\r\n]/.test(child.value);
296
542
  }
543
+ /** Whitespace React drops, which nothing is lost with. A comment is kept. */
544
+ function dropped(child) {
545
+ return child.type === 'JSXText' && !reachesReact(child);
546
+ }
297
547
  /** A plain HTML child as the planner reads it; nothing for a component. */
298
548
  function childFacts(child) {
299
549
  const name = child.openingElement.name;
@@ -311,8 +561,25 @@ function childFacts(child) {
311
561
  }),
312
562
  attributes: attributesBesides(child, classAttr, childAttributeValue),
313
563
  hasSpread: hasSpread(child),
564
+ isEmpty: (child.children ?? []).every(dropped),
314
565
  };
315
566
  }
567
+ /**
568
+ * An element's children as plain HTML elements, when that is every one
569
+ * React renders; undefined when one is anything else.
570
+ */
571
+ function childElementsOf(element) {
572
+ const children = [];
573
+ for (const child of element.children ?? []) {
574
+ if (dropped(child))
575
+ continue;
576
+ const found = child.type === 'JSXElement' ? childFacts(child) : undefined;
577
+ if (!found)
578
+ return undefined;
579
+ children.push(found);
580
+ }
581
+ return children;
582
+ }
316
583
  function insideForeignContent(elementPath) {
317
584
  let current = elementPath.parent;
318
585
  while (current) {
@@ -392,6 +659,33 @@ export default function transform(fileInfo, api, options = {}) {
392
659
  // a child that is one of its parts.
393
660
  const planned = new Map();
394
661
  const programScope = root.find(j.Program).paths()[0]?.scope;
662
+ // The joiners the file imports, by local name, and the elements whose
663
+ // joiner call converts.
664
+ const joinerLocals = new Set();
665
+ root.find(j.ImportDeclaration).forEach((importPath) => {
666
+ const node = importPath.node;
667
+ const exported = Object.hasOwn(JOINERS, node.source.value)
668
+ ? JOINERS[node.source.value]
669
+ : undefined;
670
+ if (!exported || node.importKind === 'type')
671
+ return;
672
+ for (const spec of node.specifiers ?? []) {
673
+ const name = spec.type === 'ImportDefaultSpecifier'
674
+ ? 'default'
675
+ : spec.type === 'ImportSpecifier' && spec.importKind !== 'type'
676
+ ? (spec.imported.name ?? spec.imported.value)
677
+ : undefined;
678
+ if (name && exported.includes(name))
679
+ joinerLocals.add(spec.local.name);
680
+ }
681
+ });
682
+ /** Whether a callee is one of those joiners where it is called. */
683
+ const isJoiner = (callee, scopePath) => callee?.type === 'Identifier' &&
684
+ joinerLocals.has(callee.name) &&
685
+ resolvesToBinding(scopePath, callee.name, programScope);
686
+ const joins = new Map();
687
+ /** Joiners a rewritten className no longer calls. */
688
+ const unjoined = new Set();
395
689
  /**
396
690
  * The bestax component an element already is, when its name is the bestax
397
691
  * import and not a local that shadows it; `scopePath` is where the name is
@@ -419,7 +713,7 @@ export default function transform(fileInfo, api, options = {}) {
419
713
  * React renders, and no comment, which folding the element would drop.
420
714
  */
421
715
  const soleChild = (element) => {
422
- const content = (element.children ?? []).filter((child) => child.type !== 'JSXText' || reachesReact(child));
716
+ const content = (element.children ?? []).filter((child) => !dropped(child));
423
717
  return content.length === 1 && content[0].type === 'JSXElement'
424
718
  ? content[0]
425
719
  : undefined;
@@ -492,26 +786,52 @@ export default function transform(fileInfo, api, options = {}) {
492
786
  childTargets: childTargets(elementPath),
493
787
  soleChildTarget: planned.get(sole)?.conversion?.target,
494
788
  soleChild: sole && childFacts(sole),
789
+ childElements: childElementsOf(element),
495
790
  bestaxInside: bestaxInside.get(element) ?? [],
496
791
  bestaxAround: surrounding.bestax,
497
792
  componentsAround: surrounding.other,
498
793
  onlyChildOf: onlyChildOf(elementPath, bestaxLocals),
499
794
  };
500
- let result = plan(facts);
795
+ // A joiner call the codemod can read converts exactly: its fixed classes
796
+ // as for a static className, and each condition on a flag as its prop.
797
+ const joined = className === null
798
+ ? readJoiner(classAttr.value, callee => isJoiner(callee, elementPath))
799
+ : undefined;
800
+ let exact;
801
+ if (joined) {
802
+ exact = plan({
803
+ ...facts,
804
+ tokens: joined.tokens,
805
+ conditional: joined.parts.map(part => part.tokens),
806
+ impure: joined.parts.flatMap((part, index) => sideEffectFree(part.condition) ? [] : [index]),
807
+ });
808
+ if (exact.conversion)
809
+ joins.set(element, joined);
810
+ else
811
+ exact = undefined;
812
+ }
813
+ let result = exact ?? plan(facts);
501
814
  const converts = result.conversion !== null || result.fold !== undefined;
502
815
  const becomes = result.conversion?.target ?? result.fold?.target;
503
- if (className === null && becomes) {
504
- // The same element with a static className would convert; with a
505
- // computed one, only a person can turn each condition into its prop.
816
+ if (className === null && !exact && becomes) {
817
+ // The same element with a static className would convert; with one
818
+ // computed any other way, only a person can turn each condition into
819
+ // its prop.
506
820
  const target = becomes;
821
+ // A call it could read says what stopped it instead.
822
+ const conditionalRoot = joined?.parts
823
+ .flatMap(part => part.tokens)
824
+ .find(token => rootFor(token)?.target === target);
825
+ const message = !joined
826
+ ? `this \`className\` is computed, and the codemod reads only a \`clsx\` or \`classnames\` call of class strings and conditional classes; convert this element to bestax \`${target}\` by hand, turning each condition into its prop`
827
+ : conditionalRoot
828
+ ? `this \`className\` adds \`.${conditionalRoot}\` only under a condition, so whether this element is a bestax \`${target}\` at all depends on it; convert it by hand if it always is, turning each condition into its prop`
829
+ : `this \`className\` is computed, and the codemod can't write every class it adds onto bestax \`${target}\` as it is; convert this element by hand, turning each condition into its prop`;
507
830
  result = {
508
831
  conversion: null,
509
832
  todos: [
510
833
  ...result.todos,
511
- {
512
- rule: ruleId('dynamic-class', target),
513
- message: `this \`className\` is computed, and the codemod converts static class strings only; convert this element to bestax \`${target}\` by hand, turning each condition into its prop`,
514
- },
834
+ { rule: ruleId('dynamic-class', target), message },
515
835
  ],
516
836
  };
517
837
  }
@@ -631,13 +951,42 @@ export default function transform(fileInfo, api, options = {}) {
631
951
  findAttr(child, from).name = j.jsxIdentifier(to);
632
952
  }
633
953
  }
954
+ // A target that renders the element's children itself takes their place:
955
+ // they go, and so does the closing tag, their comments and its own moving
956
+ // to the name that stays.
957
+ const gone = conversion.rendersChildren
958
+ ? [
959
+ ...(element.children ?? [])
960
+ .filter((node) => node.type === 'JSXElement')
961
+ .flatMap((node) => [
962
+ ...(node.comments ?? []),
963
+ ...tagComments(node),
964
+ ]),
965
+ ...[element.closingElement, element.closingElement?.name].flatMap((node) => node?.comments ?? []),
966
+ ]
967
+ : [];
634
968
  const [head, ...rest] = conversion.target.split('.');
635
969
  renameElement(j, element, [ctx.reserve(head), ...rest].join('.'));
636
970
  for (const name of conversion.drop) {
637
971
  const holder = findAttr(element, name) ? element : child;
638
972
  removeAttr(holder, findAttr(holder, name));
639
973
  }
640
- writeClassName(j, element, conversion.props, conversion.className);
974
+ const joined = joins.get(element);
975
+ if (!joined) {
976
+ writeClassName(j, element, conversion.props, conversion.className);
977
+ }
978
+ else if (writeJoined(j, element, conversion, joined)) {
979
+ unjoined.add(joined.call.callee.name);
980
+ }
981
+ if (conversion.rendersChildren) {
982
+ element.children = [];
983
+ element.openingElement.selfClosing = true;
984
+ element.closingElement = null;
985
+ if (gone.length) {
986
+ const name = element.openingElement.name;
987
+ name.comments = [...(name.comments ?? []), ...gone.map(afterTagName)];
988
+ }
989
+ }
641
990
  let written = element;
642
991
  if (child) {
643
992
  child.openingElement.name = element.openingElement.name;
@@ -670,6 +1019,34 @@ export default function transform(fileInfo, api, options = {}) {
670
1019
  }
671
1020
  ctx.dirty = true;
672
1021
  }
1022
+ // A joiner import only these rewrites stopped using goes too, so the file
1023
+ // gains no unused import; one the file already left unused stays.
1024
+ for (const name of unjoined) {
1025
+ const referenced = root
1026
+ .find(j.Identifier, { name })
1027
+ .filter((identifierPath) => identifierPath.parent.node.type !== 'ImportSpecifier' &&
1028
+ identifierPath.parent.node.type !== 'ImportDefaultSpecifier');
1029
+ if (referenced.size() > 0)
1030
+ continue;
1031
+ root.find(j.ImportDeclaration).forEach((importPath) => {
1032
+ const node = importPath.node;
1033
+ const specifiers = node.specifiers ?? [];
1034
+ const spec = specifiers.find((found) => found.local?.name === name);
1035
+ if (!spec)
1036
+ return;
1037
+ specifiers.splice(specifiers.indexOf(spec), 1);
1038
+ if (specifiers.length > 0)
1039
+ return;
1040
+ // The declaration goes with its last name; its comments (a file
1041
+ // header, a directive) stay, on the statement beside it.
1042
+ const index = program.body.indexOf(node);
1043
+ const neighbour = program.body[index + 1] ?? program.body[index - 1];
1044
+ if (node.comments?.length && neighbour) {
1045
+ neighbour.comments = [...node.comments, ...(neighbour.comments ?? [])];
1046
+ }
1047
+ program.body.splice(index, 1);
1048
+ });
1049
+ }
673
1050
  // ---- 5. The bestax import ---------------------------------------------------
674
1051
  const body = program.body;
675
1052
  placeBestaxImport(bestaxImportState, buildBestaxImport(ctx, bestaxImportState), {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bestax-migrate",
3
- "version": "2.10.0",
3
+ "version": "2.12.0",
4
4
  "description": "Codemods for migrating existing React apps from other Bulma libraries to @allxsmith/bestax-bulma",
5
5
  "type": "module",
6
6
  "bin": {
@@ -38,7 +38,7 @@
38
38
  "chalk": "^6.0.0",
39
39
  "commander": "^15.0.0",
40
40
  "jscodeshift": "^17.3.0",
41
- "@allxsmith/bestax-bulma": "^5.16.9"
41
+ "@allxsmith/bestax-bulma": "^5.16.10"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@jest/globals": "^30.0.0",
@@ -47,6 +47,8 @@
47
47
  "@types/node": "^26.3.0",
48
48
  "@types/react": "^19.2.18",
49
49
  "@types/react-dom": "^19.2.5",
50
+ "classnames": "^2.5.1",
51
+ "clsx": "^2.1.1",
50
52
  "jest": "^30.2.0",
51
53
  "react": "^19.2.8",
52
54
  "react-dom": "^19.2.8",