bestax-migrate 2.8.0 → 2.10.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.
@@ -35,8 +35,43 @@ export interface Modifier {
35
35
  * - `todo`: bestax has a component, but the markup does not map element by
36
36
  * element yet; the family's outermost class gets a TODO.
37
37
  * - `plain`: valid Bulma with nothing to convert to; left alone, no TODO.
38
+ * - `fold`: a wrapper the `target` renders itself from a prop, so the element
39
+ * folds into the one `target` inside it (`.table-container` into
40
+ * `Table isResponsive`).
38
41
  */
39
- export type RootStatus = 'mapped' | 'todo' | 'plain';
42
+ export type RootStatus = 'mapped' | 'todo' | 'plain' | 'fold';
43
+ /**
44
+ * An element whose only child the target renders itself (the `<select>`
45
+ * inside `.select`), so the two convert together: the target is written
46
+ * where the child was, and the element goes.
47
+ */
48
+ export interface Absorbs {
49
+ /** The child's tag. */
50
+ readonly tag: string;
51
+ /**
52
+ * Where the target puts the attributes it is given. On the child
53
+ * (`SelectBase`): the child's become the target's, and the element may
54
+ * carry none but a `key`. On the element (`Breadcrumb`): the element's
55
+ * stay, and the child may carry none.
56
+ */
57
+ readonly attributesOn: 'child' | 'element';
58
+ /** The child's classes, as the target's props; any other class refuses. */
59
+ readonly modifiers?: Readonly<Record<string, Modifier>>;
60
+ /**
61
+ * A bare child attribute that one of the element's modifiers writes as a
62
+ * prop (`multiple`, beside `is-multiple`). Each converts only beside the
63
+ * other, and the attribute goes, since the prop renders it.
64
+ */
65
+ readonly pairs?: Readonly<Record<string, string>>;
66
+ /**
67
+ * A child attribute the target reads under another name, only beside a
68
+ * paired attribute (`size` as `multipleSize`, beside `multiple`).
69
+ */
70
+ readonly renames?: Readonly<Record<string, {
71
+ readonly to: string;
72
+ readonly beside: string;
73
+ }>>;
74
+ }
40
75
  export interface RootEntry {
41
76
  readonly status: RootStatus;
42
77
  /**
@@ -89,6 +124,13 @@ export interface RootEntry {
89
124
  * takes a generated `id` when the element has none of its own.
90
125
  */
91
126
  readonly adoptsIdFrom?: readonly string[];
127
+ /**
128
+ * For a `fold` root: the props that render it on the target inside. Its
129
+ * modifiers add their props beside these.
130
+ */
131
+ readonly folds?: readonly PropWrite[];
132
+ /** The target renders the element's only child itself. */
133
+ readonly absorbs?: Absorbs;
92
134
  /** The target takes no helper props, so every helper class stays a class. */
93
135
  readonly noHelpers?: boolean;
94
136
  /**
@@ -97,8 +139,10 @@ export interface RootEntry {
97
139
  */
98
140
  readonly ownClassOnly?: boolean;
99
141
  /**
100
- * Attributes the target types as numbers. A numeric string (`value="40"`)
101
- * becomes a number, which renders the same; any other string refuses.
142
+ * Attributes the target types as numbers, among the ones it is given (the
143
+ * child's, when it `absorbs` the child and puts them there). A numeric
144
+ * string (`value="40"`) becomes a number, which renders the same; any
145
+ * other string refuses.
102
146
  */
103
147
  readonly numberAttrs?: readonly string[];
104
148
  /** Attributes the target drops: on these tags, or on all tags except these. */
@@ -1 +1 @@
1
- {"version":3,"file":"class-map.d.ts","sourceRoot":"","sources":["../../../src/sources/bulma-classes/class-map.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"class-map.d.ts","sourceRoot":"","sources":["../../../src/sources/bulma-classes/class-map.ts"],"names":[],"mappings":"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"}
@@ -185,10 +185,13 @@ const flag = (prop) => ({ writes: [{ prop }] });
185
185
  const set = (prop, value) => ({
186
186
  writes: [{ prop, value }],
187
187
  });
188
- /** `prefix + n` → `prop={n}` for `n` from 1 to `max`, for a prop typed as a number. */
189
- function numbers(prefix, max, prop) {
188
+ /**
189
+ * `prefix + n + suffix` → `prop={n}` for `n` from 1 to `max`, for a prop
190
+ * typed as a number.
191
+ */
192
+ function numbers(prefix, max, prop, suffix = '') {
190
193
  return Object.fromEntries(Array.from({ length: max }, (_, i) => String(i + 1)).map(value => [
191
- `${prefix}${value}`,
194
+ `${prefix}${value}${suffix}`,
192
195
  { writes: [{ prop, value, numeric: true }] },
193
196
  ]));
194
197
  }
@@ -771,6 +774,55 @@ export const ROOTS = {
771
774
  ],
772
775
  passThrough: ['disabled', 'readOnly', 'rows', 'ref'],
773
776
  },
777
+ // `SelectBase` renders `.select` and the `<select>` inside it together, and
778
+ // gives the `<select>` every attribute it is given.
779
+ select: {
780
+ ...BASE,
781
+ target: 'SelectBase',
782
+ tag: 'div',
783
+ textColor: null,
784
+ bgColor: 'backgroundColor',
785
+ modifiers: {
786
+ ...tokens('is-', ['small', 'medium', 'large'], 'size'),
787
+ ...flags({
788
+ 'is-rounded': 'isRounded',
789
+ 'is-loading': 'isLoading',
790
+ 'is-active': 'isActive',
791
+ 'is-fullwidth': 'isFullwidth',
792
+ 'is-multiple': 'multiple',
793
+ }),
794
+ },
795
+ omits: Object.fromEntries(COMPONENT_COLORS.map(color => [
796
+ `is-${color}`,
797
+ '`color` renders `has-text-<color>` on the select as well',
798
+ ])),
799
+ absorbs: {
800
+ tag: 'select',
801
+ attributesOn: 'child',
802
+ modifiers: flags({
803
+ 'is-hovered': 'isHovered',
804
+ 'is-focused': 'isFocused',
805
+ }),
806
+ pairs: { multiple: 'is-multiple' },
807
+ renames: { size: { to: 'multipleSize', beside: 'multiple' } },
808
+ },
809
+ numberAttrs: ['multipleSize'],
810
+ adoptsIdFrom: ['Field'],
811
+ ownProps: [
812
+ 'color',
813
+ 'size',
814
+ 'isRounded',
815
+ 'isLoading',
816
+ 'isActive',
817
+ 'isHovered',
818
+ 'isFocused',
819
+ 'isFullwidth',
820
+ 'isFullWidth',
821
+ 'multiple',
822
+ 'multipleSize',
823
+ ],
824
+ passThrough: ['disabled', 'ref'],
825
+ },
774
826
  footer: {
775
827
  ...BASE,
776
828
  target: 'Footer',
@@ -1085,14 +1137,29 @@ export const ROOTS = {
1085
1137
  },
1086
1138
  ownProps: ['size', 'isSpaced', 'as', 'hasSkeleton', 'textColor', 'bgColor'],
1087
1139
  },
1140
+ // `Breadcrumb` renders the bare `<ul>` inside it, and its `aria-label`
1141
+ // comes before the attributes it is given, which replace it.
1142
+ breadcrumb: {
1143
+ ...BASE,
1144
+ target: 'Breadcrumb',
1145
+ tag: 'nav',
1146
+ // Breadcrumb types no color prop at all.
1147
+ textColor: null,
1148
+ bgColor: null,
1149
+ modifiers: {
1150
+ ...tokens('is-', ['centered', 'right'], 'alignment'),
1151
+ ...tokens('has-', ['arrow', 'bullet', 'dot', 'succeeds'], 'separator', '-separator'),
1152
+ ...tokens('is-', ['small', 'medium', 'large'], 'size'),
1153
+ },
1154
+ defaults: { 'aria-label': 'breadcrumbs' },
1155
+ absorbs: { tag: 'ul', attributesOn: 'element' },
1156
+ ownProps: ['alignment', 'separator', 'size'],
1157
+ },
1088
1158
  // ---- Families bestax has but that do not map element by element yet ------
1089
- breadcrumb: todo('Breadcrumb', 'bestax `Breadcrumb` renders the `<ul>` itself and adds an `aria-label`'),
1090
- 'fixed-grid': todo('Grid', '`Grid isFixed` renders the `.fixed-grid` wrapper itself, from `fixedCols`; replace the wrapper and its `.grid` with one `Grid`, by hand'),
1091
1159
  checkbox: todo('Checkbox', "bestax `Checkbox` renders its own styled markup, not Bulma's"),
1092
1160
  checkboxes: todo('Checkboxes', 'bestax `Checkboxes` renders its own `.field` and `.control` wrappers'),
1093
1161
  radio: todo('Radio', "bestax `Radio` renders its own styled markup, not Bulma's"),
1094
1162
  radios: todo('Radios', 'bestax `Radios` renders its own `.field` and `.control` wrappers'),
1095
- select: todo('Select', 'bestax `Select` renders the `.select` wrapper and the `<select>` together'),
1096
1163
  file: todo('File', 'bestax `File` renders the whole `.file-label` tree itself'),
1097
1164
  'file-label': part(),
1098
1165
  'file-input': part(),
@@ -1138,9 +1205,28 @@ export const ROOTS = {
1138
1205
  'panel-icon': part(),
1139
1206
  'panel-list': part(),
1140
1207
  tabs: todo('Tabs', "bestax `Tabs` renders each tab's `<li>` and `<a>` together, with tab roles"),
1141
- 'table-container': todo('Table', 'bestax renders `.table-container` from `Table isResponsive`'),
1142
1208
  'skeleton-block': todo('Skeleton', 'bestax `Skeleton` renders its own markup'),
1143
1209
  'skeleton-lines': todo('Skeleton', 'bestax `Skeleton` renders its own markup'),
1210
+ // ---- Wrappers a component renders from a prop ---------------------------------
1211
+ // Each renders the wrapper with its own class and nothing else, so the
1212
+ // element folds only when it carries no attribute and no other class.
1213
+ 'table-container': {
1214
+ status: 'fold',
1215
+ target: 'Table',
1216
+ tag: 'div',
1217
+ folds: [{ prop: 'isResponsive' }],
1218
+ },
1219
+ 'fixed-grid': {
1220
+ status: 'fold',
1221
+ target: 'Grid',
1222
+ tag: 'div',
1223
+ folds: [{ prop: 'isFixed' }],
1224
+ modifiers: {
1225
+ ...numbers('has-', 12, 'fixedCols', '-cols'),
1226
+ ...Object.assign({}, ...SIZE_VIEWPORTS.map(viewport => numbers('has-', 12, `fixedCols${viewportSuffix(viewport)}`, `-cols-${viewport}`))),
1227
+ 'has-auto-count': set('fixedCols', 'auto'),
1228
+ },
1229
+ },
1144
1230
  // ---- Valid Bulma, nothing to convert to --------------------------------------
1145
1231
  // The other sources emit these on purpose where bestax has no component.
1146
1232
  help: plain('bestax renders `.help` only inside its form controls'),
@@ -1236,6 +1322,7 @@ export const FORWARDS_REF = [
1236
1322
  'Control',
1237
1323
  'InputBase',
1238
1324
  'TextAreaBase',
1325
+ 'SelectBase',
1239
1326
  ];
1240
1327
  /**
1241
1328
  * Plain tags bestax wraps, for an element with helper classes and no root
@@ -1387,8 +1474,8 @@ export const PASSTHROUGH = [
1387
1474
  match: /^(?:is-(?:col|row)-.+|is-(?:column|row)-gap-\d+|is-gap-\d+|is-auto-fill)$/,
1388
1475
  },
1389
1476
  {
1390
- why: 'a `.fixed-grid` column count, which stays with its `.fixed-grid` (a family this source leaves as markup)',
1391
- match: /^has-\d+-cols(?:-.+)?$/,
1477
+ why: 'a `.fixed-grid` column count, which converts only on its own `.fixed-grid`',
1478
+ match: /^(?:has-\d+-cols(?:-.+)?|has-auto-count)$/,
1392
1479
  },
1393
1480
  {
1394
1481
  why: 'an `.image` modifier; `.image` stays as markup',
@@ -1398,9 +1485,17 @@ export const PASSTHROUGH = [
1398
1485
  why: 'a Bulma helper with no bestax prop',
1399
1486
  match: /^(?:is-display-.+|is-visibility-.+|is-overflow-.+|is-position-.+|is-float-.+|is-clear-.+|has-radius-.+|has-text-weight-extrabold|is-align-content-(?:baseline|start|end)|is-align-items-self-(?:start|end)|is-offset-0(?:-[a-z]+)?)$/,
1400
1487
  },
1488
+ {
1489
+ why: 'a `.breadcrumb` separator, which converts only on its own `.breadcrumb`',
1490
+ match: /^has-[a-z]+-separator$/,
1491
+ },
1492
+ {
1493
+ why: "`.select`'s `is-multiple`, which converts only on its own `.select`, around a `<select multiple>`",
1494
+ match: /^is-multiple$/,
1495
+ },
1401
1496
  {
1402
1497
  why: 'a modifier of markup this source leaves alone, or one bestax has no prop for',
1403
- match: /^(?:has-addons-.+|has-[a-z]+-separator|has-dropdown(?:-up)?|has-fixed-size|has-icons-(?:left|right)|has-name|has-(?:spaced-)?navbar-fixed-(?:top|bottom)(?:-desktop)?|has-shadow|has-auto-count|is-arrowless|is-boxed|is-center|is-current|is-delete|is-disabled|is-empty|is-expanded|is-fixed-(?:top|bottom)(?:-desktop)?|is-flexible|is-grouped(?:-.+)?|is-halfheight|is-horizontal|is-left|is-(?:lower|upper)-(?:alpha|roman)|is-multiple|is-responsive|is-selected|is-tab|is-toggle(?:-rounded)?|is-transparent|is-underlined|is-up|is-wrapped)$/,
1498
+ match: /^(?:has-addons-.+|has-dropdown(?:-up)?|has-fixed-size|has-icons-(?:left|right)|has-name|has-(?:spaced-)?navbar-fixed-(?:top|bottom)(?:-desktop)?|has-shadow|is-arrowless|is-boxed|is-center|is-current|is-delete|is-disabled|is-empty|is-expanded|is-fixed-(?:top|bottom)(?:-desktop)?|is-flexible|is-grouped(?:-.+)?|is-halfheight|is-horizontal|is-left|is-(?:lower|upper)-(?:alpha|roman)|is-responsive|is-selected|is-tab|is-toggle(?:-rounded)?|is-transparent|is-underlined|is-up|is-wrapped)$/,
1404
1499
  },
1405
1500
  ];
1406
1501
  /**
@@ -28,6 +28,16 @@ export interface ElementFacts {
28
28
  * counts: one in an expression may not render.
29
29
  */
30
30
  childTargets?: readonly string[];
31
+ /**
32
+ * What the element's only child converts to, when the element holds one
33
+ * element and nothing else (not even whitespace React would render).
34
+ */
35
+ soleChildTarget?: string;
36
+ /**
37
+ * That only child as written, when it is a plain HTML element: what an
38
+ * entry that `absorbs` it reads.
39
+ */
40
+ soleChild?: ChildFacts;
31
41
  /** The bestax components already in the file inside this element. */
32
42
  bestaxInside?: readonly string[];
33
43
  /** The bestax components already in the file around this element. */
@@ -44,6 +54,20 @@ export interface ElementFacts {
44
54
  */
45
55
  onlyChildOf?: string;
46
56
  }
57
+ export interface ChildFacts {
58
+ tag: string;
59
+ /**
60
+ * Its static classes, or null when its `className` is computed. Left out
61
+ * when it has no `className` at all: an empty one still renders `class=""`.
62
+ */
63
+ tokens?: readonly string[] | null;
64
+ /**
65
+ * Its attributes but `className`, read as `ElementFacts.attributes` is,
66
+ * except that a number literal (`size={3}`) is that number.
67
+ */
68
+ attributes: ReadonlyMap<string, string | number | true | null>;
69
+ hasSpread: boolean;
70
+ }
47
71
  export interface Todo {
48
72
  rule: string;
49
73
  message: string;
@@ -62,9 +86,31 @@ export interface Conversion {
62
86
  * a number (`minCol={4}`), because the target types them as numbers.
63
87
  */
64
88
  numbers: string[];
89
+ /**
90
+ * The element's only child, which the target renders itself: the target is
91
+ * written in the child's place, with the element's props and attributes
92
+ * first, then the child's, and the element goes.
93
+ */
94
+ absorbs?: {
95
+ /** What the child's classes become, in place of its `className`. */
96
+ props: Array<[name: string, value: string | true]>;
97
+ /** Child attributes the target reads under another name. */
98
+ renames: Array<[from: string, to: string]>;
99
+ /** Child attributes a prop above renders instead. */
100
+ drop: string[];
101
+ };
65
102
  }
66
103
  export interface Plan {
67
104
  conversion: Conversion | null;
105
+ /**
106
+ * The element is a wrapper its only child's component renders from these
107
+ * props: they join that child's conversion, and the element goes.
108
+ */
109
+ fold?: {
110
+ target: string;
111
+ props: Array<[name: string, value: string | true]>;
112
+ numbers: string[];
113
+ };
68
114
  todos: Todo[];
69
115
  }
70
116
  export declare function plan(facts: ElementFacts): Plan;
@@ -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,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,IAAI;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,UAAU;IACzB,0CAA0C;IAC1C,MAAM,EAAE,MAAM,CAAC;IACf,6DAA6D;IAC7D,KAAK,EAAE,KAAK,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC;IACnD,4DAA4D;IAC5D,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,mEAAmE;IACnE,IAAI,EAAE,MAAM,EAAE,CAAC;IACf;;;OAGG;IACH,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,IAAI;IACnB,UAAU,EAAE,UAAU,GAAG,IAAI,CAAC;IAC9B,KAAK,EAAE,IAAI,EAAE,CAAC;CACf;AA+BD,wBAAgB,IAAI,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI,CA+R9C"}
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;CACpB;AAED,MAAM,WAAW,IAAI;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,UAAU;IACzB,0CAA0C;IAC1C,MAAM,EAAE,MAAM,CAAC;IACf,6DAA6D;IAC7D,KAAK,EAAE,KAAK,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC;IACnD,4DAA4D;IAC5D,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,mEAAmE;IACnE,IAAI,EAAE,MAAM,EAAE,CAAC;IACf;;;OAGG;IACH,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB;;;;OAIG;IACH,OAAO,CAAC,EAAE;QACR,oEAAoE;QACpE,KAAK,EAAE,KAAK,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC;QACnD,4DAA4D;QAC5D,OAAO,EAAE,KAAK,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC;QAC3C,qDAAqD;QACrD,IAAI,EAAE,MAAM,EAAE,CAAC;KAChB,CAAC;CACH;AAED,MAAM,WAAW,IAAI;IACnB,UAAU,EAAE,UAAU,GAAG,IAAI,CAAC;IAC9B;;;OAGG;IACH,IAAI,CAAC,EAAE;QACL,MAAM,EAAE,MAAM,CAAC;QACf,KAAK,EAAE,KAAK,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC;QACnD,OAAO,EAAE,MAAM,EAAE,CAAC;KACnB,CAAC;IACF,KAAK,EAAE,IAAI,EAAE,CAAC;CACf;AA+BD,wBAAgB,IAAI,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI,CA0S9C"}
@@ -61,6 +61,9 @@ export function plan(facts) {
61
61
  });
62
62
  return { conversion: null, todos };
63
63
  }
64
+ const wrapper = tokens.find(token => rootFor(token)?.status === 'fold');
65
+ if (wrapper)
66
+ return planFold(facts, wrapper, todos);
64
67
  const root = tokens
65
68
  .filter(token => rootFor(token)?.status === 'mapped')
66
69
  .sort((a, b) => precedence(a) - precedence(b))[0];
@@ -93,21 +96,34 @@ export function plan(facts) {
93
96
  if (facts.onlyChildOf) {
94
97
  return refuse('only-child', target, `this element is the only child of \`<${facts.onlyChildOf}>\`, which may hand it props or a ref with \`cloneElement\` (next/link's legacy behavior, a tooltip, a Radix \`asChild\` trigger) that bestax \`${target}\` would not take the same way; convert it by hand if \`<${facts.onlyChildOf}>\` only renders its children`);
95
98
  }
96
- if (facts.attributes.has('dangerouslySetInnerHTML')) {
99
+ // The attributes the target is given: the element's, or its only child's
100
+ // when the target renders that child itself and puts them there.
101
+ let attributes = facts.attributes;
102
+ let absorbed;
103
+ if (entry.absorbs) {
104
+ const outcome = absorb(facts, root, entry, target, refuse);
105
+ if ('todos' in outcome)
106
+ return outcome;
107
+ ({ attributes, absorbed } = outcome);
108
+ }
109
+ const renamed = new Set(absorbed?.renames.map(([, to]) => to));
110
+ if (attributes.has('dangerouslySetInnerHTML')) {
97
111
  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`);
98
112
  }
99
- for (const name of facts.attributes.keys()) {
113
+ for (const name of attributes.keys()) {
114
+ if (renamed.has(name))
115
+ continue;
100
116
  const readAsProp = entry.ownProps?.includes(name) || HELPER_PROPS.has(name);
101
117
  if (readAsProp && !entry.passThrough?.includes(name)) {
102
118
  return refuse('attr', name, `\`${name}\` is also a bestax \`${target}\` prop, which would read it differently; rename or drop the attribute, then re-run`);
103
119
  }
104
120
  }
105
- const missing = Object.entries(entry.defaults ?? {}).filter(([name]) => !facts.attributes.has(name));
121
+ const missing = Object.entries(entry.defaults ?? {}).filter(([name]) => !attributes.has(name));
106
122
  const drop = [];
107
123
  for (const name of entry.untypedAttrs ?? []) {
108
- if (!facts.attributes.has(name))
124
+ if (!attributes.has(name))
109
125
  continue;
110
- if (entry.defaults?.[name] === facts.attributes.get(name)) {
126
+ if (entry.defaults?.[name] === attributes.get(name)) {
111
127
  drop.push(name);
112
128
  continue;
113
129
  }
@@ -115,7 +131,7 @@ export function plan(facts) {
115
131
  }
116
132
  const numbers = [];
117
133
  for (const name of entry.numberAttrs ?? []) {
118
- const value = facts.attributes.get(name);
134
+ const value = attributes.get(name);
119
135
  if (typeof value !== 'string')
120
136
  continue;
121
137
  // Only a string that is already the number's own spelling: `040`, `1.50`
@@ -131,7 +147,7 @@ export function plan(facts) {
131
147
  if (entry.providesContext && facts.bestaxInside?.length) {
132
148
  return refuse('context', target, `bestax \`${target}\` tells the bestax form controls inside it to skip wrappers of their own, and this element already holds \`${facts.bestaxInside[0]}\`, which could render differently inside it; keep this element as markup, or convert it and check that component by hand`);
133
149
  }
134
- if (entry.adoptsIdFrom && !facts.attributes.has('id')) {
150
+ if (entry.adoptsIdFrom && !attributes.has('id')) {
135
151
  // Context follows the render tree, not the file: a component of the app
136
152
  // around the element can render a labelled `Field` around it just as well.
137
153
  const around = facts.bestaxAround?.find(name => entry.adoptsIdFrom.includes(name)) ??
@@ -145,18 +161,14 @@ export function plan(facts) {
145
161
  (!wraps.when || tokens.includes(wraps.when)) &&
146
162
  (facts.hasChildren || wraps.whenEmpty) &&
147
163
  !facts.childTargets?.some(child => wraps.unless.includes(child))) {
148
- const parts = wraps.unless.map(part => `\`${part}\``);
149
- const list = parts.length > 1
150
- ? `${parts.slice(0, -1).join(', ')} or ${parts[parts.length - 1]}`
151
- : parts[0];
152
- return refuse('children', target, `bestax \`${target}\` renders its children inside a \`.${wraps.in}\` of its own unless one of them is a ${list}, so this element stays markup`);
164
+ 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`);
153
165
  }
154
166
  if (missing.length > 0) {
155
167
  const list = missing.map(([name, value]) => `\`${name}="${value}"\``);
156
168
  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`);
157
169
  }
158
170
  for (const [name, tags] of Object.entries(entry.dropsAttr ?? {})) {
159
- if (facts.attributes.has(name) && inTagSet(tags, tag)) {
171
+ if (attributes.has(name) && inTagSet(tags, tag)) {
160
172
  // Bulma greys out `.button[disabled]` on any tag, so there it is not
161
173
  // inert: dropping it changes how the element looks.
162
174
  const visible = name === 'disabled';
@@ -253,7 +265,163 @@ export function plan(facts) {
253
265
  className: rest.length > 0 ? rest.join(' ') : null,
254
266
  drop,
255
267
  numbers,
268
+ ...(absorbed ? { absorbs: absorbed } : {}),
256
269
  },
257
270
  todos,
258
271
  };
259
272
  }
273
+ /** `a`, `a or b`, `a, b or c`, each in backticks. */
274
+ function orList(names) {
275
+ const quoted = names.map(name => `\`${name}\``);
276
+ return quoted.length > 1
277
+ ? `${quoted.slice(0, -1).join(', ')} or ${quoted[quoted.length - 1]}`
278
+ : quoted[0];
279
+ }
280
+ /**
281
+ * The only child an entry's target renders itself (`.select`'s `<select>`):
282
+ * what its classes become, and the attributes the target is given. It
283
+ * converts with the element only when the target would render it exactly
284
+ * as written.
285
+ */
286
+ function absorb(facts, root, entry, target, refuse) {
287
+ const spec = entry.absorbs;
288
+ const child = facts.soleChild;
289
+ const itself = `bestax \`${target}\` renders the <${spec.tag}> inside \`.${root}\` itself`;
290
+ if (child?.tag !== spec.tag) {
291
+ return refuse('children', target, `${itself}, so this element converts only around a single <${spec.tag}>, with nothing else beside it`);
292
+ }
293
+ if (child.hasSpread) {
294
+ 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
+ }
296
+ if (child.tokens === null) {
297
+ return refuse('dynamic-class', target, `the <${spec.tag}> inside has a computed \`className\`, and the codemod converts static class strings only; convert the two to bestax \`${target}\` by hand, turning each condition into its prop`);
298
+ }
299
+ if (child.tokens?.length === 0) {
300
+ 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`);
301
+ }
302
+ const modifiers = spec.modifiers ?? {};
303
+ const props = [];
304
+ for (const token of child.tokens ?? []) {
305
+ const modifier = Object.hasOwn(modifiers, token)
306
+ ? modifiers[token]
307
+ : undefined;
308
+ if (!modifier) {
309
+ const allowed = Object.keys(modifiers);
310
+ return refuse('attr', 'className', `${itself}, with no class on it${allowed.length > 0 ? ` but ${orList(allowed)}` : ''}, so its \`${token}\` would be lost; keep this element as markup`);
311
+ }
312
+ for (const write of modifier.writes) {
313
+ props.push([write.prop, write.value ?? true]);
314
+ }
315
+ }
316
+ if (spec.attributesOn === 'element') {
317
+ const name = [...child.attributes.keys()][0];
318
+ if (name !== undefined) {
319
+ return refuse('attr', name, `${itself}, with no attributes, so its \`${name}\` would be lost; keep this element as markup`);
320
+ }
321
+ return {
322
+ attributes: facts.attributes,
323
+ absorbed: { props, renames: [], drop: [] },
324
+ };
325
+ }
326
+ // The target puts what it is given on the child, so the element can carry
327
+ // nothing but its `key`, which stays with the component in its place.
328
+ const own = [...facts.attributes.keys()].find(name => name !== 'key');
329
+ if (own !== undefined) {
330
+ return refuse('attr', own, `bestax \`${target}\` puts the attributes it is given on the <${spec.tag}> inside \`.${root}\`, so this element's \`${own}\` would move there; move it onto the <${spec.tag}> if that is what you want, then re-run`);
331
+ }
332
+ if (child.attributes.has('key')) {
333
+ return refuse('attr', 'key', `the <${spec.tag}> inside has a \`key\`, which would move up to bestax \`${target}\` in place of this element's; keep this element as markup`);
334
+ }
335
+ const drop = [];
336
+ for (const [name, token] of Object.entries(spec.pairs ?? {})) {
337
+ const withClass = facts.tokens.includes(token);
338
+ if (withClass && child.attributes.get(name) === true) {
339
+ drop.push(name);
340
+ }
341
+ else if (withClass || child.attributes.has(name)) {
342
+ return refuse('attr', name, `bestax \`${target}\` renders \`${name}\` on the <${spec.tag}> and \`.${token}\` on this element together, from one prop, so the two convert only as a pair, with \`${name}\` written bare; keep this element as markup`);
343
+ }
344
+ }
345
+ // A number literal is carried over as written, like any other expression.
346
+ const given = (value) => typeof value === 'number' ? null : value;
347
+ const attributes = new Map();
348
+ const renames = [];
349
+ for (const [name, value] of child.attributes) {
350
+ if (drop.includes(name))
351
+ continue;
352
+ const rename = spec.renames && Object.hasOwn(spec.renames, name)
353
+ ? spec.renames[name]
354
+ : undefined;
355
+ if (!rename) {
356
+ attributes.set(name, given(value));
357
+ continue;
358
+ }
359
+ // `SelectBase` writes `multipleSize` back as `size` only when it holds a
360
+ // number, and an expression may not: only a literal converts. A string
361
+ // one is held to a number's spelling below, with the target's numbers.
362
+ if (!drop.includes(rename.beside) ||
363
+ (typeof value !== 'number' && typeof value !== 'string')) {
364
+ return refuse('attr', name, `bestax \`${target}\` writes the <${spec.tag}>'s \`${name}\` only beside \`${rename.beside}\`, and only from a number in \`${rename.to}\`, so it converts only as a number written out (\`${name}={4}\`); keep this element as markup, or convert it by hand if \`${name}\` is always a number`);
365
+ }
366
+ if (child.attributes.has(rename.to)) {
367
+ return refuse('attr', rename.to, `\`${rename.to}\` is also a bestax \`${target}\` prop, which would read it differently; rename or drop the attribute, then re-run`);
368
+ }
369
+ renames.push([name, rename.to]);
370
+ attributes.set(rename.to, given(value));
371
+ }
372
+ return { attributes, absorbed: { props, renames, drop } };
373
+ }
374
+ /**
375
+ * A wrapper the component inside it renders from a prop (`.table-container`
376
+ * from `Table isResponsive`). It folds into that component only when it is
377
+ * exactly what the component renders for it: its own tag and class, its own
378
+ * modifiers, no attribute, and one child that becomes the component.
379
+ */
380
+ function planFold(facts, wrapper, todos) {
381
+ const entry = rootFor(wrapper);
382
+ const target = entry.target;
383
+ if (!target)
384
+ return { conversion: null, todos };
385
+ const folds = entry.folds ?? [];
386
+ const renders = `\`${target} ${folds.map(write => write.prop).join(' ')}\``;
387
+ const refuse = (kind, token, message) => ({
388
+ conversion: null,
389
+ todos: [...todos, { rule: ruleId(kind, token), message }],
390
+ });
391
+ if (facts.tag !== entry.tag) {
392
+ return refuse('tag', target, `bestax ${renders} renders \`.${wrapper}\` on a <${entry.tag}>, not a <${facts.tag}>; keep the markup, or change the tag and re-run`);
393
+ }
394
+ if (facts.hasSpread) {
395
+ return refuse('spread', target, `this element spreads props, and bestax ${renders} renders \`.${wrapper}\` with none of its own; keep it as markup`);
396
+ }
397
+ if (facts.onlyChildOf) {
398
+ return refuse('only-child', target, `this element is the only child of \`<${facts.onlyChildOf}>\`, which may hand it props or a ref with \`cloneElement\`, and folding it would move those onto the \`${target}\` inside; convert it by hand if \`<${facts.onlyChildOf}>\` only renders its children`);
399
+ }
400
+ const attribute = [...facts.attributes.keys()][0];
401
+ if (attribute) {
402
+ return refuse('attr', attribute, `bestax ${renders} renders \`.${wrapper}\` with no attributes, so this element's \`${attribute}\` would be lost; keep it as markup`);
403
+ }
404
+ const props = folds.map(write => [
405
+ write.prop,
406
+ write.value ?? true,
407
+ ]);
408
+ const numbers = [];
409
+ for (const token of facts.tokens) {
410
+ if (token === wrapper)
411
+ continue;
412
+ const modifier = modifierFor(entry, token);
413
+ const taken = modifier?.writes.some(write => props.some(([prop]) => prop === write.prop));
414
+ if (!modifier || taken) {
415
+ return refuse('attr', 'className', `bestax ${renders} renders \`.${wrapper}\` with no class but its own and one of each of its modifiers, so \`${token}\` would be lost; keep this element as markup`);
416
+ }
417
+ for (const write of modifier.writes) {
418
+ props.push([write.prop, write.value ?? true]);
419
+ if (write.numeric)
420
+ numbers.push(write.prop);
421
+ }
422
+ }
423
+ if (facts.soleChildTarget !== target) {
424
+ return refuse('children', target, `bestax ${renders} renders \`.${wrapper}\` itself, so this element converts only around a single element that becomes a \`${target}\`, with nothing else beside it`);
425
+ }
426
+ return { conversion: null, fold: { target, props, numbers }, todos };
427
+ }
@@ -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;AAuBH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEnD;AAED,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAK1D"}
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;AAyBH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEnD;AAED,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAK1D"}
@@ -44,6 +44,8 @@ const VOCABULARY = new Set([
44
44
  'dangerouslySetInnerHTML',
45
45
  // Refused where the target drops its own class for one it is given.
46
46
  'className',
47
+ // Refused on the child a target renders itself.
48
+ 'key',
47
49
  ]);
48
50
  export function inVocabulary(token) {
49
51
  return VOCABULARY.has(token);
@@ -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;AAmSvD,MAAM,CAAC,OAAO,UAAU,SAAS,CAC/B,QAAQ,EAAE,QAAQ,EAClB,GAAG,EAAE,GAAG,EACR,OAAO,GAAE,gBAAqB,GAC7B,MAAM,GAAG,SAAS,CA8VpB"}
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;AA8YvD,MAAM,CAAC,OAAO,UAAU,SAAS,CAC/B,QAAQ,EAAE,QAAQ,EAClB,GAAG,EAAE,GAAG,EACR,OAAO,GAAE,gBAAqB,GAC7B,MAAM,GAAG,SAAS,CAwZpB"}
@@ -17,7 +17,7 @@
17
17
  * 5. write the bestax import, after the file's last import
18
18
  */
19
19
  import path from 'node:path';
20
- import { addTodo, findAttr, forgetJsxParens, jsxNameParts, literalValueOf, makeAttr, renameElement, reprintDirectives, } from '../_shared/jsx-utils.js';
20
+ import { addTodo, findAttr, forgetJsxParens, jsxNameParts, literalValueOf, makeAttr, removeAttr, renameElement, reprintDirectives, } from '../_shared/jsx-utils.js';
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';
@@ -47,6 +47,43 @@ const JSX_FACTORY = /^\s*\*?\s*@jsx\s+(\S+)/m;
47
47
  const HEADER_PRAGMA = /^[\s*]*(?:@ts-(?:no)?check|@flow|@noflow|eslint-disable(?!-)|@jsx|@jsxImportSource|@jsxRuntime|@license|@preserve)\b/;
48
48
  /** Directives that act on the line after them, so must stay above it. */
49
49
  const NEXT_LINE_DIRECTIVE = /^[\s*]*(?:eslint-disable-next-line|@ts-expect-error|@ts-ignore|prettier-ignore)\b/;
50
+ /**
51
+ * A comment moved to sit after a JSX tag name. It has to be a block comment
52
+ * there: recast prints a line comment as `<// …`, which TypeScript reads as a
53
+ * closing tag.
54
+ */
55
+ function afterTagName(comment) {
56
+ if (comment.type !== 'CommentLine') {
57
+ return { ...comment, leading: false, trailing: true };
58
+ }
59
+ return {
60
+ type: 'CommentBlock',
61
+ value: ` ${comment.value.trim().replace(/\*\//g, '*\\/')} `,
62
+ leading: false,
63
+ trailing: true,
64
+ };
65
+ }
66
+ /**
67
+ * Put `props`, then what stays in `className`, where an element's
68
+ * `className` was. A comment on the className goes with whatever takes its
69
+ * place: the first new attribute, else the next one along, else the tag name.
70
+ */
71
+ function writeClassName(j, element, props, className) {
72
+ const attrs = element.openingElement.attributes;
73
+ const classAttr = findAttr(element, 'className');
74
+ const replacement = props.map(([name, value]) => makeAttr(j, name, value === true ? undefined : value));
75
+ if (className)
76
+ replacement.push(makeAttr(j, 'className', className));
77
+ if (classAttr.comments?.length) {
78
+ const name = element.openingElement.name;
79
+ const carrier = replacement[0] ?? attrs.find((attr) => attr !== classAttr) ?? name;
80
+ carrier.comments = [
81
+ ...(carrier.comments ?? []),
82
+ ...classAttr.comments.map((comment) => carrier === name ? afterTagName(comment) : comment),
83
+ ];
84
+ }
85
+ attrs.splice(attrs.indexOf(classAttr), 1, ...replacement);
86
+ }
50
87
  /** A lowercase intrinsic tag: not a component, not a custom element. */
51
88
  const INTRINSIC = /^[a-z][a-z0-9]*$/;
52
89
  /** Subtrees whose tags are not HTML. */
@@ -126,6 +163,34 @@ function attributeName(attr) {
126
163
  ? `${attr.name.namespace.name}:${attr.name.name.name}`
127
164
  : attr.name.name;
128
165
  }
166
+ /** An element's attributes but `className`, as the planner reads them. */
167
+ function attributesBesides(element, classAttr, read) {
168
+ const attributes = new Map();
169
+ for (const attr of element.openingElement.attributes ?? []) {
170
+ if (attr.type !== 'JSXAttribute' || attr === classAttr)
171
+ continue;
172
+ attributes.set(attributeName(attr), read(attr));
173
+ }
174
+ return attributes;
175
+ }
176
+ /** A child's attribute as `ChildFacts` reads it: a number literal is that number. */
177
+ function childAttributeValue(attr) {
178
+ const literal = literalValueOf(attr);
179
+ return literal.kind === 'number' ? literal.value : attributeValue(attr);
180
+ }
181
+ function hasSpread(element) {
182
+ return (element.openingElement.attributes ?? []).some((attr) => attr.type === 'JSXSpreadAttribute');
183
+ }
184
+ /** The comments inside an element's tags, on the tags and their names. */
185
+ function tagComments(element) {
186
+ const { openingElement, closingElement } = element;
187
+ return [
188
+ openingElement,
189
+ openingElement.name,
190
+ closingElement,
191
+ closingElement?.name,
192
+ ].flatMap((node) => node?.comments ?? []);
193
+ }
129
194
  /** Every comment in the file (recast hangs them on the nodes). */
130
195
  function commentsOf(root, j) {
131
196
  const comments = new Set();
@@ -229,6 +294,25 @@ function reachesReact(child) {
229
294
  return isContent(child);
230
295
  return /[^ \t\r\n]/.test(child.value) || !/[\r\n]/.test(child.value);
231
296
  }
297
+ /** A plain HTML child as the planner reads it; nothing for a component. */
298
+ function childFacts(child) {
299
+ const name = child.openingElement.name;
300
+ if (name.type !== 'JSXIdentifier' || !INTRINSIC.test(name.name)) {
301
+ return undefined;
302
+ }
303
+ const classAttr = findAttr(child, 'className');
304
+ const className = classAttr && staticClassName(classAttr);
305
+ return {
306
+ tag: name.name,
307
+ ...(classAttr && {
308
+ tokens: className === null
309
+ ? null
310
+ : [...new Set(className.split(/\s+/).filter(Boolean))],
311
+ }),
312
+ attributes: attributesBesides(child, classAttr, childAttributeValue),
313
+ hasSpread: hasSpread(child),
314
+ };
315
+ }
232
316
  function insideForeignContent(elementPath) {
233
317
  let current = elementPath.parent;
234
318
  while (current) {
@@ -325,10 +409,21 @@ export default function transform(fileInfo, api, options = {}) {
325
409
  if (child.type !== 'JSXElement')
326
410
  return [];
327
411
  const target = planned.get(child)?.conversion?.target ??
412
+ planned.get(child)?.fold?.target ??
328
413
  // A direct child is looked up in the element's own scope, the child's.
329
414
  bestaxTarget(child, elementPath);
330
415
  return target ? [target] : [];
331
416
  });
417
+ /**
418
+ * The one element an element holds, when it holds nothing else: no text
419
+ * React renders, and no comment, which folding the element would drop.
420
+ */
421
+ const soleChild = (element) => {
422
+ const content = (element.children ?? []).filter((child) => child.type !== 'JSXText' || reachesReact(child));
423
+ return content.length === 1 && content[0].type === 'JSXElement'
424
+ ? content[0]
425
+ : undefined;
426
+ };
332
427
  // For each element, the bestax components already in the file inside it:
333
428
  // `Field` and `Control` change how bestax's form controls render.
334
429
  const bestaxInside = new Map();
@@ -381,35 +476,34 @@ export default function transform(fileInfo, api, options = {}) {
381
476
  const classAttr = findAttr(element, 'className');
382
477
  if (!classAttr)
383
478
  continue;
384
- const attributes = new Map();
385
- for (const attr of element.openingElement.attributes ?? []) {
386
- if (attr.type !== 'JSXAttribute' || attr === classAttr)
387
- continue;
388
- attributes.set(attributeName(attr), attributeValue(attr));
389
- }
479
+ const attributes = attributesBesides(element, classAttr, attributeValue);
390
480
  const className = staticClassName(classAttr);
391
481
  const surrounding = around(elementPath);
482
+ const sole = soleChild(element);
392
483
  const facts = {
393
484
  tag: name.name,
394
485
  tokens: className === null
395
486
  ? classTokensOf(classAttr.value)
396
487
  : [...new Set(className.split(/\s+/).filter(Boolean))],
397
488
  attributes,
398
- hasSpread: (element.openingElement.attributes ?? []).some((attr) => attr.type === 'JSXSpreadAttribute'),
489
+ hasSpread: hasSpread(element),
399
490
  hasRef: attributes.has('ref'),
400
491
  hasChildren: (element.children ?? []).some(reachesReact),
401
492
  childTargets: childTargets(elementPath),
493
+ soleChildTarget: planned.get(sole)?.conversion?.target,
494
+ soleChild: sole && childFacts(sole),
402
495
  bestaxInside: bestaxInside.get(element) ?? [],
403
496
  bestaxAround: surrounding.bestax,
404
497
  componentsAround: surrounding.other,
405
498
  onlyChildOf: onlyChildOf(elementPath, bestaxLocals),
406
499
  };
407
500
  let result = plan(facts);
408
- const converts = result.conversion !== null;
409
- if (className === null && result.conversion) {
501
+ const converts = result.conversion !== null || result.fold !== undefined;
502
+ const becomes = result.conversion?.target ?? result.fold?.target;
503
+ if (className === null && becomes) {
410
504
  // The same element with a static className would convert; with a
411
505
  // computed one, only a person can turn each condition into its prop.
412
- const target = result.conversion.target;
506
+ const target = becomes;
413
507
  result = {
414
508
  conversion: null,
415
509
  todos: [
@@ -421,8 +515,15 @@ export default function transform(fileInfo, api, options = {}) {
421
515
  ],
422
516
  };
423
517
  }
518
+ if (result.fold) {
519
+ // The wrapper's props join its child's conversion, which has not been
520
+ // applied yet: conversions are applied after every element is planned.
521
+ const inner = planned.get(soleChild(element)).conversion;
522
+ inner.props.push(...result.fold.props);
523
+ inner.numbers.push(...result.fold.numbers);
524
+ }
424
525
  planned.set(element, result);
425
- if (result.conversion || result.todos.length > 0) {
526
+ if (result.conversion || result.fold || result.todos.length > 0) {
426
527
  elements.unshift({ path: elementPath, plan: result, converts });
427
528
  }
428
529
  }
@@ -436,7 +537,7 @@ export default function transform(fileInfo, api, options = {}) {
436
537
  if (converting)
437
538
  addTodo(ctx, elements[0].path, rule, message);
438
539
  for (const element of elements) {
439
- element.plan = { ...element.plan, conversion: null };
540
+ element.plan = { ...element.plan, conversion: null, fold: undefined };
440
541
  }
441
542
  };
442
543
  const runtime = fileJsxRuntime(root, j) ??
@@ -482,50 +583,89 @@ export default function transform(fileInfo, api, options = {}) {
482
583
  }
483
584
  }
484
585
  for (const { path: elementPath, plan: result } of [...elements].reverse()) {
586
+ if (result.fold) {
587
+ // The child inside has converted already (innermost first), with the
588
+ // wrapper's props on it; the wrapper itself goes, its comments with the
589
+ // child: one on the wrapper moves to the child, and one inside the
590
+ // wrapper's tags moves inside the child's opening tag.
591
+ const wrapper = elementPath.node;
592
+ const inner = soleChild(wrapper);
593
+ const inTags = [
594
+ ...tagComments(wrapper),
595
+ ...(wrapper.openingElement.attributes ?? []).flatMap((attr) => attr.comments ?? []),
596
+ ];
597
+ if (inTags.length) {
598
+ const name = inner.openingElement.name;
599
+ name.comments = [...(name.comments ?? []), ...inTags.map(afterTagName)];
600
+ }
601
+ if (wrapper.comments?.length) {
602
+ inner.comments = [...wrapper.comments, ...(inner.comments ?? [])];
603
+ }
604
+ elementPath.replace(inner);
605
+ ctx.dirty = true;
606
+ continue;
607
+ }
485
608
  const conversion = result.conversion;
486
609
  if (!conversion)
487
610
  continue;
488
611
  const element = elementPath.node;
612
+ // A target that renders the element's only child itself is written in
613
+ // that child's place, so the child's children stay where they are.
614
+ const child = conversion.absorbs ? soleChild(element) : undefined;
615
+ // The element's tags go, and so do the child's tag names: their comments
616
+ // move to the name that stays. The child's tags themselves stay, and
617
+ // keep their own.
618
+ const inTags = child
619
+ ? [
620
+ ...tagComments(element),
621
+ ...[child.openingElement.name, child.closingElement?.name].flatMap((node) => node?.comments ?? []),
622
+ ]
623
+ : [];
624
+ if (child) {
625
+ // Renamed before the drops below, which name each attribute as the
626
+ // component is given it.
627
+ const { renames, drop } = conversion.absorbs;
628
+ for (const name of drop)
629
+ removeAttr(child, findAttr(child, name));
630
+ for (const [from, to] of renames) {
631
+ findAttr(child, from).name = j.jsxIdentifier(to);
632
+ }
633
+ }
489
634
  const [head, ...rest] = conversion.target.split('.');
490
635
  renameElement(j, element, [ctx.reserve(head), ...rest].join('.'));
491
- const attrs = element.openingElement.attributes;
492
636
  for (const name of conversion.drop) {
493
- attrs.splice(attrs.indexOf(findAttr(element, name)), 1);
494
- }
495
- const classAttr = findAttr(element, 'className');
496
- const replacement = conversion.props.map(([name, value]) => makeAttr(j, name, value === true ? undefined : value));
497
- if (conversion.className) {
498
- replacement.push(makeAttr(j, 'className', conversion.className));
637
+ const holder = findAttr(element, name) ? element : child;
638
+ removeAttr(holder, findAttr(holder, name));
499
639
  }
500
- // A comment on the className goes with whatever takes its place: the
501
- // first new attribute, else the next one along, else the tag name. After
502
- // the name it has to be a block comment: recast prints a line comment
503
- // there as `<// …`, which TypeScript reads as a closing tag.
504
- if (classAttr.comments?.length) {
505
- const name = element.openingElement.name;
506
- const carrier = replacement[0] ?? attrs.find((attr) => attr !== classAttr) ?? name;
507
- carrier.comments = [
508
- ...(carrier.comments ?? []),
509
- ...classAttr.comments.map((comment) => {
510
- if (carrier !== name)
511
- return comment;
512
- if (comment.type !== 'CommentLine') {
513
- return { ...comment, leading: false, trailing: true };
514
- }
515
- return {
516
- type: 'CommentBlock',
517
- value: ` ${comment.value.trim().replace(/\*\//g, '*\\/')} `,
518
- leading: false,
519
- trailing: true,
520
- };
521
- }),
640
+ writeClassName(j, element, conversion.props, conversion.className);
641
+ let written = element;
642
+ if (child) {
643
+ child.openingElement.name = element.openingElement.name;
644
+ if (child.closingElement) {
645
+ child.closingElement.name = element.closingElement.name;
646
+ }
647
+ if (findAttr(child, 'className')) {
648
+ writeClassName(j, child, conversion.absorbs.props, null);
649
+ }
650
+ child.openingElement.attributes = [
651
+ ...(element.openingElement.attributes ?? []),
652
+ ...(child.openingElement.attributes ?? []),
522
653
  ];
654
+ // A comment on the element moves to the component in its place.
655
+ if (inTags.length) {
656
+ const name = child.openingElement.name;
657
+ name.comments = [...(name.comments ?? []), ...inTags.map(afterTagName)];
658
+ }
659
+ if (element.comments?.length) {
660
+ child.comments = [...element.comments, ...(child.comments ?? [])];
661
+ }
662
+ elementPath.replace(child);
663
+ written = child;
523
664
  }
524
- attrs.splice(attrs.indexOf(classAttr), 1, ...replacement);
525
665
  // After the splice, so a prop the conversion wrote is found as well as
526
666
  // an attribute the element already had.
527
667
  for (const name of conversion.numbers) {
528
- const attr = findAttr(element, name);
668
+ const attr = findAttr(written, name);
529
669
  attr.value = j.jsxExpressionContainer(j.numericLiteral(Number(attributeValue(attr))));
530
670
  }
531
671
  ctx.dirty = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bestax-migrate",
3
- "version": "2.8.0",
3
+ "version": "2.10.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": {