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,14 @@
1
+ /**
2
+ * package.json migration for an app that styled plain markup with Bulma:
3
+ * bestax-bulma comes in, and nothing goes out. Pure data-in/data-out: no
4
+ * package manager is ever invoked.
5
+ *
6
+ * Under the default `--css keep` the app's styling stack is left as it is:
7
+ * converted elements render the same classes, which the app's own Bulma
8
+ * stylesheet already styles, so its Bulma version and Sass compiler stay.
9
+ * `--css bestax` or `--css bulma` asks for the move to Bulma v1, so only then
10
+ * is a pre-1 Bulma bumped and node-sass swapped for sass.
11
+ */
12
+ import type { DependenciesUpdate } from '../../types.js';
13
+ export declare const updateDependencies: DependenciesUpdate;
14
+ //# sourceMappingURL=deps.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deps.d.ts","sourceRoot":"","sources":["../../../src/sources/bulma-classes/deps.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAoBzD,eAAO,MAAM,kBAAkB,EAAE,kBAgDhC,CAAC"}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * package.json migration for an app that styled plain markup with Bulma:
3
+ * bestax-bulma comes in, and nothing goes out. Pure data-in/data-out: no
4
+ * package manager is ever invoked.
5
+ *
6
+ * Under the default `--css keep` the app's styling stack is left as it is:
7
+ * converted elements render the same classes, which the app's own Bulma
8
+ * stylesheet already styles, so its Bulma version and Sass compiler stay.
9
+ * `--css bestax` or `--css bulma` asks for the move to Bulma v1, so only then
10
+ * is a pre-1 Bulma bumped and node-sass swapped for sass.
11
+ */
12
+ import path from 'node:path';
13
+ import { addBestax, DEP_SECTIONS, moveBulmaToV1, openManifest, replaceNodeSass, reportPeerRanges, } from '../_shared/deps-common.js';
14
+ import { isPreV1 } from '../_shared/semver-range.js';
15
+ import { runtimeOf } from './project.js';
16
+ /** Packages that run PurgeCSS over the app's source. */
17
+ const PURGECSS = [
18
+ 'purgecss',
19
+ '@fullhuman/postcss-purgecss',
20
+ 'purgecss-webpack-plugin',
21
+ 'gulp-purgecss',
22
+ ];
23
+ export const updateDependencies = (filePath, pkg, collector, options) => {
24
+ const manifest = openManifest(filePath, pkg, collector);
25
+ // Peers too: a shared UI package declares its runtime as one.
26
+ const deps = {
27
+ ...pkg.peerDependencies,
28
+ };
29
+ for (const name of DEP_SECTIONS)
30
+ Object.assign(deps, manifest.section(name));
31
+ // A Preact, Solid or other non-React package gets no React library.
32
+ const runtime = runtimeOf(path.dirname(path.resolve(filePath)), deps);
33
+ if (runtime !== null && runtime !== 'react') {
34
+ manifest.report('deps', `left package.json alone: this package's JSX renders through \`${runtime}\`, and @allxsmith/bestax-bulma is a React library`);
35
+ return manifest.result();
36
+ }
37
+ addBestax(manifest);
38
+ if ((options.cssMode ?? 'keep') === 'keep') {
39
+ for (const name of DEP_SECTIONS) {
40
+ const range = manifest.section(name)?.bulma;
41
+ if (range && isPreV1(range)) {
42
+ manifest.report('deps', `left bulma ${range} in ${name} as it is: under --css keep the app's own stylesheet styles the converted elements, which render the same classes; move to Bulma v1 when you are ready (--css bestax, or the Bulma 0.9 to 1 guide)`);
43
+ }
44
+ }
45
+ }
46
+ else {
47
+ moveBulmaToV1(manifest, options.bulmaReferenced);
48
+ replaceNodeSass(manifest);
49
+ }
50
+ reportPeerRanges(manifest);
51
+ const purger = PURGECSS.find(name => deps[name]);
52
+ if (purger) {
53
+ manifest.report('deps', `this app runs PurgeCSS (${purger}), and a converted element's Bulma classes are now written by @allxsmith/bestax-bulma, some of them built from props at runtime (\`mt="4"\` renders \`mt-4\`), so your source no longer spells them out; add ./node_modules/@allxsmith/bestax-bulma/dist/**/*.js to its content and safelist the patterns in https://bestax.io/docs/guides/getting-started/optimizing-css, or those styles are purged from production builds`);
54
+ }
55
+ return manifest.result();
56
+ };
@@ -0,0 +1,3 @@
1
+ import type { MigrationSource } from '../../types.js';
2
+ export declare const bulmaClasses: MigrationSource;
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/sources/bulma-classes/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AA6BtD,eAAO,MAAM,YAAY,EAAE,eAgB1B,CAAC"}
@@ -0,0 +1,35 @@
1
+ import { flaggableRoot } from './class-map.js';
2
+ import { analyzeProject } from './project.js';
3
+ import transform from './transform.js';
4
+ import { transformStyles } from './styles.js';
5
+ import { updateDependencies } from './deps.js';
6
+ /**
7
+ * The first line of an unparseable file whose markup carries a class this
8
+ * source converts or flags: in a `class` or `className` attribute (not
9
+ * `data-class`), including the strings inside a computed one
10
+ * (`className={cn("button", x)}`).
11
+ */
12
+ function findUnsupportedReference(text) {
13
+ const lines = text.split('\n');
14
+ const index = lines.findIndex(line => [
15
+ ...line.matchAll(/(?<![\w-])class(?:Name)?=(\{.*|"[^"]*"|'[^']*'|`[^`]*`)/g),
16
+ ].some(match => [...match[1].matchAll(/["'`]([^"'`]*)["'`]/g)].some(literal => literal[1].split(/\s+/).some(token => flaggableRoot(token)))));
17
+ return index === -1 ? null : index + 1;
18
+ }
19
+ export const bulmaClasses = {
20
+ name: 'bulma-classes',
21
+ label: 'Bulma classes → @allxsmith/bestax-bulma',
22
+ packageName: null,
23
+ // The app's own Bulma stylesheet already styles every class a converted
24
+ // element renders; swapping it for bestax.css would restyle the app's own
25
+ // markup too (bestax's extras use generic names, and its primary differs).
26
+ defaultCssMode: 'keep',
27
+ analyzeProject,
28
+ findUnsupportedReference,
29
+ // Formats that can render React components; a .vue or .svelte file cannot
30
+ // use bestax whatever classes it carries.
31
+ unsupportedExtensions: ['mdx', 'astro'],
32
+ transform,
33
+ transformStyles,
34
+ updateDependencies,
35
+ };
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Decide what one plain JSX element becomes, from its tag, its static class
3
+ * tokens and the names of its other attributes. Pure and AST-free, so the
4
+ * render tests can drive it directly.
5
+ *
6
+ * The rule it enforces: an element converts only when the bestax component
7
+ * renders exactly the markup the element did. Anything that would change the
8
+ * markup is either left in `className` (a class with no prop here) or refuses
9
+ * the whole element with a TODO (an attribute or tag the component would
10
+ * render differently).
11
+ */
12
+ export interface ElementFacts {
13
+ tag: string;
14
+ /** The static className, split on whitespace, in order. */
15
+ tokens: readonly string[];
16
+ /**
17
+ * The element's other attributes: a string value, `true` for a bare
18
+ * attribute, or null for an expression.
19
+ */
20
+ attributes: ReadonlyMap<string, string | true | null>;
21
+ hasSpread: boolean;
22
+ hasRef: boolean;
23
+ /** Whether the element has children (JSX text, elements or expressions). */
24
+ hasChildren: boolean;
25
+ /**
26
+ * The component this element is the only child of, if any (`Link` for
27
+ * `<Link href="/x"><a className="button">`), since that component can
28
+ * reach into it with `cloneElement`.
29
+ */
30
+ onlyChildOf?: string;
31
+ }
32
+ export interface Todo {
33
+ rule: string;
34
+ message: string;
35
+ }
36
+ export interface Conversion {
37
+ /** bestax JSX name, dotted for a part. */
38
+ target: string;
39
+ /** Attributes to write in place of `className`, in order. */
40
+ props: Array<[name: string, value: string | true]>;
41
+ /** What stays in `className`, or null when nothing does. */
42
+ className: string | null;
43
+ /** Attributes to remove: defaults the target renders by itself. */
44
+ drop: string[];
45
+ /** Attributes whose numeric string value becomes a number. */
46
+ numbers: string[];
47
+ }
48
+ export interface Plan {
49
+ conversion: Conversion | null;
50
+ todos: Todo[];
51
+ }
52
+ export declare function plan(facts: ElementFacts): Plan;
53
+ //# sourceMappingURL=plan.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plan.d.ts","sourceRoot":"","sources":["../../../src/sources/bulma-classes/plan.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAiBH,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,2DAA2D;IAC3D,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1B;;;OAGG;IACH,UAAU,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC;IACtD,SAAS,EAAE,OAAO,CAAC;IACnB,MAAM,EAAE,OAAO,CAAC;IAChB,4EAA4E;IAC5E,WAAW,EAAE,OAAO,CAAC;IACrB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,IAAI;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,UAAU;IACzB,0CAA0C;IAC1C,MAAM,EAAE,MAAM,CAAC;IACf,6DAA6D;IAC7D,KAAK,EAAE,KAAK,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC;IACnD,4DAA4D;IAC5D,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,mEAAmE;IACnE,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,8DAA8D;IAC9D,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,IAAI;IACnB,UAAU,EAAE,UAAU,GAAG,IAAI,CAAC;IAC9B,KAAK,EAAE,IAAI,EAAE,CAAC;CACf;AA+BD,wBAAgB,IAAI,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI,CAgP9C"}
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Decide what one plain JSX element becomes, from its tag, its static class
3
+ * tokens and the names of its other attributes. Pure and AST-free, so the
4
+ * render tests can drive it directly.
5
+ *
6
+ * The rule it enforces: an element converts only when the bestax component
7
+ * renders exactly the markup the element did. Anything that would change the
8
+ * markup is either left in `className` (a class with no prop here) or refuses
9
+ * the whole element with a TODO (an attribute or tag the component would
10
+ * render differently).
11
+ */
12
+ import { FORWARDS_REF, HELPER_PROPS, HELPER_TOKENS, inTagSet, legacyHint, modifierFor, PRECEDENCE, rootFor, WRAPPER_OWN_PROPS, wrapperFor, } from './class-map.js';
13
+ import { ruleId } from './rules.js';
14
+ function wrapperEntry(tag) {
15
+ const target = wrapperFor(tag);
16
+ if (!target)
17
+ return null;
18
+ return {
19
+ status: 'mapped',
20
+ target,
21
+ tag,
22
+ textColor: 'textColor',
23
+ bgColor: 'bgColor',
24
+ ownProps: WRAPPER_OWN_PROPS[target],
25
+ };
26
+ }
27
+ function precedence(token) {
28
+ const index = PRECEDENCE.indexOf(token);
29
+ return index === -1 ? PRECEDENCE.length : index;
30
+ }
31
+ /** The root class an element carries, for a message. */
32
+ function rootLabel(tokens) {
33
+ return tokens.find(token => rootFor(token)?.status === 'mapped') ?? 'button';
34
+ }
35
+ function tagsItReaches(entry) {
36
+ if (entry.as === 'any')
37
+ return 'any tag through `as`';
38
+ if (entry.as)
39
+ return entry.as.map(tag => `<${tag}>`).join(', ');
40
+ return `only <${entry.tag}>`;
41
+ }
42
+ export function plan(facts) {
43
+ const todos = [];
44
+ const { tag, tokens } = facts;
45
+ for (const token of tokens) {
46
+ const hint = legacyHint(token);
47
+ if (hint)
48
+ todos.push({ rule: ruleId('legacy', token), message: hint });
49
+ }
50
+ // A family this source leaves as markup keeps its element as markup, even
51
+ // beside a class it would convert (`card box`): the family's parts carry no
52
+ // TODO of their own, so its outermost class is the one place it is flagged.
53
+ const family = tokens.find(token => {
54
+ const found = rootFor(token);
55
+ return found?.status === 'todo' && !found.part;
56
+ });
57
+ if (family) {
58
+ todos.push({
59
+ rule: ruleId('family', family),
60
+ message: `\`.${family}\` stays as markup: ${rootFor(family).why}`,
61
+ });
62
+ return { conversion: null, todos };
63
+ }
64
+ const root = tokens
65
+ .filter(token => rootFor(token)?.status === 'mapped')
66
+ .sort((a, b) => precedence(a) - precedence(b))[0];
67
+ let entry;
68
+ if (root) {
69
+ entry = rootFor(root);
70
+ }
71
+ else {
72
+ // A root the table does not convert keeps the element as markup, so a
73
+ // later pass (or a person) still finds it as the class it is.
74
+ if (tokens.some(token => rootFor(token))) {
75
+ return { conversion: null, todos };
76
+ }
77
+ entry = wrapperEntry(tag);
78
+ if (!entry)
79
+ return { conversion: null, todos };
80
+ }
81
+ const target = entry.target;
82
+ const refuse = (kind, token, message) => ({
83
+ conversion: null,
84
+ todos: [...todos, { rule: ruleId(kind, token), message }],
85
+ });
86
+ // ---- Refusals: the component would render something else ---------------
87
+ if (facts.hasSpread) {
88
+ return refuse('spread', target, `this element spreads props, which bestax \`${target}\` may read differently than the element did (a spread \`className\` merges with its classes instead of replacing them); convert it to \`${target}\` by hand`);
89
+ }
90
+ if (facts.hasRef && !FORWARDS_REF.includes(target)) {
91
+ return refuse('ref', target, `bestax \`${target}\` does not forward refs, so this \`ref\` would stop reaching the DOM node; keep this element as markup`);
92
+ }
93
+ if (facts.onlyChildOf) {
94
+ return refuse('only-child', target, `this element is the only child of \`<${facts.onlyChildOf}>\`, which may hand it props or a ref with \`cloneElement\` (next/link's legacy behavior, a tooltip, a Radix \`asChild\` trigger) that bestax \`${target}\` would not take the same way; convert it by hand if \`<${facts.onlyChildOf}>\` only renders its children`);
95
+ }
96
+ if (facts.attributes.has('dangerouslySetInnerHTML')) {
97
+ return refuse('attr', 'dangerouslySetInnerHTML', `\`dangerouslySetInnerHTML\` sets the element's content directly, and some bestax components render content of their own beside \`children\`, which React rejects; keep this element as markup`);
98
+ }
99
+ for (const name of facts.attributes.keys()) {
100
+ const readAsProp = entry.ownProps?.includes(name) || HELPER_PROPS.has(name);
101
+ if (readAsProp && !entry.passThrough?.includes(name)) {
102
+ return refuse('attr', name, `\`${name}\` is also a bestax \`${target}\` prop, which would read it differently; rename or drop the attribute, then re-run`);
103
+ }
104
+ }
105
+ const missing = Object.entries(entry.defaults ?? {}).filter(([name]) => !facts.attributes.has(name));
106
+ const drop = [];
107
+ for (const name of entry.untypedAttrs ?? []) {
108
+ if (!facts.attributes.has(name))
109
+ continue;
110
+ if (entry.defaults?.[name] === facts.attributes.get(name)) {
111
+ drop.push(name);
112
+ continue;
113
+ }
114
+ return refuse('attr', name, `bestax \`${target}\`'s props take no \`${name}\`${entry.defaults?.[name] ? `, and it renders \`${name}="${entry.defaults[name]}"\` when none is given` : ''}; keep this element as markup`);
115
+ }
116
+ const numbers = [];
117
+ for (const name of entry.numberAttrs ?? []) {
118
+ const value = facts.attributes.get(name);
119
+ if (typeof value !== 'string')
120
+ continue;
121
+ // Only a string that is already the number's own spelling: `040`, `1.50`
122
+ // and ` 40 ` would render as `40`, `1.5` and `40`.
123
+ if (!/^-?\d+(?:\.\d+)?$/.test(value) || String(Number(value)) !== value) {
124
+ return refuse('attr', name, `bestax \`${target}\` types \`${name}\` as a number, and \`${value}\` is not a number spelled the way it renders; keep this element as markup`);
125
+ }
126
+ numbers.push(name);
127
+ }
128
+ if (entry.requiresChildren && !facts.hasChildren) {
129
+ return refuse('children', target, `bestax \`${target}\` requires children, and this element has none; keep it as markup`);
130
+ }
131
+ if (missing.length > 0) {
132
+ const list = missing.map(([name, value]) => `\`${name}="${value}"\``);
133
+ return refuse('defaults', target, `bestax \`${target}\` renders ${list.join(' and ')} when the element does not set ${missing.length === 1 ? 'it' : 'them'}; add ${missing.length === 1 ? 'it' : 'them'} here if that is what you want, then re-run`);
134
+ }
135
+ for (const [name, tags] of Object.entries(entry.dropsAttr ?? {})) {
136
+ if (facts.attributes.has(name) && inTagSet(tags, tag)) {
137
+ // Bulma greys out `.button[disabled]` on any tag, so there it is not
138
+ // inert: dropping it changes how the element looks.
139
+ const visible = name === 'disabled';
140
+ return refuse('drops', target, visible
141
+ ? `bestax \`${target}\` drops \`disabled\` on a <${tag}>, and Bulma styles a disabled \`.${rootLabel(facts.tokens)}\` on any tag, so converting would change how it looks; keep this element as markup`
142
+ : `bestax \`${target}\` drops \`${name}\` on a <${tag}>, where it does nothing anyway; remove it, then re-run`);
143
+ }
144
+ }
145
+ // ---- Tokens → props ---------------------------------------------------------
146
+ const writes = new Map();
147
+ /** The tokens each written prop came from, so a group rule can undo it. */
148
+ const sourceOf = new Map();
149
+ const groupOf = new Map();
150
+ const converted = new Set(root ? [root] : []);
151
+ for (const token of tokens) {
152
+ if (converted.has(token))
153
+ continue;
154
+ const modifier = modifierFor(entry, token);
155
+ if (modifier) {
156
+ if (modifier.tagIn && !modifier.tagIn.includes(tag))
157
+ continue;
158
+ if (modifier.writes.some(write => writes.has(write.prop)))
159
+ continue;
160
+ for (const write of modifier.writes) {
161
+ writes.set(write.prop, write.value ?? true);
162
+ sourceOf.set(write.prop, token);
163
+ }
164
+ converted.add(token);
165
+ continue;
166
+ }
167
+ const helper = HELPER_TOKENS.get(token);
168
+ if (!helper)
169
+ continue;
170
+ const prop = helper.group === 'text-color'
171
+ ? entry.textColor
172
+ : helper.group === 'background'
173
+ ? entry.bgColor
174
+ : helper.write.prop;
175
+ if (!prop || writes.has(prop))
176
+ continue;
177
+ writes.set(prop, helper.write.value ?? true);
178
+ sourceOf.set(prop, token);
179
+ groupOf.set(prop, helper.group);
180
+ converted.add(token);
181
+ }
182
+ const undo = (prop) => {
183
+ converted.delete(sourceOf.get(prop));
184
+ writes.delete(prop);
185
+ };
186
+ const displayProps = [...writes.keys()].filter(prop => groupOf.get(prop) === 'display');
187
+ // bestax drops a base `display` whenever a per-viewport one is set.
188
+ if (displayProps.includes('display') && displayProps.length > 1) {
189
+ undo('display');
190
+ }
191
+ // Flex-container helpers only render beside a flex `display`.
192
+ const flexDisplay = [...writes].some(([prop, value]) => groupOf.get(prop) === 'display' &&
193
+ (value === 'flex' || value === 'inline-flex'));
194
+ if (!flexDisplay) {
195
+ for (const prop of [...writes.keys()]) {
196
+ if (groupOf.get(prop) === 'flex-container')
197
+ undo(prop);
198
+ }
199
+ }
200
+ // A wrapper exists only to carry helper props; with none, leave the tag.
201
+ if (!root && writes.size === 0)
202
+ return { conversion: null, todos };
203
+ // ---- The tag ------------------------------------------------------------
204
+ let renders = entry.tag;
205
+ if (entry.sizeDrivesTag && tag !== 'p' && writes.has('size')) {
206
+ renders = `h${writes.get('size')}`;
207
+ }
208
+ let as;
209
+ if (renders !== tag) {
210
+ const reachable = entry.as === 'any' || (entry.as?.includes(tag) ?? false);
211
+ if (!reachable) {
212
+ return refuse('tag', target, `bestax \`${target}\` renders ${tagsItReaches(entry)}, not a <${tag}>; keep the markup, or change the tag and re-run`);
213
+ }
214
+ as = tag;
215
+ }
216
+ const props = [];
217
+ if (as)
218
+ props.push(['as', as]);
219
+ props.push(...writes);
220
+ const rest = tokens.filter(token => !converted.has(token));
221
+ return {
222
+ conversion: {
223
+ target,
224
+ props,
225
+ className: rest.length > 0 ? rest.join(' ') : null,
226
+ drop,
227
+ numbers,
228
+ },
229
+ todos,
230
+ };
231
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * What the bulma-classes transform needs to know about the project and cannot
3
+ * see from one file: which files a Next.js App Router may render as server
4
+ * components, and which packages render JSX through a runtime other than
5
+ * React.
6
+ *
7
+ * Read once per run, from every package.json the run touches: the nearest one
8
+ * above each target, and every one below it, so a monorepo run from its root
9
+ * still finds `apps/web`.
10
+ */
11
+ /** `jsxImportSource` values whose JSX is React's. */
12
+ export declare const REACT_RUNTIMES: ReadonlySet<string>;
13
+ export interface JsxRuntime {
14
+ /** A package's directory, absolute. */
15
+ dir: string;
16
+ /** `react`, or the other runtime its JSX renders through. */
17
+ runtime: string;
18
+ }
19
+ /**
20
+ * A Next.js package with an App Router. Any module in it can be rendered as a
21
+ * server component (a component under `components/` is one when a server page
22
+ * renders it), so every file counts but those under a Pages Router directory,
23
+ * which are always client code.
24
+ */
25
+ export interface ServerComponentRoot {
26
+ dir: string;
27
+ except: string[];
28
+ }
29
+ export interface ProjectFacts {
30
+ serverComponentRoots: ServerComponentRoot[];
31
+ jsxRuntimes: JsxRuntime[];
32
+ }
33
+ /**
34
+ * The JSX runtime a package declares: what its tsconfig's `jsxImportSource`
35
+ * names, else React when it depends on React, else the first other runtime
36
+ * it depends on, else null.
37
+ */
38
+ export declare function runtimeOf(dir: string, deps: Readonly<Record<string, string | undefined>>): string | null;
39
+ export declare function analyzeProject(targets: string[]): ProjectFacts;
40
+ //# sourceMappingURL=project.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../../../src/sources/bulma-classes/project.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAKH,qDAAqD;AACrD,eAAO,MAAM,cAAc,EAAE,WAAW,CAAC,MAAM,CAK7C,CAAC;AAqBH,MAAM,WAAW,UAAU;IACzB,uCAAuC;IACvC,GAAG,EAAE,MAAM,CAAC;IACZ,6DAA6D;IAC7D,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,oBAAoB,EAAE,mBAAmB,EAAE,CAAC;IAC5C,WAAW,EAAE,UAAU,EAAE,CAAC;CAC3B;AA2GD;;;;GAIG;AACH,wBAAgB,SAAS,CACvB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,GACjD,MAAM,GAAG,IAAI,CAKf;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,YAAY,CA4C9D"}
@@ -0,0 +1,203 @@
1
+ /**
2
+ * What the bulma-classes transform needs to know about the project and cannot
3
+ * see from one file: which files a Next.js App Router may render as server
4
+ * components, and which packages render JSX through a runtime other than
5
+ * React.
6
+ *
7
+ * Read once per run, from every package.json the run touches: the nearest one
8
+ * above each target, and every one below it, so a monorepo run from its root
9
+ * still finds `apps/web`.
10
+ */
11
+ import fs from 'node:fs';
12
+ import path from 'node:path';
13
+ /** `jsxImportSource` values whose JSX is React's. */
14
+ export const REACT_RUNTIMES = new Set([
15
+ 'react',
16
+ '@emotion/react',
17
+ 'theme-ui',
18
+ '@theme-ui/core',
19
+ ]);
20
+ /** Packages that bring a JSX runtime of their own. */
21
+ const OTHER_RUNTIMES = [
22
+ 'preact',
23
+ 'solid-js',
24
+ 'inferno',
25
+ '@builder.io/qwik',
26
+ 'hono',
27
+ ];
28
+ const SKIP_DIRS = new Set([
29
+ 'node_modules',
30
+ '.git',
31
+ 'dist',
32
+ 'build',
33
+ 'coverage',
34
+ '.next',
35
+ '.e2e-tmp',
36
+ ]);
37
+ function nearestManifest(target) {
38
+ let dir = path.resolve(target);
39
+ if (fs.statSync(dir, { throwIfNoEntry: false })?.isFile()) {
40
+ dir = path.dirname(dir);
41
+ }
42
+ for (;;) {
43
+ const candidate = path.join(dir, 'package.json');
44
+ if (fs.existsSync(candidate))
45
+ return candidate;
46
+ const parent = path.dirname(dir);
47
+ if (parent === dir)
48
+ return null;
49
+ dir = parent;
50
+ }
51
+ }
52
+ function manifestsBelow(dir, out) {
53
+ let entries;
54
+ try {
55
+ entries = fs.readdirSync(dir, { withFileTypes: true });
56
+ }
57
+ catch {
58
+ return;
59
+ }
60
+ for (const entry of entries) {
61
+ if (entry.isDirectory() && !SKIP_DIRS.has(entry.name)) {
62
+ manifestsBelow(path.join(dir, entry.name), out);
63
+ }
64
+ else if (entry.isFile() && entry.name === 'package.json') {
65
+ out.push(path.join(dir, entry.name));
66
+ }
67
+ }
68
+ }
69
+ /**
70
+ * JSONC without its comments. tsconfig allows them, and a commented-out
71
+ * `jsxImportSource` is not one the package sets. Strings are copied as they
72
+ * are, so a `//` inside one (a URL) survives.
73
+ */
74
+ function withoutComments(text) {
75
+ let out = '';
76
+ for (let i = 0; i < text.length; i += 1) {
77
+ const char = text[i];
78
+ if (char === '"') {
79
+ let end = i + 1;
80
+ while (end < text.length && text[end] !== '"') {
81
+ end += text[end] === '\\' ? 2 : 1;
82
+ }
83
+ out += text.slice(i, end + 1);
84
+ i = end;
85
+ }
86
+ else if (char === '/' && text[i + 1] === '/') {
87
+ while (i < text.length && text[i] !== '\n')
88
+ i += 1;
89
+ out += '\n';
90
+ }
91
+ else if (char === '/' && text[i + 1] === '*') {
92
+ const end = text.indexOf('*/', i + 2);
93
+ i = end === -1 ? text.length : end + 1;
94
+ }
95
+ else {
96
+ out += char;
97
+ }
98
+ }
99
+ return out;
100
+ }
101
+ /** Where a relative `extends` points: the file as named, else with `.json`. */
102
+ function extendedFile(fromDir, spec) {
103
+ const file = path.resolve(fromDir, spec);
104
+ return fs.statSync(file, { throwIfNoEntry: false })?.isFile()
105
+ ? file
106
+ : `${file}.json`;
107
+ }
108
+ /**
109
+ * The `jsxImportSource` a config sets, or inherits through `extends`: its own
110
+ * setting first, then its parents, a later one in an `extends` list over an
111
+ * earlier one, as TypeScript reads them. A parent named by path is followed
112
+ * (the usual monorepo base config); one named as a package is not.
113
+ */
114
+ function importSourceOf(file, seen) {
115
+ if (seen.has(file))
116
+ return null;
117
+ seen.add(file);
118
+ let text;
119
+ try {
120
+ text = withoutComments(fs.readFileSync(file, 'utf8'));
121
+ }
122
+ catch {
123
+ return null;
124
+ }
125
+ const own = text.match(/"jsxImportSource"\s*:\s*"([^"]+)"/);
126
+ if (own)
127
+ return own[1];
128
+ const extendsValue = text.match(/"extends"\s*:\s*("[^"]*"|\[[^\]]*\])/)?.[1];
129
+ const parents = [...(extendsValue ?? '').matchAll(/"([^"]+)"/g)]
130
+ .map(match => match[1])
131
+ .filter(spec => spec.startsWith('.') || path.isAbsolute(spec))
132
+ .map(spec => extendedFile(path.dirname(file), spec));
133
+ for (const parent of parents.reverse()) {
134
+ const inherited = importSourceOf(parent, seen);
135
+ if (inherited)
136
+ return inherited;
137
+ }
138
+ return null;
139
+ }
140
+ /** The `jsxImportSource` a package's tsconfig or jsconfig sets, if any. */
141
+ function configuredImportSource(dir) {
142
+ for (const name of ['tsconfig.json', 'jsconfig.json']) {
143
+ const found = importSourceOf(path.join(dir, name), new Set());
144
+ if (found)
145
+ return found;
146
+ }
147
+ return null;
148
+ }
149
+ /**
150
+ * The JSX runtime a package declares: what its tsconfig's `jsxImportSource`
151
+ * names, else React when it depends on React, else the first other runtime
152
+ * it depends on, else null.
153
+ */
154
+ export function runtimeOf(dir, deps) {
155
+ const configured = configuredImportSource(dir);
156
+ if (configured)
157
+ return REACT_RUNTIMES.has(configured) ? 'react' : configured;
158
+ if (deps.react)
159
+ return 'react';
160
+ return OTHER_RUNTIMES.find(name => deps[name]) ?? null;
161
+ }
162
+ export function analyzeProject(targets) {
163
+ const manifests = new Set();
164
+ for (const target of targets) {
165
+ const nearest = nearestManifest(target);
166
+ if (nearest)
167
+ manifests.add(path.resolve(nearest));
168
+ const below = [];
169
+ if (fs.statSync(target, { throwIfNoEntry: false })?.isDirectory()) {
170
+ manifestsBelow(path.resolve(target), below);
171
+ }
172
+ for (const manifest of below)
173
+ manifests.add(manifest);
174
+ }
175
+ const facts = { serverComponentRoots: [], jsxRuntimes: [] };
176
+ for (const manifest of [...manifests].sort()) {
177
+ let pkg;
178
+ try {
179
+ // A byte-order mark is legal in a file JSON.parse will not read.
180
+ pkg = JSON.parse(fs.readFileSync(manifest, 'utf8').replace(/^\uFEFF/, ''));
181
+ }
182
+ catch {
183
+ continue;
184
+ }
185
+ const deps = {
186
+ ...pkg.peerDependencies,
187
+ ...pkg.dependencies,
188
+ ...pkg.devDependencies,
189
+ };
190
+ const dir = path.dirname(manifest);
191
+ const hasAppRouter = ['app', path.join('src', 'app')].some(candidate => fs.existsSync(path.join(dir, candidate)));
192
+ if (deps.next && hasAppRouter) {
193
+ facts.serverComponentRoots.push({
194
+ dir,
195
+ except: ['pages', path.join('src', 'pages')].map(pages => path.join(dir, pages)),
196
+ });
197
+ }
198
+ const runtime = runtimeOf(dir, deps);
199
+ if (runtime)
200
+ facts.jsxRuntimes.push({ dir, runtime });
201
+ }
202
+ return facts;
203
+ }
@@ -0,0 +1,16 @@
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
+ export declare const KINDS: ReadonlySet<string>;
14
+ export declare function inVocabulary(token: string): boolean;
15
+ export declare function ruleId(kind: string, token: string): string;
16
+ //# sourceMappingURL=rules.d.ts.map