bestax-migrate 2.1.0 → 2.1.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.
|
@@ -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,EAUL,KAAK,gBAAgB,EACtB,MAAM,gBAAgB,CAAC;AAIxB,MAAM,WAAW,aAAa;IAC5B,2DAA2D;IAC3D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oEAAoE;IACpE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,yEAAyE;IACzE,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;CACzB;
|
|
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,EAUL,KAAK,gBAAgB,EACtB,MAAM,gBAAgB,CAAC;AAIxB,MAAM,WAAW,aAAa;IAC5B,2DAA2D;IAC3D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oEAAoE;IACpE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,yEAAyE;IACzE,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;CACzB;AA21BD,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"}
|
|
@@ -131,33 +131,90 @@ const SPECIALS = {
|
|
|
131
131
|
ctx.dirty = true;
|
|
132
132
|
return { handledProps: ['multiline'] };
|
|
133
133
|
},
|
|
134
|
-
/**
|
|
135
|
-
|
|
134
|
+
/**
|
|
135
|
+
* `<Button remove />` (or any truthy literal) is Bulma's delete
|
|
136
|
+
* cross → bestax `<Delete />`. A falsy literal just drops the prop; only a
|
|
137
|
+
* genuine expression can render as either one depending on runtime state, so
|
|
138
|
+
* it's a TODO instead of a silent guess.
|
|
139
|
+
*/
|
|
140
|
+
button(ctx, path, element) {
|
|
136
141
|
const attr = findAttr(element, 'remove');
|
|
137
142
|
if (!attr)
|
|
138
143
|
return {};
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
144
|
+
const literal = literalValueOf(attr);
|
|
145
|
+
if (literal.kind !== 'expression') {
|
|
146
|
+
// A statically-known value always renders the same at runtime: truthy
|
|
147
|
+
// (`remove`, `remove={true}`, `remove="true"`) → `<Delete />`; falsy
|
|
148
|
+
// (`remove={false}`, `remove=""`, `remove={0}`) → just drop the prop.
|
|
149
|
+
removeAttr(element, attr);
|
|
150
|
+
ctx.dirty = true;
|
|
151
|
+
return literal.value ? { target: 'Delete' } : {};
|
|
152
|
+
}
|
|
153
|
+
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');
|
|
154
|
+
return {};
|
|
142
155
|
},
|
|
143
|
-
/**
|
|
156
|
+
/**
|
|
157
|
+
* Heading → Title / SubTitle / plain `.heading` paragraph, matching RBC's
|
|
158
|
+
* `title: !subtitle && !heading` / `subtitle: subtitle` — the *value* of
|
|
159
|
+
* each prop picks the target, not merely whether it was passed. Any
|
|
160
|
+
* statically-known literal resolves to a fixed truthiness at codemod time
|
|
161
|
+
* (a bare prop and `heading={true}`/`heading="heading"` are all truthy;
|
|
162
|
+
* `heading={false}`/`heading=""`/`heading={0}` are falsy and equivalent to
|
|
163
|
+
* the prop being absent), exactly as RBC evaluates the prop at runtime —
|
|
164
|
+
* this mirrors the Button `remove` handler so the two don't contradict each
|
|
165
|
+
* other on string/number literals. Only a genuine expression can't be
|
|
166
|
+
* resolved: it's left in place with a TODO and a conservative
|
|
167
|
+
* (Title/SubTitle, never the structural plain-element rewrite) fallback so
|
|
168
|
+
* the branch can be split by hand. The subtitle class is applied
|
|
169
|
+
* independently of heading in RBC, so a dynamic `subtitle` blocks the
|
|
170
|
+
* structural `heading` collapse — collapsing would silently drop it.
|
|
171
|
+
*/
|
|
144
172
|
heading(ctx, path, element) {
|
|
145
173
|
const headingAttr = findAttr(element, 'heading');
|
|
146
|
-
|
|
174
|
+
const subtitleAttr = findAttr(element, 'subtitle');
|
|
175
|
+
const headingLit = headingAttr ? literalValueOf(headingAttr) : undefined;
|
|
176
|
+
const subtitleLit = subtitleAttr ? literalValueOf(subtitleAttr) : undefined;
|
|
177
|
+
const headingDynamic = headingLit?.kind === 'expression';
|
|
178
|
+
const subtitleDynamic = subtitleLit?.kind === 'expression';
|
|
179
|
+
// A resolvable literal (boolean/string/number) collapses to its runtime
|
|
180
|
+
// truthiness; an expression stays dynamic.
|
|
181
|
+
const headingTruthy = !!headingLit && headingLit.kind !== 'expression' && !!headingLit.value;
|
|
182
|
+
const subtitleTruthy = !!subtitleLit && subtitleLit.kind !== 'expression' && !!subtitleLit.value;
|
|
183
|
+
// Dynamic values can't be resolved to a target: leave the prop on the
|
|
184
|
+
// element (so its expression survives for hand-splitting) plus a TODO.
|
|
185
|
+
if (subtitleDynamic) {
|
|
186
|
+
addTodo(ctx, path, 'prop:subtitle', '`subtitle` has a dynamic value; pick between Title / SubTitle by hand');
|
|
187
|
+
}
|
|
188
|
+
if (headingDynamic) {
|
|
189
|
+
addTodo(ctx, path, 'prop:heading', '`heading` has a dynamic value; when truthy it renders a plain `<p className="heading">` instead of Title/SubTitle — resolve by hand');
|
|
190
|
+
}
|
|
191
|
+
// A truthy literal `heading` collapses to a plain `.heading` paragraph —
|
|
192
|
+
// but only when `subtitle` isn't a dynamic value the collapse would drop.
|
|
193
|
+
// A truthy *literal* `subtitle` is safe to collapse: RBC applies its class
|
|
194
|
+
// independently, so it rides along on the paragraph as `heading subtitle`
|
|
195
|
+
// (dropping it would be the same silent loss the dynamic guard prevents).
|
|
196
|
+
if (headingTruthy && !subtitleDynamic) {
|
|
147
197
|
removeAttr(element, headingAttr);
|
|
148
|
-
const className = mergeClassName(ctx, path, element, 'heading', 'Heading');
|
|
198
|
+
const className = mergeClassName(ctx, path, element, subtitleTruthy ? 'heading subtitle' : 'heading', 'Heading');
|
|
149
199
|
const rest = stripModifierProps(ctx, path, attributesOf(element).filter(a => !['size', 'weight', 'spaced', 'subtitle'].includes(a.name.name)), 'Heading');
|
|
150
200
|
const replacement = plainElement(ctx.j, 'p', className, rest, element.children ?? []);
|
|
151
201
|
path.replace(replacement);
|
|
152
202
|
ctx.dirty = true;
|
|
153
203
|
return { replaced: true };
|
|
154
204
|
}
|
|
155
|
-
|
|
156
|
-
|
|
205
|
+
// A resolvable literal `heading` that didn't collapse (falsy, or truthy
|
|
206
|
+
// but blocked by a dynamic subtitle) behaves as if the prop were absent —
|
|
207
|
+
// drop it. A dynamic `heading` stays put (its TODO is already filed above).
|
|
208
|
+
if (headingLit && !headingDynamic) {
|
|
209
|
+
removeAttr(element, headingAttr);
|
|
210
|
+
ctx.dirty = true;
|
|
211
|
+
}
|
|
212
|
+
if (subtitleLit && !subtitleDynamic) {
|
|
157
213
|
removeAttr(element, subtitleAttr);
|
|
158
214
|
ctx.dirty = true;
|
|
159
|
-
return { target: 'SubTitle' };
|
|
215
|
+
return { target: subtitleTruthy ? 'SubTitle' : 'Title' };
|
|
160
216
|
}
|
|
217
|
+
// Dynamic subtitle (attr kept) or no subtitle → conservative Title.
|
|
161
218
|
return { target: 'Title' };
|
|
162
219
|
},
|
|
163
220
|
'level-side'(ctx, path, element) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../../../src/sources/react-bulma-components/styles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAiB,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../../../src/sources/react-bulma-components/styles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAiB,MAAM,gBAAgB,CAAC;AAuKrE,eAAO,MAAM,eAAe,EAAE,eAmL7B,CAAC"}
|
|
@@ -60,13 +60,98 @@ const OTHER_BULMA_IMPORT = /^\s*@import\s+['"][^'"]*bulma[^'"]*['"]/;
|
|
|
60
60
|
const EXTENSION_IMPORT = /^\s*@import\s+['"][^'"]*\bbulma-([\w-]+?)(?:\/|\.|['"])/;
|
|
61
61
|
/** Any `$name: value;` declaration (with or without `!default`). */
|
|
62
62
|
const VAR_DECL = /^\s*\$([\w-]+)\s*:\s*(.+?)\s*(!default)?\s*;\s*$/;
|
|
63
|
+
/**
|
|
64
|
+
* Strips Sass block comments (slash-star … star-slash spans) from a value
|
|
65
|
+
* before it is analyzed for foldability or top-level commas. A comment can
|
|
66
|
+
* carry an unbalanced quote (an apostrophe in `user's`) or a stray `(`/`$`
|
|
67
|
+
* that would otherwise fool the character scanners into mis-reading the
|
|
68
|
+
* value's structure; the comment is inert to Sass, so removing it for
|
|
69
|
+
* analysis is safe. The emitted value still carries the comment verbatim.
|
|
70
|
+
*
|
|
71
|
+
* The scan tracks quote state first and only recognizes a block comment
|
|
72
|
+
* outside a string, in one pass — a bare regex would treat a slash-star …
|
|
73
|
+
* star-slash-shaped span that straddles two quoted strings (opening marker in
|
|
74
|
+
* one string, closing marker in the next) as a comment and delete the real
|
|
75
|
+
* top-level comma between them, so a genuine list would fold unparenthesized.
|
|
76
|
+
* An unterminated comment consumes the rest of the value.
|
|
77
|
+
*/
|
|
78
|
+
function stripSassComments(value) {
|
|
79
|
+
let result = '';
|
|
80
|
+
let quote = null;
|
|
81
|
+
let escaped = false;
|
|
82
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
83
|
+
const char = value[i];
|
|
84
|
+
if (quote) {
|
|
85
|
+
result += char;
|
|
86
|
+
if (escaped) {
|
|
87
|
+
escaped = false;
|
|
88
|
+
}
|
|
89
|
+
else if (char === '\\') {
|
|
90
|
+
escaped = true;
|
|
91
|
+
}
|
|
92
|
+
else if (char === quote) {
|
|
93
|
+
quote = null;
|
|
94
|
+
}
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (char === '/' && value[i + 1] === '*') {
|
|
98
|
+
const end = value.indexOf('*/', i + 2);
|
|
99
|
+
if (end === -1)
|
|
100
|
+
break; // unterminated comment — drop the rest
|
|
101
|
+
i = end + 1; // resume after the closing */
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (char === '"' || char === "'")
|
|
105
|
+
quote = char;
|
|
106
|
+
result += char;
|
|
107
|
+
}
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
63
110
|
/**
|
|
64
111
|
* A value is fold-safe for `with (…)` when it is a plain literal — no
|
|
65
112
|
* function calls, interpolation, variable references, or at-rules. Hex
|
|
66
|
-
* colors (`#ff6b35`) are fine; `#{…}` interpolation is not.
|
|
113
|
+
* colors (`#ff6b35`) are fine; `#{…}` interpolation is not. Block comments are
|
|
114
|
+
* ignored for this test — their contents don't make the value non-literal.
|
|
67
115
|
*/
|
|
68
116
|
function isFoldableValue(value) {
|
|
69
|
-
return !/[()$@]|#\{/.test(value);
|
|
117
|
+
return !/[()$@]|#\{/.test(stripSassComments(value));
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Whether `value` has a comma outside any quoted string — a bare Sass list
|
|
121
|
+
* (`'Nunito', sans-serif`) that must be parenthesized before it can sit
|
|
122
|
+
* inside `with (…)`, or Dart Sass reads the comma as an argument separator
|
|
123
|
+
* instead of a list delimiter. Block comments are stripped first so a quote or
|
|
124
|
+
* comma inside one (an apostrophe in `user's`) can't skew the scan.
|
|
125
|
+
*/
|
|
126
|
+
function hasTopLevelComma(value) {
|
|
127
|
+
let quote = null;
|
|
128
|
+
let escaped = false;
|
|
129
|
+
for (const char of stripSassComments(value)) {
|
|
130
|
+
if (escaped) {
|
|
131
|
+
escaped = false;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (char === '\\') {
|
|
135
|
+
escaped = true;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (quote) {
|
|
139
|
+
if (char === quote)
|
|
140
|
+
quote = null;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (char === '"' || char === "'") {
|
|
144
|
+
quote = char;
|
|
145
|
+
}
|
|
146
|
+
else if (char === ',') {
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
/** Formats a folded value for the `with (…)` clause, parenthesizing a bare list. */
|
|
153
|
+
function formatFoldedValue(value) {
|
|
154
|
+
return hasTopLevelComma(value) ? `(${value})` : value;
|
|
70
155
|
}
|
|
71
156
|
function report(collector, file, line, rule, message) {
|
|
72
157
|
collector?.add({ file, line, rule, message });
|
|
@@ -113,7 +198,7 @@ export const transformStyles = (filePath, source, collector, options) => {
|
|
|
113
198
|
out.push(`${indent}@use '${bulmaSass}' with (`);
|
|
114
199
|
foldableVars.forEach(({ name, value }, index) => {
|
|
115
200
|
const comma = index < foldableVars.length - 1 ? ',' : '';
|
|
116
|
-
out.push(`${indent} $${name}: ${value}${comma}`);
|
|
201
|
+
out.push(`${indent} $${name}: ${formatFoldedValue(value)}${comma}`);
|
|
117
202
|
});
|
|
118
203
|
out.push(`${indent});`);
|
|
119
204
|
}
|