bestax-migrate 2.3.0 → 2.3.2

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.
@@ -8,7 +8,8 @@
8
8
  * the modifier vocabulary is the one thing here that differs per source.
9
9
  */
10
10
  import { addAttrOnce } from './props.js';
11
- import { addAttr, addTodo, attributesOf, findAttr, literalValueOf, makeAttr, plainElement, removeAttr, } from './jsx-utils.js';
11
+ import { LINK_ATTRS, elementTakesLinkAttr, tagRejectsHref, } from './polymorphic.js';
12
+ import { addAttr, addTodo, attrSource, attributesOf, findAttr, literalValueOf, makeAttr, plainElement, removeAttr, } from './jsx-utils.js';
12
13
  /** Pick a target based on a literal align-style prop, removing the prop. */
13
14
  export function alignTarget(ctx, path, element, prop, targets, fallback) {
14
15
  const attr = findAttr(element, prop);
@@ -206,6 +207,50 @@ export function restrictAsToTargets(ctx, path, element, target, allowed, prop =
206
207
  .join(' / ')} does); render the tag directly or restructure`);
207
208
  ctx.dirty = true;
208
209
  }
210
+ /**
211
+ * Strip the link attributes a plain element cannot carry.
212
+ *
213
+ * Exported because not every source reaches plain markup through
214
+ * `replaceWithPlain`: react-bulma-components builds six of its replacements
215
+ * by hand, and they bypassed this entirely -- `<Form.Help href="/x">` became
216
+ * `<p className="help" href="/x">`, which does not compile.
217
+ */
218
+ export function dropLinkAttrsForPlainTag(ctx, path, element, tag, where) {
219
+ // An `href` only belongs on the anchor. The source component put it on
220
+ // whatever tag it rendered, where the browser ignored it; a plain element
221
+ // is typed as itself, so carrying it here is a type error for an attribute
222
+ // that never navigated anywhere.
223
+ // A plain rewrite can legitimately produce an `<area>` or a `<link>`, and
224
+ // React types an `href` onto both. The anchor-only rule that applies to
225
+ // bestax components does not apply here -- there is no component in the way
226
+ // to disagree about what it forwards.
227
+ //
228
+ // The message deliberately does not add "it navigated nowhere in the source
229
+ // either", the way the polymorphic path does. That holds only where `tag`
230
+ // came from the source -- bloomer's call sites all pass one -- while rbx and
231
+ // react-bulma-components hard-code the plain tag and ignore the source's
232
+ // element prop, so there the element really did change.
233
+ const href = tagRejectsHref(tag) ? findAttr(element, 'href') : undefined;
234
+ if (href) {
235
+ const was = attrSource(ctx.j, href);
236
+ removeAttr(element, href);
237
+ addTodo(ctx, path, 'prop:href', `${where} became a plain <${tag}>, which takes no \`href\` — make it an <a>, or put one inside${was ? ` — it read \`${was}\`` : ''}`);
238
+ ctx.dirty = true;
239
+ }
240
+ // Its siblings are judged against the same tag, each on its own, so a
241
+ // `referrerPolicy` on an `<img>` survives.
242
+ for (const name of LINK_ATTRS) {
243
+ if (elementTakesLinkAttr(name, tag))
244
+ continue;
245
+ const attr = findAttr(element, name);
246
+ if (!attr)
247
+ continue;
248
+ const was = attrSource(ctx.j, attr);
249
+ removeAttr(element, attr);
250
+ addTodo(ctx, path, `prop:${name}`, `${where} became a plain <${tag}>, which takes no \`${name}\`${was ? ` — it read \`${was}\`` : ''}; put it on an <a> inside, or change the element`);
251
+ ctx.dirty = true;
252
+ }
253
+ }
209
254
  /**
210
255
  * The two structural rewrites every source needs, bound to that source's own
211
256
  * attribute strip (its modifier vocabulary is the one thing that differs):
@@ -216,6 +261,7 @@ export function makeStructuralHelpers(strip) {
216
261
  /** Replace the element with a plain HTML tag carrying `className`. */
217
262
  function replaceWithPlain(ctx, path, element, tag, className, where) {
218
263
  const merged = mergeClassName(ctx, path, element, className, where);
264
+ dropLinkAttrsForPlainTag(ctx, path, element, tag, where);
219
265
  const kept = new Set(strip(ctx, path, attributesOf(element), where));
220
266
  // Spread attributes pass through untouched, in their original places:
221
267
  // they are the caller's own props, and a plain element takes them as
@@ -1 +1 @@
1
- {"version":3,"file":"mapping.d.ts","sourceRoot":"","sources":["../../../src/sources/bloomer/mapping.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AA2BnE;;;;GAIG;AACH,eAAO,MAAM,WAAW,EAAE,UAEzB,CAAC;AA+CF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAsBtD,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAG1D,CAAC;AAEF;;;;;GAKG;AACH;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,aAI/B,CAAC;AAEH,eAAO,MAAM,kBAAkB,aAqB7B,CAAC;AAEH,8EAA8E;AAC9E,wBAAgB,eAAe,CAC7B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,GAC3C,MAAM,GAAG,IAAI,CA0Bf;AAED,iFAAiF;AACjF,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAOlD,CAAC;AAEF,eAAO,MAAM,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAsoBpD,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAiIpD,CAAC;AAEF,sDAAsD;AACtD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,gBAAgB,GAAG,SAAS,CAM3E"}
1
+ {"version":3,"file":"mapping.d.ts","sourceRoot":"","sources":["../../../src/sources/bloomer/mapping.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AA2BnE;;;;GAIG;AACH,eAAO,MAAM,WAAW,EAAE,UAEzB,CAAC;AA+CF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAsBtD,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAG1D,CAAC;AAEF;;;;;GAKG;AACH;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,aAI/B,CAAC;AAEH,eAAO,MAAM,kBAAkB,aAqB7B,CAAC;AAEH,8EAA8E;AAC9E,wBAAgB,eAAe,CAC7B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,GAC3C,MAAM,GAAG,IAAI,CA0Bf;AAED,iFAAiF;AACjF,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAOlD,CAAC;AAEF,eAAO,MAAM,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CA0nBpD,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAiIpD,CAAC;AAEF,sDAAsD;AACtD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,gBAAgB,GAAG,SAAS,CAM3E"}
@@ -276,9 +276,6 @@ export const MAPPING = {
276
276
  props: {
277
277
  hasTextColor: TEXT_COLOR_OK,
278
278
  isSize: size,
279
- href: {
280
- todo: 'bestax `Delete` renders a <button> and has no anchor form; wrap it in an <a>, or handle the navigation in `onClick`',
281
- },
282
279
  },
283
280
  },
284
281
  Icon: {
@@ -484,9 +481,6 @@ export const MAPPING = {
484
481
  target: 'Card.Header.Icon',
485
482
  props: {
486
483
  hasTextColor: TEXT_COLOR_OK,
487
- href: {
488
- todo: 'bestax `Card.Header.Icon` renders a <button>; put an <a> inside it, or handle the navigation in `onClick`',
489
- },
490
484
  },
491
485
  },
492
486
  CardFooter: {
@@ -499,9 +493,6 @@ export const MAPPING = {
499
493
  target: 'Card.FooterItem',
500
494
  props: {
501
495
  hasTextColor: TEXT_COLOR_OK,
502
- href: {
503
- todo: 'bestax `Card.FooterItem` renders a <span> with no anchor form; put an <a> inside it',
504
- },
505
496
  },
506
497
  },
507
498
  // ---- dropdown -----------------------------------------------------------
@@ -536,9 +527,6 @@ export const MAPPING = {
536
527
  props: {
537
528
  isActive: active,
538
529
  tag: TAG_AS,
539
- href: {
540
- todo: 'bestax `Dropdown.Item` declares no `href`; navigate in `onClick`, or put an <a> inside the item',
541
- },
542
530
  },
543
531
  },
544
532
  DropdownDivider: { status: 'mapped', target: 'Dropdown.Divider' },
@@ -1 +1 @@
1
- {"version":3,"file":"specials.d.ts","sourceRoot":"","sources":["../../../src/sources/bloomer/specials.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAE3C,OAAO,EASL,KAAK,gBAAgB,EACtB,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EAQL,KAAK,aAAa,EACnB,MAAM,8BAA8B,CAAC;AAEtC,YAAY,EAAE,aAAa,EAAE,CAAC;AAuiC9B,wBAAgB,UAAU,CACxB,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,gBAAgB,EACrB,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,EAClB,OAAO,EAAE,GAAG,GACX,aAAa,CAQf;AAED,eAAO,MAAM,aAAa,UAAwB,CAAC"}
1
+ {"version":3,"file":"specials.d.ts","sourceRoot":"","sources":["../../../src/sources/bloomer/specials.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAE3C,OAAO,EAUL,KAAK,gBAAgB,EACtB,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EAQL,KAAK,aAAa,EACnB,MAAM,8BAA8B,CAAC;AAEtC,YAAY,EAAE,aAAa,EAAE,CAAC;AA0nC9B,wBAAgB,UAAU,CACxB,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,gBAAgB,EACrB,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,EAClB,OAAO,EAAE,GAAG,GACX,aAAa,CAQf;AAED,eAAO,MAAM,aAAa,UAAwB,CAAC"}
@@ -11,7 +11,7 @@
11
11
  * element `replaced` when it substituted the node itself.
12
12
  */
13
13
  import { RENDER_TODO, RESPONSIVE_PROPS, UNIVERSAL_PROPS } from './mapping.js';
14
- import { addAttr, addTodo, attributesOf, findAttr, literalValueOf, makeAttr, removeAttr, resolveBooleanish, } from '../_shared/jsx-utils.js';
14
+ import { addAttr, attrSource, addTodo, attributesOf, findAttr, literalValueOf, makeAttr, removeAttr, resolveBooleanish, } from '../_shared/jsx-utils.js';
15
15
  import { applyPropAction } from '../_shared/props.js';
16
16
  import { applyIconProps, makeStripModifierProps, makeStructuralHelpers, modifierClass, parseIconClasses, restrictAsToTargets, } from '../_shared/specials-utils.js';
17
17
  /* eslint-disable @typescript-eslint/no-explicit-any */
@@ -76,14 +76,56 @@ const join = (...parts) => {
76
76
  * (and the Nav family). bestax's defaults differ per component, so this
77
77
  * keeps bloomer's markup for those either way:
78
78
  *
79
- * - with a literal `href`, a surviving `tag` (which the mapping turns into
80
- * `as`) would undo the anchor on targets that already render one, so it is
81
- * dropped; where the target defaults to something else, `as="a"` is set
82
- * (`setAs`). A dynamic `href` was a runtime decision bloomer made and
83
- * bestax cannot: it is flagged, not guessed;
79
+ * - with an `href` that has a value, the anchor is the element bloomer
80
+ * rendered, so the `tag` beside it (which the mapping would turn into `as`)
81
+ * is the branch that did not happen and goes; where the target defaults to
82
+ * something other than an <a>, `as="a"` is set (`setAs`);
83
+ * - a dynamic `href` was a runtime decision bloomer made and bestax's `as` is
84
+ * one element or the other. It resolves to the anchor, which is the branch
85
+ * the `href` is there for, and the TODO names the element to restore for
86
+ * the empty case. Keeping both would emit `href` beside a non-anchor `as`,
87
+ * which the library does not type.
88
+ *
89
+ * Settled deliberately, and reviewers have raised it more than once. The
90
+ * alternative that keeps bloomer's own choice is a conditional --
91
+ * `as={href ? 'a' : 'span'}` -- which does typecheck, on `Button` as well
92
+ * as `Level.Item`. It was weighed and declined: it writes a conditional the
93
+ * author did not, it only works where the source `tag` is a literal, and it
94
+ * leaves a second shape to explain. A single element plus a TODO that names
95
+ * the other is the output this package prefers. Do not re-litigate without
96
+ * new information;
84
97
  * - without `href` or `tag`, a target that defaults to an <a> gets
85
98
  * `as={bareAs}` so bloomer's <div> stays a <div>.
86
99
  */
100
+ /**
101
+ * Did bloomer's `props.href ? 'a' : tag` take the `tag` branch for this value?
102
+ *
103
+ * True for `""`, `false` and `0`, and for `null` and `undefined` -- both reach
104
+ * `literalValueOf` as expressions, because it has no kind for them, but both
105
+ * are statically falsy and neither ever selected the anchor. `undefined` is
106
+ * shadowable (a parameter may be named it), so it is resolved by binding
107
+ * rather than by text, which is the rule this package applies to component
108
+ * references.
109
+ *
110
+ * Shared by `anchorWhenHref` and the `panel-block` handler: they are two
111
+ * copies of the same decision, and the second one missed these two literals.
112
+ */
113
+ function isFalsyHref(path, attr) {
114
+ const expr = attr.value?.type === 'JSXExpressionContainer'
115
+ ? attr.value.expression
116
+ : undefined;
117
+ if (expr) {
118
+ if (expr.type === 'NullLiteral')
119
+ return true;
120
+ if (expr.type === 'Literal' && expr.value === null)
121
+ return true;
122
+ if (expr.type === 'Identifier' && expr.name === 'undefined') {
123
+ return !path.scope?.lookup('undefined');
124
+ }
125
+ }
126
+ const literal = literalValueOf(attr);
127
+ return literal.kind !== 'expression' && !literal.value;
128
+ }
87
129
  function anchorWhenHref(ctx, path, element, options = {}) {
88
130
  const handled = [];
89
131
  const tagAttr = findAttr(element, 'tag');
@@ -92,31 +134,65 @@ function anchorWhenHref(ctx, path, element, options = {}) {
92
134
  // false `href` kept the default element. It selected nothing, and bestax
93
135
  // types `href` as a string where it exists at all, so it is dropped.
94
136
  const hrefLiteral = hrefAttr ? literalValueOf(hrefAttr) : undefined;
95
- const hrefFalsy = hrefLiteral !== undefined &&
96
- hrefLiteral.kind !== 'expression' &&
97
- !hrefLiteral.value;
137
+ const hrefFalsy = hrefAttr !== undefined && isFalsyHref(path, hrefAttr);
98
138
  if (hrefAttr && hrefFalsy) {
99
139
  removeAttr(element, hrefAttr);
100
140
  handled.push('href');
101
141
  ctx.dirty = true;
102
142
  }
103
143
  if (hrefAttr && !hrefFalsy) {
104
- const literal = hrefLiteral;
105
- if (literal.kind === 'expression') {
106
- addTodo(ctx, path, 'prop:href', options.setAs
107
- ? `bloomer rendered an <a> only when \`href\` had a value, and this one is dynamic; set \`as="a"\` conditionally by hand${tagAttr ? ' (the `tag` beside it becomes `as`, which that conditional has to account for)' : ''}`
108
- : `bloomer rendered an <a> only when \`href\` had a value, and this one is dynamic; the bestax target renders an <a> already, so drop the \`href\` where it is empty${tagAttr ? ' — the `tag` beside it is flagged separately' : ''}`);
109
- return handled;
110
- }
144
+ // The href wins the element. bloomer read `props.href ? 'a' : tag`, so a
145
+ // `tag` here is the branch that did not run, and the anchor is what
146
+ // rendered. That holds for an expression too, so the `tag` still goes --
147
+ // but it is a live reference, so its removal is announced rather than
148
+ // silent, which is what the first version of this got wrong.
149
+ const tagValue = tagAttr ? literalValueOf(tagAttr) : undefined;
150
+ const dynamicTag = tagAttr !== undefined && tagValue?.kind !== 'string';
151
+ // The only removal in this pass that deletes an arbitrary expression, so
152
+ // it owes the same quote every other one gives: the reference may be the
153
+ // last thing keeping an import alive.
154
+ const dynamicTagSource = dynamicTag ? attrSource(ctx.j, tagAttr) : '';
111
155
  if (tagAttr) {
112
156
  removeAttr(element, tagAttr);
113
157
  handled.push('tag');
114
158
  ctx.dirty = true;
115
159
  }
