@codefast/cli 0.3.12 → 0.3.13-canary.1

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 (159) hide show
  1. package/README.md +206 -51
  2. package/dist/{bin.js → bin.mjs} +4 -1
  3. package/dist/commands/arrange.mjs +107 -0
  4. package/dist/commands/mirror.mjs +66 -0
  5. package/dist/commands/tag.mjs +58 -0
  6. package/dist/lib/arrange/application/analyze.mjs +92 -0
  7. package/dist/lib/arrange/application/group-file.mjs +113 -0
  8. package/dist/lib/arrange/application/run-target.mjs +65 -0
  9. package/dist/lib/arrange/domain/ast/ast-helpers.mjs +115 -0
  10. package/dist/lib/arrange/domain/ast/collectors-cn.mjs +61 -0
  11. package/dist/lib/arrange/domain/ast/collectors-jsx.mjs +20 -0
  12. package/dist/lib/arrange/domain/ast/collectors-tv.mjs +170 -0
  13. package/dist/lib/arrange/domain/ast/targets.mjs +106 -0
  14. package/dist/lib/arrange/domain/constants.mjs +169 -0
  15. package/dist/lib/arrange/domain/errors.mjs +19 -0
  16. package/dist/lib/arrange/domain/grouping.mjs +235 -0
  17. package/dist/lib/arrange/domain/imports.mjs +54 -0
  18. package/dist/lib/arrange/domain/tokenizer.mjs +216 -0
  19. package/dist/lib/arrange/index.mjs +15 -0
  20. package/dist/lib/arrange/infra/walk.mjs +33 -0
  21. package/dist/lib/arrange/presentation/formatters.mjs +52 -0
  22. package/dist/lib/arrange/presentation/report.mjs +24 -0
  23. package/dist/lib/config/domain/schema.mjs +25 -0
  24. package/dist/lib/config/index.mjs +3 -0
  25. package/dist/lib/config/infra/loader.mjs +78 -0
  26. package/dist/lib/infra/caught-unknown-message.mjs +20 -0
  27. package/dist/lib/infra/config-reporter.mjs +10 -0
  28. package/dist/lib/infra/node-io.mjs +30 -0
  29. package/dist/lib/infra/workspace/repo-root.mjs +22 -0
  30. package/dist/lib/mirror/application/engine.mjs +248 -0
  31. package/dist/lib/mirror/application/sync.mjs +162 -0
  32. package/dist/lib/mirror/domain/constants.mjs +12 -0
  33. package/dist/lib/mirror/domain/errors.mjs +15 -0
  34. package/dist/lib/mirror/domain/types.mjs +1 -0
  35. package/dist/lib/mirror/index.mjs +2 -0
  36. package/dist/lib/mirror/infra/dirent-list.mjs +14 -0
  37. package/dist/lib/mirror/infra/package-filter.mjs +26 -0
  38. package/dist/lib/mirror/infra/path-normalizer.mjs +7 -0
  39. package/dist/lib/mirror/infra/update-pkg.mjs +105 -0
  40. package/dist/lib/mirror/infra/workspace-packages.mjs +159 -0
  41. package/dist/lib/mirror/presentation/reporter.mjs +111 -0
  42. package/dist/lib/tag/application/engine.mjs +287 -0
  43. package/dist/lib/tag/domain/types.mjs +1 -0
  44. package/dist/lib/tag/index.mjs +4 -0
  45. package/dist/lib/tag/infra/target-resolver.mjs +170 -0
  46. package/dist/lib/tag/presentation/colors.mjs +12 -0
  47. package/dist/lib/tag/presentation/tag-presenter.mjs +63 -0
  48. package/dist/program.mjs +34 -0
  49. package/package.json +11 -11
  50. package/dist/bin.d.ts +0 -3
  51. package/dist/bin.d.ts.map +0 -1
  52. package/dist/commands/arrange.d.ts +0 -3
  53. package/dist/commands/arrange.d.ts.map +0 -1
  54. package/dist/commands/arrange.js +0 -114
  55. package/dist/commands/mirror.d.ts +0 -4
  56. package/dist/commands/mirror.d.ts.map +0 -1
  57. package/dist/commands/mirror.js +0 -63
  58. package/dist/lib/arrange/analyze.d.ts +0 -4
  59. package/dist/lib/arrange/analyze.d.ts.map +0 -1
  60. package/dist/lib/arrange/analyze.js +0 -105
  61. package/dist/lib/arrange/ast/collectors-cn.d.ts +0 -10
  62. package/dist/lib/arrange/ast/collectors-cn.d.ts.map +0 -1
  63. package/dist/lib/arrange/ast/collectors-cn.js +0 -84
  64. package/dist/lib/arrange/ast/collectors-jsx.d.ts +0 -4
  65. package/dist/lib/arrange/ast/collectors-jsx.d.ts.map +0 -1
  66. package/dist/lib/arrange/ast/collectors-jsx.js +0 -18
  67. package/dist/lib/arrange/ast/collectors-tv.d.ts +0 -13
  68. package/dist/lib/arrange/ast/collectors-tv.d.ts.map +0 -1
  69. package/dist/lib/arrange/ast/collectors-tv.js +0 -273
  70. package/dist/lib/arrange/ast/targets.d.ts +0 -8
  71. package/dist/lib/arrange/ast/targets.d.ts.map +0 -1
  72. package/dist/lib/arrange/ast/targets.js +0 -143
  73. package/dist/lib/arrange/ast/utils.d.ts +0 -36
  74. package/dist/lib/arrange/ast/utils.d.ts.map +0 -1
  75. package/dist/lib/arrange/ast/utils.js +0 -138
  76. package/dist/lib/arrange/constants.d.ts +0 -68
  77. package/dist/lib/arrange/constants.d.ts.map +0 -1
  78. package/dist/lib/arrange/constants.js +0 -179
  79. package/dist/lib/arrange/errors.d.ts +0 -14
  80. package/dist/lib/arrange/errors.d.ts.map +0 -1
  81. package/dist/lib/arrange/errors.js +0 -16
  82. package/dist/lib/arrange/formatters.d.ts +0 -16
  83. package/dist/lib/arrange/formatters.d.ts.map +0 -1
  84. package/dist/lib/arrange/formatters.js +0 -57
  85. package/dist/lib/arrange/group-file.d.ts +0 -4
  86. package/dist/lib/arrange/group-file.d.ts.map +0 -1
  87. package/dist/lib/arrange/group-file.js +0 -115
  88. package/dist/lib/arrange/grouping.d.ts +0 -41
  89. package/dist/lib/arrange/grouping.d.ts.map +0 -1
  90. package/dist/lib/arrange/grouping.js +0 -280
  91. package/dist/lib/arrange/imports.d.ts +0 -6
  92. package/dist/lib/arrange/imports.d.ts.map +0 -1
  93. package/dist/lib/arrange/imports.js +0 -74
  94. package/dist/lib/arrange/report.d.ts +0 -4
  95. package/dist/lib/arrange/report.d.ts.map +0 -1
  96. package/dist/lib/arrange/report.js +0 -37
  97. package/dist/lib/arrange/run-target.d.ts +0 -4
  98. package/dist/lib/arrange/run-target.d.ts.map +0 -1
  99. package/dist/lib/arrange/run-target.js +0 -28
  100. package/dist/lib/arrange/tokenizer.d.ts +0 -37
  101. package/dist/lib/arrange/tokenizer.d.ts.map +0 -1
  102. package/dist/lib/arrange/tokenizer.js +0 -390
  103. package/dist/lib/arrange/types.d.ts +0 -104
  104. package/dist/lib/arrange/types.d.ts.map +0 -1
  105. package/dist/lib/arrange/types.js +0 -7
  106. package/dist/lib/arrange/walk.d.ts +0 -4
  107. package/dist/lib/arrange/walk.d.ts.map +0 -1
  108. package/dist/lib/arrange/walk.js +0 -34
  109. package/dist/lib/arrange.d.ts +0 -41
  110. package/dist/lib/arrange.d.ts.map +0 -1
  111. package/dist/lib/arrange.js +0 -38
  112. package/dist/lib/infra/fs-contract.d.ts +0 -28
  113. package/dist/lib/infra/fs-contract.d.ts.map +0 -1
  114. package/dist/lib/infra/node-io.d.ts +0 -4
  115. package/dist/lib/infra/node-io.d.ts.map +0 -1
  116. package/dist/lib/infra/node-io.js +0 -27
  117. package/dist/lib/mirror/config.d.ts +0 -7
  118. package/dist/lib/mirror/config.d.ts.map +0 -1
  119. package/dist/lib/mirror/config.js +0 -146
  120. package/dist/lib/mirror/constants.d.ts +0 -10
  121. package/dist/lib/mirror/constants.d.ts.map +0 -1
  122. package/dist/lib/mirror/constants.js +0 -13
  123. package/dist/lib/mirror/engine.d.ts +0 -12
  124. package/dist/lib/mirror/engine.d.ts.map +0 -1
  125. package/dist/lib/mirror/engine.js +0 -248
  126. package/dist/lib/mirror/errors.d.ts +0 -10
  127. package/dist/lib/mirror/errors.d.ts.map +0 -1
  128. package/dist/lib/mirror/errors.js +0 -12
  129. package/dist/lib/mirror/package-filter.d.ts +0 -8
  130. package/dist/lib/mirror/package-filter.d.ts.map +0 -1
  131. package/dist/lib/mirror/package-filter.js +0 -33
  132. package/dist/lib/mirror/reporter.d.ts +0 -24
  133. package/dist/lib/mirror/reporter.d.ts.map +0 -1
  134. package/dist/lib/mirror/reporter.js +0 -126
  135. package/dist/lib/mirror/sync.d.ts +0 -4
  136. package/dist/lib/mirror/sync.d.ts.map +0 -1
  137. package/dist/lib/mirror/sync.js +0 -135
  138. package/dist/lib/mirror/types.d.ts +0 -81
  139. package/dist/lib/mirror/types.d.ts.map +0 -1
  140. package/dist/lib/mirror/update-pkg.d.ts +0 -13
  141. package/dist/lib/mirror/update-pkg.d.ts.map +0 -1
  142. package/dist/lib/mirror/update-pkg.js +0 -39
  143. package/dist/lib/mirror/workspace-packages.d.ts +0 -26
  144. package/dist/lib/mirror/workspace-packages.d.ts.map +0 -1
  145. package/dist/lib/mirror/workspace-packages.js +0 -160
  146. package/dist/lib/mirror.d.ts +0 -7
  147. package/dist/lib/mirror.d.ts.map +0 -1
  148. package/dist/lib/mirror.js +0 -5
  149. package/dist/lib/repo-root.d.ts +0 -7
  150. package/dist/lib/repo-root.d.ts.map +0 -1
  151. package/dist/lib/repo-root.js +0 -23
  152. package/dist/lib/shared/utils.d.ts +0 -9
  153. package/dist/lib/shared/utils.d.ts.map +0 -1
  154. package/dist/lib/shared/utils.js +0 -12
  155. package/dist/program.d.ts +0 -4
  156. package/dist/program.d.ts.map +0 -1
  157. package/dist/program.js +0 -38
  158. /package/dist/lib/{infra/fs-contract.js → arrange/domain/types.mjs} +0 -0
  159. /package/dist/lib/{mirror/types.js → infra/fs-contract.mjs} +0 -0
