@codefast/cli 0.3.7-canary.0 → 0.3.13-canary.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.
Files changed (124) hide show
  1. package/README.md +178 -262
  2. package/dist/analyze-DvtJ3j9m.mjs +92 -0
  3. package/dist/arrange-Bbh62DrU.mjs +107 -0
  4. package/dist/arrange-BqP2DI3w.mjs +8 -0
  5. package/dist/ast-helpers-MDRYuIAk.mjs +115 -0
  6. package/dist/bin.mjs +8 -0
  7. package/dist/caught-unknown-message-CRhkDWO7.mjs +20 -0
  8. package/dist/collectors-cn-BS8TTQ8O.mjs +61 -0
  9. package/dist/collectors-jsx-D9tL2ZPM.mjs +20 -0
  10. package/dist/collectors-tv-D2PXZaRt.mjs +170 -0
  11. package/dist/colors-BjdkCfGK.mjs +12 -0
  12. package/dist/commands/arrange.mjs +2 -0
  13. package/dist/commands/mirror.mjs +2 -0
  14. package/dist/commands/mirror.test.mjs +48 -0
  15. package/dist/commands/tag.mjs +2 -0
  16. package/dist/config-DVia_f9a.mjs +3 -0
  17. package/dist/config-reporter-SCcWDZc1.mjs +10 -0
  18. package/dist/constants-AtPhF-Ct.mjs +169 -0
  19. package/dist/constants-Bq5wdyLC.mjs +8 -0
  20. package/dist/dirent-list-BSDeRG5i.mjs +14 -0
  21. package/dist/engine-CcWEHSCq.mjs +287 -0
  22. package/dist/engine-DRTdkc5u.mjs +236 -0
  23. package/dist/errors-CFH4w6ic.mjs +15 -0
  24. package/dist/errors-CpU1ogfA.mjs +19 -0
  25. package/dist/formatters-DGE8jJ53.mjs +52 -0
  26. package/dist/group-file-ClW7XdD6.mjs +113 -0
  27. package/dist/grouping-c1grpCQT.mjs +235 -0
  28. package/dist/imports-DSVq1xhB.mjs +54 -0
  29. package/dist/lib/arrange/application/analyze.mjs +2 -0
  30. package/dist/lib/arrange/application/analyze.test.mjs +171 -0
  31. package/dist/lib/arrange/application/group-file.mjs +2 -0
  32. package/dist/lib/arrange/application/group-file.test.mjs +273 -0
  33. package/dist/lib/arrange/application/run-target.mjs +2 -0
  34. package/dist/lib/arrange/application/run-target.test.mjs +97 -0
  35. package/dist/lib/arrange/domain/ast/ast-helpers.mjs +2 -0
  36. package/dist/lib/arrange/domain/ast/ast-helpers.test.mjs +148 -0
  37. package/dist/lib/arrange/domain/ast/collectors-cn.mjs +2 -0
  38. package/dist/lib/arrange/domain/ast/collectors-cn.test.mjs +58 -0
  39. package/dist/lib/arrange/domain/ast/collectors-jsx.mjs +2 -0
  40. package/dist/lib/arrange/domain/ast/collectors-jsx.test.mjs +20 -0
  41. package/dist/lib/arrange/domain/ast/collectors-tv.mjs +2 -0
  42. package/dist/lib/arrange/domain/ast/collectors-tv.test.mjs +145 -0
  43. package/dist/lib/arrange/domain/ast/targets.mjs +2 -0
  44. package/dist/lib/arrange/domain/ast/targets.test.mjs +50 -0
  45. package/dist/lib/arrange/domain/constants.mjs +2 -0
  46. package/dist/lib/arrange/domain/errors.mjs +2 -0
  47. package/dist/lib/arrange/domain/grouping.mjs +2 -0
  48. package/dist/lib/arrange/domain/grouping.test.mjs +186 -0
  49. package/dist/lib/arrange/domain/imports.mjs +2 -0
  50. package/dist/lib/arrange/domain/imports.test.mjs +48 -0
  51. package/dist/lib/arrange/domain/tokenizer.mjs +2 -0
  52. package/dist/lib/arrange/domain/tokenizer.test.mjs +165 -0
  53. package/dist/lib/arrange/domain/types.mjs +1 -0
  54. package/dist/lib/arrange/index.mjs +16 -0
  55. package/dist/lib/arrange/infra/walk.mjs +2 -0
  56. package/dist/lib/arrange/infra/walk.test.mjs +50 -0
  57. package/dist/lib/arrange/presentation/formatters.mjs +2 -0
  58. package/dist/lib/arrange/presentation/formatters.test.mjs +73 -0
  59. package/dist/lib/arrange/presentation/report.mjs +2 -0
  60. package/dist/lib/config/domain/schema.mjs +2 -0
  61. package/dist/lib/config/index.mjs +4 -0
  62. package/dist/lib/config/infra/loader.mjs +2 -0
  63. package/dist/lib/config/infra/loader.test.mjs +116 -0
  64. package/dist/lib/infra/caught-unknown-message.mjs +2 -0
  65. package/dist/lib/infra/config-reporter.mjs +2 -0
  66. package/dist/lib/infra/fs-contract.mjs +1 -0
  67. package/dist/lib/infra/node-io.mjs +2 -0
  68. package/dist/lib/infra/workspace/repo-root.mjs +2 -0
  69. package/dist/lib/infra/workspace/repo-root.test.mjs +45 -0
  70. package/dist/lib/mirror/application/engine.mjs +2 -0
  71. package/dist/lib/mirror/application/sync.mjs +2 -0
  72. package/dist/lib/mirror/application/sync.test.mjs +482 -0
  73. package/dist/lib/mirror/domain/constants.mjs +2 -0
  74. package/dist/lib/mirror/domain/errors.mjs +2 -0
  75. package/dist/lib/mirror/domain/types.mjs +1 -0
  76. package/dist/lib/mirror/index.mjs +3 -0
  77. package/dist/lib/mirror/infra/dirent-list.mjs +2 -0
  78. package/dist/lib/mirror/infra/dirent-list.test.mjs +18 -0
  79. package/dist/lib/mirror/infra/package-filter.mjs +2 -0
  80. package/dist/lib/mirror/infra/package-filter.test.mjs +32 -0
  81. package/dist/lib/mirror/infra/path-normalizer.mjs +2 -0
  82. package/dist/lib/mirror/infra/update-pkg.mjs +2 -0
  83. package/dist/lib/mirror/infra/workspace-packages.mjs +2 -0
  84. package/dist/lib/mirror/infra/workspace-packages.test.mjs +167 -0
  85. package/dist/lib/mirror/presentation/reporter.mjs +2 -0
  86. package/dist/lib/tag/application/engine.mjs +2 -0
  87. package/dist/lib/tag/application/engine.test.mjs +159 -0
  88. package/dist/lib/tag/domain/types.mjs +1 -0
  89. package/dist/lib/tag/index.mjs +5 -0
  90. package/dist/lib/tag/infra/target-resolver.mjs +2 -0
  91. package/dist/lib/tag/presentation/colors.mjs +2 -0
  92. package/dist/lib/tag/presentation/tag-presenter.mjs +2 -0
  93. package/dist/lib/tag/presentation/tag-presenter.test.mjs +29 -0
  94. package/dist/loader-DpN-zl5g.mjs +78 -0
  95. package/dist/mirror-CbGiLXKT.mjs +2 -0
  96. package/dist/mirror-DqJE3ejL.mjs +66 -0
  97. package/dist/node-io-DFRbHO6m.mjs +30 -0
  98. package/dist/package-filter-C_8ryqvz.mjs +26 -0
  99. package/dist/path-normalizer-D4ceBO3l.mjs +7 -0
  100. package/dist/program-DR-H4tH3.mjs +34 -0
  101. package/dist/program.mjs +2 -0
  102. package/dist/repo-root-HmtIEm6J.mjs +22 -0
  103. package/dist/report-DYxvw34T.mjs +24 -0
  104. package/dist/reporter-CwIe_tAM.mjs +108 -0
  105. package/dist/run-target-CJWrqNT4.mjs +65 -0
  106. package/dist/schema-Or8RAARd.mjs +25 -0
  107. package/dist/sync-e1Sp-shi.mjs +157 -0
  108. package/dist/tag-Cd9JujU5.mjs +2 -0
  109. package/dist/tag-hEKVZ_-7.mjs +58 -0
  110. package/dist/tag-presenter-Cs66UBsH.mjs +63 -0
  111. package/dist/target-resolver-CR3FXMTy.mjs +170 -0
  112. package/dist/targets-pCTslnFh.mjs +106 -0
  113. package/dist/tokenizer-CeZx73Zu.mjs +216 -0
  114. package/dist/update-pkg-CnGfjWBy.mjs +37 -0
  115. package/dist/walk-DjgQn66n.mjs +33 -0
  116. package/dist/workspace-packages-K2NYTHJq.mjs +159 -0
  117. package/package.json +42 -53
  118. package/CHANGELOG.md +0 -15
  119. package/dist/cjs/index.cjs +0 -2
  120. package/dist/cjs/src/index.d.ts +0 -3
  121. package/dist/cjs/src/index.d.ts.map +0 -1
  122. package/dist/esm/index.js +0 -2
  123. package/dist/esm/src/index.d.ts +0 -3
  124. package/dist/esm/src/index.d.ts.map +0 -1
