@codefast/cli 0.3.16-canary.3 → 0.4.0-canary.5
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 +69 -62
- package/dist/arrange/analyze.mjs +2 -2
- package/dist/arrange/command.mjs +51 -27
- package/dist/arrange/domain/analyze-service.mjs +2 -2
- package/dist/arrange/domain/ast/collectors-cn.mjs +1 -1
- package/dist/arrange/domain/ast/collectors-tv.mjs +2 -2
- package/dist/arrange/domain/ast/helpers.mjs +2 -2
- package/dist/arrange/domain/ast/simplify-targets.mjs +109 -0
- package/dist/arrange/domain/ast/targets.mjs +25 -10
- package/dist/arrange/domain/grouping-service.mjs +1 -1
- package/dist/arrange/domain/grouping.mjs +5 -5
- package/dist/arrange/domain/imports.mjs +50 -2
- package/dist/arrange/domain/token-classifier.mjs +12 -12
- package/dist/arrange/output.mjs +11 -3
- package/dist/arrange/process-file.mjs +1 -1
- package/dist/arrange/scan-target.mjs +10 -1
- package/dist/arrange/simplify-process-file.mjs +35 -0
- package/dist/arrange/simplify-sync.mjs +32 -0
- package/dist/arrange/suggest.mjs +1 -1
- package/dist/arrange/sync.mjs +1 -1
- package/dist/arrange/workspace.mjs +1 -1
- package/dist/cli.mjs +1 -1
- package/dist/core/cli/result-handle.mjs +1 -1
- package/dist/core/source-text-edit.mjs +1 -1
- package/dist/core/workspace/resolver.mjs +3 -3
- package/dist/mirror/cli-result.mjs +2 -1
- package/dist/mirror/cli-schema.mjs +2 -1
- package/dist/mirror/command.mjs +12 -11
- package/dist/mirror/domain/exports.mjs +2 -2
- package/dist/mirror/output.mjs +3 -0
- package/dist/mirror/supplement-exports.mjs +2 -2
- package/dist/mirror/sync-reporter.mjs +4 -1
- package/dist/mirror/sync-workspace-package.mjs +4 -4
- package/dist/mirror/sync.mjs +2 -1
- package/dist/mirror/write-exports.mjs +3 -2
- package/dist/tag/command.mjs +6 -6
- package/dist/tag/output.mjs +1 -1
- package/dist/tag/sync.mjs +1 -1
- package/dist/tag/target-runner.mjs +1 -1
- package/package.json +9 -9
package/README.md
CHANGED
|
@@ -18,8 +18,8 @@ A small developer CLI for maintenance tasks in a TypeScript monorepo — Tailwin
|
|
|
18
18
|
- [Global options](#global-options)
|
|
19
19
|
- [Exit codes](#exit-codes)
|
|
20
20
|
- [`arrange`](#arrange)
|
|
21
|
-
- [`mirror
|
|
22
|
-
- [`tag`
|
|
21
|
+
- [`mirror`](#mirror)
|
|
22
|
+
- [`tag`](#tag)
|
|
23
23
|
- [Configuration (`codefast.config.*`)](#configuration-codefastconfig)
|
|
24
24
|
- [Full skeleton](#full-skeleton)
|
|
25
25
|
- [`mirror` configuration](#mirror-configuration)
|
|
@@ -40,7 +40,9 @@ Three recurring maintenance chores you don't want to script by hand:
|
|
|
40
40
|
|
|
41
41
|
- **`arrange`** — regroup Tailwind class strings inside `cn()` / `tv()` calls in render-pipeline order.
|
|
42
42
|
- **`mirror`** — regenerate `package.json` `exports` fields from built `dist/` trees across a pnpm workspace.
|
|
43
|
-
- **`tag`**
|
|
43
|
+
- **`tag`** — add `@since <version>` JSDoc tags to exported declarations that are missing version metadata.
|
|
44
|
+
|
|
45
|
+
Each top-level command **performs its action by default** (writing files). Pass `--dry-run` to preview without writing. `arrange` additionally exposes a read-only `inspect` report.
|
|
44
46
|
|
|
45
47
|
```mermaid
|
|
46
48
|
flowchart LR
|
|
@@ -49,19 +51,16 @@ flowchart LR
|
|
|
49
51
|
R --> M[mirror]
|
|
50
52
|
R --> T[tag]
|
|
51
53
|
|
|
52
|
-
A --> A0[
|
|
53
|
-
A --> A1[
|
|
54
|
-
A --> A2[
|
|
55
|
-
A --> A3[group]
|
|
56
|
-
|
|
57
|
-
M --> M0[sync]
|
|
54
|
+
A --> A0[inspect]
|
|
55
|
+
A --> A1[simplify]
|
|
56
|
+
A --> A2[group]
|
|
58
57
|
```
|
|
59
58
|
|
|
60
59
|
---
|
|
61
60
|
|
|
62
61
|
## Requirements
|
|
63
62
|
|
|
64
|
-
- Node.js `>=
|
|
63
|
+
- Node.js `>= 24.0.0`
|
|
65
64
|
- pnpm (recommended — the CLI discovers workspaces via `pnpm-workspace.yaml`)
|
|
66
65
|
|
|
67
66
|
---
|
|
@@ -87,17 +86,17 @@ npx @codefast/cli --help
|
|
|
87
86
|
## Quick Start
|
|
88
87
|
|
|
89
88
|
```bash
|
|
90
|
-
#
|
|
91
|
-
codefast arrange
|
|
89
|
+
# Inspect Tailwind classes in the nearest package (read-only report)
|
|
90
|
+
codefast arrange inspect
|
|
92
91
|
|
|
93
92
|
# Preview proposed rewrites — no files written
|
|
94
|
-
codefast arrange
|
|
93
|
+
codefast arrange --dry-run packages/ui/src/components
|
|
95
94
|
|
|
96
95
|
# Apply after reviewing
|
|
97
|
-
codefast arrange
|
|
96
|
+
codefast arrange packages/ui/src/components
|
|
98
97
|
|
|
99
98
|
# Regenerate every package's `exports` from built dist/
|
|
100
|
-
codefast mirror
|
|
99
|
+
codefast mirror
|
|
101
100
|
|
|
102
101
|
# Add @since <version> to exported APIs under ./src
|
|
103
102
|
codefast tag
|
|
@@ -113,15 +112,17 @@ codefast tag
|
|
|
113
112
|
| `-V`, `--version` | Print the CLI version and exit. |
|
|
114
113
|
| `-h`, `--help` | Show contextual help for the invoked command. |
|
|
115
114
|
|
|
115
|
+
> **Placement.** Global flags must come **before** the command name (git-style), e.g. `codefast --no-color mirror`. A flag after the command name binds to that command.
|
|
116
|
+
|
|
116
117
|
---
|
|
117
118
|
|
|
118
119
|
## Exit codes
|
|
119
120
|
|
|
120
|
-
| Code | Meaning
|
|
121
|
-
| ---- |
|
|
122
|
-
| `0` | Success.
|
|
123
|
-
| `1` | General failure (missing paths, infrastructure errors, partial failures in `mirror
|
|
124
|
-
| `2` | Invalid invocation or input (Zod schema validation on CLI requests — `CLI_EXIT_USAGE`).
|
|
121
|
+
| Code | Meaning |
|
|
122
|
+
| ---- | --------------------------------------------------------------------------------------------------- |
|
|
123
|
+
| `0` | Success. |
|
|
124
|
+
| `1` | General failure (missing paths, infrastructure errors, partial failures in `mirror`, failed hooks). |
|
|
125
|
+
| `2` | Invalid invocation or input (Zod schema validation on CLI requests — `CLI_EXIT_USAGE`). |
|
|
125
126
|
|
|
126
127
|
Diagnostics go to **stderr**; primary command output goes to **stdout**. When a subcommand accepts `--json`, only the JSON object is written to stdout and all human progress is suppressed, so the stream stays pipeline-safe.
|
|
127
128
|
|
|
@@ -137,25 +138,28 @@ When `[target]` is omitted, `arrange` auto-detects the **nearest package directo
|
|
|
137
138
|
|
|
138
139
|
### Workflow
|
|
139
140
|
|
|
140
|
-
| Step | Command
|
|
141
|
-
| ---- |
|
|
142
|
-
| 1 | `codefast arrange
|
|
143
|
-
| 2 | `codefast arrange
|
|
144
|
-
| 3 | `codefast arrange
|
|
141
|
+
| Step | Command | Effect |
|
|
142
|
+
| ---- | ------------------------------------- | ---------------------------------------------- |
|
|
143
|
+
| 1 | `codefast arrange inspect [target]` | Report only — no files changed |
|
|
144
|
+
| 2 | `codefast arrange --dry-run [target]` | Show exactly what a bare `arrange` would write |
|
|
145
|
+
| 3 | `codefast arrange [target]` | Write the changes |
|
|
145
146
|
|
|
146
|
-
### Flags (
|
|
147
|
+
### Flags (`arrange`)
|
|
147
148
|
|
|
148
149
|
| Flag | Description |
|
|
149
150
|
| -------------------- | --------------------------------------------------------------------------- |
|
|
151
|
+
| `--dry-run` | Preview the rewrite without writing files. |
|
|
150
152
|
| `--with-class-name` | Append `className` as the last argument when rewriting a `cn(...)` call. |
|
|
151
153
|
| `--cn-import <spec>` | Override the module specifier used when a missing `cn` import is added. |
|
|
152
154
|
| `--json` | Print a single JSON object on stdout; suppresses human progress and colors. |
|
|
153
155
|
|
|
154
|
-
`
|
|
156
|
+
`inspect` also supports `--json`. `simplify` accepts `--dry-run` and `--json`.
|
|
157
|
+
|
|
158
|
+
> **Test files are skipped.** Directory scans exclude `*.test.*` / `*.spec.*` files — a `cn(...)` inside an assertion is test data, not styling to reformat. Pass such a file explicitly to override.
|
|
155
159
|
|
|
156
160
|
### `arrange group` — one-shot classification
|
|
157
161
|
|
|
158
|
-
Groups a class string without touching the filesystem. Useful for checking how classes would be grouped before running `
|
|
162
|
+
Groups a class string without touching the filesystem. Useful for checking how classes would be grouped before running `arrange`:
|
|
159
163
|
|
|
160
164
|
```bash
|
|
161
165
|
# Quoted string
|
|
@@ -176,44 +180,47 @@ codefast arrange group --tv "flex items-center gap-2"
|
|
|
176
180
|
|
|
177
181
|
### `--json` payloads
|
|
178
182
|
|
|
179
|
-
| Subcommand
|
|
180
|
-
|
|
|
181
|
-
| `
|
|
182
|
-
| `
|
|
183
|
-
| `
|
|
183
|
+
| Subcommand | Payload highlights |
|
|
184
|
+
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
185
|
+
| `arrange` | `schemaVersion`, `write`, `ok` (`false` if the `onAfterWrite` hook failed), full `result` (`filePaths`, `modifiedFiles`, `totalFound`, …). |
|
|
186
|
+
| `inspect` | `schemaVersion`, `analyzeRootPath`, full `report` (same data as the human report). |
|
|
187
|
+
| `simplify` | `schemaVersion`, `write`, `ok`, full `result`. |
|
|
188
|
+
| `group` | `schemaVersion`, `primaryLine`, `bucketsCommentLine`. |
|
|
184
189
|
|
|
185
190
|
---
|
|
186
191
|
|
|
187
|
-
## `mirror
|
|
192
|
+
## `mirror`
|
|
188
193
|
|
|
189
194
|
Scans built `dist/` trees and regenerates the `exports` field of every workspace package. Run it from anywhere inside the monorepo — the workspace root is discovered via `pnpm-workspace.yaml`.
|
|
190
195
|
|
|
191
196
|
```bash
|
|
192
|
-
codefast mirror
|
|
193
|
-
codefast mirror
|
|
194
|
-
codefast mirror
|
|
195
|
-
codefast mirror
|
|
197
|
+
codefast mirror # all workspace packages
|
|
198
|
+
codefast mirror packages/ui # one package (path relative to repo root)
|
|
199
|
+
codefast mirror --dry-run # preview — report changes without writing
|
|
200
|
+
codefast mirror -v # verbose diagnostics
|
|
201
|
+
codefast mirror --json # JSON summary for scripts / CI
|
|
196
202
|
```
|
|
197
203
|
|
|
198
|
-
| Flag | Description
|
|
199
|
-
| ----------------- |
|
|
200
|
-
|
|
|
201
|
-
| `--
|
|
204
|
+
| Flag | Description |
|
|
205
|
+
| ----------------- | ---------------------------------------------------------------------------- |
|
|
206
|
+
| `--dry-run` | Report what would change without writing any `package.json`. |
|
|
207
|
+
| `-v`, `--verbose` | Print extra diagnostics. |
|
|
208
|
+
| `--json` | Print a single `{ schemaVersion, ok, write, elapsedSeconds, stats }` object. |
|
|
202
209
|
|
|
203
|
-
> **Build first.** `mirror
|
|
210
|
+
> **Build first.** `mirror` reads from `dist/`. Run your build before syncing or exports will reflect stale output.
|
|
204
211
|
|
|
205
212
|
Exit code is `1` when any package fails (`stats.packagesErrored > 0`), `0` otherwise.
|
|
206
213
|
|
|
207
214
|
---
|
|
208
215
|
|
|
209
|
-
## `tag`
|
|
216
|
+
## `tag`
|
|
210
217
|
|
|
211
218
|
Scans `.ts` / `.tsx` sources and adds `@since <current-package-version>` to exported declarations that don't already carry one. The version is read from the nearest `package.json` walking up from the target path.
|
|
212
219
|
|
|
213
220
|
```bash
|
|
214
221
|
codefast tag # auto-discover workspace packages from cwd
|
|
215
|
-
codefast tag packages/ui/src #
|
|
216
|
-
codefast
|
|
222
|
+
codefast tag packages/ui/src # tag a custom target
|
|
223
|
+
codefast tag --dry-run # preview only, do not write
|
|
217
224
|
codefast tag --json # JSON summary for scripts / CI
|
|
218
225
|
```
|
|
219
226
|
|
|
@@ -296,7 +303,7 @@ export default {
|
|
|
296
303
|
|
|
297
304
|
#### Skipping a package
|
|
298
305
|
|
|
299
|
-
Set a package to `false` to exclude it from `codefast mirror
|
|
306
|
+
Set a package to `false` to exclude it from `codefast mirror` entirely:
|
|
300
307
|
|
|
301
308
|
```javascript
|
|
302
309
|
mirror: {
|
|
@@ -362,7 +369,7 @@ Keeps the existing `package.json#exports` map exactly as-is and only fills in mi
|
|
|
362
369
|
|
|
363
370
|
#### `css`
|
|
364
371
|
|
|
365
|
-
Controls CSS file export generation. `mirror
|
|
372
|
+
Controls CSS file export generation. `mirror` scans `dist/` for `.css` files and writes wildcard or per-file export entries.
|
|
366
373
|
|
|
367
374
|
```javascript
|
|
368
375
|
// Shorthand: auto-detect all CSS files in dist/
|
|
@@ -387,7 +394,7 @@ Controls CSS file export generation. `mirror sync` scans `dist/` for `.css` file
|
|
|
387
394
|
|
|
388
395
|
When `css` is omitted, CSS files found in `dist/` are still exported by default.
|
|
389
396
|
|
|
390
|
-
#### What `mirror
|
|
397
|
+
#### What `mirror` writes
|
|
391
398
|
|
|
392
399
|
For a package with `dist/button.mjs`, `dist/button.d.ts`, and `source: true`, the generated export entry looks like:
|
|
393
400
|
|
|
@@ -430,22 +437,22 @@ tag: {
|
|
|
430
437
|
|
|
431
438
|
```javascript
|
|
432
439
|
arrange: {
|
|
433
|
-
// Called after files are written by `codefast arrange
|
|
440
|
+
// Called after files are written by `codefast arrange`
|
|
434
441
|
onAfterWrite: ({ files }) => {
|
|
435
442
|
execSync(`oxfmt ${files.join(" ")}`, { stdio: "inherit" });
|
|
436
443
|
},
|
|
437
444
|
}
|
|
438
445
|
```
|
|
439
446
|
|
|
440
|
-
| Field | Type | Description
|
|
441
|
-
| -------------- | ----------------------------------------------------- |
|
|
442
|
-
| `onAfterWrite` | `(ctx: { files: string[] }) => void \| Promise<void>` | Lifecycle hook — runs after `arrange
|
|
447
|
+
| Field | Type | Description |
|
|
448
|
+
| -------------- | ----------------------------------------------------- | -------------------------------------------------------------------------------- |
|
|
449
|
+
| `onAfterWrite` | `(ctx: { files: string[] }) => void \| Promise<void>` | Lifecycle hook — runs after `arrange` writes files. Not called with `--dry-run`. |
|
|
443
450
|
|
|
444
451
|
---
|
|
445
452
|
|
|
446
453
|
## Lifecycle hooks
|
|
447
454
|
|
|
448
|
-
Both `tag` and `arrange
|
|
455
|
+
Both `tag` and `arrange` call `onAfterWrite` immediately after writing files to disk. The hook receives the list of written file paths and can run any synchronous or asynchronous work — formatters, linters, codegen, notifications.
|
|
449
456
|
|
|
450
457
|
```javascript
|
|
451
458
|
export default {
|
|
@@ -467,8 +474,8 @@ export default {
|
|
|
467
474
|
Contract:
|
|
468
475
|
|
|
469
476
|
- `tag.onAfterWrite` fires after `codefast tag` writes JSDoc annotations.
|
|
470
|
-
- `arrange.onAfterWrite` fires after `codefast arrange
|
|
471
|
-
- Hook is **not** called on `--dry-run
|
|
477
|
+
- `arrange.onAfterWrite` fires after `codefast arrange` rewrites class strings.
|
|
478
|
+
- Hook is **not** called on `--dry-run`.
|
|
472
479
|
- Hooks may be synchronous or `async` (`void | Promise<void>`).
|
|
473
480
|
- If the hook throws or rejects, the command reports the error on stderr and exits with code `1`.
|
|
474
481
|
|
|
@@ -509,14 +516,14 @@ To change placement, extend `classifyBareUtility` in `packages/cli/src/arrange/d
|
|
|
509
516
|
**`codefast: command not found`**
|
|
510
517
|
Install globally with `pnpm add -g @codefast/cli`, or run via `pnpm dlx @codefast/cli <command>`.
|
|
511
518
|
|
|
512
|
-
**`mirror
|
|
513
|
-
Packages must be built first. Ensure `dist/` exists by running your build step, then re-run `codefast mirror
|
|
519
|
+
**`mirror` writes little or no output**
|
|
520
|
+
Packages must be built first. Ensure `dist/` exists by running your build step, then re-run `codefast mirror`.
|
|
514
521
|
|
|
515
|
-
**Unexpected class reorder after `arrange
|
|
516
|
-
Run `arrange
|
|
522
|
+
**Unexpected class reorder after `arrange`**
|
|
523
|
+
Run `arrange --dry-run` first and smoke-test the UI. Some components rely on cascade-sensitive ordering that `arrange` cannot detect automatically.
|
|
517
524
|
|
|
518
525
|
**`--json` output mixed with progress lines**
|
|
519
|
-
Some shells buffer progress writes on stderr into stdout when piping — redirect stderr explicitly: `codefast mirror
|
|
526
|
+
Some shells buffer progress writes on stderr into stdout when piping — redirect stderr explicitly: `codefast mirror --json 2>/dev/null | jq`.
|
|
520
527
|
|
|
521
528
|
---
|
|
522
529
|
|
|
@@ -538,7 +545,7 @@ A few naming conventions:
|
|
|
538
545
|
|
|
539
546
|
- **`codefast <command>`** refers to CLI commands exposed via the `bin` entry in `@codefast/cli`.
|
|
540
547
|
- **Scripts in `packages/cli/package.json`** (`build`, `test`, …) are package-local dev scripts, not CLI commands.
|
|
541
|
-
- The root `package.json` includes optional convenience wrappers (e.g. `cli:mirror
|
|
548
|
+
- The root `package.json` includes optional convenience wrappers (e.g. `cli:mirror`, `cli:arrange-inspect`) for common dev workflows.
|
|
542
549
|
|
|
543
550
|
---
|
|
544
551
|
|
package/dist/arrange/analyze.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { AppError, messageFrom } from "../core/errors.mjs";
|
|
2
|
-
import { err, ok } from "../core/result.mjs";
|
|
3
1
|
import { accumulateAnalyzeReportForSourceFile, createEmptyAnalyzeReport } from "./domain/analyze-service.mjs";
|
|
2
|
+
import { AppError, messageFrom } from "../core/errors.mjs";
|
|
4
3
|
import { scanArrangeTargets } from "./scan-target.mjs";
|
|
5
4
|
import { parseDomainSourceFile } from "./source-parse.mjs";
|
|
5
|
+
import { err, ok } from "../core/result.mjs";
|
|
6
6
|
//#region src/arrange/analyze.ts
|
|
7
7
|
/**
|
|
8
8
|
* @since 0.3.16-canary.0
|
package/dist/arrange/command.mjs
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import { parseWithSchema } from "../core/schema-parse.mjs";
|
|
2
|
-
import "../core/exit-codes.mjs";
|
|
3
1
|
import { logger } from "../core/logger.mjs";
|
|
4
|
-
import { consumeCliAppError, runCliResultAsync } from "../core/cli/result-handle.mjs";
|
|
5
|
-
import { nodeFilesystem } from "../core/filesystem/node.mjs";
|
|
6
|
-
import { arrangeAnalyzeDirectoryRequestSchema, arrangeSuggestGroupsRequestSchema, arrangeSyncRunRequestSchema } from "./cli-schema.mjs";
|
|
7
|
-
import { prepareArrangeWorkspace } from "./workspace.mjs";
|
|
8
2
|
import { analyzeDirectory } from "./analyze.mjs";
|
|
9
|
-
import {
|
|
3
|
+
import { arrangeAnalyzeDirectoryRequestSchema, arrangeSuggestGroupsRequestSchema, arrangeSyncRunRequestSchema } from "./cli-schema.mjs";
|
|
4
|
+
import { printAnalyzeReport, printGroupFilePreviewFromWork, printSimplifyResult, printSyncResult } from "./output.mjs";
|
|
5
|
+
import { runArrangeSimplify } from "./simplify-sync.mjs";
|
|
10
6
|
import { suggestCnGroupsFromCli } from "./suggest.mjs";
|
|
11
|
-
import {
|
|
7
|
+
import { runArrangeSync } from "./sync.mjs";
|
|
8
|
+
import { prepareArrangeWorkspace } from "./workspace.mjs";
|
|
12
9
|
import { readOptionalPositionalArg } from "../core/cli/positional.mjs";
|
|
10
|
+
import "../core/exit-codes.mjs";
|
|
11
|
+
import { consumeCliAppError, runCliResultAsync } from "../core/cli/result-handle.mjs";
|
|
12
|
+
import { nodeFilesystem } from "../core/filesystem/node.mjs";
|
|
13
|
+
import { parseWithSchema } from "../core/schema-parse.mjs";
|
|
13
14
|
import process from "node:process";
|
|
14
15
|
import { Command } from "commander";
|
|
15
16
|
//#region src/arrange/command.ts
|
|
@@ -17,22 +18,8 @@ import { Command } from "commander";
|
|
|
17
18
|
* @since 0.3.16-canary.0
|
|
18
19
|
*/
|
|
19
20
|
function createArrangeCommand() {
|
|
20
|
-
const cmd = new Command("arrange").description("
|
|
21
|
-
|
|
22
|
-
const prelude = await prepareArrangeWorkspace(nodeFilesystem, {
|
|
23
|
-
currentWorkingDirectory: process.cwd(),
|
|
24
|
-
rawTarget: readOptionalPositionalArg(target)
|
|
25
|
-
});
|
|
26
|
-
if (!consumeCliAppError(prelude)) return;
|
|
27
|
-
const { resolvedTarget } = prelude.value;
|
|
28
|
-
const parsed = parseWithSchema(arrangeAnalyzeDirectoryRequestSchema, { analyzeRootPath: resolvedTarget });
|
|
29
|
-
if (!consumeCliAppError(parsed)) return;
|
|
30
|
-
const outcome = analyzeDirectory(nodeFilesystem, parsed.value.analyzeRootPath);
|
|
31
|
-
if (!consumeCliAppError(outcome)) return;
|
|
32
|
-
if (opts.json) logger.out(formatArrangeAnalyzeJsonOutput(resolvedTarget, outcome.value));
|
|
33
|
-
else printAnalyzeReport(resolvedTarget, outcome.value);
|
|
34
|
-
});
|
|
35
|
-
const previewOrApply = (write) => async (target, opts) => {
|
|
21
|
+
const cmd = new Command("arrange").description("Regroup Tailwind classes in cn() / tv() calls in render-pipeline order").enablePositionalOptions().argument("[target]", "Directory or file (default: nearest package directory from cwd)").option("--dry-run", "Preview suggested replacements without writing files", false).option("--with-classname, --with-class-name", "Append className as final cn() argument", false).option("--cn-import <spec>", "Override module specifier when adding cn import").option("--json", "Print one JSON object on stdout (suppresses human progress)", false).action(async (target, opts) => {
|
|
22
|
+
const write = !opts.dryRun;
|
|
36
23
|
const prelude = await prepareArrangeWorkspace(nodeFilesystem, {
|
|
37
24
|
currentWorkingDirectory: process.cwd(),
|
|
38
25
|
rawTarget: readOptionalPositionalArg(target)
|
|
@@ -57,9 +44,46 @@ function createArrangeCommand() {
|
|
|
57
44
|
printSyncResult(value, write);
|
|
58
45
|
return exitCodeForArrangeSyncResult(value);
|
|
59
46
|
});
|
|
60
|
-
};
|
|
61
|
-
cmd.command("
|
|
62
|
-
|
|
47
|
+
});
|
|
48
|
+
cmd.command("inspect").description("Report long strings, nested cn in tv(), and related findings (read-only)").argument("[target]", "Directory or file (default: nearest package directory from cwd)").option("--json", "Print one JSON object on stdout instead of a human report", false).action(async (target, opts) => {
|
|
49
|
+
const prelude = await prepareArrangeWorkspace(nodeFilesystem, {
|
|
50
|
+
currentWorkingDirectory: process.cwd(),
|
|
51
|
+
rawTarget: readOptionalPositionalArg(target)
|
|
52
|
+
});
|
|
53
|
+
if (!consumeCliAppError(prelude)) return;
|
|
54
|
+
const { resolvedTarget } = prelude.value;
|
|
55
|
+
const parsed = parseWithSchema(arrangeAnalyzeDirectoryRequestSchema, { analyzeRootPath: resolvedTarget });
|
|
56
|
+
if (!consumeCliAppError(parsed)) return;
|
|
57
|
+
const outcome = analyzeDirectory(nodeFilesystem, parsed.value.analyzeRootPath);
|
|
58
|
+
if (!consumeCliAppError(outcome)) return;
|
|
59
|
+
if (opts.json) logger.out(formatArrangeAnalyzeJsonOutput(resolvedTarget, outcome.value));
|
|
60
|
+
else printAnalyzeReport(resolvedTarget, outcome.value);
|
|
61
|
+
});
|
|
62
|
+
cmd.command("simplify").description("Flatten grouped arrays and static-only cn() calls back to plain strings in tv() slots").argument("[target]", "Directory or file (default: nearest package directory from cwd)").option("--dry-run", "Show what simplify would change without writing files", false).option("--json", "Print one JSON object on stdout (suppresses human progress)", false).action(async (target, opts) => {
|
|
63
|
+
const write = !opts.dryRun;
|
|
64
|
+
const prelude = await prepareArrangeWorkspace(nodeFilesystem, {
|
|
65
|
+
currentWorkingDirectory: process.cwd(),
|
|
66
|
+
rawTarget: readOptionalPositionalArg(target)
|
|
67
|
+
});
|
|
68
|
+
if (!consumeCliAppError(prelude)) return;
|
|
69
|
+
const { resolvedTarget } = prelude.value;
|
|
70
|
+
await runCliResultAsync(runArrangeSimplify(nodeFilesystem, {
|
|
71
|
+
targetPath: resolvedTarget,
|
|
72
|
+
write
|
|
73
|
+
}), (value) => {
|
|
74
|
+
if (opts.json) {
|
|
75
|
+
logger.out(JSON.stringify({
|
|
76
|
+
schemaVersion: 1,
|
|
77
|
+
ok: true,
|
|
78
|
+
write,
|
|
79
|
+
result: value
|
|
80
|
+
}));
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
printSimplifyResult(value, write);
|
|
84
|
+
return 0;
|
|
85
|
+
});
|
|
86
|
+
});
|
|
63
87
|
cmd.command("group").description("Try grouping on a pasted class string (stdout: cn(...) or tv array with --tv)").argument("<tokens...>", "Class tokens (quote a single string if it contains spaces)").option("--tv", "Emit tv()-style array instead of cn() call", false).option("--with-classname, --with-class-name", "Append className as final cn() argument", false).option("--json", "Print one JSON object on stdout instead of plain lines", false).action(async (classTokenSeries, opts) => {
|
|
64
88
|
const parsed = parseWithSchema(arrangeSuggestGroupsRequestSchema, {
|
|
65
89
|
inlineClasses: classTokenSeries.join(" ").trim(),
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import "./constants.mjs";
|
|
2
|
-
import { tokenizeClassString } from "./tailwind-token.mjs";
|
|
3
1
|
import { forEachDomainChild, isDomainCallExpression, isDomainJsxAttribute, isDomainObjectLiteralExpression } from "./ast/ast-node.mjs";
|
|
2
|
+
import "./constants.mjs";
|
|
4
3
|
import { forEachStringLiteralInClassExpression } from "./ast/collectors-cn.mjs";
|
|
5
4
|
import { jsxClassNameStaticLiteral } from "./ast/collectors-jsx.mjs";
|
|
6
5
|
import { buildKnownCnTvBindings, isCnOrTvIdentifier, lineOf } from "./ast/helpers.mjs";
|
|
6
|
+
import { tokenizeClassString } from "./tailwind-token.mjs";
|
|
7
7
|
import { collectCnCallsInsideTv, traverseTvObject } from "./ast/collectors-tv.mjs";
|
|
8
8
|
//#region src/arrange/domain/analyze-service.ts
|
|
9
9
|
/**
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import "../constants.mjs";
|
|
2
|
-
import { tokenizeClassString } from "../tailwind-token.mjs";
|
|
3
1
|
import { forEachDomainChild, isDomainArrayLiteralExpression, isDomainCallExpression, isDomainObjectLiteralExpression, isDomainPropertyAssignment, isDomainSpreadElement, isDomainTailwindClassLiteral } from "./ast-node.mjs";
|
|
2
|
+
import "../constants.mjs";
|
|
4
3
|
import { CN_APPLY_LITERAL_WALK_OPTS, collectUnconditionalTailwindLiteralsFromCnArguments, forEachStringLiteralInClassExpression, isUnsafeLiteralForCnStyleApplySplit } from "./collectors-cn.mjs";
|
|
5
4
|
import { buildKnownCnTvBindings, isCnOrTvIdentifier, propertyAssignmentNameText } from "./helpers.mjs";
|
|
5
|
+
import { tokenizeClassString } from "../tailwind-token.mjs";
|
|
6
6
|
//#region src/arrange/domain/ast/collectors-tv.ts
|
|
7
7
|
/**
|
|
8
8
|
* @since 0.3.16-canary.0
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { EMPTY_CN_TV_BINDINGS } from "../constants.mjs";
|
|
2
1
|
import { isDomainIdentifier, isDomainImportDeclaration, isDomainNamedImports, isDomainNamespaceImport, isDomainPropertyAccessExpression, isDomainStringLiteral, lineOfSourcePosition } from "./ast-node.mjs";
|
|
2
|
+
import { EMPTY_CN_TV_BINDINGS } from "../constants.mjs";
|
|
3
3
|
import { applyEditsDescending, indentOfLineContaining } from "../../../core/source-text-edit.mjs";
|
|
4
4
|
//#region src/arrange/domain/ast/helpers.ts
|
|
5
5
|
/**
|
|
@@ -21,7 +21,7 @@ const KNOWN_CN_TV_MODULES = new Set([
|
|
|
21
21
|
function moduleLooksLikeCnTvReexport(moduleSpecifier) {
|
|
22
22
|
const norm = moduleSpecifier.replace(/\\/g, "/");
|
|
23
23
|
if (/(?:^|[./])utils(?:\/|$)/.test(norm)) return true;
|
|
24
|
-
if (/\/utils\//.test(norm) ||
|
|
24
|
+
if (/\/utils\//.test(norm) || norm.endsWith("/utils")) return true;
|
|
25
25
|
if (/(?:^|\/)cn\.tsx?$/.test(norm)) return true;
|
|
26
26
|
return false;
|
|
27
27
|
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { forEachDomainChild, isDomainArrayLiteralExpression, isDomainCallExpression, isDomainJsxAttribute, isDomainJsxExpression, isDomainObjectLiteralExpression, isDomainPropertyAssignment, isDomainSpreadElement, isDomainTailwindClassLiteral } from "./ast-node.mjs";
|
|
2
|
+
import "../constants.mjs";
|
|
3
|
+
import { indentOfLineContaining } from "../../../core/source-text-edit.mjs";
|
|
4
|
+
import { buildKnownCnTvBindings, isCnOrTvIdentifier } from "./helpers.mjs";
|
|
5
|
+
import { escapeTsStringLiteralContent } from "../source-text-formatters.mjs";
|
|
6
|
+
//#region src/arrange/domain/ast/simplify-targets.ts
|
|
7
|
+
function toFlatString(text) {
|
|
8
|
+
return `"${escapeTsStringLiteralContent(text.trim())}"`;
|
|
9
|
+
}
|
|
10
|
+
function joinLiterals(args) {
|
|
11
|
+
return args.filter(isDomainTailwindClassLiteral).map((lit) => lit.text).join(" ");
|
|
12
|
+
}
|
|
13
|
+
function isAllStaticLiterals(args) {
|
|
14
|
+
return args.length > 0 && args.every(isDomainTailwindClassLiteral);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* For a cn() call that has both static and dynamic args:
|
|
18
|
+
* - Merge ALL static string literals (regardless of position) into one string.
|
|
19
|
+
* - Place the merged string first.
|
|
20
|
+
* - Append all dynamic args in their original relative order.
|
|
21
|
+
*
|
|
22
|
+
* Returns `null` when nothing changes (0 statics, or already 1 static at arg[0]).
|
|
23
|
+
*/
|
|
24
|
+
function buildMixedCnReplacement(call, sourceText) {
|
|
25
|
+
const args = [...call.arguments];
|
|
26
|
+
if (args.length === 0) return null;
|
|
27
|
+
const staticTexts = [];
|
|
28
|
+
const dynamicSrcs = [];
|
|
29
|
+
for (const arg of args) if (isDomainTailwindClassLiteral(arg)) staticTexts.push(arg.text);
|
|
30
|
+
else dynamicSrcs.push(sourceText.slice(arg.pos, arg.end));
|
|
31
|
+
if (staticTexts.length === 0) return null;
|
|
32
|
+
const firstArg = args[0];
|
|
33
|
+
if (staticTexts.length === 1 && firstArg !== void 0 && isDomainTailwindClassLiteral(firstArg)) return null;
|
|
34
|
+
const flatStatic = staticTexts.join(" ").trim();
|
|
35
|
+
const baseIndent = indentOfLineContaining(sourceText, call.pos);
|
|
36
|
+
const argIndent = `${baseIndent} `;
|
|
37
|
+
return `cn(\n${[`${argIndent}"${escapeTsStringLiteralContent(flatStatic)}",`, ...dynamicSrcs.map((src) => `${argIndent}${src},`)].join("\n")}\n${baseIndent})`;
|
|
38
|
+
}
|
|
39
|
+
function collectTvArrayEdits(obj, results, depth) {
|
|
40
|
+
if (depth > 12) return;
|
|
41
|
+
for (const prop of obj.properties) {
|
|
42
|
+
if (!isDomainPropertyAssignment(prop)) continue;
|
|
43
|
+
const init = prop.initializer;
|
|
44
|
+
if (isDomainArrayLiteralExpression(init)) {
|
|
45
|
+
const elements = [...init.elements];
|
|
46
|
+
if (!elements.some(isDomainSpreadElement) && elements.length > 0 && isAllStaticLiterals(elements)) results.push({
|
|
47
|
+
start: init.pos,
|
|
48
|
+
end: init.end,
|
|
49
|
+
replacement: toFlatString(joinLiterals(elements)),
|
|
50
|
+
label: "tv-array"
|
|
51
|
+
});
|
|
52
|
+
} else if (isDomainObjectLiteralExpression(init)) collectTvArrayEdits(init, results, depth + 1);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Collect all simplify edits for a source file:
|
|
57
|
+
* - Arrays of pure string literals inside tv() slots → flat string
|
|
58
|
+
* - cn() calls whose every argument is a static string literal → flat string (unwrap cn)
|
|
59
|
+
* - cn() calls with mixed static + dynamic args → merge adjacent statics into one string
|
|
60
|
+
* - JSX className={cn(...all-static...)} → className="flat string"
|
|
61
|
+
*
|
|
62
|
+
* @since 0.3.16-canary.0
|
|
63
|
+
*/
|
|
64
|
+
function collectSimplifyTargets(sourceFile) {
|
|
65
|
+
const sourceText = sourceFile.text;
|
|
66
|
+
const results = [];
|
|
67
|
+
const knownBindings = buildKnownCnTvBindings(sourceFile);
|
|
68
|
+
const seenCnPos = /* @__PURE__ */ new Set();
|
|
69
|
+
const visitNode = (node) => {
|
|
70
|
+
if (isDomainCallExpression(node)) {
|
|
71
|
+
if (isCnOrTvIdentifier(node.expression, "tv", knownBindings)) {
|
|
72
|
+
const arg0 = node.arguments[0];
|
|
73
|
+
if (arg0 && isDomainObjectLiteralExpression(arg0)) collectTvArrayEdits(arg0, results, 0);
|
|
74
|
+
} else if (isCnOrTvIdentifier(node.expression, "cn", knownBindings) && !seenCnPos.has(node.pos)) {
|
|
75
|
+
seenCnPos.add(node.pos);
|
|
76
|
+
const args = [...node.arguments];
|
|
77
|
+
if (isAllStaticLiterals(args)) {
|
|
78
|
+
const flat = joinLiterals(args);
|
|
79
|
+
const parent = node.parent;
|
|
80
|
+
if (parent && isDomainJsxExpression(parent) && parent.parent && isDomainJsxAttribute(parent.parent)) results.push({
|
|
81
|
+
start: parent.pos,
|
|
82
|
+
end: parent.end,
|
|
83
|
+
replacement: toFlatString(flat),
|
|
84
|
+
label: "jsx-cn"
|
|
85
|
+
});
|
|
86
|
+
else results.push({
|
|
87
|
+
start: node.pos,
|
|
88
|
+
end: node.end,
|
|
89
|
+
replacement: toFlatString(flat),
|
|
90
|
+
label: "cn-static"
|
|
91
|
+
});
|
|
92
|
+
} else {
|
|
93
|
+
const replacement = buildMixedCnReplacement(node, sourceText);
|
|
94
|
+
if (replacement !== null) results.push({
|
|
95
|
+
start: node.pos,
|
|
96
|
+
end: node.end,
|
|
97
|
+
replacement,
|
|
98
|
+
label: "cn-merge"
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
forEachDomainChild(node, visitNode);
|
|
104
|
+
};
|
|
105
|
+
for (const stmt of sourceFile.statements) visitNode(stmt);
|
|
106
|
+
return results;
|
|
107
|
+
}
|
|
108
|
+
//#endregion
|
|
109
|
+
export { collectSimplifyTargets };
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
+
import { forEachDomainChild, isDomainArrayLiteralExpression, isDomainJsxAttribute, isDomainPropertyAssignment, isDomainTailwindClassLiteral } from "./ast-node.mjs";
|
|
1
2
|
import "../constants.mjs";
|
|
2
|
-
import { tokenizeClassString } from "../tailwind-token.mjs";
|
|
3
|
-
import { forEachDomainChild, isDomainArrayLiteralExpression, isDomainJsxAttribute, isDomainTailwindClassLiteral } from "./ast-node.mjs";
|
|
4
3
|
import { isUnsafeLiteralForCnStyleApplySplit } from "./collectors-cn.mjs";
|
|
5
4
|
import { jsxClassNameStaticLiteral } from "./collectors-jsx.mjs";
|
|
6
5
|
import { endAfterOptionalCommaFollowingInSource, indentOfLineContaining, textPrefixFromLineStartToPosition } from "../../../core/source-text-edit.mjs";
|
|
6
|
+
import { tokenizeClassString } from "../tailwind-token.mjs";
|
|
7
7
|
import { collectGroupableStringNodes, slotClassString } from "./collectors-tv.mjs";
|
|
8
|
-
import { areCnTailwindPartitionsEquivalent, suggestCnGroups, summarizeGroupBucketLabels } from "../grouping.mjs";
|
|
9
8
|
import { escapeTsStringLiteralContent, formatArray, formatArrayElementsAsSiblingLines, formatJsxCnAttributeValue } from "../source-text-formatters.mjs";
|
|
9
|
+
import { areCnTailwindPartitionsEquivalent, suggestCnGroups, summarizeGroupBucketLabels } from "../grouping.mjs";
|
|
10
10
|
//#region src/arrange/domain/ast/targets.ts
|
|
11
11
|
/**
|
|
12
12
|
* @since 0.3.16-canary.0
|
|
@@ -62,7 +62,7 @@ function formatCnCallReplacement(stringNode, sourceText, withClassName) {
|
|
|
62
62
|
for (const dynamicArgumentSource of dynamicArgTexts) allArgs.push(`${argIndent}${dynamicArgumentSource}`);
|
|
63
63
|
if (withClassName) allArgs.push(`${argIndent}className`);
|
|
64
64
|
const commaAfterLastArg = allArgs.length > 1;
|
|
65
|
-
return `cn(\n${allArgs.map((argLine, lineIndex) => lineIndex < allArgs.length - 1 || commaAfterLastArg ? `${argLine},` :
|
|
65
|
+
return `cn(\n${allArgs.map((argLine, lineIndex) => lineIndex < allArgs.length - 1 || commaAfterLastArg ? `${argLine},` : argLine).join("\n")}\n${baseIndent})`;
|
|
66
66
|
}
|
|
67
67
|
/**
|
|
68
68
|
* @since 0.3.16-canary.0
|
|
@@ -88,14 +88,29 @@ function planGroupEditForTarget(target, textAfterUnwrap, withClassName) {
|
|
|
88
88
|
if (areCnTailwindPartitionsEquivalent(target.item.nodes.map((classLiteral) => classLiteral.text), groups)) return;
|
|
89
89
|
if (!target.item.cnCall) {
|
|
90
90
|
const anchorClassLiteral = target.item.primaryClassLiteral;
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
|
|
91
|
+
const parentNode = anchorClassLiteral.parent;
|
|
92
|
+
const parentArray = target.item.nodes.length > 1 && parentNode !== null && isDomainArrayLiteralExpression(parentNode) ? parentNode : null;
|
|
93
|
+
const bareStringProperty = !parentArray && parentNode !== null && isDomainPropertyAssignment(parentNode);
|
|
94
|
+
if (parentArray || bareStringProperty) {
|
|
95
|
+
const start = parentArray ? parentArray.pos : anchorClassLiteral.pos;
|
|
96
|
+
const end = parentArray ? parentArray.end : anchorClassLiteral.end;
|
|
97
|
+
const baseIndent = indentOfLineContaining(textAfterUnwrap, start);
|
|
98
|
+
return {
|
|
99
|
+
start,
|
|
100
|
+
end,
|
|
101
|
+
replacement: formatArray(groups).split("\n").map((line, lineIndex) => lineIndex === 0 ? line : `${baseIndent}${line}`).join("\n"),
|
|
102
|
+
bucketSummary: summarizeGroupBucketLabels(groups),
|
|
103
|
+
jsxCn: false,
|
|
104
|
+
lineSf: target.item.sf,
|
|
105
|
+
reportNode: anchorClassLiteral,
|
|
106
|
+
label: target.item.isTvContext ? "tv" : "cn"
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
const start = anchorClassLiteral.pos;
|
|
95
110
|
return {
|
|
96
111
|
start,
|
|
97
|
-
end,
|
|
98
|
-
replacement:
|
|
112
|
+
end: endAfterOptionalCommaFollowingInSource(textAfterUnwrap, anchorClassLiteral.end),
|
|
113
|
+
replacement: formatArrayElementsAsSiblingLines(groups, textPrefixFromLineStartToPosition(textAfterUnwrap, start)),
|
|
99
114
|
bucketSummary: summarizeGroupBucketLabels(groups),
|
|
100
115
|
jsxCn: false,
|
|
101
116
|
lineSf: target.item.sf,
|
|
@@ -42,7 +42,7 @@ function tryBuildGroupFileWorkPlan(input) {
|
|
|
42
42
|
if (domainSfGrouped.text !== unwrap.textAfterUnwrap) throw new Error("Domain invariant: domainSfGrouped.text must match unwrap phase textAfterUnwrap");
|
|
43
43
|
const groupTargets = collectGroupTargets(domainSfGrouped, filePath);
|
|
44
44
|
if (unwrap.cnInTvCalls.length === 0 && groupTargets.length === 0) return null;
|
|
45
|
-
const sortedTargets = [...groupTargets].
|
|
45
|
+
const sortedTargets = [...groupTargets].toSorted((leftTarget, rightTarget) => targetReplaceStart(rightTarget) - targetReplaceStart(leftTarget));
|
|
46
46
|
const plannedGroupEdits = [];
|
|
47
47
|
for (const groupTarget of sortedTargets) {
|
|
48
48
|
const plan = planGroupEditForTarget(groupTarget, unwrap.textAfterUnwrap, withClassName);
|