@codefast/cli 0.3.5 → 0.3.12

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 (118) hide show
  1. package/README.md +66 -91
  2. package/dist/bin.d.ts +3 -0
  3. package/dist/bin.d.ts.map +1 -0
  4. package/dist/bin.js +5 -0
  5. package/dist/commands/arrange.d.ts +3 -0
  6. package/dist/commands/arrange.d.ts.map +1 -0
  7. package/dist/commands/arrange.js +114 -0
  8. package/dist/commands/mirror.d.ts +4 -0
  9. package/dist/commands/mirror.d.ts.map +1 -0
  10. package/dist/commands/mirror.js +63 -0
  11. package/dist/lib/arrange/analyze.d.ts +4 -0
  12. package/dist/lib/arrange/analyze.d.ts.map +1 -0
  13. package/dist/lib/arrange/analyze.js +105 -0
  14. package/dist/lib/arrange/ast/collectors-cn.d.ts +10 -0
  15. package/dist/lib/arrange/ast/collectors-cn.d.ts.map +1 -0
  16. package/dist/lib/arrange/ast/collectors-cn.js +84 -0
  17. package/dist/lib/arrange/ast/collectors-jsx.d.ts +4 -0
  18. package/dist/lib/arrange/ast/collectors-jsx.d.ts.map +1 -0
  19. package/dist/lib/arrange/ast/collectors-jsx.js +18 -0
  20. package/dist/lib/arrange/ast/collectors-tv.d.ts +13 -0
  21. package/dist/lib/arrange/ast/collectors-tv.d.ts.map +1 -0
  22. package/dist/lib/arrange/ast/collectors-tv.js +273 -0
  23. package/dist/lib/arrange/ast/targets.d.ts +8 -0
  24. package/dist/lib/arrange/ast/targets.d.ts.map +1 -0
  25. package/dist/lib/arrange/ast/targets.js +143 -0
  26. package/dist/lib/arrange/ast/utils.d.ts +36 -0
  27. package/dist/lib/arrange/ast/utils.d.ts.map +1 -0
  28. package/dist/lib/arrange/ast/utils.js +138 -0
  29. package/dist/lib/arrange/constants.d.ts +68 -0
  30. package/dist/lib/arrange/constants.d.ts.map +1 -0
  31. package/dist/lib/arrange/constants.js +179 -0
  32. package/dist/lib/arrange/errors.d.ts +14 -0
  33. package/dist/lib/arrange/errors.d.ts.map +1 -0
  34. package/dist/lib/arrange/errors.js +16 -0
  35. package/dist/lib/arrange/formatters.d.ts +16 -0
  36. package/dist/lib/arrange/formatters.d.ts.map +1 -0
  37. package/dist/lib/arrange/formatters.js +57 -0
  38. package/dist/lib/arrange/group-file.d.ts +4 -0
  39. package/dist/lib/arrange/group-file.d.ts.map +1 -0
  40. package/dist/lib/arrange/group-file.js +115 -0
  41. package/dist/lib/arrange/grouping.d.ts +41 -0
  42. package/dist/lib/arrange/grouping.d.ts.map +1 -0
  43. package/dist/lib/arrange/grouping.js +280 -0
  44. package/dist/lib/arrange/imports.d.ts +6 -0
  45. package/dist/lib/arrange/imports.d.ts.map +1 -0
  46. package/dist/lib/arrange/imports.js +74 -0
  47. package/dist/lib/arrange/report.d.ts +4 -0
  48. package/dist/lib/arrange/report.d.ts.map +1 -0
  49. package/dist/lib/arrange/report.js +37 -0
  50. package/dist/lib/arrange/run-target.d.ts +4 -0
  51. package/dist/lib/arrange/run-target.d.ts.map +1 -0
  52. package/dist/lib/arrange/run-target.js +28 -0
  53. package/dist/lib/arrange/tokenizer.d.ts +37 -0
  54. package/dist/lib/arrange/tokenizer.d.ts.map +1 -0
  55. package/dist/lib/arrange/tokenizer.js +390 -0
  56. package/dist/lib/arrange/types.d.ts +104 -0
  57. package/dist/lib/arrange/types.d.ts.map +1 -0
  58. package/dist/lib/arrange/types.js +7 -0
  59. package/dist/lib/arrange/walk.d.ts +4 -0
  60. package/dist/lib/arrange/walk.d.ts.map +1 -0
  61. package/dist/lib/arrange/walk.js +34 -0
  62. package/dist/lib/arrange.d.ts +41 -0
  63. package/dist/lib/arrange.d.ts.map +1 -0
  64. package/dist/lib/arrange.js +38 -0
  65. package/dist/lib/infra/fs-contract.d.ts +28 -0
  66. package/dist/lib/infra/fs-contract.d.ts.map +1 -0
  67. package/dist/lib/infra/fs-contract.js +1 -0
  68. package/dist/lib/infra/node-io.d.ts +4 -0
  69. package/dist/lib/infra/node-io.d.ts.map +1 -0
  70. package/dist/lib/infra/node-io.js +27 -0
  71. package/dist/lib/mirror/config.d.ts +7 -0
  72. package/dist/lib/mirror/config.d.ts.map +1 -0
  73. package/dist/lib/mirror/config.js +146 -0
  74. package/dist/lib/mirror/constants.d.ts +10 -0
  75. package/dist/lib/mirror/constants.d.ts.map +1 -0
  76. package/dist/lib/mirror/constants.js +13 -0
  77. package/dist/lib/mirror/engine.d.ts +12 -0
  78. package/dist/lib/mirror/engine.d.ts.map +1 -0
  79. package/dist/lib/mirror/engine.js +248 -0
  80. package/dist/lib/mirror/errors.d.ts +10 -0
  81. package/dist/lib/mirror/errors.d.ts.map +1 -0
  82. package/dist/lib/mirror/errors.js +12 -0
  83. package/dist/lib/mirror/package-filter.d.ts +8 -0
  84. package/dist/lib/mirror/package-filter.d.ts.map +1 -0
  85. package/dist/lib/mirror/package-filter.js +33 -0
  86. package/dist/lib/mirror/reporter.d.ts +24 -0
  87. package/dist/lib/mirror/reporter.d.ts.map +1 -0
  88. package/dist/lib/mirror/reporter.js +126 -0
  89. package/dist/lib/mirror/sync.d.ts +4 -0
  90. package/dist/lib/mirror/sync.d.ts.map +1 -0
  91. package/dist/lib/mirror/sync.js +135 -0
  92. package/dist/lib/mirror/types.d.ts +81 -0
  93. package/dist/lib/mirror/types.d.ts.map +1 -0
  94. package/dist/lib/mirror/types.js +1 -0
  95. package/dist/lib/mirror/update-pkg.d.ts +13 -0
  96. package/dist/lib/mirror/update-pkg.d.ts.map +1 -0
  97. package/dist/lib/mirror/update-pkg.js +39 -0
  98. package/dist/lib/mirror/workspace-packages.d.ts +26 -0
  99. package/dist/lib/mirror/workspace-packages.d.ts.map +1 -0
  100. package/dist/lib/mirror/workspace-packages.js +160 -0
  101. package/dist/lib/mirror.d.ts +7 -0
  102. package/dist/lib/mirror.d.ts.map +1 -0
  103. package/dist/lib/mirror.js +5 -0
  104. package/dist/lib/repo-root.d.ts +7 -0
  105. package/dist/lib/repo-root.d.ts.map +1 -0
  106. package/dist/lib/repo-root.js +23 -0
  107. package/dist/lib/shared/utils.d.ts +9 -0
  108. package/dist/lib/shared/utils.d.ts.map +1 -0
  109. package/dist/lib/shared/utils.js +12 -0
  110. package/dist/program.d.ts +4 -0
  111. package/dist/program.d.ts.map +1 -0
  112. package/dist/program.js +38 -0
  113. package/package.json +53 -34
  114. package/CHANGELOG.md +0 -333
  115. package/dist/index.cjs +0 -1440
  116. package/dist/index.cjs.map +0 -1
  117. package/dist/index.js +0 -1406
  118. package/dist/index.js.map +0 -1
