bestax-migrate 2.10.0 → 2.12.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/dist/sources/bulma-classes/class-map.d.ts +17 -0
- package/dist/sources/bulma-classes/class-map.d.ts.map +1 -1
- package/dist/sources/bulma-classes/class-map.js +32 -8
- package/dist/sources/bulma-classes/plan.d.ts +34 -0
- package/dist/sources/bulma-classes/plan.d.ts.map +1 -1
- package/dist/sources/bulma-classes/plan.js +71 -7
- package/dist/sources/bulma-classes/rules.d.ts.map +1 -1
- package/dist/sources/bulma-classes/rules.js +2 -1
- package/dist/sources/bulma-classes/transform.d.ts.map +1 -1
- package/dist/sources/bulma-classes/transform.js +406 -29
- package/package.json +4 -2
|
@@ -29,6 +29,12 @@ export interface Modifier {
|
|
|
29
29
|
readonly writes: readonly PropWrite[];
|
|
30
30
|
/** The raw tags the writes are exact on (Title's size picks its heading). */
|
|
31
31
|
readonly tagIn?: readonly string[];
|
|
32
|
+
/**
|
|
33
|
+
* The prop renders the class for `true` alone, not for any truthy value
|
|
34
|
+
* (`Field`'s `grouped`), so a condition a joiner adds it under can't be
|
|
35
|
+
* handed to the prop as it is.
|
|
36
|
+
*/
|
|
37
|
+
readonly onlyTrue?: boolean;
|
|
32
38
|
}
|
|
33
39
|
/**
|
|
34
40
|
* - `mapped`: converts to `target`.
|
|
@@ -131,6 +137,17 @@ export interface RootEntry {
|
|
|
131
137
|
readonly folds?: readonly PropWrite[];
|
|
132
138
|
/** The target renders the element's only child itself. */
|
|
133
139
|
readonly absorbs?: Absorbs;
|
|
140
|
+
/** Props the target needs to render this class at all (`variant="lines"`). */
|
|
141
|
+
readonly writes?: readonly PropWrite[];
|
|
142
|
+
/**
|
|
143
|
+
* The target renders the element's children itself, `prop` of them, each
|
|
144
|
+
* a bare, empty `<tag>`. The element converts only when its children are
|
|
145
|
+
* just that, and they go (`.skeleton-lines`' `<div>`s, as `lines`).
|
|
146
|
+
*/
|
|
147
|
+
readonly countsChildren?: {
|
|
148
|
+
readonly tag: string;
|
|
149
|
+
readonly prop: string;
|
|
150
|
+
};
|
|
134
151
|
/** The target takes no helper props, so every helper class stays a class. */
|
|
135
152
|
readonly noHelpers?: boolean;
|
|
136
153
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"class-map.d.ts","sourceRoot":"","sources":["../../../src/sources/bulma-classes/class-map.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,uCAAuC;IACvC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,qEAAqE;IACrE,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,MAAM,MAAM,GAChB;IAAE,QAAQ,CAAC,EAAE,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,GAAG;IAAE,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,CAAC;AAE9E,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAE1D;AAED,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,CAAC;IACtC,6EAA6E;IAC7E,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"class-map.d.ts","sourceRoot":"","sources":["../../../src/sources/bulma-classes/class-map.ts"],"names":[],"mappings":"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"}
|
|
@@ -621,12 +621,16 @@ export const ROOTS = {
|
|
|
621
621
|
tag: 'div',
|
|
622
622
|
// `has-addons-centered`, `is-grouped-right` and the like stay classes:
|
|
623
623
|
// the prop values that render them render `has-addons` too.
|
|
624
|
-
modifiers:
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
624
|
+
modifiers: {
|
|
625
|
+
...flags({
|
|
626
|
+
'is-horizontal': 'horizontal',
|
|
627
|
+
'has-addons': 'hasAddons',
|
|
628
|
+
'is-narrow': 'narrow',
|
|
629
|
+
}),
|
|
630
|
+
// `grouped` renders `is-grouped` for `true` and its three layouts, and
|
|
631
|
+
// for nothing else truthy.
|
|
632
|
+
'is-grouped': { writes: [{ prop: 'grouped' }], onlyTrue: true },
|
|
633
|
+
},
|
|
630
634
|
wrapsChildren: {
|
|
631
635
|
in: 'field-body',
|
|
632
636
|
unless: ['Field.Label', 'Field.Body'],
|
|
@@ -1155,6 +1159,28 @@ export const ROOTS = {
|
|
|
1155
1159
|
absorbs: { tag: 'ul', attributesOn: 'element' },
|
|
1156
1160
|
ownProps: ['alignment', 'separator', 'size'],
|
|
1157
1161
|
},
|
|
1162
|
+
// `Skeleton` takes no helper props: what it isn't given as its own reaches
|
|
1163
|
+
// the DOM as an attribute, so every helper class stays a class.
|
|
1164
|
+
'skeleton-block': {
|
|
1165
|
+
status: 'mapped',
|
|
1166
|
+
target: 'Skeleton',
|
|
1167
|
+
tag: 'div',
|
|
1168
|
+
textColor: null,
|
|
1169
|
+
bgColor: null,
|
|
1170
|
+
noHelpers: true,
|
|
1171
|
+
ownProps: ['variant', 'lines'],
|
|
1172
|
+
},
|
|
1173
|
+
'skeleton-lines': {
|
|
1174
|
+
status: 'mapped',
|
|
1175
|
+
target: 'Skeleton',
|
|
1176
|
+
tag: 'div',
|
|
1177
|
+
textColor: null,
|
|
1178
|
+
bgColor: null,
|
|
1179
|
+
noHelpers: true,
|
|
1180
|
+
writes: [{ prop: 'variant', value: 'lines' }],
|
|
1181
|
+
countsChildren: { tag: 'div', prop: 'lines' },
|
|
1182
|
+
ownProps: ['variant', 'lines'],
|
|
1183
|
+
},
|
|
1158
1184
|
// ---- Families bestax has but that do not map element by element yet ------
|
|
1159
1185
|
checkbox: todo('Checkbox', "bestax `Checkbox` renders its own styled markup, not Bulma's"),
|
|
1160
1186
|
checkboxes: todo('Checkboxes', 'bestax `Checkboxes` renders its own `.field` and `.control` wrappers'),
|
|
@@ -1205,8 +1231,6 @@ export const ROOTS = {
|
|
|
1205
1231
|
'panel-icon': part(),
|
|
1206
1232
|
'panel-list': part(),
|
|
1207
1233
|
tabs: todo('Tabs', "bestax `Tabs` renders each tab's `<li>` and `<a>` together, with tab roles"),
|
|
1208
|
-
'skeleton-block': todo('Skeleton', 'bestax `Skeleton` renders its own markup'),
|
|
1209
|
-
'skeleton-lines': todo('Skeleton', 'bestax `Skeleton` renders its own markup'),
|
|
1210
1234
|
// ---- Wrappers a component renders from a prop ---------------------------------
|
|
1211
1235
|
// Each renders the wrapper with its own class and nothing else, so the
|
|
1212
1236
|
// element folds only when it carries no attribute and no other class.
|
|
@@ -13,6 +13,19 @@ export interface ElementFacts {
|
|
|
13
13
|
tag: string;
|
|
14
14
|
/** The static className, split on whitespace, in order. */
|
|
15
15
|
tokens: readonly string[];
|
|
16
|
+
/**
|
|
17
|
+
* For a `className` a class joiner builds (`clsx('button', busy &&
|
|
18
|
+
* 'is-loading')`), the classes each condition adds, one list per condition;
|
|
19
|
+
* `tokens` then holds the ones it always adds. Set only when the codemod
|
|
20
|
+
* can read every argument.
|
|
21
|
+
*/
|
|
22
|
+
conditional?: readonly (readonly string[])[];
|
|
23
|
+
/**
|
|
24
|
+
* Indexes into `conditional` of conditions that may have side effects (a
|
|
25
|
+
* call, an assignment). A condition that becomes a prop is evaluated before
|
|
26
|
+
* the ones left in the call, so it passes them only when neither is one.
|
|
27
|
+
*/
|
|
28
|
+
impure?: readonly number[];
|
|
16
29
|
/**
|
|
17
30
|
* The element's other attributes: a string value, `true` for a bare
|
|
18
31
|
* attribute, or null for an expression.
|
|
@@ -38,6 +51,12 @@ export interface ElementFacts {
|
|
|
38
51
|
* entry that `absorbs` it reads.
|
|
39
52
|
*/
|
|
40
53
|
soleChild?: ChildFacts;
|
|
54
|
+
/**
|
|
55
|
+
* The element's children, when every one React renders is a plain HTML
|
|
56
|
+
* element; left out when one is anything else (text, an expression, a
|
|
57
|
+
* component). What an entry that `countsChildren` reads.
|
|
58
|
+
*/
|
|
59
|
+
childElements?: readonly ChildFacts[];
|
|
41
60
|
/** The bestax components already in the file inside this element. */
|
|
42
61
|
bestaxInside?: readonly string[];
|
|
43
62
|
/** The bestax components already in the file around this element. */
|
|
@@ -67,6 +86,11 @@ export interface ChildFacts {
|
|
|
67
86
|
*/
|
|
68
87
|
attributes: ReadonlyMap<string, string | number | true | null>;
|
|
69
88
|
hasSpread: boolean;
|
|
89
|
+
/**
|
|
90
|
+
* Nothing inside it but whitespace React drops. A comment is something:
|
|
91
|
+
* an element that goes would take it with it.
|
|
92
|
+
*/
|
|
93
|
+
isEmpty: boolean;
|
|
70
94
|
}
|
|
71
95
|
export interface Todo {
|
|
72
96
|
rule: string;
|
|
@@ -99,6 +123,16 @@ export interface Conversion {
|
|
|
99
123
|
/** Child attributes a prop above renders instead. */
|
|
100
124
|
drop: string[];
|
|
101
125
|
};
|
|
126
|
+
/**
|
|
127
|
+
* The target renders the element's children itself, from a prop above:
|
|
128
|
+
* they go, and the element closes itself.
|
|
129
|
+
*/
|
|
130
|
+
rendersChildren?: true;
|
|
131
|
+
/**
|
|
132
|
+
* Conditional classes (`ElementFacts.conditional`) that become a boolean
|
|
133
|
+
* prop set to their condition, each once. The rest stay in the joiner call.
|
|
134
|
+
*/
|
|
135
|
+
conditional?: Array<[prop: string, token: string]>;
|
|
102
136
|
}
|
|
103
137
|
export interface Plan {
|
|
104
138
|
conversion: Conversion | null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plan.d.ts","sourceRoot":"","sources":["../../../src/sources/bulma-classes/plan.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAiBH,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,2DAA2D;IAC3D,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1B;;;OAGG;IACH,UAAU,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC;IACtD,SAAS,EAAE,OAAO,CAAC;IACnB,MAAM,EAAE,OAAO,CAAC;IAChB,4EAA4E;IAC5E,WAAW,EAAE,OAAO,CAAC;IACrB;;;;OAIG;IACH,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,SAAS,CAAC,EAAE,UAAU,CAAC;IACvB,qEAAqE;IACrE,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,qEAAqE;IACrE,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC;;;OAGG;IACH,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ;;;OAGG;IACH,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAAC;IAClC;;;OAGG;IACH,UAAU,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC;IAC/D,SAAS,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"plan.d.ts","sourceRoot":"","sources":["../../../src/sources/bulma-classes/plan.ts"],"names":[],"mappings":"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"}
|
|
@@ -42,7 +42,13 @@ function tagsItReaches(entry) {
|
|
|
42
42
|
export function plan(facts) {
|
|
43
43
|
const todos = [];
|
|
44
44
|
const { tag, tokens } = facts;
|
|
45
|
-
|
|
45
|
+
// Every class the element may carry: its fixed ones, and any a condition
|
|
46
|
+
// adds, which the element has to be right with as well as without.
|
|
47
|
+
const carried = [
|
|
48
|
+
...new Set([...tokens, ...(facts.conditional ?? []).flat()]),
|
|
49
|
+
];
|
|
50
|
+
const mayCarry = (token) => carried.includes(token);
|
|
51
|
+
for (const token of carried) {
|
|
46
52
|
const hint = legacyHint(token);
|
|
47
53
|
if (hint)
|
|
48
54
|
todos.push({ rule: ruleId('legacy', token), message: hint });
|
|
@@ -50,7 +56,7 @@ export function plan(facts) {
|
|
|
50
56
|
// A family this source leaves as markup keeps its element as markup, even
|
|
51
57
|
// beside a class it would convert (`navbar box`): the family's parts carry no
|
|
52
58
|
// TODO of their own, so its outermost class is the one place it is flagged.
|
|
53
|
-
const family =
|
|
59
|
+
const family = carried.find(token => {
|
|
54
60
|
const found = rootFor(token);
|
|
55
61
|
return found?.status === 'todo' && !found.part;
|
|
56
62
|
});
|
|
@@ -74,7 +80,7 @@ export function plan(facts) {
|
|
|
74
80
|
else {
|
|
75
81
|
// A root the table does not convert keeps the element as markup, so a
|
|
76
82
|
// later pass (or a person) still finds it as the class it is.
|
|
77
|
-
if (
|
|
83
|
+
if (carried.some(token => rootFor(token))) {
|
|
78
84
|
return { conversion: null, todos };
|
|
79
85
|
}
|
|
80
86
|
entry = wrapperEntry(tag);
|
|
@@ -110,6 +116,11 @@ export function plan(facts) {
|
|
|
110
116
|
if (attributes.has('dangerouslySetInnerHTML')) {
|
|
111
117
|
return refuse('attr', 'dangerouslySetInnerHTML', `\`dangerouslySetInnerHTML\` sets the element's content directly, and some bestax components render content of their own beside \`children\`, which React rejects; keep this element as markup`);
|
|
112
118
|
}
|
|
119
|
+
// Every fact about the element's children comes from the JSX inside it, so
|
|
120
|
+
// children passed as an attribute are content none of them sees.
|
|
121
|
+
if (attributes.has('children')) {
|
|
122
|
+
return refuse('attr', 'children', `this element sets \`children\` as an attribute, and the codemod reads an element's children from the JSX inside it, so it can't tell what bestax \`${target}\` would render from these; move them inside the element, then re-run`);
|
|
123
|
+
}
|
|
113
124
|
for (const name of attributes.keys()) {
|
|
114
125
|
if (renamed.has(name))
|
|
115
126
|
continue;
|
|
@@ -158,11 +169,20 @@ export function plan(facts) {
|
|
|
158
169
|
}
|
|
159
170
|
const wraps = entry.wrapsChildren;
|
|
160
171
|
if (wraps &&
|
|
161
|
-
(!wraps.when ||
|
|
172
|
+
(!wraps.when || mayCarry(wraps.when)) &&
|
|
162
173
|
(facts.hasChildren || wraps.whenEmpty) &&
|
|
163
174
|
!facts.childTargets?.some(child => wraps.unless.includes(child))) {
|
|
164
175
|
return refuse('children', target, `bestax \`${target}\` renders its children inside a \`.${wraps.in}\` of its own unless one of them is a ${orList(wraps.unless)}, so this element stays markup`);
|
|
165
176
|
}
|
|
177
|
+
const counts = entry.countsChildren;
|
|
178
|
+
const counted = facts.childElements?.every(child => child.tag === counts?.tag &&
|
|
179
|
+
child.tokens === undefined &&
|
|
180
|
+
child.attributes.size === 0 &&
|
|
181
|
+
!child.hasSpread &&
|
|
182
|
+
child.isEmpty);
|
|
183
|
+
if (counts && !counted) {
|
|
184
|
+
return refuse('children', target, `bestax \`${target}\` renders this element's children itself, \`${counts.prop}\` bare, empty <${counts.tag}>s, so it converts only when its children are just that; keep it as markup`);
|
|
185
|
+
}
|
|
166
186
|
if (missing.length > 0) {
|
|
167
187
|
const list = missing.map(([name, value]) => `\`${name}="${value}"\``);
|
|
168
188
|
return refuse('defaults', target, `bestax \`${target}\` renders ${list.join(' and ')} when the element does not set ${missing.length === 1 ? 'it' : 'them'}; add ${missing.length === 1 ? 'it' : 'them'} here if that is what you want, then re-run`);
|
|
@@ -178,7 +198,8 @@ export function plan(facts) {
|
|
|
178
198
|
}
|
|
179
199
|
}
|
|
180
200
|
// ---- Tokens → props ---------------------------------------------------------
|
|
181
|
-
|
|
201
|
+
// What the root itself needs comes first, so no class can take its prop.
|
|
202
|
+
const writes = new Map((entry.writes ?? []).map(write => [write.prop, write.value ?? true]));
|
|
182
203
|
/** The tokens each written prop came from, so a group rule can undo it. */
|
|
183
204
|
const sourceOf = new Map();
|
|
184
205
|
const groupOf = new Map();
|
|
@@ -254,8 +275,49 @@ export function plan(facts) {
|
|
|
254
275
|
if (as)
|
|
255
276
|
props.push(['as', as]);
|
|
256
277
|
props.push(...writes);
|
|
278
|
+
if (counts) {
|
|
279
|
+
props.push([counts.prop, String(facts.childElements.length)]);
|
|
280
|
+
numbers.push(counts.prop);
|
|
281
|
+
}
|
|
282
|
+
// A class a condition adds becomes the boolean prop that renders it, set to
|
|
283
|
+
// that condition, when it is one of the root's flags on its own: the
|
|
284
|
+
// component renders it exactly when the condition is truthy, as the joiner
|
|
285
|
+
// does. Anything else a condition adds stays in the joiner call.
|
|
286
|
+
const parts = facts.conditional ?? [];
|
|
287
|
+
const conditional = [];
|
|
288
|
+
let conditionalStays = false;
|
|
289
|
+
const places = (token) => parts.filter(part => part.includes(token)).length +
|
|
290
|
+
(tokens.includes(token) ? 1 : 0);
|
|
291
|
+
const paired = new Set(Object.values(entry.absorbs?.pairs ?? {}));
|
|
292
|
+
const impure = new Set(facts.impure ?? []);
|
|
293
|
+
let impureStays = false;
|
|
294
|
+
parts.forEach((part, index) => {
|
|
295
|
+
const [token] = part;
|
|
296
|
+
const modifier = part.length === 1 && places(token) === 1 && !paired.has(token)
|
|
297
|
+
? modifierFor(entry, token)
|
|
298
|
+
: undefined;
|
|
299
|
+
const [write, ...more] = modifier?.writes ?? [];
|
|
300
|
+
const flag = write !== undefined &&
|
|
301
|
+
more.length === 0 &&
|
|
302
|
+
write.value === undefined &&
|
|
303
|
+
!modifier.onlyTrue &&
|
|
304
|
+
(!modifier.tagIn || modifier.tagIn.includes(tag)) &&
|
|
305
|
+
!writes.has(write.prop) &&
|
|
306
|
+
!conditional.some(([prop]) => prop === write.prop);
|
|
307
|
+
// Written as a prop, the condition is evaluated before any left in the
|
|
308
|
+
// call ahead of it, which is safe only when neither can have side effects.
|
|
309
|
+
const reorders = conditionalStays && (impureStays || impure.has(index));
|
|
310
|
+
if (flag && !reorders) {
|
|
311
|
+
conditional.push([write.prop, token]);
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
conditionalStays = true;
|
|
315
|
+
if (impure.has(index))
|
|
316
|
+
impureStays = true;
|
|
317
|
+
}
|
|
318
|
+
});
|
|
257
319
|
const rest = tokens.filter(token => !converted.has(token));
|
|
258
|
-
if (entry.ownClassOnly && rest.length > 0) {
|
|
320
|
+
if (entry.ownClassOnly && (rest.length > 0 || conditionalStays)) {
|
|
259
321
|
return refuse('attr', 'className', `bestax \`${target}\` drops its own class when it is given a \`className\`, so this element's other classes would take the place of \`.${root}\`; keep it as markup`);
|
|
260
322
|
}
|
|
261
323
|
return {
|
|
@@ -266,6 +328,8 @@ export function plan(facts) {
|
|
|
266
328
|
drop,
|
|
267
329
|
numbers,
|
|
268
330
|
...(absorbed ? { absorbs: absorbed } : {}),
|
|
331
|
+
...(counts ? { rendersChildren: true } : {}),
|
|
332
|
+
...(conditional.length > 0 ? { conditional } : {}),
|
|
269
333
|
},
|
|
270
334
|
todos,
|
|
271
335
|
};
|
|
@@ -294,7 +358,7 @@ function absorb(facts, root, entry, target, refuse) {
|
|
|
294
358
|
return refuse('spread', target, `the <${spec.tag}> inside spreads props, which bestax \`${target}\` may read differently than the <${spec.tag}> did; convert the two to \`${target}\` by hand`);
|
|
295
359
|
}
|
|
296
360
|
if (child.tokens === null) {
|
|
297
|
-
return refuse('dynamic-class', target, `the <${spec.tag}> inside has a computed \`className\`,
|
|
361
|
+
return refuse('dynamic-class', target, `the <${spec.tag}> inside has a computed \`className\`, which the codemod does not read there; convert the two to bestax \`${target}\` by hand, turning each condition into its prop`);
|
|
298
362
|
}
|
|
299
363
|
if (child.tokens?.length === 0) {
|
|
300
364
|
return refuse('attr', 'className', `the <${spec.tag}> inside has an empty \`className\`, which renders \`class=""\`, and bestax \`${target}\` renders it with no class attribute; drop the empty \`className\`, then re-run`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../../../src/sources/bulma-classes/rules.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAUH,eAAO,MAAM,KAAK,EAAE,WAAW,CAAC,MAAM,CAapC,CAAC;
|
|
1
|
+
{"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../../../src/sources/bulma-classes/rules.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAUH,eAAO,MAAM,KAAK,EAAE,WAAW,CAAC,MAAM,CAapC,CAAC;AA0BH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEnD;AAED,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAK1D"}
|
|
@@ -40,8 +40,9 @@ const VOCABULARY = new Set([
|
|
|
40
40
|
...Object.values(WRAPPERS),
|
|
41
41
|
...Object.values(WRAPPER_OWN_PROPS).flat(),
|
|
42
42
|
...HELPER_PROPS,
|
|
43
|
-
// Refused on every target, so no entry lists
|
|
43
|
+
// Refused on every target, so no entry lists them.
|
|
44
44
|
'dangerouslySetInnerHTML',
|
|
45
|
+
'children',
|
|
45
46
|
// Refused where the target drops its own class for one it is given.
|
|
46
47
|
'className',
|
|
47
48
|
// Refused on the child a target renders itself.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transform.d.ts","sourceRoot":"","sources":["../../../src/sources/bulma-classes/transform.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAW,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE1D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"transform.d.ts","sourceRoot":"","sources":["../../../src/sources/bulma-classes/transform.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAW,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE1D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAmtBvD,MAAM,CAAC,OAAO,UAAU,SAAS,CAC/B,QAAQ,EAAE,QAAQ,EAClB,GAAG,EAAE,GAAG,EACR,OAAO,GAAE,gBAAqB,GAC7B,MAAM,GAAG,SAAS,CAygBpB"}
|
|
@@ -21,8 +21,9 @@ import { addTodo, findAttr, forgetJsxParens, jsxNameParts, literalValueOf, makeA
|
|
|
21
21
|
import { collectBoundNames, makeReserve, prefersTabs, resolvesToBinding, } from '../_shared/imports.js';
|
|
22
22
|
import { rewriteStylesheetImports } from '../_shared/css-imports.js';
|
|
23
23
|
import { BESTAX, buildBestaxImport, placeBestaxImport, seedBestaxImport, } from '../_shared/bestax-import.js';
|
|
24
|
-
import { plan } from './plan.js';
|
|
24
|
+
import { plan, } from './plan.js';
|
|
25
25
|
import { REACT_RUNTIMES, } from './project.js';
|
|
26
|
+
import { rootFor } from './class-map.js';
|
|
26
27
|
import { ruleId } from './rules.js';
|
|
27
28
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
28
29
|
/**
|
|
@@ -48,48 +49,293 @@ const HEADER_PRAGMA = /^[\s*]*(?:@ts-(?:no)?check|@flow|@noflow|eslint-disable(?
|
|
|
48
49
|
/** Directives that act on the line after them, so must stay above it. */
|
|
49
50
|
const NEXT_LINE_DIRECTIVE = /^[\s*]*(?:eslint-disable-next-line|@ts-expect-error|@ts-ignore|prettier-ignore)\b/;
|
|
50
51
|
/**
|
|
51
|
-
* A comment moved
|
|
52
|
-
*
|
|
53
|
-
* closing tag
|
|
52
|
+
* A comment moved into a JSX tag, before or after a node there. It has to be
|
|
53
|
+
* a block comment: after a tag name recast prints a line comment as `<// …`,
|
|
54
|
+
* which TypeScript reads as a closing tag, and before an attribute it would
|
|
55
|
+
* run on into it.
|
|
54
56
|
*/
|
|
55
|
-
function
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
function asBlock(comment, trailing) {
|
|
58
|
+
const placed = { leading: !trailing, trailing };
|
|
59
|
+
if (comment.type !== 'CommentLine')
|
|
60
|
+
return { ...comment, ...placed };
|
|
59
61
|
return {
|
|
60
62
|
type: 'CommentBlock',
|
|
61
63
|
value: ` ${comment.value.trim().replace(/\*\//g, '*\\/')} `,
|
|
62
|
-
|
|
63
|
-
trailing: true,
|
|
64
|
+
...placed,
|
|
64
65
|
};
|
|
65
66
|
}
|
|
67
|
+
/** A comment moved to sit after a JSX tag name. */
|
|
68
|
+
function afterTagName(comment) {
|
|
69
|
+
return asBlock(comment, true);
|
|
70
|
+
}
|
|
66
71
|
/**
|
|
67
|
-
* Put `
|
|
68
|
-
*
|
|
69
|
-
* place: the first new attribute, else the next one along, else the tag
|
|
72
|
+
* Put `replacement` where an element's `className` was. A comment on the
|
|
73
|
+
* className, and any `carried` from inside it, goes with whatever takes its
|
|
74
|
+
* place: the first new attribute, else the next one along, else the tag
|
|
75
|
+
* name. A className kept in `replacement` keeps its own.
|
|
70
76
|
*/
|
|
71
|
-
function
|
|
77
|
+
function spliceClassName(element, replacement, carried = []) {
|
|
72
78
|
const attrs = element.openingElement.attributes;
|
|
73
79
|
const classAttr = findAttr(element, 'className');
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (classAttr.comments?.length) {
|
|
80
|
+
const kept = replacement.includes(classAttr);
|
|
81
|
+
const moving = [...(kept ? [] : (classAttr.comments ?? [])), ...carried];
|
|
82
|
+
if (moving.length > 0) {
|
|
78
83
|
const name = element.openingElement.name;
|
|
79
84
|
const carrier = replacement[0] ?? attrs.find((attr) => attr !== classAttr) ?? name;
|
|
80
85
|
carrier.comments = [
|
|
81
86
|
...(carrier.comments ?? []),
|
|
82
|
-
...
|
|
87
|
+
...moving.map((comment) => carrier === name ? afterTagName(comment) : comment),
|
|
83
88
|
];
|
|
84
89
|
}
|
|
85
90
|
attrs.splice(attrs.indexOf(classAttr), 1, ...replacement);
|
|
86
91
|
}
|
|
92
|
+
/** Put `props`, then what stays in `className`, where `className` was. */
|
|
93
|
+
function writeClassName(j, element, props, className) {
|
|
94
|
+
const replacement = props.map(([name, value]) => makeAttr(j, name, value === true ? undefined : value));
|
|
95
|
+
if (className)
|
|
96
|
+
replacement.push(makeAttr(j, 'className', className));
|
|
97
|
+
spliceClassName(element, replacement);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The same, for a `className` a joiner builds: the classes a prop now
|
|
101
|
+
* renders leave the call, each condition that became a prop moves onto it,
|
|
102
|
+
* and what is left stays in the call (a plain string when nothing in it is
|
|
103
|
+
* conditional, no `className` when nothing is left). Returns whether the
|
|
104
|
+
* call is gone.
|
|
105
|
+
*/
|
|
106
|
+
function writeJoined(j, element, conversion, joined) {
|
|
107
|
+
const { call, statics, parts } = joined;
|
|
108
|
+
const kept = new Set(conversion.className?.split(' ') ?? []);
|
|
109
|
+
const carried = [];
|
|
110
|
+
const remove = (list, node) => list.splice(list.indexOf(node), 1);
|
|
111
|
+
for (const node of statics) {
|
|
112
|
+
const classes = classesIn(staticText(node));
|
|
113
|
+
const remaining = classes.filter(token => kept.has(token));
|
|
114
|
+
if (remaining.length === classes.length)
|
|
115
|
+
continue;
|
|
116
|
+
if (remaining.length === 0) {
|
|
117
|
+
carried.push(...commentsWithin(node));
|
|
118
|
+
remove(call.arguments, node);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const literal = j.stringLiteral(remaining.join(' '));
|
|
122
|
+
literal.comments = node.comments;
|
|
123
|
+
call.arguments[call.arguments.indexOf(node)] = literal;
|
|
124
|
+
}
|
|
125
|
+
const conditions = (conversion.conditional ?? []).map(([prop, token]) => {
|
|
126
|
+
const part = parts.find(found => found.tokens.length === 1 && found.tokens[0] === token);
|
|
127
|
+
carried.push(...commentsWithin(part.node, part.condition));
|
|
128
|
+
if (part.object) {
|
|
129
|
+
remove(part.object.properties, part.node);
|
|
130
|
+
if (part.object.properties.length === 0) {
|
|
131
|
+
carried.push(...commentsWithin(part.object));
|
|
132
|
+
remove(call.arguments, part.object);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
remove(call.arguments, part.node);
|
|
137
|
+
}
|
|
138
|
+
const value = part.negate
|
|
139
|
+
? j.unaryExpression('!', part.condition)
|
|
140
|
+
: part.condition;
|
|
141
|
+
return j.jsxAttribute(j.jsxIdentifier(prop), j.jsxExpressionContainer(value));
|
|
142
|
+
});
|
|
143
|
+
const classAttr = findAttr(element, 'className');
|
|
144
|
+
let rest = [classAttr];
|
|
145
|
+
const gone = call.arguments.every((arg) => staticText(arg) !== null);
|
|
146
|
+
if (gone) {
|
|
147
|
+
carried.push(...commentsWithin(classAttr.value));
|
|
148
|
+
const classes = call.arguments.flatMap((arg) => classesIn(staticText(arg)));
|
|
149
|
+
rest =
|
|
150
|
+
classes.length > 0 ? [makeAttr(j, 'className', classes.join(' '))] : [];
|
|
151
|
+
}
|
|
152
|
+
spliceClassName(element, [
|
|
153
|
+
...conversion.props.map(([name, value]) => makeAttr(j, name, value === true ? undefined : value)),
|
|
154
|
+
...conditions,
|
|
155
|
+
...rest,
|
|
156
|
+
], carried.map(comment => asBlock(comment, false)));
|
|
157
|
+
return gone;
|
|
158
|
+
}
|
|
87
159
|
/** A lowercase intrinsic tag: not a component, not a custom element. */
|
|
88
160
|
const INTRINSIC = /^[a-z][a-z0-9]*$/;
|
|
89
161
|
/** Subtrees whose tags are not HTML. */
|
|
90
162
|
const FOREIGN_ROOTS = new Set(['svg', 'math']);
|
|
91
163
|
/** Parents that render their only child as it is. */
|
|
92
164
|
const PASS_THROUGH_PARENTS = new Set(['Fragment', 'React.Fragment']);
|
|
165
|
+
/**
|
|
166
|
+
* Class joiners whose arguments the codemod can read exactly, by package and
|
|
167
|
+
* export: a string adds its classes, and an object key, `a && 'x'` or
|
|
168
|
+
* `a ? 'x' : ''` adds them when the condition is truthy. Not `clsx/lite`,
|
|
169
|
+
* which ignores objects, `classnames/dedupe`, where a later key can take a
|
|
170
|
+
* class back, or `classnames/bind`, which looks names up in a CSS module.
|
|
171
|
+
*/
|
|
172
|
+
const JOINERS = {
|
|
173
|
+
clsx: ['default', 'clsx'],
|
|
174
|
+
classnames: ['default'],
|
|
175
|
+
};
|
|
176
|
+
/** A string literal's or an expression-free template's text, else null. */
|
|
177
|
+
function staticText(node) {
|
|
178
|
+
if (node?.type === 'StringLiteral')
|
|
179
|
+
return node.value;
|
|
180
|
+
if (node?.type === 'TemplateLiteral' && node.expressions.length === 0) {
|
|
181
|
+
return node.quasis[0].value.cooked ?? null;
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
function classesIn(text) {
|
|
186
|
+
return text.split(/\s+/).filter(Boolean);
|
|
187
|
+
}
|
|
188
|
+
/** A joiner argument that adds nothing: `''`, `null`, `undefined`, `false`. */
|
|
189
|
+
function addsNothing(node) {
|
|
190
|
+
if (staticText(node)?.trim() === '')
|
|
191
|
+
return true;
|
|
192
|
+
if (node.type === 'NullLiteral')
|
|
193
|
+
return true;
|
|
194
|
+
if (node.type === 'BooleanLiteral')
|
|
195
|
+
return node.value === false;
|
|
196
|
+
return node.type === 'Identifier' && node.name === 'undefined';
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Whether evaluating an expression can't change anything: names, literals,
|
|
200
|
+
* property reads, and operators over those. A call, an assignment, `new` or
|
|
201
|
+
* `delete` can. (So can a getter; a property read is taken as a read.)
|
|
202
|
+
*/
|
|
203
|
+
function sideEffectFree(node) {
|
|
204
|
+
switch (node?.type) {
|
|
205
|
+
case 'Identifier':
|
|
206
|
+
case 'StringLiteral':
|
|
207
|
+
case 'NumericLiteral':
|
|
208
|
+
case 'BooleanLiteral':
|
|
209
|
+
case 'NullLiteral':
|
|
210
|
+
case 'BigIntLiteral':
|
|
211
|
+
case 'ThisExpression':
|
|
212
|
+
return true;
|
|
213
|
+
case 'TemplateLiteral':
|
|
214
|
+
return node.expressions.every(sideEffectFree);
|
|
215
|
+
case 'MemberExpression':
|
|
216
|
+
case 'OptionalMemberExpression':
|
|
217
|
+
return (sideEffectFree(node.object) &&
|
|
218
|
+
(!node.computed || sideEffectFree(node.property)));
|
|
219
|
+
case 'UnaryExpression':
|
|
220
|
+
return node.operator !== 'delete' && sideEffectFree(node.argument);
|
|
221
|
+
case 'BinaryExpression':
|
|
222
|
+
case 'LogicalExpression':
|
|
223
|
+
return sideEffectFree(node.left) && sideEffectFree(node.right);
|
|
224
|
+
case 'ConditionalExpression':
|
|
225
|
+
return [node.test, node.consequent, node.alternate].every(sideEffectFree);
|
|
226
|
+
case 'TSAsExpression':
|
|
227
|
+
case 'TSSatisfiesExpression':
|
|
228
|
+
case 'TSNonNullExpression':
|
|
229
|
+
case 'TSTypeAssertion':
|
|
230
|
+
case 'ParenthesizedExpression':
|
|
231
|
+
return sideEffectFree(node.expression);
|
|
232
|
+
default:
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* A `className={joiner(…)}` whose every argument the codemod can read, or
|
|
238
|
+
* undefined: an argument it can't (a variable, a call, a spread, an array)
|
|
239
|
+
* could add any class.
|
|
240
|
+
*/
|
|
241
|
+
function readJoiner(value, isJoiner) {
|
|
242
|
+
const call = value?.type === 'JSXExpressionContainer' ? value.expression : null;
|
|
243
|
+
if (call?.type !== 'CallExpression' || !isJoiner(call.callee)) {
|
|
244
|
+
return undefined;
|
|
245
|
+
}
|
|
246
|
+
const statics = [];
|
|
247
|
+
const parts = [];
|
|
248
|
+
for (const arg of call.arguments) {
|
|
249
|
+
if (staticText(arg) !== null) {
|
|
250
|
+
statics.push(arg);
|
|
251
|
+
}
|
|
252
|
+
else if (arg.type === 'ObjectExpression') {
|
|
253
|
+
for (const property of arg.properties) {
|
|
254
|
+
const plain = (property.type === 'ObjectProperty' ||
|
|
255
|
+
property.type === 'Property') &&
|
|
256
|
+
!property.computed &&
|
|
257
|
+
!property.method &&
|
|
258
|
+
(property.kind === undefined || property.kind === 'init');
|
|
259
|
+
const key = !plain
|
|
260
|
+
? undefined
|
|
261
|
+
: property.key.type === 'Identifier'
|
|
262
|
+
? property.key.name
|
|
263
|
+
: staticText(property.key);
|
|
264
|
+
if (key == null)
|
|
265
|
+
return undefined;
|
|
266
|
+
parts.push({
|
|
267
|
+
tokens: classesIn(key),
|
|
268
|
+
condition: property.value,
|
|
269
|
+
negate: false,
|
|
270
|
+
node: property,
|
|
271
|
+
object: arg,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
else if (arg.type === 'LogicalExpression' &&
|
|
276
|
+
arg.operator === '&&' &&
|
|
277
|
+
staticText(arg.right) !== null) {
|
|
278
|
+
parts.push({
|
|
279
|
+
tokens: classesIn(staticText(arg.right)),
|
|
280
|
+
condition: arg.left,
|
|
281
|
+
negate: false,
|
|
282
|
+
node: arg,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
else if (arg.type === 'ConditionalExpression' &&
|
|
286
|
+
staticText(arg.consequent) !== null &&
|
|
287
|
+
addsNothing(arg.alternate)) {
|
|
288
|
+
parts.push({
|
|
289
|
+
tokens: classesIn(staticText(arg.consequent)),
|
|
290
|
+
condition: arg.test,
|
|
291
|
+
negate: false,
|
|
292
|
+
node: arg,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
else if (arg.type === 'ConditionalExpression' &&
|
|
296
|
+
staticText(arg.alternate) !== null &&
|
|
297
|
+
addsNothing(arg.consequent)) {
|
|
298
|
+
parts.push({
|
|
299
|
+
tokens: classesIn(staticText(arg.alternate)),
|
|
300
|
+
condition: arg.test,
|
|
301
|
+
negate: true,
|
|
302
|
+
node: arg,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
return undefined;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
const tokens = [
|
|
310
|
+
...new Set(statics.flatMap(node => classesIn(staticText(node)))),
|
|
311
|
+
];
|
|
312
|
+
return { call, tokens, statics, parts };
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Every comment on a node and inside it, except inside `keep` (a condition
|
|
316
|
+
* that moves, taking its comments with it).
|
|
317
|
+
*/
|
|
318
|
+
function commentsWithin(node, keep) {
|
|
319
|
+
const found = [];
|
|
320
|
+
const visit = (current) => {
|
|
321
|
+
if (!current || typeof current !== 'object' || current === keep)
|
|
322
|
+
return;
|
|
323
|
+
if (Array.isArray(current)) {
|
|
324
|
+
current.forEach(visit);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
if (typeof current.type !== 'string')
|
|
328
|
+
return;
|
|
329
|
+
found.push(...(current.comments ?? []));
|
|
330
|
+
for (const [key, child] of Object.entries(current)) {
|
|
331
|
+
if (key !== 'comments' && key !== 'loc' && key !== 'original') {
|
|
332
|
+
visit(child);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
visit(node);
|
|
337
|
+
return found;
|
|
338
|
+
}
|
|
93
339
|
/**
|
|
94
340
|
* The static text of a `className`, or null when it is computed. A string
|
|
95
341
|
* literal, `{'…'}`, and a template literal with no expressions are static.
|
|
@@ -294,6 +540,10 @@ function reachesReact(child) {
|
|
|
294
540
|
return isContent(child);
|
|
295
541
|
return /[^ \t\r\n]/.test(child.value) || !/[\r\n]/.test(child.value);
|
|
296
542
|
}
|
|
543
|
+
/** Whitespace React drops, which nothing is lost with. A comment is kept. */
|
|
544
|
+
function dropped(child) {
|
|
545
|
+
return child.type === 'JSXText' && !reachesReact(child);
|
|
546
|
+
}
|
|
297
547
|
/** A plain HTML child as the planner reads it; nothing for a component. */
|
|
298
548
|
function childFacts(child) {
|
|
299
549
|
const name = child.openingElement.name;
|
|
@@ -311,8 +561,25 @@ function childFacts(child) {
|
|
|
311
561
|
}),
|
|
312
562
|
attributes: attributesBesides(child, classAttr, childAttributeValue),
|
|
313
563
|
hasSpread: hasSpread(child),
|
|
564
|
+
isEmpty: (child.children ?? []).every(dropped),
|
|
314
565
|
};
|
|
315
566
|
}
|
|
567
|
+
/**
|
|
568
|
+
* An element's children as plain HTML elements, when that is every one
|
|
569
|
+
* React renders; undefined when one is anything else.
|
|
570
|
+
*/
|
|
571
|
+
function childElementsOf(element) {
|
|
572
|
+
const children = [];
|
|
573
|
+
for (const child of element.children ?? []) {
|
|
574
|
+
if (dropped(child))
|
|
575
|
+
continue;
|
|
576
|
+
const found = child.type === 'JSXElement' ? childFacts(child) : undefined;
|
|
577
|
+
if (!found)
|
|
578
|
+
return undefined;
|
|
579
|
+
children.push(found);
|
|
580
|
+
}
|
|
581
|
+
return children;
|
|
582
|
+
}
|
|
316
583
|
function insideForeignContent(elementPath) {
|
|
317
584
|
let current = elementPath.parent;
|
|
318
585
|
while (current) {
|
|
@@ -392,6 +659,33 @@ export default function transform(fileInfo, api, options = {}) {
|
|
|
392
659
|
// a child that is one of its parts.
|
|
393
660
|
const planned = new Map();
|
|
394
661
|
const programScope = root.find(j.Program).paths()[0]?.scope;
|
|
662
|
+
// The joiners the file imports, by local name, and the elements whose
|
|
663
|
+
// joiner call converts.
|
|
664
|
+
const joinerLocals = new Set();
|
|
665
|
+
root.find(j.ImportDeclaration).forEach((importPath) => {
|
|
666
|
+
const node = importPath.node;
|
|
667
|
+
const exported = Object.hasOwn(JOINERS, node.source.value)
|
|
668
|
+
? JOINERS[node.source.value]
|
|
669
|
+
: undefined;
|
|
670
|
+
if (!exported || node.importKind === 'type')
|
|
671
|
+
return;
|
|
672
|
+
for (const spec of node.specifiers ?? []) {
|
|
673
|
+
const name = spec.type === 'ImportDefaultSpecifier'
|
|
674
|
+
? 'default'
|
|
675
|
+
: spec.type === 'ImportSpecifier' && spec.importKind !== 'type'
|
|
676
|
+
? (spec.imported.name ?? spec.imported.value)
|
|
677
|
+
: undefined;
|
|
678
|
+
if (name && exported.includes(name))
|
|
679
|
+
joinerLocals.add(spec.local.name);
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
/** Whether a callee is one of those joiners where it is called. */
|
|
683
|
+
const isJoiner = (callee, scopePath) => callee?.type === 'Identifier' &&
|
|
684
|
+
joinerLocals.has(callee.name) &&
|
|
685
|
+
resolvesToBinding(scopePath, callee.name, programScope);
|
|
686
|
+
const joins = new Map();
|
|
687
|
+
/** Joiners a rewritten className no longer calls. */
|
|
688
|
+
const unjoined = new Set();
|
|
395
689
|
/**
|
|
396
690
|
* The bestax component an element already is, when its name is the bestax
|
|
397
691
|
* import and not a local that shadows it; `scopePath` is where the name is
|
|
@@ -419,7 +713,7 @@ export default function transform(fileInfo, api, options = {}) {
|
|
|
419
713
|
* React renders, and no comment, which folding the element would drop.
|
|
420
714
|
*/
|
|
421
715
|
const soleChild = (element) => {
|
|
422
|
-
const content = (element.children ?? []).filter((child) =>
|
|
716
|
+
const content = (element.children ?? []).filter((child) => !dropped(child));
|
|
423
717
|
return content.length === 1 && content[0].type === 'JSXElement'
|
|
424
718
|
? content[0]
|
|
425
719
|
: undefined;
|
|
@@ -492,26 +786,52 @@ export default function transform(fileInfo, api, options = {}) {
|
|
|
492
786
|
childTargets: childTargets(elementPath),
|
|
493
787
|
soleChildTarget: planned.get(sole)?.conversion?.target,
|
|
494
788
|
soleChild: sole && childFacts(sole),
|
|
789
|
+
childElements: childElementsOf(element),
|
|
495
790
|
bestaxInside: bestaxInside.get(element) ?? [],
|
|
496
791
|
bestaxAround: surrounding.bestax,
|
|
497
792
|
componentsAround: surrounding.other,
|
|
498
793
|
onlyChildOf: onlyChildOf(elementPath, bestaxLocals),
|
|
499
794
|
};
|
|
500
|
-
|
|
795
|
+
// A joiner call the codemod can read converts exactly: its fixed classes
|
|
796
|
+
// as for a static className, and each condition on a flag as its prop.
|
|
797
|
+
const joined = className === null
|
|
798
|
+
? readJoiner(classAttr.value, callee => isJoiner(callee, elementPath))
|
|
799
|
+
: undefined;
|
|
800
|
+
let exact;
|
|
801
|
+
if (joined) {
|
|
802
|
+
exact = plan({
|
|
803
|
+
...facts,
|
|
804
|
+
tokens: joined.tokens,
|
|
805
|
+
conditional: joined.parts.map(part => part.tokens),
|
|
806
|
+
impure: joined.parts.flatMap((part, index) => sideEffectFree(part.condition) ? [] : [index]),
|
|
807
|
+
});
|
|
808
|
+
if (exact.conversion)
|
|
809
|
+
joins.set(element, joined);
|
|
810
|
+
else
|
|
811
|
+
exact = undefined;
|
|
812
|
+
}
|
|
813
|
+
let result = exact ?? plan(facts);
|
|
501
814
|
const converts = result.conversion !== null || result.fold !== undefined;
|
|
502
815
|
const becomes = result.conversion?.target ?? result.fold?.target;
|
|
503
|
-
if (className === null && becomes) {
|
|
504
|
-
// The same element with a static className would convert; with
|
|
505
|
-
// computed
|
|
816
|
+
if (className === null && !exact && becomes) {
|
|
817
|
+
// The same element with a static className would convert; with one
|
|
818
|
+
// computed any other way, only a person can turn each condition into
|
|
819
|
+
// its prop.
|
|
506
820
|
const target = becomes;
|
|
821
|
+
// A call it could read says what stopped it instead.
|
|
822
|
+
const conditionalRoot = joined?.parts
|
|
823
|
+
.flatMap(part => part.tokens)
|
|
824
|
+
.find(token => rootFor(token)?.target === target);
|
|
825
|
+
const message = !joined
|
|
826
|
+
? `this \`className\` is computed, and the codemod reads only a \`clsx\` or \`classnames\` call of class strings and conditional classes; convert this element to bestax \`${target}\` by hand, turning each condition into its prop`
|
|
827
|
+
: conditionalRoot
|
|
828
|
+
? `this \`className\` adds \`.${conditionalRoot}\` only under a condition, so whether this element is a bestax \`${target}\` at all depends on it; convert it by hand if it always is, turning each condition into its prop`
|
|
829
|
+
: `this \`className\` is computed, and the codemod can't write every class it adds onto bestax \`${target}\` as it is; convert this element by hand, turning each condition into its prop`;
|
|
507
830
|
result = {
|
|
508
831
|
conversion: null,
|
|
509
832
|
todos: [
|
|
510
833
|
...result.todos,
|
|
511
|
-
{
|
|
512
|
-
rule: ruleId('dynamic-class', target),
|
|
513
|
-
message: `this \`className\` is computed, and the codemod converts static class strings only; convert this element to bestax \`${target}\` by hand, turning each condition into its prop`,
|
|
514
|
-
},
|
|
834
|
+
{ rule: ruleId('dynamic-class', target), message },
|
|
515
835
|
],
|
|
516
836
|
};
|
|
517
837
|
}
|
|
@@ -631,13 +951,42 @@ export default function transform(fileInfo, api, options = {}) {
|
|
|
631
951
|
findAttr(child, from).name = j.jsxIdentifier(to);
|
|
632
952
|
}
|
|
633
953
|
}
|
|
954
|
+
// A target that renders the element's children itself takes their place:
|
|
955
|
+
// they go, and so does the closing tag, their comments and its own moving
|
|
956
|
+
// to the name that stays.
|
|
957
|
+
const gone = conversion.rendersChildren
|
|
958
|
+
? [
|
|
959
|
+
...(element.children ?? [])
|
|
960
|
+
.filter((node) => node.type === 'JSXElement')
|
|
961
|
+
.flatMap((node) => [
|
|
962
|
+
...(node.comments ?? []),
|
|
963
|
+
...tagComments(node),
|
|
964
|
+
]),
|
|
965
|
+
...[element.closingElement, element.closingElement?.name].flatMap((node) => node?.comments ?? []),
|
|
966
|
+
]
|
|
967
|
+
: [];
|
|
634
968
|
const [head, ...rest] = conversion.target.split('.');
|
|
635
969
|
renameElement(j, element, [ctx.reserve(head), ...rest].join('.'));
|
|
636
970
|
for (const name of conversion.drop) {
|
|
637
971
|
const holder = findAttr(element, name) ? element : child;
|
|
638
972
|
removeAttr(holder, findAttr(holder, name));
|
|
639
973
|
}
|
|
640
|
-
|
|
974
|
+
const joined = joins.get(element);
|
|
975
|
+
if (!joined) {
|
|
976
|
+
writeClassName(j, element, conversion.props, conversion.className);
|
|
977
|
+
}
|
|
978
|
+
else if (writeJoined(j, element, conversion, joined)) {
|
|
979
|
+
unjoined.add(joined.call.callee.name);
|
|
980
|
+
}
|
|
981
|
+
if (conversion.rendersChildren) {
|
|
982
|
+
element.children = [];
|
|
983
|
+
element.openingElement.selfClosing = true;
|
|
984
|
+
element.closingElement = null;
|
|
985
|
+
if (gone.length) {
|
|
986
|
+
const name = element.openingElement.name;
|
|
987
|
+
name.comments = [...(name.comments ?? []), ...gone.map(afterTagName)];
|
|
988
|
+
}
|
|
989
|
+
}
|
|
641
990
|
let written = element;
|
|
642
991
|
if (child) {
|
|
643
992
|
child.openingElement.name = element.openingElement.name;
|
|
@@ -670,6 +1019,34 @@ export default function transform(fileInfo, api, options = {}) {
|
|
|
670
1019
|
}
|
|
671
1020
|
ctx.dirty = true;
|
|
672
1021
|
}
|
|
1022
|
+
// A joiner import only these rewrites stopped using goes too, so the file
|
|
1023
|
+
// gains no unused import; one the file already left unused stays.
|
|
1024
|
+
for (const name of unjoined) {
|
|
1025
|
+
const referenced = root
|
|
1026
|
+
.find(j.Identifier, { name })
|
|
1027
|
+
.filter((identifierPath) => identifierPath.parent.node.type !== 'ImportSpecifier' &&
|
|
1028
|
+
identifierPath.parent.node.type !== 'ImportDefaultSpecifier');
|
|
1029
|
+
if (referenced.size() > 0)
|
|
1030
|
+
continue;
|
|
1031
|
+
root.find(j.ImportDeclaration).forEach((importPath) => {
|
|
1032
|
+
const node = importPath.node;
|
|
1033
|
+
const specifiers = node.specifiers ?? [];
|
|
1034
|
+
const spec = specifiers.find((found) => found.local?.name === name);
|
|
1035
|
+
if (!spec)
|
|
1036
|
+
return;
|
|
1037
|
+
specifiers.splice(specifiers.indexOf(spec), 1);
|
|
1038
|
+
if (specifiers.length > 0)
|
|
1039
|
+
return;
|
|
1040
|
+
// The declaration goes with its last name; its comments (a file
|
|
1041
|
+
// header, a directive) stay, on the statement beside it.
|
|
1042
|
+
const index = program.body.indexOf(node);
|
|
1043
|
+
const neighbour = program.body[index + 1] ?? program.body[index - 1];
|
|
1044
|
+
if (node.comments?.length && neighbour) {
|
|
1045
|
+
neighbour.comments = [...node.comments, ...(neighbour.comments ?? [])];
|
|
1046
|
+
}
|
|
1047
|
+
program.body.splice(index, 1);
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
673
1050
|
// ---- 5. The bestax import ---------------------------------------------------
|
|
674
1051
|
const body = program.body;
|
|
675
1052
|
placeBestaxImport(bestaxImportState, buildBestaxImport(ctx, bestaxImportState), {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bestax-migrate",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.12.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": {
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"chalk": "^6.0.0",
|
|
39
39
|
"commander": "^15.0.0",
|
|
40
40
|
"jscodeshift": "^17.3.0",
|
|
41
|
-
"@allxsmith/bestax-bulma": "^5.16.
|
|
41
|
+
"@allxsmith/bestax-bulma": "^5.16.10"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@jest/globals": "^30.0.0",
|
|
@@ -47,6 +47,8 @@
|
|
|
47
47
|
"@types/node": "^26.3.0",
|
|
48
48
|
"@types/react": "^19.2.18",
|
|
49
49
|
"@types/react-dom": "^19.2.5",
|
|
50
|
+
"classnames": "^2.5.1",
|
|
51
|
+
"clsx": "^2.1.1",
|
|
50
52
|
"jest": "^30.2.0",
|
|
51
53
|
"react": "^19.2.8",
|
|
52
54
|
"react-dom": "^19.2.8",
|