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.
- package/README.md +59 -0
- package/dist/cli.d.ts +22 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +225 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/report.d.ts +24 -0
- package/dist/report.d.ts.map +1 -0
- package/dist/report.js +66 -0
- package/dist/runner.d.ts +16 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +69 -0
- package/dist/sources/react-bulma-components/deps.d.ts +9 -0
- package/dist/sources/react-bulma-components/deps.d.ts.map +1 -0
- package/dist/sources/react-bulma-components/deps.js +96 -0
- package/dist/sources/react-bulma-components/index.d.ts +3 -0
- package/dist/sources/react-bulma-components/index.d.ts.map +1 -0
- package/dist/sources/react-bulma-components/index.js +10 -0
- package/dist/sources/react-bulma-components/jsx-utils.d.ts +69 -0
- package/dist/sources/react-bulma-components/jsx-utils.d.ts.map +1 -0
- package/dist/sources/react-bulma-components/jsx-utils.js +130 -0
- package/dist/sources/react-bulma-components/mapping.d.ts +31 -0
- package/dist/sources/react-bulma-components/mapping.d.ts.map +1 -0
- package/dist/sources/react-bulma-components/mapping.js +852 -0
- package/dist/sources/react-bulma-components/props.d.ts +15 -0
- package/dist/sources/react-bulma-components/props.d.ts.map +1 -0
- package/dist/sources/react-bulma-components/props.js +124 -0
- package/dist/sources/react-bulma-components/responsive.d.ts +17 -0
- package/dist/sources/react-bulma-components/responsive.d.ts.map +1 -0
- package/dist/sources/react-bulma-components/responsive.js +113 -0
- package/dist/sources/react-bulma-components/specials.d.ts +23 -0
- package/dist/sources/react-bulma-components/specials.d.ts.map +1 -0
- package/dist/sources/react-bulma-components/specials.js +546 -0
- package/dist/sources/react-bulma-components/styles.d.ts +13 -0
- package/dist/sources/react-bulma-components/styles.d.ts.map +1 -0
- package/dist/sources/react-bulma-components/styles.js +199 -0
- package/dist/sources/react-bulma-components/transform.d.ts +19 -0
- package/dist/sources/react-bulma-components/transform.d.ts.map +1 -0
- package/dist/sources/react-bulma-components/transform.js +495 -0
- package/dist/sources/registry.d.ts +9 -0
- package/dist/sources/registry.d.ts.map +1 -0
- package/dist/sources/registry.js +14 -0
- package/dist/types.d.ts +122 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +9 -0
- package/package.json +74 -0
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The react-bulma-components (v4) → @allxsmith/bestax-bulma transform.
|
|
3
|
+
*
|
|
4
|
+
* Passes, in order:
|
|
5
|
+
* 1. resolve imports (named/namespace) and `const { X } = Form`-style
|
|
6
|
+
* destructuring into canonical RBC component paths
|
|
7
|
+
* 2. per-element: structural special → rename → responsive flattening →
|
|
8
|
+
* component prop map → universal modifier props
|
|
9
|
+
* 3. rewrite imports: drop RBC, add one merged bestax-bulma import; keep a
|
|
10
|
+
* trimmed RBC import (with a TODO) only for components that have no
|
|
11
|
+
* bestax equivalent (Element, Tile)
|
|
12
|
+
*
|
|
13
|
+
* Anything unsafe gets a `// TODO(bestax-migrate)` comment on the enclosing
|
|
14
|
+
* statement plus an entry in the run report.
|
|
15
|
+
*/
|
|
16
|
+
import { MAPPING, resolveMapping } from './mapping.js';
|
|
17
|
+
import { addTodo, attributesOf, jsxNameParts, renameElement, } from './jsx-utils.js';
|
|
18
|
+
import { applyPropAction, applyUniversalProps } from './props.js';
|
|
19
|
+
import { flattenResponsiveProps } from './responsive.js';
|
|
20
|
+
import { RESPONSIVE_KINDS, runSpecial } from './specials.js';
|
|
21
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
22
|
+
const RBC = 'react-bulma-components';
|
|
23
|
+
const BESTAX = '@allxsmith/bestax-bulma';
|
|
24
|
+
const BULMA_CSS = 'bulma/css/bulma.min.css';
|
|
25
|
+
const BESTAX_CSS = '@allxsmith/bestax-bulma/bestax.css';
|
|
26
|
+
const EXTRAS_CSS = '@allxsmith/bestax-bulma/extras.css';
|
|
27
|
+
const BULMA_CSS_SPECIFIERS = new Set([
|
|
28
|
+
'bulma/css/bulma.css',
|
|
29
|
+
'bulma/css/bulma.min.css',
|
|
30
|
+
]);
|
|
31
|
+
const BESTAX_CSS_SPECIFIERS = new Set([
|
|
32
|
+
BESTAX_CSS,
|
|
33
|
+
'@allxsmith/bestax-bulma/bestax.min.css',
|
|
34
|
+
'@allxsmith/bestax-bulma/dist/bestax.css',
|
|
35
|
+
'@allxsmith/bestax-bulma/dist/bestax.min.css',
|
|
36
|
+
]);
|
|
37
|
+
const EXTRAS_CSS_SPECIFIERS = new Set([
|
|
38
|
+
EXTRAS_CSS,
|
|
39
|
+
'@allxsmith/bestax-bulma/dist/extras.css',
|
|
40
|
+
]);
|
|
41
|
+
export default function transform(fileInfo, api, options = {}) {
|
|
42
|
+
const j = api.jscodeshift;
|
|
43
|
+
const root = j(fileInfo.source);
|
|
44
|
+
const ctx = {
|
|
45
|
+
j,
|
|
46
|
+
file: fileInfo.path,
|
|
47
|
+
collector: options.collector,
|
|
48
|
+
retained: new Set(),
|
|
49
|
+
needed: new Map(),
|
|
50
|
+
reserve: root => root, // replaced below once local bindings are known
|
|
51
|
+
overrides: new WeakMap(),
|
|
52
|
+
dirty: false,
|
|
53
|
+
};
|
|
54
|
+
// ---- 1. Collect RBC imports -------------------------------------------
|
|
55
|
+
/** local identifier → imported RBC name ('*' for namespace imports). */
|
|
56
|
+
const imports = new Map();
|
|
57
|
+
/** local identifier → canonical RBC path (from destructuring). */
|
|
58
|
+
const aliases = new Map();
|
|
59
|
+
const rbcImportPaths = [];
|
|
60
|
+
const nameOf = (node) => typeof node === 'string' ? node : String(node?.name ?? '');
|
|
61
|
+
root.find(j.ImportDeclaration).forEach(path => {
|
|
62
|
+
const source = String(path.node.source.value);
|
|
63
|
+
if (source === RBC) {
|
|
64
|
+
rbcImportPaths.push(path);
|
|
65
|
+
for (const spec of path.node.specifiers ?? []) {
|
|
66
|
+
if (spec.type === 'ImportSpecifier' && spec.local) {
|
|
67
|
+
imports.set(nameOf(spec.local), nameOf(spec.imported));
|
|
68
|
+
}
|
|
69
|
+
else if (spec.type === 'ImportNamespaceSpecifier' && spec.local) {
|
|
70
|
+
imports.set(nameOf(spec.local), '*');
|
|
71
|
+
}
|
|
72
|
+
else if (spec.type === 'ImportDefaultSpecifier') {
|
|
73
|
+
// RBC has no default export; leave it and flag it.
|
|
74
|
+
addTodo(ctx, path, 'imports', 'react-bulma-components has no default export; convert to named imports first');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
else if (source.startsWith(`${RBC}/`) && source.endsWith('.css')) {
|
|
79
|
+
// v3-era bundled CSS import — handled by the CSS pass below.
|
|
80
|
+
}
|
|
81
|
+
else if (source.startsWith(`${RBC}/`)) {
|
|
82
|
+
addTodo(ctx, path, 'imports', 'deep react-bulma-components import paths are a v3 pattern; move to v4 named root imports first');
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
// ---- 1a. Stylesheet imports (mode-driven) -----------------------------
|
|
86
|
+
// `bestax` (default): everything converges on the recommended combined
|
|
87
|
+
// bundle. `bulma`: plain Bulma v1 CSS plus the separate extras file.
|
|
88
|
+
// `keep`: only the dead RBC v3 CSS import is touched.
|
|
89
|
+
const cssMode = options.cssMode ?? 'bestax';
|
|
90
|
+
let sawBestaxCss = root
|
|
91
|
+
.find(j.ImportDeclaration)
|
|
92
|
+
.paths()
|
|
93
|
+
.some(p => BESTAX_CSS_SPECIFIERS.has(String(p.node.source.value)));
|
|
94
|
+
root.find(j.ImportDeclaration).forEach(path => {
|
|
95
|
+
const source = String(path.node.source.value);
|
|
96
|
+
const isRbcCss = source.startsWith(`${RBC}/`) && source.endsWith('.css');
|
|
97
|
+
const isBulmaCss = BULMA_CSS_SPECIFIERS.has(source);
|
|
98
|
+
const isExtrasCss = EXTRAS_CSS_SPECIFIERS.has(source);
|
|
99
|
+
if (!isRbcCss && !isBulmaCss && !isExtrasCss)
|
|
100
|
+
return;
|
|
101
|
+
if (cssMode === 'bestax') {
|
|
102
|
+
if (isRbcCss || isBulmaCss) {
|
|
103
|
+
if (sawBestaxCss) {
|
|
104
|
+
path.prune(); // bestax.css already imported elsewhere in this file
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
path.node.source = j.stringLiteral(BESTAX_CSS);
|
|
108
|
+
sawBestaxCss = true;
|
|
109
|
+
}
|
|
110
|
+
ctx.dirty = true;
|
|
111
|
+
}
|
|
112
|
+
else if (isExtrasCss && sawBestaxCss) {
|
|
113
|
+
// bestax.css already contains the extras.
|
|
114
|
+
path.prune();
|
|
115
|
+
ctx.dirty = true;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
else if (cssMode === 'bulma') {
|
|
119
|
+
if (isRbcCss) {
|
|
120
|
+
path.node.source = j.stringLiteral(BULMA_CSS);
|
|
121
|
+
ctx.dirty = true;
|
|
122
|
+
}
|
|
123
|
+
if ((isRbcCss || isBulmaCss) && !sawBestaxCss) {
|
|
124
|
+
const hasExtras = root
|
|
125
|
+
.find(j.ImportDeclaration)
|
|
126
|
+
.paths()
|
|
127
|
+
.some(p => EXTRAS_CSS_SPECIFIERS.has(String(p.node.source.value)));
|
|
128
|
+
if (!hasExtras) {
|
|
129
|
+
// Themed Radio/Checkbox need the bestax extras next to plain Bulma.
|
|
130
|
+
path.insertAfter(j.importDeclaration([], j.stringLiteral(EXTRAS_CSS)));
|
|
131
|
+
ctx.dirty = true;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else if (isRbcCss) {
|
|
136
|
+
// keep: minimal fix — the v3 bundled CSS no longer exists at all.
|
|
137
|
+
path.node.source = j.stringLiteral(BULMA_CSS);
|
|
138
|
+
addTodo(ctx, path, 'css', `replaced the react-bulma-components CSS import with '${BULMA_CSS}'; install bulma@^1 (see https://bestax.io/docs/guides/getting-started/installation)`);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
if (imports.size === 0 && !ctx.dirty) {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
// ---- 1b. Resolve `const { Input, Field: F } = Form` destructuring -----
|
|
145
|
+
root.find(j.VariableDeclarator).forEach(path => {
|
|
146
|
+
const node = path.node;
|
|
147
|
+
if (node.id?.type !== 'ObjectPattern' || node.init?.type !== 'Identifier')
|
|
148
|
+
return;
|
|
149
|
+
const base = node.init.name;
|
|
150
|
+
const imported = imports.get(base) ?? aliases.get(base)?.join('.');
|
|
151
|
+
if (!imported || imported === '*')
|
|
152
|
+
return;
|
|
153
|
+
const basePath = imports.has(base)
|
|
154
|
+
? [imports.get(base)]
|
|
155
|
+
: aliases.get(base);
|
|
156
|
+
let allResolved = true;
|
|
157
|
+
for (const prop of node.id.properties) {
|
|
158
|
+
if ((prop.type === 'ObjectProperty' || prop.type === 'Property') &&
|
|
159
|
+
prop.key?.type === 'Identifier' &&
|
|
160
|
+
prop.value?.type === 'Identifier') {
|
|
161
|
+
aliases.set(prop.value.name, [...basePath, prop.key.name]);
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
allResolved = false;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (allResolved) {
|
|
168
|
+
const declaration = path.parent;
|
|
169
|
+
if (declaration?.node?.type === 'VariableDeclaration' &&
|
|
170
|
+
declaration.node.declarations.length === 1) {
|
|
171
|
+
declaration.prune();
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
path.prune();
|
|
175
|
+
}
|
|
176
|
+
ctx.dirty = true;
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
addTodo(ctx, path, 'imports', `could not resolve every name destructured from \`${base}\`; migrate the leftovers by hand`);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
/** Resolve a JSX name into a canonical RBC component path, or null. */
|
|
183
|
+
function resolveJsxPath(name) {
|
|
184
|
+
const parts = jsxNameParts(name);
|
|
185
|
+
if (!parts)
|
|
186
|
+
return null;
|
|
187
|
+
const [head, ...rest] = parts;
|
|
188
|
+
if (aliases.has(head))
|
|
189
|
+
return [...aliases.get(head), ...rest];
|
|
190
|
+
const imported = imports.get(head);
|
|
191
|
+
if (imported === undefined)
|
|
192
|
+
return null;
|
|
193
|
+
if (imported === '*')
|
|
194
|
+
return rest.length > 0 ? rest : null;
|
|
195
|
+
return [imported, ...rest];
|
|
196
|
+
}
|
|
197
|
+
ctx.resolve = resolveJsxPath;
|
|
198
|
+
// ---- 1c. Collect local bindings so new imports never collide -----------
|
|
199
|
+
// A file may already bind names like `Field` or `Control` (components,
|
|
200
|
+
// variables, function params); importing the bestax component under the
|
|
201
|
+
// same name would redeclare it, so those imports get a Bulma* alias.
|
|
202
|
+
const bound = new Set();
|
|
203
|
+
const collectPattern = (node) => {
|
|
204
|
+
if (!node)
|
|
205
|
+
return;
|
|
206
|
+
switch (node.type) {
|
|
207
|
+
case 'Identifier':
|
|
208
|
+
bound.add(node.name);
|
|
209
|
+
break;
|
|
210
|
+
case 'ObjectPattern':
|
|
211
|
+
for (const prop of node.properties ?? []) {
|
|
212
|
+
collectPattern(prop.value ?? prop.argument);
|
|
213
|
+
}
|
|
214
|
+
break;
|
|
215
|
+
case 'ArrayPattern':
|
|
216
|
+
for (const el of node.elements ?? [])
|
|
217
|
+
collectPattern(el);
|
|
218
|
+
break;
|
|
219
|
+
case 'RestElement':
|
|
220
|
+
collectPattern(node.argument);
|
|
221
|
+
break;
|
|
222
|
+
case 'AssignmentPattern':
|
|
223
|
+
collectPattern(node.left);
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
root.find(j.VariableDeclarator).forEach(path => collectPattern(path.node.id));
|
|
228
|
+
root.find(j.FunctionDeclaration).forEach(path => {
|
|
229
|
+
if (path.node.id)
|
|
230
|
+
bound.add(nameOf(path.node.id));
|
|
231
|
+
for (const param of path.node.params ?? [])
|
|
232
|
+
collectPattern(param);
|
|
233
|
+
});
|
|
234
|
+
root
|
|
235
|
+
.find(j.ClassDeclaration)
|
|
236
|
+
.forEach(path => path.node.id && bound.add(nameOf(path.node.id)));
|
|
237
|
+
root.find(j.FunctionExpression).forEach(path => {
|
|
238
|
+
for (const param of path.node.params ?? [])
|
|
239
|
+
collectPattern(param);
|
|
240
|
+
});
|
|
241
|
+
root.find(j.ArrowFunctionExpression).forEach(path => {
|
|
242
|
+
for (const param of path.node.params ?? [])
|
|
243
|
+
collectPattern(param);
|
|
244
|
+
});
|
|
245
|
+
root.find(j.ImportDeclaration).forEach(path => {
|
|
246
|
+
if (String(path.node.source.value) === RBC)
|
|
247
|
+
return; // pruned later
|
|
248
|
+
for (const spec of path.node.specifiers ?? []) {
|
|
249
|
+
if (spec.local)
|
|
250
|
+
bound.add(nameOf(spec.local));
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
ctx.reserve = (importedName) => {
|
|
254
|
+
const existing = ctx.needed.get(importedName);
|
|
255
|
+
if (existing)
|
|
256
|
+
return existing;
|
|
257
|
+
let local = importedName;
|
|
258
|
+
if (bound.has(local)) {
|
|
259
|
+
local = `Bulma${importedName}`;
|
|
260
|
+
let suffix = 2;
|
|
261
|
+
const locals = new Set(ctx.needed.values());
|
|
262
|
+
while (bound.has(local) || locals.has(local)) {
|
|
263
|
+
local = `Bulma${importedName}${suffix}`;
|
|
264
|
+
suffix += 1;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
ctx.needed.set(importedName, local);
|
|
268
|
+
return local;
|
|
269
|
+
};
|
|
270
|
+
// Merge with an existing bestax import: reuse its locals verbatim.
|
|
271
|
+
const existingBestax = root
|
|
272
|
+
.find(j.ImportDeclaration, { source: { value: BESTAX } })
|
|
273
|
+
.paths()[0];
|
|
274
|
+
const preExistingImports = new Set();
|
|
275
|
+
if (existingBestax) {
|
|
276
|
+
for (const spec of existingBestax.node.specifiers ?? []) {
|
|
277
|
+
if (spec.type === 'ImportSpecifier' && spec.local) {
|
|
278
|
+
ctx.needed.set(nameOf(spec.imported), nameOf(spec.local));
|
|
279
|
+
preExistingImports.add(nameOf(spec.imported));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
// ---- 2. Transform JSX elements ----------------------------------------
|
|
284
|
+
root.find(j.JSXElement).forEach(path => {
|
|
285
|
+
const element = path.node;
|
|
286
|
+
const rbcPath = resolveJsxPath(element.openingElement.name);
|
|
287
|
+
if (!rbcPath)
|
|
288
|
+
return;
|
|
289
|
+
const mapping = resolveMapping(rbcPath);
|
|
290
|
+
const dotted = rbcPath.join('.');
|
|
291
|
+
if (!mapping) {
|
|
292
|
+
ctx.retained.add(rbcPath[0]);
|
|
293
|
+
addTodo(ctx, path, 'unknown-component', `\`${dotted}\` is not a known react-bulma-components v4 export; migrate it by hand`);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (mapping.status === 'todo') {
|
|
297
|
+
ctx.retained.add(rbcPath[0]);
|
|
298
|
+
addTodo(ctx, path, `component:${dotted}`, `\`${dotted}\` — ${mapping.todo ?? 'migrate by hand'}`);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
// Structural special first — it may pick the target or replace the node.
|
|
302
|
+
let target = mapping.target;
|
|
303
|
+
const handled = new Set();
|
|
304
|
+
if (mapping.special) {
|
|
305
|
+
const result = runSpecial(mapping.special, ctx, path, element);
|
|
306
|
+
if (result.replaced)
|
|
307
|
+
return;
|
|
308
|
+
if (result.target)
|
|
309
|
+
target = result.target;
|
|
310
|
+
for (const prop of result.handledProps ?? [])
|
|
311
|
+
handled.add(prop);
|
|
312
|
+
}
|
|
313
|
+
// A parent's structural handler may have picked this element's target.
|
|
314
|
+
const override = ctx.overrides.get(element);
|
|
315
|
+
if (override)
|
|
316
|
+
target = override;
|
|
317
|
+
if (!target)
|
|
318
|
+
return;
|
|
319
|
+
// Rename to the bestax name, aliasing the import root on collision.
|
|
320
|
+
const [targetRoot, ...targetRest] = target.split('.');
|
|
321
|
+
const localTarget = [ctx.reserve(targetRoot), ...targetRest].join('.');
|
|
322
|
+
const currentParts = jsxNameParts(element.openingElement.name);
|
|
323
|
+
if (!currentParts || currentParts.join('.') !== localTarget) {
|
|
324
|
+
renameElement(j, element, localTarget);
|
|
325
|
+
ctx.dirty = true;
|
|
326
|
+
}
|
|
327
|
+
// Responsive breakpoint objects (with Columns/Column extensions).
|
|
328
|
+
const kind = mapping.special
|
|
329
|
+
? (RESPONSIVE_KINDS[mapping.special] ?? 'generic')
|
|
330
|
+
: 'generic';
|
|
331
|
+
flattenResponsiveProps(ctx, path, element, kind);
|
|
332
|
+
// Component-specific prop actions, then the universal modifier pass.
|
|
333
|
+
if (mapping.props) {
|
|
334
|
+
for (const attr of [...attributesOf(element)]) {
|
|
335
|
+
const name = attr.name.name;
|
|
336
|
+
const action = mapping.props[name];
|
|
337
|
+
if (!action || handled.has(name))
|
|
338
|
+
continue;
|
|
339
|
+
handled.add(name);
|
|
340
|
+
applyPropAction(ctx, path, element, attr, action);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
applyUniversalProps(ctx, path, element, handled);
|
|
344
|
+
});
|
|
345
|
+
// ---- 2b. Value references to RBC components ---------------------------
|
|
346
|
+
// Components can be referenced as plain values too (`renderAs={Block}`,
|
|
347
|
+
// passed to helpers, …). JSX usages were renamed above; map the leftover
|
|
348
|
+
// identifier references so the pruned RBC import doesn't strand them.
|
|
349
|
+
root.find(j.Identifier).forEach(path => {
|
|
350
|
+
// find(Identifier) also matches JSXIdentifier (a subtype) — JSX names
|
|
351
|
+
// were already handled by the element walker above.
|
|
352
|
+
if (path.node.type !== 'Identifier')
|
|
353
|
+
return;
|
|
354
|
+
const name = path.node.name;
|
|
355
|
+
const imported = imports.get(name);
|
|
356
|
+
if (imported === undefined || imported === '*')
|
|
357
|
+
return;
|
|
358
|
+
const parentNode = path.parent?.node;
|
|
359
|
+
const parentType = parentNode?.type;
|
|
360
|
+
if (parentType === 'ImportSpecifier' ||
|
|
361
|
+
parentType === 'ImportDefaultSpecifier' ||
|
|
362
|
+
parentType === 'ImportNamespaceSpecifier' ||
|
|
363
|
+
parentType === 'JSXOpeningElement' ||
|
|
364
|
+
parentType === 'JSXClosingElement' ||
|
|
365
|
+
parentType === 'JSXMemberExpression') {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
// Non-reference positions: member property names and object keys.
|
|
369
|
+
if (parentType === 'MemberExpression' &&
|
|
370
|
+
parentNode.property === path.node &&
|
|
371
|
+
!parentNode.computed) {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
// Member-expression value references (`Icon.Text`, `Columns.Column` used
|
|
375
|
+
// as expressions): resolve the full chain; when the bestax target is the
|
|
376
|
+
// same dotted chain the runtime compound still exists — only flag (or
|
|
377
|
+
// rewrite, for flat single-name targets) when it differs.
|
|
378
|
+
if (parentType === 'MemberExpression' &&
|
|
379
|
+
parentNode.object === path.node &&
|
|
380
|
+
!parentNode.computed) {
|
|
381
|
+
const memberPath = [imported, nameOf(parentNode.property)];
|
|
382
|
+
const memberMapping = resolveMapping(memberPath);
|
|
383
|
+
const dotted = memberPath.join('.');
|
|
384
|
+
if (memberMapping &&
|
|
385
|
+
memberMapping.status !== 'todo' &&
|
|
386
|
+
memberMapping.target &&
|
|
387
|
+
!memberMapping.special) {
|
|
388
|
+
if (memberMapping.target === dotted) {
|
|
389
|
+
// Same compound exists on the bestax component at runtime.
|
|
390
|
+
const rootMapping = MAPPING[imported];
|
|
391
|
+
if (rootMapping?.target) {
|
|
392
|
+
const local = ctx.reserve(rootMapping.target);
|
|
393
|
+
if (local !== name)
|
|
394
|
+
path.node.name = local;
|
|
395
|
+
ctx.dirty = true;
|
|
396
|
+
}
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
if (!memberMapping.target.includes('.')) {
|
|
400
|
+
// Flat target (Icon.Text → IconText): swap the member expression.
|
|
401
|
+
const local = ctx.reserve(memberMapping.target);
|
|
402
|
+
path.parent.replace(j.identifier(local));
|
|
403
|
+
ctx.dirty = true;
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
addTodo(ctx, path.parent, 'value-reference', `\`${dotted}\` is referenced as a value; migrate this usage by hand`);
|
|
408
|
+
ctx.retained.add(imported);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
if ((parentType === 'ObjectProperty' || parentType === 'Property') &&
|
|
412
|
+
parentNode.key === path.node &&
|
|
413
|
+
!parentNode.shorthand) {
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
const mapping = MAPPING[imported];
|
|
417
|
+
if (mapping &&
|
|
418
|
+
mapping.status !== 'todo' &&
|
|
419
|
+
mapping.target &&
|
|
420
|
+
!mapping.target.includes('.')) {
|
|
421
|
+
const local = ctx.reserve(mapping.target);
|
|
422
|
+
if (local !== name)
|
|
423
|
+
path.node.name = local;
|
|
424
|
+
ctx.dirty = true;
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
ctx.retained.add(imported);
|
|
428
|
+
addTodo(ctx, path, 'value-reference', `\`${name}\` is referenced as a value; migrate this usage by hand`);
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
// ---- 3. Rewrite imports -----------------------------------------------
|
|
432
|
+
if (imports.size > 0) {
|
|
433
|
+
const retainedNames = [...ctx.retained].sort((a, b) => a.localeCompare(b));
|
|
434
|
+
const freshNames = [...ctx.needed.entries()]
|
|
435
|
+
.filter(([imported]) => !preExistingImports.has(imported))
|
|
436
|
+
.sort((a, b) => a[0].localeCompare(b[0]));
|
|
437
|
+
const bestaxImport = freshNames.length > 0
|
|
438
|
+
? j.importDeclaration(freshNames.map(([imported, local]) => j.importSpecifier(j.identifier(imported), j.identifier(local))), j.stringLiteral(BESTAX))
|
|
439
|
+
: null;
|
|
440
|
+
let inserted = false;
|
|
441
|
+
// A retained RBC specifier must never collide with a bestax import local
|
|
442
|
+
// (possible when one component is both JSX-migrated and value-retained).
|
|
443
|
+
const bestaxLocals = new Set(ctx.needed.values());
|
|
444
|
+
for (const path of rbcImportPaths) {
|
|
445
|
+
const node = path.node;
|
|
446
|
+
const keepSpecifiers = (node.specifiers ?? []).filter((spec) => spec.type === 'ImportSpecifier' &&
|
|
447
|
+
ctx.retained.has(nameOf(spec.imported)) &&
|
|
448
|
+
!bestaxLocals.has(nameOf(spec.local)));
|
|
449
|
+
if (!inserted) {
|
|
450
|
+
if (existingBestax && bestaxImport) {
|
|
451
|
+
existingBestax.node.specifiers = [
|
|
452
|
+
...(existingBestax.node.specifiers ?? []),
|
|
453
|
+
...bestaxImport.specifiers,
|
|
454
|
+
];
|
|
455
|
+
}
|
|
456
|
+
else if (bestaxImport) {
|
|
457
|
+
path.insertBefore(bestaxImport);
|
|
458
|
+
}
|
|
459
|
+
inserted = true;
|
|
460
|
+
}
|
|
461
|
+
if (keepSpecifiers.length > 0) {
|
|
462
|
+
node.specifiers = keepSpecifiers;
|
|
463
|
+
node.comments = node.comments ?? [];
|
|
464
|
+
const text = ` TODO(bestax-migrate): ${retainedNames.join(', ')} ${retainedNames.length === 1 ? 'has' : 'have'} no bestax-bulma equivalent yet — migrate and remove this import`;
|
|
465
|
+
if (!node.comments.some((c) => c.value === text)) {
|
|
466
|
+
node.comments.push(j.commentLine(text, true, false));
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
path.prune();
|
|
471
|
+
}
|
|
472
|
+
ctx.dirty = true;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
if (!ctx.dirty)
|
|
476
|
+
return undefined;
|
|
477
|
+
// Double quotes match the dominant JSX-attribute convention; users run
|
|
478
|
+
// their own formatter afterwards anyway. Tab-indented sources keep tabs so
|
|
479
|
+
// reprinted nodes don't drift from the untouched lines around them.
|
|
480
|
+
return root.toSource({
|
|
481
|
+
quote: 'double',
|
|
482
|
+
useTabs: prefersTabs(fileInfo.source),
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
function prefersTabs(source) {
|
|
486
|
+
let tabs = 0;
|
|
487
|
+
let spaces = 0;
|
|
488
|
+
for (const line of source.split('\n')) {
|
|
489
|
+
if (line.startsWith('\t'))
|
|
490
|
+
tabs += 1;
|
|
491
|
+
else if (line.startsWith(' '))
|
|
492
|
+
spaces += 1;
|
|
493
|
+
}
|
|
494
|
+
return tabs > spaces;
|
|
495
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The multi-source seam: every library bestax-migrate can migrate from
|
|
3
|
+
* registers here, and the CLI resolves its first argument against this map.
|
|
4
|
+
*/
|
|
5
|
+
import type { MigrationSource } from '../types.js';
|
|
6
|
+
export declare const SOURCES: Record<string, MigrationSource>;
|
|
7
|
+
export declare function getSource(name: string): MigrationSource | undefined;
|
|
8
|
+
export declare function sourceNames(): string[];
|
|
9
|
+
//# sourceMappingURL=registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/sources/registry.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAGnD,eAAO,MAAM,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAEnD,CAAC;AAEF,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAEnE;AAED,wBAAgB,WAAW,IAAI,MAAM,EAAE,CAEtC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The multi-source seam: every library bestax-migrate can migrate from
|
|
3
|
+
* registers here, and the CLI resolves its first argument against this map.
|
|
4
|
+
*/
|
|
5
|
+
import { reactBulmaComponents } from './react-bulma-components/index.js';
|
|
6
|
+
export const SOURCES = {
|
|
7
|
+
[reactBulmaComponents.name]: reactBulmaComponents,
|
|
8
|
+
};
|
|
9
|
+
export function getSource(name) {
|
|
10
|
+
return SOURCES[name];
|
|
11
|
+
}
|
|
12
|
+
export function sourceNames() {
|
|
13
|
+
return Object.keys(SOURCES);
|
|
14
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for the bestax-migrate codemod platform.
|
|
3
|
+
*
|
|
4
|
+
* Each source library (e.g. react-bulma-components) contributes a declarative
|
|
5
|
+
* mapping table plus a jscodeshift transform that consumes it. Everything the
|
|
6
|
+
* transform cannot convert safely is annotated with a `TODO(bestax-migrate)`
|
|
7
|
+
* comment and collected into the run report.
|
|
8
|
+
*/
|
|
9
|
+
import type { API, FileInfo } from 'jscodeshift';
|
|
10
|
+
/**
|
|
11
|
+
* How completely a source-library component converts to bestax-bulma.
|
|
12
|
+
*
|
|
13
|
+
* - `mapped` — converts fully; no follow-up expected.
|
|
14
|
+
* - `partial` — converts, but some props or structures are left as TODOs.
|
|
15
|
+
* - `todo` — no bestax equivalent; usage is annotated and left in place.
|
|
16
|
+
*/
|
|
17
|
+
export type MappingStatus = 'mapped' | 'partial' | 'todo';
|
|
18
|
+
/** A declarative action to apply to a single JSX prop. */
|
|
19
|
+
export interface PropAction {
|
|
20
|
+
/** Rename the attribute, keeping its value. */
|
|
21
|
+
rename?: string;
|
|
22
|
+
/** Remap literal string values (applied after `numberToString`). */
|
|
23
|
+
valueMap?: Record<string, string>;
|
|
24
|
+
/**
|
|
25
|
+
* Changes `valueMap` semantics: matched literal values become bare boolean
|
|
26
|
+
* prop(s) named by the mapped value (space-separated for several), and the
|
|
27
|
+
* original attribute is removed. Unmatched literal values are left as-is
|
|
28
|
+
* unless listed in `valueTodo`; non-literal values get a TODO.
|
|
29
|
+
*/
|
|
30
|
+
valueToProp?: boolean;
|
|
31
|
+
/** Convert a numeric literal value to its string form ({4} → "4"). */
|
|
32
|
+
numberToString?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Replace a boolean (valueless) attribute with `name="value"`, or with a
|
|
35
|
+
* bare boolean `name` when `value` is omitted.
|
|
36
|
+
*/
|
|
37
|
+
booleanToProp?: {
|
|
38
|
+
name: string;
|
|
39
|
+
value?: string;
|
|
40
|
+
};
|
|
41
|
+
/** Remove the attribute entirely. */
|
|
42
|
+
drop?: boolean;
|
|
43
|
+
/** Leave the attribute as-is and attach a TODO with this hint. */
|
|
44
|
+
todo?: string;
|
|
45
|
+
/**
|
|
46
|
+
* TODO hints keyed by specific literal values; other values convert
|
|
47
|
+
* normally. Takes precedence over `valueMap` for the listed values.
|
|
48
|
+
*/
|
|
49
|
+
valueTodo?: Record<string, string>;
|
|
50
|
+
}
|
|
51
|
+
/** Mapping for one source-library component (or compound sub-component). */
|
|
52
|
+
export interface ComponentMapping {
|
|
53
|
+
status: MappingStatus;
|
|
54
|
+
/**
|
|
55
|
+
* Target bestax-bulma JSX name, dotted for compounds (`Card.FooterItem`).
|
|
56
|
+
* The first segment is the import added to `@allxsmith/bestax-bulma`.
|
|
57
|
+
* Omitted when `status` is `todo` or a `special` handler chooses the target.
|
|
58
|
+
*/
|
|
59
|
+
target?: string;
|
|
60
|
+
/** Hint attached to the TODO comment when `status` is `todo`. */
|
|
61
|
+
todo?: string;
|
|
62
|
+
/** Key of a structural handler in the transform (align-based targets etc.). */
|
|
63
|
+
special?: string;
|
|
64
|
+
/** Per-prop actions, applied before the universal modifier-prop pass. */
|
|
65
|
+
props?: Record<string, PropAction>;
|
|
66
|
+
/** Compound sub-components keyed by their source-library name. */
|
|
67
|
+
subs?: Record<string, ComponentMapping>;
|
|
68
|
+
}
|
|
69
|
+
/** One annotation left in the code, mirrored into the run report. */
|
|
70
|
+
export interface TodoEntry {
|
|
71
|
+
file: string;
|
|
72
|
+
line: number | null;
|
|
73
|
+
rule: string;
|
|
74
|
+
message: string;
|
|
75
|
+
}
|
|
76
|
+
/** Per-file outcome of a transform run. */
|
|
77
|
+
export interface FileResult {
|
|
78
|
+
file: string;
|
|
79
|
+
changed: boolean;
|
|
80
|
+
todos: TodoEntry[];
|
|
81
|
+
}
|
|
82
|
+
/** Collects TODO entries for the file currently being transformed. */
|
|
83
|
+
export interface TodoCollector {
|
|
84
|
+
add(entry: TodoEntry): void;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Which stylesheet target CSS/SCSS rewrites aim at:
|
|
88
|
+
* - `bestax` (default): the recommended `@allxsmith/bestax-bulma/bestax.css`
|
|
89
|
+
* combined bundle (Bulma v1 + extras; bulma comes as a transitive dep).
|
|
90
|
+
* - `bulma`: plain `bulma/css/bulma.min.css` plus a separate extras import.
|
|
91
|
+
* - `keep`: leave existing bulma stylesheet imports untouched.
|
|
92
|
+
*/
|
|
93
|
+
export type CssMode = 'bestax' | 'bulma' | 'keep';
|
|
94
|
+
/** Options threaded through to transforms by the CLI and the tests. */
|
|
95
|
+
export interface TransformOptions {
|
|
96
|
+
collector?: TodoCollector;
|
|
97
|
+
cssMode?: CssMode;
|
|
98
|
+
[key: string]: unknown;
|
|
99
|
+
}
|
|
100
|
+
/** Options for the package.json dependency updater. */
|
|
101
|
+
export interface DepsOptions {
|
|
102
|
+
cssMode?: CssMode;
|
|
103
|
+
/** True when migrated sources still reference `bulma/…` specifiers. */
|
|
104
|
+
bulmaReferenced?: boolean;
|
|
105
|
+
}
|
|
106
|
+
/** The standard jscodeshift transform contract. */
|
|
107
|
+
export type Transform = (fileInfo: FileInfo, api: API, options: TransformOptions) => string | undefined;
|
|
108
|
+
/** Text-level transform for stylesheet files (.scss/.sass). */
|
|
109
|
+
export type StylesTransform = (filePath: string, source: string, collector: TodoCollector | undefined, options: TransformOptions) => string | null;
|
|
110
|
+
/** package.json updater; returns the changed manifest or null. */
|
|
111
|
+
export type DependenciesUpdate = (filePath: string, pkg: Record<string, unknown>, collector: TodoCollector | undefined, options: DepsOptions) => Record<string, unknown> | null;
|
|
112
|
+
/** A migration source registered with the CLI. */
|
|
113
|
+
export interface MigrationSource {
|
|
114
|
+
/** CLI name, e.g. `react-bulma-components`. */
|
|
115
|
+
name: string;
|
|
116
|
+
/** Human-readable label for the report header. */
|
|
117
|
+
label: string;
|
|
118
|
+
transform: Transform;
|
|
119
|
+
transformStyles?: StylesTransform;
|
|
120
|
+
updateDependencies?: DependenciesUpdate;
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for the bestax-migrate codemod platform.
|
|
3
|
+
*
|
|
4
|
+
* Each source library (e.g. react-bulma-components) contributes a declarative
|
|
5
|
+
* mapping table plus a jscodeshift transform that consumes it. Everything the
|
|
6
|
+
* transform cannot convert safely is annotated with a `TODO(bestax-migrate)`
|
|
7
|
+
* comment and collected into the run report.
|
|
8
|
+
*/
|
|
9
|
+
export {};
|