package/README.md CHANGED
@@ -1,125 +1,100 @@
1
- # Command `update-exports`
1
+ # @codefast/cli
2
2
 
3
- ## Tổng Quan
3
+ Two tools bundled in one CLI:
4
4
 
5
- Command `update-exports` là một công cụ tự động hóa việc cập nhật cấu hình `exports` trong file `package.json` của các package trong dự án. Nó phân tích mã nguồn để xác định các module được export và tạo cấu trúc exports phù hợp.
5
+ | Command | Purpose |
6
+ | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
7
+ | `mirror sync` | Regenerate `package.json` `exports` from each package's built `dist/` tree (pnpm workspace packages) |
8
+ | `arrange` | Analyze, dry-run, or apply suggested grouping for long Tailwind class strings inside `cn()` / `tv()` call sites (Tailwind v4–oriented heuristics), or `group` a pasted class string |
6
9
 
7
- ## Cách Sử Dụng
10
+ ---
8
11
 
9
- ```shell script
10
- codefast update-exports [options]
11
- ```
12
+ ## Requirements & Install
13
+
14
+ **Node.js ≥ 24** is required.
12
15
 
13
- ### Tùy Chọn
14
-
15
- | Tùy chọn | Mô tả |
16
- | ------------------------- | ------------------------------------------------ |
17
- | `-p, --package <package>` | Lọc theo tên package cụ thể |
18
- | `-d, --dry-run` | Chạy mà không thực hiện thay đổi (chỉ xem trước) |
19
- | `-v, --verbose` | Hiển thị đầu ra chi tiết |
20
- | `-c, --config <path>` | Đường dẫn đến file cấu hình tùy chỉnh |
21
-
22
- ## Luồng Hoạt Động
23
-
24
- ```mermaid
25
- flowchart TD
26
- A[Bắt đầu update-exports] --> B[processAllPackages]
27
- B --> C[Tìm kiếm packages với findAllPackages]
28
- C --> D[Lấy cấu hình với getConfig]
29
- D --> E[Tìm tất cả package.json với glob]
30
- E --> F{Đã tìm thấy packages?}
31
- F -- Không --> G[Hiển thị cảnh báo]
32
- G --> Z[Kết thúc]
33
- F -- Có --> H[Xử lý từng package]
34
- H --> I[processPackage cho từng package]
35
- I --> J[Đọc package.json]
36
- J --> K{Match với package filter?}
37
- K -- Không --> L[Bỏ qua package]
38
- K -- Có --> M[Phân tích imports với analyzeImports]
39
- M --> N[Lưu kết quả phân tích]
40
- N --> O[Tạo exports mới với generateExports]
41
- O --> P{Dry run mode?}
42
- P -- Có --> Q[Tạo preview exports]
43
- Q --> Z
44
- P -- Không --> R[Sao lưu package.json]
45
- R --> S[Cập nhật package.json với exports mới]
46
- S --> Z
16
+ ```bash
17
+ # Global
18
+ pnpm add -g @codefast/cli
19
+
20
+ # Without a global install
21
+ pnpm dlx @codefast/cli -- --help
47
22
  ```
48
23
 
49
- 1. **Khởi tạo và Tìm Kiếm Package**
24
+ ---
50
25
 
51
- - Command đọc các tùy chọn từ người dùng
52
- - Tìm kiếm tất cả các file `package.json` trong workspace theo mẫu glob được cấu hình
26
+ ## `arrange`
53
27
 
54
- 2. **Xử Lý Từng Package**
28
+ ### Recommended workflow
55
29
 
56
- - Kiểm tra xem package có khớp với filter hay không (nếu có)
57
- - Đọc cấu hình cụ thể cho package
58
- - Xác định đường dẫn đến file index
30
+ 1. **`analyze [target]`** — Prints a report (long strings, nested `cn` in `tv`, related notes). No files changed.
31
+ 2. **`preview [target]`** — Same transforms as `apply`, but writes nothing. Inspect stdout before touching the tree.
32
+ 3. **`apply [target]`** — Writes edits. Run `preview` first.
59
33
 
60
- 3. **Phân Tích Imports**
34
+ **Default target** (when path is omitted): `packages/ui/src/components` resolved from `process.cwd()`.
61
35
 
62
- - Sử dụng ts-morph để phân tích cú pháp của file index
63
- - Tìm tất cả các khai báo export
64
- - Chuẩn hóa đường dẫn (ví dụ: @/ thành ./)
65
- - Bỏ qua các import từ modules bên ngoài
66
- - Xử lý đường dẫn export theo cấu hình
36
+ ### Useful options
67
37
 
68
- 4. **Tạo Exports Mới**
38
+ - **`--with-class-name`** — Append `className` as the last argument to the suggested `cn(...)`.
39
+ - **`--cn-import <spec>`** — Override the module specifier when the tool adds a `cn` import.
69
40
 
70
- - Dựa trên imports đã phân tích, tạo cấu trúc exports mới
71
- - So sánh với exports hiện tại
41
+ ### `group [tokens...]`
72
42
 
73
- 5. **Lưu Thay Đổi**
43
+ No filesystem involved — paste a class string, get back a suggested `cn(...)` (or a `tv()`-style array with `--tv`) plus a short buckets summary. Use this to tune your mental model before running `analyze` on a large tree.
74
44
 
75
- - Nếu ở chế độ dry-run:
76
- - Tạo preview của exports mới mà không thực hiện thay đổi
77
- - Nếu không ở chế độ dry-run:
78
- - Tạo bản sao lưu của package.json
79
- - Cập nhật package.json với exports mới
45
+ ---
80
46
 
81
- 6. **Báo Cáo Kết Quả**
47
+ ## Grouping philosophy — Render Pipeline Order
82
48
 
83
- - Hiển thị thống kê về số lượng package đã xử lý thành công, bỏ qua, thất bại
49
+ `arrange` does **not** sort alphabetically. It groups utilities in roughly the same order the browser reasons about them:
84
50
 
85
- ## Cấu Hình
51
+ **Existence → Position → Layout → Sizing → Spacing → Shape → Background → Shadow → Typography → Composite → Motion → Starting → Behavior → Conditions (State)**
86
52
 
87
- Command `update-exports` sử dụng file cấu hình để xác định:
53
+ Bucket breakdown:
88
54
 
89
- - Mẫu glob để tìm packages
90
- - Các prefix đường dẫn cần loại bỏ
91
- - Cấu hình cụ thể cho từng package
55
+ | Bucket | What it covers | Examples |
56
+ | -------------- | ------------------------------------------------- | ------------------------------------------------- |
57
+ | **Existence** | Display / containment context | `hidden`, `block`, `@container`, `group`, `peer` |
58
+ | **Position** | Where the box sits | `absolute`, `inset-*`, `top-*`, `z-*` |
59
+ | **Layout** | How children flow | `flex`, `grid`, `gap-*`, `items-*` |
60
+ | **Sizing** | Box dimensions and overflow | `w-*`, `h-*`, `aspect-*`, `overflow-*` |
61
+ | **Spacing** | Padding and margin only (gaps stay with Layout) | `p-*`, `m-*` |
62
+ | **Shape** | Corners and strokes | `rounded-*`, `border-*`, `ring-*` |
63
+ | **Background** | Surfaces and masks | `bg-*`, `from-*`, `via-*`, `to-*`, `mask-*` |
64
+ | **Shadow** | Depth | `shadow-*`, `inset-shadow-*`, `text-shadow-*` |
65
+ | **Typography** | Text appearance | `font-*`, `text-*`, `leading-*` |
66
+ | **Composite** | Layers and transforms — 3D context → 3D → 2D | `opacity-*`, `rotate-x-*`, `translate-*` |
67
+ | **Motion** | Time-based change | `transition-*`, `animate-*` |
68
+ | **Starting** | Tailwind's `starting:` layer, kept next to Motion | `starting:*` |
69
+ | **Behavior** | Input / scrolling / chrome | `cursor-*`, `scroll-*`, `field-sizing-*`, `inert` |
70
+ | **State** | Everything with a variant stack | `hover:`, `md:`, `@md/sidebar:`, `data-[…]:` |
92
71
 
93
- Bạn có thể chỉ định file cấu hình tùy chỉnh bằng tùy chọn `-c, --config`.
72
+ Some adjacent buckets may be merged into one string literal when declared _compatible_ (e.g. `layout` + `sizing`) — keeps `cn()` readable without flattening unrelated concerns.
94
73
 
95
- ## Ví Dụ
74
+ To change a placement, edit `classifyBareUtility` in `src/lib/arrange/tokenizer.ts` and add a `classifyToken` test in `src/lib/arrange.test.ts`.
96
75
 
97
- Cập nhật exports cho tất cả các package:
76
+ ---
98
77
 
99
- ```shell script
100
- codefast update-exports
101
- ```
78
+ ## `mirror sync`
102
79
 
103
- Chạy ở chế độ verbose để xem thông tin chi tiết:
80
+ Run from anywhere under the monorepo — the CLI finds the root via `pnpm-workspace.yaml`.
104
81
 
105
- ```shell script
106
- codefast update-exports --verbose
82
+ ```bash
83
+ codefast mirror sync # All workspace packages
84
+ codefast mirror sync packages/ui # One package only
85
+ codefast mirror sync -v # Verbose
107
86
  ```
