bestax-migrate 2.3.8 → 2.4.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.
Files changed (72) hide show
  1. package/README.md +21 -17
  2. package/dist/cli.d.ts +3 -0
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +45 -11
  5. package/dist/sources/_shared/bestax-import.d.ts +48 -0
  6. package/dist/sources/_shared/bestax-import.d.ts.map +1 -0
  7. package/dist/sources/_shared/bestax-import.js +142 -0
  8. package/dist/sources/_shared/css-imports.d.ts +33 -0
  9. package/dist/sources/_shared/css-imports.d.ts.map +1 -0
  10. package/dist/sources/_shared/css-imports.js +103 -0
  11. package/dist/sources/_shared/deps-common.d.ts +51 -0
  12. package/dist/sources/_shared/deps-common.d.ts.map +1 -0
  13. package/dist/sources/_shared/deps-common.js +115 -0
  14. package/dist/sources/_shared/jsx-utils.d.ts +28 -0
  15. package/dist/sources/_shared/jsx-utils.d.ts.map +1 -1
  16. package/dist/sources/_shared/jsx-utils.js +59 -1
  17. package/dist/sources/_shared/make-styles-transform.d.ts +6 -2
  18. package/dist/sources/_shared/make-styles-transform.d.ts.map +1 -1
  19. package/dist/sources/_shared/make-styles-transform.js +12 -4
  20. package/dist/sources/_shared/props.d.ts.map +1 -1
  21. package/dist/sources/_shared/props.js +2 -2
  22. package/dist/sources/_shared/specials-utils.d.ts +1 -1
  23. package/dist/sources/_shared/specials-utils.d.ts.map +1 -1
  24. package/dist/sources/_shared/specials-utils.js +19 -1
  25. package/dist/sources/bloomer/deps.d.ts.map +1 -1
  26. package/dist/sources/bloomer/deps.js +14 -89
  27. package/dist/sources/bloomer/transform.d.ts.map +1 -1
  28. package/dist/sources/bloomer/transform.js +12 -186
  29. package/dist/sources/bulma-classes/class-map.d.ts +161 -0
  30. package/dist/sources/bulma-classes/class-map.d.ts.map +1 -0
  31. package/dist/sources/bulma-classes/class-map.js +1046 -0
  32. package/dist/sources/bulma-classes/deps.d.ts +14 -0
  33. package/dist/sources/bulma-classes/deps.d.ts.map +1 -0
  34. package/dist/sources/bulma-classes/deps.js +56 -0
  35. package/dist/sources/bulma-classes/index.d.ts +3 -0
  36. package/dist/sources/bulma-classes/index.d.ts.map +1 -0
  37. package/dist/sources/bulma-classes/index.js +35 -0
  38. package/dist/sources/bulma-classes/plan.d.ts +53 -0
  39. package/dist/sources/bulma-classes/plan.d.ts.map +1 -0
  40. package/dist/sources/bulma-classes/plan.js +231 -0
  41. package/dist/sources/bulma-classes/project.d.ts +40 -0
  42. package/dist/sources/bulma-classes/project.d.ts.map +1 -0
  43. package/dist/sources/bulma-classes/project.js +203 -0
  44. package/dist/sources/bulma-classes/rules.d.ts +16 -0
  45. package/dist/sources/bulma-classes/rules.d.ts.map +1 -0
  46. package/dist/sources/bulma-classes/rules.js +53 -0
  47. package/dist/sources/bulma-classes/styles.d.ts +13 -0
  48. package/dist/sources/bulma-classes/styles.d.ts.map +1 -0
  49. package/dist/sources/bulma-classes/styles.js +18 -0
  50. package/dist/sources/bulma-classes/transform.d.ts +21 -0
  51. package/dist/sources/bulma-classes/transform.d.ts.map +1 -0
  52. package/dist/sources/bulma-classes/transform.js +486 -0
  53. package/dist/sources/rbx/deps.d.ts.map +1 -1
  54. package/dist/sources/rbx/deps.js +29 -117
  55. package/dist/sources/rbx/mapping.d.ts.map +1 -1
  56. package/dist/sources/rbx/mapping.js +9 -1
  57. package/dist/sources/rbx/specials.d.ts.map +1 -1
  58. package/dist/sources/rbx/specials.js +20 -10
  59. package/dist/sources/rbx/transform.d.ts.map +1 -1
  60. package/dist/sources/rbx/transform.js +22 -204
  61. package/dist/sources/react-bulma-components/deps.d.ts.map +1 -1
  62. package/dist/sources/react-bulma-components/deps.js +12 -87
  63. package/dist/sources/react-bulma-components/mapping.js +1 -1
  64. package/dist/sources/react-bulma-components/specials.d.ts.map +1 -1
  65. package/dist/sources/react-bulma-components/specials.js +42 -4
  66. package/dist/sources/react-bulma-components/transform.d.ts.map +1 -1
  67. package/dist/sources/react-bulma-components/transform.js +16 -187
  68. package/dist/sources/registry.d.ts.map +1 -1
  69. package/dist/sources/registry.js +2 -0
  70. package/dist/types.d.ts +25 -0
  71. package/dist/types.d.ts.map +1 -1
  72. package/package.json +4 -3
