bestax-migrate 0.0.1

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 (47) hide show
  1. package/README.md +59 -0
  2. package/dist/cli.d.ts +22 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/cli.js +225 -0
  5. package/dist/index.d.ts +7 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +12 -0
  8. package/dist/report.d.ts +24 -0
  9. package/dist/report.d.ts.map +1 -0
  10. package/dist/report.js +66 -0
  11. package/dist/runner.d.ts +16 -0
  12. package/dist/runner.d.ts.map +1 -0
  13. package/dist/runner.js +69 -0
  14. package/dist/sources/react-bulma-components/deps.d.ts +9 -0
  15. package/dist/sources/react-bulma-components/deps.d.ts.map +1 -0
  16. package/dist/sources/react-bulma-components/deps.js +96 -0
  17. package/dist/sources/react-bulma-components/index.d.ts +3 -0
  18. package/dist/sources/react-bulma-components/index.d.ts.map +1 -0
  19. package/dist/sources/react-bulma-components/index.js +10 -0
  20. package/dist/sources/react-bulma-components/jsx-utils.d.ts +69 -0
  21. package/dist/sources/react-bulma-components/jsx-utils.d.ts.map +1 -0
  22. package/dist/sources/react-bulma-components/jsx-utils.js +130 -0
  23. package/dist/sources/react-bulma-components/mapping.d.ts +31 -0
  24. package/dist/sources/react-bulma-components/mapping.d.ts.map +1 -0
  25. package/dist/sources/react-bulma-components/mapping.js +852 -0
  26. package/dist/sources/react-bulma-components/props.d.ts +15 -0
  27. package/dist/sources/react-bulma-components/props.d.ts.map +1 -0
  28. package/dist/sources/react-bulma-components/props.js +124 -0
  29. package/dist/sources/react-bulma-components/responsive.d.ts +17 -0
  30. package/dist/sources/react-bulma-components/responsive.d.ts.map +1 -0
  31. package/dist/sources/react-bulma-components/responsive.js +113 -0
  32. package/dist/sources/react-bulma-components/specials.d.ts +23 -0
  33. package/dist/sources/react-bulma-components/specials.d.ts.map +1 -0
  34. package/dist/sources/react-bulma-components/specials.js +546 -0
  35. package/dist/sources/react-bulma-components/styles.d.ts +13 -0
  36. package/dist/sources/react-bulma-components/styles.d.ts.map +1 -0
  37. package/dist/sources/react-bulma-components/styles.js +199 -0
  38. package/dist/sources/react-bulma-components/transform.d.ts +19 -0
  39. package/dist/sources/react-bulma-components/transform.d.ts.map +1 -0
  40. package/dist/sources/react-bulma-components/transform.js +495 -0
  41. package/dist/sources/registry.d.ts +9 -0
  42. package/dist/sources/registry.d.ts.map +1 -0
  43. package/dist/sources/registry.js +14 -0
  44. package/dist/types.d.ts +122 -0
  45. package/dist/types.d.ts.map +1 -0
  46. package/dist/types.js +9 -0
  47. package/package.json +74 -0
