bestax-migrate 2.2.0 → 2.2.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.
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Minimal semver-range reading for the manifest passes, shared by every source.
3
+ *
4
+ * Not a semver implementation: the workspace deliberately carries no `semver`
5
+ * dependency for this, and the two questions the passes ask are narrow. Both
6
+ * are answered conservatively, so an unrecognised shape is left alone rather
7
+ * than guessed at. The one thing this file must never do is rewrite a value it
8
+ * does not understand, so every accepting path below goes through one grammar.
9
+ */
10
+ /**
11
+ * True only when `range` PROVABLY admits no version >= 1.0.0.
12
+ *
13
+ * The first version of this matched a leading `0` and nothing else, so a range
14
+ * written as comparators (`>=0.7 <1`) was left on 0.x and then reported as
15
+ * already v1. It lived in the rbx source only; the react-bulma-components
16
+ * manifest pass kept the regex until review found the unported half.
17
+ * This walks each `||` alternative and its comparators: a set is pre-v1 when
18
+ * some comparator caps it below 1.0.0 and none of them opens it at 1 or above.
19
+ *
20
+ * Conservative by design: an unrecognised shape returns false and the range is
21
+ * left alone (and reported as such). Bumping a range that might already admit
22
+ * v1 would be the best-guess rewrite this package refuses to make.
23
+ */
24
+ export declare function isPreV1(range: string): boolean;
25
+ /**
26
+ * True when every comparator in every alternative parses to a semver major,
27
+ * so the headline can tell "already v1" apart from "not a version range at
28
+ * all" (`latest`, a git URL) rather than calling both v1.
29
+ */
30
+ export declare function isRecognisedRange(range: string): boolean;
31
+ //# sourceMappingURL=semver-range.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"semver-range.d.ts","sourceRoot":"","sources":["../../../src/sources/_shared/semver-range.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AA4BH;;;;;;;;;;;;;GAaG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAG9C;AAgGD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAoBxD"}
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Minimal semver-range reading for the manifest passes, shared by every source.
3
+ *
4
+ * Not a semver implementation: the workspace deliberately carries no `semver`
5
+ * dependency for this, and the two questions the passes ask are narrow. Both
6
+ * are answered conservatively, so an unrecognised shape is left alone rather
7
+ * than guessed at. The one thing this file must never do is rewrite a value it
8
+ * does not understand, so every accepting path below goes through one grammar.
9
+ */
10
+ const NUMERIC = String.raw `(?:0|[1-9]\d*)`;
11
+ const WILD = String.raw `(?:x|X|\*)`;
12
+ // SemVer's own identifier grammar: dot-separated, non-empty, no leading zero on
13
+ // a numeric prerelease identifier. Looser fragments accepted `0.9.0-alpha..1`,
14
+ // `0.9.0-01` and `0.9.0+foo..bar` as complete versions and overwrote them.
15
+ const PRE_ID = String.raw `(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)`;
16
+ const PRERELEASE = String.raw `-${PRE_ID}(?:\.${PRE_ID})*`;
17
+ const BUILD = String.raw `\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*`;
18
+ /**
19
+ * An exclusive upper bound that sits exactly at 1.0.0: `1`, `1.0`, `1.0.0`,
20
+ * and the x-ranges that desugar to it (`1.x`, `1.0.x`, `1.*`), optionally with
21
+ * build metadata, which carries no precedence. `1.1.x` desugars to 1.1.0 and
22
+ * still admits 1.0.x, so it is deliberately not here.
23
+ */
24
+ const STABLE_ONE_BOUND = new RegExp(`^1(?:\\.(?:0|${WILD}))?(?:\\.(?:0|${WILD}))?(?:${BUILD})?$`);
25
+ /**
26
+ * A plain version or x-range: `N`, `N.N`, `N.N.N[-pre][+build]`, `N.x`,
27
+ * `N.x.x`, `N.N.x`. SemVer forbids leading zeros in numeric parts, and once a
28
+ * component is a wildcard everything after it must be a wildcard or omitted.
29
+ */
30
+ const VERSION = new RegExp(`^(${NUMERIC})(?:\\.(?:${NUMERIC}(?:\\.(?:${WILD}|${NUMERIC}(?:${PRERELEASE})?(?:${BUILD})?))?|${WILD}(?:\\.${WILD})?))?$`);
31
+ /**
32
+ * True only when `range` PROVABLY admits no version >= 1.0.0.
33
+ *
34
+ * The first version of this matched a leading `0` and nothing else, so a range
35
+ * written as comparators (`>=0.7 <1`) was left on 0.x and then reported as
36
+ * already v1. It lived in the rbx source only; the react-bulma-components
37
+ * manifest pass kept the regex until review found the unported half.
38
+ * This walks each `||` alternative and its comparators: a set is pre-v1 when
39
+ * some comparator caps it below 1.0.0 and none of them opens it at 1 or above.
40
+ *
41
+ * Conservative by design: an unrecognised shape returns false and the range is
42
+ * left alone (and reported as such). Bumping a range that might already admit
43
+ * v1 would be the best-guess rewrite this package refuses to make.
44
+ */
45
+ export function isPreV1(range) {
46
+ const alternatives = range.trim().split(/\s*\|\|\s*/);
47
+ return alternatives.length > 0 && alternatives.every(setIsPreV1);
48
+ }
49
+ function setIsPreV1(set) {
50
+ // npm accepts whitespace between an operator and its version
51
+ // (`>= 0.7.0 < 1.0.0`); splitting on whitespace first made the operator a
52
+ // token of its own, so the range parsed as unrecognised and was left alone.
53
+ const text = glueOperators(set);
54
+ if (text === '' || /^(\*|x|X|latest)$/.test(text))
55
+ return false;
56
+ const hyphen = text.match(/^(\S+)\s+-\s+(\S+)$/);
57
+ if (hyphen) {
58
+ // Both endpoints must be plain versions; `not-a-range - 0.9.4` was
59
+ // accepted on its upper bound alone and overwritten.
60
+ const lower = versionMajor(hyphen[1]);
61
+ const upper = versionMajor(hyphen[2]);
62
+ if (lower === null || upper === null)
63
+ return false;
64
+ return upper === 0 || (upper === 1 && isPrereleaseOfOne(hyphen[2]));
65
+ }
66
+ let cappedBelowOne = false;
67
+ for (const token of text.split(/\s+/)) {
68
+ const match = token.match(/^(>=|<=|>|<|=)?(.*)$/);
69
+ if (!match)
70
+ return false;
71
+ const [, op = '', version] = match;
72
+ // After a comparator only a plain version may follow: `==0.9.4` and
73
+ // `<^0.9` were each read by stripping a second operator, then overwritten.
74
+ const major = op ? versionMajor(version) : majorOf(version);
75
+ if (major === null)
76
+ return false;
77
+ const plain = version.replace(/^v/, '');
78
+ switch (op) {
79
+ case '>':
80
+ case '>=':
81
+ // A lower bound on a 1.0.0 prerelease (`>=1.0.0-rc.1`) does not open
82
+ // the range at a stable v1; keep scanning for an upper cap.
83
+ if (major >= 1 && !isPrereleaseOfOne(version))
84
+ return false;
85
+ break;
86
+ case '<':
87
+ // `<1`, `<1.0`, `<1.0.0` (build metadata carries no precedence, so
88
+ // `<1.0.0+build.1` is the same) exclude every stable v1, as does a
89
+ // prerelease of 1.0.0 (`<1.0.0-0`, `<1.0.0-rc.1`); `<0.9` does too.
90
+ // A higher upper bound (`<2` in `<1 <2`) never widens the set, so it
91
+ // is neutral rather than disqualifying; the result still needs some
92
+ // other comparator to cap it. Alone, `<2` therefore stays unbumped.
93
+ if (major === 0 ||
94
+ (major === 1 && STABLE_ONE_BOUND.test(plain)) ||
95
+ isPrereleaseOfOne(version)) {
96
+ cappedBelowOne = true;
97
+ }
98
+ break;
99
+ case '<=':
100
+ // `<=1.0.0-0` caps at the lowest 1.0.0 prerelease, so it excludes
101
+ // every stable v1 just as `<1.0.0-0` does. A higher inclusive bound
102
+ // is neutral for the same reason as above.
103
+ if (major === 0 || isPrereleaseOfOne(version)) {
104
+ cappedBelowOne = true;
105
+ }
106
+ break;
107
+ default:
108
+ // `0.9.4`, `=0.9.4`, `^0.9`, `~0.7.5`, `0.x`: caret and tilde never
109
+ // cross a major, so major 0 stays below 1. An exact `1.0.0-rc.1` pins
110
+ // a prerelease and admits no stable v1 either; `^1.0.0-rc.1` does.
111
+ if (major === 0) {
112
+ cappedBelowOne = true;
113
+ }
114
+ else if (!/^[~^]/.test(version) &&
115
+ isPrereleaseOfOne(version.replace(/^=/, ''))) {
116
+ cappedBelowOne = true;
117
+ }
118
+ else {
119
+ return false;
120
+ }
121
+ }
122
+ }
123
+ return cappedBelowOne;
124
+ }
125
+ /**
126
+ * `1.0.0-0`, `1.0.0-rc.1`: a prerelease of exactly 1.0.0 orders below every
127
+ * stable 1.x, so as an upper bound or an exact pin it admits no usable v1.
128
+ * Three numeric parts only, matching semver; `1.0-beta` is not a token this
129
+ * parser reads and is left alone. A caret or tilde on one is different
130
+ * (`^1.0.0-rc.1` admits 1.0.4), which is why the callers check the operator.
131
+ * Accepts the same optional `v` that the version grammar does.
132
+ */
133
+ function isPrereleaseOfOne(version) {
134
+ return /^v?1\.0\.0-/.test(version);
135
+ }
136
+ function glueOperators(set) {
137
+ // Only when what follows starts a plain version. Gluing unconditionally
138
+ // turned the invalid `> =0.7` into a valid `>=0.7`, which was then bumped.
139
+ // node-semver normalises a spaced tilde or caret (`~ 0.7.5`) the same way.
140
+ return set.trim().replace(/(>=|<=|>|<|=|~|\^)\s+(?=[0-9vxX*])/g, '$1');
141
+ }
142
+ /**
143
+ * True when every comparator in every alternative parses to a semver major,
144
+ * so the headline can tell "already v1" apart from "not a version range at
145
+ * all" (`latest`, a git URL) rather than calling both v1.
146
+ */
147
+ export function isRecognisedRange(range) {
148
+ return range
149
+ .trim()
150
+ .split(/\s*\|\|\s*/)
151
+ .every(alt => {
152
+ const text = glueOperators(alt);
153
+ if (/^(\*|x|X)$/.test(text))
154
+ return true;
155
+ const hyphen = text.match(/^(\S+)\s+-\s+(\S+)$/);
156
+ if (hyphen) {
157
+ return (versionMajor(hyphen[1]) !== null && versionMajor(hyphen[2]) !== null);
158
+ }
159
+ return text.split(/\s+/).every(tok => {
160
+ const m = tok.match(/^(>=|<=|>|<|=)?(.*)$/);
161
+ if (!m)
162
+ return false;
163
+ const [, op = '', v] = m;
164
+ return (op ? versionMajor(v) : majorOf(v)) !== null;
165
+ });
166
+ });
167
+ }
168
+ /**
169
+ * The major of a COMPLETE plain version or x-range, or null. npm's optional
170
+ * leading `v` is the only prefix allowed here. Reading a numeric prefix alone
171
+ * accepted a dist-tag like `0.next` as major 0, and the manifest was then
172
+ * overwritten with `^1.0.4`, which is the one thing this parser must never do
173
+ * to a value it does not understand.
174
+ */
175
+ function versionMajor(version) {
176
+ const m = version.replace(/^v/, '').match(VERSION);
177
+ return m ? Number(m[1]) : null;
178
+ }
179
+ /**
180
+ * The major of a BARE token, which may carry exactly one range prefix (`^`,
181
+ * `~`, `=`) before the version. Stripping any run of prefix characters
182
+ * accepted `^^0.9.4` and `vv0.9.4` as major 0 and overwrote them.
183
+ */
184
+ function majorOf(token) {
185
+ return versionMajor(token.replace(/^[~^=]/, ''));
186
+ }
@@ -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;AAgHzD,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;AAsBzD,eAAO,MAAM,kBAAkB,EAAE,kBAqLhC,CAAC"}
@@ -13,6 +13,7 @@
13
13
  * see the note on that below.
