bestax-migrate 2.1.6 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -8
- package/dist/cli.d.ts +53 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +159 -9
- package/dist/sources/_shared/imports.d.ts +46 -0
- package/dist/sources/_shared/imports.d.ts.map +1 -0
- package/dist/sources/_shared/imports.js +152 -0
- package/dist/sources/{react-bulma-components → _shared}/jsx-utils.d.ts +20 -12
- package/dist/sources/_shared/jsx-utils.d.ts.map +1 -0
- package/dist/sources/{react-bulma-components → _shared}/jsx-utils.js +15 -10
- package/dist/sources/_shared/make-styles-transform.d.ts +37 -0
- package/dist/sources/_shared/make-styles-transform.d.ts.map +1 -0
- package/dist/sources/_shared/make-styles-transform.js +440 -0
- package/dist/sources/_shared/props.d.ts +20 -0
- package/dist/sources/_shared/props.d.ts.map +1 -0
- package/dist/sources/{react-bulma-components → _shared}/props.js +11 -7
- package/dist/sources/_shared/specials-utils.d.ts +45 -0
- package/dist/sources/_shared/specials-utils.d.ts.map +1 -0
- package/dist/sources/_shared/specials-utils.js +124 -0
- package/dist/sources/rbx/deps.d.ts +18 -0
- package/dist/sources/rbx/deps.d.ts.map +1 -0
- package/dist/sources/rbx/deps.js +287 -0
- package/dist/sources/rbx/index.d.ts +3 -0
- package/dist/sources/rbx/index.d.ts.map +1 -0
- package/dist/sources/rbx/index.js +10 -0
- package/dist/sources/rbx/mapping.d.ts +49 -0
- package/dist/sources/rbx/mapping.d.ts.map +1 -0
- package/dist/sources/rbx/mapping.js +924 -0
- package/dist/sources/rbx/responsive.d.ts +30 -0
- package/dist/sources/rbx/responsive.d.ts.map +1 -0
- package/dist/sources/rbx/responsive.js +279 -0
- package/dist/sources/rbx/specials.d.ts +21 -0
- package/dist/sources/rbx/specials.d.ts.map +1 -0
- package/dist/sources/rbx/specials.js +706 -0
- package/dist/sources/rbx/styles.d.ts +21 -0
- package/dist/sources/rbx/styles.d.ts.map +1 -0
- package/dist/sources/rbx/styles.js +29 -0
- package/dist/sources/rbx/transform.d.ts +25 -0
- package/dist/sources/rbx/transform.d.ts.map +1 -0
- package/dist/sources/rbx/transform.js +911 -0
- package/dist/sources/react-bulma-components/deps.d.ts.map +1 -1
- package/dist/sources/react-bulma-components/deps.js +16 -0
- package/dist/sources/react-bulma-components/responsive.d.ts +1 -1
- package/dist/sources/react-bulma-components/responsive.d.ts.map +1 -1
- package/dist/sources/react-bulma-components/responsive.js +1 -1
- package/dist/sources/react-bulma-components/specials.d.ts +3 -9
- package/dist/sources/react-bulma-components/specials.d.ts.map +1 -1
- package/dist/sources/react-bulma-components/specials.js +4 -91
- package/dist/sources/react-bulma-components/styles.d.ts +9 -9
- package/dist/sources/react-bulma-components/styles.d.ts.map +1 -1
- package/dist/sources/react-bulma-components/styles.js +17 -389
- package/dist/sources/react-bulma-components/transform.d.ts.map +1 -1
- package/dist/sources/react-bulma-components/transform.js +378 -101
- package/dist/sources/registry.d.ts.map +1 -1
- package/dist/sources/registry.js +2 -0
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +8 -7
- package/dist/sources/react-bulma-components/jsx-utils.d.ts.map +0 -1
- package/dist/sources/react-bulma-components/props.d.ts +0 -15
- package/dist/sources/react-bulma-components/props.d.ts.map +0 -1
|
@@ -13,9 +13,10 @@
|
|
|
13
13
|
* Anything unsafe gets a `// TODO(bestax-migrate)` comment on the enclosing
|
|
14
14
|
* statement plus an entry in the run report.
|
|
15
15
|
*/
|
|
16
|
-
import { MAPPING, resolveMapping } from './mapping.js';
|
|
17
|
-
import { addTodo, attributesOf, jsxNameParts, renameElement, } from '
|
|
18
|
-
import { applyPropAction, applyUniversalProps } from '
|
|
16
|
+
import { MAPPING, UNIVERSAL_PROPS, resolveMapping } from './mapping.js';
|
|
17
|
+
import { addTodo, attributesOf, jsxNameParts, renameElement, } from '../_shared/jsx-utils.js';
|
|
18
|
+
import { applyPropAction, applyUniversalProps } from '../_shared/props.js';
|
|
19
|
+
import { collectBoundNames, makeReserve, nameOf, prefersTabs, resolvesToBinding, } from '../_shared/imports.js';
|
|
19
20
|
import { flattenResponsiveProps } from './responsive.js';
|
|
20
21
|
import { RESPONSIVE_KINDS, runSpecial } from './specials.js';
|
|
21
22
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
@@ -57,7 +58,12 @@ export default function transform(fileInfo, api, options = {}) {
|
|
|
57
58
|
/** local identifier → canonical RBC path (from destructuring). */
|
|
58
59
|
const aliases = new Map();
|
|
59
60
|
const rbcImportPaths = [];
|
|
60
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Whether a namespace binding (`import * as RBC`) is still referenced after
|
|
63
|
+
* the JSX pass — either by a retained component (`<RBC.Tile>`) or as a bare
|
|
64
|
+
* value. Pruning the import under one leaves `RBC is not defined`.
|
|
65
|
+
*/
|
|
66
|
+
let namespaceStillReferenced = false;
|
|
61
67
|
root.find(j.ImportDeclaration).forEach(path => {
|
|
62
68
|
const source = String(path.node.source.value);
|
|
63
69
|
if (source === RBC) {
|
|
@@ -141,24 +147,115 @@ export default function transform(fileInfo, api, options = {}) {
|
|
|
141
147
|
if (imports.size === 0 && !ctx.dirty) {
|
|
142
148
|
return undefined;
|
|
143
149
|
}
|
|
150
|
+
// The scope each binding was collected in, so a reference can be checked
|
|
151
|
+
// against the binding it actually resolves to rather than matched by name.
|
|
152
|
+
const programScope = root.find(j.Program).paths()[0]?.scope;
|
|
153
|
+
// Aliases are keyed by the AST node that owns the declaring scope, not by
|
|
154
|
+
// bare name. Two functions can each destructure `Header` from a different
|
|
155
|
+
// component; a name-keyed map let the second overwrite the first, and both
|
|
156
|
+
// declarations were then pruned while both `<Header…>` references were left
|
|
157
|
+
// resolving to nothing.
|
|
158
|
+
//
|
|
159
|
+
// Resolution walks the reference's ANCESTORS rather than consulting
|
|
160
|
+
// `scope.lookup`, because by the time references are rewritten the alias
|
|
161
|
+
// pass has already pruned the declaration -- so the binding is gone from
|
|
162
|
+
// the scope table, and `path.scope` objects are not identity-stable across
|
|
163
|
+
// traversals either.
|
|
164
|
+
const aliasesByOwner = new Map();
|
|
165
|
+
const ownersFor = (name) => {
|
|
166
|
+
const owners = [];
|
|
167
|
+
for (const [owner, perScope] of aliasesByOwner) {
|
|
168
|
+
if (perScope.has(name))
|
|
169
|
+
owners.push(owner);
|
|
170
|
+
}
|
|
171
|
+
return owners;
|
|
172
|
+
};
|
|
173
|
+
const aliasAt = (name, at) => {
|
|
174
|
+
const owners = ownersFor(name);
|
|
175
|
+
if (owners.length === 0)
|
|
176
|
+
return undefined;
|
|
177
|
+
// Without a reference location there is nothing to walk, so the only
|
|
178
|
+
// owner is the best answer available. WITH one, always walk: an unrelated
|
|
179
|
+
// `Header` parameter in another function must not resolve to the alias
|
|
180
|
+
// merely because the alias is the only one by that name. A single-owner
|
|
181
|
+
// shortcut here rewrote such a parameter to `{ Header: Card.Header }`,
|
|
182
|
+
// which is not even valid syntax.
|
|
183
|
+
if (at === undefined) {
|
|
184
|
+
return owners.length === 1
|
|
185
|
+
? aliasesByOwner.get(owners[0]).get(name)
|
|
186
|
+
: undefined;
|
|
187
|
+
}
|
|
188
|
+
let cursor = at;
|
|
189
|
+
while (cursor) {
|
|
190
|
+
if (cursor.node !== undefined) {
|
|
191
|
+
const hit = aliasesByOwner.get(cursor.node)?.get(name);
|
|
192
|
+
if (hit) {
|
|
193
|
+
// The walk finds the nearest OWNER, but a binding can intervene
|
|
194
|
+
// between the reference and that owner: `function F(Header)` under
|
|
195
|
+
// a module-level `const { Header } = Card` shadows the alias, and
|
|
196
|
+
// resolving through it rewrote the parameter itself to
|
|
197
|
+
// `F(Card.Header)`. If the scope table still binds the name
|
|
198
|
+
// somewhere other than the owner, that nearer binding wins. (After
|
|
199
|
+
// the alias declaration is pruned the lookup returns nothing for
|
|
200
|
+
// the alias itself, which is the case this must keep allowing.)
|
|
201
|
+
const declaring = at.scope?.lookup(name)?.path?.node;
|
|
202
|
+
if (declaring !== undefined &&
|
|
203
|
+
declaring !== null &&
|
|
204
|
+
declaring !== cursor.node) {
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
return hit;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
cursor = cursor.parent;
|
|
211
|
+
}
|
|
212
|
+
return undefined;
|
|
213
|
+
};
|
|
214
|
+
const anyAlias = (name) => ownersFor(name).length > 0;
|
|
144
215
|
// ---- 1b. Resolve `const { Input, Field: F } = Form` destructuring -----
|
|
145
216
|
root.find(j.VariableDeclarator).forEach(path => {
|
|
146
217
|
const node = path.node;
|
|
147
218
|
if (node.id?.type !== 'ObjectPattern' || node.init?.type !== 'Identifier')
|
|
148
219
|
return;
|
|
149
220
|
const base = node.init.name;
|
|
150
|
-
const
|
|
221
|
+
const baseAlias = aliasAt(base, path);
|
|
222
|
+
const imported = imports.get(base) ?? baseAlias?.join('.');
|
|
151
223
|
if (!imported || imported === '*')
|
|
152
224
|
return;
|
|
225
|
+
if (!imports.has(base) && !baseAlias)
|
|
226
|
+
return;
|
|
153
227
|
const basePath = imports.has(base)
|
|
154
228
|
? [imports.get(base)]
|
|
155
|
-
:
|
|
229
|
+
: baseAlias;
|
|
156
230
|
let allResolved = true;
|
|
157
231
|
for (const prop of node.id.properties) {
|
|
158
232
|
if ((prop.type === 'ObjectProperty' || prop.type === 'Property') &&
|
|
159
233
|
prop.key?.type === 'Identifier' &&
|
|
160
234
|
prop.value?.type === 'Identifier') {
|
|
161
|
-
|
|
235
|
+
const owner = path.scope
|
|
236
|
+
?.path?.node;
|
|
237
|
+
let perScope = aliasesByOwner.get(owner);
|
|
238
|
+
if (!perScope) {
|
|
239
|
+
perScope = new Map();
|
|
240
|
+
aliasesByOwner.set(owner, perScope);
|
|
241
|
+
}
|
|
242
|
+
const target = [...basePath, prop.key.name];
|
|
243
|
+
perScope.set(prop.value.name, target);
|
|
244
|
+
aliases.set(prop.value.name, target);
|
|
245
|
+
// Pruning the declaration is only safe if every use of the alias will
|
|
246
|
+
// actually be rewritten. For a name the table cannot map, the JSX is
|
|
247
|
+
// left as written -- so dropping `const { Header } = Panel` left
|
|
248
|
+
// `<Header/>` referencing nothing at all.
|
|
249
|
+
// A `special` handler rewrites the element without naming a plain
|
|
250
|
+
// target (`Form.Label` becomes a bare <label>), so it counts as
|
|
251
|
+
// resolved just as a target does. Requiring `target` alone kept every
|
|
252
|
+
// declaration that mentioned one.
|
|
253
|
+
const targetMapping = resolveMapping(target);
|
|
254
|
+
if (!targetMapping ||
|
|
255
|
+
targetMapping.status === 'todo' ||
|
|
256
|
+
!(targetMapping.target || targetMapping.special)) {
|
|
257
|
+
allResolved = false;
|
|
258
|
+
}
|
|
162
259
|
}
|
|
163
260
|
else {
|
|
164
261
|
allResolved = false;
|
|
@@ -180,13 +277,18 @@ export default function transform(fileInfo, api, options = {}) {
|
|
|
180
277
|
}
|
|
181
278
|
});
|
|
182
279
|
/** Resolve a JSX name into a canonical RBC component path, or null. */
|
|
183
|
-
function resolveJsxPath(name) {
|
|
280
|
+
function resolveJsxPath(name, at) {
|
|
184
281
|
const parts = jsxNameParts(name);
|
|
185
282
|
if (!parts)
|
|
186
283
|
return null;
|
|
187
284
|
const [head, ...rest] = parts;
|
|
188
|
-
|
|
189
|
-
|
|
285
|
+
const alias = aliasAt(head, at);
|
|
286
|
+
if (alias)
|
|
287
|
+
return [...alias, ...rest];
|
|
288
|
+
// An alias exists under this name but not for THIS reference's binding,
|
|
289
|
+
// so the reference is not ours to rewrite.
|
|
290
|
+
if (anyAlias(head) && !imports.has(head))
|
|
291
|
+
return null;
|
|
190
292
|
const imported = imports.get(head);
|
|
191
293
|
if (imported === undefined)
|
|
192
294
|
return null;
|
|
@@ -199,78 +301,92 @@ export default function transform(fileInfo, api, options = {}) {
|
|
|
199
301
|
// A file may already bind names like `Field` or `Control` (components,
|
|
200
302
|
// variables, function params); importing the bestax component under the
|
|
201
303
|
// same name would redeclare it, so those imports get a Bulma* alias.
|
|
202
|
-
const bound =
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
break;
|
|
222
|
-
case 'AssignmentPattern':
|
|
223
|
-
collectPattern(node.left);
|
|
224
|
-
break;
|
|
304
|
+
const bound = collectBoundNames(j, root, RBC);
|
|
305
|
+
// Seed the reserved set with the locals of RBC imports that CANNOT be
|
|
306
|
+
// migrated, so a bestax import never claims one of them. Without this, an
|
|
307
|
+
// unmappable component aliased to a name bestax also wants
|
|
308
|
+
// (`import { Element as Button }`) had its retained specifier dropped on
|
|
309
|
+
// the collision, and its `<Button>` JSX then silently resolved to the
|
|
310
|
+
// bestax Button — one component quietly becoming another.
|
|
311
|
+
for (const [local, imported] of imports) {
|
|
312
|
+
if (imported === '*')
|
|
313
|
+
continue;
|
|
314
|
+
const entry = MAPPING[imported];
|
|
315
|
+
// Unmappable roots, plus ANY aliased import. A root can be retained by an
|
|
316
|
+
// unknown or `todo` child even when the root itself maps (`Icon` is
|
|
317
|
+
// `partial`, but `<Icon.Unknown>` retains it), and when the local name
|
|
318
|
+
// differs from the imported one that retained binding can collide with a
|
|
319
|
+
// bestax local for a completely different component. An unaliased import
|
|
320
|
+
// is safe: its name already means the same component on both sides.
|
|
321
|
+
if (!entry || entry.status === 'todo' || local !== imported) {
|
|
322
|
+
bound.add(local);
|
|
225
323
|
}
|
|
226
|
-
}
|
|
227
|
-
root
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
root
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
root.find(j.
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
324
|
+
}
|
|
325
|
+
// A root that DOES map can still be retained by a child the table cannot
|
|
326
|
+
// (`Icon` is `partial`, but `<Icon.Unknown>` keeps the source import alive).
|
|
327
|
+
// The status check above misses that case whenever the local name equals the
|
|
328
|
+
// imported one, so the bestax import took the plain local, the retained
|
|
329
|
+
// specifier was dropped on the collision, and `<Icon.Unknown>` silently
|
|
330
|
+
// resolved to the bestax component instead.
|
|
331
|
+
//
|
|
332
|
+
// Scanned rather than reserved wholesale: adding every partial root to
|
|
333
|
+
// `bound` would alias them in every file that uses one, whether or not
|
|
334
|
+
// anything is actually retained.
|
|
335
|
+
root.find(j.JSXElement).forEach(path => {
|
|
336
|
+
const head = jsxNameParts(path.node.openingElement.name)?.[0];
|
|
337
|
+
if (!head || !imports.has(head))
|
|
338
|
+
return;
|
|
339
|
+
const parts = resolveJsxPath(path.node.openingElement.name, path);
|
|
340
|
+
const mapping = parts ? resolveMapping(parts) : undefined;
|
|
341
|
+
if (!mapping || mapping.status === 'todo')
|
|
342
|
+
bound.add(head);
|
|
244
343
|
});
|
|
245
|
-
root
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
344
|
+
// The same root can be retained by a VALUE chain -- `const X = Icon.Unknown`
|
|
345
|
+
// -- which only the value-reference pass discovers, after `reserve` has
|
|
346
|
+
// already handed the plain local to bestax. Scan those chains here too.
|
|
347
|
+
root.find(j.Identifier).forEach(path => {
|
|
348
|
+
if (path.node.type !== 'Identifier')
|
|
349
|
+
return;
|
|
350
|
+
const head = path.node.name;
|
|
351
|
+
const imported = imports.get(head);
|
|
352
|
+
if (imported === undefined || imported === '*')
|
|
353
|
+
return;
|
|
354
|
+
const parent = path.parent?.node;
|
|
355
|
+
if (parent?.type !== 'MemberExpression' ||
|
|
356
|
+
parent.object !== path.node ||
|
|
357
|
+
parent.computed) {
|
|
358
|
+
return;
|
|
251
359
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
360
|
+
const chain = [imported];
|
|
361
|
+
let outer = path.parent;
|
|
362
|
+
while (outer?.node?.type === 'MemberExpression' &&
|
|
363
|
+
!outer.node.computed &&
|
|
364
|
+
outer.node.property?.type === 'Identifier') {
|
|
365
|
+
chain.push(nameOf(outer.node.property));
|
|
366
|
+
const next = outer.parent;
|
|
367
|
+
if (next?.node?.type === 'MemberExpression' &&
|
|
368
|
+
next.node.object === outer.node) {
|
|
369
|
+
outer = next;
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
break;
|
|
265
373
|
}
|
|
266
374
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
375
|
+
const mapping = resolveMapping(chain);
|
|
376
|
+
if (!mapping || mapping.status === 'todo')
|
|
377
|
+
bound.add(head);
|
|
378
|
+
});
|
|
379
|
+
ctx.reserve = makeReserve(ctx, bound);
|
|
270
380
|
// Merge with an existing bestax import: reuse its locals verbatim.
|
|
381
|
+
// Only a declaration made entirely of NAMED specifiers can be merged into.
|
|
382
|
+
// `import * as Bulma from '@allxsmith/bestax-bulma'` matches the source too,
|
|
383
|
+
// and appending named specifiers to it emitted
|
|
384
|
+
// `import * as Bulma, { Box } from …` — not valid JavaScript, so the whole
|
|
385
|
+
// file failed to parse after a migration that reported success.
|
|
271
386
|
const existingBestax = root
|
|
272
387
|
.find(j.ImportDeclaration, { source: { value: BESTAX } })
|
|
273
|
-
.paths()
|
|
388
|
+
.paths()
|
|
389
|
+
.find(path => (path.node.specifiers ?? []).every((spec) => spec.type === 'ImportSpecifier'));
|
|
274
390
|
const preExistingImports = new Set();
|
|
275
391
|
if (existingBestax) {
|
|
276
392
|
for (const spec of existingBestax.node.specifiers ?? []) {
|
|
@@ -283,9 +399,17 @@ export default function transform(fileInfo, api, options = {}) {
|
|
|
283
399
|
// ---- 2. Transform JSX elements ----------------------------------------
|
|
284
400
|
root.find(j.JSXElement).forEach(path => {
|
|
285
401
|
const element = path.node;
|
|
286
|
-
const rbcPath = resolveJsxPath(element.openingElement.name);
|
|
402
|
+
const rbcPath = resolveJsxPath(element.openingElement.name, path);
|
|
287
403
|
if (!rbcPath)
|
|
288
404
|
return;
|
|
405
|
+
// Resolve by BINDING, not by name. `function F({ Card })` shadows the
|
|
406
|
+
// import with the caller's object, and rewriting its JSX to bestax's
|
|
407
|
+
// `Card.FooterItem` changed which component rendered.
|
|
408
|
+
const jsxHead = jsxNameParts(element.openingElement.name)?.[0];
|
|
409
|
+
if (jsxHead &&
|
|
410
|
+
!resolvesToBinding(path, jsxHead, anyAlias(jsxHead) ? undefined : programScope)) {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
289
413
|
const mapping = resolveMapping(rbcPath);
|
|
290
414
|
const dotted = rbcPath.join('.');
|
|
291
415
|
if (!mapping) {
|
|
@@ -340,21 +464,40 @@ export default function transform(fileInfo, api, options = {}) {
|
|
|
340
464
|
applyPropAction(ctx, path, element, attr, action);
|
|
341
465
|
}
|
|
342
466
|
}
|
|
343
|
-
applyUniversalProps(ctx, path, element, handled);
|
|
467
|
+
applyUniversalProps(ctx, path, element, handled, UNIVERSAL_PROPS);
|
|
344
468
|
});
|
|
345
469
|
// ---- 2b. Value references to RBC components ---------------------------
|
|
346
470
|
// Components can be referenced as plain values too (`renderAs={Block}`,
|
|
347
471
|
// passed to helpers, …). JSX usages were renamed above; map the leftover
|
|
348
472
|
// identifier references so the pruned RBC import doesn't strand them.
|
|
473
|
+
// A shorthand property's key and value are distinct nodes, so the walker
|
|
474
|
+
// reaches the property twice. The first visit rewrites it and detaches the
|
|
475
|
+
// original; the second then matched neither key nor value and fell through
|
|
476
|
+
// to the generic "referenced as a value" branch, emitting a spurious TODO
|
|
477
|
+
// and marking the root retained when it was not.
|
|
478
|
+
const handledValueRefs = new WeakSet();
|
|
349
479
|
root.find(j.Identifier).forEach(path => {
|
|
350
480
|
// find(Identifier) also matches JSXIdentifier (a subtype) — JSX names
|
|
351
481
|
// were already handled by the element walker above.
|
|
352
482
|
if (path.node.type !== 'Identifier')
|
|
353
483
|
return;
|
|
484
|
+
if (handledValueRefs.has(path.node))
|
|
485
|
+
return;
|
|
354
486
|
const name = path.node.name;
|
|
355
|
-
const
|
|
356
|
-
|
|
487
|
+
// A name bound by `const { Field } = Form` lives in `aliases`, not
|
|
488
|
+
// `imports`, and the destructuring pass has already deleted the
|
|
489
|
+
// declaration that bound it. Skipping those left `Field.Label` in the
|
|
490
|
+
// output with its `Form` import pruned: a reference to nothing.
|
|
491
|
+
const aliasPath = aliasAt(name, path);
|
|
492
|
+
const imported = aliasPath ? aliasPath[0] : imports.get(name);
|
|
493
|
+
if (imported === undefined)
|
|
357
494
|
return;
|
|
495
|
+
// Resolve by BINDING, not by name: a local that shadows the import is a
|
|
496
|
+
// different object, and rewriting it repoints the code at bestax's
|
|
497
|
+
// component.
|
|
498
|
+
if (!resolvesToBinding(path, name, aliasPath ? undefined : programScope)) {
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
358
501
|
const parentNode = path.parent?.node;
|
|
359
502
|
const parentType = parentNode?.type;
|
|
360
503
|
if (parentType === 'ImportSpecifier' ||
|
|
@@ -365,6 +508,20 @@ export default function transform(fileInfo, api, options = {}) {
|
|
|
365
508
|
parentType === 'JSXMemberExpression') {
|
|
366
509
|
return;
|
|
367
510
|
}
|
|
511
|
+
// A namespace binding in a real value position. `RBC.Box` is still a
|
|
512
|
+
// mappable component reference, so fall through to the member-expression
|
|
513
|
+
// branch below with an empty prefix (the namespace itself is not part of
|
|
514
|
+
// the RBC path). Anything else — a bare `RBC` — just pins the import.
|
|
515
|
+
// Treating every namespace member as opaque left `const C = RBC.Box`
|
|
516
|
+
// referencing an import that dependency migration then removed.
|
|
517
|
+
const isNamespace = imported === '*';
|
|
518
|
+
if (isNamespace &&
|
|
519
|
+
!(parentType === 'MemberExpression' &&
|
|
520
|
+
parentNode.object === path.node &&
|
|
521
|
+
!parentNode.computed)) {
|
|
522
|
+
namespaceStillReferenced = true;
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
368
525
|
// Non-reference positions: member property names and object keys.
|
|
369
526
|
if (parentType === 'MemberExpression' &&
|
|
370
527
|
parentNode.property === path.node &&
|
|
@@ -378,7 +535,25 @@ export default function transform(fileInfo, api, options = {}) {
|
|
|
378
535
|
if (parentType === 'MemberExpression' &&
|
|
379
536
|
parentNode.object === path.node &&
|
|
380
537
|
!parentNode.computed) {
|
|
381
|
-
|
|
538
|
+
// Walk the full chain: `Card.Footer.Item` is one path, not
|
|
539
|
+
// `Card.Footer` with a stray `.Item`. Resolving only the first level
|
|
540
|
+
// left the tail pointing at a compound bestax does not have
|
|
541
|
+
// (bestax exposes `Card.FooterItem`).
|
|
542
|
+
const memberPath = isNamespace ? [] : [...(aliasPath ?? [imported])];
|
|
543
|
+
let outer = path.parent;
|
|
544
|
+
while (outer?.node?.type === 'MemberExpression' &&
|
|
545
|
+
!outer.node.computed &&
|
|
546
|
+
outer.node.property?.type === 'Identifier') {
|
|
547
|
+
memberPath.push(nameOf(outer.node.property));
|
|
548
|
+
const next = outer.parent;
|
|
549
|
+
if (next?.node?.type === 'MemberExpression' &&
|
|
550
|
+
next.node.object === outer.node) {
|
|
551
|
+
outer = next;
|
|
552
|
+
}
|
|
553
|
+
else {
|
|
554
|
+
break;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
382
557
|
const memberMapping = resolveMapping(memberPath);
|
|
383
558
|
const dotted = memberPath.join('.');
|
|
384
559
|
if (memberMapping &&
|
|
@@ -386,7 +561,36 @@ export default function transform(fileInfo, api, options = {}) {
|
|
|
386
561
|
memberMapping.target &&
|
|
387
562
|
!memberMapping.special) {
|
|
388
563
|
if (memberMapping.target === dotted) {
|
|
564
|
+
if (isNamespace) {
|
|
565
|
+
// `RBC.Card.Header` → `Card.Header`: the namespace prefix has to
|
|
566
|
+
// go, so rebuild the chain from the target rather than renaming
|
|
567
|
+
// the namespace identifier in place (there is no MAPPING['*']
|
|
568
|
+
// root to rename it to).
|
|
569
|
+
const [root, ...rest] = memberMapping.target.split('.');
|
|
570
|
+
let rebuilt = j.identifier(ctx.reserve(root));
|
|
571
|
+
for (const part of rest) {
|
|
572
|
+
rebuilt = j.memberExpression(rebuilt, j.identifier(part));
|
|
573
|
+
}
|
|
574
|
+
outer.replace(rebuilt);
|
|
575
|
+
ctx.dirty = true;
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
389
578
|
// Same compound exists on the bestax component at runtime.
|
|
579
|
+
if (aliasPath) {
|
|
580
|
+
// A destructured alias stands for MORE than one segment of the
|
|
581
|
+
// chain (`const { Header } = Card` makes `Header` mean
|
|
582
|
+
// `Card.Header`), so renaming the identifier drops every segment
|
|
583
|
+
// but the last: `Header.Title` became `Card.Title`. Rebuild the
|
|
584
|
+
// whole chain from the target instead.
|
|
585
|
+
const [aliasRoot, ...aliasRest] = memberMapping.target.split('.');
|
|
586
|
+
let rebuilt = j.identifier(ctx.reserve(aliasRoot));
|
|
587
|
+
for (const part of aliasRest) {
|
|
588
|
+
rebuilt = j.memberExpression(rebuilt, j.identifier(part));
|
|
589
|
+
}
|
|
590
|
+
outer.replace(rebuilt);
|
|
591
|
+
ctx.dirty = true;
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
390
594
|
const rootMapping = MAPPING[imported];
|
|
391
595
|
if (rootMapping?.target) {
|
|
392
596
|
const local = ctx.reserve(rootMapping.target);
|
|
@@ -397,15 +601,30 @@ export default function transform(fileInfo, api, options = {}) {
|
|
|
397
601
|
return;
|
|
398
602
|
}
|
|
399
603
|
if (!memberMapping.target.includes('.')) {
|
|
400
|
-
// Flat target (Icon.Text → IconText): swap the
|
|
604
|
+
// Flat target (Icon.Text → IconText): swap the whole chain.
|
|
401
605
|
const local = ctx.reserve(memberMapping.target);
|
|
402
|
-
|
|
606
|
+
outer.replace(j.identifier(local));
|
|
403
607
|
ctx.dirty = true;
|
|
404
608
|
return;
|
|
405
609
|
}
|
|
610
|
+
// Dotted target that differs from the source chain
|
|
611
|
+
// (Card.Footer.Item → Card.FooterItem): rebuild the chain against the
|
|
612
|
+
// bestax path rather than flagging something the table can answer.
|
|
613
|
+
const [targetRoot, ...targetRest] = memberMapping.target.split('.');
|
|
614
|
+
const rootLocal = ctx.reserve(targetRoot);
|
|
615
|
+
let rebuilt = j.identifier(rootLocal);
|
|
616
|
+
for (const part of targetRest) {
|
|
617
|
+
rebuilt = j.memberExpression(rebuilt, j.identifier(part));
|
|
618
|
+
}
|
|
619
|
+
outer.replace(rebuilt);
|
|
620
|
+
ctx.dirty = true;
|
|
621
|
+
return;
|
|
406
622
|
}
|
|
407
|
-
addTodo(ctx,
|
|
408
|
-
|
|
623
|
+
addTodo(ctx, outer, 'value-reference', `\`${isNamespace ? `${name}.` : ''}${dotted}\` is referenced as a value; migrate this usage by hand`);
|
|
624
|
+
if (isNamespace)
|
|
625
|
+
namespaceStillReferenced = true;
|
|
626
|
+
else
|
|
627
|
+
ctx.retained.add(imported);
|
|
409
628
|
return;
|
|
410
629
|
}
|
|
411
630
|
if ((parentType === 'ObjectProperty' || parentType === 'Property') &&
|
|
@@ -413,14 +632,75 @@ export default function transform(fileInfo, api, options = {}) {
|
|
|
413
632
|
!parentNode.shorthand) {
|
|
414
633
|
return;
|
|
415
634
|
}
|
|
416
|
-
|
|
635
|
+
// A shorthand property is both the key and the reference. Renaming the
|
|
636
|
+
// node in place rewrote the object's PUBLIC key
|
|
637
|
+
// (`{ Textarea }` → `{ TextArea }`), so expand it instead: the key keeps
|
|
638
|
+
// the name callers use, the value points at the migrated binding.
|
|
639
|
+
if ((parentType === 'ObjectProperty' || parentType === 'Property') &&
|
|
640
|
+
parentNode.shorthand) {
|
|
641
|
+
// Resolve through the alias path when there is one: `const { Footer } =
|
|
642
|
+
// Card` makes `{ Footer }` mean `Card.Footer`, not `Card`. Reading the
|
|
643
|
+
// root's mapping here emitted `{ Footer: Card }` — the wrong component
|
|
644
|
+
// under the right key.
|
|
645
|
+
const shorthandMapping = aliasPath
|
|
646
|
+
? resolveMapping(aliasPath)
|
|
647
|
+
: MAPPING[imported];
|
|
648
|
+
if (shorthandMapping &&
|
|
649
|
+
shorthandMapping.status !== 'todo' &&
|
|
650
|
+
shorthandMapping.target &&
|
|
651
|
+
!shorthandMapping.special) {
|
|
652
|
+
const [root, ...rest] = shorthandMapping.target.split('.');
|
|
653
|
+
const local = ctx.reserve(root);
|
|
654
|
+
let value = j.identifier(local);
|
|
655
|
+
for (const part of rest) {
|
|
656
|
+
value = j.memberExpression(value, j.identifier(part));
|
|
657
|
+
}
|
|
658
|
+
if (rest.length > 0 || local !== name) {
|
|
659
|
+
// ast-types keeps DISTINCT key and value nodes even when
|
|
660
|
+
// `shorthand` is true, so both positions are visited. Mark both, or
|
|
661
|
+
// the second visit sees a node that matches neither the (new) key
|
|
662
|
+
// nor the (new) value and falls through to the generic branch.
|
|
663
|
+
handledValueRefs.add(parentNode.key);
|
|
664
|
+
handledValueRefs.add(parentNode.value);
|
|
665
|
+
parentNode.shorthand = false;
|
|
666
|
+
parentNode.key = j.identifier(name);
|
|
667
|
+
parentNode.value = value;
|
|
668
|
+
ctx.dirty = true;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
else {
|
|
672
|
+
// Mark both positions before reporting: the key and the value are
|
|
673
|
+
// distinct nodes, so an unmarked branch reports the same property
|
|
674
|
+
// twice. The AST comment dedupes, which hid this in the output while
|
|
675
|
+
// the report still double-counted it.
|
|
676
|
+
handledValueRefs.add(parentNode.key);
|
|
677
|
+
handledValueRefs.add(parentNode.value);
|
|
678
|
+
ctx.retained.add(imported);
|
|
679
|
+
addTodo(ctx, path, 'value-reference', `\`${name}\` is referenced as a value; migrate this usage by hand`);
|
|
680
|
+
}
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
const mapping = aliasPath ? resolveMapping(aliasPath) : MAPPING[imported];
|
|
417
684
|
if (mapping &&
|
|
418
685
|
mapping.status !== 'todo' &&
|
|
419
686
|
mapping.target &&
|
|
420
|
-
!mapping.
|
|
421
|
-
const
|
|
422
|
-
|
|
687
|
+
!mapping.special) {
|
|
688
|
+
const [root, ...rest] = mapping.target.split('.');
|
|
689
|
+
const local = ctx.reserve(root);
|
|
690
|
+
if (rest.length > 0) {
|
|
691
|
+
// A dotted target needs a member expression, not a rename — otherwise
|
|
692
|
+
// a destructured alias used as a bare value (`const { Header } = Card;
|
|
693
|
+
// const V = Header`) fell through to the TODO branch, left referencing
|
|
694
|
+
// a binding the destructuring pass had already deleted.
|
|
695
|
+
let value = j.identifier(local);
|
|
696
|
+
for (const part of rest) {
|
|
697
|
+
value = j.memberExpression(value, j.identifier(part));
|
|
698
|
+
}
|
|
699
|
+
path.replace(value);
|
|
700
|
+
}
|
|
701
|
+
else if (local !== name) {
|
|
423
702
|
path.node.name = local;
|
|
703
|
+
}
|
|
424
704
|
ctx.dirty = true;
|
|
425
705
|
}
|
|
426
706
|
else {
|
|
@@ -443,9 +723,17 @@ export default function transform(fileInfo, api, options = {}) {
|
|
|
443
723
|
const bestaxLocals = new Set(ctx.needed.values());
|
|
444
724
|
for (const path of rbcImportPaths) {
|
|
445
725
|
const node = path.node;
|
|
446
|
-
const keepSpecifiers = (node.specifiers ?? []).filter((spec) =>
|
|
447
|
-
|
|
448
|
-
|
|
726
|
+
const keepSpecifiers = (node.specifiers ?? []).filter((spec) => {
|
|
727
|
+
// `import * as RBC` reaches every export at once, so it must survive
|
|
728
|
+
// whenever a component is retained (`<RBC.Tile>` stays in the JSX) or
|
|
729
|
+
// the namespace is referenced as a value.
|
|
730
|
+
if (spec.type === 'ImportNamespaceSpecifier') {
|
|
731
|
+
return ctx.retained.size > 0 || namespaceStillReferenced;
|
|
732
|
+
}
|
|
733
|
+
return (spec.type === 'ImportSpecifier' &&
|
|
734
|
+
ctx.retained.has(nameOf(spec.imported)) &&
|
|
735
|
+
!bestaxLocals.has(nameOf(spec.local)));
|
|
736
|
+
});
|
|
449
737
|
if (!inserted) {
|
|
450
738
|
if (existingBestax && bestaxImport) {
|
|
451
739
|
existingBestax.node.specifiers = [
|
|
@@ -482,14 +770,3 @@ export default function transform(fileInfo, api, options = {}) {
|
|
|
482
770
|
useTabs: prefersTabs(fileInfo.source),
|
|
483
771
|
});
|
|
484
772
|
}
|
|
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
|
-
}
|
|
@@ -1 +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;
|
|
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;AAInD,eAAO,MAAM,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAGnD,CAAC;AAEF,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAEnE;AAED,wBAAgB,WAAW,IAAI,MAAM,EAAE,CAEtC"}
|
package/dist/sources/registry.js
CHANGED
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
* registers here, and the CLI resolves its first argument against this map.
|
|
4
4
|
*/
|
|
5
5
|
import { reactBulmaComponents } from './react-bulma-components/index.js';
|
|
6
|
+
import { rbx } from './rbx/index.js';
|
|
6
7
|
export const SOURCES = {
|
|
7
8
|
[reactBulmaComponents.name]: reactBulmaComponents,
|
|
9
|
+
[rbx.name]: rbx,
|
|
8
10
|
};
|
|
9
11
|
export function getSource(name) {
|
|
10
12
|
return SOURCES[name];
|
package/dist/types.d.ts
CHANGED
|
@@ -109,6 +109,14 @@ export interface DepsOptions {
|
|
|
109
109
|
cssMode?: CssMode;
|
|
110
110
|
/** True when migrated sources still reference `bulma/…` specifiers. */
|
|
111
111
|
bulmaReferenced?: boolean;
|
|
112
|
+
/**
|
|
113
|
+
* True when at least one migrated file still imports the SOURCE library —
|
|
114
|
+
* the trimmed, TODO-annotated import the transform deliberately keeps for
|
|
115
|
+
* components with no bestax equivalent. Removing the package from the
|
|
116
|
+
* manifest while those remain leaves them unresolvable after the install
|
|
117
|
+
* step the report tells the user to run.
|
|
118
|
+
*/
|
|
119
|
+
sourceStillImported?: boolean;
|
|
112
120
|
}
|
|
113
121
|
/** The standard jscodeshift transform contract. */
|
|
114
122
|
export type Transform = (fileInfo: FileInfo, api: API, options: TransformOptions) => string | undefined;
|