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.
- package/dist/sources/_shared/jsx-utils.d.ts +7 -0
- package/dist/sources/_shared/jsx-utils.d.ts.map +1 -1
- package/dist/sources/_shared/jsx-utils.js +24 -1
- package/dist/sources/_shared/polymorphic.d.ts +92 -0
- package/dist/sources/_shared/polymorphic.d.ts.map +1 -0
- package/dist/sources/_shared/polymorphic.js +593 -0
- package/dist/sources/_shared/specials-utils.d.ts +9 -0
- package/dist/sources/_shared/specials-utils.d.ts.map +1 -1
- package/dist/sources/_shared/specials-utils.js +47 -1
- package/dist/sources/bloomer/mapping.d.ts.map +1 -1
- package/dist/sources/bloomer/mapping.js +0 -12
- package/dist/sources/bloomer/specials.d.ts.map +1 -1
- package/dist/sources/bloomer/specials.js +93 -19
- package/dist/sources/bloomer/transform.d.ts.map +1 -1
- package/dist/sources/bloomer/transform.js +4 -0
- package/dist/sources/rbx/mapping.d.ts.map +1 -1
- package/dist/sources/rbx/mapping.js +13 -1
- package/dist/sources/rbx/transform.d.ts.map +1 -1
- package/dist/sources/rbx/transform.js +7 -0
- package/dist/sources/react-bulma-components/mapping.d.ts.map +1 -1
- package/dist/sources/react-bulma-components/mapping.js +7 -0
- package/dist/sources/react-bulma-components/specials.d.ts.map +1 -1
- package/dist/sources/react-bulma-components/specials.js +43 -14
- package/dist/sources/react-bulma-components/transform.d.ts.map +1 -1
- package/dist/sources/react-bulma-components/transform.js +4 -0
- package/package.json +4 -4
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bestax's DOM props follow `as`: an element's attributes are the attributes
|
|
3
|
+
* of whatever `as` renders, and a few components narrow `as` to a literal
|
|
4
|
+
* union of the elements Bulma's markup allows there. Every source library
|
|
5
|
+
* this package migrates from is looser — rbx and react-bulma-components take
|
|
6
|
+
* any tag, bloomer took any `tag` — so a faithful prop-for-prop rename emits
|
|
7
|
+
* shapes that do not compile against the library:
|
|
8
|
+
*
|
|
9
|
+
* - an `as` naming an element the bestax component does not offer;
|
|
10
|
+
* - an `href` beside an `as` that is not an `<a>`;
|
|
11
|
+
* - an `href` on a component that declares none at any `as`;
|
|
12
|
+
* - `target` and its siblings on an element, or a component, without them.
|
|
13
|
+
*
|
|
14
|
+
* All are dropped with a TODO rather than carried, because carrying them
|
|
15
|
+
* hands the user a project that does not typecheck, which is the one thing a
|
|
16
|
+
* codemod must not do.
|
|
17
|
+
*
|
|
18
|
+
* What that costs differs by case, and each TODO says so. Dropping an `href`
|
|
19
|
+
* or a `target` costs nothing the source had: they sat on a `<span>` or a
|
|
20
|
+
* `<div>`, where no browser acts on them. Dropping an `as` does change the
|
|
21
|
+
* element — every source here rendered the tag it was given, and bestax
|
|
22
|
+
* renders its own instead — so that TODO names the tag to restore rather than
|
|
23
|
+
* claiming nothing moved.
|
|
24
|
+
*
|
|
25
|
+
* Source-agnostic: what it knows is bestax's side of the rename, so all three
|
|
26
|
+
* sources run it over every element after their prop passes.
|
|
27
|
+
*/
|
|
28
|
+
import { addTodo, attrSource, findAttr, literalValueOf, removeAttr, } from './jsx-utils.js';
|
|
29
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
30
|
+
/**
|
|
31
|
+
* The bestax components whose `as` is a closed literal union, by the name the
|
|
32
|
+
* mappings use as their `target`. Every other component either takes no `as`
|
|
33
|
+
* (`restrictAsToTargets` handles those) or is generic over `React.ElementType`
|
|
34
|
+
* and accepts any tag.
|
|
35
|
+
*
|
|
36
|
+
* `as-unions.test.ts` holds each row to the library's own type, so a union
|
|
37
|
+
* that widens or narrows in bulma-ui fails here rather than drifting into
|
|
38
|
+
* output that does not compile.
|
|
39
|
+
*/
|
|
40
|
+
const AS_UNION_TABLE = {
|
|
41
|
+
Control: ['div', 'p'],
|
|
42
|
+
'Dropdown.Item': ['a', 'div', 'button'],
|
|
43
|
+
Footer: ['footer', 'div'],
|
|
44
|
+
Image: ['figure', 'div', 'p'],
|
|
45
|
+
'Level.Item': ['div', 'p', 'a'],
|
|
46
|
+
Media: ['article', 'div'],
|
|
47
|
+
'Media.Left': ['figure', 'div'],
|
|
48
|
+
SubTitle: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p'],
|
|
49
|
+
Title: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p'],
|
|
50
|
+
};
|
|
51
|
+
export const AS_UNIONS = AS_UNION_TABLE;
|
|
52
|
+
/**
|
|
53
|
+
* The targets that accept an `as` beyond a narrowed union -- generic over
|
|
54
|
+
* `React.ElementType`, so any tag is valid. Every other target either narrows
|
|
55
|
+
* `as` (the table above) or declares none at all.
|
|
56
|
+
*
|
|
57
|
+
* That last group is why this set has to exist. An earlier pass flags an `as`
|
|
58
|
+
* a component cannot take and leaves the attribute in place as the marker for
|
|
59
|
+
* its TODO, so reading the element off whatever `as` is present would believe
|
|
60
|
+
* a prop the component never had -- and `<Panel.Block as="span" href="/x">`
|
|
61
|
+
* would lose the `href` that compiles and keep the `as` that does not.
|
|
62
|
+
*/
|
|
63
|
+
const AS_ANY = new Set(['Button', 'Menu.Item', 'Navbar.Item', 'Navbar.Link']);
|
|
64
|
+
/**
|
|
65
|
+
* Where an `href` can live, by bestax target -- the value is the element the
|
|
66
|
+
* component renders when no `as` is given. An `href` survives only where that
|
|
67
|
+
* element is the anchor, or where `as="a"` names one; a target absent from
|
|
68
|
+
* this table takes no `href` at any `as`, which is most of them.
|
|
69
|
+
*
|
|
70
|
+
* Stated this way round on purpose. The source libraries put an `href` on
|
|
71
|
+
* anything, and a table of what cannot take one is a list nobody can keep
|
|
72
|
+
* complete -- the first version of this rule named four targets and missed
|
|
73
|
+
* `Tabs.Item`, `Media.Left`, `Card.Image` and ninety more. Note the two
|
|
74
|
+
* `Pagination` controls: a source reaches them through a `special`, not a
|
|
75
|
+
* `target:` line, so a table built by reading the mappings alone misses them.
|
|
76
|
+
*
|
|
77
|
+
* The default element is here rather than a bare "takes an href" flag because
|
|
78
|
+
* typechecking cannot answer the question on its own. `Level.Item` declares
|
|
79
|
+
* `href` on its props at every `as`, so `<Level.Item href="/x">` compiles --
|
|
80
|
+
* but it renders a <div> and forwards `href` only when the tag is an `<a>`
|
|
81
|
+
* (`bulma-ui/src/layout/Level.tsx:227`), so the attribute is dropped at
|
|
82
|
+
* runtime. A table built from the types alone calls that supported; it is the
|
|
83
|
+
* same dead attribute this pass exists to remove.
|
|
84
|
+
*/
|
|
85
|
+
const HREF_OK = {
|
|
86
|
+
Button: 'button',
|
|
87
|
+
'Level.Item': 'div',
|
|
88
|
+
'Menu.Item': 'a',
|
|
89
|
+
'Navbar.Item': 'a',
|
|
90
|
+
'Navbar.Link': 'a',
|
|
91
|
+
'Pagination.Link': 'a',
|
|
92
|
+
'Pagination.Next': 'a',
|
|
93
|
+
'Pagination.Previous': 'a',
|
|
94
|
+
'Panel.Block': 'a',
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Where "put an <a> inside" is not the useful advice. bloomer's navbar handler
|
|
98
|
+
* already said this for its own source; the other two reached the same target
|
|
99
|
+
* and got the generic line, which is the sibling drift `bestax-migrate`'s
|
|
100
|
+
* CLAUDE.md warns about. Keyed by target, all three get it.
|
|
101
|
+
*/
|
|
102
|
+
const NO_HREF_HINT = {
|
|
103
|
+
'Navbar.Dropdown': 'bestax `Navbar.Dropdown` is the container and takes no `href`; put it on the `<Navbar.Link>` inside',
|
|
104
|
+
// `Delete` renders a self-closing <button/> with no `{children}` at all, so
|
|
105
|
+
// the generic "put an <a> inside it" is advice nobody can follow. bloomer's
|
|
106
|
+
// mapping said the right thing here before the entry moved into this table.
|
|
107
|
+
Delete: 'bestax `Delete` renders a <button> with no children and has no anchor form; wrap it in an <a>, or navigate in `onClick`',
|
|
108
|
+
'Card.Header.Icon': 'bestax `Card.Header.Icon` renders a <button>, so an <a> inside it would nest interactive elements; navigate in `onClick`, or wrap the whole icon in an <a>',
|
|
109
|
+
'Dropdown.Item': 'bestax `Dropdown.Item` declares no `href` and already renders an <a> by default, so an <a> inside would nest anchors; navigate in `onClick`',
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* Where to put a link on a target, for the attributes beside `href`. The same
|
|
113
|
+
* fact as `NO_HREF_HINT` in its short form -- fixing the `Delete` advice in
|
|
114
|
+
* one branch and leaving the sibling saying "put it on an <a> inside" to a
|
|
115
|
+
* component with no children is how these two drifted apart the first time.
|
|
116
|
+
*/
|
|
117
|
+
const LINK_REMEDY = {
|
|
118
|
+
Delete: 'wrap it in an <a>, or navigate in `onClick`',
|
|
119
|
+
'Navbar.Dropdown': 'put it on the `<Navbar.Link>` inside',
|
|
120
|
+
// Both already render interactive elements -- a <button> and an <a> -- so
|
|
121
|
+
// "put an <a> inside" would nest one inside the other, which is invalid.
|
|
122
|
+
'Card.Header.Icon': 'navigate in `onClick`, or wrap the whole icon in an <a>',
|
|
123
|
+
'Dropdown.Item': 'navigate in `onClick`',
|
|
124
|
+
};
|
|
125
|
+
const remedyFor = (target) => LINK_REMEDY[target] ?? 'put it on an <a> inside, or change the element';
|
|
126
|
+
/** The targets whose `as` this pass may believe. */
|
|
127
|
+
export function declaresAs(target) {
|
|
128
|
+
return target in AS_UNIONS || AS_ANY.has(target);
|
|
129
|
+
}
|
|
130
|
+
/** Whether `target` renders `value` when told `as={value}`. */
|
|
131
|
+
function acceptsAs(target, value) {
|
|
132
|
+
const union = AS_UNIONS[target];
|
|
133
|
+
return union ? union.includes(value) : AS_ANY.has(target);
|
|
134
|
+
}
|
|
135
|
+
/** The rows the type test holds to the library. */
|
|
136
|
+
export const HREF_TABLE = HREF_OK;
|
|
137
|
+
export const AS_ANY_TARGETS = [...AS_ANY].sort();
|
|
138
|
+
/**
|
|
139
|
+
* The `as` value that keeps an `href`. Only the anchor, and not because it is
|
|
140
|
+
* the only intrinsic React types an `href` onto -- `area`, `link` and `base`
|
|
141
|
+
* carry one too, and a target generic over `as` accepts them.
|
|
142
|
+
*
|
|
143
|
+
* They are excluded because the components disagree about what they forward,
|
|
144
|
+
* so no shared set is right. `Menu.Item` strips `href` unless the tag is an
|
|
145
|
+
* `<a>` or a custom component (`bulma-ui/src/components/Menu.tsx:212-222`),
|
|
146
|
+
* while `Button` routes everything but `'button'` through its anchor path and
|
|
147
|
+
* would forward it. Keeping `href` beside `as="area"` therefore typechecks on
|
|
148
|
+
* both and does nothing on one of them, which is the silently-dead attribute
|
|
149
|
+
* this pass exists to remove. Dropping it is announced and the TODO quotes
|
|
150
|
+
* the value; keeping it is not. No source here emits `as="area"` anyway, and
|
|
151
|
+
* an `<area href>` outside a `<map>` navigates nowhere regardless.
|
|
152
|
+
*/
|
|
153
|
+
const ANCHOR = 'a';
|
|
154
|
+
/**
|
|
155
|
+
* The elements each link attribute is valid on. Not a blanket "anchor-only"
|
|
156
|
+
* list, because they are not: `referrerPolicy` is real on `<img>` and
|
|
157
|
+
* `<script>`, `target` on `<form>`, `rel` on `<link>`. Treating them as one
|
|
158
|
+
* set deleted working attributes from those elements.
|
|
159
|
+
*
|
|
160
|
+
* They still have to be checked. Dropping the `href` alone left
|
|
161
|
+
* `<Navbar.Link as="span" target="_blank">`, which does not compile either --
|
|
162
|
+
* and the check has to run whether or not an `href` was there to start with,
|
|
163
|
+
* since that shape is just as invalid without one.
|
|
164
|
+
*/
|
|
165
|
+
const LINK_ATTR_ELEMENTS = {
|
|
166
|
+
target: ['a', 'area', 'base', 'form'],
|
|
167
|
+
download: ['a', 'area'],
|
|
168
|
+
hrefLang: ['a', 'area', 'link'],
|
|
169
|
+
ping: ['a'],
|
|
170
|
+
referrerPolicy: ['a', 'area', 'iframe', 'img', 'link', 'script'],
|
|
171
|
+
media: ['a', 'area', 'link', 'meta', 'source', 'style'],
|
|
172
|
+
};
|
|
173
|
+
// `type` is the other member `AnchorHTMLAttributes` adds, and it is
|
|
174
|
+
// deliberately absent. It is not a link attribute in any useful sense: it is
|
|
175
|
+
// an ordinary prop on form controls and buttons, which bestax components
|
|
176
|
+
// declare in their own right. This rule cannot tell "the anchor contributed
|
|
177
|
+
// it" from "the component owns it", and treating it as a link attribute
|
|
178
|
+
// stripped `type="text"` off every `<Input>` in the fixtures. So
|
|
179
|
+
// `<Button as="span" type="button">` still ships a type error -- one the
|
|
180
|
+
// source wrote rather than one the codemod created, which is the trade.
|
|
181
|
+
// `rel` is deliberately absent: React declares it on `HTMLAttributes`, so it
|
|
182
|
+
// is valid on every intrinsic and there is nothing to remove. The same point
|
|
183
|
+
// `bulma-ui/src/__typetests__/polymorphic.tsx` makes about it.
|
|
184
|
+
/**
|
|
185
|
+
* The link attributes a target accepts, where its props do not follow `as`.
|
|
186
|
+
*
|
|
187
|
+
* Most components here either follow `as` (the element decides, above) or
|
|
188
|
+
* extend `AnchorHTMLAttributes` outright (`Pagination.*`, `Panel.Block`, so
|
|
189
|
+
* everything is fine). `Level.Item` is the exception that enumerates: it
|
|
190
|
+
* declares `href`, `target` and `rel` and nothing else, so `download`,
|
|
191
|
+
* `hrefLang`, `ping` and `referrerPolicy` are type errors there even at
|
|
192
|
+
* `as="a"`.
|
|
193
|
+
*
|
|
194
|
+
* A target absent from `HREF_OK` needs no row: a component that takes no
|
|
195
|
+
* `href` at any `as` takes none of its siblings either -- verified for
|
|
196
|
+
* `Dropdown.Item`, `Card.FooterItem`, `Tabs.Item` and `Delete`, which is why
|
|
197
|
+
* this is derived from that table rather than being a second list to keep.
|
|
198
|
+
*/
|
|
199
|
+
const TARGET_LINK_ATTRS = {
|
|
200
|
+
'Level.Item': ['target'],
|
|
201
|
+
};
|
|
202
|
+
/** The rows the type test holds to the library. */
|
|
203
|
+
export const TARGET_LINK_ATTR_TABLE = TARGET_LINK_ATTRS;
|
|
204
|
+
/**
|
|
205
|
+
* The intrinsic elements React types an `href` onto.
|
|
206
|
+
*
|
|
207
|
+
* Only relevant to plain markup. On a bestax component the anchor is the only
|
|
208
|
+
* `as` that keeps an `href`, because the components disagree about what they
|
|
209
|
+
* forward (see `ANCHOR` below) -- but a plain `<area href>` has no component
|
|
210
|
+
* in the way, and it is valid.
|
|
211
|
+
*/
|
|
212
|
+
export const HREF_ELEMENTS = ['a', 'area', 'base', 'link'];
|
|
213
|
+
/**
|
|
214
|
+
* The HTML half of `JSX.IntrinsicElements`, as of `@types/react` 19.
|
|
215
|
+
*
|
|
216
|
+
* Regenerate by reading the keys between `interface IntrinsicElements {` and
|
|
217
|
+
* the `// SVG` comment in `@types/react/index.d.ts`. `as-unions.test.ts`
|
|
218
|
+
* holds the link tables to this set, so it is checked rather than trusted.
|
|
219
|
+
*
|
|
220
|
+
* SVG is excluded deliberately: `SVGAttributes` declares `href`, `media` and
|
|
221
|
+
* `target`, so every SVG tag would join those rows. What that means for a
|
|
222
|
+
* plain rewrite is `tagRejectsHref`'s problem, not a reason to widen this.
|
|
223
|
+
*/
|
|
224
|
+
export const HTML_INTRINSICS = [
|
|
225
|
+
'a',
|
|
226
|
+
'abbr',
|
|
227
|
+
'address',
|
|
228
|
+
'area',
|
|
229
|
+
'article',
|
|
230
|
+
'aside',
|
|
231
|
+
'audio',
|
|
232
|
+
'b',
|
|
233
|
+
'base',
|
|
234
|
+
'bdi',
|
|
235
|
+
'bdo',
|
|
236
|
+
'big',
|
|
237
|
+
'blockquote',
|
|
238
|
+
'body',
|
|
239
|
+
'br',
|
|
240
|
+
'button',
|
|
241
|
+
'canvas',
|
|
242
|
+
'caption',
|
|
243
|
+
'center',
|
|
244
|
+
'cite',
|
|
245
|
+
'code',
|
|
246
|
+
'col',
|
|
247
|
+
'colgroup',
|
|
248
|
+
'data',
|
|
249
|
+
'datalist',
|
|
250
|
+
'dd',
|
|
251
|
+
'del',
|
|
252
|
+
'details',
|
|
253
|
+
'dfn',
|
|
254
|
+
'dialog',
|
|
255
|
+
'div',
|
|
256
|
+
'dl',
|
|
257
|
+
'dt',
|
|
258
|
+
'em',
|
|
259
|
+
'embed',
|
|
260
|
+
'fieldset',
|
|
261
|
+
'figcaption',
|
|
262
|
+
'figure',
|
|
263
|
+
'footer',
|
|
264
|
+
'form',
|
|
265
|
+
'h1',
|
|
266
|
+
'h2',
|
|
267
|
+
'h3',
|
|
268
|
+
'h4',
|
|
269
|
+
'h5',
|
|
270
|
+
'h6',
|
|
271
|
+
'head',
|
|
272
|
+
'header',
|
|
273
|
+
'hgroup',
|
|
274
|
+
'hr',
|
|
275
|
+
'html',
|
|
276
|
+
'i',
|
|
277
|
+
'iframe',
|
|
278
|
+
'img',
|
|
279
|
+
'input',
|
|
280
|
+
'ins',
|
|
281
|
+
'kbd',
|
|
282
|
+
'keygen',
|
|
283
|
+
'label',
|
|
284
|
+
'legend',
|
|
285
|
+
'li',
|
|
286
|
+
'link',
|
|
287
|
+
'main',
|
|
288
|
+
'map',
|
|
289
|
+
'mark',
|
|
290
|
+
'menu',
|
|
291
|
+
'menuitem',
|
|
292
|
+
'meta',
|
|
293
|
+
'meter',
|
|
294
|
+
'nav',
|
|
295
|
+
'noindex',
|
|
296
|
+
'noscript',
|
|
297
|
+
'object',
|
|
298
|
+
'ol',
|
|
299
|
+
'optgroup',
|
|
300
|
+
'option',
|
|
301
|
+
'output',
|
|
302
|
+
'p',
|
|
303
|
+
'param',
|
|
304
|
+
'picture',
|
|
305
|
+
'pre',
|
|
306
|
+
'progress',
|
|
307
|
+
'q',
|
|
308
|
+
'rp',
|
|
309
|
+
'rt',
|
|
310
|
+
'ruby',
|
|
311
|
+
's',
|
|
312
|
+
'samp',
|
|
313
|
+
'script',
|
|
314
|
+
'search',
|
|
315
|
+
'section',
|
|
316
|
+
'select',
|
|
317
|
+
'slot',
|
|
318
|
+
'small',
|
|
319
|
+
'source',
|
|
320
|
+
'span',
|
|
321
|
+
'strong',
|
|
322
|
+
'style',
|
|
323
|
+
'sub',
|
|
324
|
+
'summary',
|
|
325
|
+
'sup',
|
|
326
|
+
'table',
|
|
327
|
+
'tbody',
|
|
328
|
+
'td',
|
|
329
|
+
'template',
|
|
330
|
+
'textarea',
|
|
331
|
+
'tfoot',
|
|
332
|
+
'th',
|
|
333
|
+
'thead',
|
|
334
|
+
'time',
|
|
335
|
+
'title',
|
|
336
|
+
'tr',
|
|
337
|
+
'track',
|
|
338
|
+
'u',
|
|
339
|
+
'ul',
|
|
340
|
+
'var',
|
|
341
|
+
'video',
|
|
342
|
+
'wbr',
|
|
343
|
+
'webview',
|
|
344
|
+
];
|
|
345
|
+
/**
|
|
346
|
+
* Whether a plain tag is known NOT to take an `href`.
|
|
347
|
+
*
|
|
348
|
+
* `HREF_ELEMENTS` is the HTML answer, and bloomer's `plainTag` can return any
|
|
349
|
+
* literal the source wrote -- including an SVG tag, where `SVGAttributes`
|
|
350
|
+
* declares `href`. Treating "not in the HTML list" as "takes no href" lost a
|
|
351
|
+
* working `href` off `<TabLink tag="use" href="#icon">`. An unrecognised tag
|
|
352
|
+
* is left alone: this pass removes attributes it can prove are dead, and it
|
|
353
|
+
* cannot prove that here.
|
|
354
|
+
*/
|
|
355
|
+
export function tagRejectsHref(tag) {
|
|
356
|
+
return HTML_INTRINSICS.includes(tag) && !HREF_ELEMENTS.includes(tag);
|
|
357
|
+
}
|
|
358
|
+
// `<style href>` is React 19 only -- it arrived with stylesheet hoisting, and
|
|
359
|
+
// `StyleHTMLAttributes` has no `href` in React 18. bulma-ui's peer range is
|
|
360
|
+
// `^18 || ^19` and CI builds both, so a row true of only one major would have
|
|
361
|
+
// the codemod emit output that fails in a supported project. These rows are
|
|
362
|
+
// the common denominator, which is not what a probe against the installed
|
|
363
|
+
// types answers -- only React 19's are here, and they said to add `style`.
|
|
364
|
+
/** Whether `element` renders `attr` legally -- exported for the plain-markup path. */
|
|
365
|
+
export function elementTakesLinkAttr(name, element) {
|
|
366
|
+
const allowed = LINK_ATTR_ELEMENTS[name];
|
|
367
|
+
return !allowed || allowed.includes(element);
|
|
368
|
+
}
|
|
369
|
+
/** The link attributes, for callers that iterate them. */
|
|
370
|
+
export const LINK_ATTRS = Object.keys(LINK_ATTR_ELEMENTS);
|
|
371
|
+
/** The rows the type test holds to React. */
|
|
372
|
+
export const LINK_ATTR_TABLE = LINK_ATTR_ELEMENTS;
|
|
373
|
+
/**
|
|
374
|
+
* Drop an `as` the bestax target cannot render, naming the elements it can.
|
|
375
|
+
*
|
|
376
|
+
* A literal only. A dynamic `as` is left exactly as written, and the reason is
|
|
377
|
+
* the one `rbx/mapping.ts` gives for its `AS_OK` entries: `as={SomeComponent}`
|
|
378
|
+
* against a narrowed union should surface as a type error the author reads,
|
|
379
|
+
* not a silent rewrite. Removing it here would delete a live reference to
|
|
380
|
+
* their component and quietly render something else.
|
|
381
|
+
*/
|
|
382
|
+
function restrictAsValue(ctx, path, element, target, attr, literal) {
|
|
383
|
+
const allowed = AS_UNIONS[target];
|
|
384
|
+
if (!allowed || !attr || !literal)
|
|
385
|
+
return;
|
|
386
|
+
if (literal.kind !== 'string' || allowed.includes(literal.value))
|
|
387
|
+
return;
|
|
388
|
+
const offered = allowed.map(a => `\`${a}\``).join(' / ');
|
|
389
|
+
removeAttr(element, attr);
|
|
390
|
+
addTodo(ctx, path, 'prop:as', `bestax \`${target}\` renders only ${offered}, so \`as="${literal.value}"\` cannot carry across — wrap it in a <${literal.value}> or restructure`);
|
|
391
|
+
ctx.dirty = true;
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Drop an `href` the element cannot take.
|
|
395
|
+
*
|
|
396
|
+
* The element is read from the `as` the target actually declares, never from
|
|
397
|
+
* whatever attribute happens to be spelled `as`, and from the value as it was
|
|
398
|
+
* written -- `restrictAsValue` may be about to remove it, and an element this
|
|
399
|
+
* pass has already forgotten cannot be judged.
|
|
400
|
+
*/
|
|
401
|
+
function dropInertHref(ctx, path, element, target, literal, elementKnown) {
|
|
402
|
+
const href = findAttr(element, 'href');
|
|
403
|
+
if (!href)
|
|
404
|
+
return;
|
|
405
|
+
const wasWritten = attrSource(ctx.j, href);
|
|
406
|
+
const drop = (why) => {
|
|
407
|
+
removeAttr(element, href);
|
|
408
|
+
const was = wasWritten ? ` -- it read \`${wasWritten}\`` : '';
|
|
409
|
+
addTodo(ctx, path, 'prop:href', `${why}${was}`);
|
|
410
|
+
ctx.dirty = true;
|
|
411
|
+
};
|
|
412
|
+
const defaultEl = HREF_OK[target];
|
|
413
|
+
if (!defaultEl) {
|
|
414
|
+
drop(NO_HREF_HINT[target] ??
|
|
415
|
+
`bestax \`${target}\` takes no \`href\` at any \`as\` -- navigate in \`onClick\`, or put an <a> inside it`);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
// Past here every answer depends on which element renders. A dynamic `as`
|
|
419
|
+
// may well be an anchor at runtime, and a spread may supply one; guessing
|
|
420
|
+
// either way is worse than leaving the pair for the author.
|
|
421
|
+
if (!elementKnown)
|
|
422
|
+
return;
|
|
423
|
+
if (literal && literal.kind !== 'string')
|
|
424
|
+
return;
|
|
425
|
+
// An `as` naming a tag the target does not render is dropped by
|
|
426
|
+
// `restrictAsValue`, so what renders is the component's own default -- the
|
|
427
|
+
// same element as if no `as` had been written at all.
|
|
428
|
+
const rendered = literal && literal.kind === 'string' && acceptsAs(target, literal.value)
|
|
429
|
+
? literal.value
|
|
430
|
+
: undefined;
|
|
431
|
+
if (rendered === undefined) {
|
|
432
|
+
if (defaultEl === ANCHOR)
|
|
433
|
+
return;
|
|
434
|
+
drop(`bestax \`${target}\` renders a <${defaultEl}> unless \`as\` says otherwise, and only its <a> form carries an \`href\` -- set \`as="a"\` to make this a link, or navigate in \`onClick\``);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
if (rendered === ANCHOR)
|
|
438
|
+
return;
|
|
439
|
+
// Which remedy is right depends on what the target renders with no `as`.
|
|
440
|
+
// "drop the `as`" is only a link on the targets whose bare element is the
|
|
441
|
+
// anchor; on `Button` it gives a <button> that will not compile, and on
|
|
442
|
+
// `Level.Item` a <div> that compiles and quietly is not a link.
|
|
443
|
+
const remedy = defaultEl === ANCHOR
|
|
444
|
+
? 'drop the `as` to make this a link, or put an <a> inside'
|
|
445
|
+
: `set \`as="a"\` to make this a link -- dropping the \`as\` gives you a <${defaultEl}> -- or put an <a> inside`;
|
|
446
|
+
drop(`\`href\` on \`as="${rendered}"\`: bestax gives an element the attributes of the tag \`as\` names, and a <${rendered}> takes no \`href\` (it navigated nowhere in the source either) -- ${remedy}`);
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Drop a link attribute the rendered element does not take.
|
|
450
|
+
*
|
|
451
|
+
* Independent of `href`: `<Navbar.Link as="span" target="_blank">` is invalid
|
|
452
|
+
* with or without one, and the sources emit both shapes. Each attribute is
|
|
453
|
+
* judged against the element rather than as a group, so a `referrerPolicy` on
|
|
454
|
+
* an `<img>` and a `target` on a `<form>` stay.
|
|
455
|
+
*/
|
|
456
|
+
function dropInertLinkAttrs(ctx, path, element, target, rendered) {
|
|
457
|
+
const carriesLinks = HREF_OK[target] !== undefined;
|
|
458
|
+
const declared = TARGET_LINK_ATTRS[target];
|
|
459
|
+
for (const name of LINK_ATTRS) {
|
|
460
|
+
// Both have to allow it. A target that enumerates its props can be
|
|
461
|
+
// narrower than the element, and the element can be narrower than the
|
|
462
|
+
// target: `Level.Item` declares `target` but forwards it only when the
|
|
463
|
+
// tag is an `<a>`, so `<Level.Item as="p" target="_blank">` was keeping
|
|
464
|
+
// the same inert attribute this pass removes everywhere else.
|
|
465
|
+
// Which check rejects it decides both the outcome and what the TODO says.
|
|
466
|
+
// Keying the message off "the target has a row" instead told a
|
|
467
|
+
// `<Level.Item as="p" target>` reader that `Level.Item` does not declare
|
|
468
|
+
// `target` -- it does; the <p> is what refuses it.
|
|
469
|
+
let rejectedBy = null;
|
|
470
|
+
if (!carriesLinks)
|
|
471
|
+
rejectedBy = 'component';
|
|
472
|
+
else if (declared && !declared.includes(name))
|
|
473
|
+
rejectedBy = 'component';
|
|
474
|
+
// A dynamic `as` leaves the element unknown, and guessing either way is
|
|
475
|
+
// worse than leaving it for the author.
|
|
476
|
+
else if (rendered !== undefined && !elementTakesLinkAttr(name, rendered))
|
|
477
|
+
rejectedBy = 'element';
|
|
478
|
+
if (!rejectedBy)
|
|
479
|
+
continue;
|
|
480
|
+
const attr = findAttr(element, name);
|
|
481
|
+
if (!attr)
|
|
482
|
+
continue;
|
|
483
|
+
const was = attrSource(ctx.j, attr);
|
|
484
|
+
removeAttr(element, attr);
|
|
485
|
+
const because = rejectedBy === 'element'
|
|
486
|
+
? `\`${name}\` needs an element that takes it, and \`${target}\` renders a <${rendered}> here`
|
|
487
|
+
: !carriesLinks
|
|
488
|
+
? `bestax \`${target}\` is not a link at any \`as\`, so it takes no \`${name}\` either`
|
|
489
|
+
: `bestax \`${target}\` declares its own props rather than taking the element's, and \`${name}\` is not among them`;
|
|
490
|
+
addTodo(ctx, path, `prop:${name}`, `${because} -- ${was ? `it read \`${was}\`; ` : ''}${remedyFor(target)}`);
|
|
491
|
+
ctx.dirty = true;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* The `as` attribute only if no spread can overwrite it. `findAttr` reads by
|
|
496
|
+
* name and knows nothing about `{...rest}`, which JSX applies last-write-wins.
|
|
497
|
+
*/
|
|
498
|
+
function lastWordOnAs(element, spreadCanCarryAs) {
|
|
499
|
+
const attrs = element.openingElement?.attributes ?? [];
|
|
500
|
+
let seen;
|
|
501
|
+
let shadowed = false;
|
|
502
|
+
for (const a of attrs) {
|
|
503
|
+
if (a.type === 'JSXSpreadAttribute') {
|
|
504
|
+
if (seen && spreadCanCarryAs) {
|
|
505
|
+
seen = undefined;
|
|
506
|
+
shadowed = true;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
else if (a.name?.name === 'as') {
|
|
510
|
+
seen = a;
|
|
511
|
+
shadowed = false;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return { attr: seen, shadowed };
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* The whole-element pass each source runs after its prop passes, once the
|
|
518
|
+
* bestax `target` and the final `as` are both known.
|
|
519
|
+
*/
|
|
520
|
+
export function enforcePolymorphicProps(ctx, path, element, target,
|
|
521
|
+
/**
|
|
522
|
+
* Whether a spread on this element could be carrying an `as`.
|
|
523
|
+
*
|
|
524
|
+
* Only where the SOURCE spells its element prop `as`, which is rbx alone.
|
|
525
|
+
* bloomer's is `tag` and react-bulma-components' is `renderAs`, and the
|
|
526
|
+
* rename only ever touches a literal attribute -- a `renderAs` key inside
|
|
527
|
+
* `{...rest}` never becomes an `as`. Guessing `true` everywhere let
|
|
528
|
+
* `<Button renderAs="span" {...rest} href="/x">` through untouched, which
|
|
529
|
+
* is the shape this pass exists to remove.
|
|
530
|
+
*/
|
|
531
|
+
spreadCanCarryAs = false) {
|
|
532
|
+
// Read the `as` once, before either rule can remove it, and only where the
|
|
533
|
+
// target declares one at all -- and only when JSX precedence says this
|
|
534
|
+
// attribute is the one that wins. A spread after it overwrites it, so
|
|
535
|
+
// `<Button as="span" {...p} href="/x">` with `p.as === "a"` rendered an
|
|
536
|
+
// anchor while this pass read `span` and deleted a working `href`. An `as`
|
|
537
|
+
// the spread can overwrite is treated as unknown, which is the same answer
|
|
538
|
+
// this pass already gives for a dynamic one.
|
|
539
|
+
const read = declaresAs(target)
|
|
540
|
+
? lastWordOnAs(element, spreadCanCarryAs)
|
|
541
|
+
: { attr: undefined, shadowed: false };
|
|
542
|
+
// A spread with no `as` written beside it keeps the ordinary reading, and
|
|
543
|
+
// that is a settled decision rather than an oversight. `{...rest}` is on
|
|
544
|
+
// half the elements in a real app; treating every one as an unknown element
|
|
545
|
+
// would keep `href` on components that take none, which is the defect this
|
|
546
|
+
// pass exists to remove. It trades a silent invalid `href` for a reported
|
|
547
|
+
// dropped one, and the reported one is the better failure. Weighed against
|
|
548
|
+
// the alternative and chosen deliberately -- these are dead source
|
|
549
|
+
// libraries and the simpler rule is the right one. Do not re-open it
|
|
550
|
+
// without a real migration that it got wrong.
|
|
551
|
+
//
|
|
552
|
+
// `restrictAsValue` still runs on a shadowed `as`. An out-of-union literal
|
|
553
|
+
// is invalid exactly as written, and dead if the spread overwrites it, so
|
|
554
|
+
// removing it is right either way.
|
|
555
|
+
const attr = read.attr ?? findAttr(element, 'as');
|
|
556
|
+
const literal = attr ? literalValueOf(attr) : undefined;
|
|
557
|
+
// Shadowing makes the ELEMENT unknown, not the component. Returning here
|
|
558
|
+
// skipped the union check and the component-level `href` rule as well, so
|
|
559
|
+
// `<Image as="span" {...p} href="/x">` kept both an `as` outside `Image`'s
|
|
560
|
+
// union and an `href` it declares at no `as` -- two facts the spread cannot
|
|
561
|
+
// change.
|
|
562
|
+
dropInertHref(ctx, path, element, target, literal, !read.shadowed);
|
|
563
|
+
// The element as it will render: the `as` if the target takes it, otherwise
|
|
564
|
+
// the component's own. Unknown for a dynamic `as`, and unknown for a target
|
|
565
|
+
// outside `HREF_OK` that was given no `as` -- the default element is only
|
|
566
|
+
// recorded for the nine that can carry a link. So this rule reaches an
|
|
567
|
+
// explicit accepted `as` on any target, plus those nine bare; elsewhere it
|
|
568
|
+
// declines rather than guesses.
|
|
569
|
+
const rendered = read.shadowed
|
|
570
|
+
? undefined
|
|
571
|
+
: literal && literal.kind === 'string'
|
|
572
|
+
? acceptsAs(target, literal.value)
|
|
573
|
+
? literal.value
|
|
574
|
+
: HREF_OK[target]
|
|
575
|
+
: literal
|
|
576
|
+
? undefined
|
|
577
|
+
: HREF_OK[target];
|
|
578
|
+
dropInertLinkAttrs(ctx, path, element, target, rendered);
|
|
579
|
+
restrictAsValue(ctx, path, element, target, attr, literal);
|
|
580
|
+
// Only now, and only if something element-dependent actually survived. The
|
|
581
|
+
// first version fired on every `as` beside a spread -- `<Title as="h2"
|
|
582
|
+
// {...rest}>` is a complete migration with nothing at stake and was getting
|
|
583
|
+
// a TODO -- and it ran before the passes, so it also claimed attributes
|
|
584
|
+
// were "left as written" that the component-level rules had just removed.
|
|
585
|
+
if (read.shadowed) {
|
|
586
|
+
const left = ['href', ...LINK_ATTRS].filter(n => findAttr(element, n));
|
|
587
|
+
if (left.length) {
|
|
588
|
+
addTodo(ctx, path, 'prop:as', `a spread after \`as\` can overwrite it, so which element \`${target}\` renders here is not knowable -- ${left
|
|
589
|
+
.map(n => `\`${n}\``)
|
|
590
|
+
.join(', ')} ${left.length > 1 ? 'were' : 'was'} left as written, and whether ${left.length > 1 ? 'they are' : 'it is'} valid depends on what the spread supplies`);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
@@ -76,6 +76,15 @@ export declare function modifierClass(ctx: TransformContext, path: ASTPath<any>,
|
|
|
76
76
|
* it is picked.
|
|
77
77
|
*/
|
|
78
78
|
export declare function restrictAsToTargets(ctx: TransformContext, path: ASTPath<any>, element: any, target: string | undefined, allowed: string[], prop?: string): void;
|
|
79
|
+
/**
|
|
80
|
+
* Strip the link attributes a plain element cannot carry.
|
|
81
|
+
*
|
|
82
|
+
* Exported because not every source reaches plain markup through
|
|
83
|
+
* `replaceWithPlain`: react-bulma-components builds six of its replacements
|
|
84
|
+
* by hand, and they bypassed this entirely -- `<Form.Help href="/x">` became
|
|
85
|
+
* `<p className="help" href="/x">`, which does not compile.
|
|
86
|
+
*/
|
|
87
|
+
export declare function dropLinkAttrsForPlainTag(ctx: TransformContext, path: ASTPath<any>, element: any, tag: string, where: string): void;
|
|
79
88
|
/** Attribute filter a source applies before an element becomes plain HTML. */
|
|
80
89
|
export type AttrStrip = (ctx: TransformContext, path: ASTPath<any>, attrs: any[], where: string) => any[];
|
|
81
90
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"specials-utils.d.ts","sourceRoot":"","sources":["../../../src/sources/_shared/specials-utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"specials-utils.d.ts","sourceRoot":"","sources":["../../../src/sources/_shared/specials-utils.ts"],"names":[],"mappings":"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"}
|