@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.
- package/README.md +66 -91
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +5 -0
- package/dist/commands/arrange.d.ts +3 -0
- package/dist/commands/arrange.d.ts.map +1 -0
- package/dist/commands/arrange.js +114 -0
- package/dist/commands/mirror.d.ts +4 -0
- package/dist/commands/mirror.d.ts.map +1 -0
- package/dist/commands/mirror.js +63 -0
- package/dist/lib/arrange/analyze.d.ts +4 -0
- package/dist/lib/arrange/analyze.d.ts.map +1 -0
- package/dist/lib/arrange/analyze.js +105 -0
- package/dist/lib/arrange/ast/collectors-cn.d.ts +10 -0
- package/dist/lib/arrange/ast/collectors-cn.d.ts.map +1 -0
- package/dist/lib/arrange/ast/collectors-cn.js +84 -0
- package/dist/lib/arrange/ast/collectors-jsx.d.ts +4 -0
- package/dist/lib/arrange/ast/collectors-jsx.d.ts.map +1 -0
- package/dist/lib/arrange/ast/collectors-jsx.js +18 -0
- package/dist/lib/arrange/ast/collectors-tv.d.ts +13 -0
- package/dist/lib/arrange/ast/collectors-tv.d.ts.map +1 -0
- package/dist/lib/arrange/ast/collectors-tv.js +273 -0
- package/dist/lib/arrange/ast/targets.d.ts +8 -0
- package/dist/lib/arrange/ast/targets.d.ts.map +1 -0
- package/dist/lib/arrange/ast/targets.js +143 -0
- package/dist/lib/arrange/ast/utils.d.ts +36 -0
- package/dist/lib/arrange/ast/utils.d.ts.map +1 -0
- package/dist/lib/arrange/ast/utils.js +138 -0
- package/dist/lib/arrange/constants.d.ts +68 -0
- package/dist/lib/arrange/constants.d.ts.map +1 -0
- package/dist/lib/arrange/constants.js +179 -0
- package/dist/lib/arrange/errors.d.ts +14 -0
- package/dist/lib/arrange/errors.d.ts.map +1 -0
- package/dist/lib/arrange/errors.js +16 -0
- package/dist/lib/arrange/formatters.d.ts +16 -0
- package/dist/lib/arrange/formatters.d.ts.map +1 -0
- package/dist/lib/arrange/formatters.js +57 -0
- package/dist/lib/arrange/group-file.d.ts +4 -0
- package/dist/lib/arrange/group-file.d.ts.map +1 -0
- package/dist/lib/arrange/group-file.js +115 -0
- package/dist/lib/arrange/grouping.d.ts +41 -0
- package/dist/lib/arrange/grouping.d.ts.map +1 -0
- package/dist/lib/arrange/grouping.js +280 -0
- package/dist/lib/arrange/imports.d.ts +6 -0
- package/dist/lib/arrange/imports.d.ts.map +1 -0
- package/dist/lib/arrange/imports.js +74 -0
- package/dist/lib/arrange/report.d.ts +4 -0
- package/dist/lib/arrange/report.d.ts.map +1 -0
- package/dist/lib/arrange/report.js +37 -0
- package/dist/lib/arrange/run-target.d.ts +4 -0
- package/dist/lib/arrange/run-target.d.ts.map +1 -0
- package/dist/lib/arrange/run-target.js +28 -0
- package/dist/lib/arrange/tokenizer.d.ts +37 -0
- package/dist/lib/arrange/tokenizer.d.ts.map +1 -0
- package/dist/lib/arrange/tokenizer.js +390 -0
- package/dist/lib/arrange/types.d.ts +104 -0
- package/dist/lib/arrange/types.d.ts.map +1 -0
- package/dist/lib/arrange/types.js +7 -0
- package/dist/lib/arrange/walk.d.ts +4 -0
- package/dist/lib/arrange/walk.d.ts.map +1 -0
- package/dist/lib/arrange/walk.js +34 -0
- package/dist/lib/arrange.d.ts +41 -0
- package/dist/lib/arrange.d.ts.map +1 -0
- package/dist/lib/arrange.js +38 -0
- package/dist/lib/infra/fs-contract.d.ts +28 -0
- package/dist/lib/infra/fs-contract.d.ts.map +1 -0
- package/dist/lib/infra/fs-contract.js +1 -0
- package/dist/lib/infra/node-io.d.ts +4 -0
- package/dist/lib/infra/node-io.d.ts.map +1 -0
- package/dist/lib/infra/node-io.js +27 -0
- package/dist/lib/mirror/config.d.ts +7 -0
- package/dist/lib/mirror/config.d.ts.map +1 -0
- package/dist/lib/mirror/config.js +146 -0
- package/dist/lib/mirror/constants.d.ts +10 -0
- package/dist/lib/mirror/constants.d.ts.map +1 -0
- package/dist/lib/mirror/constants.js +13 -0
- package/dist/lib/mirror/engine.d.ts +12 -0
- package/dist/lib/mirror/engine.d.ts.map +1 -0
- package/dist/lib/mirror/engine.js +248 -0
- package/dist/lib/mirror/errors.d.ts +10 -0
- package/dist/lib/mirror/errors.d.ts.map +1 -0
- package/dist/lib/mirror/errors.js +12 -0
- package/dist/lib/mirror/package-filter.d.ts +8 -0
- package/dist/lib/mirror/package-filter.d.ts.map +1 -0
- package/dist/lib/mirror/package-filter.js +33 -0
- package/dist/lib/mirror/reporter.d.ts +24 -0
- package/dist/lib/mirror/reporter.d.ts.map +1 -0
- package/dist/lib/mirror/reporter.js +126 -0
- package/dist/lib/mirror/sync.d.ts +4 -0
- package/dist/lib/mirror/sync.d.ts.map +1 -0
- package/dist/lib/mirror/sync.js +135 -0
- package/dist/lib/mirror/types.d.ts +81 -0
- package/dist/lib/mirror/types.d.ts.map +1 -0
- package/dist/lib/mirror/types.js +1 -0
- package/dist/lib/mirror/update-pkg.d.ts +13 -0
- package/dist/lib/mirror/update-pkg.d.ts.map +1 -0
- package/dist/lib/mirror/update-pkg.js +39 -0
- package/dist/lib/mirror/workspace-packages.d.ts +26 -0
- package/dist/lib/mirror/workspace-packages.d.ts.map +1 -0
- package/dist/lib/mirror/workspace-packages.js +160 -0
- package/dist/lib/mirror.d.ts +7 -0
- package/dist/lib/mirror.d.ts.map +1 -0
- package/dist/lib/mirror.js +5 -0
- package/dist/lib/repo-root.d.ts +7 -0
- package/dist/lib/repo-root.d.ts.map +1 -0
- package/dist/lib/repo-root.js +23 -0
- package/dist/lib/shared/utils.d.ts +9 -0
- package/dist/lib/shared/utils.d.ts.map +1 -0
- package/dist/lib/shared/utils.js +12 -0
- package/dist/program.d.ts +4 -0
- package/dist/program.d.ts.map +1 -0
- package/dist/program.js +38 -0
- package/package.json +53 -34
- package/CHANGELOG.md +0 -333
- package/dist/index.cjs +0 -1440
- package/dist/index.cjs.map +0 -1
- package/dist/index.js +0 -1406
- package/dist/index.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,125 +1,100 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @codefast/cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Two tools bundled in one CLI:
|
|
4
4
|
|
|
5
|
-
Command
|
|
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
|
-
|
|
10
|
+
---
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
## Requirements & Install
|
|
13
|
+
|
|
14
|
+
**Node.js ≥ 24** is required.
|
|
12
15
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
24
|
+
---
|
|
50
25
|
|
|
51
|
-
|
|
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
|
-
|
|
28
|
+
### Recommended workflow
|
|
55
29
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
34
|
+
**Default target** (when path is omitted): `packages/ui/src/components` resolved from `process.cwd()`.
|
|
61
35
|
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
|
|
71
|
-
- So sánh với exports hiện tại
|
|
41
|
+
### `group [tokens...]`
|
|
72
42
|
|
|
73
|
-
|
|
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
|
-
|
|
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
|
-
|
|
47
|
+
## Grouping philosophy — Render Pipeline Order
|
|
82
48
|
|
|
83
|
-
|
|
49
|
+
`arrange` does **not** sort alphabetically. It groups utilities in roughly the same order the browser reasons about them:
|
|
84
50
|
|
|
85
|
-
|
|
51
|
+
**Existence → Position → Layout → Sizing → Spacing → Shape → Background → Shadow → Typography → Composite → Motion → Starting → Behavior → Conditions (State)**
|
|
86
52
|
|
|
87
|
-
|
|
53
|
+
Bucket breakdown:
|
|
88
54
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
76
|
+
---
|
|
98
77
|
|
|
99
|
-
|
|
100
|
-
codefast update-exports
|
|
101
|
-
```
|
|
78
|
+
## `mirror sync`
|
|
102
79
|
|
|
103
|
-
|
|
80
|
+
Run from anywhere under the monorepo — the CLI finds the root via `pnpm-workspace.yaml`.
|
|
104
81
|
|
|
105
|
-
```
|
|
106
|
-
codefast
|
|
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
|
-
|
|
88
|
+
**Config:** Place `codefast.config.js` (or `.mjs` / `.cjs` / `.json`) at repo root with a `mirror` object (`skipPackages`, `pathTransformations`, `customExports`, …).
|
|
110
89
|
|
|
111
|
-
|
|
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
|
-
|
|
92
|
+
---
|
|
116
93
|
|
|
117
|
-
|
|
118
|
-
codefast update-exports --dry-run
|
|
119
|
-
```
|
|
94
|
+
## Developing inside this monorepo
|
|
120
95
|
|
|
121
|
-
|
|
96
|
+
```bash
|
|
97
|
+
pnpm exec codefast --help
|
|
98
|
+
```
|
|
122
99
|
|
|
123
|
-
|
|
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 @@
|
|
|
1
|
+
{"version":3,"file":"bin.d.ts","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":""}
|
package/dist/bin.js
ADDED
|
@@ -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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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
|
+
}
|