bestax-migrate 2.1.6 → 2.2.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.
Files changed (61) hide show
  1. package/README.md +13 -8
  2. package/dist/cli.d.ts +53 -0
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +159 -9
  5. package/dist/sources/_shared/imports.d.ts +46 -0
  6. package/dist/sources/_shared/imports.d.ts.map +1 -0
  7. package/dist/sources/_shared/imports.js +152 -0
  8. package/dist/sources/{react-bulma-components → _shared}/jsx-utils.d.ts +20 -12
  9. package/dist/sources/_shared/jsx-utils.d.ts.map +1 -0
  10. package/dist/sources/{react-bulma-components → _shared}/jsx-utils.js +15 -10
  11. package/dist/sources/_shared/make-styles-transform.d.ts +37 -0
  12. package/dist/sources/_shared/make-styles-transform.d.ts.map +1 -0
  13. package/dist/sources/_shared/make-styles-transform.js +440 -0
  14. package/dist/sources/_shared/props.d.ts +20 -0
  15. package/dist/sources/_shared/props.d.ts.map +1 -0
  16. package/dist/sources/{react-bulma-components → _shared}/props.js +11 -7
  17. package/dist/sources/_shared/specials-utils.d.ts +45 -0
  18. package/dist/sources/_shared/specials-utils.d.ts.map +1 -0
  19. package/dist/sources/_shared/specials-utils.js +124 -0
  20. package/dist/sources/rbx/deps.d.ts +18 -0
  21. package/dist/sources/rbx/deps.d.ts.map +1 -0
  22. package/dist/sources/rbx/deps.js +287 -0
  23. package/dist/sources/rbx/index.d.ts +3 -0
  24. package/dist/sources/rbx/index.d.ts.map +1 -0
  25. package/dist/sources/rbx/index.js +10 -0
  26. package/dist/sources/rbx/mapping.d.ts +49 -0
  27. package/dist/sources/rbx/mapping.d.ts.map +1 -0
  28. package/dist/sources/rbx/mapping.js +924 -0
  29. package/dist/sources/rbx/responsive.d.ts +30 -0
  30. package/dist/sources/rbx/responsive.d.ts.map +1 -0
  31. package/dist/sources/rbx/responsive.js +279 -0
  32. package/dist/sources/rbx/specials.d.ts +21 -0
  33. package/dist/sources/rbx/specials.d.ts.map +1 -0
  34. package/dist/sources/rbx/specials.js +706 -0
  35. package/dist/sources/rbx/styles.d.ts +21 -0
  36. package/dist/sources/rbx/styles.d.ts.map +1 -0
  37. package/dist/sources/rbx/styles.js +29 -0
  38. package/dist/sources/rbx/transform.d.ts +25 -0
  39. package/dist/sources/rbx/transform.d.ts.map +1 -0
  40. package/dist/sources/rbx/transform.js +911 -0
  41. package/dist/sources/react-bulma-components/deps.d.ts.map +1 -1
  42. package/dist/sources/react-bulma-components/deps.js +16 -0
  43. package/dist/sources/react-bulma-components/responsive.d.ts +1 -1
  44. package/dist/sources/react-bulma-components/responsive.d.ts.map +1 -1
  45. package/dist/sources/react-bulma-components/responsive.js +1 -1
  46. package/dist/sources/react-bulma-components/specials.d.ts +3 -9
  47. package/dist/sources/react-bulma-components/specials.d.ts.map +1 -1
  48. package/dist/sources/react-bulma-components/specials.js +4 -91
  49. package/dist/sources/react-bulma-components/styles.d.ts +9 -9
  50. package/dist/sources/react-bulma-components/styles.d.ts.map +1 -1
  51. package/dist/sources/react-bulma-components/styles.js +17 -389
  52. package/dist/sources/react-bulma-components/transform.d.ts.map +1 -1
  53. package/dist/sources/react-bulma-components/transform.js +378 -101
  54. package/dist/sources/registry.d.ts.map +1 -1
  55. package/dist/sources/registry.js +2 -0
  56. package/dist/types.d.ts +8 -0
  57. package/dist/types.d.ts.map +1 -1
  58. package/package.json +8 -7
  59. package/dist/sources/react-bulma-components/jsx-utils.d.ts.map +0 -1
  60. package/dist/sources/react-bulma-components/props.d.ts +0 -15
  61. package/dist/sources/react-bulma-components/props.d.ts.map +0 -1