@@ -0,0 +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,CAYpC,CAAC;AAqBH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEnD;AAED,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAK1D"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Rule ids for the bulma-classes source's TODOs.
3
+ *
4
+ * A rule id reaches opt-in telemetry (`todosByRule`), and this source reads
5
+ * the app's own class strings, so an id must never carry one of those. Every
6
+ * id is `kind:token`, where the token comes from a closed vocabulary: a Bulma
7
+ * class this table knows, a bestax component, or an attribute or prop name
8
+ * the table itself lists. A token outside it is dropped and the id is just the
9
+ * kind, so a gap in the vocabulary costs detail, never the file. The planner
10
+ * tests sweep every refusal the table can produce and hold each id to the
11
+ * vocabulary, so a gap is still caught.
12
+ */
13
+ import { HELPER_PROPS, LEGACY_09, ROOTS, WRAPPERS, WRAPPER_OWN_PROPS, } from './class-map.js';
14
+ export const KINDS = new Set([
15
+ 'attr',
16
+ 'children',
17
+ 'defaults',
18
+ 'drops',
19
+ 'dynamic-class',
20
+ 'family',
21
+ 'legacy',
22
+ 'only-child',
23
+ 'ref',
24
+ 'spread',
25
+ 'tag',
26
+ ]);
27
+ const VOCABULARY = new Set([
28
+ ...Object.keys(ROOTS),
29
+ ...Object.keys(LEGACY_09),
30
+ ...Object.values(ROOTS).flatMap(entry => [
31
+ ...(entry.target ? [entry.target] : []),
32
+ ...(entry.ownProps ?? []),
33
+ ...(entry.passThrough ?? []),
34
+ ...(entry.untypedAttrs ?? []),
35
+ ...(entry.numberAttrs ?? []),
36
+ ...Object.keys(entry.defaults ?? {}),
37
+ ...Object.keys(entry.dropsAttr ?? {}),
38
+ ]),
39
+ ...Object.values(WRAPPERS),
40
+ ...Object.values(WRAPPER_OWN_PROPS).flat(),
41
+ ...HELPER_PROPS,
42
+ // Refused on every target, so no entry lists it.
43
+ 'dangerouslySetInnerHTML',
44
+ ]);
45
+ export function inVocabulary(token) {
46
+ return VOCABULARY.has(token);
47
+ }
48
+ export function ruleId(kind, token) {
49
+ if (!KINDS.has(kind)) {
50
+ throw new Error(`not a bulma-classes rule kind: ${kind}`);
51
+ }
52
+ return VOCABULARY.has(token) ? `${kind}:${token}` : kind;
53
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Stylesheet migration for an app that styled plain markup with Bulma. There
3
+ * is no source package, so the shared factory only rewrites the app's own
4
+ * Bulma 0.9 imports and variable overrides into Bulma v1's `@use` form.
5
+ *
6
+ * Only when asked. Under the default `--css keep` the app keeps its Bulma
7
+ * version (see deps.ts), and v1's `@use 'bulma/sass'` against a Bulma 0.9
8
+ * install would break the app's Sass build, so the pass leaves every
9
+ * stylesheet alone.
10
+ */
11
+ import type { StylesTransform } from '../../types.js';
12
+ export declare const transformStyles: StylesTransform;
13
+ //# sourceMappingURL=styles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../../../src/sources/bulma-classes/styles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAStD,eAAO,MAAM,eAAe,EAAE,eAQuB,CAAC"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Stylesheet migration for an app that styled plain markup with Bulma. There
3
+ * is no source package, so the shared factory only rewrites the app's own
4
+ * Bulma 0.9 imports and variable overrides into Bulma v1's `@use` form.
5
+ *
6
+ * Only when asked. Under the default `--css keep` the app keeps its Bulma
7
+ * version (see deps.ts), and v1's `@use 'bulma/sass'` against a Bulma 0.9
8
+ * install would break the app's Sass build, so the pass leaves every
9
+ * stylesheet alone.
10
+ */
11
+ import { makeStylesTransform } from '../_shared/make-styles-transform.js';
12
+ const toBulmaV1 = makeStylesTransform({
13
+ guideUrl: 'https://bestax.io/docs/guides/getting-started/migration/bulma-classes',
14
+ rootStylesheetSuffixes: [],
15
+ });
16
+ export const transformStyles = (filePath, source, collector, options) => (options.cssMode ?? 'keep') === 'keep'
17
+ ? null
18
+ : toBulmaV1(filePath, source, collector, options);
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Raw Bulma classes on plain JSX → @allxsmith/bestax-bulma components.
3
+ *
4
+ * Unlike the library sources there is no import to anchor on: a file is in
5
+ * scope when a lowercase JSX element carries a `className` with a Bulma class
6
+ * `class-map.ts` knows. Each such element goes through `plan()`, which
7
+ * converts it only when the component renders exactly the same markup.
8
+ *
9
+ * Passes, in order:
10
+ * 1. the shared stylesheet-import pass (a no-op under the default `keep`)
11
+ * 2. plan every element in scope; a computed className is planned from the
12
+ * strings in it and becomes a TODO, never a conversion
13
+ * 3. file-level gates: a non-React JSX runtime, CommonJS, styled-jsx, and a
14
+ * Next.js server component (bestax's components are client components)
15
+ * 4. TODOs in reading order; conversions innermost first
16
+ * 5. write the bestax import, after the file's last import
17
+ */
18
+ import type { API, FileInfo } from 'jscodeshift';
19
+ import type { TransformOptions } from '../../types.js';
20
+ export default function transform(fileInfo: FileInfo, api: API, options?: TransformOptions): string | undefined;
21
+ //# sourceMappingURL=transform.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transform.d.ts","sourceRoot":"","sources":["../../../src/sources/bulma-classes/transform.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAW,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE1D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAuRvD,MAAM,CAAC,OAAO,UAAU,SAAS,CAC/B,QAAQ,EAAE,QAAQ,EAClB,GAAG,EAAE,GAAG,EACR,OAAO,GAAE,gBAAqB,GAC7B,MAAM,GAAG,SAAS,CA4QpB"}
@@ -0,0 +1,486 @@
1
+ /**
2
+ * Raw Bulma classes on plain JSX → @allxsmith/bestax-bulma components.
3
+ *
4
+ * Unlike the library sources there is no import to anchor on: a file is in
5
+ * scope when a lowercase JSX element carries a `className` with a Bulma class
6
+ * `class-map.ts` knows. Each such element goes through `plan()`, which
7
+ * converts it only when the component renders exactly the same markup.
8
+ *
9
+ * Passes, in order:
10
+ * 1. the shared stylesheet-import pass (a no-op under the default `keep`)
11
+ * 2. plan every element in scope; a computed className is planned from the
12
+ * strings in it and becomes a TODO, never a conversion
13
+ * 3. file-level gates: a non-React JSX runtime, CommonJS, styled-jsx, and a
14
+ * Next.js server component (bestax's components are client components)
15
+ * 4. TODOs in reading order; conversions innermost first
16
+ * 5. write the bestax import, after the file's last import
17
+ */
18
+ import path from 'node:path';
19
+ import { addTodo, findAttr, forgetJsxParens, jsxNameParts, literalValueOf, makeAttr, renameElement, reprintDirectives, } from '../_shared/jsx-utils.js';
20
+ import { collectBoundNames, makeReserve, prefersTabs, } from '../_shared/imports.js';
21
+ import { rewriteStylesheetImports } from '../_shared/css-imports.js';
22
+ import { BESTAX, buildBestaxImport, placeBestaxImport, seedBestaxImport, } from '../_shared/bestax-import.js';
23
+ import { plan } from './plan.js';
24
+ import { REACT_RUNTIMES, } from './project.js';
25
+ import { ruleId } from './rules.js';
26
+ /* eslint-disable @typescript-eslint/no-explicit-any */
27
+ /**
28
+ * Modules whose element factory is React's, for the classic
29
+ * `/** @jsx … *\/` pragma: React itself, and the `jsx` of Emotion and
30
+ * theme-ui, which wraps it.
31
+ */
32
+ const REACT_PRAGMA_SOURCES = new Set([
33
+ 'react',
34
+ '@emotion/react',
35
+ '@emotion/core',
36
+ 'theme-ui',
37
+ '@theme-ui/core',
38
+ ]);
39
+ /** A pragma line, the way Babel reads one: at the start of a comment line. */
40
+ const JSX_IMPORT_SOURCE = /^\s*\*?\s*@jsxImportSource\s+(\S+)/m;
41
+ const JSX_FACTORY = /^\s*\*?\s*@jsx\s+(\S+)/m;
42
+ /**
43
+ * File-level comments that must stay above everything else: type-checker and
44
+ * linter directives, JSX pragmas, licence markers. Not the next-line ones.
45
+ */
46
+ const HEADER_PRAGMA = /^[\s*]*(?:@ts-(?:no)?check|@flow|@noflow|eslint-disable(?!-)|@jsx|@jsxImportSource|@jsxRuntime|@license|@preserve)\b/;
47
+ /** Directives that act on the line after them, so must stay above it. */
48
+ const NEXT_LINE_DIRECTIVE = /^[\s*]*(?:eslint-disable-next-line|@ts-expect-error|@ts-ignore|prettier-ignore)\b/;
49
+ /** A lowercase intrinsic tag: not a component, not a custom element. */
50
+ const INTRINSIC = /^[a-z][a-z0-9]*$/;
51
+ /** Subtrees whose tags are not HTML. */
52
+ const FOREIGN_ROOTS = new Set(['svg', 'math']);
53
+ /** Parents that render their only child as it is. */
54
+ const PASS_THROUGH_PARENTS = new Set(['Fragment', 'React.Fragment']);
55
+ /**
56
+ * The static text of a `className`, or null when it is computed. A string
57
+ * literal, `{'…'}`, and a template literal with no expressions are static.
58
+ */
59
+ function staticClassName(attr) {
60
+ const value = attr.value;
61
+ if (!value)
62
+ return null;
63
+ if (value.type === 'StringLiteral')
64
+ return value.value;
65
+ if (value.type !== 'JSXExpressionContainer')
66
+ return null;
67
+ const expression = value.expression;
68
+ if (expression.type === 'StringLiteral')
69
+ return expression.value;
70
+ if (expression.type === 'TemplateLiteral' &&
71
+ expression.expressions.length === 0) {
72
+ return expression.quasis[0].value.cooked ?? null;
73
+ }
74
+ return null;
75
+ }
76
+ /**
77
+ * The class names a computed `className` can produce: its strings, template
78
+ * text and object keys (`cx('box', { 'is-active': on })`). A string used as a
79
+ * computed member (`styles['box']`) is a lookup key, not a class.
80
+ */
81
+ function classTokensOf(node) {
82
+ const out = [];
83
+ const visit = (current, parent, key) => {
84
+ if (!current || typeof current !== 'object')
85
+ return;
86
+ if (current.type === 'StringLiteral') {
87
+ const lookupKey = parent?.type === 'MemberExpression' &&
88
+ parent.computed &&
89
+ key === 'property';
90
+ if (!lookupKey)
91
+ out.push(current.value);
92
+ }
93
+ else if (current.type === 'TemplateElement') {
94
+ out.push(current.value.cooked ?? '');
95
+ }
96
+ else if ((current.type === 'ObjectProperty' || current.type === 'Property') &&
97
+ !current.computed &&
98
+ current.key?.type === 'Identifier') {
99
+ out.push(current.key.name);
100
+ }
101
+ for (const [childKey, child] of Object.entries(current)) {
102
+ if (childKey === 'loc' || childKey === 'comments')
103
+ continue;
104
+ if (Array.isArray(child)) {
105
+ for (const item of child)
106
+ visit(item, current, childKey);
107
+ }
108
+ else if (child && typeof child.type === 'string') {
109
+ visit(child, current, childKey);
110
+ }
111
+ }
112
+ };
113
+ visit(node, null, '');
114
+ return [...new Set(out.flatMap(text => text.split(/\s+/)).filter(Boolean))];
115
+ }
116
+ /** A string attribute's value, `true` for a bare one, else null. */
117
+ function attributeValue(attr) {
118
+ const literal = literalValueOf(attr);
119
+ if (literal.kind === 'string')
120
+ return literal.value;
121
+ return literal.kind === 'boolean' && literal.value ? true : null;
122
+ }
123
+ function attributeName(attr) {
124
+ return attr.name.type === 'JSXNamespacedName'
125
+ ? `${attr.name.namespace.name}:${attr.name.name.name}`
126
+ : attr.name.name;
127
+ }
128
+ /** Every comment in the file (recast hangs them on the nodes). */
129
+ function commentsOf(root, j) {
130
+ const comments = new Set();
131
+ root.find(j.Node).forEach((nodePath) => {
132
+ for (const comment of nodePath.node.comments ?? [])
133
+ comments.add(comment);
134
+ });
135
+ return [...comments];
136
+ }
137
+ /**
138
+ * The JSX runtime a file names for itself: `'react'`, another runtime's
139
+ * name, or null when it names none.
140
+ */
141
+ function fileJsxRuntime(root, j) {
142
+ const comments = commentsOf(root, j).map(comment => comment.value);
143
+ const importSource = comments
144
+ .map(text => text.match(JSX_IMPORT_SOURCE)?.[1])
145
+ .find(Boolean);
146
+ if (importSource) {
147
+ return REACT_RUNTIMES.has(importSource) ? 'react' : importSource;
148
+ }
149
+ const factory = comments
150
+ .map(text => text.match(JSX_FACTORY)?.[1])
151
+ .find(Boolean);
152
+ if (!factory)
153
+ return null;
154
+ // The module the factory is imported from names the runtime (`h` from
155
+ // preact is preact); a factory the file does not import is named as written.
156
+ const local = factory.split('.')[0];
157
+ const source = root
158
+ .find(j.ImportDeclaration)
159
+ .paths()
160
+ .map((importPath) => importPath.node)
161
+ .find((node) => (node.specifiers ?? []).some((spec) => spec.local?.name === local))?.source.value;
162
+ if (source === undefined) {
163
+ return factory === 'React.createElement' ? 'react' : factory;
164
+ }
165
+ return REACT_PRAGMA_SOURCES.has(String(source)) ? 'react' : String(source);
166
+ }
167
+ /** The runtime the innermost package around this file declares, if any. */
168
+ function projectJsxRuntime(file, runtimes) {
169
+ if (!Array.isArray(runtimes))
170
+ return null;
171
+ const resolved = path.resolve(file);
172
+ const found = runtimes
173
+ .filter(entry => resolved.startsWith(`${entry.dir}${path.sep}`))
174
+ .sort((a, b) => b.dir.length - a.dir.length)[0];
175
+ return found ? found.runtime : null;
176
+ }
177
+ /** Whether a file is CommonJS: `require` or `module.exports`, no ES modules. */
178
+ function isCommonJs(root, j) {
179
+ const esModule = root.find(j.ImportDeclaration).length > 0 ||
180
+ root.find(j.ExportNamedDeclaration).length > 0 ||
181
+ root.find(j.ExportDefaultDeclaration).length > 0 ||
182
+ root.find(j.ExportAllDeclaration).length > 0;
183
+ if (esModule)
184
+ return false;
185
+ const requires = root.find(j.CallExpression, {
186
+ callee: { type: 'Identifier', name: 'require' },
187
+ }).length > 0;
188
+ const moduleExports = root.find(j.MemberExpression, {
189
+ object: { type: 'Identifier', name: 'module' },
190
+ property: { name: 'exports' },
191
+ }).length > 0 ||
192
+ root.find(j.AssignmentExpression, {
193
+ left: { type: 'MemberExpression', object: { name: 'exports' } },
194
+ }).length > 0;
195
+ return requires || moduleExports;
196
+ }
197
+ /** Whether the file scopes styles with styled-jsx (`<style jsx>`). */
198
+ function usesStyledJsx(root, j) {
199
+ return (root
200
+ .find(j.JSXOpeningElement, { name: { name: 'style' } })
201
+ .filter((openingPath) => Boolean(findAttr(openingPath.parent.node, 'jsx'))).length > 0);
202
+ }
203
+ /** Whether a Next.js App Router may render this file as a server component. */
204
+ function mayBeServerComponent(file, roots) {
205
+ if (!Array.isArray(roots))
206
+ return false;
207
+ const resolved = path.resolve(file);
208
+ const inside = (dir) => resolved.startsWith(`${dir}${path.sep}`);
209
+ return roots.some(root => inside(root.dir) && !root.except.some(inside));
210
+ }
211
+ /** Whether a JSX child is real content, not whitespace or a comment. */
212
+ function isContent(child) {
213
+ if (child.type === 'JSXText')
214
+ return child.value.trim().length > 0;
215
+ if (child.type === 'JSXExpressionContainer') {
216
+ return child.expression.type !== 'JSXEmptyExpression';
217
+ }
218
+ return true;
219
+ }
220
+ function insideForeignContent(elementPath) {
221
+ let current = elementPath.parent;
222
+ while (current) {
223
+ const name = current.node?.openingElement?.name;
224
+ if (name?.type === 'JSXIdentifier' && FOREIGN_ROOTS.has(name.name)) {
225
+ return true;
226
+ }
227
+ current = current.parent;
228
+ }
229
+ return false;
230
+ }
231
+ /**
232
+ * The component this element is the only child of, when that component is
233
+ * not a bestax one or a fragment: `Link` for `<Link><a className="button">`.
234
+ */
235
+ function onlyChildOf(elementPath, bestaxLocals) {
236
+ const parent = elementPath.parent?.node;
237
+ if (parent?.type !== 'JSXElement')
238
+ return undefined;
239
+ const parts = jsxNameParts(parent.openingElement.name);
240
+ if (!parts || INTRINSIC.test(parts.join('.')))
241
+ return undefined;
242
+ const name = parts.join('.');
243
+ if (PASS_THROUGH_PARENTS.has(name) || bestaxLocals.has(parts[0])) {
244
+ return undefined;
245
+ }
246
+ const content = (parent.children ?? []).filter(isContent);
247
+ return content.length === 1 && content[0] === elementPath.node
248
+ ? name
249
+ : undefined;
250
+ }
251
+ export default function transform(fileInfo, api, options = {}) {
252
+ const source = fileInfo.source;
253
+ // Cheap pre-filter: no className and no stylesheet import, nothing to do.
254
+ if (!source.includes('className') && !source.includes('bulma')) {
255
+ return undefined;
256
+ }
257
+ const j = api.jscodeshift;
258
+ const root = j(source);
259
+ const ctx = {
260
+ j,
261
+ file: fileInfo.path,
262
+ collector: options.collector,
263
+ retained: new Set(),
264
+ needed: new Map(),
265
+ reserve: name => name, // replaced below once local bindings are known
266
+ overrides: new WeakMap(),
267
+ dirty: false,
268
+ };
269
+ const print = () => {
270
+ if (!ctx.dirty)
271
+ return undefined;
272
+ forgetJsxParens(j, root);
273
+ reprintDirectives(j, root);
274
+ return root.toSource({ quote: 'double', useTabs: prefersTabs(source) });
275
+ };
276
+ // ---- 1. Stylesheet imports ------------------------------------------------
277
+ // The default is `keep`: the app's own Bulma stylesheet already styles
278
+ // every class a converted element renders.
279
+ rewriteStylesheetImports(ctx, root, options.cssMode ?? 'keep');
280
+ // ---- 2. Plan the elements in scope ----------------------------------------
281
+ const bestaxLocals = new Set();
282
+ root
283
+ .find(j.ImportDeclaration, { source: { value: BESTAX } })
284
+ .forEach((importPath) => {
285
+ for (const spec of importPath.node.specifiers ?? []) {
286
+ if (spec.local)
287
+ bestaxLocals.add(spec.local.name);
288
+ }
289
+ });
290
+ // `converts` is whether the element would become a component with its
291
+ // classes written out, so it holds for a computed className too.
292
+ const elements = [];
293
+ root.find(j.JSXElement).forEach(elementPath => {
294
+ const element = elementPath.node;
295
+ const name = element.openingElement.name;
296
+ if (name.type !== 'JSXIdentifier' || !INTRINSIC.test(name.name))
297
+ return;
298
+ if (FOREIGN_ROOTS.has(name.name) || insideForeignContent(elementPath)) {
299
+ return;
300
+ }
301
+ const classAttr = findAttr(element, 'className');
302
+ if (!classAttr)
303
+ return;
304
+ const attributes = new Map();
305
+ for (const attr of element.openingElement.attributes ?? []) {
306
+ if (attr.type !== 'JSXAttribute' || attr === classAttr)
307
+ continue;
308
+ attributes.set(attributeName(attr), attributeValue(attr));
309
+ }
310
+ const className = staticClassName(classAttr);
311
+ const facts = {
312
+ tag: name.name,
313
+ tokens: className === null
314
+ ? classTokensOf(classAttr.value)
315
+ : [...new Set(className.split(/\s+/).filter(Boolean))],
316
+ attributes,
317
+ hasSpread: (element.openingElement.attributes ?? []).some((attr) => attr.type === 'JSXSpreadAttribute'),
318
+ hasRef: attributes.has('ref'),
319
+ hasChildren: (element.children ?? []).some(isContent),
320
+ onlyChildOf: onlyChildOf(elementPath, bestaxLocals),
321
+ };
322
+ let result = plan(facts);
323
+ const converts = result.conversion !== null;
324
+ if (className === null && result.conversion) {
325
+ // The same element with a static className would convert; with a
326
+ // computed one, only a person can turn each condition into its prop.
327
+ const target = result.conversion.target;
328
+ result = {
329
+ conversion: null,
330
+ todos: [
331
+ ...result.todos,
332
+ {
333
+ rule: ruleId('dynamic-class', target),
334
+ message: `this \`className\` is computed, and the codemod converts static class strings only; convert this element to bestax \`${target}\` by hand, turning each condition into its prop`,
335
+ },
336
+ ],
337
+ };
338
+ }
339
+ if (result.conversion || result.todos.length > 0) {
340
+ elements.push({ path: elementPath, plan: result, converts });
341
+ }
342
+ });
343
+ if (elements.length === 0)
344
+ return print();
345
+ // ---- 3. File-level gates ------------------------------------------------------
346
+ // A gate speaks only when an element would convert: its TODO says what to
347
+ // change so a re-run converts them, which is noise when a re-run could not.
348
+ const converting = elements.some(element => element.converts);
349
+ const block = (rule, message) => {
350
+ if (converting)
351
+ addTodo(ctx, elements[0].path, rule, message);
352
+ for (const element of elements) {
353
+ element.plan = { ...element.plan, conversion: null };
354
+ }
355
+ };
356
+ const runtime = fileJsxRuntime(root, j) ??
357
+ projectJsxRuntime(fileInfo.path, options.jsxRuntimes);
358
+ const program = root.find(j.Program).paths()[0].node;
359
+ if (runtime !== null && runtime !== 'react') {
360
+ block('jsx-runtime', `this file's JSX renders through \`${runtime}\`, not React, and bestax-bulma components are React components; convert it by hand if it does run on React`);
361
+ // Every element TODO is advice about bestax components (rules.test.ts
362
+ // holds each to naming one), and this file cannot use them.
363
+ for (const element of elements) {
364
+ element.plan = { ...element.plan, todos: [] };
365
+ }
366
+ }
367
+ else if (isCommonJs(root, j)) {
368
+ block('imports', 'this file is CommonJS (`require` or `module.exports`), and the codemod adds an ES `import` for the bestax components; move the file to ES modules, then re-run');
369
+ }
370
+ else if (usesStyledJsx(root, j)) {
371
+ block('styled-jsx', 'this component scopes its styles with styled-jsx, which adds its scoping class to plain elements and not to an imported component, so a converted element would lose its scoped styles; move those styles out of `<style jsx>`, or scope them with `:global()`, then re-run');
372
+ }
373
+ else if (mayBeServerComponent(fileInfo.path, options.serverComponentRoots) &&
374
+ !program.directives?.some((d) => d.value.value === 'use client')) {
375
+ block('rsc', "this file is in a Next.js App Router project and has no `'use client'`, so it may render as a server component, and bestax-bulma's components are client components; add `'use client'` if the file can be one, then re-run");
376
+ }
377
+ // ---- 4. TODOs in reading order; conversions innermost first ------------------
378
+ const bound = collectBoundNames(j, root, BESTAX);
379
+ // Any name the file already says anywhere forces an alias for the new
380
+ // import: a function expression's own name, a method's parameter, a browser
381
+ // global (`Notification.requestPermission()`). An alias costs nothing;
382
+ // shadowing one of those renders the wrong thing or recurses.
383
+ root.find(j.Identifier).forEach(identifierPath => {
384
+ bound.add(identifierPath.node.name);
385
+ });
386
+ root.find(j.JSXIdentifier).forEach(identifierPath => {
387
+ if (/^[A-Z]/.test(identifierPath.node.name)) {
388
+ bound.add(identifierPath.node.name);
389
+ }
390
+ });
391
+ ctx.reserve = makeReserve(ctx, bound);
392
+ const bestaxImportState = seedBestaxImport(ctx, root);
393
+ for (const { path: elementPath, plan: result } of elements) {
394
+ for (const todo of result.todos) {
395
+ addTodo(ctx, elementPath, todo.rule, todo.message);
396
+ }
397
+ }
398
+ for (const { path: elementPath, plan: result } of [...elements].reverse()) {
399
+ const conversion = result.conversion;
400
+ if (!conversion)
401
+ continue;
402
+ const element = elementPath.node;
403
+ const [head, ...rest] = conversion.target.split('.');
404
+ renameElement(j, element, [ctx.reserve(head), ...rest].join('.'));
405
+ const attrs = element.openingElement.attributes;
406
+ for (const name of conversion.drop) {
407
+ attrs.splice(attrs.indexOf(findAttr(element, name)), 1);
408
+ }
409
+ for (const name of conversion.numbers) {
410
+ const attr = findAttr(element, name);
411
+ attr.value = j.jsxExpressionContainer(j.numericLiteral(Number(attributeValue(attr))));
412
+ }
413
+ const classAttr = findAttr(element, 'className');
414
+ const replacement = conversion.props.map(([name, value]) => makeAttr(j, name, value === true ? undefined : value));
415
+ if (conversion.className) {
416
+ replacement.push(makeAttr(j, 'className', conversion.className));
417
+ }
418
+ // A comment on the className goes with whatever takes its place: the
419
+ // first new attribute, else the next one along, else the tag name. After
420
+ // the name it has to be a block comment: recast prints a line comment
421
+ // there as `<// …`, which TypeScript reads as a closing tag.
422
+ if (classAttr.comments?.length) {
423
+ const name = element.openingElement.name;
424
+ const carrier = replacement[0] ?? attrs.find((attr) => attr !== classAttr) ?? name;
425
+ carrier.comments = [
426
+ ...(carrier.comments ?? []),
427
+ ...classAttr.comments.map((comment) => {
428
+ if (carrier !== name)
429
+ return comment;
430
+ if (comment.type !== 'CommentLine') {
431
+ return { ...comment, leading: false, trailing: true };
432
+ }
433
+ return {
434
+ type: 'CommentBlock',
435
+ value: ` ${comment.value.trim().replace(/\*\//g, '*\\/')} `,
436
+ leading: false,
437
+ trailing: true,
438
+ };
439
+ }),
440
+ ];
441
+ }
442
+ attrs.splice(attrs.indexOf(classAttr), 1, ...replacement);
443
+ ctx.dirty = true;
444
+ }
445
+ // ---- 5. The bestax import ---------------------------------------------------
446
+ const body = program.body;
447
+ placeBestaxImport(bestaxImportState, buildBestaxImport(ctx, bestaxImportState), {
448
+ insertBefore: (node) => {
449
+ let last = -1;
450
+ body.forEach((statement, index) => {
451
+ if (statement.type === 'ImportDeclaration')
452
+ last = index;
453
+ });
454
+ if (last === -1 && body.length > 0) {
455
+ // A file header stays at the top: the source's own leading comments
456
+ // up to the last file-level directive (`@ts-nocheck`) or the last
457
+ // one set off by a blank line. The scan stops at a next-line
458
+ // directive and at a TODO this run wrote, since both belong to the
459
+ // statement below them.
460
+ const first = body[0];
461
+ const leading = (first.comments ?? []).filter((comment) => comment.leading);
462
+ let end = -1;
463
+ for (let index = 0; index < leading.length; index += 1) {
464
+ const comment = leading[index];
465
+ if (typeof comment.end !== 'number' ||
466
+ NEXT_LINE_DIRECTIVE.test(comment.value)) {
467
+ break;
468
+ }
469
+ const next = leading[index + 1];
470
+ const nextStart = typeof next?.start === 'number' ? next.start : first.start;
471
+ if (HEADER_PRAGMA.test(comment.value) ||
472
+ /\n\s*\n/.test(source.slice(comment.end, nextStart))) {
473
+ end = index;
474
+ }
475
+ }
476
+ const header = leading.slice(0, end + 1);
477
+ if (header.length > 0) {
478
+ first.comments = first.comments.filter((comment) => !header.includes(comment));
479
+ node.comments = header;
480
+ }
481
+ }
482
+ body.splice(last + 1, 0, node);
483
+ },
484
+ });
485
+ return print();
486
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"deps.d.ts","sourceRoot":"","sources":["../../../src/sources/rbx/deps.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAsBzD,eAAO,MAAM,kBAAkB,EAAE,kBAqLhC,CAAC"}
1
+ {"version":3,"file":"deps.d.ts","sourceRoot":"","sources":["../../../src/sources/rbx/deps.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAwBzD,eAAO,MAAM,kBAAkB,EAAE,kBAgGhC,CAAC"}