@@ -0,0 +1,169 @@
1
+ //#region src/lib/arrange/domain/constants.ts
2
+ /** Analyze report: long literal threshold (token count). */
3
+ const LONG_STRING_TOKEN_THRESHOLD = 18;
4
+ /**
5
+ * Minimum token count for a string to be considered a candidate for grouping
6
+ * in the apply/preview pipeline. Intentionally much lower than
7
+ * {@link LONG_STRING_TOKEN_THRESHOLD} (analyze only).
8
+ */
9
+ const APPLY_MIN_TOKENS = 2;
10
+ /**
11
+ * Minimum tokens a group must have to stand alone before singleton-merging.
12
+ * Set to 2 so single-token groups are not collapsed into unrelated buckets by the merger.
13
+ */
14
+ const MIN_GROUP_TOKENS = 2;
15
+ /** Dynamic max groups clamp: base bound. */
16
+ const MAX_GROUPS_BASE = 4;
17
+ /** Dynamic max groups clamp: upper cap. */
18
+ const MAX_GROUPS_CAP = 24;
19
+ /**
20
+ * Extra slots so a few state / aria groups do not force `capGroups` to merge
21
+ * unrelated buckets (e.g. `bg-border` + `outline-hidden`).
22
+ */
23
+ const MAX_GROUPS_HEADROOM = 2;
24
+ /** Maximum findings printed per category in the analyze report. */
25
+ const MAX_REPORT_LINES = 40;
26
+ /** Maximum recursion depth when traversing `tv()` object literals. */
27
+ const MAX_OBJECT_DEPTH = 12;
28
+ /** Maximum depth when peeling conditional / parens / arrays inside `cn(...)` args. */
29
+ const MAX_CLASS_EXPR_DEPTH = 12;
30
+ /**
31
+ * Maximum variant-stripping passes in {@link stripVariants}.
32
+ * Real-world Tailwind stacks rarely exceed 4–5 segments (e.g. `@md/sidebar:group-hover:dark:focus-visible:`);
33
+ * 12 is a conservative safety cap that prevents runaway loops on malformed input.
34
+ */
35
+ const MAX_STRIP_VARIANT_PASSES = 12;
36
+ /**
37
+ * Passed when optional `knownBindings` is missing — size 0 disables matching
38
+ * for `cn` / `tv` identifier resolution.
39
+ */
40
+ const EMPTY_CN_TV_BINDINGS = /* @__PURE__ */ new Set();
41
+ /**
42
+ * Bucket sort order — **render pipeline** (lower → earlier in output).
43
+ * Existence → Position → Layout → Sizing → Spacing → Shape → Background → Shadow
44
+ * → Typography → Composite → Motion → Starting → Behavior → State (variants) → Selector variants.
45
+ */
46
+ const BUCKET_ORDER = {
47
+ existence: 0,
48
+ position: 1,
49
+ layout: 2,
50
+ sizing: 3,
51
+ spacing: 4,
52
+ shape: 5,
53
+ background: 6,
54
+ shadow: 7,
55
+ typography: 8,
56
+ composite: 9,
57
+ motion: 10,
58
+ starting: 11,
59
+ behavior: 12,
60
+ state: 13,
61
+ selector: 14,
62
+ other: 15,
63
+ arbitrary: 16
64
+ };
65
+ /**
66
+ * Compatible bucket **pairs** — two adjacent tokens whose buckets appear in the same
67
+ * `Set` may stay in one `cn()` string chunk. Compatibility is **not transitive**: there is
68
+ * no `{position, sizing}` pair, so `fixed inset-0` and `overflow-auto` become **separate**
69
+ * literals unless **layout** tokens sit between them in sorted order; then
70
+ * `position↔layout` and `layout↔sizing` each justify one hop and the whole “box in flow”
71
+ * (place + tracks + scrollport) stays in one chunk — co-located because they jointly define
72
+ * stacking and scroll behavior for the same surface.
73
+ */
74
+ const COMPATIBLE_BUCKET_SETS = [
75
+ new Set(["existence", "position"]),
76
+ new Set(["existence", "layout"]),
77
+ new Set(["position", "layout"]),
78
+ new Set(["layout", "sizing"]),
79
+ new Set(["shape", "shadow"]),
80
+ new Set(["background", "shadow"]),
81
+ new Set(["background", "typography"]),
82
+ new Set(["layout", "typography"]),
83
+ new Set(["sizing", "typography"]),
84
+ new Set(["motion", "behavior"]),
85
+ new Set(["motion", "starting"]),
86
+ new Set(["composite", "motion"]),
87
+ new Set(["sizing", "composite"]),
88
+ new Set(["sizing", "spacing"])
89
+ ];
90
+ /**
91
+ * Responsive / variant prefix — Tailwind v4 aware.
92
+ *
93
+ * v3: sm: md: … — v4: @sm:, @min-[600px]:, @[480px]:, named @md/sidebar:,
94
+ * viewport md/sidebar:, min-[100px]: / max-[100px]:, …
95
+ */
96
+ const RESPONSIVE_PREFIX = /^(?:@(?:min|max)-\[[^\]]+\]:|@\[[^\]]+\]:|@(?:[a-z0-9]+(?:-[a-z0-9]+)*)(?:\/[a-z][a-z0-9-]*)?:|(?:max-|min-)?(?:sm|md|lg|xl|2xl|3xl)(?:\/[a-z][a-z0-9-]*)?:|(?:max-|min-)\[[^\]]+\]:)/;
97
+ /** State variant stems — hoisted to module scope (not recreated per call). */
98
+ const STATE_PREFIXES = new Set([
99
+ "hover",
100
+ "focus",
101
+ "focus-within",
102
+ "focus-visible",
103
+ "active",
104
+ "visited",
105
+ "link",
106
+ "any-link",
107
+ "disabled",
108
+ "enabled",
109
+ "checked",
110
+ "indeterminate",
111
+ "required",
112
+ "optional",
113
+ "valid",
114
+ "invalid",
115
+ "in-range",
116
+ "out-of-range",
117
+ "read-only",
118
+ "read-write",
119
+ "placeholder-shown",
120
+ "default",
121
+ "autofill",
122
+ "open",
123
+ "closed",
124
+ "first",
125
+ "last",
126
+ "only",
127
+ "odd",
128
+ "even",
129
+ "first-of-type",
130
+ "last-of-type",
131
+ "only-of-type",
132
+ "first-child",
133
+ "last-child",
134
+ "only-child",
135
+ "empty",
136
+ "target",
137
+ "group",
138
+ "peer",
139
+ "aria",
140
+ "data",
141
+ "dark",
142
+ "light",
143
+ "supports",
144
+ "motion-reduce",
145
+ "motion-safe",
146
+ "print",
147
+ "screen",
148
+ "ltr",
149
+ "rtl",
150
+ "not",
151
+ "has",
152
+ "in",
153
+ "nth",
154
+ "file",
155
+ "selection",
156
+ "marker",
157
+ "placeholder",
158
+ "before",
159
+ "after",
160
+ "first-line",
161
+ "first-letter",
162
+ "backdrop",
163
+ "details-content",
164
+ "popover-open"
165
+ ]);
166
+ /** Default directory for arrange analyze/preview/apply when no target is passed. */
167
+ const DEFAULT_ARRANGE_TARGET = "packages/ui/src/components";
168
+ //#endregion
169
+ export { APPLY_MIN_TOKENS, BUCKET_ORDER, COMPATIBLE_BUCKET_SETS, DEFAULT_ARRANGE_TARGET, EMPTY_CN_TV_BINDINGS, LONG_STRING_TOKEN_THRESHOLD, MAX_CLASS_EXPR_DEPTH, MAX_GROUPS_BASE, MAX_GROUPS_CAP, MAX_GROUPS_HEADROOM, MAX_OBJECT_DEPTH, MAX_REPORT_LINES, MAX_STRIP_VARIANT_PASSES, MIN_GROUP_TOKENS, RESPONSIVE_PREFIX, STATE_PREFIXES };
@@ -0,0 +1,19 @@
1
+ //#region src/lib/arrange/domain/errors.ts
2
+ /**
3
+ * Structured errors for the arrange CLI library (throw from lib; handle exit in commands).
4
+ */
5
+ const ArrangeErrorCode = {
6
+ TARGET_NOT_FOUND: "TARGET_NOT_FOUND",
7
+ INVALID_INPUT: "INVALID_INPUT",
8
+ INTERNAL: "INTERNAL"
9
+ };
10
+ var ArrangeError = class extends Error {
11
+ code;
12
+ constructor(code, message, options) {
13
+ super(message, options);
14
+ this.name = "ArrangeError";
15
+ this.code = code;
16
+ }
17
+ };
18
+ //#endregion
19
+ export { ArrangeError, ArrangeErrorCode };
@@ -0,0 +1,235 @@
1
+ import { BUCKET_ORDER } from "./constants.mjs";
2
+ import { bucketsCompatible, bucketsMergeCompatible, classifyToken, compositeSecondaryOrder, selectorKey, stateKey, stripVariants, tokenizeClassString } from "./tokenizer.mjs";
3
+ //#region src/lib/arrange/domain/grouping.ts
4
+ function isVariantKeyedBucket(bucket) {
5
+ return bucket === "selector" || bucket === "state" || bucket === "starting";
6
+ }
7
+ function variantGroupKey(bucket, classToken) {
8
+ return bucket === "selector" ? selectorKey(classToken) : stateKey(classToken);
9
+ }
10
+ /**
11
+ * True when `staticLiteralTexts` carries the **same partition** of Tailwind tokens as
12
+ * `suggestedGroups` from {@link suggestCnGroups}, ignoring order of arguments and
13
+ * order of tokens within each chunk.
14
+ */
15
+ function areCnTailwindPartitionsEquivalent(staticLiteralTexts, suggestedGroups) {
16
+ const partitionSignatures = (chunks) => chunks.map((chunk) => {
17
+ const classTokens = tokenizeClassString(chunk);
18
+ return classTokens.length === 0 ? "" : [...classTokens].sort().join(" ");
19
+ }).filter((s) => s.length > 0).sort((a, b) => a.localeCompare(b));
20
+ const staticPartitionSigs = partitionSignatures(staticLiteralTexts);
21
+ const suggestedPartitionSigs = partitionSignatures(suggestedGroups);
22
+ if (staticPartitionSigs.length !== suggestedPartitionSigs.length) return false;
23
+ for (let i = 0; i < staticPartitionSigs.length; i++) if (staticPartitionSigs[i] !== suggestedPartitionSigs[i]) return false;
24
+ return true;
25
+ }
26
+ /** Dominant bucket of a whitespace-delimited class group (for merge heuristics). */
27
+ function dominantBucketOfGroup(groupStr) {
28
+ const counts = /* @__PURE__ */ new Map();
29
+ for (const classToken of tokenizeClassString(groupStr)) {
30
+ const tokenBucket = classifyToken(classToken);
31
+ counts.set(tokenBucket, (counts.get(tokenBucket) ?? 0) + 1);
32
+ }
33
+ let best = "other";
34
+ let bestN = 0;
35
+ for (const [bucket, count] of counts) {
36
+ if (count > bestN) {
37
+ best = bucket;
38
+ bestN = count;
39
+ continue;
40
+ }
41
+ if (count === bestN && BUCKET_ORDER[bucket] < BUCKET_ORDER[best]) best = bucket;
42
+ }
43
+ return best;
44
+ }
45
+ /** Dynamic cap: more tokens → allow more groups, within [BASE, CAP]. */
46
+ function dynamicMaxGroups(tokenCount) {
47
+ const byTokens = Math.ceil(tokenCount / 2) + 2;
48
+ return Math.max(4, Math.min(24, byTokens));
49
+ }
50
+ /**
51
+ * Merge singleton groups (< MIN_GROUP_TOKENS tokens) into their nearest
52
+ * neighbour. Prefers merging toward a bucket-compatible neighbor.
53
+ *
54
+ * Loop termination: each iteration either splices one element (shrinking the
55
+ * array, guaranteeing progress) or finds no singleton that can merge and exits
56
+ * via `changed = false`. Two mutually-incompatible singletons both hit
57
+ * `continue` and `changed` stays false, so the loop exits without spinning.
58
+ */
59
+ function mergeSingletons(groups) {
60
+ if (groups.length <= 1) return groups;
61
+ const result = [...groups];
62
+ let changed = true;
63
+ while (changed) {
64
+ changed = false;
65
+ for (let i = 0; i < result.length; i++) if (tokenizeClassString(result[i]).length < 2) {
66
+ if (result.length === 1) break;
67
+ const myBucket = dominantBucketOfGroup(result[i]);
68
+ const prevCompat = i > 0 && bucketsMergeCompatible(myBucket, dominantBucketOfGroup(result[i - 1]));
69
+ const nextCompat = i < result.length - 1 && bucketsMergeCompatible(myBucket, dominantBucketOfGroup(result[i + 1]));
70
+ let mergeDir;
71
+ if (nextCompat && !prevCompat) mergeDir = "forward";
72
+ else if (prevCompat && !nextCompat) mergeDir = "backward";
73
+ else if (prevCompat && nextCompat) mergeDir = i < result.length - 1 ? "forward" : "backward";
74
+ else continue;
75
+ if (mergeDir === "forward") result[i + 1] = result[i] + " " + result[i + 1];
76
+ else result[i - 1] = result[i - 1] + " " + result[i];
77
+ result.splice(i, 1);
78
+ changed = true;
79
+ break;
80
+ }
81
+ }
82
+ return result;
83
+ }
84
+ /** Tie-break when two merge candidates are both {@link bucketsMergeCompatible} (same bucket or COMPATIBLE_BUCKET_SETS). */
85
+ function capMergePenalty(leftBucket, rightBucket) {
86
+ if (leftBucket === rightBucket) return 0;
87
+ if (bucketsCompatible(leftBucket, rightBucket)) return 0;
88
+ return 500;
89
+ }
90
+ /**
91
+ * Merge adjacent groups until total count ≤ maxGroups.
92
+ * Only merges pairs allowed by {@link bucketsMergeCompatible} — never glues incompatible
93
+ * buckets (which previously defaulted to penalty 0 and merged layout+state when over cap).
94
+ *
95
+ * Complexity: O(n²) in the number of groups — each merge pass scans all remaining adjacent
96
+ * pairs. In practice `n` is bounded by {@link MAX_GROUPS_CAP} (24) and typical class strings
97
+ * are well below that, so this is not a concern for the current usage pattern. If this ever
98
+ * runs in a high-throughput batch mode, consider a priority-queue approach.
99
+ */
100
+ function capGroups(groups, maxGroups) {
101
+ const result = [...groups];
102
+ const lengths = result.map((groupStr) => tokenizeClassString(groupStr).length);
103
+ while (result.length > maxGroups) {
104
+ const mergePairCandidates = [];
105
+ for (let i = 0; i < result.length - 1; i++) {
106
+ const dominantLeft = dominantBucketOfGroup(result[i]);
107
+ const dominantRight = dominantBucketOfGroup(result[i + 1]);
108
+ if (!bucketsMergeCompatible(dominantLeft, dominantRight)) continue;
109
+ mergePairCandidates.push({
110
+ i,
111
+ size: lengths[i] + lengths[i + 1],
112
+ penalty: capMergePenalty(dominantLeft, dominantRight)
113
+ });
114
+ }
115
+ if (mergePairCandidates.length === 0) break;
116
+ let best = mergePairCandidates[0];
117
+ for (const candidate of mergePairCandidates) if (candidate.penalty < best.penalty || candidate.penalty === best.penalty && candidate.size < best.size) best = candidate;
118
+ const bestIdx = best.i;
119
+ result[bestIdx] = result[bestIdx] + " " + result[bestIdx + 1];
120
+ lengths[bestIdx] = lengths[bestIdx] + lengths[bestIdx + 1];
121
+ result.splice(bestIdx + 1, 1);
122
+ lengths.splice(bestIdx + 1, 1);
123
+ }
124
+ return result;
125
+ }
126
+ /**
127
+ * If a chunk is **only** bare `ease-*` timing (Motion), buffer it and prepend to the **first**
128
+ * **later** chunk that is predominantly `state` **and** contains some `animate-*` utility —
129
+ * skipping intermediate `state` chunks (`sm:…`, `group-data-[…]:…`, etc.) that sit between
130
+ * `ease-ui` and `data-open:animate-*`. If no such chunk exists, the ease chunk is left as-is.
131
+ */
132
+ function mergeEaseTimingIntoFollowingAnimatedState(groups) {
133
+ const result = [...groups];
134
+ let changed = true;
135
+ while (changed) {
136
+ changed = false;
137
+ for (let i = 0; i < result.length; i++) {
138
+ if (!chunkIsOnlyEaseTimingMotion(result[i])) continue;
139
+ const easeStr = result[i];
140
+ let target = -1;
141
+ for (let j = i + 1; j < result.length; j++) {
142
+ if (dominantBucketOfGroup(result[j]) !== "state") continue;
143
+ if (!tokenizeClassString(result[j]).some((classToken) => /^animate/.test(stripVariants(classToken)))) continue;
144
+ target = j;
145
+ break;
146
+ }
147
+ if (target === -1) continue;
148
+ result[target] = `${easeStr} ${result[target]}`.trim();
149
+ result.splice(i, 1);
150
+ changed = true;
151
+ break;
152
+ }
153
+ }
154
+ return result;
155
+ }
156
+ function chunkIsOnlyEaseTimingMotion(groupStr) {
157
+ const classTokens = tokenizeClassString(groupStr);
158
+ if (classTokens.length === 0) return false;
159
+ return classTokens.every((classToken) => classifyToken(classToken) === "motion" && /^ease-/.test(stripVariants(classToken)));
160
+ }
161
+ function suggestCnGroups(classString) {
162
+ const tokens = tokenizeClassString(classString);
163
+ if (tokens.length === 0) return [];
164
+ const classified = tokens.map((classToken, index) => ({
165
+ classToken,
166
+ bucket: classifyToken(classToken),
167
+ index
168
+ }));
169
+ classified.sort((left, right) => {
170
+ const bucketOrderDiff = BUCKET_ORDER[left.bucket] - BUCKET_ORDER[right.bucket];
171
+ if (bucketOrderDiff !== 0) return bucketOrderDiff;
172
+ if (left.bucket === "composite" && right.bucket === "composite") {
173
+ const compositeOrderDiff = compositeSecondaryOrder(stripVariants(left.classToken)) - compositeSecondaryOrder(stripVariants(right.classToken));
174
+ if (compositeOrderDiff !== 0) return compositeOrderDiff;
175
+ }
176
+ return left.index - right.index;
177
+ });
178
+ const rawGroups = [];
179
+ /** Bucket of the last token already placed in the current run (pairwise compat with `COMPATIBLE_BUCKET_SETS`). */
180
+ let lastBucketInRun = null;
181
+ let currentStateKey = null;
182
+ let currentTokens = [];
183
+ const flush = () => {
184
+ if (currentTokens.length > 0) {
185
+ rawGroups.push(currentTokens.join(" "));
186
+ currentTokens = [];
187
+ }
188
+ lastBucketInRun = null;
189
+ currentStateKey = null;
190
+ };
191
+ for (const { classToken, bucket: tokenBucket } of classified) {
192
+ if (lastBucketInRun === null) {
193
+ lastBucketInRun = tokenBucket;
194
+ currentStateKey = isVariantKeyedBucket(tokenBucket) ? variantGroupKey(tokenBucket, classToken) : null;
195
+ currentTokens.push(classToken);
196
+ continue;
197
+ }
198
+ if (isVariantKeyedBucket(tokenBucket)) {
199
+ const key = variantGroupKey(tokenBucket, classToken);
200
+ if (currentStateKey !== null && key !== currentStateKey) {
201
+ flush();
202
+ lastBucketInRun = tokenBucket;
203
+ currentStateKey = key;
204
+ currentTokens.push(classToken);
205
+ continue;
206
+ }
207
+ }
208
+ if (!bucketsCompatible(tokenBucket, lastBucketInRun)) {
209
+ flush();
210
+ lastBucketInRun = tokenBucket;
211
+ currentStateKey = isVariantKeyedBucket(tokenBucket) ? variantGroupKey(tokenBucket, classToken) : null;
212
+ currentTokens.push(classToken);
213
+ continue;
214
+ }
215
+ if (isVariantKeyedBucket(tokenBucket)) currentStateKey = variantGroupKey(tokenBucket, classToken);
216
+ else if (currentStateKey !== null) currentStateKey = null;
217
+ lastBucketInRun = tokenBucket;
218
+ currentTokens.push(classToken);
219
+ }
220
+ flush();
221
+ const withEaseMerged = mergeEaseTimingIntoFollowingAnimatedState(mergeSingletons(rawGroups));
222
+ return capGroups(withEaseMerged, Math.max(dynamicMaxGroups(tokens.length), withEaseMerged.length));
223
+ }
224
+ /**
225
+ * One label per suggested group: a single bucket name, or `mixed:a+b` when that chunk
226
+ * spans multiple Tailwind buckets (matches `codefast arrange group` stdout).
227
+ */
228
+ function summarizeGroupBucketLabels(groups) {
229
+ return groups.map((g) => {
230
+ const uniq = new Set(tokenizeClassString(g).map(classifyToken));
231
+ return uniq.size === 1 ? [...uniq][0] : `mixed:${[...uniq].sort().join("+")}`;
232
+ });
233
+ }
234
+ //#endregion
235
+ export { areCnTailwindPartitionsEquivalent, capGroups, mergeEaseTimingIntoFollowingAnimatedState, mergeSingletons, suggestCnGroups, summarizeGroupBucketLabels };
@@ -0,0 +1,54 @@
1
+ import ts from "typescript";
2
+ //#region src/lib/arrange/domain/imports.ts
3
+ function sourceFileImportsCn(sf) {
4
+ for (const statement of sf.statements) {
5
+ if (!ts.isImportDeclaration(statement) || !statement.importClause) continue;
6
+ const clause = statement.importClause;
7
+ if (clause.name?.text === "cn") return true;
8
+ if (clause.namedBindings && ts.isNamedImports(clause.namedBindings)) {
9
+ for (const importedBinding of clause.namedBindings.elements) if (importedBinding.name.text === "cn") return true;
10
+ }
11
+ }
12
+ return false;
13
+ }
14
+ /**
15
+ * Resolve the module specifier used when injecting a `cn` import into a file.
16
+ */
17
+ function cnModuleSpecifierForFile(filePath, override) {
18
+ if (override) return override;
19
+ return "@codefast/tailwind-variants";
20
+ }
21
+ function findImportDeclarationFromModule(sf, moduleSpecifier) {
22
+ for (const statement of sf.statements) {
23
+ if (!ts.isImportDeclaration(statement)) continue;
24
+ const spec = statement.moduleSpecifier;
25
+ if (ts.isStringLiteral(spec) && spec.text === moduleSpecifier) return statement;
26
+ }
27
+ }
28
+ function ensureCnImport(sourceText, filePath, cnImportOverride) {
29
+ const sf = ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true, filePath.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
30
+ if (sourceFileImportsCn(sf)) return sourceText;
31
+ const moduleSpecifier = cnModuleSpecifierForFile(filePath, cnImportOverride);
32
+ const decl = findImportDeclarationFromModule(sf, moduleSpecifier);
33
+ if (decl?.importClause && !decl.importClause.isTypeOnly && decl.importClause.namedBindings && ts.isNamedImports(decl.importClause.namedBindings)) {
34
+ const elements = decl.importClause.namedBindings.elements;
35
+ if (elements.length > 0) {
36
+ const pos = elements[0].getStart(sf);
37
+ return `${sourceText.slice(0, pos)}cn, ${sourceText.slice(pos)}`;
38
+ }
39
+ }
40
+ const importLine = `import { cn } from "${moduleSpecifier}";`;
41
+ let firstImport = -1;
42
+ for (const statement of sf.statements) if (ts.isImportDeclaration(statement)) {
43
+ firstImport = statement.getStart(sf);
44
+ break;
45
+ }
46
+ const useClient = /^["']use client["'];?\s*\r?\n/.exec(sourceText);
47
+ let insertAt;
48
+ if (useClient) insertAt = useClient[0].length;
49
+ else if (firstImport !== -1) insertAt = firstImport;
50
+ else insertAt = 0;
51
+ return `${sourceText.slice(0, insertAt)}${importLine}\n${sourceText.slice(insertAt)}`;
52
+ }
53
+ //#endregion
54
+ export { cnModuleSpecifierForFile, ensureCnImport };