160
+ if (dynamicTag) {
161
+ addTodo(ctx, path, 'prop:tag', `bloomer rendered an <a> whenever \`href\` had a value, so the \`tag\` expression beside this one chose nothing and is removed${dynamicTagSource ? ` -- it read \`${dynamicTagSource}\`` : ''}. Restore it by hand if the \`href\` can be empty`);
162
+ }
163
+ // An `as` on the element is not bloomer's element choice -- it took no
164
+ // such prop -- so a literal one that is not the anchor is dropped. An
165
+ // expression stays: it is the author's own component, it is the element
166
+ // they meant, and on a target generic over `as` it takes the `href` with
167
+ // it. Rewriting it to `as="a"` swapped a router link for a plain anchor.
168
+ const asAttr = findAttr(element, 'as');
169
+ const asLiteral = asAttr ? literalValueOf(asAttr) : undefined;
170
+ const literalAs = asLiteral?.kind === 'string' ? asLiteral.value : undefined;
171
+ if (asAttr && asLiteral?.kind === 'string' && literalAs !== 'a') {
172
+ removeAttr(element, asAttr);
173
+ // Say so. bloomer had no `as`, so this one was spread onto the element
174
+ // as a stray DOM attribute and did nothing -- but in bestax `as` picks
175
+ // the element, so removing it is a decision about the author's code and
176
+ // not a no-op the way it was in the source.
177
+ addTodo(ctx, path, 'prop:as', `bloomer had no \`as\` prop, so \`as="${literalAs}"\` rode onto the element it rendered and did nothing; in bestax it would pick the element, and the \`href\` here needs the <a> -- it is removed rather than left to fight the anchor`);
178
+ ctx.dirty = true;
179
+ }
116
180
  if (options.setAs && !findAttr(element, 'as')) {
117
181
  addAttr(element, makeAttr(ctx.j, 'as', 'a'));
118
182
  ctx.dirty = true;
119
183
  }
