yummaui 0.7.0 → 0.8.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/LICENSE +21 -0
- package/README.md +19 -19
- package/dist/cli.mjs +1060 -36
- package/package.json +22 -25
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yumma CSS
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
[](https://www.yummaui.com)
|
|
2
2
|
|
|
3
3
|
# Yumma UI
|
|
4
4
|
|
|
5
|
-
Add [Yumma UI](https://
|
|
5
|
+
Add [Yumma UI](https://www.yummaui.com) components to your project, from the terminal.
|
|
6
6
|
|
|
7
7
|
## Usage
|
|
8
8
|
|
|
@@ -26,7 +26,7 @@ Detects your framework, package manager and import alias, then writes a `yummaui
|
|
|
26
26
|
{
|
|
27
27
|
"componentsDir": "components/ui",
|
|
28
28
|
"alias": "@/components/ui",
|
|
29
|
-
"registry": "https://
|
|
29
|
+
"registry": "https://www.yummaui.com/ui/registry"
|
|
30
30
|
}
|
|
31
31
|
```
|
|
32
32
|
|
|
@@ -47,13 +47,13 @@ pnpm dlx yummaui add
|
|
|
47
47
|
pnpm dlx yummaui add button --style compact --radius small
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
| Option
|
|
51
|
-
|
|
52
|
-
| `-a, --all`
|
|
53
|
-
| `--style <name>`
|
|
54
|
-
| `--radius <step>`
|
|
55
|
-
| `--overwrite`
|
|
56
|
-
| `-y, --yes`
|
|
50
|
+
| Option | |
|
|
51
|
+
| ----------------- | --------------------------------------------- |
|
|
52
|
+
| `-a, --all` | Add every component |
|
|
53
|
+
| `--style <name>` | `soft`, `compact` or `squircle` |
|
|
54
|
+
| `--radius <step>` | `none`, `small`, `medium`, `large` or `extra` |
|
|
55
|
+
| `--overwrite` | Replace files that already exist |
|
|
56
|
+
| `-y, --yes` | Skip prompts and take the defaults |
|
|
57
57
|
|
|
58
58
|
With no names, `add` opens a list to search and pick from.
|
|
59
59
|
|
|
@@ -77,10 +77,10 @@ pnpm dlx yummaui prune # list them
|
|
|
77
77
|
pnpm dlx yummaui prune --write # delete them, after confirming
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
-
| Option |
|
|
81
|
-
|
|
82
|
-
| `--write` | Delete, instead of only listing
|
|
83
|
-
| `-y, --yes` | Skip the confirmation
|
|
80
|
+
| Option | |
|
|
81
|
+
| ----------- | ------------------------------- |
|
|
82
|
+
| `--write` | Delete, instead of only listing |
|
|
83
|
+
| `-y, --yes` | Skip the confirmation |
|
|
84
84
|
|
|
85
85
|
A component can import another, so "is anything importing this file" would
|
|
86
86
|
keep a whole unused chain alive. `prune` asks whether a file is reachable from
|
|
@@ -88,13 +88,13 @@ outside `componentsDir` instead.
|
|
|
88
88
|
|
|
89
89
|
## Registry
|
|
90
90
|
|
|
91
|
-
The CLI reads a static JSON registry published by
|
|
91
|
+
The CLI reads a static JSON registry published by yummaui.com:
|
|
92
92
|
|
|
93
93
|
```
|
|
94
|
-
https://
|
|
95
|
-
https://
|
|
96
|
-
https://
|
|
97
|
-
https://
|
|
94
|
+
https://www.yummaui.com/ui/registry/index.json every component
|
|
95
|
+
https://www.yummaui.com/ui/registry/<id>.json one component's source and dependencies
|
|
96
|
+
https://www.yummaui.com/ui/registry/styles.json the styles and the radius steps each takes
|
|
97
|
+
https://www.yummaui.com/ui/registry/<style>-<radius>/<id>.json the same, rewritten for a style
|
|
98
98
|
```
|
|
99
99
|
|
|
100
100
|
## License
|
package/dist/cli.mjs
CHANGED
|
@@ -1,44 +1,1068 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
import { parseArgs, styleText } from "node:util";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
5
|
+
import * as p from "@clack/prompts";
|
|
6
|
+
import { distance } from "fastest-levenshtein";
|
|
7
|
+
import { spawn } from "node:child_process";
|
|
8
|
+
import { generateCode, loadFile } from "magicast";
|
|
9
|
+
import { addVitePlugin, getConfigFromVariableDeclaration, getDefaultExportOptions } from "magicast/helpers";
|
|
10
|
+
//#region package.json
|
|
11
|
+
var version = "0.8.0";
|
|
12
|
+
//#endregion
|
|
13
|
+
//#region src/registry.ts
|
|
14
|
+
const DEFAULT_REGISTRY = "https://www.yummaui.com/ui/registry";
|
|
15
|
+
const LEGACY_REGISTRY = /^https:\/\/(www\.)?yummacss\.com\/ui\/(r|registry)\/?$/;
|
|
16
|
+
/** The registry a config names, with an earlier default read as today's. */
|
|
17
|
+
function registryUrl(stored) {
|
|
18
|
+
return !stored || LEGACY_REGISTRY.test(stored) ? DEFAULT_REGISTRY : stored;
|
|
19
|
+
}
|
|
20
|
+
var RegistryError = class extends Error {};
|
|
21
|
+
async function getJson(url) {
|
|
22
|
+
let res;
|
|
23
|
+
try {
|
|
24
|
+
res = await fetch(url, { headers: { accept: "application/json" } });
|
|
25
|
+
} catch (cause) {
|
|
26
|
+
throw new RegistryError(`Could not reach the registry at ${url}. Are you online?`, { cause });
|
|
27
|
+
}
|
|
28
|
+
if (res.status === 404) throw new RegistryError(`Not found: ${url}`);
|
|
29
|
+
if (!res.ok) throw new RegistryError(`Registry returned ${res.status} for ${url}`);
|
|
30
|
+
try {
|
|
31
|
+
return await res.json();
|
|
32
|
+
} catch (cause) {
|
|
33
|
+
throw new RegistryError(`Registry sent invalid JSON from ${url}`, { cause });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function fetchIndex(registry) {
|
|
37
|
+
return getJson(`${registry}/index.json`);
|
|
38
|
+
}
|
|
39
|
+
function fetchItem(registry, id) {
|
|
40
|
+
return getJson(`${registry}/${id}.json`);
|
|
41
|
+
}
|
|
42
|
+
function fetchStyles(registry) {
|
|
43
|
+
return getJson(`${registry}/styles.json`);
|
|
44
|
+
}
|
|
45
|
+
function resolveStyle(table, style, radius) {
|
|
46
|
+
const name = style ?? table.default;
|
|
47
|
+
const spec = table.styles[name];
|
|
48
|
+
if (!spec) return { error: `There is no ${name} style. Try one of: ${Object.keys(table.styles).join(", ")}.` };
|
|
49
|
+
const step = radius ?? spec.radius;
|
|
50
|
+
if (!spec.allow.includes(step)) return { error: `${spec.refused[step] ?? `${spec.name} does not take ${step}.`} ${spec.name} takes: ${spec.allow.join(", ")}.` };
|
|
51
|
+
return { folder: `${name}-${step}` };
|
|
52
|
+
}
|
|
53
|
+
//#endregion
|
|
54
|
+
//#region src/project.ts
|
|
55
|
+
const CONFIG_FILE = "yummaui.json";
|
|
56
|
+
const CSS_CONFIG_FILE = "yumma.config.mjs";
|
|
57
|
+
const DOCS = "https://yummacss.com/docs/installation";
|
|
58
|
+
const UI_DOCS = "https://www.yummaui.com/docs/installation";
|
|
59
|
+
const MARKER = "@yummacss;";
|
|
60
|
+
const LOCKFILES = {
|
|
61
|
+
"pnpm-lock.yaml": "pnpm",
|
|
62
|
+
"bun.lockb": "bun",
|
|
63
|
+
"bun.lock": "bun",
|
|
64
|
+
"yarn.lock": "yarn",
|
|
65
|
+
"package-lock.json": "npm"
|
|
66
|
+
};
|
|
67
|
+
function findProjectRoot(from = process.cwd()) {
|
|
68
|
+
let dir = resolve(from);
|
|
69
|
+
for (;;) {
|
|
70
|
+
if (existsSync(join(dir, "package.json"))) return dir;
|
|
71
|
+
const parent = dirname(dir);
|
|
72
|
+
if (parent === dir) return null;
|
|
73
|
+
dir = parent;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function readPackageJson(root) {
|
|
77
|
+
try {
|
|
78
|
+
return JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
79
|
+
} catch {
|
|
80
|
+
return {};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function detectPackageManager(root) {
|
|
84
|
+
for (const [lockfile, pm] of Object.entries(LOCKFILES)) if (existsSync(join(root, lockfile))) return pm;
|
|
85
|
+
const field = readPackageJson(root).packageManager;
|
|
86
|
+
if (typeof field === "string") {
|
|
87
|
+
const name = field.split("@")[0];
|
|
88
|
+
if (name === "pnpm" || name === "npm" || name === "yarn" || name === "bun") return name;
|
|
89
|
+
}
|
|
90
|
+
return "npm";
|
|
91
|
+
}
|
|
92
|
+
function detectFramework(root) {
|
|
93
|
+
const pkg = readPackageJson(root);
|
|
94
|
+
const deps = {
|
|
95
|
+
...pkg.dependencies,
|
|
96
|
+
...pkg.devDependencies
|
|
97
|
+
};
|
|
98
|
+
if (deps.next) return ["app", "src/app"].some((d) => existsSync(join(root, d))) ? "Next.js (App Router)" : "Next.js (Pages Router)";
|
|
99
|
+
if (deps.astro) return "Astro";
|
|
100
|
+
if (deps["react-router"] || deps["@remix-run/react"]) return "React Router";
|
|
101
|
+
if (deps.vite) return "Vite";
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
function detectAlias(root) {
|
|
105
|
+
for (const file of ["tsconfig.json", "jsconfig.json"]) {
|
|
106
|
+
const path = join(root, file);
|
|
107
|
+
if (!existsSync(path)) continue;
|
|
108
|
+
try {
|
|
109
|
+
const raw = readFileSync(path, "utf8").replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|\s)\/\/.*$/gm, "$1");
|
|
110
|
+
if ((JSON.parse(raw)?.compilerOptions?.paths)?.["@/*"]) return "@";
|
|
111
|
+
} catch {}
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
function configPath(root) {
|
|
116
|
+
return join(root, CONFIG_FILE);
|
|
117
|
+
}
|
|
118
|
+
function readConfig(root) {
|
|
119
|
+
const path = configPath(root);
|
|
120
|
+
if (!existsSync(path)) return null;
|
|
121
|
+
try {
|
|
122
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
123
|
+
return {
|
|
124
|
+
componentsDir: parsed.componentsDir ?? "components/ui",
|
|
125
|
+
alias: parsed.alias ?? null,
|
|
126
|
+
registry: registryUrl(parsed.registry),
|
|
127
|
+
...parsed.style ? { style: parsed.style } : {},
|
|
128
|
+
...parsed.radius ? { radius: parsed.radius } : {}
|
|
129
|
+
};
|
|
130
|
+
} catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function writeConfig(root, config) {
|
|
135
|
+
writeFileSync(configPath(root), `${JSON.stringify(config, null, " ")}\n`);
|
|
136
|
+
}
|
|
137
|
+
function missingDependencies(root, deps) {
|
|
138
|
+
const pkg = readPackageJson(root);
|
|
139
|
+
const installed = /* @__PURE__ */ new Set([
|
|
140
|
+
...Object.keys(pkg.dependencies ?? {}),
|
|
141
|
+
...Object.keys(pkg.devDependencies ?? {}),
|
|
142
|
+
...Object.keys(pkg.peerDependencies ?? {})
|
|
143
|
+
]);
|
|
144
|
+
return deps.filter((d) => !installed.has(d.name));
|
|
145
|
+
}
|
|
146
|
+
function installCommand(pm, specs, dev = false) {
|
|
147
|
+
return {
|
|
148
|
+
command: pm,
|
|
149
|
+
args: [
|
|
150
|
+
pm === "npm" ? "install" : "add",
|
|
151
|
+
...dev ? ["-D"] : [],
|
|
152
|
+
...specs
|
|
153
|
+
]
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function runner(root = findProjectRoot()) {
|
|
157
|
+
switch (root ? detectPackageManager(root) : "npm") {
|
|
158
|
+
case "pnpm": return "pnpm dlx yummaui";
|
|
159
|
+
case "yarn": return "yarn dlx yummaui";
|
|
160
|
+
case "bun": return "bunx yummaui";
|
|
161
|
+
default: return "npx yummaui";
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
//#endregion
|
|
165
|
+
//#region src/ui.ts
|
|
166
|
+
const style = (format) => (text) => styleText(format, text);
|
|
167
|
+
const depth = process.stdout.hasColors?.(2 ** 24) ? 24 : process.stdout.hasColors?.() ? 4 : 0;
|
|
168
|
+
const rgb = (code, fallback) => (text) => depth === 24 ? `\x1b[${code}m${text}\x1b[39m` : styleText(fallback, text);
|
|
169
|
+
const ACCENT = "38;2;190;198;242";
|
|
170
|
+
const WARNING = "38;2;225;212;168";
|
|
171
|
+
const SUCCESS = "38;2;168;225;173";
|
|
172
|
+
const DANGER = "38;2;225;168;168";
|
|
173
|
+
const c = {
|
|
174
|
+
bold: style("bold"),
|
|
175
|
+
dim: style("dim"),
|
|
176
|
+
accent: rgb(ACCENT, "blue"),
|
|
177
|
+
success: rgb(SUCCESS, "green"),
|
|
178
|
+
warning: rgb(WARNING, "yellow"),
|
|
179
|
+
danger: rgb(DANGER, "red"),
|
|
180
|
+
badge: (text) => depth === 24 ? `\x1b[1;97;48;2;76;95;199m${text}\x1b[0m` : styleText([
|
|
181
|
+
"bold",
|
|
182
|
+
"whiteBright",
|
|
183
|
+
"bgBlue"
|
|
184
|
+
], text)
|
|
185
|
+
};
|
|
186
|
+
const REPAINT = {
|
|
187
|
+
"31": DANGER,
|
|
188
|
+
"32": SUCCESS,
|
|
189
|
+
"33": WARNING,
|
|
190
|
+
"35": ACCENT,
|
|
191
|
+
"36": ACCENT
|
|
192
|
+
};
|
|
193
|
+
const ANSI_COLOR = new RegExp(`${String.fromCharCode(27)}\\[(3[1-6])m`, "g");
|
|
194
|
+
function repaint() {
|
|
195
|
+
if (depth !== 24) return;
|
|
196
|
+
const write = process.stdout.write.bind(process.stdout);
|
|
197
|
+
process.stdout.write = ((chunk, ...rest) => write(typeof chunk === "string" ? chunk.replace(ANSI_COLOR, (code, n) => REPAINT[n] ? `\x1b[${REPAINT[n]}m` : code) : chunk, ...rest));
|
|
198
|
+
}
|
|
199
|
+
const plural = (n, word) => `${n} ${word}${n === 1 ? "" : word.endsWith("s") ? "es" : "s"}`;
|
|
200
|
+
const WIDTH = 11;
|
|
201
|
+
function tagged(stage, text) {
|
|
202
|
+
const [first, ...rest] = text.split("\n");
|
|
203
|
+
return [`${c.dim(stage.padEnd(WIDTH))}${first}`, ...rest.map((line) => `${" ".repeat(WIDTH)}${line}`)].join("\n");
|
|
204
|
+
}
|
|
205
|
+
const line = (symbol) => (stage, text) => p.log.message(tagged(stage, text), {
|
|
206
|
+
symbol,
|
|
207
|
+
spacing: 0
|
|
208
|
+
});
|
|
209
|
+
const say = {
|
|
210
|
+
done: line(c.success(p.S_STEP_SUBMIT)),
|
|
211
|
+
info: line(p.S_INFO),
|
|
212
|
+
warn: line(c.warning(p.S_WARN)),
|
|
213
|
+
error: line(c.danger(p.S_ERROR))
|
|
214
|
+
};
|
|
215
|
+
const tag = tagged;
|
|
216
|
+
function intro() {
|
|
217
|
+
p.intro(`${depth ? c.badge(" yummaui ") : "yummaui"} ${c.dim(version)}`);
|
|
218
|
+
}
|
|
219
|
+
function gap() {
|
|
220
|
+
p.log.message("", {
|
|
221
|
+
symbol: c.dim(p.S_BAR),
|
|
222
|
+
spacing: 0
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
function outro(stage, text) {
|
|
226
|
+
p.outro(tagged(stage, text));
|
|
227
|
+
}
|
|
228
|
+
function fail(error) {
|
|
229
|
+
p.cancel(tagged("error", error instanceof Error ? error.message : String(error)));
|
|
230
|
+
return 1;
|
|
231
|
+
}
|
|
232
|
+
function cancelled() {
|
|
233
|
+
p.cancel(tagged("cancelled", ""));
|
|
234
|
+
return 1;
|
|
235
|
+
}
|
|
236
|
+
function project() {
|
|
237
|
+
const root = findProjectRoot();
|
|
238
|
+
if (!root) return m.project.missing;
|
|
239
|
+
const config = readConfig(root);
|
|
240
|
+
if (!config) return m.project.noConfig(runner(root));
|
|
241
|
+
return {
|
|
242
|
+
root,
|
|
243
|
+
config
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
async function loadIndex(registry) {
|
|
247
|
+
const s = p.spinner();
|
|
248
|
+
s.start(tagged("registry", m.registry.reading));
|
|
249
|
+
try {
|
|
250
|
+
const index = await fetchIndex(registry);
|
|
251
|
+
s.stop(tagged("registry", m.registry.found(index.components.length)));
|
|
252
|
+
return index;
|
|
253
|
+
} catch (error) {
|
|
254
|
+
s.error(tagged("registry", m.registry.unavailable));
|
|
255
|
+
throw error;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
//#endregion
|
|
259
|
+
//#region src/messages.ts
|
|
260
|
+
const m = {
|
|
261
|
+
cli: {
|
|
262
|
+
unknownOption: (option = "") => `Unknown option ${option}.`,
|
|
263
|
+
unknownCommand: (command = "") => `Unknown command "${command}".`,
|
|
264
|
+
seeHelp: (run) => `Run ${run} --help.`
|
|
265
|
+
},
|
|
266
|
+
help: (run, version) => {
|
|
267
|
+
const row = (name, text) => ` ${c.accent(name.padEnd(24))}${text}`;
|
|
268
|
+
return `
|
|
269
|
+
${c.bold("yummaui")} ${c.dim(`v${version}`)} Copies Yumma UI components into your project.
|
|
6
270
|
|
|
7
|
-
${
|
|
271
|
+
${c.bold("Usage")} ${run} <command> [options]
|
|
8
272
|
|
|
9
|
-
${
|
|
10
|
-
${
|
|
11
|
-
${
|
|
12
|
-
${
|
|
13
|
-
${
|
|
273
|
+
${c.bold("Commands")}
|
|
274
|
+
${row("init", "Write yummaui.json and set up Yumma CSS")}
|
|
275
|
+
${row("add [components]", "Copy components in, or pick from a list")}
|
|
276
|
+
${row("list", "Show every component")}
|
|
277
|
+
${row("prune", "Find components nothing uses")}
|
|
14
278
|
|
|
15
|
-
${
|
|
16
|
-
${
|
|
17
|
-
${
|
|
18
|
-
${
|
|
19
|
-
${
|
|
20
|
-
${
|
|
21
|
-
${
|
|
22
|
-
${
|
|
279
|
+
${c.bold("Options")}
|
|
280
|
+
${row("-a, --all", "add: every component")}
|
|
281
|
+
${row("--style <name>", "add: soft, compact or squircle")}
|
|
282
|
+
${row("--radius <step>", "add: none, small, medium, large or extra")}
|
|
283
|
+
${row("--overwrite", "add: replace files that exist")}
|
|
284
|
+
${row("--write", "prune: delete what it finds")}
|
|
285
|
+
${row("--force", "init: replace yummaui.json")}
|
|
286
|
+
${row("-y, --yes", "Skip prompts and take the defaults")}
|
|
23
287
|
|
|
24
|
-
${
|
|
25
|
-
${
|
|
26
|
-
${
|
|
27
|
-
${
|
|
28
|
-
|
|
29
|
-
|
|
288
|
+
${c.bold("Examples")}
|
|
289
|
+
${run} add button tooltip
|
|
290
|
+
${run} add button --style compact --radius small
|
|
291
|
+
${run} prune --write
|
|
292
|
+
`;
|
|
293
|
+
},
|
|
294
|
+
project: {
|
|
295
|
+
missing: "No package.json found. Run this inside a project.",
|
|
296
|
+
noConfig: (run) => `No ${CONFIG_FILE} found. Run ${c.accent(`${run} init`)} first.`
|
|
297
|
+
},
|
|
298
|
+
registry: {
|
|
299
|
+
reading: "Reading",
|
|
300
|
+
found: (n) => plural(n, "component"),
|
|
301
|
+
unavailable: "Unavailable"
|
|
302
|
+
},
|
|
303
|
+
init: {
|
|
304
|
+
exists: () => `${CONFIG_FILE} exists. ${c.accent("--force")} replaces it.`,
|
|
305
|
+
detected: (framework, pm, alias) => `${framework ?? "No framework"} with ${pm}${alias ? `, imports through ${alias}/` : ""}`,
|
|
306
|
+
where: "Where do components go?",
|
|
307
|
+
useAlias: (alias) => `Import them through ${c.bold(`${alias}/`)}?`
|
|
308
|
+
},
|
|
309
|
+
add: {
|
|
310
|
+
nameOne: (run) => `Name a component: ${run} add button`,
|
|
311
|
+
pick: "Which components?",
|
|
312
|
+
search: "Type to search",
|
|
313
|
+
unknown: (name, near, run) => `There is no ${c.bold(name)} component. ${near.length ? `Did you mean ${near.join(", ")}?` : `${run} list shows them all.`}`,
|
|
314
|
+
empty: (id) => `${id} has no files.`,
|
|
315
|
+
replace: (file) => `${file} exists. Replace it?`,
|
|
316
|
+
kept: (file) => `${file} ${c.dim("already there")}`,
|
|
317
|
+
nothing: "Nothing written. --overwrite replaces them.",
|
|
318
|
+
next: (from) => `Import from ${c.accent(from)}`
|
|
319
|
+
},
|
|
320
|
+
install: {
|
|
321
|
+
ask: (packages, pm) => `${packages.join(", ")} with ${pm}?`,
|
|
322
|
+
yourself: (command) => c.accent(command),
|
|
323
|
+
failed: (pm) => `${pm} could not install them.`,
|
|
324
|
+
running: (pm) => `Running ${pm}`,
|
|
325
|
+
noStart: (command) => `Could not start ${command}.`,
|
|
326
|
+
exited: (command, code) => `${command} exited with ${code}`
|
|
327
|
+
},
|
|
328
|
+
setup: {
|
|
329
|
+
ask: "Yumma CSS is not set up here. Set it up now?",
|
|
330
|
+
unstyled: () => `Yumma CSS is not set up here, so components render unstyled\n${c.accent(DOCS)}`,
|
|
331
|
+
declined: () => `Components render unstyled until it is\n${c.accent(DOCS)}`,
|
|
332
|
+
docs: () => c.accent(DOCS),
|
|
333
|
+
registered: (file, plugin) => `${file} registers ${plugin}`,
|
|
334
|
+
marked: (file) => `${file} starts with ${MARKER}`,
|
|
335
|
+
noVite: "Add yummacss() from @yummacss/vite to your Vite plugins",
|
|
336
|
+
noBuild: "Add @yummacss/vite or @yummacss/postcss to your build",
|
|
337
|
+
noStylesheet: `Add ${MARKER} to the stylesheet your app loads`,
|
|
338
|
+
failed: (file) => `Could not change ${file}. The docs have the line to add.`,
|
|
339
|
+
missingTheme: (names) => `Add these to the theme in yumma.config.mjs\n${names.join(", ")}\n${c.accent(UI_DOCS)}`,
|
|
340
|
+
themeAdded: (file, names) => `${file} adds ${names.join(", ")}`,
|
|
341
|
+
ownColors: (names) => `yumma.config.mjs already defines ${names.join(", ")}, so components use your colors`
|
|
342
|
+
},
|
|
343
|
+
list: { next: (run) => `${c.accent(`${run} add <component>`)} ${c.dim("yummaui.com")}` },
|
|
344
|
+
prune: {
|
|
345
|
+
clean: (kept, scanned) => `Nothing to prune. All ${kept} in use, across ${plural(scanned, "file")}.`,
|
|
346
|
+
hidden: (n) => `${plural(n, "file")} build an import at runtime, so what they load cannot be seen`,
|
|
347
|
+
next: (run) => `Delete them: ${c.accent(`${run} prune --write`)}`,
|
|
348
|
+
ask: (n) => `${plural(n, "file")}?`,
|
|
349
|
+
kept: "Nothing deleted.",
|
|
350
|
+
deleted: (n) => `${plural(n, "file")} deleted.`
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
//#endregion
|
|
354
|
+
//#region src/install.ts
|
|
355
|
+
function installPackages(root, pm, specs, dev = false) {
|
|
356
|
+
const packages = specs.map((spec) => spec.slice(0, spec.lastIndexOf("@")));
|
|
357
|
+
const { command, args } = installCommand(pm, specs, dev);
|
|
358
|
+
const log = p.taskLog({
|
|
359
|
+
title: tag("install", m.install.running(pm)),
|
|
360
|
+
limit: 8
|
|
361
|
+
});
|
|
362
|
+
const child = spawn(command, args, {
|
|
363
|
+
cwd: root,
|
|
364
|
+
shell: process.platform === "win32"
|
|
365
|
+
});
|
|
366
|
+
for (const stream of [child.stdout, child.stderr]) stream.on("data", (chunk) => {
|
|
367
|
+
for (const line of chunk.toString().split(/\r?\n/)) if (line.trim()) log.message(line);
|
|
368
|
+
});
|
|
369
|
+
return new Promise((resolve) => {
|
|
370
|
+
child.on("error", () => {
|
|
371
|
+
log.error(tag("install", m.install.noStart(command)));
|
|
372
|
+
resolve(false);
|
|
373
|
+
});
|
|
374
|
+
child.on("close", (code) => {
|
|
375
|
+
if (code === 0) log.success(tag("install", packages.join(", ")));
|
|
376
|
+
else log.error(tag("install", m.install.exited(command, code)), { showLog: true });
|
|
377
|
+
resolve(code === 0);
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
//#endregion
|
|
382
|
+
//#region src/setup.ts
|
|
383
|
+
const EXTENSIONS$1 = "{js,jsx,ts,tsx,mdx}";
|
|
384
|
+
const first = (root, paths) => paths.find((path) => existsSync(join(root, path))) ?? null;
|
|
385
|
+
const has = (root, path, text) => readFileSync(join(root, path), "utf8").includes(text);
|
|
386
|
+
function bundlerFor(framework) {
|
|
387
|
+
if (!framework) return null;
|
|
388
|
+
return framework.startsWith("Next.js") ? "postcss" : "vite";
|
|
389
|
+
}
|
|
390
|
+
function sources(root, componentsDir) {
|
|
391
|
+
const top = componentsDir.split("/")[0] ?? "src";
|
|
392
|
+
const dirs = existsSync(join(root, "src")) ? ["src"] : [
|
|
393
|
+
"app",
|
|
394
|
+
"pages",
|
|
395
|
+
"components",
|
|
396
|
+
top
|
|
397
|
+
].filter((dir) => existsSync(join(root, dir)));
|
|
398
|
+
if (!dirs.includes(top)) dirs.push(top);
|
|
399
|
+
return [...new Set(dirs)].map((dir) => `./${dir}/**/*.${EXTENSIONS$1}`);
|
|
400
|
+
}
|
|
401
|
+
function stylesheet(root, bundler) {
|
|
402
|
+
if (bundler === "postcss") return first(root, [
|
|
403
|
+
"app/globals.css",
|
|
404
|
+
"src/app/globals.css",
|
|
405
|
+
"styles/globals.css",
|
|
406
|
+
"src/styles/globals.css"
|
|
407
|
+
]);
|
|
408
|
+
const entry = first(root, ["main", "index"].flatMap((name) => [
|
|
409
|
+
"tsx",
|
|
410
|
+
"ts",
|
|
411
|
+
"jsx",
|
|
412
|
+
"js"
|
|
413
|
+
].map((ext) => `src/${name}.${ext}`)));
|
|
414
|
+
if (entry) {
|
|
415
|
+
const source = readFileSync(join(root, entry), "utf8");
|
|
416
|
+
const css = /import\s+["'](\.{1,2}\/[^"']+\.css)["']/.exec(source)?.[1];
|
|
417
|
+
if (css) {
|
|
418
|
+
const path = relative(root, join(root, dirname(entry), css));
|
|
419
|
+
if (existsSync(join(root, path))) return path.replace(/\\/g, "/");
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return first(root, [
|
|
423
|
+
"src/index.css",
|
|
424
|
+
"src/style.css",
|
|
425
|
+
"src/styles.css",
|
|
426
|
+
"src/App.css"
|
|
427
|
+
]);
|
|
428
|
+
}
|
|
429
|
+
const quoted = (key) => /^[a-z]+$/.test(key) ? key : JSON.stringify(key);
|
|
430
|
+
const literal = (value) => typeof value === "string" ? JSON.stringify(value) : `{ light: ${JSON.stringify(value.light)}, dark: ${JSON.stringify(value.dark)} }`;
|
|
431
|
+
const block = (name, table) => Object.keys(table).length ? ` ${name}: {\n${Object.entries(table).map(([key, value]) => ` ${quoted(key)}: ${literal(value)},`).join("\n")}\n },\n` : "";
|
|
432
|
+
const KINDS = [
|
|
433
|
+
"states",
|
|
434
|
+
"easings",
|
|
435
|
+
"keyframes",
|
|
436
|
+
"colors"
|
|
437
|
+
];
|
|
438
|
+
function configSource(globs, theme) {
|
|
439
|
+
const body = theme ? KINDS.map((kind) => block(kind, theme[kind] ?? {})).join("") : "";
|
|
440
|
+
return `import { defineConfig } from "yummacss";
|
|
30
441
|
|
|
31
442
|
export default defineConfig({
|
|
32
|
-
source: [${
|
|
33
|
-
${
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
443
|
+
source: [${globs.map((glob) => `"${glob}"`).join(", ")}],
|
|
444
|
+
${body ? ` theme: {\n${body} },\n` : ""}});
|
|
445
|
+
`;
|
|
446
|
+
}
|
|
447
|
+
function missingTheme(source, theme) {
|
|
448
|
+
return Object.keys({
|
|
449
|
+
...theme?.states,
|
|
450
|
+
...theme?.easings,
|
|
451
|
+
...theme?.keyframes,
|
|
452
|
+
...theme?.colors
|
|
453
|
+
}).filter((name) => !new RegExp(`(?<![\\w-])["']?${name}["']?\\s*:`).test(source));
|
|
454
|
+
}
|
|
455
|
+
const save = (mod, path) => writeFileSync(path, generateCode(mod).code);
|
|
456
|
+
function editExport(mod, change) {
|
|
457
|
+
const options = getDefaultExportOptions(mod);
|
|
458
|
+
const { config, declaration } = options.$type === "identifier" ? getConfigFromVariableDeclaration(mod) : {
|
|
459
|
+
config: options,
|
|
460
|
+
declaration: null
|
|
461
|
+
};
|
|
462
|
+
if (!config) throw new Error("no config object");
|
|
463
|
+
change(config);
|
|
464
|
+
if (declaration) {
|
|
465
|
+
if (declaration.init?.type !== "ObjectExpression") throw new Error("config is not an object literal");
|
|
466
|
+
declaration.init = generateCode(config).code;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
function addPostcssPlugin(mod) {
|
|
470
|
+
editExport(mod, (options) => {
|
|
471
|
+
options.plugins ??= {};
|
|
472
|
+
options.plugins["@yummacss/postcss"] = {};
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
function addTheme(mod, theme) {
|
|
476
|
+
const added = [];
|
|
477
|
+
editExport(mod, (options) => {
|
|
478
|
+
options.theme ??= {};
|
|
479
|
+
for (const kind of KINDS) {
|
|
480
|
+
const entries = Object.entries(theme[kind] ?? {});
|
|
481
|
+
if (!entries.length) continue;
|
|
482
|
+
options.theme[kind] ??= {};
|
|
483
|
+
const table = options.theme[kind];
|
|
484
|
+
for (const [name, value] of entries) {
|
|
485
|
+
if (table[name] !== void 0) continue;
|
|
486
|
+
table[name] = value;
|
|
487
|
+
added.push(name);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
return added;
|
|
492
|
+
}
|
|
493
|
+
function planSetup(root, componentsDir, theme) {
|
|
494
|
+
const pkg = readPackageJson(root);
|
|
495
|
+
const deps = /* @__PURE__ */ new Set([...Object.keys(pkg.dependencies ?? {}), ...Object.keys(pkg.devDependencies ?? {})]);
|
|
496
|
+
const bundler = bundlerFor(detectFramework(root));
|
|
497
|
+
const plugin = bundler && `@yummacss/${bundler}`;
|
|
498
|
+
const plan = {
|
|
499
|
+
packages: [],
|
|
500
|
+
steps: [],
|
|
501
|
+
manual: [],
|
|
502
|
+
notes: [],
|
|
503
|
+
theme: null
|
|
504
|
+
};
|
|
505
|
+
if (!deps.has("yummacss")) plan.packages.push("yummacss");
|
|
506
|
+
if (plugin && !deps.has(plugin)) plan.packages.push(plugin);
|
|
507
|
+
if (bundler === "postcss") {
|
|
508
|
+
const file = first(root, [
|
|
509
|
+
"postcss.config.mjs",
|
|
510
|
+
"postcss.config.js",
|
|
511
|
+
"postcss.config.cjs"
|
|
512
|
+
]);
|
|
513
|
+
if (!file) plan.steps.push({
|
|
514
|
+
stage: "write",
|
|
515
|
+
file: "postcss.config.mjs",
|
|
516
|
+
apply: async () => {
|
|
517
|
+
writeFileSync(join(root, "postcss.config.mjs"), `export default {\n plugins: {\n "@yummacss/postcss": {},\n },\n};\n`);
|
|
518
|
+
return "postcss.config.mjs";
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
else if (!has(root, file, "@yummacss/postcss")) plan.steps.push({
|
|
522
|
+
stage: "edit",
|
|
523
|
+
file,
|
|
524
|
+
apply: async () => {
|
|
525
|
+
const mod = await loadFile(join(root, file));
|
|
526
|
+
addPostcssPlugin(mod);
|
|
527
|
+
save(mod, join(root, file));
|
|
528
|
+
return m.setup.registered(file, "@yummacss/postcss");
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
} else if (bundler === "vite") {
|
|
532
|
+
const file = first(root, [
|
|
533
|
+
"ts",
|
|
534
|
+
"mts",
|
|
535
|
+
"js",
|
|
536
|
+
"mjs"
|
|
537
|
+
].map((ext) => `vite.config.${ext}`));
|
|
538
|
+
if (!file) plan.manual.push(m.setup.noVite);
|
|
539
|
+
else if (!has(root, file, "@yummacss/vite")) plan.steps.push({
|
|
540
|
+
stage: "edit",
|
|
541
|
+
file,
|
|
542
|
+
apply: async () => {
|
|
543
|
+
const mod = await loadFile(join(root, file));
|
|
544
|
+
addVitePlugin(mod, {
|
|
545
|
+
from: "@yummacss/vite",
|
|
546
|
+
constructor: "yummacss"
|
|
547
|
+
});
|
|
548
|
+
save(mod, join(root, file));
|
|
549
|
+
return m.setup.registered(file, "yummacss()");
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
} else plan.manual.push(m.setup.noBuild);
|
|
553
|
+
if (existsSync(join(root, "yumma.config.mjs"))) {
|
|
554
|
+
const missing = missingTheme(readFileSync(join(root, CSS_CONFIG_FILE), "utf8"), theme);
|
|
555
|
+
const own = Object.keys(theme?.colors ?? {}).filter((name) => !missing.includes(name));
|
|
556
|
+
if (own.length) plan.notes.push(m.setup.ownColors(own));
|
|
557
|
+
if (missing.length && theme) plan.theme = {
|
|
558
|
+
stage: "edit",
|
|
559
|
+
file: CSS_CONFIG_FILE,
|
|
560
|
+
apply: async () => {
|
|
561
|
+
const path = join(root, CSS_CONFIG_FILE);
|
|
562
|
+
const mod = await loadFile(path);
|
|
563
|
+
const added = addTheme(mod, theme);
|
|
564
|
+
save(mod, path);
|
|
565
|
+
return m.setup.themeAdded(CSS_CONFIG_FILE, added);
|
|
566
|
+
},
|
|
567
|
+
fallback: m.setup.missingTheme(missing)
|
|
568
|
+
};
|
|
569
|
+
} else plan.steps.push({
|
|
570
|
+
stage: "write",
|
|
571
|
+
file: CSS_CONFIG_FILE,
|
|
572
|
+
apply: async () => {
|
|
573
|
+
writeFileSync(join(root, CSS_CONFIG_FILE), configSource(sources(root, componentsDir), theme));
|
|
574
|
+
return CSS_CONFIG_FILE;
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
const css = bundler && stylesheet(root, bundler);
|
|
578
|
+
if (!css) plan.manual.push(m.setup.noStylesheet);
|
|
579
|
+
else if (!has(root, css, "@yummacss")) plan.steps.push({
|
|
580
|
+
stage: "edit",
|
|
581
|
+
file: css,
|
|
582
|
+
apply: async () => {
|
|
583
|
+
const path = join(root, css);
|
|
584
|
+
writeFileSync(path, `${MARKER}\n\n${readFileSync(path, "utf8")}`);
|
|
585
|
+
return m.setup.marked(css);
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
return plan;
|
|
589
|
+
}
|
|
590
|
+
async function setUpStyling(root, componentsDir, yes, theme) {
|
|
591
|
+
const plan = planSetup(root, componentsDir, theme);
|
|
592
|
+
if (plan.theme) try {
|
|
593
|
+
say.done(plan.theme.stage, await plan.theme.apply());
|
|
594
|
+
} catch {
|
|
595
|
+
say.warn("manual", plan.theme.fallback);
|
|
596
|
+
}
|
|
597
|
+
for (const line of plan.notes) say.info("theme", line);
|
|
598
|
+
if (!plan.packages.length && !plan.steps.length) {
|
|
599
|
+
for (const line of plan.manual) say.warn("check", line);
|
|
600
|
+
return true;
|
|
601
|
+
}
|
|
602
|
+
if (!yes && !process.stdout.isTTY) {
|
|
603
|
+
say.warn("check", m.setup.unstyled());
|
|
604
|
+
return true;
|
|
605
|
+
}
|
|
606
|
+
if (!yes) {
|
|
607
|
+
const answer = await p.confirm({ message: tag("setup", m.setup.ask) });
|
|
608
|
+
if (p.isCancel(answer)) {
|
|
609
|
+
cancelled();
|
|
610
|
+
return false;
|
|
611
|
+
}
|
|
612
|
+
if (!answer) {
|
|
613
|
+
say.warn("check", m.setup.declined());
|
|
614
|
+
return true;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
if (plan.packages.length) {
|
|
618
|
+
if (!await installPackages(root, detectPackageManager(root), plan.packages, true)) return false;
|
|
619
|
+
}
|
|
620
|
+
for (const step of plan.steps) try {
|
|
621
|
+
say.done(step.stage, await step.apply());
|
|
622
|
+
} catch {
|
|
623
|
+
say.warn(step.stage, m.setup.failed(step.file));
|
|
624
|
+
}
|
|
625
|
+
for (const line of plan.manual) say.warn("manual", line);
|
|
626
|
+
if (plan.manual.length) say.info("docs", m.setup.docs());
|
|
627
|
+
return true;
|
|
628
|
+
}
|
|
629
|
+
//#endregion
|
|
630
|
+
//#region src/commands/add.ts
|
|
631
|
+
function targetFileName(id, component, variant) {
|
|
632
|
+
return variant === "base" ? `${component}.tsx` : `${id}.tsx`;
|
|
633
|
+
}
|
|
634
|
+
function resolveNames(index, names, options = {}) {
|
|
635
|
+
const ids = [];
|
|
636
|
+
if (options.all) ids.push(...index.components.map((x) => x.base));
|
|
637
|
+
for (const name of names) {
|
|
638
|
+
const component = index.components.find((x) => x.component === name);
|
|
639
|
+
if (!component) return { unknown: name };
|
|
640
|
+
ids.push(component.base);
|
|
641
|
+
}
|
|
642
|
+
return { ids: [...new Set(ids)] };
|
|
643
|
+
}
|
|
644
|
+
function nearest(index, name) {
|
|
645
|
+
return index.components.map(({ component }) => ({
|
|
646
|
+
component,
|
|
647
|
+
score: component.includes(name) || name.includes(component) ? 0 : distance(component, name)
|
|
648
|
+
})).filter((x) => x.score <= 2).sort((a, b) => a.score - b.score).slice(0, 5).map((x) => x.component);
|
|
649
|
+
}
|
|
650
|
+
async function add(names, flags) {
|
|
651
|
+
intro();
|
|
652
|
+
const found = project();
|
|
653
|
+
if (typeof found === "string") return fail(found);
|
|
654
|
+
const { root, config } = found;
|
|
655
|
+
let index;
|
|
656
|
+
try {
|
|
657
|
+
index = await loadIndex(config.registry);
|
|
658
|
+
} catch (error) {
|
|
659
|
+
return fail(error);
|
|
660
|
+
}
|
|
661
|
+
if (names.length === 0 && !flags.all) {
|
|
662
|
+
if (flags.yes || !process.stdout.isTTY) return fail(m.add.nameOne(runner(root)));
|
|
663
|
+
const picked = await p.autocompleteMultiselect({
|
|
664
|
+
message: m.add.pick,
|
|
665
|
+
placeholder: m.add.search,
|
|
666
|
+
options: index.components.map((x) => ({
|
|
667
|
+
value: x.component,
|
|
668
|
+
label: x.component
|
|
669
|
+
})),
|
|
670
|
+
required: true
|
|
671
|
+
});
|
|
672
|
+
if (p.isCancel(picked)) return cancelled();
|
|
673
|
+
names = picked;
|
|
674
|
+
}
|
|
675
|
+
const resolution = resolveNames(index, names, { all: flags.all });
|
|
676
|
+
if ("unknown" in resolution) {
|
|
677
|
+
const near = nearest(index, resolution.unknown);
|
|
678
|
+
return fail(m.add.unknown(resolution.unknown, near, runner(root)));
|
|
679
|
+
}
|
|
680
|
+
let registry = config.registry;
|
|
681
|
+
const style = flags.style ?? config.style ?? null;
|
|
682
|
+
const radius = flags.radius ?? config.radius ?? null;
|
|
683
|
+
if (style || radius) try {
|
|
684
|
+
const styled = resolveStyle(await fetchStyles(config.registry), style, radius);
|
|
685
|
+
if ("error" in styled) return fail(styled.error);
|
|
686
|
+
registry = `${config.registry}/${styled.folder}`;
|
|
687
|
+
say.info("style", styled.folder);
|
|
688
|
+
} catch (error) {
|
|
689
|
+
return fail(error);
|
|
690
|
+
}
|
|
691
|
+
const items = [];
|
|
692
|
+
const seen = /* @__PURE__ */ new Set();
|
|
693
|
+
async function collect(id) {
|
|
694
|
+
if (seen.has(id)) return;
|
|
695
|
+
seen.add(id);
|
|
696
|
+
const item = await fetchItem(registry, id);
|
|
697
|
+
for (const dep of item.registryDeps) await collect(dep);
|
|
698
|
+
items.push(item);
|
|
699
|
+
}
|
|
700
|
+
try {
|
|
701
|
+
for (const id of resolution.ids) await collect(id);
|
|
702
|
+
} catch (error) {
|
|
703
|
+
return fail(error);
|
|
704
|
+
}
|
|
705
|
+
const written = [];
|
|
706
|
+
const skipped = [];
|
|
707
|
+
const deps = /* @__PURE__ */ new Map();
|
|
708
|
+
for (const item of items) {
|
|
709
|
+
const source = item.files[0];
|
|
710
|
+
if (!source) return fail(m.add.empty(item.id));
|
|
711
|
+
const dest = join(root, config.componentsDir, targetFileName(item.id, item.component, item.variant));
|
|
712
|
+
const shown = relative(root, dest).replace(/\\/g, "/");
|
|
713
|
+
if (existsSync(dest) && !flags.overwrite) {
|
|
714
|
+
if (!resolution.ids.includes(item.id) || flags.yes || !process.stdout.isTTY) {
|
|
715
|
+
skipped.push(shown);
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
const answer = await p.confirm({
|
|
719
|
+
message: tag("write", m.add.replace(shown)),
|
|
720
|
+
initialValue: false
|
|
721
|
+
});
|
|
722
|
+
if (p.isCancel(answer)) return cancelled();
|
|
723
|
+
if (!answer) {
|
|
724
|
+
skipped.push(shown);
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
729
|
+
writeFileSync(dest, source.content);
|
|
730
|
+
written.push(shown);
|
|
731
|
+
for (const dep of item.dependencies) deps.set(dep.name, dep.version);
|
|
732
|
+
}
|
|
733
|
+
if (written.length > 0) say.done("write", written.join("\n"));
|
|
734
|
+
if (skipped.length > 0) say.warn("kept", skipped.map(m.add.kept).join("\n"));
|
|
735
|
+
if (written.length === 0) {
|
|
736
|
+
outro("next", m.add.nothing);
|
|
737
|
+
return 0;
|
|
738
|
+
}
|
|
739
|
+
const missing = missingDependencies(root, [...deps].map(([name, version]) => ({
|
|
740
|
+
name,
|
|
741
|
+
version
|
|
742
|
+
})));
|
|
743
|
+
if (missing.length > 0) {
|
|
744
|
+
const pm = detectPackageManager(root);
|
|
745
|
+
const specs = missing.map((d) => `${d.name}@${d.version}`);
|
|
746
|
+
const packages = missing.map((d) => d.name);
|
|
747
|
+
let install = flags.yes === true;
|
|
748
|
+
if (!install) {
|
|
749
|
+
const answer = await p.confirm({ message: tag("install", m.install.ask(packages, pm)) });
|
|
750
|
+
if (p.isCancel(answer)) return cancelled();
|
|
751
|
+
install = answer;
|
|
752
|
+
}
|
|
753
|
+
if (!install) {
|
|
754
|
+
const { command, args } = installCommand(pm, specs);
|
|
755
|
+
say.info("install", m.install.yourself([command, ...args].join(" ")));
|
|
756
|
+
} else if (!await installPackages(root, pm, specs)) return fail(m.install.failed(pm));
|
|
757
|
+
}
|
|
758
|
+
if (!await setUpStyling(root, config.componentsDir, flags.yes === true, index.theme)) return 1;
|
|
759
|
+
outro("next", m.add.next(config.alias ?? config.componentsDir));
|
|
760
|
+
return 0;
|
|
761
|
+
}
|
|
762
|
+
//#endregion
|
|
763
|
+
//#region src/commands/init.ts
|
|
764
|
+
async function init(flags) {
|
|
765
|
+
intro();
|
|
766
|
+
gap();
|
|
767
|
+
const root = findProjectRoot();
|
|
768
|
+
if (!root) return fail(m.project.missing);
|
|
769
|
+
if (readConfig(root) && !flags.force) {
|
|
770
|
+
outro("next", m.init.exists());
|
|
771
|
+
return 0;
|
|
772
|
+
}
|
|
773
|
+
const framework = detectFramework(root);
|
|
774
|
+
const alias = detectAlias(root);
|
|
775
|
+
say.info("project", m.init.detected(framework, detectPackageManager(root), alias));
|
|
776
|
+
const fallback = existsSync(join(root, "src")) ? "src/components/ui" : "components/ui";
|
|
777
|
+
const componentsDir = flags.yes ? fallback : await p.text({
|
|
778
|
+
message: m.init.where,
|
|
779
|
+
placeholder: fallback,
|
|
780
|
+
defaultValue: fallback
|
|
781
|
+
});
|
|
782
|
+
if (p.isCancel(componentsDir)) return cancelled();
|
|
783
|
+
let useAlias = Boolean(alias);
|
|
784
|
+
if (alias && !flags.yes) {
|
|
785
|
+
const answer = await p.confirm({ message: m.init.useAlias(alias) });
|
|
786
|
+
if (p.isCancel(answer)) return cancelled();
|
|
787
|
+
useAlias = answer;
|
|
788
|
+
}
|
|
789
|
+
const config = {
|
|
790
|
+
componentsDir,
|
|
791
|
+
alias: useAlias ? `${alias}/${componentsDir}` : null,
|
|
792
|
+
registry: DEFAULT_REGISTRY
|
|
793
|
+
};
|
|
794
|
+
writeConfig(root, config);
|
|
795
|
+
say.done("write", CONFIG_FILE);
|
|
796
|
+
const theme = await fetchIndex(config.registry).then((index) => index.theme, () => void 0);
|
|
797
|
+
if (!await setUpStyling(root, componentsDir, flags.yes === true, theme)) return 1;
|
|
798
|
+
outro("next", c.accent(`${runner(root)} add`));
|
|
799
|
+
return 0;
|
|
800
|
+
}
|
|
801
|
+
//#endregion
|
|
802
|
+
//#region src/commands/list.ts
|
|
803
|
+
async function list() {
|
|
804
|
+
intro();
|
|
805
|
+
const root = findProjectRoot();
|
|
806
|
+
const registry = root && readConfig(root)?.registry || "https://www.yummaui.com/ui/registry";
|
|
807
|
+
let index;
|
|
808
|
+
try {
|
|
809
|
+
index = await loadIndex(registry);
|
|
810
|
+
} catch (error) {
|
|
811
|
+
return fail(error);
|
|
812
|
+
}
|
|
813
|
+
const names = index.components.map((x) => x.component);
|
|
814
|
+
const width = Math.max(...names.map((x) => x.length)) + 2;
|
|
815
|
+
const rows = [];
|
|
816
|
+
for (let i = 0; i < names.length; i += 3) rows.push(names.slice(i, i + 3).map((x) => x.padEnd(width)).join("").trimEnd());
|
|
817
|
+
say.done("available", rows.join("\n"));
|
|
818
|
+
outro("next", m.list.next(runner(root)));
|
|
819
|
+
return 0;
|
|
820
|
+
}
|
|
821
|
+
//#endregion
|
|
822
|
+
//#region src/prune.ts
|
|
823
|
+
const EXTENSIONS = [
|
|
824
|
+
".astro",
|
|
825
|
+
".cjs",
|
|
826
|
+
".cts",
|
|
827
|
+
".js",
|
|
828
|
+
".jsx",
|
|
829
|
+
".md",
|
|
830
|
+
".mdx",
|
|
831
|
+
".mjs",
|
|
832
|
+
".mts",
|
|
833
|
+
".svelte",
|
|
834
|
+
".ts",
|
|
835
|
+
".tsx",
|
|
836
|
+
".vue"
|
|
837
|
+
];
|
|
838
|
+
const SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git"]);
|
|
839
|
+
const SPECIFIER_PATTERNS = [
|
|
840
|
+
/\bfrom\s*["']([^"'\n]+)["']/g,
|
|
841
|
+
/\bimport\s*\(\s*["']([^"'\n]+)["']/g,
|
|
842
|
+
/\brequire\s*\(\s*["']([^"'\n]+)["']/g,
|
|
843
|
+
/\bimport\s+["']([^"'\n]+)["']/g
|
|
844
|
+
];
|
|
845
|
+
function extractSpecifiers(source) {
|
|
846
|
+
const found = /* @__PURE__ */ new Set();
|
|
847
|
+
for (const pattern of SPECIFIER_PATTERNS) {
|
|
848
|
+
pattern.lastIndex = 0;
|
|
849
|
+
for (const match of source.matchAll(pattern)) if (match[1]) found.add(match[1]);
|
|
850
|
+
}
|
|
851
|
+
return [...found];
|
|
852
|
+
}
|
|
853
|
+
const COMPUTED_SPECIFIER = /\b(?:from|import\s*\(|require\s*\()\s*`/;
|
|
854
|
+
function hasComputedSpecifier(source) {
|
|
855
|
+
return COMPUTED_SPECIFIER.test(source);
|
|
856
|
+
}
|
|
857
|
+
function normalizeKey(path) {
|
|
858
|
+
const posix = path.split(sep).join("/").replace(/^\.\//, "");
|
|
859
|
+
for (const ext of EXTENSIONS) if (posix.endsWith(ext)) return posix.slice(0, -ext.length);
|
|
860
|
+
return posix;
|
|
861
|
+
}
|
|
862
|
+
function resolveToComponent(specifier, fromFile, shape) {
|
|
863
|
+
const dir = resolve(shape.root, shape.componentsDir);
|
|
864
|
+
if (shape.alias) {
|
|
865
|
+
const prefix = `${shape.alias}/`;
|
|
866
|
+
if (specifier === shape.alias) return "index";
|
|
867
|
+
if (specifier.startsWith(prefix)) {
|
|
868
|
+
const rest = specifier.slice(prefix.length);
|
|
869
|
+
return rest ? normalizeKey(rest) : "index";
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
if (!specifier.startsWith(".")) return null;
|
|
873
|
+
const target = resolve(dirname(fromFile), specifier);
|
|
874
|
+
const rel = relative(dir, target);
|
|
875
|
+
if (!rel) return "index";
|
|
876
|
+
if (rel === ".." || rel.startsWith(`..${sep}`)) return null;
|
|
877
|
+
return normalizeKey(rel);
|
|
878
|
+
}
|
|
879
|
+
function componentKey(file, shape) {
|
|
880
|
+
return normalizeKey(relative(resolve(shape.root, shape.componentsDir), file));
|
|
881
|
+
}
|
|
882
|
+
function walk(dir, skip, out = []) {
|
|
883
|
+
let entries;
|
|
884
|
+
try {
|
|
885
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
886
|
+
} catch {
|
|
887
|
+
return out;
|
|
888
|
+
}
|
|
889
|
+
for (const entry of entries) {
|
|
890
|
+
const path = join(dir, entry.name);
|
|
891
|
+
if (entry.isDirectory()) {
|
|
892
|
+
if (SKIP_DIRS.has(entry.name) || path === skip) continue;
|
|
893
|
+
walk(path, skip, out);
|
|
894
|
+
} else if (entry.isFile()) {
|
|
895
|
+
const dot = entry.name.lastIndexOf(".");
|
|
896
|
+
if (dot > 0 && EXTENSIONS.includes(entry.name.slice(dot))) out.push(path);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
return out;
|
|
900
|
+
}
|
|
901
|
+
function installableFileNames(index) {
|
|
902
|
+
const names = /* @__PURE__ */ new Set();
|
|
903
|
+
for (const component of index.components) names.add(`${component.component}.tsx`);
|
|
904
|
+
return names;
|
|
905
|
+
}
|
|
906
|
+
function findUnused(shape, installable) {
|
|
907
|
+
const dir = resolve(shape.root, shape.componentsDir);
|
|
908
|
+
const inside = walk(dir, "");
|
|
909
|
+
const outside = walk(shape.root, dir);
|
|
910
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
911
|
+
const roots = /* @__PURE__ */ new Set();
|
|
912
|
+
const edges = /* @__PURE__ */ new Map();
|
|
913
|
+
const computed = [];
|
|
914
|
+
for (const file of inside) {
|
|
915
|
+
const key = componentKey(file, shape);
|
|
916
|
+
if (file.endsWith(".tsx") && !key.includes("/") && installable.has(`${key}.tsx`)) candidates.set(key, file);
|
|
917
|
+
else roots.add(key);
|
|
918
|
+
const source = readFileSync(file, "utf8");
|
|
919
|
+
if (hasComputedSpecifier(source)) computed.push(file);
|
|
920
|
+
const out = /* @__PURE__ */ new Set();
|
|
921
|
+
for (const specifier of extractSpecifiers(source)) {
|
|
922
|
+
const hit = resolveToComponent(specifier, file, shape);
|
|
923
|
+
if (hit) out.add(hit);
|
|
924
|
+
}
|
|
925
|
+
edges.set(key, out);
|
|
926
|
+
}
|
|
927
|
+
for (const file of outside) {
|
|
928
|
+
const source = readFileSync(file, "utf8");
|
|
929
|
+
if (hasComputedSpecifier(source)) computed.push(file);
|
|
930
|
+
for (const specifier of extractSpecifiers(source)) {
|
|
931
|
+
const hit = resolveToComponent(specifier, file, shape);
|
|
932
|
+
if (hit) roots.add(hit);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
const reached = /* @__PURE__ */ new Set();
|
|
936
|
+
const queue = [...roots];
|
|
937
|
+
while (queue.length > 0) {
|
|
938
|
+
const key = queue.pop();
|
|
939
|
+
if (reached.has(key)) continue;
|
|
940
|
+
reached.add(key);
|
|
941
|
+
for (const next of edges.get(key) ?? []) if (!reached.has(next)) queue.push(next);
|
|
942
|
+
}
|
|
943
|
+
const unused = [];
|
|
944
|
+
for (const [key, file] of candidates) if (!reached.has(key)) unused.push(file);
|
|
945
|
+
return {
|
|
946
|
+
unused: unused.sort(),
|
|
947
|
+
kept: candidates.size - unused.length,
|
|
948
|
+
foreign: inside.length - candidates.size,
|
|
949
|
+
scanned: inside.length + outside.length,
|
|
950
|
+
computed
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
//#endregion
|
|
954
|
+
//#region src/commands/prune.ts
|
|
955
|
+
async function prune(flags) {
|
|
956
|
+
intro();
|
|
957
|
+
const found = project();
|
|
958
|
+
if (typeof found === "string") return fail(found);
|
|
959
|
+
const { root, config } = found;
|
|
960
|
+
let index;
|
|
961
|
+
try {
|
|
962
|
+
index = await loadIndex(config.registry);
|
|
963
|
+
} catch (error) {
|
|
964
|
+
return fail(error);
|
|
965
|
+
}
|
|
966
|
+
const result = findUnused({
|
|
967
|
+
root,
|
|
968
|
+
componentsDir: config.componentsDir,
|
|
969
|
+
alias: config.alias
|
|
970
|
+
}, installableFileNames(index));
|
|
971
|
+
const show = (path) => relative(root, path).replace(/\\/g, "/");
|
|
972
|
+
if (result.computed.length > 0) say.warn("hidden", `${m.prune.hidden(result.computed.length)}\n${result.computed.slice(0, 5).map((f) => c.dim(show(f))).join("\n")}`);
|
|
973
|
+
if (result.unused.length === 0) {
|
|
974
|
+
outro("next", m.prune.clean(result.kept, result.scanned));
|
|
975
|
+
return 0;
|
|
976
|
+
}
|
|
977
|
+
say.warn("unused", result.unused.map(show).join("\n"));
|
|
978
|
+
if (!flags.write) {
|
|
979
|
+
outro("next", m.prune.next(runner(root)));
|
|
980
|
+
return 0;
|
|
981
|
+
}
|
|
982
|
+
if (!flags.yes) {
|
|
983
|
+
const answer = await p.confirm({
|
|
984
|
+
message: tag("delete", m.prune.ask(result.unused.length)),
|
|
985
|
+
initialValue: false
|
|
986
|
+
});
|
|
987
|
+
if (p.isCancel(answer)) return cancelled();
|
|
988
|
+
if (!answer) {
|
|
989
|
+
outro("next", m.prune.kept);
|
|
990
|
+
return 0;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
for (const file of result.unused) rmSync(file, { force: true });
|
|
994
|
+
outro("delete", m.prune.deleted(result.unused.length));
|
|
995
|
+
return 0;
|
|
996
|
+
}
|
|
997
|
+
//#endregion
|
|
998
|
+
//#region src/cli.ts
|
|
999
|
+
const options = {
|
|
1000
|
+
all: {
|
|
1001
|
+
type: "boolean",
|
|
1002
|
+
short: "a"
|
|
1003
|
+
},
|
|
1004
|
+
style: { type: "string" },
|
|
1005
|
+
radius: { type: "string" },
|
|
1006
|
+
overwrite: { type: "boolean" },
|
|
1007
|
+
write: { type: "boolean" },
|
|
1008
|
+
force: { type: "boolean" },
|
|
1009
|
+
yes: {
|
|
1010
|
+
type: "boolean",
|
|
1011
|
+
short: "y"
|
|
1012
|
+
},
|
|
1013
|
+
help: {
|
|
1014
|
+
type: "boolean",
|
|
1015
|
+
short: "h"
|
|
1016
|
+
},
|
|
1017
|
+
version: {
|
|
1018
|
+
type: "boolean",
|
|
1019
|
+
short: "V"
|
|
1020
|
+
}
|
|
38
1021
|
};
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
1022
|
+
const parse = (args) => parseArgs({
|
|
1023
|
+
args,
|
|
1024
|
+
options,
|
|
1025
|
+
allowPositionals: true
|
|
1026
|
+
});
|
|
1027
|
+
const help = () => m.help(runner(), version);
|
|
1028
|
+
async function main() {
|
|
1029
|
+
repaint();
|
|
1030
|
+
let parsed;
|
|
1031
|
+
try {
|
|
1032
|
+
parsed = parse(process.argv.slice(2));
|
|
1033
|
+
} catch (error) {
|
|
1034
|
+
const unknown = error.code === "ERR_PARSE_ARGS_UNKNOWN_OPTION";
|
|
1035
|
+
const option = String(error).match(/'(-[^']+)'/)?.[1];
|
|
1036
|
+
console.error(c.danger(unknown ? m.cli.unknownOption(option) : String(error.message)));
|
|
1037
|
+
console.error(m.cli.seeHelp(runner()));
|
|
1038
|
+
return 1;
|
|
1039
|
+
}
|
|
1040
|
+
const { values, positionals } = parsed;
|
|
1041
|
+
const [command, ...names] = positionals;
|
|
1042
|
+
if (values.version) {
|
|
1043
|
+
console.log(version);
|
|
1044
|
+
return 0;
|
|
1045
|
+
}
|
|
1046
|
+
switch (values.help ? void 0 : command) {
|
|
1047
|
+
case "init": return init(values);
|
|
1048
|
+
case "add": return add(names, values);
|
|
1049
|
+
case "list":
|
|
1050
|
+
case "ls": return list();
|
|
1051
|
+
case "prune": return prune(values);
|
|
1052
|
+
case void 0:
|
|
1053
|
+
console.log(help());
|
|
1054
|
+
return 0;
|
|
1055
|
+
default:
|
|
1056
|
+
console.error(c.danger(m.cli.unknownCommand(command)));
|
|
1057
|
+
console.log(help());
|
|
1058
|
+
return 1;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
main().then((code) => {
|
|
1062
|
+
process.exitCode = code;
|
|
1063
|
+
}).catch((error) => {
|
|
1064
|
+
console.error(c.danger(error instanceof Error ? error.message : String(error)));
|
|
1065
|
+
process.exitCode = 1;
|
|
1066
|
+
});
|
|
1067
|
+
//#endregion
|
|
1068
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,53 +1,50 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yummaui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Add Yumma UI components to your project.",
|
|
5
5
|
"keywords": [
|
|
6
|
-
"yummacss",
|
|
7
|
-
"yumma-ui",
|
|
8
|
-
"components",
|
|
9
6
|
"base-ui",
|
|
10
|
-
"cli"
|
|
7
|
+
"cli",
|
|
8
|
+
"components",
|
|
9
|
+
"yumma-ui",
|
|
10
|
+
"yummacss"
|
|
11
11
|
],
|
|
12
|
-
"homepage": "https://
|
|
12
|
+
"homepage": "https://www.yummaui.com",
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"author": "Renildo Pereira",
|
|
13
15
|
"repository": {
|
|
14
16
|
"type": "git",
|
|
15
|
-
"url": "git+https://github.com/yummacss/yummaui.git"
|
|
17
|
+
"url": "git+https://github.com/yummacss/yummaui.git",
|
|
18
|
+
"directory": "packages/cli"
|
|
16
19
|
},
|
|
17
|
-
"license": "MIT",
|
|
18
|
-
"author": "Renildo Pereira",
|
|
19
|
-
"type": "module",
|
|
20
20
|
"bin": {
|
|
21
21
|
"yummaui": "./dist/cli.mjs"
|
|
22
22
|
},
|
|
23
|
-
"engines": {
|
|
24
|
-
"node": ">=20.12"
|
|
25
|
-
},
|
|
26
23
|
"files": [
|
|
27
24
|
"dist"
|
|
28
25
|
],
|
|
26
|
+
"type": "module",
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public",
|
|
29
|
+
"provenance": true
|
|
30
|
+
},
|
|
29
31
|
"dependencies": {
|
|
30
32
|
"@clack/prompts": "^1.8.1",
|
|
31
33
|
"fastest-levenshtein": "^1.0.16",
|
|
32
34
|
"magicast": "^0.5.5"
|
|
33
35
|
},
|
|
34
36
|
"devDependencies": {
|
|
35
|
-
"@biomejs/biome": "^2.4.1",
|
|
36
37
|
"@types/node": "^26.1.2",
|
|
37
|
-
"tsdown": "^0.16.5",
|
|
38
38
|
"typescript": "^5.9.3",
|
|
39
|
-
"
|
|
39
|
+
"vite-plus": "1.3.0"
|
|
40
40
|
},
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"provenance": true
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=20.12"
|
|
44
43
|
},
|
|
45
44
|
"scripts": {
|
|
46
|
-
"build": "
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"test": "vitest run",
|
|
51
|
-
"test:watch": "vitest"
|
|
45
|
+
"build": "vp pack",
|
|
46
|
+
"dev": "vp pack --watch",
|
|
47
|
+
"test": "vp test run",
|
|
48
|
+
"test:watch": "vp test"
|
|
52
49
|
}
|
|
53
50
|
}
|