modern-cmdk 1.4.0 → 1.6.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.
@@ -1,65 +1,609 @@
1
- // src/codemod/transforms/data-attrs.ts
2
- /** Mapping from old bare attribute names to new data-attribute names */
1
+ import { t as printOptions } from "../chunk-print-options.mjs";
2
+ // ../modern-cmdk-codemod/src/transforms/data-attrs.ts
3
+ /** Mapping from cmdk attribute names to the attribute modern-cmdk renders. */
3
4
  const ATTR_MAP = /* @__PURE__ */ new Map([
4
- ["cmdk-root", "data-command"],
5
+ ["cmdk-root", "data-command-root"],
6
+ ["cmdk-dialog", "data-command-dialog"],
7
+ ["cmdk-overlay", "data-command-overlay"],
5
8
  ["cmdk-input", "data-command-input"],
6
9
  ["cmdk-list", "data-command-list"],
10
+ ["cmdk-list-sizer", "data-command-list-inner"],
7
11
  ["cmdk-item", "data-command-item"],
8
12
  ["cmdk-group", "data-command-group"],
13
+ ["cmdk-group-heading", "data-command-group-heading"],
14
+ ["cmdk-group-items", "data-command-group-items"],
9
15
  ["cmdk-separator", "data-command-separator"],
10
16
  ["cmdk-empty", "data-command-empty"],
11
17
  ["cmdk-loading", "data-command-loading"]
12
18
  ]);
13
- /** CSS custom property renames */
19
+ /** CSS custom property renames. */
14
20
  const CSS_VAR_MAP = /* @__PURE__ */ new Map([["--cmdk-list-height", "--command-list-height"]]);
