@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,216 @@
1
+ import { COMPATIBLE_BUCKET_SETS, RESPONSIVE_PREFIX, STATE_PREFIXES } from "./constants.mjs";
2
+ //#region src/lib/arrange/domain/tokenizer.ts
3
+ /**
4
+ * Variant stems not covered by a single entry in STATE_PREFIXES (compound `has-*`,
5
+ * numbered `nth-*`, media features, v4 `in-[…]`, child selectors `*` / `**`, …).
6
+ * When these are missed, `isStateToken` is false and the variant is stripped — the
7
+ * utility is then bucketed as if it were unconditional, which breaks preview grouping.
8
+ */
9
+ function isCompoundOrMediaVariantPrefix(prefix) {
10
+ if (prefix === "*" || prefix === "**") return true;
11
+ if (prefix === "inert") return true;
12
+ if (prefix.startsWith("has-")) return true;
13
+ if (prefix.startsWith("in-[")) return true;
14
+ if (/^nth(?:-last)?(?:-of-type)?(?:-\d+|-\[)/.test(prefix) || /^nth-of-type-\d+$/.test(prefix) || /^nth-last-of-type-\d+$/.test(prefix)) return true;
15
+ if (/^(?:user-valid|user-invalid|contrast-more|contrast-less|forced-colors|inverted-colors)$/.test(prefix)) return true;
16
+ if (/^(?:any-)?pointer-(?:fine|coarse|none)$/.test(prefix)) return true;
17
+ if (/^(?:portrait|landscape|noscript)$/.test(prefix)) return true;
18
+ return false;
19
+ }
20
+ function tokenizeClassString(classString) {
21
+ return classString.trim().split(/\s+/).filter(Boolean);
22
+ }
23
+ /**
24
+ * Index of the first `:` that separates a Tailwind variant segment from the rest.
25
+ * Colons inside `[...]` (at positive bracket depth) are ignored so selectors like
26
+ * `[&_a:hover]:text-red-500` split as `[&_a:hover]:` + `text-red-500`.
27
+ */
28
+ function indexOfFirstVariantColon(text) {
29
+ let depth = 0;
30
+ for (let i = 0; i < text.length; i++) {
31
+ const ch = text[i];
32
+ if (ch === "[") depth++;
33
+ else if (ch === "]") depth = Math.max(0, depth - 1);
34
+ else if (ch === ":" && depth === 0) return i;
35
+ }
36
+ return -1;
37
+ }
38
+ function stripVariants(token) {
39
+ let withoutVariants = token;
40
+ const MAX_PASSES = 12;
41
+ for (let i = 0; i < MAX_PASSES; i++) {
42
+ const colonIdx = indexOfFirstVariantColon(withoutVariants);
43
+ if (colonIdx === -1) break;
44
+ withoutVariants = withoutVariants.slice(colonIdx + 1);
45
+ }
46
+ return withoutVariants;
47
+ }
48
+ /** Outermost variant segment: first `:` at bracket depth 0 → text before it. */
49
+ function firstLeadingVariantPrefix(token) {
50
+ const colonIdx = indexOfFirstVariantColon(token);
51
+ if (colonIdx === -1) return;
52
+ return token.slice(0, colonIdx);
53
+ }
54
+ function isSelectorVariantToken(token) {
55
+ const prefix = firstLeadingVariantPrefix(token);
56
+ if (prefix === void 0) return false;
57
+ if (prefix === "*" || prefix === "**") return true;
58
+ if (prefix.startsWith("has-")) return true;
59
+ if (prefix.startsWith("in-[")) return true;
60
+ if (prefix.startsWith("group-[")) return true;
61
+ if (prefix.startsWith("peer-[")) return true;
62
+ if (prefix.startsWith("not-[")) return true;
63
+ return false;
64
+ }
65
+ function isStateToken(token) {
66
+ const prefix = firstLeadingVariantPrefix(token);
67
+ if (prefix === void 0) return false;
68
+ if (RESPONSIVE_PREFIX.test(token)) return true;
69
+ if (STATE_PREFIXES.has(prefix)) return true;
70
+ if (prefix.startsWith("data-[")) return true;
71
+ if (prefix.startsWith("aria-[")) return true;
72
+ if (prefix.startsWith("supports-[")) return true;
73
+ if (prefix.startsWith("in-data-")) return true;
74
+ if (/^data-(?!\[)/.test(prefix)) return true;
75
+ if (/^aria-/.test(prefix)) return true;
76
+ if (/^(group|peer)-/.test(prefix)) return true;
77
+ if (/^not-/.test(prefix)) return true;
78
+ if (isCompoundOrMediaVariantPrefix(prefix)) return true;
79
+ if (prefix.startsWith("[") && prefix.endsWith("]")) return true;
80
+ return false;
81
+ }
82
+ /**
83
+ * Secondary sort inside the **composite** bucket: opacity / blend / isolation →
84
+ * 3D context → 3D transforms → 2D transforms → filters → will-change.
85
+ */
86
+ function compositeSecondaryOrder(bareUtility) {
87
+ const b = bareUtility;
88
+ if (/^opacity(?:-|$)/.test(b) || /^mix-blend-/.test(b) || /^isolation(?:-|$)/.test(b) || b === "isolate") return 0;
89
+ if (b === "transform-3d" || /^perspective(?:-|$)/.test(b)) return 10;
90
+ if (/^rotate-[xyz](?:-|$)/.test(b) || /^translate-z(?:-|$)/.test(b) || /^scale-z(?:-|$)/.test(b)) return 20;
91
+ if (/^translate(?:-|$)/.test(b) || /^scale(?:-|$)/.test(b) || /^rotate(?:-|$)/.test(b) || /^skew(?:-|$)/.test(b)) return 30;
92
+ if (/^transform(?:-(?:gpu|cpu|none))?$/.test(b)) return 35;
93
+ if (/^blur(?:-|$)/.test(b) || /^backdrop-/.test(b) || /^filter(?:-|$)/.test(b) || /^brightness-/.test(b) || /^contrast-/.test(b) || /^grayscale(?:-|$)/.test(b) || /^hue-rotate-/.test(b) || /^invert(?:-|$)/.test(b) || /^saturate-/.test(b) || /^sepia(?:-|$)/.test(b)) return 40;
94
+ if (/^will-change/.test(b)) return 50;
95
+ return 99;
96
+ }
97
+ /** Classify a **bare** utility (no `hover:` / `md:` / … prefixes). */
98
+ function classifyBareUtility(bareUtility) {
99
+ const b = bareUtility;
100
+ if (/^@container(?:\/[a-z][a-z0-9-]*)?$/i.test(b)) return "existence";
101
+ if (/^(?:hidden|contents|sr-only|not-sr-only|list-item|flow-root)$/.test(b) || /^(?:block|inline-block|inline)$/.test(b)) return "existence";
102
+ if (/^table(?:$|-)/.test(b)) return "existence";
103
+ if (/^contain-/.test(b)) return "existence";
104
+ if (/^(?:group|peer)(?:\/[a-z][a-z0-9-]*)?$/.test(b)) return "existence";
105
+ if (/^(?:static|relative|absolute|fixed|sticky)$/.test(b)) return "position";
106
+ if (/^-?(?:inset|top|right|bottom|left|start|end)(?:-|\/|$)/.test(b) || /^-?(?:inset-x|inset-y|inset-s|inset-e)-/.test(b)) return "position";
107
+ if (/^-?z-/.test(b) || b === "z-auto") return "position";
108
+ if (/^(?:flex|inline-flex|grid|inline-grid|subgrid|masonry)(?:$|-)/.test(b) || /^(?:items|justify|justify-items|justify-self|content|self|place|place-content|place-items|place-self)-/.test(b) || /^-?(?:gap|space-[xy]|col-|row-|grid-|auto-cols|auto-rows|order-|order$)/.test(b) || /^-?(?:auto-flow|grid-flow)-/.test(b) || /^(?:columns-|break-after|break-before|break-inside)-/.test(b) || /^(?:float|clear)-/.test(b) || /^(?:wrap-|flex-wrap|flex-nowrap|flex-row|flex-col|flex-auto|flex-initial|flex-none|flex-1|flex-\[)/.test(b) || /^container$/.test(b)) return "layout";
109
+ if (/^-?(?:w|h|min-w|max-w|min-h|max-h|size|aspect|shrink|grow|basis)-/.test(b) || /^(?:shrink|grow)(?:$|-)/.test(b) || /^-?overflow-/.test(b) || /^-?object-/.test(b)) return "sizing";
110
+ if (/^-?(?:p|px|py|pt|pb|pl|pr|ps|pe|m|mx|my|mt|mb|ml|mr|ms|me)-/.test(b)) return "spacing";
111
+ if (/^-?(?:rounded|border|ring|divide|inset-ring)(?:-|\/|$)/.test(b) || /^(?:rounded|border|ring|divide|inset-ring)$/.test(b) || /^ring-offset-/.test(b)) return "shape";
112
+ if (/^-?bg-/.test(b) || /^(?:from|via|to)(?:-|\/|$)/.test(b) || /^bg-(?:linear|radial|conic)-/.test(b) || /^-?mask-/.test(b) || /^-?(?:fill|stroke)-/.test(b)) return "background";
113
+ if (/^-?(?:shadow|inset-shadow|drop-shadow|text-shadow)(?:-|\/|$)/.test(b) || /^(?:shadow|inset-shadow)$/.test(b) || /^-?outline(?:-|\/|$)/.test(b) || /^outline$/.test(b)) return "shadow";
114
+ if (/^-?(?:font|leading|tracking|list|indent|align|decoration|underline-offset|text-wrap|text-balance|text-pretty)-/.test(b) || /^-?(?:text-(?!shadow))/.test(b) || /^-?(?:whitespace|break|line-clamp|hyphens)-/.test(b) || /^(?:antialiased|subpixel-antialiased|italic|not-italic|overline|line-through|underline|no-underline|uppercase|lowercase|capitalize|normal-case|truncate|text-wrap|text-balance|text-pretty)$/.test(b) || /^text-wrap-/.test(b) || /^tabular-nums$|^slashed-zero$|^lining-nums$|^oldstyle-nums$|^proportional-nums$/.test(b)) return "typography";
115
+ if (/^opacity(?:-|$)/.test(b) || /^mix-blend-/.test(b) || /^isolation(?:-|$)/.test(b) || b === "isolate" || /^transform(?:-(?:gpu|cpu|none))?$/.test(b) || /^-?(?:translate|scale|rotate|skew)-/.test(b) || /^perspective(?:-|$)/.test(b) || b === "transform-3d" || /^blur(?:-|$)/.test(b) || /^backdrop-/.test(b) || /^filter(?:-|$)/.test(b) || /^brightness-/.test(b) || /^contrast-/.test(b) || /^grayscale(?:-|$)/.test(b) || /^hue-rotate-/.test(b) || /^invert(?:-|$)/.test(b) || /^saturate-/.test(b) || /^sepia(?:-|$)/.test(b) || /^will-change/.test(b)) return "composite";
116
+ if (/^(?:transition|duration|ease|delay|animate)(?:-|$)/.test(b) || /^ease-/.test(b)) return "motion";
117
+ if (/^(?:cursor|pointer-events|select|touch|scroll|scroll-m|scroll-p|overscroll|snap|resize|field-sizing|appearance|scheme|color-scheme|accent|caret)(?:-|$)/.test(b) || /^(?:cursor|select|resize)$/.test(b) || /^scroll-behavior/.test(b) || /^scroll-snap/.test(b) || /^touch-action/.test(b) || b === "inert") return "behavior";
118
+ return "other";
119
+ }
120
+ /**
121
+ * Arbitrary **parent** selectors (`[&…]`, `[&>a]:…`, slot forms with `&` inside the bracket)
122
+ * scope utilities to descendants — bucket as `selector` so they chunk apart from base layout/typography
123
+ * and are labeled distinctly from interactive `state` variants (`hover:`, `data-[…]:`, …).
124
+ * Pure arbitrary **properties** (`[--x]:`, `[color:red]`) have no `&` in the leading `[…]` segment.
125
+ */
126
+ function isArbitraryParentSelectorStateToken(token) {
127
+ if (/^\[&/.test(token)) return true;
128
+ if (!token.startsWith("[")) return false;
129
+ const colonIdx = indexOfFirstVariantColon(token);
130
+ if (colonIdx === -1 || colonIdx === 0 || token[colonIdx - 1] !== "]") return false;
131
+ return token.slice(0, colonIdx).includes("&");
132
+ }
133
+ function classifyToken(token) {
134
+ if (isArbitraryParentSelectorStateToken(token)) return "selector";
135
+ if (isSelectorVariantToken(token)) return "selector";
136
+ if (/^@container(?:\/[a-z][a-z0-9-]*)?$/i.test(token)) return "existence";
137
+ if (token.startsWith("[")) {
138
+ const splitIdx = indexOfFirstVariantColon(token);
139
+ if (splitIdx === -1) return "arbitrary";
140
+ if (splitIdx === 0 || token[splitIdx - 1] !== "]") return "arbitrary";
141
+ }
142
+ if (firstLeadingVariantPrefix(token) === "starting") return "starting";
143
+ if (isStateToken(token)) return "state";
144
+ return classifyBareUtility(stripVariants(token));
145
+ }
146
+ /**
147
+ * Stable key for `data-[…]` variants so unrelated selectors stay split, while
148
+ * **branching on the full bracket** (e.g. `data-[vaul-drawer-direction=bottom]`
149
+ * vs `=left`) keeps direction-specific utilities in separate state groups.
150
+ */
151
+ function dataAttributeStem(token) {
152
+ const match = token.match(/^data-\[([^\]]*)\]/);
153
+ return match ? `data-[${match[1]}]` : "data";
154
+ }
155
+ /** Same idea as {@link dataAttributeStem} for `aria-[…]:` variants. */
156
+ function ariaAttributeStem(token) {
157
+ const match = token.match(/^aria-\[([^\]]*)\]/);
158
+ return match ? `aria-[${match[1]}]` : "aria";
159
+ }
160
+ /** `in-data-[…]:` — normalize on the full bracket expression like {@link dataAttributeStem}. */
161
+ function inDataAttributeStem(token) {
162
+ const match = token.match(/^in-data-\[([^\]]*)\]/);
163
+ return match ? `in-data-[${match[1]}]` : "in-data";
164
+ }
165
+ /**
166
+ * Stable key for splitting adjacent `state` / `starting` tokens in {@link suggestCnGroups}
167
+ * (`selector` uses {@link selectorKey}).
168
+ * Uses the **full variant stack** (every `:` segment outside `[…]`), not only the
169
+ * outermost prefix, so `@md/foo:[&>*]:w-auto` and `@md/foo:has-[…]:mt-px` stay in
170
+ * separate groups while `hover:opacity` still keys as `hover` + `opacity`…
171
+ *
172
+ * `data-[…]` / `aria-[…]` normalize the first segment via {@link dataAttributeStem} /
173
+ * {@link ariaAttributeStem} (full token required for bracket capture).
174
+ */
175
+ function stateKey(token) {
176
+ const layers = [];
177
+ let rest = token;
178
+ while (rest.length > 0) {
179
+ const colonIdx = indexOfFirstVariantColon(rest);
180
+ if (colonIdx === -1) break;
181
+ layers.push(rest.slice(0, colonIdx));
182
+ rest = rest.slice(colonIdx + 1);
183
+ }
184
+ if (layers.length === 0) return token;
185
+ if (layers[0].startsWith("data-[")) layers[0] = dataAttributeStem(token);
186
+ else if (layers[0].startsWith("aria-[")) layers[0] = ariaAttributeStem(token);
187
+ else if (layers[0].startsWith("in-data-[")) layers[0] = inDataAttributeStem(token);
188
+ return layers.join("");
189
+ }
190
+ const SELECTOR_KEY_SEP = "";
191
+ /**
192
+ * Variant key for {@link Bucket} `"selector"` tokens in {@link suggestCnGroups}.
193
+ * Reuses {@link stateKey} layer splitting, then normalizes common shadcn/Radix SVG patterns so
194
+ * `[&_svg]:…` and `[&_svg:not([class*='size-'])]:…` stay in one chunk.
195
+ */
196
+ function selectorKey(token) {
197
+ return stateKey(token).split(SELECTOR_KEY_SEP).map(normalizeSelectorVariantLayer).join(SELECTOR_KEY_SEP);
198
+ }
199
+ function normalizeSelectorVariantLayer(layer) {
200
+ if (layer === "[&_svg:not([class*='size-'])]") return "[&_svg]";
201
+ return layer;
202
+ }
203
+ function bucketsCompatible(a, b) {
204
+ if (a === b) return true;
205
+ return COMPATIBLE_BUCKET_SETS.some((bucketSet) => bucketSet.has(a) && bucketSet.has(b));
206
+ }
207
+ /** Like {@link bucketsCompatible}, but never merge two distinct state / starting / selector variant blobs. */
208
+ function bucketsMergeCompatible(a, b) {
209
+ if (a === "state" && b === "state") return false;
210
+ if (a === "starting" && b === "starting") return false;
211
+ if (a === "selector" && b === "selector") return false;
212
+ if (a === "arbitrary" || b === "arbitrary") return false;
213
+ return bucketsCompatible(a, b);
214
+ }
215
+ //#endregion
216
+ export { bucketsCompatible, bucketsMergeCompatible, classifyBareUtility, classifyToken, compositeSecondaryOrder, indexOfFirstVariantColon, isArbitraryParentSelectorStateToken, selectorKey, stateKey, stripVariants, tokenizeClassString };
@@ -0,0 +1,15 @@
1
+ import { DEFAULT_ARRANGE_TARGET, LONG_STRING_TOKEN_THRESHOLD, MAX_STRIP_VARIANT_PASSES } from "./domain/constants.mjs";
2
+ import { bucketsCompatible, bucketsMergeCompatible, classifyToken, indexOfFirstVariantColon, selectorKey, stateKey, stripVariants, tokenizeClassString } from "./domain/tokenizer.mjs";
3
+ import { forEachStringLiteralInClassExpression, mergeCnUnconditionalLiteralPoolForTest } from "./domain/ast/collectors-cn.mjs";
4
+ import { KNOWN_CN_TV_MODULES, applyEditsDescending, buildKnownCnTvBindings, indentOfLineContaining, isCnOrTvIdentifier, lineOf, moduleLooksLikeCnTvReexport, unwrapCnInsideTvCallReplacement } from "./domain/ast/ast-helpers.mjs";
5
+ import { DEFAULT_SKIP_DIRS, walkTsxFiles } from "./infra/walk.mjs";
6
+ import { analyzeDirectory } from "./application/analyze.mjs";
7
+ import { printAnalyzeReport } from "./presentation/report.mjs";
8
+ import { areCnTailwindPartitionsEquivalent, capGroups, mergeEaseTimingIntoFollowingAnimatedState, mergeSingletons, suggestCnGroups, summarizeGroupBucketLabels } from "./domain/grouping.mjs";
9
+ import { escapeTsStringLiteralContent, formatArray, formatCnArguments, formatCnCall, formatJsxCnAttributeValue } from "./presentation/formatters.mjs";
10
+ import { cnModuleSpecifierForFile, ensureCnImport } from "./domain/imports.mjs";
11
+ import { groupFile } from "./application/group-file.mjs";
12
+ import { ArrangeError, ArrangeErrorCode } from "./domain/errors.mjs";
13
+ import { runArrangeSync, runOnTarget } from "./application/run-target.mjs";
14
+ import { createNodeCliFs, createNodeCliLogger } from "../infra/node-io.mjs";
15
+ export { ArrangeError, ArrangeErrorCode, DEFAULT_ARRANGE_TARGET, DEFAULT_SKIP_DIRS, KNOWN_CN_TV_MODULES, LONG_STRING_TOKEN_THRESHOLD, MAX_STRIP_VARIANT_PASSES, analyzeDirectory, applyEditsDescending, areCnTailwindPartitionsEquivalent, bucketsCompatible, bucketsMergeCompatible, buildKnownCnTvBindings, capGroups, classifyToken, cnModuleSpecifierForFile, createNodeCliFs, createNodeCliLogger, ensureCnImport, escapeTsStringLiteralContent, forEachStringLiteralInClassExpression, formatArray, formatCnArguments, formatCnCall, formatJsxCnAttributeValue, groupFile, indentOfLineContaining, indexOfFirstVariantColon, isCnOrTvIdentifier, lineOf, mergeCnUnconditionalLiteralPoolForTest, mergeEaseTimingIntoFollowingAnimatedState, mergeSingletons, moduleLooksLikeCnTvReexport, printAnalyzeReport, runArrangeSync, runOnTarget, selectorKey, stateKey, stripVariants, suggestCnGroups, summarizeGroupBucketLabels, tokenizeClassString, unwrapCnInsideTvCallReplacement, walkTsxFiles };
@@ -0,0 +1,33 @@
1
+ import path from "node:path";
2
+ //#region src/lib/arrange/infra/walk.ts
3
+ const DEFAULT_SKIP_DIRS = new Set([
4
+ "node_modules",
5
+ "dist",
6
+ ".git",
7
+ ".turbo",
8
+ ".next",
9
+ ".cache",
10
+ "out",
11
+ "build",
12
+ "coverage",
13
+ ".vercel",
14
+ ".output"
15
+ ]);
16
+ function walkTsxFiles(rootDirectoryPath, fs) {
17
+ const result = [];
18
+ const visit = (entryPath) => {
19
+ if (fs.statSync(entryPath).isDirectory()) {
20
+ for (const childName of fs.readdirSync(entryPath)) {
21
+ if (DEFAULT_SKIP_DIRS.has(childName)) continue;
22
+ visit(path.join(entryPath, childName));
23
+ }
24
+ return;
25
+ }
26
+ if (entryPath.endsWith(".d.ts")) return;
27
+ if (entryPath.endsWith(".tsx") || entryPath.endsWith(".ts")) result.push(entryPath);
28
+ };
29
+ visit(rootDirectoryPath);
30
+ return result;
31
+ }
32
+ //#endregion
33
+ export { DEFAULT_SKIP_DIRS, walkTsxFiles };
@@ -0,0 +1,52 @@
1
+ import { indentOfLineContaining } from "../domain/ast/ast-helpers.mjs";
2
+ //#region src/lib/arrange/presentation/formatters.ts
3
+ function escapeTsStringLiteralContent(group) {
4
+ return group.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"");
5
+ }
6
+ /**
7
+ * Multiline fragment for use *inside* an existing `cn(...)` — replaces one
8
+ * string argument with several, without producing `cn(cn(...))`.
9
+ */
10
+ function formatCnArguments(groups, options) {
11
+ const indent = options?.indent ?? " ";
12
+ const commaAfterLast = options?.commaAfterLastGroup ?? options?.trailingClassName ?? false;
13
+ const lines = [];
14
+ for (let i = 0; i < groups.length; i++) {
15
+ const group = groups[i];
16
+ const comma = i < groups.length - 1 || commaAfterLast ? "," : "";
17
+ lines.push(`${indent}"${escapeTsStringLiteralContent(group)}"${comma}`);
18
+ }
19
+ if (options?.trailingClassName) lines.push(`${indent}className,`);
20
+ return lines.join("\n");
21
+ }
22
+ function formatCnCall(groups, options) {
23
+ const lines = ["cn("];
24
+ const commaOnEachStringLine = groups.length > 1 || Boolean(options?.trailingClassName);
25
+ for (let i = 0; i < groups.length; i++) {
26
+ const group = groups[i];
27
+ const comma = commaOnEachStringLine ? "," : "";
28
+ lines.push(` "${escapeTsStringLiteralContent(group)}"${comma}`);
29
+ }
30
+ if (options?.trailingClassName) lines.push(" className,");
31
+ lines.push(")");
32
+ return lines.join("\n");
33
+ }
34
+ function formatArray(groups) {
35
+ const lines = ["["];
36
+ for (let i = 0; i < groups.length; i++) {
37
+ const group = groups[i];
38
+ const comma = i < groups.length - 1 || groups.length > 1 ? "," : "";
39
+ lines.push(` "${escapeTsStringLiteralContent(group)}"${comma}`);
40
+ }
41
+ lines.push("]");
42
+ return lines.join("\n");
43
+ }
44
+ function formatJsxCnAttributeValue(groups, source, valueNodeStart) {
45
+ const baseIndent = indentOfLineContaining(source, valueNodeStart);
46
+ return `{cn(\n${formatCnArguments(groups, {
47
+ indent: `${baseIndent} `,
48
+ commaAfterLastGroup: groups.length > 1
49
+ })}\n${baseIndent})}`;
50
+ }
51
+ //#endregion
52
+ export { escapeTsStringLiteralContent, formatArray, formatCnArguments, formatCnCall, formatJsxCnAttributeValue };
@@ -0,0 +1,24 @@
1
+ import "../domain/constants.mjs";
2
+ import path from "node:path";
3
+ //#region src/lib/arrange/presentation/report.ts
4
+ function printAnalyzeReport(analyzeRootDirectoryPath, analyzeReport, logger) {
5
+ const { out } = logger;
6
+ out(`Path: ${path.resolve(analyzeRootDirectoryPath)}`);
7
+ out(`.ts/.tsx files: ${analyzeReport.files}`);
8
+ out(`cn(...) call sites: ${analyzeReport.cnCallExpressions}`);
9
+ out(`tv(...) call sites: ${analyzeReport.tvCallExpressions}`);
10
+ out(`\nLong cn(...) string literals (≥18 tokens — consider splitting arguments): ${analyzeReport.longCnStringLiterals.length}`);
11
+ for (const findingEntry of analyzeReport.longCnStringLiterals.slice(0, 40)) out(` ${findingEntry.file}:${findingEntry.line} (${findingEntry.tokenCount} tokens) ${findingEntry.preview}`);
12
+ if (analyzeReport.longCnStringLiterals.length > 40) out(` … and ${analyzeReport.longCnStringLiterals.length - 40} more`);
13
+ out(`\nLong tv({...}) string literals in base/variants/… (≥18 tokens): ${analyzeReport.longTvStringLiterals.length}`);
14
+ for (const findingEntry of analyzeReport.longTvStringLiterals.slice(0, 40)) out(` ${findingEntry.file}:${findingEntry.line} (${findingEntry.tokenCount} tokens) ${findingEntry.preview}`);
15
+ if (analyzeReport.longTvStringLiterals.length > 40) out(` … and ${analyzeReport.longTvStringLiterals.length - 40} more`);
16
+ out(`\nJSX className="..." or className={'...'} (static strings, ≥18 tokens): ${analyzeReport.longJsxClassNameLiterals.length}`);
17
+ for (const findingEntry of analyzeReport.longJsxClassNameLiterals.slice(0, 40)) out(` ${findingEntry.file}:${findingEntry.line} (${findingEntry.tokenCount} tokens) ${findingEntry.preview}`);
18
+ if (analyzeReport.longJsxClassNameLiterals.length > 40) out(` … and ${analyzeReport.longJsxClassNameLiterals.length - 40} more`);
19
+ out(`\ncn(...) nested inside tv({...}) (prefer a string or array — preview/apply can rewrite): ${analyzeReport.cnInsideTvCalls.length}`);
20
+ for (const findingEntry of analyzeReport.cnInsideTvCalls.slice(0, 40)) out(` ${findingEntry.file}:${findingEntry.line} (${findingEntry.argCount} args) ${findingEntry.preview}`);
21
+ if (analyzeReport.cnInsideTvCalls.length > 40) out(` … and ${analyzeReport.cnInsideTvCalls.length - 40} more`);
22
+ }
23
+ //#endregion
24
+ export { printAnalyzeReport };
@@ -0,0 +1,25 @@
1
+ import { z } from "zod";
2
+ //#region src/lib/config/domain/schema.ts
3
+ const hookSchema = z.custom((value) => typeof value === "function", { message: "Expected a function" });
4
+ const mirrorConfigSchema = z.object({
5
+ skipPackages: z.array(z.string()).optional(),
6
+ pathTransformations: z.record(z.string(), z.object({ removePrefix: z.string().optional() }).strict()).optional(),
7
+ customExports: z.record(z.string(), z.record(z.string(), z.string())).optional(),
8
+ cssExports: z.record(z.string(), z.union([z.boolean(), z.object({
9
+ enabled: z.boolean().optional(),
10
+ customExports: z.record(z.string(), z.string()).optional(),
11
+ forceExportFiles: z.boolean().optional()
12
+ }).strict()])).optional()
13
+ }).strict();
14
+ const codefastTagConfigSchema = z.object({
15
+ skipPackages: z.array(z.string()).optional(),
16
+ onAfterWrite: hookSchema.optional()
17
+ }).strict();
18
+ const codefastArrangeConfigSchema = z.object({ onAfterWrite: hookSchema.optional() }).strict();
19
+ const codefastConfigSchema = z.object({
20
+ mirror: mirrorConfigSchema.optional(),
21
+ tag: codefastTagConfigSchema.optional(),
22
+ arrange: codefastArrangeConfigSchema.optional()
23
+ }).strict();
24
+ //#endregion
25
+ export { codefastArrangeConfigSchema, codefastConfigSchema, codefastTagConfigSchema, hookSchema, mirrorConfigSchema };
@@ -0,0 +1,3 @@
1
+ import { codefastArrangeConfigSchema, codefastConfigSchema, codefastTagConfigSchema, hookSchema, mirrorConfigSchema } from "./domain/schema.mjs";
2
+ import { loadConfig, resetConfigLoaderCacheForTests } from "./infra/loader.mjs";
3
+ export { codefastArrangeConfigSchema, codefastConfigSchema, codefastTagConfigSchema, hookSchema, loadConfig, mirrorConfigSchema, resetConfigLoaderCacheForTests };
@@ -0,0 +1,78 @@
1
+ import { codefastConfigSchema } from "../domain/schema.mjs";
2
+ import process from "node:process";
3
+ import path from "node:path";
4
+ import jiti from "jiti";
5
+ import { ZodError } from "zod";
6
+ //#region src/lib/config/infra/loader.ts
7
+ const CONFIG_JS_PRIORITY = [
8
+ "codefast.config.mjs",
9
+ "codefast.config.js",
10
+ "codefast.config.cjs"
11
+ ];
12
+ const CONFIG_JSON = "codefast.config.json";
13
+ const cachedLoads = /* @__PURE__ */ new Map();
14
+ function formatZodError(error, filePath) {
15
+ const formatted = error.issues.map((issue) => {
16
+ return ` - ${issue.path.length > 0 ? issue.path.join(".") : "<root>"}: ${issue.message}`;
17
+ }).join("\n");
18
+ return `Invalid config schema in ${path.basename(filePath)}:\n${formatted}`;
19
+ }
20
+ function parseLoadedConfig(raw, filePath) {
21
+ try {
22
+ return codefastConfigSchema.parse(raw);
23
+ } catch (caughtError) {
24
+ if (caughtError instanceof ZodError) throw new Error(formatZodError(caughtError, filePath));
25
+ throw caughtError;
26
+ }
27
+ }
28
+ function listConfigCandidates(startDir, fs) {
29
+ const candidates = [];
30
+ let current = path.resolve(startDir);
31
+ while (true) {
32
+ for (const name of CONFIG_JS_PRIORITY) {
33
+ const candidate = path.join(current, name);
34
+ if (fs.existsSync(candidate)) candidates.push(candidate);
35
+ }
36
+ const jsonCandidate = path.join(current, CONFIG_JSON);
37
+ if (fs.existsSync(jsonCandidate)) candidates.push(jsonCandidate);
38
+ const parent = path.dirname(current);
39
+ if (parent === current) break;
40
+ current = parent;
41
+ }
42
+ return candidates;
43
+ }
44
+ async function readConfigFromPath(filePath, fs) {
45
+ if (path.extname(filePath) === ".json") {
46
+ const content = await fs.readFile(filePath, "utf8");
47
+ return parseLoadedConfig(JSON.parse(content), filePath);
48
+ }
49
+ const moduleConfig = jiti(process.cwd(), {
50
+ interopDefault: true,
51
+ moduleCache: false
52
+ })(filePath);
53
+ return parseLoadedConfig(typeof moduleConfig === "object" && moduleConfig !== null && "default" in moduleConfig ? moduleConfig.default : moduleConfig, filePath);
54
+ }
55
+ async function loadOnce(fs, startDir) {
56
+ const warnings = [];
57
+ const configPaths = listConfigCandidates(startDir, fs);
58
+ if (configPaths.length === 0) return {
59
+ config: {},
60
+ warnings
61
+ };
62
+ const configPath = configPaths[0];
63
+ return {
64
+ config: await readConfigFromPath(configPath, fs),
65
+ warnings,
66
+ configPath
67
+ };
68
+ }
69
+ async function loadConfig(fs, startDir = process.cwd()) {
70
+ const cacheKey = path.resolve(startDir);
71
+ if (!cachedLoads.has(cacheKey)) cachedLoads.set(cacheKey, loadOnce(fs, cacheKey));
72
+ return cachedLoads.get(cacheKey);
73
+ }
74
+ function resetConfigLoaderCacheForTests() {
75
+ cachedLoads.clear();
76
+ }
77
+ //#endregion
78
+ export { loadConfig, resetConfigLoaderCacheForTests };
@@ -0,0 +1,20 @@
1
+ //#region src/lib/infra/caught-unknown-message.ts
2
+ /**
3
+ * Stable, user-facing text for values caught as `unknown`.
4
+ * Avoids `String(unknown)` / `[object Object]` and satisfies strict `no-base-to-string`.
5
+ */
6
+ function messageFromCaughtUnknown(value) {
7
+ if (value instanceof Error) return value.message;
8
+ if (typeof value === "string") return value;
9
+ if (typeof value === "number" || typeof value === "bigint" || typeof value === "boolean") return String(value);
10
+ if (typeof value === "symbol") return String(value);
11
+ if (value === null) return "null";
12
+ if (value === void 0) return "undefined";
13
+ try {
14
+ const serialized = JSON.stringify(value);
15
+ if (typeof serialized === "string") return serialized;
16
+ } catch {}
17
+ return "[unserializable non-Error value]";
18
+ }
19
+ //#endregion
20
+ export { messageFromCaughtUnknown };
@@ -0,0 +1,10 @@
1
+ //#region src/lib/infra/config-reporter.ts
2
+ const YELLOW = "\x1B[33m";
3
+ const RESET = "\x1B[0m";
4
+ /** User-visible lines for non-fatal issues returned with a successfully parsed config. */
5
+ function printConfigSchemaWarnings(logger, warnings) {
6
+ const { out } = logger;
7
+ for (const warningMessage of warnings) out(`${YELLOW}\u26A0\uFE0F ${warningMessage}${RESET}`);
8
+ }
9
+ //#endregion
10
+ export { printConfigSchemaWarnings };
@@ -0,0 +1,30 @@
1
+ import process from "node:process";
2
+ import fsSync from "node:fs";
3
+ import fsPromises from "node:fs/promises";
4
+ //#region src/lib/infra/node-io.ts
5
+ function createNodeCliFs() {
6
+ return {
7
+ existsSync: fsSync.existsSync,
8
+ statSync: fsSync.statSync,
9
+ readFileSync: fsSync.readFileSync,
10
+ writeFileSync: fsSync.writeFileSync,
11
+ readdirSync: fsSync.readdirSync,
12
+ readFile: (p, enc) => fsPromises.readFile(p, enc),
13
+ writeFile: (p, data, enc) => fsPromises.writeFile(p, data, enc),
14
+ readdir: (p, opts) => fsPromises.readdir(p, opts),
15
+ rename: (oldPath, newPath) => fsPromises.rename(oldPath, newPath),
16
+ unlink: (p) => fsPromises.unlink(p)
17
+ };
18
+ }
19
+ function createNodeCliLogger() {
20
+ return {
21
+ out: (line) => {
22
+ process.stdout.write(`${line}\n`);
23
+ },
24
+ err: (line) => {
25
+ process.stderr.write(`${line}\n`);
26
+ }
27
+ };
28
+ }
29
+ //#endregion
30
+ export { createNodeCliFs, createNodeCliLogger };
@@ -0,0 +1,22 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ //#region src/lib/infra/workspace/repo-root.ts
4
+ /**
5
+ * Resolve the monorepo root (directory containing `pnpm-workspace.yaml`).
6
+ * Tries the compiled package location first, then `process.cwd()`.
7
+ */
8
+ function findRepoRoot(fs) {
9
+ const candidates = [path.dirname(fileURLToPath(import.meta.url)), process.cwd()];
10
+ for (const start of candidates) {
11
+ let dir = path.resolve(start);
12
+ for (;;) {
13
+ if (fs.existsSync(path.join(dir, "pnpm-workspace.yaml"))) return dir;
14
+ const parent = path.dirname(dir);
15
+ if (parent === dir) break;
16
+ dir = parent;
17
+ }
18
+ }
19
+ throw new Error(`Could not locate monorepo root (missing pnpm-workspace.yaml). Searched from: ${candidates.join(", ")}`);
20
+ }
21
+ //#endregion
22
+ export { findRepoRoot };