@yopem-ui/cli 0.1.1 → 0.1.3
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 +85 -73
- package/package.json +1 -1
- package/src/init.ts +9 -7
- package/src/install.ts +40 -24
- package/src/project.ts +12 -4
package/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# @yopem-ui/cli
|
|
2
2
|
|
|
3
|
-
Copy Yopem UI components
|
|
4
|
-
|
|
3
|
+
Copy Yopem UI components into an existing React project. Configure StyleX with
|
|
4
|
+
the CLI. The CLI requires Bun. Registry requests use `https://ui.yopem.com/r` by
|
|
5
|
+
default.
|
|
5
6
|
|
|
6
7
|
```sh
|
|
7
8
|
bunx @yopem-ui/cli init
|
|
@@ -11,19 +12,20 @@ bunx @yopem-ui/cli --help
|
|
|
11
12
|
bunx @yopem-ui/cli --version
|
|
12
13
|
```
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
installed CLI version. These commands
|
|
16
|
-
Invalid commands or arguments exit with
|
|
15
|
+
Run the CLI without arguments, or with `--help` (`-h`), to show usage. Use
|
|
16
|
+
`--version` (`-v`) to show the installed CLI version. These commands do not need
|
|
17
|
+
a project. They exit successfully. Invalid commands or arguments exit with
|
|
18
|
+
status 1.
|
|
17
19
|
|
|
18
|
-
`init` supports Vite, TanStack Router, TanStack Start, React Router,
|
|
19
|
-
Router, and Astro. Pass `--framework <name>` if
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
The `init` command supports Vite, TanStack Router, TanStack Start, React Router,
|
|
21
|
+
Next.js App Router, and Astro. Pass `--framework <name>` if the CLI cannot
|
|
22
|
+
identify one framework. The `init` and `add` commands keep local changes to
|
|
23
|
+
installed source. The `update` command rejects modified files unless you pass
|
|
24
|
+
`--force`. The CLI records installed files and their hashes in `ui.json`.
|
|
23
25
|
|
|
24
26
|
## Dry run
|
|
25
27
|
|
|
26
|
-
Preview `add` or `update` before
|
|
28
|
+
Preview `add` or `update` before installation:
|
|
27
29
|
|
|
28
30
|
```sh
|
|
29
31
|
bunx @yopem-ui/cli add button --dry-run
|
|
@@ -31,33 +33,35 @@ bunx @yopem-ui/cli update button --dry-run
|
|
|
31
33
|
bunx @yopem-ui/cli update button --dry-run --force
|
|
32
34
|
```
|
|
33
35
|
|
|
34
|
-
The plan lists file writes
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
`
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
`preview.
|
|
52
|
-
`
|
|
53
|
-
|
|
36
|
+
The plan lists file writes, skipped files, local conflicts, and runtime and
|
|
37
|
+
development dependency additions. File writes include tracking changes in
|
|
38
|
+
`ui.json`. The plan identifies forced source overwrites explicitly. As in normal
|
|
39
|
+
mode, `add` skips modified tracked files. Without `--force`, `update` reports
|
|
40
|
+
those files as conflicts. Resolve conflicts before a real installation, or use
|
|
41
|
+
`--force` to overwrite files. Dependency lists show requested package arguments,
|
|
42
|
+
not package-manager version resolution.
|
|
43
|
+
|
|
44
|
+
Dry runs read the registry. They validate URLs, schemas, paths, and file
|
|
45
|
+
integrity. They do not write files or create directories or temporary files.
|
|
46
|
+
They do not run a package manager. They do not change `ui.json`, project
|
|
47
|
+
configuration, package manifests, lockfiles, or dependencies. Use `--cwd` and
|
|
48
|
+
`--registry` as usual. The `init --dry-run` command also previews setup for
|
|
49
|
+
supported frameworks and shared UI workspaces.
|
|
50
|
+
|
|
51
|
+
Programmatic callers can use `installItem(name, { dryRun: true })` through
|
|
52
|
+
`@yopem-ui/cli/install`. The result includes `preview.files`,
|
|
53
|
+
`preview.dependencies`, and `preview.devDependencies`. Each preview file has
|
|
54
|
+
`path` and `action`, with optional `reason` and `forced`. The `installed` count
|
|
55
|
+
is zero. Normal calls return `{ installed, skipped }`. Previewed writes apply
|
|
56
|
+
only if no conflicts block the command and files remain unchanged before the
|
|
57
|
+
real installation.
|
|
54
58
|
|
|
55
59
|
## Registry
|
|
56
60
|
|
|
57
61
|
Pass `--registry <URL>` to `init`, `add`, or `update` to use another registry.
|
|
58
|
-
The
|
|
59
|
-
must use HTTPS, except HTTP on `localhost` or loopback
|
|
60
|
-
query strings, and fragments
|
|
62
|
+
The setting applies only to that command. The CLI does not store it in
|
|
63
|
+
`ui.json`. URLs must use HTTPS, except HTTP on `localhost` or loopback
|
|
64
|
+
addresses. The CLI rejects credentials, query strings, and fragments.
|
|
61
65
|
|
|
62
66
|
For local development, start this repository's docs server with `bun run dev`:
|
|
63
67
|
|
|
@@ -67,16 +71,17 @@ bunx @yopem-ui/cli add button --registry http://localhost:3100/r
|
|
|
67
71
|
bunx @yopem-ui/cli update button --registry http://localhost:3100/r
|
|
68
72
|
```
|
|
69
73
|
|
|
70
|
-
Each request has a 30-second timeout
|
|
71
|
-
Failures report timeout, network, HTTP status, or malformed JSON details
|
|
72
|
-
|
|
73
|
-
`InstallOptions.registryUrl` and
|
|
74
|
+
Each request has a 30-second timeout for connection and JSON body reads.
|
|
75
|
+
Failures report timeout, network, HTTP status, or malformed JSON details. The
|
|
76
|
+
CLI makes no source, configuration, or dependency changes after these failures.
|
|
77
|
+
Programmatic callers can set `InstallOptions.registryUrl` and
|
|
78
|
+
`requestTimeoutMs`. The timeout uses milliseconds.
|
|
74
79
|
|
|
75
80
|
## Monorepos
|
|
76
81
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
Bun, npm, pnpm, and Yarn workspaces
|
|
82
|
+
Use `--cwd` to target an app from the repository root. The CLI detects the
|
|
83
|
+
package manager from the app and its workspace root. Dependencies stay in the
|
|
84
|
+
target package. The CLI supports Bun, npm, pnpm, and Yarn workspaces.
|
|
80
85
|
|
|
81
86
|
```sh
|
|
82
87
|
bunx @yopem-ui/cli init --cwd apps/web
|
|
@@ -84,7 +89,8 @@ bunx @yopem-ui/cli add button --cwd apps/web
|
|
|
84
89
|
```
|
|
85
90
|
|
|
86
91
|
For shared components, use an existing named React package in the same
|
|
87
|
-
workspace. `--ui` is relative to the target app, not the repository
|
|
92
|
+
workspace. The `--ui` path is relative to the target app, not the repository
|
|
93
|
+
root.
|
|
88
94
|
|
|
89
95
|
```sh
|
|
90
96
|
bunx @yopem-ui/cli init --cwd apps/web --ui ../../packages/ui
|
|
@@ -92,20 +98,22 @@ bunx @yopem-ui/cli add button --cwd packages/ui
|
|
|
92
98
|
bunx @yopem-ui/cli update button --cwd packages/ui
|
|
93
99
|
```
|
|
94
100
|
|
|
95
|
-
`init --ui` installs base files in the UI package
|
|
96
|
-
links the app
|
|
97
|
-
|
|
98
|
-
Oxlint rules
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
101
|
+
The `init --ui` command installs base files in the UI package. It adds source
|
|
102
|
+
subpath exports and links the app. It configures StyleX to scan both packages.
|
|
103
|
+
For Next.js, it also adds `transpilePackages`. The command registers shared
|
|
104
|
+
package imports with the Yopem UI Oxlint rules. It keeps existing rule options
|
|
105
|
+
and explicit opt-outs.
|
|
106
|
+
|
|
107
|
+
Shared source imports use the UI package name, such as
|
|
108
|
+
`@acme/ui/components/ui/button`. Later `add` and `update` commands use that name
|
|
109
|
+
from the package's `ui.json`. Run `init --ui` for each consuming app. Pass
|
|
110
|
+
`--ui` again when you repeat init. Review conflicting exports or build
|
|
111
|
+
configuration manually. The init command does not migrate app-local components.
|
|
104
112
|
|
|
105
113
|
## Development checks
|
|
106
114
|
|
|
107
|
-
|
|
108
|
-
before
|
|
115
|
+
Generate registry fixtures from the repository root. Build the lint plugin
|
|
116
|
+
before you run CLI tests:
|
|
109
117
|
|
|
110
118
|
```sh
|
|
111
119
|
bun run registry:build
|
|
@@ -113,27 +121,31 @@ bun run --cwd packages/oxlint-plugin build
|
|
|
113
121
|
bun test packages/cli
|
|
114
122
|
```
|
|
115
123
|
|
|
116
|
-
`bun run --cwd packages/cli test` also runs the CLI suite. Tests use
|
|
117
|
-
not browsers
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
124
|
+
The `bun run --cwd packages/cli test` command also runs the CLI suite. Tests use
|
|
125
|
+
`bun:test`, not browsers. Packaged installation checks explicitly pass a local
|
|
126
|
+
`--registry`. They serve registry JSON with Bun if port 3100 has no registry
|
|
127
|
+
server.
|
|
128
|
+
|
|
129
|
+
Registry network tests use isolated local Bun servers. They cover successful
|
|
130
|
+
workflows, header and body stalls, malformed JSON, and HTTP and network
|
|
131
|
+
failures. Tests save command logs and configuration artifacts in
|
|
132
|
+
`packages/cli/test-results/`.
|
|
133
|
+
|
|
134
|
+
Run `bun test packages/cli/test/dry-run.test.ts` for real fixture project
|
|
135
|
+
checks. These checks use local registries and complete file and directory
|
|
136
|
+
snapshots. They cover dependency-call tracking, conflict and force previews,
|
|
137
|
+
shared import prefixes, validation failures, and CLI argument rules. Tests save
|
|
138
|
+
snapshot and preview evidence in `step5-dry-run.json` in the results directory.
|
|
139
|
+
|
|
140
|
+
Run `bun test packages/cli/test/public.test.ts` for packed CLI production
|
|
141
|
+
checks. These checks cover help, version matching, dry-run previews, init,
|
|
142
|
+
Button installation, lint, and builds. They use Vite and Next.js App Router
|
|
143
|
+
apps, including shared UI workspaces. Next.js checks require Node.js. Fixture
|
|
144
|
+
dependencies require network access. Tests verify build artifacts and compiled
|
|
145
|
+
StyleX CSS. They save evidence in `packaged-*.log`, `packaged-*.html`, and
|
|
146
|
+
`packaged-*.css`.
|
|
135
147
|
|
|
136
148
|
## Licence
|
|
137
149
|
|
|
138
|
-
This project
|
|
150
|
+
This project uses the
|
|
139
151
|
[MIT license](https://github.com/yopem/ui/blob/main/LICENSE.md).
|
package/package.json
CHANGED
package/src/init.ts
CHANGED
|
@@ -1402,7 +1402,6 @@ const lintRules = {
|
|
|
1402
1402
|
"yopem-ui/no-raw-stylex-colors": "error",
|
|
1403
1403
|
"yopem-ui/prefer-layout-primitives": "error",
|
|
1404
1404
|
"yopem-ui/static-stylex": "error",
|
|
1405
|
-
"yopem-ui/valid-polymorphic-as": "error",
|
|
1406
1405
|
}
|
|
1407
1406
|
|
|
1408
1407
|
function lintConfig(content: string, shared?: SharedUI) {
|
|
@@ -1441,7 +1440,6 @@ function lintConfig(content: string, shared?: SharedUI) {
|
|
|
1441
1440
|
for (const name of [
|
|
1442
1441
|
"yopem-ui/enforce-styling-methods",
|
|
1443
1442
|
"yopem-ui/no-restyle",
|
|
1444
|
-
"yopem-ui/valid-polymorphic-as",
|
|
1445
1443
|
]) {
|
|
1446
1444
|
const setting = configuredRules[name] ?? "error"
|
|
1447
1445
|
const level = Array.isArray(setting) ? (setting[0] ?? "error") : setting
|
|
@@ -1519,10 +1517,11 @@ function nextScripts(value: JsonValue | undefined) {
|
|
|
1519
1517
|
}
|
|
1520
1518
|
|
|
1521
1519
|
async function chooseFile(root: string, names: string[], fallback?: string) {
|
|
1522
|
-
const
|
|
1520
|
+
const existing = await Promise.all(
|
|
1521
|
+
names.map((name) => existingFile(root, name)),
|
|
1522
|
+
)
|
|
1523
1523
|
|
|
1524
|
-
|
|
1525
|
-
if (await existingFile(root, name)) matches.push(name)
|
|
1524
|
+
const matches = names.filter((_, index) => existing[index])
|
|
1526
1525
|
|
|
1527
1526
|
if (matches.length > 1)
|
|
1528
1527
|
throw new Error(`Ambiguous configuration: ${matches.join(", ")}`)
|
|
@@ -1619,8 +1618,11 @@ export async function initProject(
|
|
|
1619
1618
|
}
|
|
1620
1619
|
}
|
|
1621
1620
|
|
|
1622
|
-
const packageRun = await
|
|
1623
|
-
|
|
1621
|
+
const [packageRun, uiRun] = await Promise.all([
|
|
1622
|
+
packageRunner(root, options.run),
|
|
1623
|
+
packageRunner(uiRoot, options.run),
|
|
1624
|
+
])
|
|
1625
|
+
|
|
1624
1626
|
const edits = new Map<string, { before: string | null; after: string }>()
|
|
1625
1627
|
|
|
1626
1628
|
const plannedFiles: {
|
package/src/install.ts
CHANGED
|
@@ -490,26 +490,34 @@ export async function writeFiles(
|
|
|
490
490
|
}
|
|
491
491
|
|
|
492
492
|
try {
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
493
|
+
staged.push(
|
|
494
|
+
...(await Promise.all(
|
|
495
|
+
[...changes].map(async ([path, change]): Promise<StagedFile> => {
|
|
496
|
+
const exists = await existingFile(root, path)
|
|
497
|
+
const before = exists ? await readFile(join(root, path)) : null
|
|
498
|
+
|
|
499
|
+
if ((before?.toString("utf8") ?? null) !== change.before) {
|
|
500
|
+
throw new Error(`File changed before commit: ${path}`)
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const mode = exists
|
|
504
|
+
? (await lstat(join(root, path))).mode & 0o7777
|
|
505
|
+
: null
|
|
506
|
+
|
|
507
|
+
const after = change.after === null ? null : Buffer.from(change.after)
|
|
508
|
+
|
|
509
|
+
return {
|
|
510
|
+
path,
|
|
511
|
+
before,
|
|
512
|
+
after,
|
|
513
|
+
mode,
|
|
514
|
+
afterMode: mode,
|
|
515
|
+
changed: change.before !== change.after,
|
|
516
|
+
directory: null,
|
|
517
|
+
}
|
|
518
|
+
}),
|
|
519
|
+
)),
|
|
520
|
+
)
|
|
513
521
|
|
|
514
522
|
for (const file of staged) {
|
|
515
523
|
if (!file.changed) continue
|
|
@@ -535,7 +543,9 @@ export async function writeFiles(
|
|
|
535
543
|
|
|
536
544
|
await prepare?.()
|
|
537
545
|
|
|
538
|
-
|
|
546
|
+
await Promise.all(
|
|
547
|
+
staged.map((file) => verify(file, file.before, file.mode)),
|
|
548
|
+
)
|
|
539
549
|
|
|
540
550
|
for (const file of staged) {
|
|
541
551
|
if (!file.changed) continue
|
|
@@ -737,10 +747,16 @@ export async function planInstall(name: string, options: InstallOptions = {}) {
|
|
|
737
747
|
manifest.provenance[path] = { registryUrl, items: ownership }
|
|
738
748
|
}
|
|
739
749
|
|
|
740
|
-
|
|
741
|
-
|
|
750
|
+
const currentFiles = await Promise.all(
|
|
751
|
+
[...files].map(async ([path, file]) => {
|
|
752
|
+
const exists = await existingFile(root, path)
|
|
753
|
+
const before = exists ? await readFile(join(root, path), "utf8") : null
|
|
754
|
+
|
|
755
|
+
return { path, file, before }
|
|
756
|
+
}),
|
|
757
|
+
)
|
|
742
758
|
|
|
743
|
-
|
|
759
|
+
for (const { path, file, before } of currentFiles) {
|
|
744
760
|
const current = before === null ? null : hash(before)
|
|
745
761
|
changes.set(path, { before, after: before })
|
|
746
762
|
const previous = manifest.files[path]
|
package/src/project.ts
CHANGED
|
@@ -166,8 +166,12 @@ async function packageManager(root: string) {
|
|
|
166
166
|
["yarn", "yarn.lock"],
|
|
167
167
|
]
|
|
168
168
|
|
|
169
|
-
|
|
170
|
-
|
|
169
|
+
const existingLocks = await Promise.all(
|
|
170
|
+
locks.map(([, path]) => existingFile(root, path)),
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
for (const [index, [manager]] of locks.entries()) {
|
|
174
|
+
if (existingLocks[index]) found.add(manager)
|
|
171
175
|
}
|
|
172
176
|
|
|
173
177
|
if (found.size > 1) {
|
|
@@ -184,8 +188,12 @@ export async function packageRunner(
|
|
|
184
188
|
run?: InstallOptions["run"],
|
|
185
189
|
): Promise<NonNullable<InstallOptions["run"]>> {
|
|
186
190
|
const target = await realpath(root)
|
|
187
|
-
|
|
188
|
-
const local = await
|
|
191
|
+
|
|
192
|
+
const [owner, local] = await Promise.all([
|
|
193
|
+
workspaceRoot(target),
|
|
194
|
+
packageManager(target),
|
|
195
|
+
])
|
|
196
|
+
|
|
189
197
|
const workspace = owner === target ? local : await packageManager(owner)
|
|
190
198
|
|
|
191
199
|
if (local && workspace && local !== workspace) {
|