108
87
 
109
- Chỉ xử lý một package cụ thể:
88
+ **Config:** Place `codefast.config.js` (or `.mjs` / `.cjs` / `.json`) at repo root with a `mirror` object (`skipPackages`, `pathTransformations`, `customExports`, …).
110
89
 
111
- ```shell script
112
- codefast update-exports --package @codefast/ui
113
- ```
90
+ > ⚠️ `.js`/`.mjs`/`.cjs` config files are loaded via `import()` — only run `mirror sync` in repositories you trust.
114
91
 
115
- Xem trước thay đổi mà không thực sự cập nhật:
92
+ ---
116
93
 
117
- ```shell script
118
- codefast update-exports --dry-run
119
- ```
94
+ ## Developing inside this monorepo
120
95
 
121
- ## Lưu Ý
96
+ ```bash
97
+ pnpm exec codefast --help
98
+ ```
122
99
 
123
- - Command sẽ tạo bản sao lưu của các file package.json trước khi thực hiện thay đổi
124
- - Các exports được tạo dựa trên việc phân tích mã nguồn, đảm bảo tính nhất quán
125
- - Sử dụng tùy chọn `--dry-run` để kiểm tra các thay đổi trước khi áp dụng
100
+ Root `package.json` defines optional `cli:*` scripts (e.g. `cli:mirror-sync`, `cli:arrange-analyze`) as thin wrappers around `pnpm exec codefast …`.
package/dist/bin.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=bin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bin.d.ts","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":""}
package/dist/bin.js ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import process from "node:process";
3
+ import { runCli } from "#program";
4
+ const code = await runCli(process.argv);
5
+ process.exit(code);
@@ -0,0 +1,3 @@
1
+ import type { Command } from "commander";
2
+ export declare function registerArrangeCommand(program: Command): void;
3
+ //# sourceMappingURL=arrange.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"arrange.d.ts","sourceRoot":"","sources":["../../src/commands/arrange.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA4EzC,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAsF7D"}
@@ -0,0 +1,114 @@
1
+ import path from "node:path";
2
+ import process from "node:process";
3
+ import { Option } from "commander";
4
+ import { ArrangeError, ArrangeErrorCode, analyzeDirectory, createNodeCliFs, createNodeCliLogger, DEFAULT_ARRANGE_TARGET, formatArray, formatCnCall, printAnalyzeReport, runOnTarget, suggestCnGroups, summarizeGroupBucketLabels, } from "#lib/arrange";
5
+ /** Commander attribute `withClassName` (second long flag `--with-class-name`). */
6
+ function createWithClassNameOption() {
7
+ return new Option("--with-classname, --with-class-name", "Append className as final cn() argument").default(false);
8
+ }
9
+ function defaultTargetPath() {
10
+ return path.resolve(process.cwd(), DEFAULT_ARRANGE_TARGET);
11
+ }
12
+ function checkTargetExists(resolved, fs, logger) {
13
+ if (!fs.existsSync(resolved)) {
14
+ logger.err(`Not found: ${resolved}`);
15
+ process.exitCode = 1;
16
+ return false;
17
+ }
18
+ return true;
19
+ }
20
+ function handleArrangeLibError(e, logger) {
21
+ if (e instanceof ArrangeError && e.code === ArrangeErrorCode.TARGET_NOT_FOUND) {
22
+ logger.err(e.message);
23
+ process.exitCode = 1;
24
+ return true;
25
+ }
26
+ return false;
27
+ }
28
+ function runArrangeAction(resolvedTarget, runOpts, fs, logger) {
29
+ try {
30
+ runOnTarget(resolvedTarget, {
31
+ write: runOpts.write,
32
+ withClassName: !!runOpts.withClassName,
33
+ cnImport: runOpts.cnImport,
34
+ }, fs, logger);
35
+ }
36
+ catch (e) {
37
+ if (!handleArrangeLibError(e, logger))
38
+ throw e;
39
+ }
40
+ }
41
+ export function registerArrangeCommand(program) {
42
+ const arrange = program
43
+ .command("arrange")
44
+ .description("Analyze and regroup Tailwind classes in cn() / tv() calls (Tailwind v4)");
45
+ arrange
46
+ .command("analyze")
47
+ .description("Report long strings, nested cn in tv(), and related findings")
48
+ .argument("[target]", "Directory or file (default: packages/ui/src/components)")
49
+ .action((target) => {
50
+ const fs = createNodeCliFs();
51
+ const logger = createNodeCliLogger();
52
+ const resolved = target ? path.resolve(target) : defaultTargetPath();
53
+ if (!checkTargetExists(resolved, fs, logger))
54
+ return;
55
+ printAnalyzeReport(resolved, analyzeDirectory(resolved, fs), logger);
56
+ });
57
+ arrange
58
+ .command("preview")
59
+ .description("Dry-run: print suggested replacements without writing files")
60
+ .argument("[target]", "Directory or file (default: packages/ui/src/components)")
61
+ .addOption(createWithClassNameOption())
62
+ .option("--cn-import <spec>", "Override module specifier when adding cn import")
63
+ .action((target, opts) => {
64
+ const fs = createNodeCliFs();
65
+ const logger = createNodeCliLogger();
66
+ const resolved = target ? path.resolve(target) : defaultTargetPath();
67
+ if (!checkTargetExists(resolved, fs, logger))
68
+ return;
69
+ runArrangeAction(resolved, {
70
+ write: false,
71
+ withClassName: opts.withClassName,
72
+ cnImport: opts.cnImport,
73
+ }, fs, logger);
74
+ });
75
+ arrange
76
+ .command("apply")
77
+ .description("Apply grouping and cn-in-tv unwrap edits to files")
78
+ .argument("[target]", "Directory or file (default: packages/ui/src/components)")
79
+ .addOption(createWithClassNameOption())
80
+ .option("--cn-import <spec>", "Override module specifier when adding cn import")
81
+ .action((target, opts) => {
82
+ const fs = createNodeCliFs();
83
+ const logger = createNodeCliLogger();
84
+ const resolved = target ? path.resolve(target) : defaultTargetPath();
85
+ if (!checkTargetExists(resolved, fs, logger))
86
+ return;
87
+ runArrangeAction(resolved, {
88
+ write: true,
89
+ withClassName: opts.withClassName,
90
+ cnImport: opts.cnImport,
91
+ }, fs, logger);
92
+ });
93
+ arrange
94
+ .command("group")
95
+ .description("Try grouping on a pasted class string (stdout: cn(...) or tv array with --tv)")
96
+ .argument("[tokens...]", "Class tokens (quote a single string if it contains spaces)")
97
+ .option("--tv", "Emit tv()-style array instead of cn() call", false)
98
+ .addOption(createWithClassNameOption())
99
+ .action((tokens, opts) => {
100
+ const inlineClasses = tokens.join(" ").trim();
101
+ if (!inlineClasses) {
102
+ process.stderr.write('Pass a class string. Example: codefast arrange group "flex gap-2 text-sm rounded-md"\n');
103
+ process.exitCode = 1;
104
+ return;
105
+ }
106
+ const groups = suggestCnGroups(inlineClasses);
107
+ const result = opts.tv
108
+ ? formatArray(groups)
109
+ : formatCnCall(groups, { trailingClassName: !!opts.withClassName });
110
+ process.stdout.write(`${result}\n`);
111
+ const bucketSummary = summarizeGroupBucketLabels(groups);
112
+ process.stdout.write(`\n// Buckets: ${JSON.stringify(bucketSummary)}\n`);
113
+ });
114
+ }
@@ -0,0 +1,4 @@
1
+ import { Command } from "commander";
2
+ export declare function packageArgToRelative(rootDir: string, arg: string | undefined): string | undefined;
3
+ export declare function registerMirrorCommand(program: Command): void;
4
+ //# sourceMappingURL=mirror.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mirror.d.ts","sourceRoot":"","sources":["../../src/commands/mirror.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAapC,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAiBjG;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAmC5D"}
@@ -0,0 +1,63 @@
1
+ import { realpathSync } from "node:fs";
2
+ import path from "node:path";
3
+ import process from "node:process";
4
+ import { createNodeCliFs } from "#lib/infra/node-io";
5
+ import { normalizePath, runMirrorSync } from "#lib/mirror";
6
+ import { findRepoRoot } from "#lib/repo-root";
7
+ function tryRealpath(entryPath) {
8
+ try {
9
+ return realpathSync.native(entryPath);
10
+ }
11
+ catch {
12
+ return path.resolve(entryPath);
13
+ }
14
+ }
15
+ export function packageArgToRelative(rootDir, arg) {
16
+ if (!arg)
17
+ return undefined;
18
+ const rootReal = tryRealpath(path.resolve(rootDir));
19
+ const cwdReal = tryRealpath(process.cwd());
20
+ const resolved = path.isAbsolute(arg) ? path.resolve(arg) : path.resolve(cwdReal, arg);
21
+ const targetReal = tryRealpath(resolved);
22
+ const rel = path.relative(rootReal, targetReal);
23
+ const normalized = normalizePath(rel);
24
+ if (normalized.startsWith("..") ||
25
+ path.isAbsolute(normalized) ||
26
+ normalized === "" ||
27
+ normalized === ".") {
28
+ throw new Error(`Package path must be a subdirectory under monorepo root: ${rootDir}`);
29
+ }
30
+ return normalized;
31
+ }
32
+ export function registerMirrorCommand(program) {
33
+ const mirror = program
34
+ .command("mirror")
35
+ .description("Keep package manifests aligned with what you ship");
36
+ mirror
37
+ .command("sync")
38
+ .description("Write package.json exports from dist/ for workspace packages")
39
+ .argument("[package]", "Optional package path relative to repo root (e.g. packages/ui)")
40
+ .option("-v, --verbose", "Print extra diagnostics", false)
41
+ .action(async function (pkg, options) {
42
+ const globals = this.optsWithGlobals();
43
+ const fs = createNodeCliFs();
44
+ const rootDir = findRepoRoot(fs);
45
+ let packageFilter;
46
+ try {
47
+ packageFilter = packageArgToRelative(rootDir, pkg);
48
+ }
49
+ catch (e) {
50
+ this.error(e instanceof Error ? e.message : String(e));
51
+ return;
52
+ }
53
+ const exitCode = await runMirrorSync({
54
+ rootDir,
55
+ verbose: options.verbose,
56
+ /** Commander sets `color: false` when `--no-color` is passed (default `color: true`). */
57
+ noColor: globals.color === false,
58
+ packageFilter,
59
+ fs,
60
+ });
61
+ process.exitCode = exitCode;
62
+ });
63
+ }
@@ -0,0 +1,4 @@
1
+ import type { CliFs } from "#lib/infra/fs-contract";
2
+ import type { AnalyzeReport } from "#lib/arrange/types";
3
+ export declare function analyzeDirectory(target: string, fs: CliFs): AnalyzeReport;
4
+ //# sourceMappingURL=analyze.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analyze.d.ts","sourceRoot":"","sources":["../../../src/lib/arrange/analyze.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,wBAAwB,CAAC;AAEpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AA8FxD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,KAAK,GAAG,aAAa,CAsCzE"}
@@ -0,0 +1,105 @@
1
+ import ts from "typescript";
2
+ import { LONG_STRING_TOKEN_THRESHOLD } from "#lib/arrange/constants";
3
+ import { tokenizeClassString } from "#lib/arrange/tokenizer";
4
+ import { forEachStringLiteralInClassExpression } from "#lib/arrange/ast/collectors-cn";
5
+ import { jsxClassNameStaticLiteral } from "#lib/arrange/ast/collectors-jsx";
6
+ import { collectCnCallsInsideTv, traverseTvObject } from "#lib/arrange/ast/collectors-tv";
7
+ import { buildKnownCnTvBindings, isCnOrTvIdentifier, lineOf } from "#lib/arrange/ast/utils";
8
+ import { walkTsxFiles } from "#lib/arrange/walk";
9
+ function analyzeCnCall(sf, call, report) {
10
+ for (const arg of call.arguments) {
11
+ forEachStringLiteralInClassExpression(arg, (lit) => {
12
+ const text = lit.text;
13
+ const tokenCount = tokenizeClassString(text).length;
14
+ if (tokenCount >= LONG_STRING_TOKEN_THRESHOLD) {
15
+ report.longCnStringLiterals.push({
16
+ file: sf.fileName,
17
+ line: lineOf(sf, lit),
18
+ tokenCount,
19
+ preview: text.length > 72 ? `${text.slice(0, 72)}…` : text,
20
+ });
21
+ }
22
+ });
23
+ }
24
+ }
25
+ function visitCallExpressionForArrangeAnalyze(node, sf, sourceText, knownBindings, report) {
26
+ if (isCnOrTvIdentifier(node.expression, "cn", knownBindings)) {
27
+ report.cnCallExpressions++;
28
+ analyzeCnCall(sf, node, report);
29
+ return;
30
+ }
31
+ if (!isCnOrTvIdentifier(node.expression, "tv", knownBindings))
32
+ return;
33
+ report.tvCallExpressions++;
34
+ const arg0 = node.arguments[0];
35
+ if (!arg0 || !ts.isObjectLiteralExpression(arg0))
36
+ return;
37
+ for (const nestedCn of collectCnCallsInsideTv(sf, arg0, knownBindings, 0)) {
38
+ const src = sourceText.slice(nestedCn.getStart(sf), nestedCn.getEnd());
39
+ const preview = src.length > 72 ? `${src.slice(0, 72)}…` : src;
40
+ report.cnInsideTvCalls.push({
41
+ file: sf.fileName,
42
+ line: lineOf(sf, nestedCn),
43
+ argCount: nestedCn.arguments.length,
44
+ preview,
45
+ });
46
+ }
47
+ traverseTvObject(sf, arg0, (strNode) => {
48
+ const text = strNode.text;
49
+ const tokenCount = tokenizeClassString(text).length;
50
+ if (tokenCount >= LONG_STRING_TOKEN_THRESHOLD) {
51
+ report.longTvStringLiterals.push({
52
+ file: sf.fileName,
53
+ line: lineOf(sf, strNode),
54
+ tokenCount,
55
+ preview: text.length > 72 ? `${text.slice(0, 72)}…` : text,
56
+ });
57
+ }
58
+ }, 0, knownBindings);
59
+ }
60
+ function visitJsxAttributeForArrangeAnalyze(node, sf, filePath, report) {
61
+ if (!filePath.endsWith(".tsx"))
62
+ return;
63
+ const parsed = jsxClassNameStaticLiteral(node);
64
+ if (!parsed)
65
+ return;
66
+ const text = parsed.lit.text;
67
+ const tokenCount = tokenizeClassString(text).length;
68
+ if (tokenCount >= LONG_STRING_TOKEN_THRESHOLD) {
69
+ report.longJsxClassNameLiterals.push({
70
+ file: sf.fileName,
71
+ line: lineOf(sf, parsed.lit),
72
+ tokenCount,
73
+ preview: text.length > 72 ? `${text.slice(0, 72)}…` : text,
74
+ });
75
+ }
76
+ }
77
+ export function analyzeDirectory(target, fs) {
78
+ const report = {
79
+ files: 0,
80
+ cnCallExpressions: 0,
81
+ tvCallExpressions: 0,
82
+ cnInsideTvCalls: [],
83
+ longCnStringLiterals: [],
84
+ longTvStringLiterals: [],
85
+ longJsxClassNameLiterals: [],
86
+ };
87
+ const files = fs.statSync(target).isDirectory() ? walkTsxFiles(target, fs) : [target];
88
+ for (const filePath of files) {
89
+ const sourceText = fs.readFileSync(filePath, "utf8");
90
+ const sf = ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true, filePath.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
91
+ report.files++;
92
+ const knownBindings = buildKnownCnTvBindings(sf);
93
+ const visit = (node) => {
94
+ if (ts.isCallExpression(node)) {
95
+ visitCallExpressionForArrangeAnalyze(node, sf, sourceText, knownBindings, report);
96
+ }
97
+ if (ts.isJsxAttribute(node)) {
98
+ visitJsxAttributeForArrangeAnalyze(node, sf, filePath, report);
99
+ }
100
+ ts.forEachChild(node, visit);
101
+ };
102
+ visit(sf);
103
+ }
104
+ return report;
105
+ }
@@ -0,0 +1,10 @@
1
+ import ts from "typescript";
2
+ import type { ForEachStringLiteralInClassExpressionOptions, TailwindClassLiteral } from "#lib/arrange/types";
3
+ export declare function forEachStringLiteralInClassExpression(expr: ts.Expression, sink: (node: TailwindClassLiteral) => void, depth?: number, options?: ForEachStringLiteralInClassExpressionOptions): void;
4
+ export declare function isUnsafeLiteralForCnStyleApplySplit(node: TailwindClassLiteral): boolean;
5
+ export declare const CN_APPLY_LITERAL_WALK_OPTS: ForEachStringLiteralInClassExpressionOptions;
6
+ export declare function collectUnconditionalTailwindLiteralsFromCnArguments(args: ts.NodeArray<ts.Expression>): TailwindClassLiteral[];
7
+ export declare function mergeCnUnconditionalLiteralPoolForTest(argsSnippet: string, options?: {
8
+ callee?: string;
9
+ }): string;
10
+ //# sourceMappingURL=collectors-cn.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collectors-cn.d.ts","sourceRoot":"","sources":["../../../../src/lib/arrange/ast/collectors-cn.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,YAAY,CAAC;AAE5B,OAAO,KAAK,EACV,4CAA4C,EAC5C,oBAAoB,EACrB,MAAM,oBAAoB,CAAC;AAE5B,wBAAgB,qCAAqC,CACnD,IAAI,EAAE,EAAE,CAAC,UAAU,EACnB,IAAI,EAAE,CAAC,IAAI,EAAE,oBAAoB,KAAK,IAAI,EAC1C,KAAK,SAAI,EACT,OAAO,CAAC,EAAE,4CAA4C,GACrD,IAAI,CA4CN;AAED,wBAAgB,mCAAmC,CAAC,IAAI,EAAE,oBAAoB,GAAG,OAAO,CAEvF;AAED,eAAO,MAAM,0BAA0B,EAAE,4CAExC,CAAC;AAEF,wBAAgB,mDAAmD,CACjE,IAAI,EAAE,EAAE,CAAC,SAAS,CAAC,EAAE,CAAC,UAAU,CAAC,GAChC,oBAAoB,EAAE,CAqBxB;AAED,wBAAgB,sCAAsC,CACpD,WAAW,EAAE,MAAM,EACnB,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAC5B,MAAM,CAyBR"}
@@ -0,0 +1,84 @@
1
+ import ts from "typescript";
2
+ import { MAX_CLASS_EXPR_DEPTH } from "#lib/arrange/constants";
3
+ export function forEachStringLiteralInClassExpression(expr, sink, depth = 0, options) {
4
+ if (depth > MAX_CLASS_EXPR_DEPTH)
5
+ return;
6
+ if (ts.isStringLiteral(expr) || ts.isNoSubstitutionTemplateLiteral(expr)) {
7
+ sink(expr);
8
+ return;
9
+ }
10
+ if (ts.isParenthesizedExpression(expr)) {
11
+ forEachStringLiteralInClassExpression(expr.expression, sink, depth + 1, options);
12
+ return;
13
+ }
14
+ if (ts.isAsExpression(expr) || ts.isSatisfiesExpression(expr)) {
15
+ forEachStringLiteralInClassExpression(expr.expression, sink, depth + 1, options);
16
+ return;
17
+ }
18
+ if (ts.isNonNullExpression(expr)) {
19
+ forEachStringLiteralInClassExpression(expr.expression, sink, depth + 1, options);
20
+ return;
21
+ }
22
+ if (ts.isConditionalExpression(expr)) {
23
+ if (options?.descendIntoConditional === false) {
24
+ return;
25
+ }
26
+ forEachStringLiteralInClassExpression(expr.whenTrue, sink, depth + 1, options);
27
+ forEachStringLiteralInClassExpression(expr.whenFalse, sink, depth + 1, options);
28
+ return;
29
+ }
30
+ if (ts.isBinaryExpression(expr) && expr.operatorToken.kind === ts.SyntaxKind.PlusToken) {
31
+ forEachStringLiteralInClassExpression(expr.left, sink, depth + 1, options);
32
+ forEachStringLiteralInClassExpression(expr.right, sink, depth + 1, options);
33
+ return;
34
+ }
35
+ if (ts.isArrayLiteralExpression(expr)) {
36
+ for (const el of expr.elements) {
37
+ if (ts.isSpreadElement(el))
38
+ continue;
39
+ forEachStringLiteralInClassExpression(el, sink, depth + 1, options);
40
+ }
41
+ }
42
+ }
43
+ export function isUnsafeLiteralForCnStyleApplySplit(node) {
44
+ return ts.isArrayLiteralExpression(node.parent);
45
+ }
46
+ export const CN_APPLY_LITERAL_WALK_OPTS = {
47
+ descendIntoConditional: false,
48
+ };
49
+ export function collectUnconditionalTailwindLiteralsFromCnArguments(args) {
50
+ const staticLits = [];
51
+ for (const arg of args) {
52
+ if (ts.isStringLiteral(arg) || ts.isNoSubstitutionTemplateLiteral(arg)) {
53
+ if (!isUnsafeLiteralForCnStyleApplySplit(arg)) {
54
+ staticLits.push(arg);
55
+ }
56
+ }
57
+ else {
58
+ forEachStringLiteralInClassExpression(arg, (lit) => {
59
+ if (!isUnsafeLiteralForCnStyleApplySplit(lit)) {
60
+ staticLits.push(lit);
61
+ }
62
+ }, 0, CN_APPLY_LITERAL_WALK_OPTS);
63
+ }
64
+ }
65
+ return staticLits;
66
+ }
67
+ export function mergeCnUnconditionalLiteralPoolForTest(argsSnippet, options) {
68
+ const callee = options?.callee ?? "cn";
69
+ const sourceText = `${callee}(${argsSnippet});`;
70
+ const sf = ts.createSourceFile("mergeCnUnconditionalLiteralPoolForTest.ts", sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
71
+ const stmt = sf.statements[0];
72
+ if (!ts.isExpressionStatement(stmt)) {
73
+ throw new Error("expected expression statement");
74
+ }
75
+ const call = stmt.expression;
76
+ if (!ts.isCallExpression(call) ||
77
+ !ts.isIdentifier(call.expression) ||
78
+ call.expression.text !== callee) {
79
+ throw new Error(`expected ${callee}(...) call`);
80
+ }
81
+ return collectUnconditionalTailwindLiteralsFromCnArguments(call.arguments)
82
+ .map((n) => n.text)
83
+ .join(" ");
84
+ }
@@ -0,0 +1,4 @@
1
+ import ts from "typescript";
2
+ import type { JsxClassNameStatic } from "#lib/arrange/types";
3
+ export declare function jsxClassNameStaticLiteral(attr: ts.JsxAttribute): JsxClassNameStatic | undefined;
4
+ //# sourceMappingURL=collectors-jsx.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collectors-jsx.d.ts","sourceRoot":"","sources":["../../../../src/lib/arrange/ast/collectors-jsx.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,YAAY,CAAC;AAC5B,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAE7D,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,EAAE,CAAC,YAAY,GAAG,kBAAkB,GAAG,SAAS,CAe/F"}
@@ -0,0 +1,18 @@
1
+ import ts from "typescript";
2
+ export function jsxClassNameStaticLiteral(attr) {
3
+ if (!ts.isIdentifier(attr.name) || attr.name.text !== "className")
4
+ return undefined;
5
+ const init = attr.initializer;
6
+ if (!init)
7
+ return undefined;
8
+ if (ts.isStringLiteral(init) || ts.isNoSubstitutionTemplateLiteral(init)) {
9
+ return { lit: init, valueNode: init };
10
+ }
11
+ if (ts.isJsxExpression(init) && init.expression) {
12
+ const innerExpr = init.expression;
13
+ if (ts.isStringLiteral(innerExpr) || ts.isNoSubstitutionTemplateLiteral(innerExpr)) {
14
+ return { lit: innerExpr, valueNode: init };
15
+ }
16
+ }
17
+ return undefined;
18
+ }