@@ -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 { ensureCnImport as n, cnModuleSpecifierForFile as t };
@@ -0,0 +1,2 @@
1
+ import { t as analyzeDirectory } from "../../../analyze-DvtJ3j9m.mjs";
2
+ export { analyzeDirectory };
@@ -0,0 +1,171 @@
1
+ import { t as analyzeDirectory } from "../../../analyze-DvtJ3j9m.mjs";
2
+ import { t as printAnalyzeReport } from "../../../report-DYxvw34T.mjs";
3
+ import { n as createNodeCliLogger, t as createNodeCliFs } from "../../../node-io-DFRbHO6m.mjs";
4
+ import "../../../arrange-BqP2DI3w.mjs";
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+ import os from "node:os";
8
+ //#region src/lib/arrange/application/analyze.test.ts
9
+ const arrangeFs = createNodeCliFs();
10
+ const arrangeLogger = createNodeCliLogger();
11
+ function captureStdout(fn) {
12
+ const chunks = [];
13
+ const spy = jest.spyOn(process.stdout, "write").mockImplementation((chunk) => {
14
+ chunks.push(typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8"));
15
+ return true;
16
+ });
17
+ try {
18
+ fn();
19
+ } finally {
20
+ spy.mockRestore();
21
+ }
22
+ return chunks.join("");
23
+ }
24
+ describe("analyzeDirectory + printAnalyzeReport", () => {
25
+ it("collects long cn / tv / JSX literals and nested cn in tv", () => {
26
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "arr-analyze-"));
27
+ const long = "peer flex size-4 shrink-0 items-center justify-center rounded-sm border border-input text-primary-foreground shadow-xs outline-hidden transition hover:not-disabled:not-aria-checked:border-ring/60 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:opacity-50";
28
+ const source = `import { cn, tv } from "@codefast/tailwind-variants";
29
+
30
+ cn("${long}");
31
+ export const styles = tv({ base: "${long}" });
32
+ export function X() { return <div className={"${long}"} />; }
33
+ export const nested = tv({ base: cn("x", "${long}") });
34
+ `;
35
+ try {
36
+ fs.writeFileSync(path.join(dir, "Report.tsx"), source, "utf8");
37
+ const report = analyzeDirectory(dir, arrangeFs);
38
+ expect(report.files).toBe(1);
39
+ expect(report.cnCallExpressions).toBeGreaterThanOrEqual(1);
40
+ expect(report.tvCallExpressions).toBeGreaterThanOrEqual(1);
41
+ expect(report.longCnStringLiterals.length).toBeGreaterThanOrEqual(1);
42
+ expect(report.longTvStringLiterals.length).toBeGreaterThanOrEqual(1);
43
+ expect(report.longJsxClassNameLiterals.length).toBeGreaterThanOrEqual(1);
44
+ expect(report.cnInsideTvCalls.length).toBeGreaterThanOrEqual(1);
45
+ const printed = captureStdout(() => printAnalyzeReport(dir, report, arrangeLogger));
46
+ expect(printed).toContain("Long cn(...)");
47
+ expect(printed).toContain("nested inside tv");
48
+ } finally {
49
+ fs.rmSync(dir, {
50
+ recursive: true,
51
+ force: true
52
+ });
53
+ }
54
+ });
55
+ it("truncates long-cn listing after MAX_REPORT_LINES", () => {
56
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "arr-analyze-many-"));
57
+ const long = "peer flex size-4 shrink-0 items-center justify-center rounded-sm border border-input text-primary-foreground shadow-xs outline-hidden transition hover:not-disabled:not-aria-checked:border-ring/60 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:opacity-50";
58
+ try {
59
+ let body = `import { cn } from "@codefast/tailwind-variants";\n`;
60
+ for (let i = 0; i < 42; i++) body += `cn("${long}");\n`;
61
+ fs.writeFileSync(path.join(dir, "Many.tsx"), body, "utf8");
62
+ const report = analyzeDirectory(dir, arrangeFs);
63
+ const printed = captureStdout(() => printAnalyzeReport(dir, report, arrangeLogger));
64
+ expect(report.longCnStringLiterals.length).toBeGreaterThan(40);
65
+ expect(printed).toMatch(/… and \d+ more/);
66
+ } finally {
67
+ fs.rmSync(dir, {
68
+ recursive: true,
69
+ force: true
70
+ });
71
+ }
72
+ });
73
+ it("truncates long-tv, JSX, and cn-inside-tv sections", () => {
74
+ const row = {
75
+ file: "x.tsx",
76
+ line: 1,
77
+ tokenCount: 22,
78
+ preview: "p".repeat(40)
79
+ };
80
+ const manyTv = Array.from({ length: 42 }, (_, i) => ({
81
+ ...row,
82
+ line: i + 1
83
+ }));
84
+ const manyJsx = Array.from({ length: 42 }, (_, i) => ({
85
+ ...row,
86
+ line: i + 1
87
+ }));
88
+ const manyNested = Array.from({ length: 42 }, (_, i) => ({
89
+ file: "x.tsx",
90
+ line: i + 1,
91
+ argCount: 1,
92
+ preview: "cn(...)"
93
+ }));
94
+ const matches = captureStdout(() => printAnalyzeReport("/tmp/arr-truncate", {
95
+ files: 1,
96
+ cnCallExpressions: 0,
97
+ tvCallExpressions: 1,
98
+ cnInsideTvCalls: manyNested,
99
+ longCnStringLiterals: [],
100
+ longTvStringLiterals: manyTv,
101
+ longJsxClassNameLiterals: manyJsx
102
+ }, arrangeLogger)).match(/… and \d+ more/g);
103
+ expect(matches?.length).toBeGreaterThanOrEqual(3);
104
+ });
105
+ it("does not count JSX className={identifier} as long literal", () => {
106
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "arr-jsx-dyn-"));
107
+ try {
108
+ fs.writeFileSync(path.join(dir, "Dyn.tsx"), `export function F(cls: string) { return <div className={cls} />; }`, "utf8");
109
+ const report = analyzeDirectory(dir, arrangeFs);
110
+ expect(report.longJsxClassNameLiterals.length).toBe(0);
111
+ } finally {
112
+ fs.rmSync(dir, {
113
+ recursive: true,
114
+ force: true
115
+ });
116
+ }
117
+ });
118
+ it("analyzes tv compoundVariants with quoted className key", () => {
119
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "arr-tv-quoted-"));
120
+ const long = "peer flex size-4 shrink-0 items-center justify-center rounded-sm border border-input text-primary-foreground shadow-xs outline-hidden transition hover:not-disabled:not-aria-checked:border-ring/60 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:opacity-50";
121
+ try {
122
+ fs.writeFileSync(path.join(dir, "Quoted.tsx"), `import { tv } from "@codefast/tailwind-variants";
123
+ export const styles = tv({ compoundVariants: [{ "className": "${long}" }] });`, "utf8");
124
+ const report = analyzeDirectory(dir, arrangeFs);
125
+ expect(report.longTvStringLiterals.length).toBeGreaterThanOrEqual(1);
126
+ } finally {
127
+ fs.rmSync(dir, {
128
+ recursive: true,
129
+ force: true
130
+ });
131
+ }
132
+ });
133
+ it("analyzes tv array slots with nested cn", () => {
134
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "arr-analyze-tv-array-"));
135
+ const long = "peer flex size-4 shrink-0 items-center justify-center rounded-sm border border-input text-primary-foreground shadow-xs outline-hidden transition hover:not-disabled:not-aria-checked:border-ring/60 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:opacity-50";
136
+ try {
137
+ fs.writeFileSync(path.join(dir, "TvArray.tsx"), `import { cn, tv } from "@codefast/tailwind-variants";
138
+ export const styles = tv({ base: [cn("${long}"), "py-0"] });`, "utf8");
139
+ const report = analyzeDirectory(dir, arrangeFs);
140
+ expect(report.tvCallExpressions).toBeGreaterThanOrEqual(1);
141
+ expect(report.cnInsideTvCalls.length).toBeGreaterThanOrEqual(1);
142
+ } finally {
143
+ fs.rmSync(dir, {
144
+ recursive: true,
145
+ force: true
146
+ });
147
+ }
148
+ });
149
+ it("accepts legacy tailwind-variants imports during analyze pass (backward compat)", () => {
150
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "arr-analyze-legacy-"));
151
+ const long = "peer flex size-4 shrink-0 items-center justify-center rounded-sm border border-input text-primary-foreground shadow-xs outline-hidden transition hover:not-disabled:not-aria-checked:border-ring/60 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:opacity-50";
152
+ const source = `import { cn, tv } from "tailwind-variants";
153
+
154
+ cn("${long}");
155
+ export const styles = tv({ base: "${long}" });
156
+ `;
157
+ try {
158
+ fs.writeFileSync(path.join(dir, "Legacy.tsx"), source, "utf8");
159
+ const report = analyzeDirectory(dir, arrangeFs);
160
+ expect(report.cnCallExpressions).toBeGreaterThanOrEqual(1);
161
+ expect(report.tvCallExpressions).toBeGreaterThanOrEqual(1);
162
+ } finally {
163
+ fs.rmSync(dir, {
164
+ recursive: true,
165
+ force: true
166
+ });
167
+ }
168
+ });
169
+ });
170
+ //#endregion
171
+ export {};
@@ -0,0 +1,2 @@
1
+ import { t as groupFile } from "../../../group-file-ClW7XdD6.mjs";
2
+ export { groupFile };
@@ -0,0 +1,273 @@
1
+ import { t as groupFile } from "../../../group-file-ClW7XdD6.mjs";
2
+ import { n as createNodeCliLogger, t as createNodeCliFs } from "../../../node-io-DFRbHO6m.mjs";
3
+ import "../../../arrange-BqP2DI3w.mjs";
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import os from "node:os";
7
+ //#region src/lib/arrange/application/group-file.test.ts
8
+ const arrangeFs = createNodeCliFs();
9
+ const arrangeLogger = createNodeCliLogger();
10
+ function withTempFixture(name, source, fn) {
11
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "group-file-"));
12
+ const filePath = path.join(dir, name);
13
+ try {
14
+ fs.writeFileSync(filePath, source, "utf8");
15
+ fn(filePath);
16
+ } finally {
17
+ fs.rmSync(dir, {
18
+ recursive: true,
19
+ force: true
20
+ });
21
+ }
22
+ }
23
+ function captureStdout(fn) {
24
+ const chunks = [];
25
+ const spy = jest.spyOn(process.stdout, "write").mockImplementation((chunk) => {
26
+ chunks.push(typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8"));
27
+ return true;
28
+ });
29
+ try {
30
+ fn();
31
+ } finally {
32
+ spy.mockRestore();
33
+ }
34
+ return chunks.join("");
35
+ }
36
+ describe("groupFile", () => {
37
+ it("unwraps cn() inside tv base into string/array form", () => {
38
+ withTempFixture("FixtureTv.tsx", `import { cn, tv } from "@codefast/tailwind-variants";
39
+
40
+ export const styles = tv({
41
+ base: cn("flex gap-2 text-sm rounded-md border px-3 font-medium"),
42
+ });
43
+ `, (filePath) => {
44
+ const result = groupFile(filePath, {
45
+ write: true,
46
+ withClassName: false
47
+ }, arrangeFs, arrangeLogger);
48
+ const after = fs.readFileSync(filePath, "utf8");
49
+ expect(result.changed).toBeGreaterThan(0);
50
+ expect(after.includes("base: cn(")).toBe(false);
51
+ expect(after).toMatch(/base:\s*(\[|"[^"]+)/);
52
+ });
53
+ });
54
+ it("converts long JSX className and inserts cn import after use client", () => {
55
+ withTempFixture("FixtureUseClient.tsx", `"use client";
56
+
57
+ export function Fixture() {
58
+ return <div className="flex items-center gap-2 px-4 py-2 text-sm rounded-md border bg-card" />;
59
+ }
60
+ `, (filePath) => {
61
+ const result = groupFile(filePath, {
62
+ write: true,
63
+ withClassName: false
64
+ }, arrangeFs, arrangeLogger);
65
+ const after = fs.readFileSync(filePath, "utf8");
66
+ expect(result.changed).toBeGreaterThan(0);
67
+ expect(after.startsWith(`"use client";\n\nimport { cn }`)).toBe(true);
68
+ expect(after).toContain("className={cn(");
69
+ });
70
+ });
71
+ it("supports namespace imports for tw.tv / tw.cn and rewrites tw.cn usage", () => {
72
+ withTempFixture("FixtureNamespace.tsx", `import * as tw from "@codefast/tailwind-variants";
73
+
74
+ export const styles = tw.tv({
75
+ base: tw.cn("flex gap-2 text-sm rounded-md border px-3 font-medium hover:bg-accent"),
76
+ });
77
+ `, (filePath) => {
78
+ const result = groupFile(filePath, {
79
+ write: true,
80
+ withClassName: false
81
+ }, arrangeFs, arrangeLogger);
82
+ const after = fs.readFileSync(filePath, "utf8");
83
+ expect(result.changed).toBeGreaterThan(0);
84
+ expect(after).toContain("tw.tv(");
85
+ expect(after.includes("tw.cn(")).toBe(false);
86
+ expect(after).toMatch(/base:\s*(\[|"[^"]+)/);
87
+ });
88
+ });
89
+ it("supports namespace imports from legacy tailwind-variants package (backward compat)", () => {
90
+ withTempFixture("FixtureNamespaceLegacy.tsx", `import * as tw from "tailwind-variants";
91
+
92
+ export const styles = tw.tv({
93
+ base: tw.cn("flex gap-2 text-sm rounded-md border px-3 font-medium hover:bg-accent"),
94
+ });
95
+ `, (filePath) => {
96
+ const result = groupFile(filePath, {
97
+ write: true,
98
+ withClassName: false
99
+ }, arrangeFs, arrangeLogger);
100
+ const after = fs.readFileSync(filePath, "utf8");
101
+ expect(result.changed).toBeGreaterThan(0);
102
+ expect(after).toContain("tw.tv(");
103
+ expect(after.includes("tw.cn(")).toBe(false);
104
+ expect(after).toMatch(/base:\s*(\[|"[^"]+)/);
105
+ });
106
+ });
107
+ it("recognizes cn/tv imports from local ./utils re-export", () => {
108
+ withTempFixture("FixtureLocalUtils.tsx", `import { cn, tv } from "./utils";
109
+ export const styles = tv({ base: cn("flex gap-2 text-sm rounded-md border px-3 font-medium hover:bg-accent") });`, (filePath) => {
110
+ const result = groupFile(filePath, {
111
+ write: true,
112
+ withClassName: false
113
+ }, arrangeFs, arrangeLogger);
114
+ expect(result.changed).toBeGreaterThan(0);
115
+ });
116
+ });
117
+ it("recognizes cn/tv imports from modules containing /utils/", () => {
118
+ withTempFixture("FixtureUtilsPath.tsx", `import { cn, tv } from "pkg/utils/cn";
119
+ export const styles = tv({ base: cn("flex gap-2 text-sm rounded-md border px-3 font-medium hover:bg-accent") });`, (filePath) => {
120
+ const result = groupFile(filePath, {
121
+ write: true,
122
+ withClassName: false
123
+ }, arrangeFs, arrangeLogger);
124
+ expect(result.changed).toBeGreaterThan(0);
125
+ });
126
+ });
127
+ it("recognizes cn/tv imports from ./cn.ts shim", () => {
128
+ withTempFixture("FixtureCnTs.tsx", `import { cn, tv } from "./cn.ts";
129
+ export const styles = tv({ base: cn("flex gap-2 text-sm rounded-md border px-3 font-medium hover:bg-accent") });`, (filePath) => {
130
+ const result = groupFile(filePath, {
131
+ write: true,
132
+ withClassName: false
133
+ }, arrangeFs, arrangeLogger);
134
+ expect(result.changed).toBeGreaterThan(0);
135
+ });
136
+ });
137
+ it("returns totalFound 0 for files without cn/tv/long class targets", () => {
138
+ withTempFixture("FixtureEmpty.tsx", "export const only = 1;\n", (filePath) => {
139
+ expect(groupFile(filePath, {
140
+ write: false,
141
+ withClassName: false
142
+ }, arrangeFs, arrangeLogger)).toEqual({
143
+ filePath,
144
+ totalFound: 0,
145
+ changed: 0
146
+ });
147
+ });
148
+ });
149
+ it("dry-run returns totalFound 0 when cn partition is already idempotent", () => {
150
+ withTempFixture("FixturePartitionEq.tsx", `import { cn } from "@codefast/tailwind-variants";
151
+ cn("flex gap-2", "text-sm");`, (filePath) => {
152
+ expect(groupFile(filePath, {
153
+ write: false,
154
+ withClassName: false
155
+ }, arrangeFs, arrangeLogger)).toEqual({
156
+ filePath,
157
+ totalFound: 0,
158
+ changed: 0
159
+ });
160
+ });
161
+ });
162
+ it("counts cn() inside tv with zero args as found but unchanged", () => {
163
+ withTempFixture("FixtureZeroArg.tsx", `import { cn, tv } from "@codefast/tailwind-variants";
164
+ export const broken = tv({ base: cn() });`, (filePath) => {
165
+ const dry = groupFile(filePath, {
166
+ write: false,
167
+ withClassName: false
168
+ }, arrangeFs, arrangeLogger);
169
+ expect(dry).toEqual({
170
+ filePath,
171
+ totalFound: 1,
172
+ changed: 0
173
+ });
174
+ const wet = groupFile(filePath, {
175
+ write: true,
176
+ withClassName: false
177
+ }, arrangeFs, arrangeLogger);
178
+ expect(wet).toEqual({
179
+ filePath,
180
+ totalFound: 1,
181
+ changed: 0
182
+ });
183
+ });
184
+ });
185
+ it("inserts cn import before first existing import", () => {
186
+ withTempFixture("FixtureImportOrder.tsx", `import React from "react";
187
+ export function Fixture() {
188
+ return <div className="flex items-center gap-2 px-4 py-2 text-sm rounded-md border bg-card" />;
189
+ }`, (filePath) => {
190
+ const result = groupFile(filePath, {
191
+ write: true,
192
+ withClassName: false
193
+ }, arrangeFs, arrangeLogger);
194
+ expect(result.changed).toBeGreaterThan(0);
195
+ const after = fs.readFileSync(filePath, "utf8");
196
+ expect(after.indexOf("import { cn }")).toBeLessThan(after.indexOf("import React"));
197
+ });
198
+ });
199
+ it("passes withClassName option through cn replacement", () => {
200
+ withTempFixture("FixtureWithClassName.tsx", `import { cn } from "@codefast/tailwind-variants";
201
+ export function Fixture() {
202
+ return <div className="flex items-center gap-2 px-4 py-2 text-sm rounded-md border bg-card" />;
203
+ }`, (filePath) => {
204
+ const result = groupFile(filePath, {
205
+ write: true,
206
+ withClassName: true
207
+ }, arrangeFs, arrangeLogger);
208
+ expect(result.changed).toBeGreaterThan(0);
209
+ const after = fs.readFileSync(filePath, "utf8");
210
+ expect(after).toContain("className={cn(");
211
+ expect(after).toContain("className");
212
+ });
213
+ });
214
+ it("merges cn into existing named import when cnImport override matches", () => {
215
+ withTempFixture("FixtureMergeImport.tsx", `import { tv } from "clsx";
216
+ export function Fixture() {
217
+ return <div className="flex items-center gap-2 px-4 py-2 text-sm rounded-md border bg-card" />;
218
+ }`, (filePath) => {
219
+ const result = groupFile(filePath, {
220
+ write: true,
221
+ withClassName: false,
222
+ cnImport: "clsx"
223
+ }, arrangeFs, arrangeLogger);
224
+ expect(result.changed).toBeGreaterThan(0);
225
+ const after = fs.readFileSync(filePath, "utf8");
226
+ expect(after).toContain("import { cn, tv } from \"clsx\"");
227
+ });
228
+ });
229
+ it("groups compoundVariants class array slots", () => {
230
+ withTempFixture("FixtureCvClassArray.tsx", `import { tv } from "@codefast/tailwind-variants";
231
+ export const sheet = tv({
232
+ compoundVariants: [{ className: ["flex gap-2 text-sm rounded-md border px-3 font-medium", "shadow-xs"] }],
233
+ });`, (filePath) => {
234
+ const result = groupFile(filePath, {
235
+ write: true,
236
+ withClassName: false
237
+ }, arrangeFs, arrangeLogger);
238
+ expect(result.changed).toBeGreaterThan(0);
239
+ const after = fs.readFileSync(filePath, "utf8");
240
+ expect(after).toContain("compoundVariants");
241
+ });
242
+ });
243
+ it("groups compoundVariants mixed class array slot with cn(...) and plain strings", () => {
244
+ withTempFixture("FixtureCvClassCn.tsx", `import { cn, tv } from "@codefast/tailwind-variants";
245
+ export const sheet = tv({
246
+ compoundVariants: [{ class: ["py-1", cn("flex gap-2 text-sm rounded-md border px-3 font-medium shadow-xs")] }],
247
+ });`, (filePath) => {
248
+ const result = groupFile(filePath, {
249
+ write: true,
250
+ withClassName: false
251
+ }, arrangeFs, arrangeLogger);
252
+ expect(result.changed).toBeGreaterThan(0);
253
+ const after = fs.readFileSync(filePath, "utf8");
254
+ expect(after).toContain("class:");
255
+ });
256
+ });
257
+ it("dry-run prints combined unwrap + grouping note", () => {
258
+ const long = "flex items-center gap-2 px-4 py-2 text-sm rounded-md border bg-card font-medium shadow-xs";
259
+ withTempFixture("FixtureDryCombined.tsx", `import { cn, tv } from "@codefast/tailwind-variants";
260
+ cn("${long}");
261
+ export const styles = tv({ base: cn("${long}") });`, (filePath) => {
262
+ const output = captureStdout(() => {
263
+ groupFile(filePath, {
264
+ write: false,
265
+ withClassName: false
266
+ }, arrangeFs, arrangeLogger);
267
+ });
268
+ expect(output).toContain("[cn] / [tv] / [JSX className]");
269
+ });
270
+ });
271
+ });
272
+ //#endregion
273
+ export {};
@@ -0,0 +1,2 @@
1
+ import { n as runOnTarget, t as runArrangeSync } from "../../../run-target-CJWrqNT4.mjs";
2
+ export { runArrangeSync, runOnTarget };
@@ -0,0 +1,97 @@
1
+ import { n as ArrangeErrorCode, t as ArrangeError } from "../../../errors-CpU1ogfA.mjs";
2
+ import { n as runOnTarget } from "../../../run-target-CJWrqNT4.mjs";
3
+ import { n as createNodeCliLogger, t as createNodeCliFs } from "../../../node-io-DFRbHO6m.mjs";
4
+ import "../../../arrange-BqP2DI3w.mjs";
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+ import os from "node:os";
8
+ //#region src/lib/arrange/application/run-target.test.ts
9
+ const arrangeFs = createNodeCliFs();
10
+ const arrangeLogger = createNodeCliLogger();
11
+ function captureStdout(fn) {
12
+ const chunks = [];
13
+ const spy = jest.spyOn(process.stdout, "write").mockImplementation((chunk) => {
14
+ chunks.push(typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8"));
15
+ return true;
16
+ });
17
+ try {
18
+ fn();
19
+ } finally {
20
+ spy.mockRestore();
21
+ }
22
+ return chunks.join("");
23
+ }
24
+ describe("runOnTarget", () => {
25
+ it("prints dry-run totals for directory targets", () => {
26
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "arr-run-dry-"));
27
+ const long = "peer flex size-4 shrink-0 items-center justify-center rounded-sm border border-input text-primary-foreground shadow-xs outline-hidden transition hover:not-disabled:not-aria-checked:border-ring/60 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:opacity-50";
28
+ try {
29
+ fs.writeFileSync(path.join(dir, "Page.tsx"), `import { cn } from "@codefast/tailwind-variants"; export function P(){ cn("${long}"); return null; }`, "utf8");
30
+ const out = captureStdout(() => runOnTarget(dir, {
31
+ write: false,
32
+ withClassName: false
33
+ }, arrangeFs, arrangeLogger));
34
+ expect(out).toContain("Total:");
35
+ expect(out).toContain("to review");
36
+ } finally {
37
+ fs.rmSync(dir, {
38
+ recursive: true,
39
+ force: true
40
+ });
41
+ }
42
+ });
43
+ it("throws ArrangeError for missing target path", () => {
44
+ let caught;
45
+ try {
46
+ runOnTarget(path.join(os.tmpdir(), "no-such-arrange-target"), {
47
+ write: false,
48
+ withClassName: false
49
+ }, arrangeFs, arrangeLogger);
50
+ } catch (caughtError) {
51
+ caught = caughtError;
52
+ }
53
+ expect(caught).toBeInstanceOf(ArrangeError);
54
+ expect(caught.code).toBe(ArrangeErrorCode.TARGET_NOT_FOUND);
55
+ });
56
+ it("prints apply summary in write mode", () => {
57
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "arr-run-apply-"));
58
+ const before = `import { cn } from "@codefast/tailwind-variants";
59
+ export function Fixture() {
60
+ return <div className="flex items-center gap-2 px-4 py-2 text-sm rounded-md border bg-card" />;
61
+ }`;
62
+ try {
63
+ fs.writeFileSync(path.join(dir, "Apply.tsx"), before, "utf8");
64
+ const out = captureStdout(() => runOnTarget(dir, {
65
+ write: true,
66
+ withClassName: false
67
+ }, arrangeFs, arrangeLogger));
68
+ expect(out).toContain("Applied:");
69
+ expect(out).toContain("Note:");
70
+ } finally {
71
+ fs.rmSync(dir, {
72
+ recursive: true,
73
+ force: true
74
+ });
75
+ }
76
+ });
77
+ it("accepts legacy tailwind-variants imports when running directory dry-run (backward compat)", () => {
78
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "arr-run-dry-legacy-"));
79
+ const long = "peer flex size-4 shrink-0 items-center justify-center rounded-sm border border-input text-primary-foreground shadow-xs outline-hidden transition hover:not-disabled:not-aria-checked:border-ring/60 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:opacity-50";
80
+ try {
81
+ fs.writeFileSync(path.join(dir, "Page.tsx"), `import { cn } from "tailwind-variants"; export function P(){ cn("${long}"); return null; }`, "utf8");
82
+ const out = captureStdout(() => runOnTarget(dir, {
83
+ write: false,
84
+ withClassName: false
85
+ }, arrangeFs, arrangeLogger));
86
+ expect(out).toContain("Total:");
87
+ expect(out).toContain("to review");
88
+ } finally {
89
+ fs.rmSync(dir, {
90
+ recursive: true,
91
+ force: true
92
+ });
93
+ }
94
+ });
95
+ });
96
+ //#endregion
97
+ export {};
@@ -0,0 +1,2 @@
1
+ import { a as isCnOrTvIdentifier, c as propertyAssignmentNameText, i as indentOfLineContaining, l as unwrapCnInsideTvCallReplacement, n as applyEditsDescending, o as lineOf, r as buildKnownCnTvBindings, s as moduleLooksLikeCnTvReexport, t as KNOWN_CN_TV_MODULES } from "../../../../ast-helpers-MDRYuIAk.mjs";
2
+ export { KNOWN_CN_TV_MODULES, applyEditsDescending, buildKnownCnTvBindings, indentOfLineContaining, isCnOrTvIdentifier, lineOf, moduleLooksLikeCnTvReexport, propertyAssignmentNameText, unwrapCnInsideTvCallReplacement };