@yopem-ui/cli 0.0.2 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +121 -3
- package/package.json +4 -2
- package/src/cli.ts +98 -18
- package/src/init.ts +370 -164
- package/src/install.ts +422 -59
- package/src/project.ts +223 -0
package/src/project.ts
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
InstallOptions,
|
|
3
|
+
JsonObject,
|
|
4
|
+
JsonValue,
|
|
5
|
+
} from "@yopem-ui/cli/install"
|
|
6
|
+
|
|
7
|
+
import { existingFile, isRecord, isString, runBun } from "@yopem-ui/cli/install"
|
|
8
|
+
import { readFile, realpath } from "node:fs/promises"
|
|
9
|
+
import { dirname, join, matchesGlob, relative } from "node:path"
|
|
10
|
+
|
|
11
|
+
type PackageManager = "bun" | "npm" | "pnpm" | "yarn"
|
|
12
|
+
|
|
13
|
+
export function isPackageName(value: unknown): value is string {
|
|
14
|
+
return (
|
|
15
|
+
isString(value) &&
|
|
16
|
+
value.length <= 214 &&
|
|
17
|
+
value === value.trim() &&
|
|
18
|
+
/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/.test(value) &&
|
|
19
|
+
value !== "node_modules" &&
|
|
20
|
+
value !== "favicon.ico"
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function readPackage(root: string): Promise<JsonObject> {
|
|
25
|
+
if (!(await existingFile(root, "package.json"))) return {}
|
|
26
|
+
|
|
27
|
+
const value: JsonValue = JSON.parse(
|
|
28
|
+
await readFile(join(root, "package.json"), "utf8"),
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
if (!isRecord(value)) throw new Error(`Invalid package.json in ${root}`)
|
|
32
|
+
|
|
33
|
+
return value
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function pnpmPatterns(content: string) {
|
|
37
|
+
// ponytail: string lists only; add YAML parser for anchors or flow lists.
|
|
38
|
+
const lines = content.split(/\r?\n/)
|
|
39
|
+
const start = lines.findIndex((line) => /^packages:\s*(?:#.*)?$/.test(line))
|
|
40
|
+
|
|
41
|
+
if (start < 0) {
|
|
42
|
+
if (
|
|
43
|
+
lines.some((line) => /^\s*(?:["']packages["']|packages)\s*:/.test(line))
|
|
44
|
+
) {
|
|
45
|
+
throw new Error("Unsupported pnpm workspace packages; use a string list")
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return ["**"]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const patterns: string[] = []
|
|
52
|
+
|
|
53
|
+
for (const line of lines.slice(start + 1)) {
|
|
54
|
+
if (/^\s*(?:#.*)?$/.test(line)) continue
|
|
55
|
+
|
|
56
|
+
if (/^\S/.test(line) && !line.startsWith("-")) break
|
|
57
|
+
|
|
58
|
+
const entry = line.match(
|
|
59
|
+
/^\s*-\s*(?:"([^"]*)"|'([^']*)'|([^"'#]+?))\s*(?:#.*)?$/,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
const pattern = entry?.[1] ?? entry?.[2] ?? entry?.[3]
|
|
63
|
+
|
|
64
|
+
if (!pattern || (entry?.[3] && /^[*&[\]{}>|]/.test(pattern))) {
|
|
65
|
+
throw new Error("Unsupported pnpm workspace pattern")
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
patterns.push(pattern)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return patterns
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function workspaceRoot(root: string) {
|
|
75
|
+
const target = await realpath(root)
|
|
76
|
+
let current = target
|
|
77
|
+
|
|
78
|
+
while (true) {
|
|
79
|
+
const manifest = await readPackage(current)
|
|
80
|
+
|
|
81
|
+
let patterns = isRecord(manifest.workspaces)
|
|
82
|
+
? manifest.workspaces.packages
|
|
83
|
+
: manifest.workspaces
|
|
84
|
+
|
|
85
|
+
if (isRecord(manifest.workspaces) && patterns === undefined) {
|
|
86
|
+
throw new Error(`Invalid workspace packages in ${current}`)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (await existingFile(current, "pnpm-workspace.yaml")) {
|
|
90
|
+
patterns = pnpmPatterns(
|
|
91
|
+
await readFile(join(current, "pnpm-workspace.yaml"), "utf8"),
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (patterns !== undefined) {
|
|
96
|
+
if (
|
|
97
|
+
!Array.isArray(patterns) ||
|
|
98
|
+
!patterns.every(isString) ||
|
|
99
|
+
patterns.some((pattern) => !pattern)
|
|
100
|
+
) {
|
|
101
|
+
throw new Error(`Invalid workspace packages in ${current}`)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const path = relative(current, target).replaceAll("\\", "/")
|
|
105
|
+
const includes: string[] = []
|
|
106
|
+
const excludes: string[] = []
|
|
107
|
+
|
|
108
|
+
for (const pattern of patterns) {
|
|
109
|
+
const excluded = pattern.startsWith("!")
|
|
110
|
+
|
|
111
|
+
const normalized = (excluded ? pattern.slice(1) : pattern)
|
|
112
|
+
.replace(/^\.\//, "")
|
|
113
|
+
.replace(/\/$/, "")
|
|
114
|
+
|
|
115
|
+
const group = excluded ? excludes : includes
|
|
116
|
+
group.push(normalized)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (
|
|
120
|
+
path &&
|
|
121
|
+
(!includes.some((pattern) => matchesGlob(path, pattern)) ||
|
|
122
|
+
excludes.some((pattern) => matchesGlob(path, pattern)))
|
|
123
|
+
) {
|
|
124
|
+
throw new Error(`Not a workspace member: ${target}`)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return current
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const parent = dirname(current)
|
|
131
|
+
|
|
132
|
+
if (parent === current) return target
|
|
133
|
+
current = parent
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function packageManager(root: string) {
|
|
138
|
+
const manifest = await readPackage(root)
|
|
139
|
+
const declared = manifest.packageManager
|
|
140
|
+
const found = new Set<PackageManager>()
|
|
141
|
+
|
|
142
|
+
if (declared !== undefined) {
|
|
143
|
+
if (!isString(declared)) {
|
|
144
|
+
throw new Error(`Package manager declaration is invalid in ${root}`)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const name = declared.split("@")[0]
|
|
148
|
+
|
|
149
|
+
if (
|
|
150
|
+
name !== "bun" &&
|
|
151
|
+
name !== "npm" &&
|
|
152
|
+
name !== "pnpm" &&
|
|
153
|
+
name !== "yarn"
|
|
154
|
+
) {
|
|
155
|
+
throw new Error(`Package manager is unsupported: ${declared}`)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
found.add(name)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const locks: [PackageManager, string][] = [
|
|
162
|
+
["bun", "bun.lock"],
|
|
163
|
+
["bun", "bun.lockb"],
|
|
164
|
+
["npm", "package-lock.json"],
|
|
165
|
+
["pnpm", "pnpm-lock.yaml"],
|
|
166
|
+
["yarn", "yarn.lock"],
|
|
167
|
+
]
|
|
168
|
+
|
|
169
|
+
for (const [manager, path] of locks) {
|
|
170
|
+
if (await existingFile(root, path)) found.add(manager)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (found.size > 1) {
|
|
174
|
+
throw new Error(
|
|
175
|
+
`Package manager declarations and lockfiles conflict in ${root}`,
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return [...found][0]
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export async function packageRunner(
|
|
183
|
+
root: string,
|
|
184
|
+
run?: InstallOptions["run"],
|
|
185
|
+
): Promise<NonNullable<InstallOptions["run"]>> {
|
|
186
|
+
const target = await realpath(root)
|
|
187
|
+
const owner = await workspaceRoot(target)
|
|
188
|
+
const local = await packageManager(target)
|
|
189
|
+
const workspace = owner === target ? local : await packageManager(owner)
|
|
190
|
+
|
|
191
|
+
if (local && workspace && local !== workspace) {
|
|
192
|
+
throw new Error("Package manager differs between target and workspace root")
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const manager = local ?? workspace ?? "bun"
|
|
196
|
+
|
|
197
|
+
if (run) return run
|
|
198
|
+
|
|
199
|
+
if (manager === "bun") return runBun
|
|
200
|
+
|
|
201
|
+
return async function runPackage(args: string[], cwd: string) {
|
|
202
|
+
const command =
|
|
203
|
+
manager === "npm" && args[0] === "add" ? "install" : args[0]!
|
|
204
|
+
|
|
205
|
+
const flags = args.slice(1).map((argument) => {
|
|
206
|
+
if (argument === "-d") return "-D"
|
|
207
|
+
|
|
208
|
+
return manager === "npm"
|
|
209
|
+
? argument.replace(/@workspace:\*$/, "@*")
|
|
210
|
+
: argument
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
const child = Bun.spawn([manager, command, ...flags], {
|
|
214
|
+
cwd,
|
|
215
|
+
stdout: "inherit",
|
|
216
|
+
stderr: "inherit",
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
if ((await child.exited) !== 0) {
|
|
220
|
+
throw new Error(`${manager} ${command} failed`)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|