184
+ if (hrefLiteral.kind === 'expression') {
185
+ // Name the element that was dropped where the source named it: it is
186
+ // the half of bloomer's runtime choice this output no longer renders.
187
+ const other = dynamicTag
188
+ ? 'the element `tag` named'
189
+ : tagValue?.kind === 'string'
190
+ ? `a <${tagValue.value}>`
191
+ : options.bareAs
192
+ ? `a <${options.bareAs}>`
193
+ : 'its default tag';
194
+ addTodo(ctx, path, 'prop:href', `bloomer chose the element at runtime — an <a> when \`href\` had a value, ${other} when it did not. bestax's \`as\` is one element or the other, and this is the anchor; render the other by hand where the \`href\` is empty`);
195
+ }
120
196
  }
121
197
  else if (options.bareAs && !tagAttr && !findAttr(element, 'as')) {
122
198
  addAttr(element, makeAttr(ctx.j, 'as', options.bareAs));
@@ -521,9 +597,7 @@ const SPECIALS = {
521
597
  // The same rule as anchorWhenHref: bloomer chose the anchor with
522
598
  // `props.href ? 'a' : tag`, so an empty or false href is no anchor.
523
599
  const hrefAttr = findAttr(element, 'href');
524
- const hrefLiteral = hrefAttr ? literalValueOf(hrefAttr) : undefined;
525
- const anchored = hrefAttr !== undefined &&
526
- (hrefLiteral.kind === 'expression' || Boolean(hrefLiteral.value));
600
+ const anchored = hrefAttr !== undefined && !isFalsyHref(path, hrefAttr);
527
601
  if (hrefAttr && !anchored) {
528
602
  // A falsy href selected nothing in bloomer and cannot sit on a <div>.
529
603
  removeAttr(element, hrefAttr);
@@ -1 +1 @@
1
- {"version":3,"file":"transform.d.ts","sourceRoot":"","sources":["../../../src/sources/bloomer/transform.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAuDvD,MAAM,CAAC,OAAO,UAAU,SAAS,CAC/B,QAAQ,EAAE,QAAQ,EAClB,GAAG,EAAE,GAAG,EACR,OAAO,GAAE,gBAAqB,GAC7B,MAAM,GAAG,SAAS,CA06BpB"}
1
+ {"version":3,"file":"transform.d.ts","sourceRoot":"","sources":["../../../src/sources/bloomer/transform.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAwDvD,MAAM,CAAC,OAAO,UAAU,SAAS,CAC/B,QAAQ,EAAE,QAAQ,EAClB,GAAG,EAAE,GAAG,EACR,OAAO,GAAE,gBAAqB,GAC7B,MAAM,GAAG,SAAS,CA86BpB"}
@@ -24,6 +24,7 @@
24
24
  import { HELPERLESS_TARGETS, MAPPING, NO_CLASSNAME_TARGETS, UNIVERSAL_PROPS, helperClassHint, resolveMapping, } from './mapping.js';
25
25
  import { addTodo, attributesOf, jsxNameParts, literalValueOf, removeAttr, renameElement, } from '../_shared/jsx-utils.js';
26
26
  import { applyPropAction, applyUniversalProps, mergeClass, } from '../_shared/props.js';
27
+ import { enforcePolymorphicProps } from '../_shared/polymorphic.js';
27
28
  import { collectBoundNames, makeReserve, nameOf, prefersTabs, resolvesToBinding, } from '../_shared/imports.js';
28
29
  import { flattenResponsiveProps, RESPONSIVE_KINDS } from './responsive.js';
29
30
  import { runSpecial } from './specials.js';
@@ -456,6 +457,9 @@ export default function transform(fileInfo, api, options = {}) {
456
457
  }
457
458
  }
458
459
  applyUniversalProps(ctx, path, element, handled, UNIVERSAL_PROPS);
460
+ // Last: bestax's props follow `as`, so the element decides which of the
461
+ // props just written it can actually take.
462
+ enforcePolymorphicProps(ctx, path, element, target);
459
463
  });
460
464
  // ---- 2b. Value references to bloomer components ---------------------------
461
465
  // Components can be referenced as plain values too (`as={Block}`, passed to
@@ -1 +1 @@
1
- {"version":3,"file":"mapping.d.ts","sourceRoot":"","sources":["../../../src/sources/rbx/mapping.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAuCnE;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAiCtD,CAAC;AAEF,uDAAuD;AACvD,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAOrD,CAAC;AAEF,2DAA2D;AAC3D,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAQvD,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAShE,CAAC;AAEF,eAAO,MAAM,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAssBpD,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CA2FhD,CAAC;AAEF,sDAAsD;AACtD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,gBAAgB,GAAG,SAAS,CAM3E"}
1
+ {"version":3,"file":"mapping.d.ts","sourceRoot":"","sources":["../../../src/sources/rbx/mapping.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAuCnE;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAiCtD,CAAC;AAEF,uDAAuD;AACvD,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAOrD,CAAC;AAEF,2DAA2D;AAC3D,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAQvD,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAShE,CAAC;AAEF,eAAO,MAAM,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAktBpD,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CA2FhD,CAAC;AAEF,sDAAsD;AACtD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,gBAAgB,GAAG,SAAS,CAM3E"}
@@ -510,7 +510,11 @@ export const MAPPING = {
510
510
  target: 'Dropdown.Item',
511
511
  // `as` lives on DropdownItemProps ('a' | 'div' | 'button'), not on
512
512
  // the Dropdown root.
513
- props: { as: AS_OK, active: {}, onClick: {} },
513
+ props: {
514
+ as: AS_OK,
515
+ active: {},
516
+ onClick: {},
517
+ },
514
518
  },
515
519
  Divider: { status: 'mapped', target: 'Dropdown.Divider' },
516
520
  Context: {
@@ -680,6 +684,14 @@ export const MAPPING = {
680
684
  },
681
685
  },
682
686
  Item: {
687
+ // The `navbar-item` handler names a target only for the `dropdown`
688
+ // case; without one here the plain item resolved to no target at all,
689
+ // and `transform.ts` skips every prop pass for an element it cannot
690
+ // name — so `backgroundColor`/`marginless` on a plain `<Navbar.Item>`
691
+ // passed through as rbx wrote them while the same props on its
692
+ // `Navbar.Link` sibling migrated. The rename is a no-op (rbx and
693
+ // bestax spell it the same), which is why it looked right.
694
+ target: 'Navbar.Item',
683
695
  status: 'mapped',
684
696
  special: 'navbar-item',
685
697
  props: { active: {}, onClick: {}, as: AS_OK },
@@ -1 +1 @@
1
- {"version":3,"file":"transform.d.ts","sourceRoot":"","sources":["../../../src/sources/rbx/transform.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AA2DvD,MAAM,CAAC,OAAO,UAAU,SAAS,CAC/B,QAAQ,EAAE,QAAQ,EAClB,GAAG,EAAE,GAAG,EACR,OAAO,GAAE,gBAAqB,GAC7B,MAAM,GAAG,SAAS,CAmoCpB"}
1
+ {"version":3,"file":"transform.d.ts","sourceRoot":"","sources":["../../../src/sources/rbx/transform.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AA4DvD,MAAM,CAAC,OAAO,UAAU,SAAS,CAC/B,QAAQ,EAAE,QAAQ,EAClB,GAAG,EAAE,GAAG,EACR,OAAO,GAAE,gBAAqB,GAC7B,MAAM,GAAG,SAAS,CA0oCpB"}
@@ -22,6 +22,7 @@
22
22
  import { BADGE_PROPS, MAPPING, TOOLTIP_PROPS, UNIVERSAL_PROPS, resolveMapping, } from './mapping.js';
23
23
  import { addTodo, attributesOf, findAttr, jsxNameParts, literalValueOf, removeAttr, renameElement, } from '../_shared/jsx-utils.js';
24
24
  import { applyPropAction, applyUniversalProps } from '../_shared/props.js';
25
+ import { enforcePolymorphicProps } from '../_shared/polymorphic.js';
25
26
  import { collectBoundNames, makeAliasRegistry, makeReserve, nameOf, prefersTabs, resolvesToBinding, } from '../_shared/imports.js';
26
27
  import { flattenResponsiveProps, RESPONSIVE_KINDS } from './responsive.js';
27
28
  import { runSpecial } from './specials.js';
@@ -560,6 +561,12 @@ export default function transform(fileInfo, api, options = {}) {
560
561
  }
561
562
  }
562
563
  applyUniversalProps(ctx, path, element, handled, UNIVERSAL_PROPS);
564
+ // After every prop pass: bestax's props follow `as`, so the element
565
+ // decides which of the props just written it can actually take. The
566
+ // wrappers below move neither `as` nor `href`, so this reads the final
567
+ // pair either way.
568
+ // rbx spells its element prop `as`, so a spread here really can carry one.
569
+ enforcePolymorphicProps(ctx, path, element, target, true);
563
570
  // ---- 2a. badge/tooltip helper props → wrapping components ------------
564
571
  // Last, so the inner element is already fully migrated. Both families are
565
572
  // read off `element` — the props live there whichever wrapper is built
@@ -1 +1 @@
1
- {"version":3,"file":"mapping.d.ts","sourceRoot":"","sources":["../../../src/sources/react-bulma-components/mapping.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAOnE;;;;GAIG;AACH,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAgFtD,CAAC;AAEF,gEAAgE;AAChE,eAAO,MAAM,sBAAsB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAUhE,CAAC;AAIF,eAAO,MAAM,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAspBpD,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAsEhD,CAAC;AAEF,6EAA6E;AAC7E,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,gBAAgB,GAAG,SAAS,CAM3E"}
1
+ {"version":3,"file":"mapping.d.ts","sourceRoot":"","sources":["../../../src/sources/react-bulma-components/mapping.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAOnE;;;;GAIG;AACH,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAgFtD,CAAC;AAEF,gEAAgE;AAChE,eAAO,MAAM,sBAAsB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAUhE,CAAC;AAIF,eAAO,MAAM,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CA6pBpD,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAsEhD,CAAC;AAEF,6EAA6E;AAC7E,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,gBAAgB,GAAG,SAAS,CAM3E"}
@@ -345,6 +345,8 @@ export const MAPPING = {
345
345
  status: 'mapped',
346
346
  target: 'Control',
347
347
  props: {
348
+ // `Control` declares `as` too, narrowed to div/p.
349
+ renderAs: { rename: 'as' },
348
350
  fullwidth: { booleanToProp: { name: 'isExpanded' } },
349
351
  loading: { booleanToProp: { name: 'isLoading' } },
350
352
  iconType: {
@@ -470,6 +472,11 @@ export const MAPPING = {
470
472
  target: 'Image',
471
473
  special: 'image',
472
474
  props: {
475
+ // bestax's `Image` does declare an `as`, narrowed to figure/div/p, so
476
+ // the universal `renderAs` TODO ("this bestax component has no `as`
477
+ // prop") was a false claim about the library and left the prop on the
478
+ // element. Renaming lets the narrowed-union check judge it.
479
+ renderAs: { rename: 'as' },
473
480
  rounded: { booleanToProp: { name: 'isRounded' } },
474
481
  fallback: {
475
482
  todo: 'no fallback prop in bestax Image; handle onError yourself',
@@ -1 +1 @@
1
- {"version":3,"file":"specials.d.ts","sourceRoot":"","sources":["../../../src/sources/react-bulma-components/specials.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAE3C,OAAO,EAWL,KAAK,gBAAgB,EACtB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAOL,KAAK,aAAa,EACnB,MAAM,8BAA8B,CAAC;AAEtC,YAAY,EAAE,aAAa,EAAE,CAAC;AAgxB9B,wBAAgB,UAAU,CACxB,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,gBAAgB,EACrB,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,EAClB,OAAO,EAAE,GAAG,GACX,aAAa,CAIf;AAED,kEAAkE;AAClE,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,GAAG,QAAQ,CAGjE,CAAC"}
1
+ {"version":3,"file":"specials.d.ts","sourceRoot":"","sources":["../../../src/sources/react-bulma-components/specials.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAE3C,OAAO,EAWL,KAAK,gBAAgB,EACtB,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EAQL,KAAK,aAAa,EACnB,MAAM,8BAA8B,CAAC;AAEtC,YAAY,EAAE,aAAa,EAAE,CAAC;AA+yB9B,wBAAgB,UAAU,CACxB,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,gBAAgB,EACrB,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,EAClB,OAAO,EAAE,GAAG,GACX,aAAa,CAIf;AAED,kEAAkE;AAClE,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,GAAG,QAAQ,CAGjE,CAAC"}
@@ -9,7 +9,8 @@
9
9
  */
10
10
  import { RESPONSIVE_BREAKPOINTS, UNIVERSAL_PROPS } from './mapping.js';
11
11
  import { addAttr, addTodo, attributesOf, buildJsxName, findAttr, literalValueOf, makeAttr, plainElement, removeAttr, resolveBooleanish, } from '../_shared/jsx-utils.js';
12
- import { alignTarget, makeStripModifierProps, mergeClassName, applyIconProps, parseIconClasses, } from '../_shared/specials-utils.js';
12
+ import { enforcePolymorphicProps } from '../_shared/polymorphic.js';
13
+ import { alignTarget, dropLinkAttrsForPlainTag, makeStripModifierProps, mergeClassName, applyIconProps, parseIconClasses, } from '../_shared/specials-utils.js';
13
14
  /* eslint-disable @typescript-eslint/no-explicit-any */
14
15
  /**
15
16
  * The attributes a plain-element rewrite keeps: the modifier strip decides
@@ -18,7 +19,12 @@ import { alignTarget, makeStripModifierProps, mergeClassName, applyIconProps, pa
18
19
  * takes as readily as the component did. `attributesOf` filters spreads out,
19
20
  * so passing its result alone dropped `{...rest}` with no TODO.
20
21
  */
21
- function keptAttrs(ctx, path, element, where) {
22
+ function keptAttrs(ctx, path, element, where, tag) {
23
+ // These replacements are built by hand rather than through
24
+ // `replaceWithPlain`, so the link-attribute cleanup has to be invoked here
25
+ // or it never runs for this source: `<Form.Help href="/x">` was becoming
26
+ // `<p className="help" href="/x">`.
27
+ dropLinkAttrsForPlainTag(ctx, path, element, tag, where);
22
28
  const kept = new Set(stripModifierProps(ctx, path, attributesOf(element), where));
23
29
  return (element.openingElement.attributes ?? []).filter((a) => a.type === 'JSXSpreadAttribute' || kept.has(a));
24
30
  }
@@ -135,6 +141,10 @@ const SPECIALS = {
135
141
  if (headingTruthy && !subtitleDynamic) {
136
142
  removeAttr(element, headingAttr);
137
143
  const className = mergeClassName(ctx, path, element, subtitleTruthy ? 'heading subtitle' : 'heading', 'Heading');
144
+ // The seventh hand-built plain rewrite in this file, and the one that
145
+ // does not go through `keptAttrs` -- so it needs the cleanup by name or
146
+ // `<Heading renderAs="a" href="/x">` lands an `href` on the `<p>`.
147
+ dropLinkAttrsForPlainTag(ctx, path, element, 'p', 'Heading');
138
148
  const consumed = new Set(stripModifierProps(ctx, path, attributesOf(element).filter(a => !['size', 'weight', 'spaced', 'subtitle'].includes(a.name.name)), 'Heading'));
139
149
  const rest = (element.openingElement.attributes ?? []).filter((a) => a.type === 'JSXSpreadAttribute' || consumed.has(a));
140
150
  const replacement = plainElement(ctx.j, 'p', className, rest, element.children ?? []);
@@ -179,14 +189,33 @@ const SPECIALS = {
179
189
  * RBC Card.Image takes Image props directly; bestax Card.Image is a plain
180
190
  * wrapper — move the props onto a new inner <Image>.
181
191
  */
182
- 'card-image'(ctx, _path, element) {
192
+ 'card-image'(ctx, path, element) {
183
193
  const children = (element.children ?? []).filter((c) => !(c.type === 'JSXText' && c.value.trim() === ''));
184
- const attrs = element.openingElement.attributes ?? [];
185
- const imageAttrs = attrs.filter((a) => a.type === 'JSXAttribute' && a.name.name !== 'className');
186
- if (children.length > 0 || imageAttrs.length === 0) {
187
- // Already wrapping its own content — nothing to restructure.
194
+ // These props are about to become an `<Image>`'s, so they are judged
195
+ // against `Image` and not against the `Card.Image` wrapper they are
196
+ // written on. The special returns `replaced`, so the shared pass never
197
+ // sees the child it creates -- `<Card.Image href="/x">` was moving the
198
+ // `href` onto an `<Image>` that declares none.
199
+ const imageAttrsOf = () => (element.openingElement.attributes ?? []).filter((a) => a.type === 'JSXAttribute' && a.name.name !== 'className');
200
+ if (children.length > 0 || imageAttrsOf().length === 0) {
201
+ // Already wrapping its own content — nothing to restructure, and
202
+ // nothing here is destined for an `<Image>`. Bail before touching the
203
+ // props: renaming `renderAs` on this path left an `as` on the
204
+ // `Card.Image` wrapper, which has none, and put it beyond the reach of
205
+ // the ordinary `renderAs` mapping that would have flagged it.
188
206
  return {};
189
207
  }
208
+ // `renderAs` is renamed first: the mapping does that for a plain `Image`,
209
+ // but this path never reaches the prop passes, so without it the check
210
+ // below would look for an `as` that is still spelled the RBC way.
211
+ const renderAsAttr = findAttr(element, 'renderAs');
212
+ if (renderAsAttr)
213
+ renderAsAttr.name = ctx.j.jsxIdentifier('as');
214
+ enforcePolymorphicProps(ctx, path, element, 'Image');
215
+ // Read the attributes back AFTER that: it removes the ones `Image` cannot
216
+ // take, and a list captured beforehand would carry the removed nodes onto
217
+ // the child regardless.
218
+ const imageAttrs = imageAttrsOf();
190
219
  for (const attr of imageAttrs) {
191
220
  if (attr.name.name === 'size') {
192
221
  const literal = literalValueOf(attr);
@@ -199,7 +228,7 @@ const SPECIALS = {
199
228
  }
200
229
  }
201
230
  const inner = ctx.j.jsxElement(ctx.j.jsxOpeningElement(ctx.j.jsxIdentifier(ctx.reserve('Image')), imageAttrs, true), null, []);
202
- element.openingElement.attributes = attrs.filter((a) => a.type === 'JSXAttribute' && a.name.name === 'className');
231
+ element.openingElement.attributes = (element.openingElement.attributes ?? []).filter((a) => a.type === 'JSXAttribute' && a.name.name === 'className');
203
232
  element.children = [inner];
204
233
  element.openingElement.selfClosing = false;
205
234
  // The walker's rename pass rewrites both names to the final (possibly
@@ -319,7 +348,7 @@ const SPECIALS = {
319
348
  }
320
349
  }
321
350
  className = mergeClassName(ctx, path, element, className, 'Form.Label');
322
- const rest = keptAttrs(ctx, path, element, 'Form.Label');
351
+ const rest = keptAttrs(ctx, path, element, 'Form.Label', 'label');
323
352
  path.replace(plainElement(ctx.j, 'label', className, rest, element.children ?? []));
324
353
  ctx.dirty = true;
325
354
  return { replaced: true };
@@ -340,7 +369,7 @@ const SPECIALS = {
340
369
  }
341
370
  }
342
371
  className = mergeClassName(ctx, path, element, className, 'Form.Help');
343
- const rest = keptAttrs(ctx, path, element, 'Form.Help');
372
+ const rest = keptAttrs(ctx, path, element, 'Form.Help', 'p');
344
373
  path.replace(plainElement(ctx.j, 'p', className, rest, element.children ?? []));
345
374
  ctx.dirty = true;
346
375
  return { replaced: true };
@@ -394,7 +423,7 @@ const SPECIALS = {
394
423
  /** RBC Loader is a plain <div class="loader"> — keep exactly that. */
395
424
  'plain-loader'(ctx, path, element) {
396
425
  const className = mergeClassName(ctx, path, element, 'loader', 'Loader');
397
- const rest = keptAttrs(ctx, path, element, 'Loader');
426
+ const rest = keptAttrs(ctx, path, element, 'Loader', 'div');
398
427
  path.replace(plainElement(ctx.j, 'div', className, rest, element.children ?? []));
399
428
  ctx.dirty = true;
400
429
  return { replaced: true };
@@ -441,7 +470,7 @@ const SPECIALS = {
441
470
  removeAttr(element, activeAttr);
442
471
  }
443
472
  className = mergeClassName(ctx, path, element, className, 'Panel.Tabs.Tab');
444
- const rest = keptAttrs(ctx, path, element, 'Panel.Tabs.Tab');
473
+ const rest = keptAttrs(ctx, path, element, 'Panel.Tabs.Tab', 'a');
445
474
  path.replace(plainElement(ctx.j, 'a', className, rest, element.children ?? []));
446
475
  ctx.dirty = true;
447
476
  return { replaced: true };
@@ -461,7 +490,7 @@ const SPECIALS = {
461
490
  removeAttr(element, activeAttr);
462
491
  }
463
492
  liClass = mergeClassName(ctx, path, element, liClass, 'Breadcrumb.Item');
464
- const anchorAttrs = keptAttrs(ctx, path, element, 'Breadcrumb.Item');
493
+ const anchorAttrs = keptAttrs(ctx, path, element, 'Breadcrumb.Item', 'a');
465
494
  const children = element.children ?? [];
466
495
  const solidChildren = children.filter((c) => !(c.type === 'JSXText' && c.value.trim() === ''));
467
496
  // `<Breadcrumb.Item><a href>…</a></Breadcrumb.Item>` already carries its
@@ -497,7 +526,7 @@ const SPECIALS = {
497
526
  return { replaced: true };
498
527
  }
499
528
  const className = mergeClassName(ctx, path, element, 'table-container', 'Table.Container');
500
- const rest = keptAttrs(ctx, path, element, 'Table.Container');
529
+ const rest = keptAttrs(ctx, path, element, 'Table.Container', 'div');
501
530
  path.replace(plainElement(ctx.j, 'div', className, rest, element.children ?? []));
502
531
  ctx.dirty = true;
503
532
  return { replaced: true };
@@ -1 +1 @@
1
- {"version":3,"file":"transform.d.ts","sourceRoot":"","sources":["../../../src/sources/react-bulma-components/transform.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AA4CvD,MAAM,CAAC,OAAO,UAAU,SAAS,CAC/B,QAAQ,EAAE,QAAQ,EAClB,GAAG,EAAE,GAAG,EACR,OAAO,GAAE,gBAAqB,GAC7B,MAAM,GAAG,SAAS,CAq8BpB"}
1
+ {"version":3,"file":"transform.d.ts","sourceRoot":"","sources":["../../../src/sources/react-bulma-components/transform.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AA6CvD,MAAM,CAAC,OAAO,UAAU,SAAS,CAC/B,QAAQ,EAAE,QAAQ,EAClB,GAAG,EAAE,GAAG,EACR,OAAO,GAAE,gBAAqB,GAC7B,MAAM,GAAG,SAAS,CAy8BpB"}
@@ -16,6 +16,7 @@
16
16
  import { MAPPING, UNIVERSAL_PROPS, resolveMapping } from './mapping.js';
17
17
  import { addTodo, attributesOf, jsxNameParts, renameElement, } from '../_shared/jsx-utils.js';
18
18
  import { applyPropAction, applyUniversalProps } from '../_shared/props.js';
19
+ import { enforcePolymorphicProps } from '../_shared/polymorphic.js';
19
20
  import { collectBoundNames, makeAliasRegistry, makeReserve, nameOf, prefersTabs, resolvesToBinding, } from '../_shared/imports.js';
20
21
  import { flattenResponsiveProps } from './responsive.js';
21
22
  import { RESPONSIVE_KINDS, runSpecial } from './specials.js';
@@ -451,6 +452,9 @@ export default function transform(fileInfo, api, options = {}) {
451
452
  }
452
453
  }
453
454
  applyUniversalProps(ctx, path, element, handled, UNIVERSAL_PROPS);
455
+ // Last: bestax's props follow `as`, so the element decides which of the
456
+ // props just written it can actually take.
457
+ enforcePolymorphicProps(ctx, path, element, target);
454
458
  });
455
459
  // ---- 2b. Value references to RBC components ---------------------------
456
460
  // Components can be referenced as plain values too (`renderAs={Block}`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bestax-migrate",
3
- "version": "2.3.0",
3
+ "version": "2.3.2",
4
4
  "description": "Codemods for migrating existing React apps from other Bulma libraries to @allxsmith/bestax-bulma",
5
5
  "type": "module",
6
6
  "bin": {
@@ -37,7 +37,8 @@
37
37
  "@babel/parser": "^7.29.8",
38
38
  "chalk": "^6.0.0",
39
39
  "commander": "^15.0.0",
40
- "jscodeshift": "^17.3.0"
40
+ "jscodeshift": "^17.3.0",
41
+ "@allxsmith/bestax-bulma": "^5.15.1"
41
42
  },
42
43
  "devDependencies": {
43
44
  "@jest/globals": "^30.0.0",
@@ -50,8 +51,7 @@
50
51
  "react": "^19.2.8",
51
52
  "react-dom": "^19.2.8",
52
53
  "ts-jest": "^29.4.12",
53
- "typescript": "^6.0.3",
54
- "@allxsmith/bestax-bulma": "^5.15.0"
54
+ "typescript": "^6.0.3"
55
55
  },
56
56
  "publishConfig": {
57
57
  "access": "public"