14
14
  * says so by name.
15
15
  */
16
+ import { isPreV1, isRecognisedRange } from '../_shared/semver-range.js';
16
17
  const BESTAX_RANGE = '^5';
17
18
  const BULMA_RANGE = '^1.0.4';
18
19
  // Bulma v1's sass tree uses `color.channel(…)` — needs dart-sass ≥ 1.79.
@@ -29,102 +30,6 @@ const RBX_STYLE_DEPS = [
29
30
  'bulma-tooltip',
30
31
  ];
31
32
  const DEP_SECTIONS = ['dependencies', 'devDependencies'];
32
- /**
33
- * True only when `range` PROVABLY admits no version >= 1.0.0.
34
- *
35
- * The first version matched a leading `0` and nothing else, so a range written
36
- * as comparators (`>=0.7 <1`) was left on 0.x and then reported as already v1.
37
- * This walks each `||` alternative and its comparators: a set is pre-v1 when
38
- * some comparator caps it below 1.0.0 and none of them opens it at 1 or above.
39
- *
40
- * Conservative by design: an unrecognised shape returns false and the range is
41
- * left alone (and reported as such). Bumping a range that might already admit
42
- * v1 would be the best-guess rewrite this package refuses to make.
43
- */
44
- function isPreV1(range) {
45
- const alternatives = range.trim().split(/\s*\|\|\s*/);
46
- return alternatives.length > 0 && alternatives.every(setIsPreV1);
47
- }
48
- function setIsPreV1(set) {
49
- // npm accepts whitespace between an operator and its version
50
- // (`>= 0.7.0 < 1.0.0`); splitting on whitespace first made the operator a
51
- // token of its own, so the range parsed as unrecognised and was left alone.
52
- const text = glueOperators(set);
53
- if (text === '' || /^(\*|x|X|latest)$/.test(text))
54
- return false;
55
- const hyphen = text.match(/^(\S+)\s+-\s+(\S+)$/);
56
- if (hyphen)
57
- return majorOf(hyphen[2]) === 0;
58
- let cappedBelowOne = false;
59
- for (const token of text.split(/\s+/)) {
60
- const match = token.match(/^(>=|<=|>|<|=)?\s*v?(.*)$/);
61
- if (!match)
62
- return false;
63
- const [, op = '', version] = match;
64
- const major = majorOf(version);
65
- if (major === null)
66
- return false;
67
- switch (op) {
68
- case '>':
69
- case '>=':
70
- if (major >= 1)
71
- return false;
72
- break;
73
- case '<':
74
- // `<1`, `<1.0`, `<1.0.0` exclude every v1; `<0.9` does too.
75
- if (major === 0 || (major === 1 && /^1(\.0)*$/.test(version))) {
76
- cappedBelowOne = true;
77
- }
78
- else {
79
- return false;
80
- }
81
- break;
82
- case '<=':
83
- if (major === 0)
84
- cappedBelowOne = true;
85
- else
86
- return false;
87
- break;
88
- default:
89
- // `0.9.4`, `=0.9.4`, `^0.9`, `~0.7.5`, `0.x`: caret and tilde never
90
- // cross a major, so major 0 stays below 1.
91
- if (major === 0)
92
- cappedBelowOne = true;
93
- else
94
- return false;
95
- }
96
- }
97
- return cappedBelowOne;
98
- }
99
- function glueOperators(set) {
100
- return set.trim().replace(/(>=|<=|>|<|=)\s+/g, '$1');
101
- }
102
- /**
103
- * True when every comparator in every alternative parses to a semver major,
104
- * so the headline can tell "already v1" apart from "not a version range at
105
- * all" (`latest`, a git URL) rather than calling both v1.
106
- */
107
- function isRecognisedRange(range) {
108
- return range
109
- .trim()
110
- .split(/\s*\|\|\s*/)
111
- .every(alt => {
112
- const text = glueOperators(alt);
113
- if (/^(\*|x|X)$/.test(text))
114
- return true;
115
- const hyphen = text.match(/^(\S+)\s+-\s+(\S+)$/);
116
- if (hyphen) {
117
- return majorOf(hyphen[1]) !== null && majorOf(hyphen[2]) !== null;
118
- }
119
- return text
120
- .split(/\s+/)
121
- .every(tok => majorOf(tok.replace(/^(>=|<=|>|<|=)/, '')) !== null);
122
- });
123
- }
124
- function majorOf(version) {
125
- const m = version.replace(/^[~^=v]+/, '').match(/^(\d+)(?:\.|$)/);
126
- return m ? Number(m[1]) : null;
127
- }
128
33
  export const updateDependencies = (filePath, pkg, collector, options) => {
129
34
  const changes = [];
130
35
  const next = pkg;
@@ -1 +1 @@
1
- {"version":3,"file":"mapping.d.ts","sourceRoot":"","sources":["../../../src/sources/rbx/mapping.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAuCnE;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAiCtD,CAAC;AAEF,uDAAuD;AACvD,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAOrD,CAAC;AAEF,2DAA2D;AAC3D,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAQvD,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAShE,CAAC;AAEF,eAAO,MAAM,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAosBpD,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CA2FhD,CAAC;AAEF,sDAAsD;AACtD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,gBAAgB,GAAG,SAAS,CAM3E"}
1
+ {"version":3,"file":"mapping.d.ts","sourceRoot":"","sources":["../../../src/sources/rbx/mapping.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAuCnE;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAiCtD,CAAC;AAEF,uDAAuD;AACvD,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAOrD,CAAC;AAEF,2DAA2D;AAC3D,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAQvD,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAShE,CAAC;AAEF,eAAO,MAAM,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAssBpD,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CA2FhD,CAAC;AAEF,sDAAsD;AACtD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,gBAAgB,GAAG,SAAS,CAM3E"}
@@ -126,7 +126,7 @@ export const MAPPING = {
126
126
  },
127
127
  forwardRefAs: {
128
128
  status: 'todo',
129
- todo: '`forwardRefAs` is rbx-internal and has no counterpart. Only the bestax form controls and a few components (Dialog, Sidebar, Toast, Carousel) forward refs; most, Button and Box included, are plain function components that do not — put the ref on a wrapping element you control',
129
+ todo: '`forwardRefAs` is rbx-internal and has no counterpart. bestax forwards a ref from the form controls and from `Button`, `LinkButton`, `Modal`, `Dropdown`, `Navbar` (plus `Navbar.Burger` and `Navbar.Link`), `Dialog`, `Sidebar`, `Toast` and `Carousel` — pass `ref` directly on those. Mind the `Navbar.Dropdown` collision: your rbx `Navbar.Dropdown` is the menu and becomes `Navbar.DropdownMenu`, which forwards no ref, while bestax `Navbar.Dropdown` — the container your `<Navbar.Item dropdown>` becomes — does. The rest, `Box` included, are plain function components that forward no ref; put the ref on a wrapping element you control',
130
130
  },
131
131
  // ---- elements -----------------------------------------------------------
132
132
  Block: { status: 'mapped', target: 'Block' },
@@ -136,6 +136,7 @@ export const MAPPING = {
136
136
  target: 'Button',
137
137
  props: {
138
138
  as: AS_OK,
139
+ innerRef: { rename: 'ref' },
139
140
  color: {},
140
141
  size: {},
141
142
  outlined: { booleanToProp: { name: 'isOutlined' } },
@@ -497,9 +498,7 @@ export const MAPPING = {
497
498
  managed: {
498
499
  todo: 'bestax `Dropdown` is uncontrolled by default; use `active` + `onActiveChange` for controlled behaviour',
499
500
  },
500
- innerRef: {
501
- todo: 'this bestax component is a plain function component and forwards no ref; drop `innerRef`, or put the ref on a wrapping element you control',
502
- },
501
+ innerRef: { rename: 'ref' },
503
502
  },
504
503
  subs: {
505
504
  Container: { status: 'mapped', special: 'dropdown-container' },
@@ -606,25 +605,25 @@ export const MAPPING = {
606
605
  props: {
607
606
  active: {},
608
607
  onClose: {},
609
- closeOnEsc: {
610
- todo: 'bestax `Modal` implements no Escape handling at all; add your own keydown listener, or drop the prop if the behaviour is not needed',
611
- },
608
+ closeOnEsc: { rename: 'closeOnEscape' },
612
609
  closeOnBlur: {
613
- todo: "bestax `Modal` has no built-in background-click close; wire it through `Modal.Background`'s onClick and `onClose`",
610
+ todo: 'bestax `Modal` closes on background click in its legacy form, where it renders its own background wired to `onClose`; a `Modal.Content` or `Modal.Card` child selects the compound form, which renders only the children you wrote — there, add a `<Modal.Background>` whose `onClick` calls your close handler, or drop the prop if you passed false',
614
611
  },
615
612
  document: {
616
- todo: 'bestax `Modal` renders inline where you place it rather than portalling into a document at all; drop this prop, and see the advisory the codemod adds on every Modal',
613
+ todo: 'bestax `Modal` portals via `portal`, but that takes `true`, a selector or an element — not a `Document`. Pass an element from the document you targeted, or drop the prop if you were passing the default document',
617
614
  },
618
615
  containerClassName: { rename: 'className' },
619
- innerRef: {
620
- todo: 'this bestax component is a plain function component and forwards no ref; drop `innerRef`, or put the ref on a wrapping element you control',
621
- },
616
+ innerRef: { rename: 'ref' },
622
617
  },
623
618
  subs: {
624
619
  Background: { status: 'mapped', target: 'Modal.Background' },
625
620
  Close: { status: 'mapped', target: 'Modal.Close' },
626
621
  Content: { status: 'mapped', target: 'Modal.Content' },
627
- Container: { status: 'mapped', special: 'modal-container' },
622
+ Container: {
623
+ status: 'mapped',
624
+ special: 'modal-container',
625
+ props: { innerRef: { rename: 'ref' } },
626
+ },
628
627
  Card: {
629
628
  status: 'mapped',
630
629
  target: 'Modal.Card',
@@ -641,7 +640,7 @@ export const MAPPING = {
641
640
  },
642
641
  Portal: {
643
642
  status: 'todo',
644
- todo: '`Modal.Portal` is rbx-internal; bestax `<Modal>` renders inline and does not portal at all — wrap it in `createPortal` yourself if you relied on that (see the advisory the codemod adds on every Modal)',
643
+ todo: '`Modal.Portal` is rbx-internal; set `portal` on the `<Modal>` instead — it takes `true`, a selector or an element',
645
644
  },
646
645
  },
647
646
  },
@@ -659,13 +658,15 @@ export const MAPPING = {
659
658
  todo: 'bestax `Navbar` is uncontrolled; drop `managed`',
660
659
  },
661
660
  document: { todo: 'bestax `Navbar` has no `document` prop; drop it' },
662
- innerRef: {
663
- todo: 'this bestax component is a plain function component and forwards no ref; drop `innerRef`, or put the ref on a wrapping element you control',
664
- },
661
+ innerRef: { rename: 'ref' },
665
662
  },
666
663
  subs: {
667
664
  Brand: { status: 'mapped', target: 'Navbar.Brand' },
668
- Burger: { status: 'mapped', target: 'Navbar.Burger' },
665
+ Burger: {
666
+ status: 'mapped',
667
+ target: 'Navbar.Burger',
668
+ props: { innerRef: { rename: 'ref' } },
669
+ },
669
670
  Divider: { status: 'mapped', target: 'Navbar.Divider' },
670
671
  Menu: { status: 'mapped', target: 'Navbar.Menu' },
671
672
  Link: {
@@ -673,6 +674,7 @@ export const MAPPING = {
673
674
  target: 'Navbar.Link',
674
675
  props: {
675
676
  as: AS_OK,
677
+ innerRef: { rename: 'ref' },
676
678
  arrowless: {},
677
679
  onClick: {},
678
680
  },
@@ -1 +1 @@
1
- {"version":3,"file":"specials.d.ts","sourceRoot":"","sources":["../../../src/sources/rbx/specials.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAO3C,OAAO,EAYL,KAAK,gBAAgB,EACtB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAML,KAAK,aAAa,EACnB,MAAM,8BAA8B,CAAC;AAEtC,YAAY,EAAE,aAAa,EAAE,CAAC;AAy8B9B,wBAAgB,UAAU,CACxB,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,gBAAgB,EACrB,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,EAClB,OAAO,EAAE,GAAG,GACX,aAAa,CAIf"}
1
+ {"version":3,"file":"specials.d.ts","sourceRoot":"","sources":["../../../src/sources/rbx/specials.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAO3C,OAAO,EAYL,KAAK,gBAAgB,EACtB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAML,KAAK,aAAa,EACnB,MAAM,8BAA8B,CAAC;AAEtC,YAAY,EAAE,aAAa,EAAE,CAAC;AAw9B9B,wBAAgB,UAAU,CACxB,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,gBAAgB,EACrB,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,EAClB,OAAO,EAAE,GAAG,GACX,aAAa,CAIf"}
@@ -538,15 +538,14 @@ const SPECIALS = {
538
538
  return { ...result, handledProps: ['align', 'as'] };
539
539
  },
540
540
  /**
541
- * The markup maps cleanly, but three rbx behaviours do not exist in bestax
542
- * and none of them fail loudly: rbx portals the modal into `document.body`,
543
- * closes it on Escape by default, and clips document scroll while it is
544
- * open. bestax renders inline and implements none of the three. An
545
- * unmodified `<Modal>` therefore migrates to something that looks right and
546
- * behaves differently, so every conversion is flagged.
541
+ * rbx's Modal portals into `document.body`, closes on Escape and clips
542
+ * document scroll, all by default. bestax matches two of those since #633 —
543
+ * `closeOnEscape` and `lockScroll` both default to `true` — but still
544
+ * renders inline unless `portal` is set. That last difference does not fail
545
+ * loudly, so every conversion is still flagged, now about the portal alone.
547
546
  */
548
547
  modal(ctx, path, _element) {
549
- addTodo(ctx, path, 'component:Modal', 'bestax `Modal` renders inline rather than portalling into document.body, and implements neither Escape-to-close nor scroll locking — rbx did all three by default. Re-add whichever your UI relied on');
548
+ addTodo(ctx, path, 'component:Modal', 'bestax `Modal` closes on Escape and locks body scroll by default, matching rbx, but it renders inline: set `portal` if you relied on rbx portalling into document.body');
550
549
  return {};
551
550
  },
552
551
  /** rbx Modal.Container is the modal itself in bestax. */
@@ -588,6 +587,21 @@ const SPECIALS = {
588
587
  addTodo(ctx, path, `prop:${prop}`, `bestax \`Navbar.Item\` has no \`${prop}\` prop; restructure or add a class by hand`);
589
588
  ctx.dirty = true;
590
589
  }
590
+ // bestax's `Navbar.Dropdown` forwards a ref, but it is the one such target
591
+ // the `innerRef: { rename: 'ref' }` entries in mapping.ts cannot reach:
592
+ // that table is keyed on the rbx name (`Navbar.Item`), and only this
593
+ // handler knows which of the two targets was picked. A plain
594
+ // `Navbar.Item` is still a function component, so the rename is
595
+ // conditional — there, `innerRef` is left alone.
596
+ const renamedInnerRef = [];
597
+ if (target === 'Navbar.Dropdown') {
598
+ const innerRefAttr = findAttr(element, 'innerRef');
599
+ if (innerRefAttr) {
600
+ innerRefAttr.name = ctx.j.jsxIdentifier('ref');
601
+ renamedInnerRef.push('innerRef');
602
+ ctx.dirty = true;
603
+ }
604
+ }
591
605
  restrictAsToTargets(ctx, path, element, target ?? 'Navbar.Item', [
592
606
  'Navbar.Item',
593
607
  ]);
@@ -601,6 +615,7 @@ const SPECIALS = {
601
615
  'expanded',
602
616
  'hoverable',
603
617
  'managed',
618
+ ...renamedInnerRef,
604
619
  ],
605
620
  };
606
621
  },
@@ -5,11 +5,11 @@
5
5
  * identical.
6
6
  *
7
7
  * Note what it does NOT do: a `bulma-*` extension import in Sass is left in
8
- * place with a TODO, not removed. Only the JS/CSS-import side of rbx's
9
- * extension cleanup is automatic (transform.ts prunes
10
- * `bulma-badge`/`bulma-divider`/`bulma-pageloader`/`bulma-tooltip` CSS
11
- * imports outright, since bestax ships all four as components). A Sass
12
- * `@import` of one may be pulling in customisation the codemod cannot see,
8
+ * place with a TODO, not removed. The JS/CSS-import side in transform.ts does
9
+ * the same for `bulma-badge`/`bulma-divider`/`bulma-pageloader`/`bulma-tooltip`
10
+ * (kept, with a TODO), and the manifest pass reports rather than removes the
11
+ * packages, since markup outside rbx may still use the extension's classes. A
12
+ * Sass `@import` of one may be pulling in customisation the codemod cannot see,
13
13
  * so it is flagged for a human instead.
14
14
  *
15
15
  * rbx's own stylesheet is `rbx/rbx` (its Sass entry, usually imported as
@@ -5,11 +5,11 @@
5
5
  * identical.
6
6
  *
7
7
  * Note what it does NOT do: a `bulma-*` extension import in Sass is left in
8
- * place with a TODO, not removed. Only the JS/CSS-import side of rbx's
9
- * extension cleanup is automatic (transform.ts prunes
10
- * `bulma-badge`/`bulma-divider`/`bulma-pageloader`/`bulma-tooltip` CSS
11
- * imports outright, since bestax ships all four as components). A Sass
12
- * `@import` of one may be pulling in customisation the codemod cannot see,
8
+ * place with a TODO, not removed. The JS/CSS-import side in transform.ts does
9
+ * the same for `bulma-badge`/`bulma-divider`/`bulma-pageloader`/`bulma-tooltip`
10
+ * (kept, with a TODO), and the manifest pass reports rather than removes the
11
+ * packages, since markup outside rbx may still use the extension's classes. A
12
+ * Sass `@import` of one may be pulling in customisation the codemod cannot see,
13
13
  * so it is flagged for a human instead.
14
14
  *
15
15
  * rbx's own stylesheet is `rbx/rbx` (its Sass entry, usually imported as
@@ -1 +1 @@
1
- {"version":3,"file":"deps.d.ts","sourceRoot":"","sources":["../../../src/sources/react-bulma-components/deps.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAazD,eAAO,MAAM,kBAAkB,EAAE,kBAqHhC,CAAC"}
1
+ {"version":3,"file":"deps.d.ts","sourceRoot":"","sources":["../../../src/sources/react-bulma-components/deps.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAUzD,eAAO,MAAM,kBAAkB,EAAE,kBAqHhC,CAAC"}
@@ -4,14 +4,12 @@
4
4
  * with dart-sass. Pure data-in/data-out — the CLI owns file IO, and no
5
5
  * package manager is ever invoked (the report tells the user to install).
6
6
  */
7
+ import { isPreV1 } from '../_shared/semver-range.js';
7
8
  const BESTAX_RANGE = '^5';
8
9
  const BULMA_RANGE = '^1.0.4';
9
10
  // Bulma v1's sass tree uses `color.channel(…)` — needs dart-sass ≥ 1.79.
10
11
  const SASS_RANGE = '^1.79.0';
11
12
  const DEP_SECTIONS = ['dependencies', 'devDependencies'];
12
- function isPreV1(range) {
13
- return /^[~^]?0[.x]/.test(range.trim());
14
- }
15
13
  export const updateDependencies = (filePath, pkg, collector, options) => {
16
14
  const changes = [];
17
15
  const next = pkg;
@@ -26,7 +26,7 @@ export const UNIVERSAL_PROPS = {
26
26
  todo: 'this bestax component has no `as` prop; restructure the element instead',
27
27
  },
28
28
  domRef: {
29
- todo: 'bestax-bulma components do not take domRef; use a ref on a DOM child or wrap the component',
29
+ todo: 'bestax-bulma has no `domRef`. The form controls and `Button`, `LinkButton`, `Modal`, `Dropdown`, `Navbar` (plus `Navbar.Burger` and `Navbar.Link`), `Dialog`, `Sidebar`, `Toast` and `Carousel` forward a ref — on those, rename `domRef` to `ref`. Mind the `Navbar.Dropdown` collision: the `Navbar.Item` that wrapped your dropdown became bestax `Navbar.Dropdown` and forwards one, while your `Navbar.Dropdown` became `Navbar.DropdownMenu` and does not. The rest forward no ref: put the ref on a DOM child or wrap the component',
30
30
  },
31
31
  // ColorProps
32
32
  backgroundColor: { rename: 'bgColor' },
@@ -555,12 +555,12 @@ export const MAPPING = {
555
555
  props: {
556
556
  show: { rename: 'active' },
557
557
  onClose: {},
558
- closeOnEsc: { todo: 'bestax Modal closes on Esc by default; remove' },
558
+ closeOnEsc: { rename: 'closeOnEscape' },
559
559
  closeOnBlur: {
560
- todo: 'bestax Modal background click closes when onClose is set; remove',
560
+ todo: 'bestax Modal closes on background click only in its legacy form; the Modal.Content/Modal.Card child selects the compound form, which renders only the children you wrote — add a `<Modal.Background>` whose `onClick` calls your close handler, or drop the prop if you passed false',
561
561
  },
562
562
  showClose: {
563
- todo: 'bestax Modal renders the close button when onClose is set; remove',
563
+ todo: 'bestax Modal renders a close button only in its legacy form; the Modal.Content/Modal.Card child selects the compound form, which renders only the children you wrote — add a `<Modal.Close variant="floating">` whose `onClick` calls your close handler, or drop the prop if you passed false',
564
564
  },
565
565
  document: { drop: true },
566
566
  },
@@ -1 +1 @@
1
- {"version":3,"file":"specials.d.ts","sourceRoot":"","sources":["../../../src/sources/react-bulma-components/specials.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAE3C,OAAO,EAWL,KAAK,gBAAgB,EACtB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAML,KAAK,aAAa,EACnB,MAAM,8BAA8B,CAAC;AAEtC,YAAY,EAAE,aAAa,EAAE,CAAC;AAsvB9B,wBAAgB,UAAU,CACxB,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,gBAAgB,EACrB,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,EAClB,OAAO,EAAE,GAAG,GACX,aAAa,CAIf;AAED,kEAAkE;AAClE,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,GAAG,QAAQ,CAGjE,CAAC"}
1
+ {"version":3,"file":"specials.d.ts","sourceRoot":"","sources":["../../../src/sources/react-bulma-components/specials.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAE3C,OAAO,EAWL,KAAK,gBAAgB,EACtB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAML,KAAK,aAAa,EACnB,MAAM,8BAA8B,CAAC;AAEtC,YAAY,EAAE,aAAa,EAAE,CAAC;AAqwB9B,wBAAgB,UAAU,CACxB,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,gBAAgB,EACrB,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,EAClB,OAAO,EAAE,GAAG,GACX,aAAa,CAIf;AAED,kEAAkE;AAClE,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,GAAG,QAAQ,CAGjE,CAAC"}
@@ -61,7 +61,18 @@ const SPECIALS = {
61
61
  // (`remove={false}`, `remove=""`, `remove={0}`) → just drop the prop.
62
62
  removeAttr(element, attr);
63
63
  ctx.dirty = true;
64
- return resolved === 'truthy' ? { target: 'Delete' } : {};
64
+ if (resolved !== 'truthy')
65
+ return {};
66
+ // The universal `domRef` advice lists `Button` among the components that
67
+ // forward a ref, but this element is no longer a Button: `Delete` is a
68
+ // plain function component. Claim the prop so that advice cannot fire
69
+ // here, and say what actually applies to the target we picked.
70
+ const handledProps = [];
71
+ if (findAttr(element, 'domRef')) {
72
+ addTodo(ctx, path, 'prop:domRef', 'this `<Button remove>` became bestax `<Delete>`, which forwards no ref — the general `domRef` advice does not apply here; put the ref on a DOM child or wrap the component');
73
+ handledProps.push('domRef');
74
+ }
75
+ return { target: 'Delete', handledProps };
65
76
  }
66
77
  addTodo(ctx, path, 'prop:remove', '`remove` has a dynamic value; this can render as either a Button or a Delete cross — split the branch by hand');
67
78
  return {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bestax-migrate",
3
- "version": "2.2.0",
3
+ "version": "2.2.2",
4
4
  "description": "Codemods for migrating existing React apps from other Bulma libraries to @allxsmith/bestax-bulma",
5
5
  "type": "module",
6
6
  "bin": {
@@ -51,7 +51,7 @@
51
51
  "react-dom": "^19.2.8",
52
52
  "ts-jest": "^29.4.12",
53
53
  "typescript": "^6.0.3",
54
- "@allxsmith/bestax-bulma": "^5.11.7"
54
+ "@allxsmith/bestax-bulma": "^5.15.0"
55
55
  },
56
56
  "publishConfig": {
57
57
  "access": "public"