bestax-migrate 2.26.0 → 2.28.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.
- package/README.md +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +12 -6
- package/dist/sources/_shared/deps-common.d.ts +2 -0
- package/dist/sources/_shared/deps-common.d.ts.map +1 -1
- package/dist/sources/_shared/deps-common.js +3 -1
- package/dist/sources/_shared/polymorphic.d.ts.map +1 -1
- package/dist/sources/_shared/polymorphic.js +16 -1
- package/dist/sources/bloomer/mapping.d.ts.map +1 -1
- package/dist/sources/bloomer/mapping.js +9 -4
- package/dist/sources/bloomer/specials.d.ts.map +1 -1
- package/dist/sources/bloomer/specials.js +30 -3
- package/dist/sources/bulma-classes/class-map.d.ts +41 -10
- package/dist/sources/bulma-classes/class-map.d.ts.map +1 -1
- package/dist/sources/bulma-classes/class-map.js +69 -13
- package/dist/sources/bulma-classes/glyph.d.ts +20 -0
- package/dist/sources/bulma-classes/glyph.d.ts.map +1 -1
- package/dist/sources/bulma-classes/glyph.js +64 -0
- package/dist/sources/bulma-classes/html-assets.d.ts +87 -0
- package/dist/sources/bulma-classes/html-assets.d.ts.map +1 -0
- package/dist/sources/bulma-classes/html-assets.js +246 -0
- package/dist/sources/bulma-classes/html-rules.d.ts +5 -0
- package/dist/sources/bulma-classes/html-rules.d.ts.map +1 -1
- package/dist/sources/bulma-classes/html-rules.js +14 -2
- package/dist/sources/bulma-classes/html.d.ts +12 -4
- package/dist/sources/bulma-classes/html.d.ts.map +1 -1
- package/dist/sources/bulma-classes/html.js +224 -49
- package/dist/sources/bulma-classes/plan.d.ts +40 -1
- package/dist/sources/bulma-classes/plan.d.ts.map +1 -1
- package/dist/sources/bulma-classes/plan.js +160 -24
- package/dist/sources/bulma-classes/project.d.ts +10 -2
- package/dist/sources/bulma-classes/project.d.ts.map +1 -1
- package/dist/sources/bulma-classes/project.js +17 -8
- package/dist/sources/bulma-classes/transform.d.ts +2 -1
- package/dist/sources/bulma-classes/transform.d.ts.map +1 -1
- package/dist/sources/bulma-classes/transform.js +231 -33
- package/dist/sources/rbx/mapping.d.ts +10 -0
- package/dist/sources/rbx/mapping.d.ts.map +1 -1
- package/dist/sources/rbx/mapping.js +13 -7
- package/dist/sources/react-bulma-components/mapping.d.ts.map +1 -1
- package/dist/sources/react-bulma-components/mapping.js +19 -5
- package/package.json +4 -3
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* Unlike the library sources there is no import to anchor on: a file is in
|
|
5
5
|
* scope when a lowercase JSX element carries a `className` with a Bulma class
|
|
6
6
|
* `class-map.ts` knows. Each such element goes through `plan()`, which
|
|
7
|
-
* converts it only when the component renders exactly the same markup
|
|
7
|
+
* converts it only when the component renders exactly the same markup, but
|
|
8
|
+
* for the one exception it states (Font Awesome's hidden `<i>`).
|
|
8
9
|
*
|
|
9
10
|
* Passes, in order:
|
|
10
11
|
* 1. the shared stylesheet-import pass (a no-op under the default `keep`)
|
|
@@ -21,9 +22,10 @@ import { addTodo, findAttr, forgetJsxParens, jsxNameParts, literalValueOf, makeA
|
|
|
21
22
|
import { collectBoundNames, makeReserve, prefersTabs, resolvesToBinding, } from '../_shared/imports.js';
|
|
22
23
|
import { rewriteStylesheetImports } from '../_shared/css-imports.js';
|
|
23
24
|
import { BESTAX, buildBestaxImport, placeBestaxImport, seedBestaxImport, } from '../_shared/bestax-import.js';
|
|
25
|
+
import { glyphProps } from './glyph.js';
|
|
24
26
|
import { fileContextTodo, plan, } from './plan.js';
|
|
25
27
|
import { REACT_RUNTIMES, } from './project.js';
|
|
26
|
-
import { PLACED, placedFor, ROOTS, rootFor } from './class-map.js';
|
|
28
|
+
import { PLACED, placedFor, ROOTS, rootFor, WRAPPERS, } from './class-map.js';
|
|
27
29
|
import { ruleId } from './rules.js';
|
|
28
30
|
/**
|
|
29
31
|
* A part imported under its flat export (`MenuList`, `FieldLabel`) is the
|
|
@@ -103,13 +105,25 @@ function spliceClassName(element, replacement, carried = []) {
|
|
|
103
105
|
}
|
|
104
106
|
attrs.splice(attrs.indexOf(classAttr), 1, ...replacement);
|
|
105
107
|
}
|
|
106
|
-
/**
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
/**
|
|
109
|
+
* Put `lead`, then `props`, then what stays in `className`, where
|
|
110
|
+
* `className` was.
|
|
111
|
+
*/
|
|
112
|
+
function writeClassName(j, element, props, className, lead = []) {
|
|
113
|
+
const replacement = [
|
|
114
|
+
...lead,
|
|
115
|
+
...props.map(([name, value]) => makeAttr(j, name, value === true ? undefined : value)),
|
|
116
|
+
];
|
|
109
117
|
if (className)
|
|
110
118
|
replacement.push(makeAttr(j, 'className', className));
|
|
111
119
|
spliceClassName(element, replacement);
|
|
112
120
|
}
|
|
121
|
+
/** A glyph's props as JSX attributes, a list of features as an array. */
|
|
122
|
+
function glyphAttributes(j, glyph) {
|
|
123
|
+
return glyphProps(glyph).map(([name, value]) => typeof value === 'string'
|
|
124
|
+
? makeAttr(j, name, value)
|
|
125
|
+
: j.jsxAttribute(j.jsxIdentifier(name), j.jsxExpressionContainer(propValue(j, value, false))));
|
|
126
|
+
}
|
|
113
127
|
/**
|
|
114
128
|
* The same, for a `className` a joiner builds: the classes a prop now
|
|
115
129
|
* renders leave the call, each condition that became a prop moves onto it,
|
|
@@ -117,7 +131,7 @@ function writeClassName(j, element, props, className) {
|
|
|
117
131
|
* conditional, no `className` when nothing is left). Returns whether the
|
|
118
132
|
* call is gone.
|
|
119
133
|
*/
|
|
120
|
-
function writeJoined(j, element, conversion, joined) {
|
|
134
|
+
function writeJoined(j, element, conversion, joined, lead = []) {
|
|
121
135
|
const { call, statics, parts } = joined;
|
|
122
136
|
const kept = new Set(conversion.className?.split(' ') ?? []);
|
|
123
137
|
const carried = [];
|
|
@@ -164,6 +178,7 @@ function writeJoined(j, element, conversion, joined) {
|
|
|
164
178
|
classes.length > 0 ? [makeAttr(j, 'className', classes.join(' '))] : [];
|
|
165
179
|
}
|
|
166
180
|
spliceClassName(element, [
|
|
181
|
+
...lead,
|
|
167
182
|
...conversion.props.map(([name, value]) => makeAttr(j, name, value === true ? undefined : value)),
|
|
168
183
|
...conditions,
|
|
169
184
|
...rest,
|
|
@@ -734,6 +749,83 @@ function staticContent(element) {
|
|
|
734
749
|
child.expression.type === 'StringLiteral' &&
|
|
735
750
|
child.expression.value !== '')));
|
|
736
751
|
}
|
|
752
|
+
/** The static classes of a plain HTML element, or undefined. */
|
|
753
|
+
function htmlTokens(element) {
|
|
754
|
+
const name = element?.openingElement?.name;
|
|
755
|
+
if (name?.type !== 'JSXIdentifier' || !INTRINSIC.test(name.name)) {
|
|
756
|
+
return undefined;
|
|
757
|
+
}
|
|
758
|
+
const classAttr = findAttr(element, 'className');
|
|
759
|
+
const className = classAttr ? staticClassName(classAttr) : '';
|
|
760
|
+
return className === null
|
|
761
|
+
? undefined
|
|
762
|
+
: className.split(/\s+/).filter(Boolean);
|
|
763
|
+
}
|
|
764
|
+
/** Whether a component builds content from props rather than children. */
|
|
765
|
+
function buildsContent(entry) {
|
|
766
|
+
return (!!entry &&
|
|
767
|
+
(!!entry.buildsIcons ||
|
|
768
|
+
!!entry.buildsFile ||
|
|
769
|
+
entry.rendersText !== undefined ||
|
|
770
|
+
!!entry.countsChildren ||
|
|
771
|
+
!!entry.absorbs));
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* What names a link or button, as `ElementFacts.clickableAround` says, with
|
|
775
|
+
* `self` left out of its content: its own `aria-labelledby` or `aria-label`,
|
|
776
|
+
* written out and not empty, else static text in it that is not inside an
|
|
777
|
+
* icon or hidden, else nothing. Undefined when it can't be told: a name or
|
|
778
|
+
* content written as an expression, a `title`, or something inside that may
|
|
779
|
+
* name it some other way (`kindOf` says which, or an `alt`, an `<svg>` or a
|
|
780
|
+
* label of its own).
|
|
781
|
+
*/
|
|
782
|
+
function labelOf(clickable, self, kindOf) {
|
|
783
|
+
if (findAttr(clickable, 'title'))
|
|
784
|
+
return undefined;
|
|
785
|
+
for (const name of ['aria-labelledby', 'aria-label']) {
|
|
786
|
+
const attr = findAttr(clickable, name);
|
|
787
|
+
if (!attr)
|
|
788
|
+
continue;
|
|
789
|
+
const value = attributeValue(attr);
|
|
790
|
+
if (value === null)
|
|
791
|
+
return undefined;
|
|
792
|
+
if (typeof value === 'string' && value.trim() !== '')
|
|
793
|
+
return name;
|
|
794
|
+
}
|
|
795
|
+
let unsure = false;
|
|
796
|
+
const named = (node) => (node.children ?? []).some((child) => {
|
|
797
|
+
if (child === self || dropped(child))
|
|
798
|
+
return false;
|
|
799
|
+
if (child.type === 'JSXText') {
|
|
800
|
+
return /\S/.test(jsxTextRenders(child.value));
|
|
801
|
+
}
|
|
802
|
+
if (child.type === 'JSXFragment')
|
|
803
|
+
return named(child);
|
|
804
|
+
if (child.type !== 'JSXElement') {
|
|
805
|
+
if (isContent(child))
|
|
806
|
+
unsure = true;
|
|
807
|
+
return false;
|
|
808
|
+
}
|
|
809
|
+
const kind = kindOf(child);
|
|
810
|
+
if (kind === 'icon')
|
|
811
|
+
return false;
|
|
812
|
+
if (kind === 'opaque') {
|
|
813
|
+
unsure = true;
|
|
814
|
+
return false;
|
|
815
|
+
}
|
|
816
|
+
const hidden = findAttr(child, 'aria-hidden');
|
|
817
|
+
if (hidden && attributeValue(hidden) === 'true')
|
|
818
|
+
return false;
|
|
819
|
+
if (['alt', 'title', 'aria-label', 'aria-labelledby'].some(name => findAttr(child, name)) ||
|
|
820
|
+
child.openingElement.name.name === 'svg') {
|
|
821
|
+
unsure = true;
|
|
822
|
+
}
|
|
823
|
+
return named(child);
|
|
824
|
+
});
|
|
825
|
+
if (named(clickable))
|
|
826
|
+
return 'text';
|
|
827
|
+
return unsure ? undefined : 'none';
|
|
828
|
+
}
|
|
737
829
|
/**
|
|
738
830
|
* An element's content as one prop value: its one element, or its text, or
|
|
739
831
|
* a fragment of them all, each text written as the string it renders.
|
|
@@ -1012,6 +1104,121 @@ export default function transform(fileInfo, api, options = {}) {
|
|
|
1012
1104
|
readings.set(child, reading);
|
|
1013
1105
|
return reading;
|
|
1014
1106
|
};
|
|
1107
|
+
// A bestax component already in the file is read as the markup the table
|
|
1108
|
+
// says it renders, so a TODO that reads what is around an element says the
|
|
1109
|
+
// same once the elements around it have converted.
|
|
1110
|
+
/** The table's entry for a bestax component, with its root class. */
|
|
1111
|
+
const entryFor = (target) => {
|
|
1112
|
+
const root = target &&
|
|
1113
|
+
Object.keys(ROOTS).find(key => ROOTS[key].status === 'mapped' && ROOTS[key].target === target);
|
|
1114
|
+
return root ? { root, entry: ROOTS[root] } : undefined;
|
|
1115
|
+
};
|
|
1116
|
+
/**
|
|
1117
|
+
* The classes an element renders, as far as the file shows: a plain HTML
|
|
1118
|
+
* element's static ones, or a bestax component's root class and the flags
|
|
1119
|
+
* written out as `true` (`<Control hasIconsLeft>` as `control
|
|
1120
|
+
* has-icons-left`).
|
|
1121
|
+
*/
|
|
1122
|
+
const renderedTokens = (path) => {
|
|
1123
|
+
const html = htmlTokens(path.node);
|
|
1124
|
+
if (html)
|
|
1125
|
+
return html;
|
|
1126
|
+
const found = entryFor(bestaxTarget(path.node, path));
|
|
1127
|
+
if (!found)
|
|
1128
|
+
return undefined;
|
|
1129
|
+
const flags = Object.entries(found.entry.modifiers ?? {})
|
|
1130
|
+
.filter(([, modifier]) => {
|
|
1131
|
+
const [write, ...more] = modifier.writes;
|
|
1132
|
+
const attr = write && more.length === 0 && write.value === undefined
|
|
1133
|
+
? findAttr(path.node, write.prop)
|
|
1134
|
+
: undefined;
|
|
1135
|
+
return attr && attributeValue(attr) === true;
|
|
1136
|
+
})
|
|
1137
|
+
.map(([token]) => token);
|
|
1138
|
+
return [found.root, ...flags];
|
|
1139
|
+
};
|
|
1140
|
+
/**
|
|
1141
|
+
* The tag an element renders: its own, or a bestax component's, from a
|
|
1142
|
+
* static `as` or the table; an empty string for a bestax component whose
|
|
1143
|
+
* tag the table doesn't give, and undefined for any other component.
|
|
1144
|
+
*/
|
|
1145
|
+
const renderedTag = (path) => {
|
|
1146
|
+
const name = path.node.openingElement.name;
|
|
1147
|
+
if (name.type === 'JSXIdentifier' && INTRINSIC.test(name.name)) {
|
|
1148
|
+
return name.name;
|
|
1149
|
+
}
|
|
1150
|
+
const target = bestaxTarget(path.node, path);
|
|
1151
|
+
if (!target)
|
|
1152
|
+
return undefined;
|
|
1153
|
+
const as = findAttr(path.node, 'as');
|
|
1154
|
+
if (as) {
|
|
1155
|
+
const value = attributeValue(as);
|
|
1156
|
+
return typeof value === 'string' ? value : undefined;
|
|
1157
|
+
}
|
|
1158
|
+
return (entryFor(target)?.entry.tag ??
|
|
1159
|
+
Object.keys(WRAPPERS).find(tag => WRAPPERS[tag] === target) ??
|
|
1160
|
+
'');
|
|
1161
|
+
};
|
|
1162
|
+
/**
|
|
1163
|
+
* Where an element whose target renders a glyph sits, for its TODO: the
|
|
1164
|
+
* classes the element directly around it renders, and the nearest <a> or
|
|
1165
|
+
* <button> around it, reached through markup and bestax components only,
|
|
1166
|
+
* since any other component between could render anything, with what
|
|
1167
|
+
* names that.
|
|
1168
|
+
*/
|
|
1169
|
+
const glyphPlace = (elementPath) => {
|
|
1170
|
+
const parent = elementPath.parent;
|
|
1171
|
+
const parentTokens = parent?.node?.type === 'JSXElement' ? renderedTokens(parent) : undefined;
|
|
1172
|
+
let clickable;
|
|
1173
|
+
let tag;
|
|
1174
|
+
for (let up = elementPath.parent; up && !clickable; up = up.parent) {
|
|
1175
|
+
if (up.node?.type !== 'JSXElement')
|
|
1176
|
+
continue;
|
|
1177
|
+
tag = renderedTag(up);
|
|
1178
|
+
if (tag === undefined)
|
|
1179
|
+
break;
|
|
1180
|
+
if (tag === 'a' || tag === 'button')
|
|
1181
|
+
clickable = up;
|
|
1182
|
+
}
|
|
1183
|
+
const kindOf = (element) => {
|
|
1184
|
+
const tokens = htmlTokens(element);
|
|
1185
|
+
const entries = tokens
|
|
1186
|
+
? tokens.map(token => rootFor(token)).filter(Boolean)
|
|
1187
|
+
: [entryFor(bestaxTarget(element, clickable))?.entry];
|
|
1188
|
+
if (!tokens && !bestaxTarget(element, clickable))
|
|
1189
|
+
return 'opaque';
|
|
1190
|
+
if (entries.some(entry => entry?.readsGlyph))
|
|
1191
|
+
return 'icon';
|
|
1192
|
+
return entries.some(buildsContent) ? 'opaque' : 'open';
|
|
1193
|
+
};
|
|
1194
|
+
const label = clickable && labelOf(clickable.node, elementPath.node, kindOf);
|
|
1195
|
+
return {
|
|
1196
|
+
...(parentTokens && { parentTokens }),
|
|
1197
|
+
...(clickable && {
|
|
1198
|
+
clickableAround: { tag: tag, ...(label && { label }) },
|
|
1199
|
+
}),
|
|
1200
|
+
};
|
|
1201
|
+
};
|
|
1202
|
+
/**
|
|
1203
|
+
* An element that goes with the one around it, which renders it from
|
|
1204
|
+
* props, converts no more on its own, and nor does it when that one stays
|
|
1205
|
+
* markup for a person: a re-run has to find the markup it found. Only its
|
|
1206
|
+
* TODOs stay. It has a class to be found by, so it has been planned.
|
|
1207
|
+
*/
|
|
1208
|
+
const forget = (node) => {
|
|
1209
|
+
const own = { conversion: null, todos: planned.get(node).todos };
|
|
1210
|
+
planned.set(node, own);
|
|
1211
|
+
readings.delete(node);
|
|
1212
|
+
const at = elements.findIndex(found => found.path.node === node);
|
|
1213
|
+
if (at === -1)
|
|
1214
|
+
return;
|
|
1215
|
+
if (own.todos.length > 0) {
|
|
1216
|
+
elements[at] = { ...elements[at], plan: own, converts: false };
|
|
1217
|
+
}
|
|
1218
|
+
else {
|
|
1219
|
+
elements.splice(at, 1);
|
|
1220
|
+
}
|
|
1221
|
+
};
|
|
1015
1222
|
/** The element directly around this one, when this is its only child. */
|
|
1016
1223
|
const holderOf = (elementPath) => {
|
|
1017
1224
|
const up = elementPath.parent?.node;
|
|
@@ -1065,6 +1272,9 @@ export default function transform(fileInfo, api, options = {}) {
|
|
|
1065
1272
|
}),
|
|
1066
1273
|
...(textOf(element) !== undefined && { text: textOf(element) }),
|
|
1067
1274
|
};
|
|
1275
|
+
if (facts.tokens.some(token => rootFor(token)?.readsGlyph)) {
|
|
1276
|
+
Object.assign(facts, glyphPlace(elementPath));
|
|
1277
|
+
}
|
|
1068
1278
|
// A joiner call the codemod can read converts exactly: its fixed classes
|
|
1069
1279
|
// as for a static className, and each condition on a flag as its prop.
|
|
1070
1280
|
const joined = className === null
|
|
@@ -1085,6 +1295,7 @@ export default function transform(fileInfo, api, options = {}) {
|
|
|
1085
1295
|
}
|
|
1086
1296
|
let result = exact ?? plan(facts);
|
|
1087
1297
|
const builds = result.conversion?.icons;
|
|
1298
|
+
const glyphs = result.conversion?.glyph !== undefined;
|
|
1088
1299
|
const converts = result.conversion !== null || result.fold !== undefined;
|
|
1089
1300
|
const becomes = result.conversion?.target ?? result.fold?.target;
|
|
1090
1301
|
if (className === null && !exact && becomes) {
|
|
@@ -1117,27 +1328,14 @@ export default function transform(fileInfo, api, options = {}) {
|
|
|
1117
1328
|
inner.numbers.push(...result.fold.numbers);
|
|
1118
1329
|
}
|
|
1119
1330
|
if (builds) {
|
|
1120
|
-
// A `.icon` the target builds from props goes with the element
|
|
1121
|
-
// doesn't convert on its own, and nor does it when a computed
|
|
1122
|
-
// `className` leaves the element to a person: a re-run has to find
|
|
1123
|
-
// the markup it found.
|
|
1331
|
+
// A `.icon` the target builds from props goes with the element.
|
|
1124
1332
|
const inside = (element.children ?? []).filter((child) => !dropped(child));
|
|
1125
|
-
for (const icon of builds)
|
|
1126
|
-
|
|
1127
|
-
const own = { conversion: null, todos: planned.get(node).todos };
|
|
1128
|
-
planned.set(node, own);
|
|
1129
|
-
readings.delete(node);
|
|
1130
|
-
const at = elements.findIndex(found => found.path.node === node);
|
|
1131
|
-
if (at === -1)
|
|
1132
|
-
continue;
|
|
1133
|
-
if (own.todos.length > 0) {
|
|
1134
|
-
elements[at] = { ...elements[at], plan: own, converts: false };
|
|
1135
|
-
}
|
|
1136
|
-
else {
|
|
1137
|
-
elements.splice(at, 1);
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1333
|
+
for (const icon of builds)
|
|
1334
|
+
forget(inside[icon.index]);
|
|
1140
1335
|
}
|
|
1336
|
+
// So does the <i> a glyph the target renders takes the place of.
|
|
1337
|
+
if (glyphs)
|
|
1338
|
+
forget(sole);
|
|
1141
1339
|
planned.set(element, result);
|
|
1142
1340
|
if (result.conversion || result.fold || result.todos.length > 0) {
|
|
1143
1341
|
elements.unshift({ path: elementPath, plan: result, converts });
|
|
@@ -1313,16 +1511,14 @@ export default function transform(fileInfo, api, options = {}) {
|
|
|
1313
1511
|
}
|
|
1314
1512
|
}
|
|
1315
1513
|
// A target that renders the element's children itself takes their place:
|
|
1316
|
-
// they go, and so does the closing tag, their comments
|
|
1317
|
-
//
|
|
1514
|
+
// they go, and so does the closing tag, their comments (in their tags
|
|
1515
|
+
// and attributes too: a glyph's <i> has a `className`) and its own
|
|
1516
|
+
// moving to the name that stays.
|
|
1318
1517
|
const gone = conversion.rendersChildren
|
|
1319
1518
|
? [
|
|
1320
1519
|
...(element.children ?? [])
|
|
1321
1520
|
.filter((node) => node.type === 'JSXElement')
|
|
1322
|
-
.flatMap((node) =>
|
|
1323
|
-
...(node.comments ?? []),
|
|
1324
|
-
...tagComments(node),
|
|
1325
|
-
]),
|
|
1521
|
+
.flatMap((node) => commentsWithin(node)),
|
|
1326
1522
|
...[element.closingElement, element.closingElement?.name].flatMap((node) => node?.comments ?? []),
|
|
1327
1523
|
]
|
|
1328
1524
|
: [];
|
|
@@ -1333,10 +1529,12 @@ export default function transform(fileInfo, api, options = {}) {
|
|
|
1333
1529
|
removeAttr(holder, findAttr(holder, name));
|
|
1334
1530
|
}
|
|
1335
1531
|
const joined = joins.get(element);
|
|
1532
|
+
// A glyph the target renders goes first, as the planner wrote it.
|
|
1533
|
+
const lead = conversion.glyph ? glyphAttributes(j, conversion.glyph) : [];
|
|
1336
1534
|
if (!joined) {
|
|
1337
|
-
writeClassName(j, element, conversion.props, conversion.className);
|
|
1535
|
+
writeClassName(j, element, conversion.props, conversion.className, lead);
|
|
1338
1536
|
}
|
|
1339
|
-
else if (writeJoined(j, element, conversion, joined)) {
|
|
1537
|
+
else if (writeJoined(j, element, conversion, joined, lead)) {
|
|
1340
1538
|
unjoined.add(joined.call.callee.name);
|
|
1341
1539
|
}
|
|
1342
1540
|
if (conversion.rendersChildren) {
|
|
@@ -13,6 +13,16 @@
|
|
|
13
13
|
* explicit conversions rather than guesses.
|
|
14
14
|
*/
|
|
15
15
|
import type { ComponentMapping, PropAction } from '../../types.js';
|
|
16
|
+
/**
|
|
17
|
+
* Components whose bestax counterpart really does accept `as`. An empty
|
|
18
|
+
* action passes the prop through untouched; its job is to claim the name so
|
|
19
|
+
* the universal `AS_TODO` never sees it.
|
|
20
|
+
*
|
|
21
|
+
* Several of these narrow `as` to a literal union (`Footer` is
|
|
22
|
+
* `'footer' | 'div'`, `Control` is `'div'`), so an `as={SomeComponent}` still
|
|
23
|
+
* surfaces — as a type error the user can see, rather than a silent rewrite.
|
|
24
|
+
*/
|
|
25
|
+
export declare const AS_OK: PropAction;
|
|
16
26
|
/**
|
|
17
27
|
* Helper props rbx mixes into every component via `HelpersProps`. Applied
|
|
18
28
|
* after each component's own prop map, to whatever attributes are left.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mapping.d.ts","sourceRoot":"","sources":["../../../src/sources/rbx/mapping.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"mapping.d.ts","sourceRoot":"","sources":["../../../src/sources/rbx/mapping.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAoBnE;;;;;;;;GAQG;AACH,eAAO,MAAM,KAAK,EAAE,UAAe,CAAC;AAEpC;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CA4CtD,CAAC;AAEF,uDAAuD;AACvD,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAOrD,CAAC;AAEF,2DAA2D;AAC3D,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAQvD,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAShE,CAAC;AAEF,eAAO,MAAM,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAutBpD,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CA2FhD,CAAC;AAEF,sDAAsD;AACtD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,gBAAgB,GAAG,SAAS,CAM3E"}
|
|
@@ -18,7 +18,7 @@ const DOCS = 'https://bestax.io/docs';
|
|
|
18
18
|
const numeric = { numberToString: true };
|
|
19
19
|
/**
|
|
20
20
|
* `as` is universal in rbx — `forwardRefAs` puts it on every component — but
|
|
21
|
-
* bestax declares it on only
|
|
21
|
+
* bestax declares it on only some components, several of them constrained to a
|
|
22
22
|
* narrow literal union (`Footer` is `'footer' | 'div'`). So the default is a
|
|
23
23
|
* TODO, and the components that really do accept `as` opt in with `AS_OK` in
|
|
24
24
|
* their own prop map, which runs first and wins.
|
|
@@ -36,7 +36,7 @@ const AS_TODO = {
|
|
|
36
36
|
* `'footer' | 'div'`, `Control` is `'div'`), so an `as={SomeComponent}` still
|
|
37
37
|
* surfaces — as a type error the user can see, rather than a silent rewrite.
|
|
38
38
|
*/
|
|
39
|
-
const AS_OK = {};
|
|
39
|
+
export const AS_OK = {};
|
|
40
40
|
/**
|
|
41
41
|
* Helper props rbx mixes into every component via `HelpersProps`. Applied
|
|
42
42
|
* after each component's own prop map, to whatever attributes are left.
|
|
@@ -129,7 +129,7 @@ export const MAPPING = {
|
|
|
129
129
|
},
|
|
130
130
|
forwardRefAs: {
|
|
131
131
|
status: 'todo',
|
|
132
|
-
todo: '`forwardRefAs` is rbx-internal and has no counterpart. bestax forwards a ref from the form controls, plus `Avatar`, `Button`, `Carousel`, `CarouselItem`, `Dialog`, `Dropdown`, `EmptyState`, `Kbd`, `Kbd.Group`, `Link`, `LinkButton`, `Loader`, `Menu.Item`, `Modal`, `Navbar`, `Navbar.Burger`, `Navbar.Dropdown`, `Navbar.Item`, `Navbar.Link`, `SegmentedControl`, `SegmentedControl.Segment`, `Sidebar`, `Timeline`, `Timeline.Item`, `Toast` and `
|
|
132
|
+
todo: '`forwardRefAs` is rbx-internal and has no counterpart. bestax forwards a ref from the form controls, plus `Avatar`, `Button`, `Carousel`, `CarouselItem`, `Dialog`, `Dropdown`, `EmptyState`, `Kbd`, `Kbd.Group`, `Link`, `LinkButton`, `Loader`, `Menu.Item`, `Modal`, `Navbar`, `Navbar.Burger`, `Navbar.Dropdown`, `Navbar.Item`, `Navbar.Link`, `SegmentedControl`, `SegmentedControl.Segment`, `Sidebar`, `Timeline`, `Timeline.Item`, `Toast`, `ToggleButton`, `TreeView` and `TreeView.Item` — pass `ref` directly on those, though not on the `Field`, `Field.Label`, `Field.Body`, `Checkboxes` and `Radios` wrappers, which forward none. Mind the `Navbar.Dropdown` collision: your rbx `Navbar.Dropdown` is the menu and becomes `Navbar.DropdownMenu`, which forwards no ref, while bestax `Navbar.Dropdown` — the container your `<Navbar.Item dropdown>` becomes — does. The rest, `Box` included, forward no ref: React 18 drops it with a warning, React 19 leaves it wherever the rest props go, so put the ref on a wrapping element you control',
|
|
133
133
|
},
|
|
134
134
|
// ---- elements -----------------------------------------------------------
|
|
135
135
|
Block: { status: 'mapped', target: 'Block' },
|
|
@@ -182,6 +182,7 @@ export const MAPPING = {
|
|
|
182
182
|
status: 'mapped',
|
|
183
183
|
target: 'Container',
|
|
184
184
|
props: {
|
|
185
|
+
as: AS_OK,
|
|
185
186
|
fluid: {},
|
|
186
187
|
// bestax's ContainerBreakpoint is 'tablet' | 'desktop' | 'widescreen';
|
|
187
188
|
// rbx accepts all six, so the other three are type errors if passed on.
|
|
@@ -729,10 +730,14 @@ export const MAPPING = {
|
|
|
729
730
|
Panel: {
|
|
730
731
|
status: 'mapped',
|
|
731
732
|
target: 'Panel',
|
|
732
|
-
props: { color: {} },
|
|
733
|
+
props: { as: AS_OK, color: {} },
|
|
733
734
|
subs: {
|
|
734
735
|
Heading: { status: 'mapped', target: 'Panel.Heading' },
|
|
735
|
-
Block: {
|
|
736
|
+
Block: {
|
|
737
|
+
status: 'mapped',
|
|
738
|
+
target: 'Panel.Block',
|
|
739
|
+
props: { active: {}, as: AS_OK },
|
|
740
|
+
},
|
|
736
741
|
Icon: { status: 'partial', target: 'Panel.Icon', special: 'icon' },
|
|
737
742
|
Tab: {
|
|
738
743
|
status: 'mapped',
|
|
@@ -815,6 +820,7 @@ export const MAPPING = {
|
|
|
815
820
|
status: 'mapped',
|
|
816
821
|
target: 'Hero',
|
|
817
822
|
props: {
|
|
823
|
+
as: AS_OK,
|
|
818
824
|
color: {},
|
|
819
825
|
// rbx's five sizes are bestax's five, `fullheight-with-navbar`
|
|
820
826
|
// included, and both render the one `is-<size>` class.
|
|
@@ -825,8 +831,8 @@ export const MAPPING = {
|
|
|
825
831
|
},
|
|
826
832
|
subs: {
|
|
827
833
|
Body: { status: 'mapped', target: 'Hero.Body' },
|
|
828
|
-
Foot: { status: 'mapped', target: 'Hero.Foot' },
|
|
829
|
-
Head: { status: 'mapped', target: 'Hero.Head' },
|
|
834
|
+
Foot: { status: 'mapped', target: 'Hero.Foot', props: { as: AS_OK } },
|
|
835
|
+
Head: { status: 'mapped', target: 'Hero.Head', props: { as: AS_OK } },
|
|
830
836
|
},
|
|
831
837
|
},
|
|
832
838
|
Section: { status: 'mapped', target: 'Section', props: { size: {} } },
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mapping.d.ts","sourceRoot":"","sources":["../../../src/sources/react-bulma-components/mapping.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAQnE;;;;GAIG;AACH,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAgFtD,CAAC;AAEF,gEAAgE;AAChE,eAAO,MAAM,sBAAsB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAUhE,CAAC;AAIF,eAAO,MAAM,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,
|
|
1
|
+
{"version":3,"file":"mapping.d.ts","sourceRoot":"","sources":["../../../src/sources/react-bulma-components/mapping.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAQnE;;;;GAIG;AACH,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAgFtD,CAAC;AAEF,gEAAgE;AAChE,eAAO,MAAM,sBAAsB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAUhE,CAAC;AAIF,eAAO,MAAM,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAirBpD,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAsEhD,CAAC;AAEF,6EAA6E;AAC7E,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,gBAAgB,GAAG,SAAS,CAM3E"}
|
|
@@ -27,7 +27,7 @@ export const UNIVERSAL_PROPS = {
|
|
|
27
27
|
todo: 'this bestax component has no `as` prop; restructure the element instead',
|
|
28
28
|
},
|
|
29
29
|
domRef: {
|
|
30
|
-
todo: 'bestax-bulma has no `domRef`. The form controls, plus `Avatar`, `Button`, `Carousel`, `CarouselItem`, `Dialog`, `Dropdown`, `EmptyState`, `Kbd`, `Kbd.Group`, `Link`, `LinkButton`, `Loader`, `Menu.Item`, `Modal`, `Navbar`, `Navbar.Burger`, `Navbar.Dropdown`, `Navbar.Item`, `Navbar.Link`, `SegmentedControl`, `SegmentedControl.Segment`, `Sidebar`, `Timeline`, `Timeline.Item`, `Toast` and `
|
|
30
|
+
todo: 'bestax-bulma has no `domRef`. The form controls, plus `Avatar`, `Button`, `Carousel`, `CarouselItem`, `Dialog`, `Dropdown`, `EmptyState`, `Kbd`, `Kbd.Group`, `Link`, `LinkButton`, `Loader`, `Menu.Item`, `Modal`, `Navbar`, `Navbar.Burger`, `Navbar.Dropdown`, `Navbar.Item`, `Navbar.Link`, `SegmentedControl`, `SegmentedControl.Segment`, `Sidebar`, `Timeline`, `Timeline.Item`, `Toast`, `ToggleButton`, `TreeView` and `TreeView.Item` forward a ref — on those, rename `domRef` to `ref`, though not on the `Field`, `Field.Label`, `Field.Body`, `Checkboxes` and `Radios` wrappers, which forward none. Mind the `Navbar.Dropdown` collision: the `Navbar.Item` that wrapped your dropdown became bestax `Navbar.Dropdown` and forwards one, while your `Navbar.Dropdown` became `Navbar.DropdownMenu` and does not. The rest forward no ref: React 18 drops it with a warning, React 19 leaves it wherever the rest props go, so put the ref on a DOM child or wrap the component',
|
|
31
31
|
},
|
|
32
32
|
// ColorProps
|
|
33
33
|
backgroundColor: { rename: 'bgColor' },
|
|
@@ -252,6 +252,7 @@ export const MAPPING = {
|
|
|
252
252
|
status: 'mapped',
|
|
253
253
|
target: 'Container',
|
|
254
254
|
props: {
|
|
255
|
+
renderAs: { rename: 'as' },
|
|
255
256
|
max: { booleanToProp: { name: 'isMax' } },
|
|
256
257
|
breakpoint: {
|
|
257
258
|
// 'tablet' / 'desktop' stay as breakpoint="..." (same prop in bestax)
|
|
@@ -430,6 +431,7 @@ export const MAPPING = {
|
|
|
430
431
|
status: 'mapped',
|
|
431
432
|
target: 'Hero',
|
|
432
433
|
props: {
|
|
434
|
+
renderAs: { rename: 'as' },
|
|
433
435
|
color: {},
|
|
434
436
|
size: {
|
|
435
437
|
valueTodo: {
|
|
@@ -442,9 +444,17 @@ export const MAPPING = {
|
|
|
442
444
|
hasNavbar: { booleanToProp: { name: 'fullheightWithNavbar' } },
|
|
443
445
|
},
|
|
444
446
|
subs: {
|
|
445
|
-
Header: {
|
|
447
|
+
Header: {
|
|
448
|
+
status: 'mapped',
|
|
449
|
+
target: 'Hero.Head',
|
|
450
|
+
props: { renderAs: { rename: 'as' } },
|
|
451
|
+
},
|
|
446
452
|
Body: { status: 'mapped', target: 'Hero.Body' },
|
|
447
|
-
Footer: {
|
|
453
|
+
Footer: {
|
|
454
|
+
status: 'mapped',
|
|
455
|
+
target: 'Hero.Foot',
|
|
456
|
+
props: { renderAs: { rename: 'as' } },
|
|
457
|
+
},
|
|
448
458
|
},
|
|
449
459
|
},
|
|
450
460
|
Icon: {
|
|
@@ -688,7 +698,7 @@ export const MAPPING = {
|
|
|
688
698
|
Panel: {
|
|
689
699
|
status: 'mapped',
|
|
690
700
|
target: 'Panel',
|
|
691
|
-
props: { color: {} },
|
|
701
|
+
props: { renderAs: { rename: 'as' }, color: {} },
|
|
692
702
|
subs: {
|
|
693
703
|
Header: { status: 'mapped', target: 'Panel.Heading' },
|
|
694
704
|
Tabs: {
|
|
@@ -699,7 +709,11 @@ export const MAPPING = {
|
|
|
699
709
|
Tab: { status: 'mapped', special: 'panel-tab' },
|
|
700
710
|
},
|
|
701
711
|
},
|
|
702
|
-
Block: {
|
|
712
|
+
Block: {
|
|
713
|
+
status: 'mapped',
|
|
714
|
+
target: 'Panel.Block',
|
|
715
|
+
props: { ...activeKept, renderAs: { rename: 'as' } },
|
|
716
|
+
},
|
|
703
717
|
Icon: { status: 'partial', target: 'Panel.Icon', special: 'icon' },
|
|
704
718
|
},
|
|
705
719
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bestax-migrate",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.28.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": {
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"jscodeshift": "^17.3.0",
|
|
41
41
|
"parse5": "^7.3.0",
|
|
42
42
|
"property-information": "^7.2.0",
|
|
43
|
-
"@allxsmith/bestax-bulma": "^5.
|
|
43
|
+
"@allxsmith/bestax-bulma": "^5.38.0"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@jest/globals": "^30.0.0",
|
|
@@ -72,10 +72,11 @@
|
|
|
72
72
|
"validate:corpus:rbx": "pnpm run build && node scripts/validate-corpus-rbx.mjs",
|
|
73
73
|
"validate:corpus:bloomer": "pnpm run build && node scripts/validate-corpus-bloomer.mjs",
|
|
74
74
|
"validate:corpus:bulma-classes": "pnpm run build && node scripts/validate-corpus-bulma-classes.mjs",
|
|
75
|
+
"validate:corpus:templates": "pnpm run build && node scripts/validate-corpus-templates.mjs",
|
|
75
76
|
"format": "prettier --write \"src/**/*.{ts,tsx}\" \"e2e/**/*.ts\"",
|
|
76
77
|
"format:check": "prettier --check \"src/**/*.{ts,tsx}\" \"e2e/**/*.ts\"",
|
|
77
78
|
"clean": "rimraf dist",
|
|
78
79
|
"release": "npx semantic-release"
|
|
79
80
|
},
|
|
80
|
-
"readme": "# bestax-migrate\n\n[](https://www.npmjs.com/package/bestax-migrate)\n[](https://www.npmjs.com/package/bestax-migrate)\n[](https://opensource.org/licenses/MIT)\n[](https://socket.dev/npm/package/bestax-migrate/overview)\n[](https://scorecard.dev/viewer/?uri=github.com/allxsmith/bestax)\n[](https://www.bestpractices.dev/projects/14361)\n[](https://www.npmjs.com/package/bestax-migrate#provenance)\n[](https://github.com/allxsmith/bestax/blob/main/SECURITY.md)\n\nCodemods that migrate existing React apps to [`@allxsmith/bestax-bulma`](https://www.npmjs.com/package/@allxsmith/bestax-bulma) — the actively maintained React component library for **Bulma v1**.\n\nCurrently supported sources:\n\n| Source | Status |\n| -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |\n| [`react-bulma-components`](https://github.com/couds/react-bulma-components) (v4) | ✅ All 32 components mapped (a few patterns are flagged as TODOs) |\n| [`rbx`](https://github.com/dfee/rbx) (v2) | ✅ Full export surface mapped (a few patterns are flagged as TODOs) |\n| [`bloomer`](https://github.com/AlgusDark/bloomer) (0.6) | ✅ All 108 exports mapped (Font Awesome 4 icons and Bulma 0.4's `Nav` are flagged) |\n| `bulma-classes`: Bulma's classes on plain JSX, no library | ✅ Converts an element only when bestax renders the same markup; the rest is flagged |\n| `bulma-classes` on HTML: static pages and templates styled with Bulma | ✅ Writes a React component beside each page; its head and scripts are flagged |\n\nPass the source as the first argument: `bestax-migrate <source> <paths…>`.\n\n## Requirements\n\n**Node.js 22 or newer.** Node 18 and 20 are both past end-of-life. On an older runtime the CLI\nexits immediately with an explicit upgrade message.\n\nThis applies only to the Node version the codemod itself runs on. It places no requirement on\nthe app being migrated — the source is read as text and is never executed.\n\n## Usage\n\n```bash\n# Preview what would change (no writes)\npnpm dlx bestax-migrate react-bulma-components src/ --dry\n# …or, for an rbx or bloomer app\npnpm dlx bestax-migrate rbx src/ --dry\npnpm dlx bestax-migrate bloomer src/ --dry\n# …or, for an app that writes Bulma's classes on plain JSX\npnpm dlx bestax-migrate bulma-classes src/ --dry\n# …or, for static HTML pages styled with Bulma: name a page, or walk a folder with -e html\n# (-e html,htm takes .htm pages too)\npnpm dlx bestax-migrate bulma-classes src/pages/login/index.html --dry\npnpm dlx bestax-migrate bulma-classes -e html src/pages/ --dry\n\n# Apply the migration\npnpm dlx bestax-migrate react-bulma-components src/\n```\n\nnpm and yarn work too: `npx bestax-migrate …` / `yarn dlx bestax-migrate …`.\n\nThe codemod uses [jscodeshift](https://github.com/facebook/jscodeshift) to:\n\n- rewrite the source library's imports to `@allxsmith/bestax-bulma` (including destructuring and namespace imports)\n- rename components and compound sub-components (`Form.Textarea` → `TextArea`, `Card.Footer.Item` → `Card.FooterItem`, `Hero.Footer` → `Hero.Foot`, …) — and, for bloomer, its flat names onto bestax's compounds (`CardHeaderTitle` → `Card.Header.Title`)\n- convert props (`renderAs` → `as`, `loading` → `isLoading`, numeric spacing/text sizes → string unions, `textAlign=\"center\"` → `textAlign=\"centered\"`, …)\n- flatten responsive breakpoint objects (`mobile={{ size: 4 }}` → `sizeMobile={4}`) and bloomer's `isDisplay`/`isHidden` strings, arrays and objects (`isHidden=\"touch\"` → `visibilityTouch=\"hidden\"`)\n- for `bulma-classes`, turn raw Bulma markup into components and props (`<a className=\"button is-primary\">` → `<Button as=\"a\" color=\"primary\">`, `<p className=\"has-text-centered mt-4\">` → `<Paragraph textAlign=\"centered\" mt=\"4\">`), converting an element only when bestax renders exactly the same markup\n- for `bulma-classes` on an HTML page, write a React module beside it (`index.html` → `index.tsx`, or `.jsx` when the package isn't TypeScript) whose default export is the page's body as a component (`LoginPage`), converted as the same markup written as JSX would be. The page is never changed and an existing module is never overwritten; the head, the `<html>` and `<body>` attributes, scripts and inline handlers are left as `html-*` TODOs\n- restructure patterns bestax models differently (`Table.Container`, Navbar dropdowns, `Form.Help`, icon-font children → `<Icon name=…>`)\n- migrate stylesheets: CSS imports converge on `@allxsmith/bestax-bulma/bestax.css`, and SCSS files move from Bulma 0.9's `@import` + `$var !default` overrides to `@use 'bulma/sass' with (…)` plus `@use '@allxsmith/bestax-bulma/scss/extras'`\n- update `package.json`: remove the source library, add `@allxsmith/bestax-bulma`, raise a declared pre-1.0 `bulma` to `^1` (adding it only when your sources import `bulma/…` directly — otherwise it arrives transitively), and swap the dead `node-sass` for dart `sass` (no install is ever run). Migrating **rbx** is what frees an app from Bulma 0.7.5: rbx pinned it as a _direct_ dependency, so the app could not choose its own Bulma version at all. The four Bulma extensions rbx also pulled in (`bulma-badge`, `bulma-divider`, `bulma-pageloader`, `bulma-tooltip`) are **reported rather than removed** — bestax replaces all four, but a manifest entry is a deliberate declaration and your own Sass may import them. A **bloomer** app declared its own Bulma 0.6, which is what gets bumped. For **bulma-classes** there is no library to remove, and by default (`--css keep`) the app's Bulma version, Sass and stylesheet imports all stay: its own Bulma already styles every class a converted element renders. `--css bestax` or `--css bulma` moves it to Bulma v1 like the other sources\n\nAnything without a safe automatic conversion is left in place with a `// TODO(bestax-migrate): …` comment, and the run ends with a report of every TODO by file and line. TODOs are expected output, not errors — resolve them with the migration guide ([react-bulma-components](https://bestax.io/docs/guides/getting-started/migration/react-bulma-components), [rbx](https://bestax.io/docs/guides/getting-started/migration/rbx), [bloomer](https://bestax.io/docs/guides/getting-started/migration/bloomer), [Bulma classes](https://bestax.io/docs/guides/getting-started/migration/bulma-classes)), or let the [`bestax-migrate` Agent Skill](https://bestax.io/docs/skills/intro) walk them for you:\n\n```bash\nnpx skills add https://github.com/allxsmith/bestax --skill bestax-migrate\n```\n\n### Through the Codemod Registry\n\nbestax-migrate is also in the [Codemod Registry](https://app.codemod.com/registry) as `bestax-migrate`, for teams that run their migrations through Codemod. That package runs this CLI from npm at a pinned release, which can trail the newest one:\n\n```bash\n# Preview (no writes)\nnpx codemod bestax-migrate --workflow preview --param source=react-bulma-components\n# Apply\nnpx codemod bestax-migrate --param source=react-bulma-components\n```\n\nRun it from your app root. `--param paths=\"src lib\"` (default `src`) and `--param options=\"--css keep\"` pass through to the CLI. Codemod's own `--dry-run` skips the shell step this package runs, so use the `preview` workflow for a dry run.\n\n## Options\n\n| Flag | Description |\n| -------------------- | ---------------------------------------------------------------------------------------------- |\n| `--dry`, `-d` | Report what would change without writing files |\n| `--print`, `-p` | Print transformed sources to stdout |\n| `--extensions`, `-e` | File extensions to include in a folder (default `js,jsx,ts,tsx,scss,sass`; `html` for pages) |\n| `--css <mode>` | Stylesheet target: `bestax`, `bulma`, or `keep` (default `bestax`; `keep` for `bulma-classes`) |\n| `--no-deps` | Skip updating package.json dependencies |\n\n## After the codemod\n\n1. Run your package manager's install (the codemod rewrote `package.json` but never installs)\n2. Search for `TODO(bestax-migrate)` and resolve each comment\n3. Typecheck/build and review the rendered app\n\nFull walkthrough: [react-bulma-components migration guide](https://bestax.io/docs/guides/getting-started/migration/react-bulma-components) · [rbx migration guide](https://bestax.io/docs/guides/getting-started/migration/rbx) · [bloomer migration guide](https://bestax.io/docs/guides/getting-started/migration/bloomer) · [Bulma classes migration guide](https://bestax.io/docs/guides/getting-started/migration/bulma-classes).\n\n## Hardened by default\n\nA codemod rewrites your source in place, so how it is built and published matters:\n\n- **Signed provenance** — every release carries a sigstore attestation linking the tarball to the exact commit and CI run that built it. Check the **Provenance** section on the [npm page](https://www.npmjs.com/package/bestax-migrate#provenance), or run `npm audit signatures`.\n- **npm OIDC trusted publishing** — short-lived, per-run credentials; no long-lived `NPM_TOKEN` exists to be stolen. Release commits and tags are GPG-signed.\n- **Socket.dev scans every PR** for malware, install scripts, obfuscated code, and privilege escalation before it can reach `main`.\n- **The libraries this tool migrates away from are never installed here** — source fixtures are read as text only, so no unmaintained third-party package enters the dependency tree.\n- **Dependencies are a deliberate act** — install scripts are blocked unless individually allow-listed, freshly published versions are refused for 3 days, and CI installs only what the reviewed lockfile resolves.\n- **Every GitHub Action is pinned to a full commit SHA**, and CodeQL, Dependency Review, and Dependabot run continuously alongside a high-severity `pnpm audit` gate.\n- **Layered AI review before merge** — [CodeRabbit](https://coderabbit.ai) plus an independent adversarial Claude review (a different model from the one writing AI-authored changes), on top of required green CI, an approving review, and a human merge.\n\nFull detail: [`SECURITY.md`](https://github.com/allxsmith/bestax/blob/main/SECURITY.md) · [Security guide](https://bestax.io/docs/guides/security)\n\n## Telemetry\n\n`bestax-migrate` can send one **anonymous** usage event after a successful\nrun — only if you opt in when asked (once, on an interactive terminal). The\nevent is just the run's shape — source library, CSS mode, the dry/deps flags, a\ncapped changed-file count, and per-rule TODO counts — plus the CLI version,\nNode major version, and OS platform name. Never file paths, file contents,\ncode, names, IPs, or machine IDs, and no identifier exists that could link two\nevents together. Opt out any time with `--no-telemetry`,\n`BESTAX_TELEMETRY=0`, or `DO_NOT_TRACK=1`.\n\nFull disclosure of every field and control: [Telemetry guide](https://bestax.io/docs/guides/telemetry)\n\n## License\n\nSource code licensed MIT\n"
|
|
81
|
+
"readme": "# bestax-migrate\n\n[](https://www.npmjs.com/package/bestax-migrate)\n[](https://www.npmjs.com/package/bestax-migrate)\n[](https://opensource.org/licenses/MIT)\n[](https://socket.dev/npm/package/bestax-migrate/overview)\n[](https://scorecard.dev/viewer/?uri=github.com/allxsmith/bestax)\n[](https://www.bestpractices.dev/projects/14361)\n[](https://www.npmjs.com/package/bestax-migrate#provenance)\n[](https://github.com/allxsmith/bestax/blob/main/SECURITY.md)\n\nCodemods that migrate existing React apps to [`@allxsmith/bestax-bulma`](https://www.npmjs.com/package/@allxsmith/bestax-bulma) — the actively maintained React component library for **Bulma v1**.\n\nCurrently supported sources:\n\n| Source | Status |\n| -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |\n| [`react-bulma-components`](https://github.com/couds/react-bulma-components) (v4) | ✅ All 32 components mapped (a few patterns are flagged as TODOs) |\n| [`rbx`](https://github.com/dfee/rbx) (v2) | ✅ Full export surface mapped (a few patterns are flagged as TODOs) |\n| [`bloomer`](https://github.com/AlgusDark/bloomer) (0.6) | ✅ All 108 exports mapped (Font Awesome 4 icons and Bulma 0.4's `Nav` are flagged) |\n| `bulma-classes`: Bulma's classes on plain JSX, no library | ✅ Converts an element only when bestax renders the same markup; the rest is flagged |\n| `bulma-classes` on HTML: static pages and templates styled with Bulma | ✅ Writes a React component beside each page; its head and scripts are flagged |\n\nPass the source as the first argument: `bestax-migrate <source> <paths…>`.\n\n## Requirements\n\n**Node.js 22 or newer.** Node 18 and 20 are both past end-of-life. On an older runtime the CLI\nexits immediately with an explicit upgrade message.\n\nThis applies only to the Node version the codemod itself runs on. It places no requirement on\nthe app being migrated — the source is read as text and is never executed.\n\n## Usage\n\n```bash\n# Preview what would change (no writes)\npnpm dlx bestax-migrate react-bulma-components src/ --dry\n# …or, for an rbx or bloomer app\npnpm dlx bestax-migrate rbx src/ --dry\npnpm dlx bestax-migrate bloomer src/ --dry\n# …or, for an app that writes Bulma's classes on plain JSX\npnpm dlx bestax-migrate bulma-classes src/ --dry\n# …or, for static HTML pages styled with Bulma: name a page, or walk a folder with -e html\n# (-e html,htm takes .htm pages too)\npnpm dlx bestax-migrate bulma-classes src/pages/login/index.html --dry\npnpm dlx bestax-migrate bulma-classes -e html src/pages/ --dry\n\n# Apply the migration\npnpm dlx bestax-migrate react-bulma-components src/\n```\n\nnpm and yarn work too: `npx bestax-migrate …` / `yarn dlx bestax-migrate …`.\n\nThe codemod uses [jscodeshift](https://github.com/facebook/jscodeshift) to:\n\n- rewrite the source library's imports to `@allxsmith/bestax-bulma` (including destructuring and namespace imports)\n- rename components and compound sub-components (`Form.Textarea` → `TextArea`, `Card.Footer.Item` → `Card.FooterItem`, `Hero.Footer` → `Hero.Foot`, …) — and, for bloomer, its flat names onto bestax's compounds (`CardHeaderTitle` → `Card.Header.Title`)\n- convert props (`renderAs` → `as`, `loading` → `isLoading`, numeric spacing/text sizes → string unions, `textAlign=\"center\"` → `textAlign=\"centered\"`, …)\n- flatten responsive breakpoint objects (`mobile={{ size: 4 }}` → `sizeMobile={4}`) and bloomer's `isDisplay`/`isHidden` strings, arrays and objects (`isHidden=\"touch\"` → `visibilityTouch=\"hidden\"`)\n- for `bulma-classes`, turn raw Bulma markup into components and props (`<a className=\"button is-primary\">` → `<Button as=\"a\" color=\"primary\">`, `<p className=\"has-text-centered mt-4\">` → `<Paragraph textAlign=\"centered\" mt=\"4\">`), converting an element only when bestax renders exactly the same markup\n- for `bulma-classes` on an HTML page, write a React module beside it (`index.html` → `index.tsx`, or `.jsx` when the package isn't TypeScript) whose default export is the page's body as a component (`LoginPage`), converted as the same markup written as JSX would be. The page is never changed and an existing module is never overwritten; the head, the `<html>` and `<body>` attributes, the stylesheets and scripts it loads, and inline handlers are left as `html-*` TODOs, except that under `--css bestax` or `--css bulma` the module imports the stylesheet in place of the page's Bulma `<link>`\n- restructure patterns bestax models differently (`Table.Container`, Navbar dropdowns, `Form.Help`, icon-font children → `<Icon name=…>`)\n- migrate stylesheets: CSS imports converge on `@allxsmith/bestax-bulma/bestax.css`, and SCSS files move from Bulma 0.9's `@import` + `$var !default` overrides to `@use 'bulma/sass' with (…)` plus `@use '@allxsmith/bestax-bulma/scss/extras'`\n- update `package.json`: remove the source library, add `@allxsmith/bestax-bulma`, raise a declared pre-1.0 `bulma` to `^1` (adding it only when your sources import `bulma/…` directly — otherwise it arrives transitively), and swap the dead `node-sass` for dart `sass` (no install is ever run). Migrating **rbx** is what frees an app from Bulma 0.7.5: rbx pinned it as a _direct_ dependency, so the app could not choose its own Bulma version at all. The four Bulma extensions rbx also pulled in (`bulma-badge`, `bulma-divider`, `bulma-pageloader`, `bulma-tooltip`) are **reported rather than removed** — bestax replaces all four, but a manifest entry is a deliberate declaration and your own Sass may import them. A **bloomer** app declared its own Bulma 0.6, which is what gets bumped. For **bulma-classes** there is no library to remove, and by default (`--css keep`) the app's Bulma version, Sass and stylesheet imports all stay: its own Bulma already styles every class a converted element renders. `--css bestax` or `--css bulma` moves it to Bulma v1 like the other sources\n\nAnything without a safe automatic conversion is left in place with a `// TODO(bestax-migrate): …` comment, and the run ends with a report of every TODO by file and line. TODOs are expected output, not errors — resolve them with the migration guide ([react-bulma-components](https://bestax.io/docs/guides/getting-started/migration/react-bulma-components), [rbx](https://bestax.io/docs/guides/getting-started/migration/rbx), [bloomer](https://bestax.io/docs/guides/getting-started/migration/bloomer), [Bulma classes](https://bestax.io/docs/guides/getting-started/migration/bulma-classes)), or let the [`bestax-migrate` Agent Skill](https://bestax.io/docs/skills/intro) walk them for you:\n\n```bash\nnpx skills add https://github.com/allxsmith/bestax --skill bestax-migrate\n```\n\n### Through the Codemod Registry\n\nbestax-migrate is also in the [Codemod Registry](https://app.codemod.com/registry) as `bestax-migrate`, for teams that run their migrations through Codemod. That package runs this CLI from npm at a pinned release, which can trail the newest one:\n\n```bash\n# Preview (no writes)\nnpx codemod bestax-migrate --workflow preview --param source=react-bulma-components\n# Apply\nnpx codemod bestax-migrate --param source=react-bulma-components\n```\n\nRun it from your app root. `--param paths=\"src lib\"` (default `src`) and `--param options=\"--css keep\"` pass through to the CLI. Codemod's own `--dry-run` skips the shell step this package runs, so use the `preview` workflow for a dry run.\n\n## Options\n\n| Flag | Description |\n| -------------------- | ---------------------------------------------------------------------------------------------- |\n| `--dry`, `-d` | Report what would change without writing files |\n| `--print`, `-p` | Print transformed sources to stdout |\n| `--extensions`, `-e` | File extensions to include in a folder (default `js,jsx,ts,tsx,scss,sass`; `html` for pages) |\n| `--css <mode>` | Stylesheet target: `bestax`, `bulma`, or `keep` (default `bestax`; `keep` for `bulma-classes`) |\n| `--no-deps` | Skip updating package.json dependencies |\n\n## After the codemod\n\n1. Run your package manager's install (the codemod rewrote `package.json` but never installs)\n2. Search for `TODO(bestax-migrate)` and resolve each comment\n3. Typecheck/build and review the rendered app\n\nFull walkthrough: [react-bulma-components migration guide](https://bestax.io/docs/guides/getting-started/migration/react-bulma-components) · [rbx migration guide](https://bestax.io/docs/guides/getting-started/migration/rbx) · [bloomer migration guide](https://bestax.io/docs/guides/getting-started/migration/bloomer) · [Bulma classes migration guide](https://bestax.io/docs/guides/getting-started/migration/bulma-classes).\n\n## Hardened by default\n\nA codemod rewrites your source in place, so how it is built and published matters:\n\n- **Signed provenance** — every release carries a sigstore attestation linking the tarball to the exact commit and CI run that built it. Check the **Provenance** section on the [npm page](https://www.npmjs.com/package/bestax-migrate#provenance), or run `npm audit signatures`.\n- **npm OIDC trusted publishing** — short-lived, per-run credentials; no long-lived `NPM_TOKEN` exists to be stolen. Release commits and tags are GPG-signed.\n- **Socket.dev scans every PR** for malware, install scripts, obfuscated code, and privilege escalation before it can reach `main`.\n- **The libraries this tool migrates away from are never installed here** — source fixtures are read as text only, so no unmaintained third-party package enters the dependency tree.\n- **Dependencies are a deliberate act** — install scripts are blocked unless individually allow-listed, freshly published versions are refused for 3 days, and CI installs only what the reviewed lockfile resolves.\n- **Every GitHub Action is pinned to a full commit SHA**, and CodeQL, Dependency Review, and Dependabot run continuously alongside a high-severity `pnpm audit` gate.\n- **Layered AI review before merge** — [CodeRabbit](https://coderabbit.ai) plus an independent adversarial Claude review (a different model from the one writing AI-authored changes), on top of required green CI, an approving review, and a human merge.\n\nFull detail: [`SECURITY.md`](https://github.com/allxsmith/bestax/blob/main/SECURITY.md) · [Security guide](https://bestax.io/docs/guides/security)\n\n## Telemetry\n\n`bestax-migrate` can send one **anonymous** usage event after a successful\nrun — only if you opt in when asked (once, on an interactive terminal). The\nevent is just the run's shape — source library, CSS mode, the dry/deps flags, a\ncapped changed-file count, and per-rule TODO counts — plus the CLI version,\nNode major version, and OS platform name. Never file paths, file contents,\ncode, names, IPs, or machine IDs, and no identifier exists that could link two\nevents together. Opt out any time with `--no-telemetry`,\n`BESTAX_TELEMETRY=0`, or `DO_NOT_TRACK=1`.\n\nFull disclosure of every field and control: [Telemetry guide](https://bestax.io/docs/guides/telemetry)\n\n## License\n\nSource code licensed MIT\n"
|
|
81
82
|
}
|