@@ -0,0 +1,706 @@
1
+ /**
2
+ * Structural handlers for rbx → bestax conversions a rename table cannot
3
+ * express: targets chosen by a prop's value (Level.Item align, Pagination.Step
4
+ * align), container collapsing (Select.Container, Image.Container,
5
+ * Modal.Container), and replacement with plain HTML where bestax has no
6
+ * component (Heading, Help, Label, Select.Option).
7
+ *
8
+ * The two genuinely new families relative to react-bulma-components are here
9
+ * too, though they are driven from transform.ts rather than the mapping table
10
+ * because they are helper *props* that can sit on any element at all:
11
+ * `badge*` and `tooltip*` become wrapping bestax components.
12
+ *
13
+ * A handler may return a `target` override for the rename step, or mark the
14
+ * element `replaced` when it substituted the node itself.
15
+ */
16
+ import { BADGE_PROPS, RESPONSIVE_BREAKPOINTS, TOOLTIP_PROPS, UNIVERSAL_PROPS, } from './mapping.js';
17
+ import { addAttr, addTodo, buildJsxName, attributesOf, findAttr, jsxNameParts, literalValueOf, makeAttr, plainElement, removeAttr, resolveBooleanish, } from '../_shared/jsx-utils.js';
18
+ import { alignTarget, makeStripModifierProps, mergeClassName, parseIconClasses, } from '../_shared/specials-utils.js';
19
+ /* eslint-disable @typescript-eslint/no-explicit-any */
20
+ const stripModifierProps = makeStripModifierProps(UNIVERSAL_PROPS, RESPONSIVE_BREAKPOINTS);
21
+ /**
22
+ * Turn a literal modifier prop into an `is-*` class fragment for an element
23
+ * on its way to becoming plain HTML. Returns the class (or undefined) and
24
+ * always removes the attribute.
25
+ */
26
+ function modifierClass(ctx, path, element, prop, base, where) {
27
+ const attr = findAttr(element, prop);
28
+ if (!attr)
29
+ return base;
30
+ const literal = literalValueOf(attr);
31
+ removeAttr(element, attr);
32
+ if (literal.kind === 'string' || literal.kind === 'number') {
33
+ return `${base} is-${literal.value}`;
34
+ }
35
+ addTodo(ctx, path, `prop:${prop}`, `dynamic ${where} \`${prop}\`; add the matching is-* class by hand`);
36
+ return base;
37
+ }
38
+ /**
39
+ * Also strips the universal `responsive` object: responsive.ts would have
40
+ * consumed it, but a `replaced: true` special skips that pass too, and it is
41
+ * deliberately absent from UNIVERSAL_PROPS so stripModifierProps misses it.
42
+ * Left behind it is an object literal on an intrinsic element — which does
43
+ * not compile, unlike the badge/tooltip case which merely warns.
44
+ *
45
+ * rbx's badge/tooltip helper props become wrapping components in
46
+ * transform.ts, but that pass runs after the rename step — which a
47
+ * `replaced: true` special skips. So a plain-element rewrite has to account
48
+ * for them itself, or they ride onto the HTML tag as unknown DOM attributes.
49
+ */
50
+ function stripHelperComponentProps(ctx, path, attrs, where) {
51
+ const kept = [];
52
+ const dropped = [];
53
+ for (const attr of attrs) {
54
+ const name = attr?.name?.name;
55
+ if (name &&
56
+ (name in BADGE_PROPS || name in TOOLTIP_PROPS || name === 'responsive')) {
57
+ dropped.push(name);
58
+ }
59
+ else {
60
+ kept.push(attr);
61
+ }
62
+ }
63
+ if (dropped.length > 0) {
64
+ addTodo(ctx, path, 'plain-element', `${where} became a plain element, so the ${dropped
65
+ .map(d => `\`${d}\``)
66
+ .join(', ')} helper prop(s) were dropped — re-apply by hand (badge/tooltip as a wrapping \`<Badge>\`/\`<Tooltip>\`, responsive settings as classes)`);
67
+ }
68
+ return kept;
69
+ }
70
+ /**
71
+ * rbx puts `as` on every component, but a handler that chooses its target from
72
+ * a prop value can land on a bestax component that has no `as` — Media.Item
73
+ * resolves to Left/Content/Right where only Left declares one, Level.Item to
74
+ * Left/Right/Item where only Item does, Navbar.Item to Item/Dropdown where
75
+ * only Item does. Opting into `as` in the mapping table cannot express that,
76
+ * so the target has to be checked after it is picked.
77
+ */
78
+ function restrictAsToTargets(ctx, path, element, target, allowed) {
79
+ const asAttr = findAttr(element, 'as');
80
+ if (!asAttr || (target && allowed.includes(target)))
81
+ return;
82
+ removeAttr(element, asAttr);
83
+ addTodo(ctx, path, 'prop:as', `bestax's \`${target}\` has no \`as\` prop (of the targets this can resolve to, only ${allowed
84
+ .map(a => `\`${a}\``)
85
+ .join(' / ')} does); render the tag directly or restructure`);
86
+ ctx.dirty = true;
87
+ }
88
+ /** Replace the element with a plain HTML tag carrying `className`. */
89
+ function replaceWithPlain(ctx, path, element, tag, className, where) {
90
+ const merged = mergeClassName(ctx, path, element, className, where);
91
+ const rest = stripHelperComponentProps(ctx, path, stripModifierProps(ctx, path, attributesOf(element), where), where);
92
+ path.replace(plainElement(ctx.j, tag, merged, rest, element.children ?? []));
93
+ ctx.dirty = true;
94
+ return { replaced: true };
95
+ }
96
+ /**
97
+ * rbx wraps several components in a `*.Container` that owns the modifiers,
98
+ * where bestax's component renders that wrapper itself. Move the container's
99
+ * (already-converted) attributes onto its single child and replace the
100
+ * container with it — otherwise both would rename to the same bestax
101
+ * component and nest, which is invalid.
102
+ *
103
+ * Returns null when the element isn't the single-JSX-child shape, so the
104
+ * caller can fall back to keeping the container.
105
+ */
106
+ function collapseOntoChild(ctx, path, element, where, expected) {
107
+ const children = (element.children ?? []).filter((c) => !(c.type === 'JSXText' && c.value.trim() === ''));
108
+ if (children.length !== 1 || children[0].type !== 'JSXElement')
109
+ return null;
110
+ const child = children[0];
111
+ // The child must be the component this container exists to wrap. rbx lets
112
+ // both of these containers hold something else — an `<iframe>` for a ratio
113
+ // box, a native `<select>` — and folding onto that put Bulma modifier props
114
+ // on an intrinsic element and dropped the wrapper the markup needs.
115
+ const childPath = ctx.resolve?.(child.openingElement?.name);
116
+ if (!childPath || childPath.join('.') !== expected)
117
+ return null;
118
+ for (const attr of [...attributesOf(element)]) {
119
+ const name = attr.name.name;
120
+ if (findAttr(child, name)) {
121
+ addTodo(ctx, path, `prop:${name}`, `\`${where}\` folded into its child, but both set \`${name}\`; the child's value was kept — reconcile by hand`);
122
+ continue;
123
+ }
124
+ removeAttr(element, attr);
125
+ addAttr(child, attr);
126
+ }
127
+ path.replace(child);
128
+ ctx.dirty = true;
129
+ return { replaced: true };
130
+ }
131
+ const SPECIALS = {
132
+ /**
133
+ * rbx's `Heading` is Bulma's `.heading` label (small caps), not a title —
134
+ * bestax has no component for it, so it becomes a plain <p>.
135
+ */
136
+ heading(ctx, path, element) {
137
+ return replaceWithPlain(ctx, path, element, 'p', 'heading', 'Heading');
138
+ },
139
+ /**
140
+ * rbx folds title and subtitle into one component behind a `subtitle`
141
+ * boolean; bestax splits them into `Title` and `SubTitle`. rbx also ignores
142
+ * `spaced` when `subtitle` is set, so a subtitle never carries isSpaced.
143
+ */
144
+ title(ctx, path, element) {
145
+ const subtitleAttr = findAttr(element, 'subtitle');
146
+ let target = 'Title';
147
+ let isSubtitle = false;
148
+ if (subtitleAttr) {
149
+ const resolved = resolveBooleanish(subtitleAttr);
150
+ if (resolved === 'expression') {
151
+ addTodo(ctx, path, 'prop:subtitle', 'dynamic `subtitle`; bestax splits these into `<Title>` and `<SubTitle>` — pick one by hand');
152
+ }
153
+ else if (resolved === 'truthy') {
154
+ target = 'SubTitle';
155
+ isSubtitle = true;
156
+ }
157
+ removeAttr(element, subtitleAttr);
158
+ ctx.dirty = true;
159
+ }
160
+ const spacedAttr = findAttr(element, 'spaced');
161
+ if (spacedAttr) {
162
+ const resolved = resolveBooleanish(spacedAttr);
163
+ // rbx ignores `spaced` on a subtitle, so a SubTitle drops it outright.
164
+ if (isSubtitle) {
165
+ removeAttr(element, spacedAttr);
166
+ }
167
+ else if (resolved === 'truthy') {
168
+ removeAttr(element, spacedAttr);
169
+ addAttr(element, makeAttr(ctx.j, 'isSpaced'));
170
+ }
171
+ else if (resolved === 'falsy') {
172
+ removeAttr(element, spacedAttr);
173
+ }
174
+ else {
175
+ // Dynamic: keep the expression, just rename the prop.
176
+ spacedAttr.name = ctx.j.jsxIdentifier('isSpaced');
177
+ }
178
+ ctx.dirty = true;
179
+ }
180
+ return { target, handledProps: ['subtitle', 'spaced'] };
181
+ },
182
+ /**
183
+ * bestax's Icon takes a `name` (plus `library`/`variant`) instead of an
184
+ * icon-font <i> child. Parse the child when we can read it, otherwise keep
185
+ * the element and explain.
186
+ */
187
+ icon(ctx, path, element) {
188
+ const children = (element.children ?? []).filter((c) => !(c.type === 'JSXText' && c.value.trim() === ''));
189
+ const iChild = children.find((c) => c.type === 'JSXElement' &&
190
+ c.openingElement?.name?.type === 'JSXIdentifier' &&
191
+ (c.openingElement.name.name === 'i' ||
192
+ c.openingElement.name.name === 'span'));
193
+ if (children.length === 1 && iChild) {
194
+ const classAttr = (iChild.openingElement.attributes ?? []).find((a) => a.type === 'JSXAttribute' && a.name.name === 'className');
195
+ const literal = classAttr ? literalValueOf(classAttr) : undefined;
196
+ if (literal?.kind === 'string') {
197
+ const parsed = parseIconClasses(literal.value);
198
+ if (parsed) {
199
+ addAttr(element, makeAttr(ctx.j, 'name', parsed.name));
200
+ if (parsed.library) {
201
+ addAttr(element, makeAttr(ctx.j, 'library', parsed.library));
202
+ }
203
+ if (parsed.variant) {
204
+ addAttr(element, makeAttr(ctx.j, 'variant', parsed.variant));
205
+ }
206
+ element.children = [];
207
+ if (element.openingElement) {
208
+ element.openingElement.selfClosing = true;
209
+ element.closingElement = null;
210
+ }
211
+ ctx.dirty = true;
212
+ return {};
213
+ }
214
+ }
215
+ }
216
+ // rbx's own docs teach `<Icon><FontAwesomeIcon icon={faHome} /></Icon>`,
217
+ // so this is the shape most rbx code is in. There is no icon name to read
218
+ // out of a component reference, and `name` is required on bestax's Icon.
219
+ addTodo(ctx, path, 'component:Icon', 'bestax `Icon` takes a required `name` (plus optional `library`/`variant`) instead of an icon child — e.g. `<FontAwesomeIcon icon={faHome} />` becomes `<Icon name="home" library="fa" variant="solid" />`');
220
+ return {};
221
+ },
222
+ /**
223
+ * rbx's Divider rendered its children as a centred label (`div.is-divider`
224
+ * with the text in `data-content`). bestax's Divider is a bare `<hr>` and
225
+ * takes no children -- React rejects children on a void element at runtime,
226
+ * so passing them through produced a crash with no TODO naming the lost
227
+ * label. The children are left in place so nothing is discarded silently;
228
+ * the TODO tells the user where the label has to go instead.
229
+ */
230
+ divider(ctx, path, element) {
231
+ const children = (element.children ?? []).filter((c) => !(c.type === 'JSXText' && c.value.trim() === ''));
232
+ if (children.length > 0) {
233
+ addTodo(ctx, path, 'component:Divider', 'rbx `Divider` rendered its children as a centred label; bestax `Divider` is a bare `<hr>` and takes no children — move the label into surrounding markup, or drop it');
234
+ }
235
+ return {};
236
+ },
237
+ /**
238
+ * rbx nests <Image> inside <Image.Container>, which carries the size; the
239
+ * bestax Image renders the <figure> itself, so the container collapses and
240
+ * hands its size down.
241
+ */
242
+ 'image-container'(ctx, path, element) {
243
+ const sizeAttr = findAttr(element, 'size');
244
+ if (sizeAttr) {
245
+ const literal = literalValueOf(sizeAttr);
246
+ if (literal.kind === 'number') {
247
+ // rbx takes a pixel dimension (64); bestax takes Bulma's `is-64x64`
248
+ // square form, so the number becomes "NxN".
249
+ sizeAttr.value = ctx.j.stringLiteral(`${literal.value}x${literal.value}`);
250
+ ctx.dirty = true;
251
+ }
252
+ else if (literal.kind !== 'string') {
253
+ addTodo(ctx, path, 'prop:size', 'dynamic `Image.Container` size; set `size` on the bestax `<Image>` by hand');
254
+ }
255
+ // A string literal is already a ratio like "16by9" — pass it through.
256
+ }
257
+ const collapsed = collapseOntoChild(ctx, path, element, 'Image.Container', 'Image');
258
+ if (collapsed)
259
+ return collapsed;
260
+ // No single child to fold into — keep the container as the Image itself.
261
+ return { target: 'Image', handledProps: ['size'] };
262
+ },
263
+ /**
264
+ * rbx's `Loader` is Bulma's plain `.loader` spinner, which always renders.
265
+ * bestax's `Loading` is a different thing — a dismissible overlay that
266
+ * returns null unless `active` — so this emits the element rbx did.
267
+ */
268
+ loader(ctx, path, element) {
269
+ return replaceWithPlain(ctx, path, element, 'div', 'loader', 'Loader');
270
+ },
271
+ /** rbx's full-screen PageLoader is bestax's Loading with isFullPage. */
272
+ 'page-loader'(ctx, _path, element) {
273
+ if (!findAttr(element, 'isFullPage')) {
274
+ addAttr(element, makeAttr(ctx.j, 'isFullPage'));
275
+ ctx.dirty = true;
276
+ }
277
+ return { target: 'Loading' };
278
+ },
279
+ /**
280
+ * rbx's Field `multiline` only means anything alongside `kind="group"`, and
281
+ * bestax folds both into one prop: `grouped="multiline"`. So this has to
282
+ * win over the `kind` mapping rather than sit beside it — `grouped` set
283
+ * twice would be invalid JSX.
284
+ */
285
+ field(ctx, path, element) {
286
+ const multilineAttr = findAttr(element, 'multiline');
287
+ if (!multilineAttr)
288
+ return {};
289
+ const resolved = resolveBooleanish(multilineAttr);
290
+ removeAttr(element, multilineAttr);
291
+ if (resolved === 'truthy') {
292
+ // rbx applies `is-grouped-multiline` only when kind === "group"
293
+ // (`[`${k}-multiline`]: k === "is-grouped" && multiline === true`), so
294
+ // consuming `kind` unconditionally dropped `has-addons` from
295
+ // `<Field kind="addons" multiline>` and added a modifier rbx would not
296
+ // have rendered.
297
+ const kindAttr = findAttr(element, 'kind');
298
+ const kindLiteral = kindAttr ? literalValueOf(kindAttr) : undefined;
299
+ const isGroup = kindLiteral?.kind === 'string' && kindLiteral.value === 'group';
300
+ // Note `!kindAttr` is NOT grouped in here: with no `kind` at all, rbx's
301
+ // `k` is undefined, so `multiline` renders nothing and the field stays a
302
+ // plain block. Treating it as group turned it into a flex row.
303
+ if (isGroup) {
304
+ if (kindAttr)
305
+ removeAttr(element, kindAttr);
306
+ const existing = findAttr(element, 'grouped');
307
+ if (existing)
308
+ removeAttr(element, existing);
309
+ addAttr(element, makeAttr(ctx.j, 'grouped', 'multiline'));
310
+ ctx.dirty = true;
311
+ return { handledProps: ['multiline', 'kind'] };
312
+ }
313
+ // kind="addons", a dynamic kind, or no kind at all: rbx ignores
314
+ // multiline in every one of those, so the codemod does too and leaves
315
+ // `kind` for the mapping table.
316
+ if (kindAttr && kindLiteral?.kind === 'expression') {
317
+ addTodo(ctx, path, 'prop:multiline', 'dynamic Field `kind` alongside `multiline`; rbx applies multiline only when kind is "group" — set `grouped="multiline"` by hand if that branch is reachable');
318
+ }
319
+ ctx.dirty = true;
320
+ return { handledProps: ['multiline'] };
321
+ }
322
+ if (resolved === 'expression') {
323
+ addTodo(ctx, path, 'prop:multiline', 'dynamic Field `multiline`; set `grouped="multiline"` conditionally by hand');
324
+ }
325
+ ctx.dirty = true;
326
+ return { handledProps: ['multiline'] };
327
+ },
328
+ /** bestax has no Help component; Bulma's markup is a plain <p class="help">. */
329
+ help(ctx, path, element) {
330
+ const className = modifierClass(ctx, path, element, 'color', 'help', 'Help');
331
+ return replaceWithPlain(ctx, path, element, 'p', className, 'Help');
332
+ },
333
+ /** bestax has no standalone Label; Bulma's markup is <label class="label">. */
334
+ label(ctx, path, element) {
335
+ let className = modifierClass(ctx, path, element, 'size', 'label', 'Label');
336
+ const disabledAttr = findAttr(element, 'disabled');
337
+ if (disabledAttr) {
338
+ // By value, not by presence: `disabled={false}` must not become a
339
+ // permanent `is-disabled`, and a dynamic value cannot be baked into a
340
+ // static class string.
341
+ const resolved = resolveBooleanish(disabledAttr);
342
+ removeAttr(element, disabledAttr);
343
+ if (resolved === 'truthy') {
344
+ className = `${className} is-disabled`;
345
+ }
346
+ else if (resolved === 'expression') {
347
+ addTodo(ctx, path, 'prop:disabled', "dynamic Label `disabled`; set className={disabled ? 'label is-disabled' : 'label'} by hand");
348
+ }
349
+ ctx.dirty = true;
350
+ }
351
+ return replaceWithPlain(ctx, path, element, 'label', className, 'Label');
352
+ },
353
+ /**
354
+ * rbx wraps <Select> in <Select.Container>, which owns the modifiers; the
355
+ * bestax Select renders its own wrapper, so the container collapses onto it.
356
+ */
357
+ 'select-container'(ctx, path, element) {
358
+ // Decide the plain-markup fallback FIRST, while the props are still rbx's
359
+ // own — converting them to bestax booleans and then emitting a <div>
360
+ // stranded things like `isRounded` on an intrinsic element.
361
+ const allKids = (element.children ?? []).filter((c) => !(c.type === 'JSXText' && c.value.trim() === ''));
362
+ if (allKids.length !== 1) {
363
+ // bestax's `Select` renders exactly one `<select>`, so a container
364
+ // holding several has no direct equivalent. rbx's own markup — a
365
+ // `div.select` wrapper — is valid whatever the children are.
366
+ let cls = 'select';
367
+ for (const prop of ['size', 'color', 'state']) {
368
+ cls = modifierClass(ctx, path, element, prop, cls, 'Select.Container');
369
+ }
370
+ for (const [prop, klass] of [
371
+ ['fullwidth', 'is-fullwidth'],
372
+ ['rounded', 'is-rounded'],
373
+ ]) {
374
+ const attr = findAttr(element, prop);
375
+ if (!attr)
376
+ continue;
377
+ const resolved = resolveBooleanish(attr);
378
+ if (resolved === 'truthy') {
379
+ cls = `${cls} ${klass}`;
380
+ }
381
+ else if (resolved === 'expression') {
382
+ // A plain element cannot carry the condition, so say so rather than
383
+ // dropping the styling along with its expression.
384
+ addTodo(ctx, path, `prop:${prop}`, `dynamic \`${prop}\` on a Select.Container that became plain markup; apply \`${klass}\` conditionally via className by hand`);
385
+ }
386
+ removeAttr(element, attr);
387
+ ctx.dirty = true;
388
+ }
389
+ addTodo(ctx, path, 'component:Select.Container', "bestax's `Select` renders a single `<select>`, so a container holding more than one has no direct equivalent; emitted the plain Bulma wrapper instead");
390
+ return replaceWithPlain(ctx, path, element, 'div', cls, 'Select.Container');
391
+ }
392
+ // A dynamic value must not simply disappear: rename it so the condition
393
+ // survives, rather than dropping the modifier along with its expression.
394
+ for (const [from, to] of [
395
+ ['fullwidth', 'isFullwidth'],
396
+ ['rounded', 'isRounded'],
397
+ ]) {
398
+ const attr = findAttr(element, from);
399
+ if (!attr)
400
+ continue;
401
+ const resolved = resolveBooleanish(attr);
402
+ if (resolved === 'truthy') {
403
+ removeAttr(element, attr);
404
+ addAttr(element, makeAttr(ctx.j, to));
405
+ }
406
+ else if (resolved === 'falsy') {
407
+ removeAttr(element, attr);
408
+ }
409
+ else {
410
+ attr.name = ctx.j.jsxIdentifier(to);
411
+ }
412
+ ctx.dirty = true;
413
+ }
414
+ const stateAttr = findAttr(element, 'state');
415
+ if (stateAttr) {
416
+ const literal = literalValueOf(stateAttr);
417
+ removeAttr(element, stateAttr);
418
+ if (literal.kind === 'string') {
419
+ const mapped = {
420
+ focused: 'isFocused',
421
+ hovered: 'isHovered',
422
+ loading: 'isLoading',
423
+ };
424
+ if (mapped[literal.value]) {
425
+ addAttr(element, makeAttr(ctx.j, mapped[literal.value]));
426
+ }
427
+ }
428
+ else {
429
+ addTodo(ctx, path, 'prop:state', 'dynamic `Select.Container` state; set the matching bestax boolean by hand');
430
+ }
431
+ ctx.dirty = true;
432
+ }
433
+ const collapsed = collapseOntoChild(ctx, path, element, 'Select.Container', 'Select');
434
+ if (collapsed)
435
+ return collapsed;
436
+ // rbx also lets the container wrap a NATIVE <select>. bestax's Select
437
+ // renders its own <select>, so keeping both nested one inside the other.
438
+ // Fold the native element's attributes and options up instead.
439
+ const kids = (element.children ?? []).filter((c) => !(c.type === 'JSXText' && c.value.trim() === ''));
440
+ const native = kids.length === 1 &&
441
+ kids[0].type === 'JSXElement' &&
442
+ kids[0].openingElement?.name?.type === 'JSXIdentifier' &&
443
+ kids[0].openingElement.name.name === 'select'
444
+ ? kids[0]
445
+ : null;
446
+ if (native) {
447
+ for (const attr of attributesOf(native)) {
448
+ if (!findAttr(element, attr.name.name))
449
+ addAttr(element, attr);
450
+ }
451
+ element.children = native.children ?? [];
452
+ ctx.dirty = true;
453
+ return {
454
+ target: 'Select',
455
+ handledProps: ['fullwidth', 'rounded', 'state'],
456
+ };
457
+ }
458
+ return {
459
+ target: 'Select',
460
+ handledProps: ['fullwidth', 'rounded', 'state'],
461
+ };
462
+ },
463
+ /** rbx's Select.Option is a plain <option>. */
464
+ 'plain-option'(ctx, path, element) {
465
+ return replaceWithPlain(ctx, path, element, 'option', undefined, 'Select.Option');
466
+ },
467
+ /** bestax Breadcrumb renders <ul>{children}</ul>; items are plain <li><a>. */
468
+ 'breadcrumb-item'(ctx, path, element) {
469
+ const j = ctx.j;
470
+ const activeAttr = findAttr(element, 'active');
471
+ let liClass;
472
+ if (activeAttr) {
473
+ const resolved = resolveBooleanish(activeAttr);
474
+ if (resolved === 'truthy')
475
+ liClass = 'is-active';
476
+ else if (resolved === 'expression') {
477
+ addTodo(ctx, path, 'prop:active', "dynamic Breadcrumb.Item active; set the li className={active ? 'is-active' : undefined} by hand");
478
+ }
479
+ removeAttr(element, activeAttr);
480
+ }
481
+ liClass = mergeClassName(ctx, path, element, liClass, 'Breadcrumb.Item');
482
+ // This handler builds its <a> by hand rather than going through
483
+ // replaceWithPlain, so it has to layer the helper-prop strip itself —
484
+ // otherwise rbx's badge*/tooltip* ride onto the intrinsic <a>.
485
+ const anchorAttrs = stripHelperComponentProps(ctx, path, stripModifierProps(ctx, path, attributesOf(element), 'Breadcrumb.Item'), 'Breadcrumb.Item');
486
+ const children = element.children ?? [];
487
+ const anchor = plainElement(j, 'a', undefined, anchorAttrs, children);
488
+ path.replace(plainElement(j, 'li', liClass, [], [anchor]));
489
+ ctx.dirty = true;
490
+ return { replaced: true };
491
+ },
492
+ /**
493
+ * rbx's Dropdown owns both the trigger and the menu; bestax's takes a
494
+ * `label` and renders the trigger itself, so the shape differs enough that
495
+ * a mechanical rewrite would be a guess.
496
+ */
497
+ dropdown(ctx, path, _element) {
498
+ addTodo(ctx, path, 'component:Dropdown', 'bestax `Dropdown` takes a `label` and renders its own trigger; move the `<Dropdown.Trigger>` content into `label` and drop the wrapper elements');
499
+ return {};
500
+ },
501
+ 'dropdown-container'(ctx, path, _element) {
502
+ addTodo(ctx, path, 'component:Dropdown.Container', 'bestax `Dropdown` is the container itself; drop this wrapper');
503
+ return {};
504
+ },
505
+ 'dropdown-content'(ctx, path, _element) {
506
+ addTodo(ctx, path, 'component:Dropdown.Content', 'bestax `Dropdown` renders its own menu content; drop this wrapper and keep the `<Dropdown.Item>`s');
507
+ return {};
508
+ },
509
+ 'dropdown-menu'(ctx, path, _element) {
510
+ addTodo(ctx, path, 'component:Dropdown.Menu', 'bestax `Dropdown` renders its own menu; drop this wrapper and keep the `<Dropdown.Item>`s');
511
+ return {};
512
+ },
513
+ 'dropdown-trigger'(ctx, path, _element) {
514
+ addTodo(ctx, path, 'component:Dropdown.Trigger', 'bestax `Dropdown` renders its own trigger; move this content into the `label` prop');
515
+ return {};
516
+ },
517
+ /** rbx Level.Item align=left|right → bestax Level.Left / Level.Right. */
518
+ 'level-item'(ctx, path, element) {
519
+ const result = alignTarget(ctx, path, element, 'align', { left: 'Level.Left', right: 'Level.Right' }, 'Level.Item');
520
+ restrictAsToTargets(ctx, path, element, result.target, ['Level.Item']);
521
+ return { ...result, handledProps: ['align', 'as'] };
522
+ },
523
+ /**
524
+ * rbx Media.Item align=content|left|right → bestax Media.{Content,Left,Right}.
525
+ *
526
+ * Only `MediaLeftProps` declares `as` ('figure' | 'div'); Content and Right
527
+ * do not. rbx puts `as` on every component, so it has to be resolved here,
528
+ * against the target the align value actually selected, rather than opted
529
+ * into once in the mapping table.
530
+ */
531
+ 'media-item'(ctx, path, element) {
532
+ const result = alignTarget(ctx, path, element, 'align', {
533
+ content: 'Media.Content',
534
+ left: 'Media.Left',
535
+ right: 'Media.Right',
536
+ }, 'Media.Content');
537
+ restrictAsToTargets(ctx, path, element, result.target, ['Media.Left']);
538
+ return { ...result, handledProps: ['align', 'as'] };
539
+ },
540
+ /**
541
+ * The markup maps cleanly, but three rbx behaviours do not exist in bestax
542
+ * and none of them fail loudly: rbx portals the modal into `document.body`,
543
+ * closes it on Escape by default, and clips document scroll while it is
544
+ * open. bestax renders inline and implements none of the three. An
545
+ * unmodified `<Modal>` therefore migrates to something that looks right and
546
+ * behaves differently, so every conversion is flagged.
547
+ */
548
+ modal(ctx, path, _element) {
549
+ addTodo(ctx, path, 'component:Modal', 'bestax `Modal` renders inline rather than portalling into document.body, and implements neither Escape-to-close nor scroll locking — rbx did all three by default. Re-add whichever your UI relied on');
550
+ return {};
551
+ },
552
+ /** rbx Modal.Container is the modal itself in bestax. */
553
+ 'modal-container'(_ctx, _path, _element) {
554
+ return { target: 'Modal' };
555
+ },
556
+ /**
557
+ * rbx's Navbar.Item carries `dropdown`/`up`/`tab` variants that bestax
558
+ * splits across Navbar.Item and Navbar.Dropdown.
559
+ */
560
+ 'navbar-item'(ctx, path, element) {
561
+ // Resolve the target FIRST, then fall through to the cleanup loop —
562
+ // returning early here left `<Navbar.Item dropdown up>` as
563
+ // `<Navbar.Dropdown up>`, with `up` neither removed nor flagged.
564
+ let target;
565
+ const dropdownAttr = findAttr(element, 'dropdown');
566
+ if (dropdownAttr) {
567
+ const resolved = resolveBooleanish(dropdownAttr);
568
+ removeAttr(element, dropdownAttr);
569
+ ctx.dirty = true;
570
+ if (resolved === 'truthy') {
571
+ target = 'Navbar.Dropdown';
572
+ }
573
+ else if (resolved === 'expression') {
574
+ addTodo(ctx, path, 'prop:dropdown', 'dynamic Navbar.Item `dropdown`; pick between `<Navbar.Item>` and `<Navbar.Dropdown>` by hand');
575
+ }
576
+ }
577
+ // `up` and `hoverable` exist on bestax's NavbarDropdownProps, so when the
578
+ // dropdown target was selected they carry straight over; discarding them
579
+ // lost working modifiers. On a plain Navbar.Item they have no home.
580
+ const unsupported = target === 'Navbar.Dropdown'
581
+ ? ['tab', 'expanded', 'managed']
582
+ : ['up', 'tab', 'expanded', 'hoverable', 'managed'];
583
+ for (const prop of unsupported) {
584
+ const attr = findAttr(element, prop);
585
+ if (!attr)
586
+ continue;
587
+ removeAttr(element, attr);
588
+ addTodo(ctx, path, `prop:${prop}`, `bestax \`Navbar.Item\` has no \`${prop}\` prop; restructure or add a class by hand`);
589
+ ctx.dirty = true;
590
+ }
591
+ restrictAsToTargets(ctx, path, element, target ?? 'Navbar.Item', [
592
+ 'Navbar.Item',
593
+ ]);
594
+ return {
595
+ target,
596
+ handledProps: [
597
+ 'as',
598
+ 'dropdown',
599
+ 'up',
600
+ 'tab',
601
+ 'expanded',
602
+ 'hoverable',
603
+ 'managed',
604
+ ],
605
+ };
606
+ },
607
+ /** rbx's Navbar.Container / Navbar.Item.Container have no bestax analogue. */
608
+ 'navbar-container'(ctx, path, _element) {
609
+ addTodo(ctx, path, 'component:Navbar.Container', 'bestax `Navbar` renders its own container; drop this wrapper');
610
+ return {};
611
+ },
612
+ /**
613
+ * rbx's `Navbar.Dropdown` is the MENU (`div.navbar-dropdown`); bestax calls
614
+ * that `Navbar.DropdownMenu` and reserves `Navbar.Dropdown` for the outer
615
+ * `navbar-item has-dropdown` container — which is what rbx's
616
+ * `<Navbar.Item dropdown>` becomes. Targeting `Navbar.Dropdown` here nested
617
+ * two containers and emitted no menu at all. The react-bulma-components
618
+ * source already makes this distinction for the same reason.
619
+ */
620
+ 'navbar-dropdown'(ctx, path, element) {
621
+ const boxedAttr = findAttr(element, 'boxed');
622
+ if (boxedAttr) {
623
+ removeAttr(element, boxedAttr);
624
+ addTodo(ctx, path, 'prop:boxed', 'bestax `Navbar.DropdownMenu` has no `boxed` prop; add className="is-boxed"');
625
+ ctx.dirty = true;
626
+ }
627
+ const alignAttr = findAttr(element, 'align');
628
+ if (alignAttr) {
629
+ const literal = literalValueOf(alignAttr);
630
+ removeAttr(element, alignAttr);
631
+ if (literal.kind === 'string' && literal.value === 'right') {
632
+ addAttr(element, makeAttr(ctx.j, 'right'));
633
+ }
634
+ else if (literal.kind !== 'string') {
635
+ addTodo(ctx, path, 'prop:align', 'dynamic `Navbar.Dropdown` align; set the bestax `right` boolean by hand');
636
+ }
637
+ ctx.dirty = true;
638
+ }
639
+ return {
640
+ target: 'Navbar.DropdownMenu',
641
+ handledProps: ['boxed', 'align'],
642
+ };
643
+ },
644
+ /** rbx Navbar.Segment align=start|end → bestax Navbar.Start / Navbar.End. */
645
+ 'navbar-segment'(ctx, path, element) {
646
+ return alignTarget(ctx, path, element, 'align', { start: 'Navbar.Start', end: 'Navbar.End' }, 'Navbar.Start');
647
+ },
648
+ /** rbx Pagination.Step align=next|previous → bestax Pagination.{Next,Previous}. */
649
+ 'pagination-step'(ctx, path, element) {
650
+ return alignTarget(ctx, path, element, 'align', { next: 'Pagination.Next', previous: 'Pagination.Previous' }, 'Pagination.Next');
651
+ },
652
+ /**
653
+ * rbx's Panel.Tab is one tab inside a Panel.Tab.Group; bestax's Panel.Tabs
654
+ * is the group and its children are plain anchors.
655
+ */
656
+ 'panel-tab'(ctx, path, element) {
657
+ const activeAttr = findAttr(element, 'active');
658
+ let className;
659
+ if (activeAttr) {
660
+ const resolved = resolveBooleanish(activeAttr);
661
+ if (resolved === 'truthy')
662
+ className = 'is-active';
663
+ else if (resolved === 'expression') {
664
+ addTodo(ctx, path, 'prop:active', "dynamic Panel.Tab active; set className={active ? 'is-active' : undefined} by hand");
665
+ }
666
+ removeAttr(element, activeAttr);
667
+ }
668
+ return replaceWithPlain(ctx, path, element, 'a', className, 'Panel.Tab');
669
+ },
670
+ /**
671
+ * rbx's `Tab.Group` renders `div.tabs > ul > li`; bestax's `Tabs` renders
672
+ * only the `div.tabs` and leaves the `<ul>` to `Tabs.List`. Renaming
673
+ * straight across put the `<li>` items directly inside the div, which is
674
+ * not valid Bulma tabs markup. Wrap the children in a `Tabs.List`.
675
+ */
676
+ 'tab-group'(ctx, _path, element) {
677
+ const children = element.children ?? [];
678
+ const solid = children.filter((c) => !(c.type === 'JSXText' && c.value.trim() === ''));
679
+ // Already wrapped by hand, or nothing to wrap.
680
+ const alreadyList = solid.length === 1 &&
681
+ solid[0].type === 'JSXElement' &&
682
+ /(^|\.)List$/.test((jsxNameParts(solid[0].openingElement?.name) ?? []).join('.'));
683
+ if (solid.length === 0 || alreadyList)
684
+ return { target: 'Tabs' };
685
+ const local = ctx.reserve('Tabs');
686
+ const listName = `${local}.List`;
687
+ element.children = [
688
+ ctx.j.jsxElement(ctx.j.jsxOpeningElement(buildJsxName(ctx.j, listName), [], false), ctx.j.jsxClosingElement(buildJsxName(ctx.j, listName)), children),
689
+ ];
690
+ ctx.dirty = true;
691
+ return { target: 'Tabs' };
692
+ },
693
+ /** Marks the element for the column breakpoint pass in responsive.ts. */
694
+ column() {
695
+ return {};
696
+ },
697
+ 'column-group'() {
698
+ return {};
699
+ },
700
+ };
701
+ export function runSpecial(name, ctx, path, element) {
702
+ const handler = SPECIALS[name];
703
+ if (!handler)
704
+ return {};
705
+ return handler(ctx, path, element);
706
+ }