@@ -0,0 +1,546 @@
1
+ /**
2
+ * Structural handlers for conversions a rename table can't express: targets
3
+ * chosen by prop values (Level.Side align), child rewrites (Icon <i> parsing,
4
+ * Tabs list wrapping), element replacement (Form.Help → plain <p>), and
5
+ * container unwrapping (Table.Container).
6
+ *
7
+ * A handler may return a `target` override for the rename step, or mark the
8
+ * element `replaced` when it substituted the node itself.
9
+ */
10
+ import { RESPONSIVE_BREAKPOINTS, UNIVERSAL_PROPS } from './mapping.js';
11
+ import { addAttr, addTodo, attributesOf, buildJsxName, findAttr, literalValueOf, makeAttr, plainElement, removeAttr, } from './jsx-utils.js';
12
+ /** Pick a target based on a literal align-style prop, removing the prop. */
13
+ function alignTarget(ctx, path, element, prop, targets, fallback) {
14
+ const attr = findAttr(element, prop);
15
+ if (!attr)
16
+ return { target: fallback };
17
+ const literal = literalValueOf(attr);
18
+ if (literal.kind === 'string' && targets[literal.value]) {
19
+ removeAttr(element, attr);
20
+ ctx.dirty = true;
21
+ return { target: targets[literal.value] };
22
+ }
23
+ if (literal.kind === 'string') {
24
+ removeAttr(element, attr);
25
+ ctx.dirty = true;
26
+ return { target: fallback };
27
+ }
28
+ addTodo(ctx, path, `prop:${prop}`, `\`${prop}\` has a dynamic value; pick between ${Object.values(targets).join(' / ')} by hand`);
29
+ removeAttr(element, attr);
30
+ ctx.dirty = true;
31
+ return { target: fallback };
32
+ }
33
+ /**
34
+ * Filter Bulma modifier props out of an attribute list bound for a plain HTML
35
+ * element (they only exist on bestax components), leaving a TODO when any
36
+ * were dropped.
37
+ */
38
+ function stripModifierProps(ctx, path, attrs, where) {
39
+ const kept = [];
40
+ const dropped = [];
41
+ for (const attr of attrs) {
42
+ const name = attr?.name?.name;
43
+ if (name && (UNIVERSAL_PROPS[name] || name in RESPONSIVE_BREAKPOINTS)) {
44
+ dropped.push(name);
45
+ }
46
+ else {
47
+ kept.push(attr);
48
+ }
49
+ }
50
+ if (dropped.length > 0) {
51
+ addTodo(ctx, path, 'plain-element', `${where} became a plain element; the Bulma helper prop(s) ${dropped
52
+ .map(d => `\`${d}\``)
53
+ .join(', ')} were dropped — restyle with classes`);
54
+ }
55
+ return kept;
56
+ }
57
+ /**
58
+ * Consume the element's `className` so a plain-element rewrite can merge it
59
+ * with its hard-coded Bulma class instead of dropping it. A dynamic value
60
+ * can't be merged safely — keep the base class and leave a TODO.
61
+ */
62
+ function mergeClassName(ctx, path, element, base, where) {
63
+ const attr = findAttr(element, 'className');
64
+ if (!attr)
65
+ return base;
66
+ const literal = literalValueOf(attr);
67
+ removeAttr(element, attr);
68
+ if (literal.kind === 'string') {
69
+ return base ? `${base} ${literal.value}` : literal.value;
70
+ }
71
+ addTodo(ctx, path, 'prop:className', base
72
+ ? `dynamic ${where} className; merge it with the \`${base}\` class by hand`
73
+ : `dynamic ${where} className; re-apply it to the emitted element by hand`);
74
+ return base;
75
+ }
76
+ /** Parse an icon-font <i className="..."> into bestax Icon name/library/variant. */
77
+ function parseIconClasses(className) {
78
+ const tokens = className.trim().split(/\s+/);
79
+ const faVariant = {
80
+ fas: 'solid',
81
+ far: 'regular',
82
+ fab: 'brands',
83
+ fal: 'light',
84
+ fad: 'duotone',
85
+ };
86
+ const faStyle = tokens.find(t => faVariant[t]);
87
+ const faName = tokens.find(t => /^fa-/.test(t) && !/^fa-(lg|xs|sm|\dx|fw|spin|pulse)$/.test(t));
88
+ if (faStyle && faName) {
89
+ return {
90
+ name: faName.replace(/^fa-/, ''),
91
+ library: 'fa',
92
+ variant: faVariant[faStyle],
93
+ };
94
+ }
95
+ if (tokens.includes('mdi')) {
96
+ const mdiName = tokens.find(t => /^mdi-/.test(t) && t !== 'mdi');
97
+ if (mdiName)
98
+ return { name: mdiName.replace(/^mdi-/, ''), library: 'mdi' };
99
+ }
100
+ return null;
101
+ }
102
+ const SPECIALS = {
103
+ /**
104
+ * RBC defaults Columns `multiline` to TRUE (Bulma and bestax default to
105
+ * false). Absent → inject isMultiline; dynamic → `expr ?? true` so an
106
+ * undefined value keeps falling back to the RBC default.
107
+ */
108
+ columns(ctx, _path, element) {
109
+ const attr = findAttr(element, 'multiline');
110
+ if (!attr) {
111
+ if (!findAttr(element, 'isMultiline')) {
112
+ addAttr(element, makeAttr(ctx.j, 'isMultiline'));
113
+ ctx.dirty = true;
114
+ }
115
+ return { handledProps: ['multiline'] };
116
+ }
117
+ const literal = literalValueOf(attr);
118
+ if (literal.kind === 'boolean') {
119
+ if (literal.value) {
120
+ attr.name = ctx.j.jsxIdentifier('isMultiline');
121
+ attr.value = null;
122
+ }
123
+ else {
124
+ removeAttr(element, attr);
125
+ }
126
+ }
127
+ else if (literal.kind === 'expression') {
128
+ attr.name = ctx.j.jsxIdentifier('isMultiline');
129
+ attr.value = ctx.j.jsxExpressionContainer(ctx.j.logicalExpression('??', attr.value.expression, ctx.j.booleanLiteral(true)));
130
+ }
131
+ ctx.dirty = true;
132
+ return { handledProps: ['multiline'] };
133
+ },
134
+ /** `<Button remove />` is Bulma's delete cross → bestax `<Delete />`. */
135
+ button(ctx, _path, element) {
136
+ const attr = findAttr(element, 'remove');
137
+ if (!attr)
138
+ return {};
139
+ removeAttr(element, attr);
140
+ ctx.dirty = true;
141
+ return { target: 'Delete' };
142
+ },
143
+ /** Heading → Title / SubTitle / plain `.heading` paragraph. */
144
+ heading(ctx, path, element) {
145
+ const headingAttr = findAttr(element, 'heading');
146
+ if (headingAttr) {
147
+ removeAttr(element, headingAttr);
148
+ const className = mergeClassName(ctx, path, element, 'heading', 'Heading');
149
+ const rest = stripModifierProps(ctx, path, attributesOf(element).filter(a => !['size', 'weight', 'spaced', 'subtitle'].includes(a.name.name)), 'Heading');
150
+ const replacement = plainElement(ctx.j, 'p', className, rest, element.children ?? []);
151
+ path.replace(replacement);
152
+ ctx.dirty = true;
153
+ return { replaced: true };
154
+ }
155
+ const subtitleAttr = findAttr(element, 'subtitle');
156
+ if (subtitleAttr) {
157
+ removeAttr(element, subtitleAttr);
158
+ ctx.dirty = true;
159
+ return { target: 'SubTitle' };
160
+ }
161
+ return { target: 'Title' };
162
+ },
163
+ 'level-side'(ctx, path, element) {
164
+ return alignTarget(ctx, path, element, 'align', { left: 'Level.Left', right: 'Level.Right' }, 'Level.Left');
165
+ },
166
+ 'media-item'(ctx, path, element) {
167
+ return alignTarget(ctx, path, element, 'align', { left: 'MediaLeft', right: 'MediaRight', center: 'MediaContent' }, 'MediaContent');
168
+ },
169
+ /** RBC Image numeric sizes ({128}) become bestax "128x128" strings. */
170
+ image(ctx, _path, element) {
171
+ const attr = findAttr(element, 'size');
172
+ if (!attr)
173
+ return {};
174
+ const literal = literalValueOf(attr);
175
+ if (literal.kind === 'number') {
176
+ attr.value = ctx.j.stringLiteral(`${literal.value}x${literal.value}`);
177
+ ctx.dirty = true;
178
+ }
179
+ return {};
180
+ },
181
+ /**
182
+ * RBC Card.Image takes Image props directly; bestax Card.Image is a plain
183
+ * wrapper — move the props onto a new inner <Image>.
184
+ */
185
+ 'card-image'(ctx, _path, element) {
186
+ const children = (element.children ?? []).filter((c) => !(c.type === 'JSXText' && c.value.trim() === ''));
187
+ const attrs = element.openingElement.attributes ?? [];
188
+ const imageAttrs = attrs.filter((a) => a.type === 'JSXAttribute' && a.name.name !== 'className');
189
+ if (children.length > 0 || imageAttrs.length === 0) {
190
+ // Already wrapping its own content — nothing to restructure.
191
+ return {};
192
+ }
193
+ for (const attr of imageAttrs) {
194
+ if (attr.name.name === 'size') {
195
+ const literal = literalValueOf(attr);
196
+ if (literal.kind === 'number') {
197
+ attr.value = ctx.j.stringLiteral(`${literal.value}x${literal.value}`);
198
+ }
199
+ }
200
+ else if (attr.name.name === 'rounded' && attr.value == null) {
201
+ attr.name = ctx.j.jsxIdentifier('isRounded');
202
+ }
203
+ }
204
+ const inner = ctx.j.jsxElement(ctx.j.jsxOpeningElement(ctx.j.jsxIdentifier(ctx.reserve('Image')), imageAttrs, true), null, []);
205
+ element.openingElement.attributes = attrs.filter((a) => a.type === 'JSXAttribute' && a.name.name === 'className');
206
+ element.children = [inner];
207
+ element.openingElement.selfClosing = false;
208
+ // The walker's rename pass rewrites both names to the final (possibly
209
+ // aliased) Card.Image afterwards.
210
+ element.closingElement = ctx.j.jsxClosingElement(buildJsxName(ctx.j, 'Card.Image'));
211
+ ctx.dirty = true;
212
+ return {};
213
+ },
214
+ /**
215
+ * A Navbar.Item that wraps an RBC Navbar.Dropdown is the Bulma
216
+ * `navbar-item has-dropdown` container — bestax models that as
217
+ * Navbar.Dropdown (container) + Navbar.DropdownMenu (menu). The wrapped
218
+ * menu gets a target override consumed when its own path is visited.
219
+ */
220
+ 'navbar-item'(ctx, path, element) {
221
+ const dropdownChild = (element.children ?? []).find((c) => c.type === 'JSXElement' &&
222
+ ctx.resolve?.(c.openingElement.name)?.join('.') === 'Navbar.Dropdown');
223
+ if (!dropdownChild)
224
+ return {};
225
+ ctx.overrides.set(dropdownChild, 'Navbar.DropdownMenu');
226
+ const renderAsAttr = findAttr(element, 'renderAs');
227
+ if (renderAsAttr) {
228
+ // The container renders a div regardless; a literal renderAs is noise.
229
+ if (literalValueOf(renderAsAttr).kind === 'string') {
230
+ removeAttr(element, renderAsAttr);
231
+ }
232
+ else {
233
+ addTodo(ctx, path, 'prop:renderAs', 'dynamic renderAs on a dropdown Navbar.Item; bestax Navbar.Dropdown always renders a div');
234
+ removeAttr(element, renderAsAttr);
235
+ }
236
+ }
237
+ ctx.dirty = true;
238
+ // hoverable/active are native on the bestax container — keep them as-is.
239
+ return {
240
+ target: 'Navbar.Dropdown',
241
+ handledProps: ['hoverable', 'active', 'renderAs'],
242
+ };
243
+ },
244
+ 'navbar-container'(ctx, path, element) {
245
+ return alignTarget(ctx, path, element, 'align', { left: 'Navbar.Start', right: 'Navbar.End' }, 'Navbar.Start');
246
+ },
247
+ /** Field kind/align/multiline → grouped / hasAddons values. */
248
+ field(ctx, path, element) {
249
+ const kindAttr = findAttr(element, 'kind');
250
+ const alignAttr = findAttr(element, 'align');
251
+ const multilineAttr = findAttr(element, 'multiline');
252
+ if (!kindAttr && !alignAttr && !multilineAttr)
253
+ return {};
254
+ const kind = kindAttr ? literalValueOf(kindAttr) : null;
255
+ const align = alignAttr ? literalValueOf(alignAttr) : null;
256
+ if ((kind && kind.kind !== 'string') ||
257
+ (align && align.kind !== 'string')) {
258
+ addTodo(ctx, path, 'prop:kind', 'dynamic Field kind/align; map to the bestax `grouped` / `hasAddons` props by hand');
259
+ return {};
260
+ }
261
+ for (const attr of [kindAttr, alignAttr, multilineAttr]) {
262
+ if (attr)
263
+ removeAttr(element, attr);
264
+ }
265
+ ctx.dirty = true;
266
+ const alignValue = align && align.kind === 'string'
267
+ ? { center: 'centered', right: 'right' }[align.value]
268
+ : undefined;
269
+ if (kind && kind.kind === 'string' && kind.value === 'addons') {
270
+ addAttr(element, makeAttr(ctx.j, 'hasAddons', alignValue));
271
+ if (multilineAttr) {
272
+ addTodo(ctx, path, 'prop:multiline', "`multiline` only combines with kind='group' in Bulma; dropped from this addons Field");
273
+ }
274
+ return {};
275
+ }
276
+ // kind='group' (or a stray multiline/align without kind)
277
+ if (multilineAttr) {
278
+ addAttr(element, makeAttr(ctx.j, 'grouped', 'multiline'));
279
+ if (alignValue) {
280
+ addTodo(ctx, path, 'prop:align', 'bestax `grouped` takes one value; choose between multiline and ' +
281
+ alignValue);
282
+ }
283
+ }
284
+ else {
285
+ addAttr(element, makeAttr(ctx.j, 'grouped', alignValue));
286
+ }
287
+ return {};
288
+ },
289
+ /** Form.Label → plain <label className="label">. */
290
+ 'plain-label'(ctx, path, element) {
291
+ const sizeAttr = findAttr(element, 'size');
292
+ let className = 'label';
293
+ if (sizeAttr) {
294
+ const literal = literalValueOf(sizeAttr);
295
+ if (literal.kind === 'string') {
296
+ className = `label is-${literal.value}`;
297
+ removeAttr(element, sizeAttr);
298
+ }
299
+ else {
300
+ addTodo(ctx, path, 'prop:size', 'dynamic Form.Label size; add an is-* class by hand');
301
+ removeAttr(element, sizeAttr);
302
+ }
303
+ }
304
+ className = mergeClassName(ctx, path, element, className, 'Form.Label');
305
+ const rest = stripModifierProps(ctx, path, attributesOf(element), 'Form.Label');
306
+ path.replace(plainElement(ctx.j, 'label', className, rest, element.children ?? []));
307
+ ctx.dirty = true;
308
+ return { replaced: true };
309
+ },
310
+ /** Form.Help → plain <p className="help is-{color}">. */
311
+ 'plain-help'(ctx, path, element) {
312
+ const colorAttr = findAttr(element, 'color');
313
+ let className = 'help';
314
+ if (colorAttr) {
315
+ const literal = literalValueOf(colorAttr);
316
+ if (literal.kind === 'string') {
317
+ className = `help is-${literal.value}`;
318
+ removeAttr(element, colorAttr);
319
+ }
320
+ else {
321
+ addTodo(ctx, path, 'prop:color', 'dynamic Form.Help color; consider the bestax Field `message`/`messageColor` props instead');
322
+ removeAttr(element, colorAttr);
323
+ }
324
+ }
325
+ className = mergeClassName(ctx, path, element, className, 'Form.Help');
326
+ const rest = stripModifierProps(ctx, path, attributesOf(element), 'Form.Help');
327
+ path.replace(plainElement(ctx.j, 'p', className, rest, element.children ?? []));
328
+ ctx.dirty = true;
329
+ return { replaced: true };
330
+ },
331
+ /** Form.InputFile filename → fileName + hasName. */
332
+ 'input-file'(ctx, _path, element) {
333
+ const attr = findAttr(element, 'filename');
334
+ if (attr) {
335
+ attr.name = ctx.j.jsxIdentifier('fileName');
336
+ addAttr(element, makeAttr(ctx.j, 'hasName'));
337
+ ctx.dirty = true;
338
+ }
339
+ return {};
340
+ },
341
+ /** Icon children: <Icon><i className="fas fa-home"/></Icon> → name/library. */
342
+ icon(ctx, path, element) {
343
+ const children = (element.children ?? []).filter((c) => !(c.type === 'JSXText' && c.value.trim() === ''));
344
+ if (children.length === 0)
345
+ return {};
346
+ const child = children[0];
347
+ const parsed = children.length === 1 &&
348
+ child.type === 'JSXElement' &&
349
+ child.openingElement.name.type === 'JSXIdentifier' &&
350
+ child.openingElement.name.name === 'i'
351
+ ? (() => {
352
+ const cls = (child.openingElement.attributes ?? []).find((a) => a.type === 'JSXAttribute' && a.name.name === 'className');
353
+ const value = cls && cls.value && cls.value.type === 'StringLiteral'
354
+ ? cls.value.value
355
+ : null;
356
+ return value ? parseIconClasses(value) : null;
357
+ })()
358
+ : null;
359
+ if (!parsed) {
360
+ addTodo(ctx, path, 'icon-children', 'bestax Icon renders from a `name` prop, not children; convert this icon markup by hand');
361
+ return {};
362
+ }
363
+ if (!findAttr(element, 'name') && !findAttr(element, 'icon')) {
364
+ addAttr(element, makeAttr(ctx.j, 'name', parsed.name));
365
+ if (parsed.library)
366
+ addAttr(element, makeAttr(ctx.j, 'library', parsed.library));
367
+ if (parsed.variant)
368
+ addAttr(element, makeAttr(ctx.j, 'variant', parsed.variant));
369
+ }
370
+ element.children = [];
371
+ if (element.closingElement) {
372
+ element.openingElement.selfClosing = true;
373
+ element.closingElement = null;
374
+ }
375
+ ctx.dirty = true;
376
+ return {};
377
+ },
378
+ /** RBC Loader is a plain <div class="loader"> — keep exactly that. */
379
+ 'plain-loader'(ctx, path, element) {
380
+ const className = mergeClassName(ctx, path, element, 'loader', 'Loader');
381
+ const rest = stripModifierProps(ctx, path, attributesOf(element), 'Loader');
382
+ path.replace(plainElement(ctx.j, 'div', className, rest, element.children ?? []));
383
+ ctx.dirty = true;
384
+ return { replaced: true };
385
+ },
386
+ /** Menu.List title="X" → a <Menu.Label>X</Menu.Label> sibling before the list. */
387
+ 'menu-list'(ctx, path, element) {
388
+ const attr = findAttr(element, 'title');
389
+ if (!attr)
390
+ return {};
391
+ const parent = path.parent?.node;
392
+ if (!parent || parent.type !== 'JSXElement') {
393
+ addTodo(ctx, path, 'prop:title', 'Menu.List `title` becomes a separate <Menu.Label> sibling; add it by hand');
394
+ return {};
395
+ }
396
+ const j = ctx.j;
397
+ const menuLocal = ctx.reserve('Menu');
398
+ const labelChildren = attr.value == null
399
+ ? []
400
+ : attr.value.type === 'StringLiteral'
401
+ ? [j.jsxText(attr.value.value)]
402
+ : [attr.value]; // JSXExpressionContainer carries over as a child
403
+ const labelName = () => j.jsxMemberExpression(j.jsxIdentifier(menuLocal), j.jsxIdentifier('Label'));
404
+ const label = j.jsxElement(j.jsxOpeningElement(labelName(), []), j.jsxClosingElement(labelName()), labelChildren);
405
+ removeAttr(element, attr);
406
+ const index = parent.children.indexOf(element);
407
+ if (index !== -1) {
408
+ parent.children.splice(index, 0, label, j.jsxText('\n'));
409
+ }
410
+ ctx.dirty = true;
411
+ return {};
412
+ },
413
+ /** Panel.Tabs.Tab → plain <a> (bestax Panel.Tabs children are anchors). */
414
+ 'panel-tab'(ctx, path, element) {
415
+ const activeAttr = findAttr(element, 'active');
416
+ let className;
417
+ if (activeAttr) {
418
+ const literal = literalValueOf(activeAttr);
419
+ if (literal.kind === 'boolean' && literal.value === true) {
420
+ className = 'is-active';
421
+ removeAttr(element, activeAttr);
422
+ }
423
+ else if (literal.kind === 'boolean') {
424
+ removeAttr(element, activeAttr);
425
+ }
426
+ else {
427
+ addTodo(ctx, path, 'prop:active', "dynamic Panel.Tabs.Tab active; set className={active ? 'is-active' : undefined} by hand");
428
+ removeAttr(element, activeAttr);
429
+ }
430
+ }
431
+ className = mergeClassName(ctx, path, element, className, 'Panel.Tabs.Tab');
432
+ const rest = stripModifierProps(ctx, path, attributesOf(element), 'Panel.Tabs.Tab');
433
+ path.replace(plainElement(ctx.j, 'a', className, rest, element.children ?? []));
434
+ ctx.dirty = true;
435
+ return { replaced: true };
436
+ },
437
+ /** Breadcrumb.Item → <li className="is-active"><a href>…</a></li>. */
438
+ 'breadcrumb-item'(ctx, path, element) {
439
+ const j = ctx.j;
440
+ const activeAttr = findAttr(element, 'active');
441
+ let liClass;
442
+ if (activeAttr) {
443
+ const literal = literalValueOf(activeAttr);
444
+ if (literal.kind === 'boolean' && literal.value === true)
445
+ liClass = 'is-active';
446
+ else if (literal.kind !== 'boolean') {
447
+ addTodo(ctx, path, 'prop:active', "dynamic Breadcrumb.Item active; set the li className={active ? 'is-active' : undefined} by hand");
448
+ }
449
+ removeAttr(element, activeAttr);
450
+ }
451
+ liClass = mergeClassName(ctx, path, element, liClass, 'Breadcrumb.Item');
452
+ const anchorAttrs = stripModifierProps(ctx, path, attributesOf(element), 'Breadcrumb.Item');
453
+ const children = element.children ?? [];
454
+ const solidChildren = children.filter((c) => !(c.type === 'JSXText' && c.value.trim() === ''));
455
+ // `<Breadcrumb.Item><a href>…</a></Breadcrumb.Item>` already carries its
456
+ // own anchor — wrap it in the <li> directly instead of nesting anchors.
457
+ const existingAnchor = solidChildren.length === 1 &&
458
+ solidChildren[0].type === 'JSXElement' &&
459
+ solidChildren[0].openingElement.name.type === 'JSXIdentifier' &&
460
+ solidChildren[0].openingElement.name.name === 'a'
461
+ ? solidChildren[0]
462
+ : null;
463
+ if (existingAnchor && anchorAttrs.length > 0) {
464
+ existingAnchor.openingElement.attributes = [
465
+ ...(existingAnchor.openingElement.attributes ?? []),
466
+ ...anchorAttrs,
467
+ ];
468
+ }
469
+ const anchor = existingAnchor ?? plainElement(j, 'a', undefined, anchorAttrs, children);
470
+ path.replace(plainElement(j, 'li', liClass, [], [anchor]));
471
+ ctx.dirty = true;
472
+ return { replaced: true };
473
+ },
474
+ /** Table.Container: fold into `isResponsive` on a single Table child. */
475
+ 'table-container'(ctx, path, element) {
476
+ const children = (element.children ?? []).filter((c) => !(c.type === 'JSXText' && c.value.trim() === ''));
477
+ if (children.length === 1 && children[0].type === 'JSXElement') {
478
+ if (findAttr(element, 'className')) {
479
+ addTodo(ctx, path, 'prop:className', 'Table.Container className; the container folded into `isResponsive` on its Table — re-apply the class by hand');
480
+ }
481
+ const child = children[0];
482
+ addAttr(child, makeAttr(ctx.j, 'isResponsive'));
483
+ path.replace(child);
484
+ ctx.dirty = true;
485
+ return { replaced: true };
486
+ }
487
+ const className = mergeClassName(ctx, path, element, 'table-container', 'Table.Container');
488
+ const rest = stripModifierProps(ctx, path, attributesOf(element), 'Table.Container');
489
+ path.replace(plainElement(ctx.j, 'div', className, rest, element.children ?? []));
490
+ ctx.dirty = true;
491
+ return { replaced: true };
492
+ },
493
+ /**
494
+ * RBC Tabs.Tab renders its own anchor; bestax Tabs.Item is the bare <li>
495
+ * and Bulma styles `.tabs li a` — wrap the children in an <a> unless one
496
+ * is already there.
497
+ */
498
+ 'tab-item'(ctx, path, element) {
499
+ const children = element.children ?? [];
500
+ const solid = children.filter((c) => !(c.type === 'JSXText' && c.value.trim() === ''));
501
+ if (solid.length === 0)
502
+ return {};
503
+ const alreadyAnchor = solid.length === 1 &&
504
+ solid[0].type === 'JSXElement' &&
505
+ solid[0].openingElement.name.type === 'JSXIdentifier' &&
506
+ solid[0].openingElement.name.name === 'a';
507
+ if (alreadyAnchor)
508
+ return {};
509
+ const anchorAttrs = stripModifierProps(ctx, path, attributesOf(element).filter(a => !['active', 'className'].includes(a.name.name)), 'Tabs.Tab');
510
+ for (const attr of attributesOf(element)) {
511
+ if (!['active', 'className'].includes(attr.name.name)) {
512
+ removeAttr(element, attr);
513
+ }
514
+ }
515
+ element.children = [
516
+ plainElement(ctx.j, 'a', undefined, anchorAttrs, children),
517
+ ];
518
+ ctx.dirty = true;
519
+ return {};
520
+ },
521
+ /** RBC Tabs children sit directly under .tabs; bestax needs a Tabs.List. */
522
+ tabs(ctx, _path, element) {
523
+ const children = element.children ?? [];
524
+ const hasContent = children.some((c) => c.type !== 'JSXText' || c.value.trim() !== '');
525
+ if (!hasContent)
526
+ return {};
527
+ const j = ctx.j;
528
+ const tabsLocal = ctx.reserve('Tabs');
529
+ const listName = () => j.jsxMemberExpression(j.jsxIdentifier(tabsLocal), j.jsxIdentifier('List'));
530
+ const list = j.jsxElement(j.jsxOpeningElement(listName(), []), j.jsxClosingElement(listName()), children);
531
+ element.children = [j.jsxText('\n'), list, j.jsxText('\n')];
532
+ ctx.dirty = true;
533
+ return {};
534
+ },
535
+ };
536
+ export function runSpecial(name, ctx, path, element) {
537
+ const handler = SPECIALS[name];
538
+ if (!handler)
539
+ return {};
540
+ return handler(ctx, path, element);
541
+ }
542
+ /** Responsive-flattening kinds keyed by mapping special names. */
543
+ export const RESPONSIVE_KINDS = {
544
+ columns: 'columns',
545
+ column: 'column',
546
+ };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Stylesheet migration for react-bulma-components apps: Bulma 0.9 Sass
3
+ * (`@import` + `$var !default` overrides, `_all` aggregator partials) →
4
+ * Bulma v1 modules (`@use "bulma/sass" with (…)`), plus the bestax extras.
5
+ *
6
+ * Sass has no jscodeshift parser, so this is a conservative line-based text
7
+ * transform: it only rewrites patterns it can prove safe and leaves a
8
+ * `// TODO(bestax-migrate): …` everywhere else (line comments are valid in
9
+ * both SCSS and indented Sass).
10
+ */
11
+ import type { StylesTransform } from '../../types.js';
12
+ export declare const transformStyles: StylesTransform;
13
+ //# sourceMappingURL=styles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../../../src/sources/react-bulma-components/styles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAiB,MAAM,gBAAgB,CAAC;AAqFrE,eAAO,MAAM,eAAe,EAAE,eAmL7B,CAAC"}