@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/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
+ }