21
+ /** cmdk names with no modern-cmdk equivalent, each with the reason a site is reported. */
22
+ const UNMAPPED_ATTRS = /* @__PURE__ */ new Map([["cmdk-label", "cmdk renders a visually hidden <label>; modern-cmdk puts the label on the root as aria-label and renders no label element"], ["cmdk-item-select", "a DOM event cmdk dispatches on an item to run its onSelect, not an attribute; modern-cmdk has no such event, use the item onSelect prop"]]);
23
+ /** cmdk state attributes (always rendered as "true" or "false") and the presence attribute that replaces each. */
24
+ const STATE_ATTRS = /* @__PURE__ */ new Map([["data-selected", "data-active"], ["data-disabled", "data-disabled"]]);
25
+ const CMDK_NAMES = /* @__PURE__ */ new Set([...ATTR_MAP.keys(), ...UNMAPPED_ATTRS.keys()]);
26
+ /** Imports whose bindings make a JSX element a cmdk element for the Tailwind rewrite. */
27
+ const CMDK_SOURCES = /* @__PURE__ */ new Set(["cmdk", "modern-cmdk/react"]);
28
+ /** The JSX attributes whose string content is a class list. */
29
+ const CLASS_ATTRS = /* @__PURE__ */ new Set(["className", "class"]);
30
+ const CSS_VARS_LONGEST_FIRST = [...CSS_VAR_MAP.keys()].sort((a, b) => b.length - a.length);
31
+ /** One attribute selector inside a compound: [name], [name=value], [name~="value" i]. */
32
+ const ATTRIBUTE_SELECTOR = /\[\s*([A-Za-z_][\w-]*)\s*(?:([~|^$*]?=)\s*("[^"]*"|'[^']*'|[^\]\s"']*)\s*)?(?:[iIsS]\s*)?\]/dy;
15
33
  /**
16
- * Replace all cmdk attribute selectors (bracketed) and CSS vars in a string.
17
- * Runs replaceAll for each ATTR_MAP entry (bracketed selectors), then for each CSS_VAR_MAP entry.
34
+ * The start of an attribute selector cut short by a template literal's `${`: [cmdk-item=" or
35
+ * [cmdk-item. Only the name is rewritten.
18
36
  */
19
- function replaceInString(input) {
20
- let result = input;
21
- ATTR_MAP.entries().forEach(([oldAttr, newAttr]) => {
22
- result = result.replaceAll(`[${oldAttr}]`, `[${newAttr}]`);
23
- });
24
- CSS_VAR_MAP.entries().forEach(([oldVar, newVar]) => {
25
- result = result.replaceAll(oldVar, newVar);
26
- });
37
+ const PARTIAL_ATTRIBUTE_SELECTOR = /\[\s*([A-Za-z_][\w-]*)(?=\s*[~|^$*]?=|\s*$)/dy;
38
+ /** Tailwind presence form of the two state attributes: data-[selected]:, data-[disabled]/name:. */
39
+ const TAILWIND_PRESENCE = /(?<!\w)data-\[((?i:selected|disabled))\](?=[:/])/g;
40
+ /** Tailwind value variants of the two state attributes: data-[selected=true]:, data-[disabled='false']/name:. */
41
+ const TAILWIND_STATE = /(?<!\w)data-\[((?i:selected|disabled))=(?:(["'])(true|false)\2|(true|false))\](?=[:/])/g;
42
+ /** Whitespace that only pads a closing parenthesis, a comma or a combinator inside parentheses. */
43
+ const ARGUMENT_PADDING = /\s*[),>+~]/y;
44
+ /** Tailwind's `*:` and `**:` variants, which move a class to the element's children or descendants. */
45
+ const CHILD_VARIANT = /(?:^|:)\*\*?:/;
46
+ function parentMatchOf(flags) {
47
+ if (flags.every(Boolean)) return "all";
48
+ return flags.some(Boolean) ? "some" : "none";
49
+ }
50
+ function applyEdits(text, edits) {
51
+ let result = text;
52
+ for (const edit of [...edits].sort((a, b) => b.start - a.start)) result = result.slice(0, edit.start) + edit.text + result.slice(edit.end);
27
53
  return result;
28
54
  }
55
+ function unquote(value) {
56
+ const quote = value[0];
57
+ return (quote === "\"" || quote === "'") && value.endsWith(quote) ? value.slice(1, -1) : value;
58
+ }
29
59
  /**
30
- * jscodeshift transform — renames cmdk data attributes in JSX and
31
- * CSS selectors within string/template literals.
60
+ * Rewrite the cmdk attribute selectors and custom properties in a piece of text: a selector
61
+ * string or template quasi (`css` false), or a whole stylesheet (`css` true).
62
+ *
63
+ * The text is cut into compounds (runs of simple selectors with no combinator, comma, brace or
64
+ * whitespace at the top level). Inside any compound, a bracketed attribute whose name is in
65
+ * ATTR_MAP gets its name replaced, operator and value kept; a name in UNMAPPED_ATTRS is reported.
66
+ * Attribute names match case-insensitively, as they do in an HTML document: [CMDK-ITEM] is [cmdk-item].
67
+ * A compound that holds a cmdk attribute also has its data-selected / data-disabled value
68
+ * attributes rewritten: ="true" to the presence attribute, ="false" to :not(presence); any other
69
+ * form is reported. In a stylesheet, a compound starting with `&` counts as holding a cmdk
70
+ * attribute when the enclosing rule's selectors all end in one; under a rule where only some do,
71
+ * each state attribute of such a compound is reported. Inside the parentheses of :not() / :is() /
72
+ * :where() / :matches(), the compound after an argument's last combinator (its subject) joins the
73
+ * outer compound, and the compounds before it stand alone, rewritten only if they hold a cmdk
74
+ * attribute themselves. Inside any other parentheses (:has(), :nth-child(), url(), var(), ...)
75
+ * every compound stands alone. `tailwind` marks a class value of a cmdk-bound element, where an
76
+ * arbitrary variant `[&[data-selected=true]_svg]` holds the bound element's own attributes up to
77
+ * its first `_`, `>`, `+` or `~`. Inside the parentheses of such a variant, `_` is a combinator too. A `[&...]` after a `*:` or `**:` variant, or after an earlier
78
+ * `[&...]` variant, is not taken as the bound element.
79
+ */
80
+ function rewriteSelectors(text, css, report, tailwind) {
81
+ return applyEdits(text, selectorEdits(text, css, report, tailwind));
82
+ }
83
+ /** The edits rewriteSelectors applies, as ranges of `text`; reports are made while collecting. */
84
+ function selectorEdits(text, css, report, tailwind) {
85
+ const edits = [];
86
+ const ruleStack = [];
87
+ let selectorFlags = [];
88
+ let lastFlag = false;
89
+ let preludeFresh = true;
90
+ let preludeIsAtRule = false;
91
+ let attrs = [];
92
+ let compoundStart = -1;
93
+ /** Index of the first `[&` arbitrary variant of the current class token, at its start or after a `:` of a variant prefix that holds no `*:` or `**:` variant. */
94
+ let ampStart = -1;
95
+ /** Index of the first `_`, `>`, `+` or `~` at bracket depth 1, outside parentheses, of a `[&...` arbitrary variant: where the part on another element starts. */
96
+ let ampDescendant = -1;
97
+ const frames = [];
98
+ let brackets = 0;
99
+ if (!css) {
100
+ const whole = text.trim();
101
+ const reason = UNMAPPED_ATTRS.get(whole.toLowerCase());
102
+ if (reason !== void 0) report(text.indexOf(whole), `${whole} has no modern-cmdk equivalent: ${reason}; left unchanged`);
103
+ const mapped = ATTR_MAP.get(whole.toLowerCase());
104
+ if (mapped !== void 0) report(text.indexOf(whole), `${whole} is a cmdk attribute name outside a selector; modern-cmdk renders ${mapped}; left unchanged`);
105
+ }
106
+ const endCompound = (isArgument = false) => {
107
+ if (compoundStart === -1) return;
108
+ const holds = attrs.some((a) => CMDK_NAMES.has(a.name));
109
+ const parentMatch = text[compoundStart] === "&" ? ruleStack.at(-1) ?? "none" : "none";
110
+ const viaParent = parentMatch === "all";
111
+ const partialParent = parentMatch === "some" && !holds;
112
+ const ampBound = tailwind && ampStart !== -1;
113
+ for (const a of attrs) {
114
+ const target = ATTR_MAP.get(a.name);
115
+ if (target !== void 0) {
116
+ edits.push({
117
+ start: a.nameStart,
118
+ end: a.nameEnd,
119
+ text: target
120
+ });
121
+ continue;
122
+ }
123
+ const reason = UNMAPPED_ATTRS.get(a.name);
124
+ if (reason !== void 0) {
125
+ report(a.start, `${text.slice(a.nameStart, a.nameEnd)} has no modern-cmdk equivalent: ${reason}; left unchanged`);
126
+ continue;
127
+ }
128
+ const presence = STATE_ATTRS.get(a.name);
129
+ if (presence === void 0 || a.partial) continue;
130
+ if (partialParent) {
131
+ report(a.start, `${text.slice(a.start, a.end)} under a rule whose selectors only partly hold a cmdk attribute cannot be migrated automatically; left unchanged`);
132
+ continue;
133
+ }
134
+ const onBound = ampBound && (ampDescendant === -1 || a.start < ampDescendant);
135
+ if (!(holds || viaParent || onBound)) continue;
136
+ const value = a.operator === "=" && a.value !== void 0 ? unquote(a.value) : void 0;
137
+ if (value === "true") edits.push({
138
+ start: a.start,
139
+ end: a.end,
140
+ text: `[${presence}]`
141
+ });
142
+ else if (value === "false") edits.push({
143
+ start: a.start,
144
+ end: a.end,
145
+ text: `:not([${presence}])`
146
+ });
147
+ else report(a.start, `${text.slice(a.start, a.end)} beside a cmdk attribute cannot be migrated automatically: cmdk rendered ${a.name} with a "true" or "false" value, modern-cmdk sets ${presence} only while the state holds; left unchanged`);
148
+ }
149
+ if (!isArgument) lastFlag = holds || viaParent;
150
+ attrs = [];
151
+ compoundStart = -1;
152
+ ampStart = -1;
153
+ ampDescendant = -1;
154
+ };
155
+ /** End the compound collected since the last combinator or comma: an argument's subject joins the outer compound. */
156
+ const endArgumentSubject = (frame) => {
157
+ if (!frame.subject) {
158
+ endCompound(true);
159
+ return;
160
+ }
161
+ frame.outer.attrs.push(...attrs);
162
+ if (frame.outer.start === -1) frame.outer.start = compoundStart;
163
+ attrs = [];
164
+ compoundStart = -1;
165
+ };
166
+ const padsArgument = (at) => {
167
+ ARGUMENT_PADDING.lastIndex = at;
168
+ return ARGUMENT_PADDING.test(text);
169
+ };
170
+ const startPrelude = () => {
171
+ selectorFlags = [];
172
+ lastFlag = false;
173
+ preludeFresh = true;
174
+ preludeIsAtRule = false;
175
+ };
176
+ for (let i = 0; i < text.length;) {
177
+ const c = text[i];
178
+ if (css && c === "/" && text[i + 1] === "*") {
179
+ endCompound();
180
+ const close = text.indexOf("*/", i + 2);
181
+ i = close === -1 ? text.length : close + 2;
182
+ continue;
183
+ }
184
+ if (css && frames.length === 0 && c === "/" && text[i + 1] === "/" && (i === 0 || /[\s;{}]/.test(text[i - 1]))) {
185
+ endCompound();
186
+ const eol = text.indexOf("\n", i);
187
+ i = eol === -1 ? text.length : eol;
188
+ continue;
189
+ }
190
+ if (brackets > 0 && /\s/.test(c)) brackets = 0;
191
+ const frame = frames.at(-1);
192
+ if (frame !== void 0 && (brackets === 0 && /[\s>+~,]/.test(c) || tailwind && frame.brackets > 0 && brackets === frame.brackets && /[_>+~,]/.test(c))) {
193
+ if (c === ",") {
194
+ endArgumentSubject(frame);
195
+ frame.hasContent = false;
196
+ } else if (!/\s/.test(c) || frame.hasContent && !padsArgument(i)) {
197
+ endCompound(true);
198
+ frame.hasContent = false;
199
+ }
200
+ i++;
201
+ continue;
202
+ }
203
+ if (frame === void 0 && brackets === 0 && /[\s>+~,{};]/.test(c)) {
204
+ endCompound();
205
+ if (c === ",") {
206
+ selectorFlags.push(lastFlag);
207
+ lastFlag = false;
208
+ } else if (c === "{") {
209
+ selectorFlags.push(lastFlag);
210
+ ruleStack.push(preludeIsAtRule ? ruleStack.at(-1) ?? "none" : parentMatchOf(selectorFlags));
211
+ startPrelude();
212
+ } else if (c === "}") {
213
+ ruleStack.pop();
214
+ startPrelude();
215
+ } else if (c === ";") startPrelude();
216
+ i++;
217
+ continue;
218
+ }
219
+ if (compoundStart === -1) {
220
+ compoundStart = i;
221
+ if (preludeFresh) {
222
+ preludeFresh = false;
223
+ preludeIsAtRule = c === "@";
224
+ }
225
+ }
226
+ if (frame !== void 0) frame.hasContent = true;
227
+ if (css && (c === "\"" || c === "'") && brackets === 0) {
228
+ let j = i + 1;
229
+ while (j < text.length && text[j] !== c) j += text[j] === "\\" ? 2 : 1;
230
+ i = j + 1;
231
+ continue;
232
+ }
233
+ if (c === "[") {
234
+ ATTRIBUTE_SELECTOR.lastIndex = i;
235
+ const m = ATTRIBUTE_SELECTOR.exec(text);
236
+ const nameSpan = m?.indices?.[1];
237
+ if (m !== null && nameSpan !== void 0) {
238
+ attrs.push({
239
+ start: i,
240
+ end: ATTRIBUTE_SELECTOR.lastIndex,
241
+ name: m[1].toLowerCase(),
242
+ nameStart: nameSpan[0],
243
+ nameEnd: nameSpan[1],
244
+ operator: m[2],
245
+ value: m[3],
246
+ partial: false
247
+ });
248
+ i = ATTRIBUTE_SELECTOR.lastIndex;
249
+ continue;
250
+ }
251
+ PARTIAL_ATTRIBUTE_SELECTOR.lastIndex = i;
252
+ const partial = PARTIAL_ATTRIBUTE_SELECTOR.exec(text);
253
+ const partialName = partial?.indices?.[1];
254
+ if (partial !== null && partialName !== void 0) {
255
+ attrs.push({
256
+ start: i,
257
+ end: PARTIAL_ATTRIBUTE_SELECTOR.lastIndex,
258
+ name: partial[1].toLowerCase(),
259
+ nameStart: partialName[0],
260
+ nameEnd: partialName[1],
261
+ operator: void 0,
262
+ value: void 0,
263
+ partial: true
264
+ });
265
+ i = PARTIAL_ATTRIBUTE_SELECTOR.lastIndex;
266
+ continue;
267
+ }
268
+ if (tailwind && brackets === 0 && ampStart === -1 && text.startsWith("[&", i) && (i === compoundStart || text[i - 1] === ":" && !CHILD_VARIANT.test(text.slice(compoundStart, i)))) {
269
+ ampStart = i;
270
+ ampDescendant = -1;
271
+ }
272
+ brackets++;
273
+ } else if (c === "]") {
274
+ if (brackets > 0) brackets--;
275
+ } else if (tailwind && brackets === 1 && frames.length === 0 && ampStart !== -1 && ampDescendant === -1 && /[_>+~]/.test(c)) ampDescendant = i;
276
+ else if (c === "(") {
277
+ frames.push({
278
+ brackets,
279
+ hasContent: false,
280
+ outer: {
281
+ attrs,
282
+ start: compoundStart,
283
+ ampStart,
284
+ ampDescendant
285
+ },
286
+ subject: /:(?:not|is|where|matches)$/i.test(text.slice(Math.max(0, i - 8), i))
287
+ });
288
+ attrs = [];
289
+ compoundStart = -1;
290
+ ampStart = -1;
291
+ ampDescendant = -1;
292
+ } else if (c === ")") {
293
+ const closing = frames.pop();
294
+ if (closing !== void 0) {
295
+ endArgumentSubject(closing);
296
+ attrs = closing.outer.attrs;
297
+ compoundStart = closing.outer.start;
298
+ ampStart = closing.outer.ampStart;
299
+ ampDescendant = closing.outer.ampDescendant;
300
+ }
301
+ } else if (c === "-" && text.startsWith("--cmdk-", i) && !/[\w-]/.test(text[i - 1] ?? "")) {
302
+ const name = CSS_VARS_LONGEST_FIRST.find((v) => text.startsWith(v, i) && !/[\w-]/.test(text[i + v.length] ?? ""));
303
+ if (name !== void 0) {
304
+ edits.push({
305
+ start: i,
306
+ end: i + name.length,
307
+ text: CSS_VAR_MAP.get(name)
308
+ });
309
+ i += name.length;
310
+ continue;
311
+ }
312
+ }
313
+ i++;
314
+ }
315
+ for (let open = frames.pop(); open !== void 0; open = frames.pop()) {
316
+ endArgumentSubject(open);
317
+ attrs = open.outer.attrs;
318
+ compoundStart = open.outer.start;
319
+ ampStart = open.outer.ampStart;
320
+ ampDescendant = open.outer.ampDescendant;
321
+ }
322
+ endCompound();
323
+ return edits;
324
+ }
325
+ /** Rewrite the Tailwind value variants of the two state attributes in a class list, reporting the presence form. */
326
+ function rewriteTailwind(text, report) {
327
+ return applyEdits(text, tailwindEdits(text, report));
328
+ }
329
+ /**
330
+ * Reports a data variant that tests the state of another element and returns true; false for a
331
+ * variant on the element itself. Those are the `group-`, `peer-` and `in-` variants, their
332
+ * `-has-` forms and `not-` forms: they style an element by the state of an ancestor or a sibling,
333
+ * which need not be a cmdk element (a cmdk item has no cmdk ancestor with a selected state), so
334
+ * rewriting them to data-active would be a guess. A bare `has-` tests the descendants, which are
335
+ * cmdk items in the common case, and stays rewritable.
336
+ */
337
+ function reportedAsOther(text, m, report) {
338
+ const start = /(?<![\w-])(?:not-)?(?:(?:group|peer)(?:-has)?|in)-$/.exec(text.slice(0, m.index))?.index;
339
+ if (start === void 0) return false;
340
+ report(start, `${text.slice(start, m.index + m[0].length)}: styles this element by the state of another element (an ancestor or a sibling) that need not be a cmdk element, so it cannot be migrated automatically; left unchanged: check it by hand`);
341
+ return true;
342
+ }
343
+ /**
344
+ * Reports the other-element data variants of a class list on an element that is not cmdk-bound:
345
+ * nothing there is rewritten, but a `group-data-[selected=true]:` under a cmdk item stops working
346
+ * once the item renders data-active.
347
+ */
348
+ function reportOtherElements(text, report) {
349
+ for (const pattern of [TAILWIND_PRESENCE, TAILWIND_STATE]) for (const m of text.matchAll(pattern)) reportedAsOther(text, m, report);
350
+ }
351
+ /** The edits rewriteTailwind applies, as ranges of `text`. */
352
+ function tailwindEdits(text, report) {
353
+ const edits = [];
354
+ for (const m of text.matchAll(TAILWIND_PRESENCE)) {
355
+ if (reportedAsOther(text, m, report)) continue;
356
+ const name = `data-${m[1].toLowerCase()}`;
357
+ const presence = STATE_ATTRS.get(name);
358
+ report(m.index, `${m[0]}: on a cmdk element cannot be migrated automatically: cmdk rendered ${name} with a "true" or "false" value, modern-cmdk sets ${presence} only while the state holds; left unchanged`);
359
+ }
360
+ for (const m of text.matchAll(TAILWIND_STATE)) {
361
+ if (reportedAsOther(text, m, report)) continue;
362
+ const presence = STATE_ATTRS.get(`data-${m[1].toLowerCase()}`);
363
+ if ((m[3] ?? m[4]) === "true") edits.push({
364
+ start: m.index,
365
+ end: m.index + m[0].length,
366
+ text: presence
367
+ });
368
+ else report(m.index, `${m[0]}: has no modern-cmdk equivalent, ${presence} is a presence attribute with no false value (use not-${presence}:); left unchanged`);
369
+ }
370
+ return edits;
371
+ }
372
+ /** 1-based line of an index in a text that itself starts at `firstLine`. */
373
+ function lineAt(text, index, firstLine) {
374
+ let line = firstLine;
375
+ for (let i = text.indexOf("\n"); i !== -1 && i < index; i = text.indexOf("\n", i + 1)) line++;
376
+ return line;
377
+ }
378
+ /**
379
+ * Stylesheet pass for .css and .scss files: the same selector and custom property rewrite as the
380
+ * script pass, with comments, strings outside attribute selectors and formatting untouched.
381
+ * Each site left for manual migration is reported once with its line.
382
+ */
383
+ function transformStylesheet(source, report) {
384
+ return rewriteSelectors(source, true, (index, message) => {
385
+ report(`line ${lineAt(source, index, 1)}: ${message}`);
386
+ }, false);
387
+ }
388
+ /** Stands in for `${...}` while a template literal's quasis are scanned as one text. */
389
+ const PLACEHOLDER = "";
390
+ /** How a placeholder reads in a report. */
391
+ const PLACEHOLDER_TEXT = "${...}";
392
+ function rawOf(node) {
393
+ return node.extra?.raw ?? node.raw;
394
+ }
395
+ const SIMPLE_ESCAPES = /* @__PURE__ */ new Map([
396
+ ["n", "\n"],
397
+ ["t", " "],
398
+ ["b", "\b"],
399
+ ["f", "\f"],
400
+ ["v", "\v"],
401
+ ["r", "\r"]
402
+ ]);
403
+ function decodeRaw(inner) {
404
+ const units = [];
405
+ const rawStart = [];
406
+ const rawEnd = [];
407
+ const push = (value, from, to) => {
408
+ for (const unit of value) for (let k = 0; k < unit.length; k++) {
409
+ units.push(unit.charAt(k));
410
+ rawStart.push(from);
411
+ rawEnd.push(to);
412
+ }
413
+ };
414
+ let i = 0;
415
+ while (i < inner.length) {
416
+ const c = inner.charAt(i);
417
+ if (c !== "\\") {
418
+ push(c, i, i + 1);
419
+ i++;
420
+ continue;
421
+ }
422
+ const next = inner.charAt(i + 1);
423
+ let end = i + 2;
424
+ let value = next;
425
+ if (next === "\r" && inner.charAt(i + 2) === "\n") {
426
+ end = i + 3;
427
+ value = "";
428
+ } else if (next === "\n" || next === "\r" || next === "\u2028" || next === "\u2029") value = "";
429
+ else if (SIMPLE_ESCAPES.has(next)) value = SIMPLE_ESCAPES.get(next);
430
+ else if (next === "x") {
431
+ end = i + 4;
432
+ value = String.fromCharCode(Number.parseInt(inner.slice(i + 2, end), 16));
433
+ } else if (next === "u") {
434
+ if (inner.charAt(i + 2) === "{") {
435
+ end = inner.indexOf("}", i + 3) + 1;
436
+ value = String.fromCodePoint(Number.parseInt(inner.slice(i + 3, end - 1), 16));
437
+ } else {
438
+ end = i + 6;
439
+ value = String.fromCharCode(Number.parseInt(inner.slice(i + 2, end), 16));
440
+ }
441
+ } else if (/[0-7]/.test(next)) {
442
+ const octal = /^[0-7]{1,3}/.exec(inner.slice(i + 1));
443
+ end = i + 1 + octal[0].length;
444
+ value = String.fromCharCode(Number.parseInt(octal[0], 8));
445
+ }
446
+ push(value, i, end);
447
+ i = end;
448
+ }
449
+ return {
450
+ cooked: units.join(""),
451
+ rawStart,
452
+ rawEnd
453
+ };
454
+ }
455
+ /** Apply edits made against `decoded.cooked` to the raw interior they came from. */
456
+ function applyCookedEdits(inner, decoded, edits) {
457
+ const total = decoded.rawStart.length;
458
+ return applyEdits(inner, edits.map((edit) => {
459
+ const start = edit.start < total ? decoded.rawStart[edit.start] : inner.length;
460
+ return {
461
+ start,
462
+ end: edit.end > edit.start ? decoded.rawEnd[edit.end - 1] : start,
463
+ text: edit.text
464
+ };
465
+ }));
466
+ }
467
+ /**
468
+ * jscodeshift transform — renames cmdk attributes in JSX, selectors in string and template
469
+ * literals, and Tailwind state variants on cmdk-bound elements.
32
470
  */
33
471
  function transform(fileInfo, api) {
34
- if (!fileInfo.source.includes("cmdk")) return fileInfo.source;
472
+ if (!/cmdk/i.test(fileInfo.source)) return fileInfo.source;
35
473
  const j = api.jscodeshift;
36
474
  const root = j(fileInfo.source);
37
- let hasChanges = false;
38
- ATTR_MAP.entries().forEach(([oldAttr, newAttr]) => {
39
- root.find(j.JSXAttribute, { name: { name: oldAttr } }).forEach((path) => {
40
- path.node.name = j.jsxIdentifier(newAttr);
41
- hasChanges = true;
475
+ const sites = [];
476
+ let sampleRaw;
477
+ let changed = false;
478
+ const reportAt = (text, firstLine) => {
479
+ return (index, message) => {
480
+ const line = firstLine === void 0 ? "" : `line ${lineAt(text, index, firstLine)}: `;
481
+ api.report(line + message);
482
+ };
483
+ };
484
+ const startOf = (node) => node.start ?? 0;
485
+ const bound = /* @__PURE__ */ new Set();
486
+ root.find(j.ImportDeclaration).forEach((path) => {
487
+ if (!CMDK_SOURCES.has(String(path.node.source.value))) return;
488
+ for (const specifier of path.node.specifiers ?? []) {
489
+ const local = specifier.local?.name;
490
+ if (typeof local === "string") bound.add(local);
491
+ }
492
+ });
493
+ const isBound = (name) => {
494
+ let current = name;
495
+ while (current?.type === "JSXMemberExpression") current = current.object;
496
+ return current?.type === "JSXIdentifier" && bound.has(current.name);
497
+ };
498
+ const tailwind = /* @__PURE__ */ new WeakSet();
499
+ const unbound = /* @__PURE__ */ new WeakSet();
500
+ root.find(j.JSXAttribute).forEach((path) => {
501
+ const attrName = path.node.name;
502
+ if (attrName.type !== "JSXIdentifier" || !CLASS_ATTRS.has(attrName.name)) return;
503
+ const element = path.parent?.node;
504
+ const into = isBound(element?.name) ? tailwind : unbound;
505
+ j(path).find(j.Literal).forEach((p) => void into.add(p.node));
506
+ j(path).find(j.TemplateLiteral).forEach((p) => void into.add(p.node));
507
+ });
508
+ root.find(j.JSXAttribute).forEach((path) => {
509
+ const attrName = path.node.name;
510
+ if (attrName.type !== "JSXIdentifier") return;
511
+ const target = ATTR_MAP.get(attrName.name.toLowerCase());
512
+ const reason = UNMAPPED_ATTRS.get(attrName.name.toLowerCase());
513
+ if (target === void 0 && reason === void 0) return;
514
+ const node = path.node;
515
+ sites.push({
516
+ pos: startOf(node),
517
+ run: () => {
518
+ if (target !== void 0) {
519
+ attrName.name = target;
520
+ changed = true;
521
+ } else reportAt("", node.loc?.start?.line === void 0 ? void 0 : node.loc.start.line)(0, `${attrName.name} has no modern-cmdk equivalent: ${reason}; left unchanged`);
522
+ }
42
523
  });
43
524
  });
44
- root.find(j.StringLiteral).forEach((path) => {
45
- const original = path.node.value;
46
- const replaced = replaceInString(original);
47
- if (replaced !== original) {
48
- path.node.value = replaced;
49
- hasChanges = true;
50
- }
525
+ root.find(j.Literal).forEach((path) => {
526
+ const node = path.node;
527
+ if (typeof node.value !== "string" || node.type === "JSXText") return;
528
+ const original = node.value;
529
+ const raw = rawOf(node);
530
+ const inJsxAttribute = path.parent?.node?.type === "JSXAttribute";
531
+ const useTailwind = tailwind.has(path.node);
532
+ const reportOther = unbound.has(path.node);
533
+ sites.push({
534
+ pos: startOf(node),
535
+ run: () => {
536
+ const quote = raw?.[0];
537
+ if (!(quote === "\"" || quote === "'")) {
538
+ const report = reportAt(original, node.loc?.start?.line);
539
+ if (reportOther) reportOtherElements(original, report);
540
+ let rewritten = rewriteSelectors(original, false, report, useTailwind);
541
+ if (useTailwind) rewritten = rewriteTailwind(rewritten, report);
542
+ if (rewritten === original) return;
543
+ changed = true;
544
+ path.node.value = rewritten;
545
+ return;
546
+ }
547
+ const inner = raw.slice(1, -1);
548
+ const decoded = inJsxAttribute ? {
549
+ cooked: inner,
550
+ rawStart: Array.from({ length: inner.length }, (_, k) => k),
551
+ rawEnd: Array.from({ length: inner.length }, (_, k) => k + 1)
552
+ } : decodeRaw(inner);
553
+ const report = reportAt(decoded.cooked, node.loc?.start?.line);
554
+ if (reportOther) reportOtherElements(decoded.cooked, report);
555
+ const edits = selectorEdits(decoded.cooked, false, report, useTailwind);
556
+ if (useTailwind) edits.push(...tailwindEdits(decoded.cooked, report));
557
+ if (edits.length === 0) return;
558
+ const edited = applyCookedEdits(inner, decoded, edits);
559
+ if (edited === inner) return;
560
+ changed = true;
561
+ if (!inJsxAttribute) sampleRaw ??= raw;
562
+ path.replace(j.jsxText(quote + edited + quote));
563
+ }
564
+ });
51
565
  });
52
566
  root.find(j.TemplateLiteral).forEach((path) => {
53
- for (const quasi of path.node.quasis) {
54
- const original = quasi.value.raw;
55
- const replaced = replaceInString(original);
56
- if (replaced !== original) {
57
- quasi.value.raw = replaced;
58
- quasi.value.cooked = replaced;
59
- hasChanges = true;
567
+ const literal = path.node;
568
+ const useTailwind = tailwind.has(path.node);
569
+ const reportOther = unbound.has(path.node);
570
+ sites.push({
571
+ pos: startOf(literal),
572
+ run: () => {
573
+ const quasis = literal.quasis;
574
+ const original = quasis.map((q) => q.value.raw).join(PLACEHOLDER);
575
+ const firstLine = literal.loc?.start?.line;
576
+ const placeholderAt = [];
577
+ for (let k = original.indexOf(PLACEHOLDER); k !== -1; k = original.indexOf(PLACEHOLDER, k + 1)) placeholderAt.push(k);
578
+ const report = (index, message) => {
579
+ let line = "";
580
+ if (firstLine !== void 0) {
581
+ let n = lineAt(original, index, firstLine);
582
+ placeholderAt.forEach((at, k) => {
583
+ const loc = literal.expressions[k]?.loc;
584
+ if (at < index && loc?.start && loc.end) n += loc.end.line - loc.start.line;
585
+ });
586
+ line = `line ${n}: `;
587
+ }
588
+ api.report(line + message.replaceAll(PLACEHOLDER, PLACEHOLDER_TEXT));
589
+ };
590
+ if (reportOther) reportOtherElements(original, report);
591
+ let rewritten = rewriteSelectors(original, false, report, useTailwind);
592
+ if (useTailwind) rewritten = rewriteTailwind(rewritten, report);
593
+ if (rewritten === original) return;
594
+ const parts = rewritten.split(PLACEHOLDER);
595
+ if (parts.length !== quasis.length) return;
596
+ quasis.forEach((quasi, k) => {
597
+ const part = parts[k];
598
+ if (part === quasi.value.raw) return;
599
+ if (quasi.value.cooked === quasi.value.raw) quasi.value.cooked = part;
600
+ quasi.value.raw = part;
601
+ });
602
+ changed = true;
60
603
  }
61
- }
604
+ });
62
605
  });
63
- return hasChanges ? root.toSource() : fileInfo.source;
606
+ for (const site of sites.sort((a, b) => a.pos - b.pos)) site.run();
607
+ return changed ? root.toSource(printOptions(fileInfo.source, sampleRaw)) : fileInfo.source;
64
608
  }
65
- export { transform as default };
609
+ export { ATTR_MAP, CSS_VAR_MAP, UNMAPPED_ATTRS, transform as default, transformStylesheet };