shadcn-nsh 0.2.0 → 0.3.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 +12 -2
- package/README.zh.md +12 -2
- package/dist/index.js +322 -165
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ Default registry: `https://raw.githubusercontent.com/aningdev/shadcn-nsh/main/pu
|
|
|
15
15
|
|
|
16
16
|
| Command | What it does |
|
|
17
17
|
| ------------------- | -------------------------------------------------------------------- |
|
|
18
|
-
| `init` | Writes `nsh-ui.json`, installs
|
|
18
|
+
| `init` | Writes `nsh-ui.json`, installs a theme, points each `nsh.json` at it |
|
|
19
19
|
| `add [components…]` | Copies items and their `registryDependencies` into the app |
|
|
20
20
|
| `list` | The registry's items, `✓` on the ones the app has |
|
|
21
21
|
| `view <items…>` | The files `add` would write: target paths and contents |
|
|
@@ -26,7 +26,8 @@ Default registry: `https://raw.githubusercontent.com/aningdev/shadcn-nsh/main/pu
|
|
|
26
26
|
Every command takes `-c, --cwd <dir>` (default: the current directory). `init`, `add`, `list`,
|
|
27
27
|
`view` and `diff` also take `--registry <url>`, a template with `{name}` in it; without it, `add`,
|
|
28
28
|
`list`, `view` and `diff` use the one in `nsh-ui.json`, and `init` uses the public registry.
|
|
29
|
-
`init` also takes `--overwrite
|
|
29
|
+
`init` also takes `--overwrite`, `-y` (accepted; nothing prompts today) and `--theme <name>`,
|
|
30
|
+
`default` (the library's own) or `shadcn` (shadcn's neutral palette and flat finishes).
|
|
30
31
|
|
|
31
32
|
## add
|
|
32
33
|
|
|
@@ -43,6 +44,11 @@ shadcn-nsh add button --overwrite
|
|
|
43
44
|
- `theme.json` is merged by key: the app keeps its colors and gains the tokens it lacks. A
|
|
44
45
|
recolored theme is kept as it is (`kept theme.json (the app's colors stay)`).
|
|
45
46
|
- `--all` (`-a`) adds every `registry:ui` item the catalog lists, dependencies included.
|
|
47
|
+
- Naming a theme (`add theme-shadcn`, or `add theme` to go back) switches the app's theme: its
|
|
48
|
+
files are written where they differ and `nsh-ui.json`'s `theme` records the new one. Naming the
|
|
49
|
+
theme the app already has is a plain `add`: its colors stay unless `--overwrite`.
|
|
50
|
+
- After that, `theme` as a dependency resolves to the theme the app has, so `add button`,
|
|
51
|
+
`add button --overwrite` and `add --all --overwrite` never write the default theme back.
|
|
46
52
|
- `--dry-run` prints the same lines as `would write …` / `would overwrite …` and writes nothing —
|
|
47
53
|
not even `nsh-ui.json` or the theme `init` would install first.
|
|
48
54
|
|
|
@@ -80,6 +86,7 @@ recolored theme is the app's own, and only `add --overwrite` replaces it.
|
|
|
80
86
|
```json
|
|
81
87
|
{
|
|
82
88
|
"registry": "https://raw.githubusercontent.com/aningdev/shadcn-nsh/main/public/r/{name}.json",
|
|
89
|
+
"theme": "theme",
|
|
83
90
|
"aliases": {
|
|
84
91
|
"components": "./components",
|
|
85
92
|
"ui": "./components/ui",
|
|
@@ -89,6 +96,9 @@ recolored theme is the app's own, and only `add --overwrite` replaces it.
|
|
|
89
96
|
}
|
|
90
97
|
```
|
|
91
98
|
|
|
99
|
+
`theme` is the `registry:theme` item the app installed (`theme`, or `theme-shadcn`); a file
|
|
100
|
+
without it is on `theme`.
|
|
101
|
+
|
|
92
102
|
In a monorepo (`apps/*` + `packages/ui`) it lives in `packages/ui` and `add -c apps/web`
|
|
93
103
|
installs there.
|
|
94
104
|
|
package/README.zh.md
CHANGED
|
@@ -15,7 +15,7 @@ bunx shadcn-nsh@latest add button
|
|
|
15
15
|
|
|
16
16
|
| 命令 | 作用 |
|
|
17
17
|
| ------------------- | ---------------------------------------------------------- |
|
|
18
|
-
| `init` | 写 `nsh-ui.json
|
|
18
|
+
| `init` | 写 `nsh-ui.json`、装一个主题、把每个 `nsh.json` 指向它 |
|
|
19
19
|
| `add [components…]` | 复制条目及其 `registryDependencies` |
|
|
20
20
|
| `list` | registry 的全部条目,应用已有的标 `✓` |
|
|
21
21
|
| `view <items…>` | `add` 会写的文件:目标路径与内容 |
|
|
@@ -25,7 +25,8 @@ bunx shadcn-nsh@latest add button
|
|
|
25
25
|
|
|
26
26
|
每个命令都接受 `-c, --cwd <dir>`(默认:当前目录)。`init`、`add`、`list`、`view`、`diff`
|
|
27
27
|
另接受 `--registry <url>`(含 `{name}` 的模板);省略时 `add`、`list`、`view`、`diff` 用
|
|
28
|
-
`nsh-ui.json` 里的,`init` 用公共 registry。`init` 还接受 `--overwrite
|
|
28
|
+
`nsh-ui.json` 里的,`init` 用公共 registry。`init` 还接受 `--overwrite`、`-y`(接受,当前不弹确认)与 `--theme <name>`:
|
|
29
|
+
`default`(本库自己的主题)或 `shadcn`(shadcn 的 neutral 配色与平面 finish)。
|
|
29
30
|
|
|
30
31
|
## add
|
|
31
32
|
|
|
@@ -42,6 +43,11 @@ shadcn-nsh add button --overwrite
|
|
|
42
43
|
- `theme.json` 按 key 合并:应用保留自己的颜色,补上缺的 token;改过颜色的主题原样保留
|
|
43
44
|
(`kept theme.json (the app's colors stay)`)。
|
|
44
45
|
- `--all`(`-a`)加全部 `registry:ui` 条目(含依赖)。
|
|
46
|
+
- 点名一个主题(`add theme-shadcn`,`add theme` 是换回默认)就是换主题:文件写到应用里,
|
|
47
|
+
`nsh-ui.json` 的 `theme` 记下新的那个。点名应用已有的主题只是普通的 `add`:不加 `--overwrite`
|
|
48
|
+
就保留应用的配色。
|
|
49
|
+
- 此后 `theme` 这个依赖按应用的主题解析,所以 `add button`、`add button --overwrite`、
|
|
50
|
+
`add --all --overwrite` 都不会把默认主题写回来。
|
|
45
51
|
- `--dry-run` 用 `would write …` / `would overwrite …` 打印同样的行,一个字节都不写——
|
|
46
52
|
包括 `init` 会先写的 `nsh-ui.json` 与主题。
|
|
47
53
|
|
|
@@ -77,6 +83,7 @@ shadcn-nsh diff || echo "run shadcn-nsh add <item> --overwrite"
|
|
|
77
83
|
```json
|
|
78
84
|
{
|
|
79
85
|
"registry": "https://raw.githubusercontent.com/aningdev/shadcn-nsh/main/public/r/{name}.json",
|
|
86
|
+
"theme": "theme",
|
|
80
87
|
"aliases": {
|
|
81
88
|
"components": "./components",
|
|
82
89
|
"ui": "./components/ui",
|
|
@@ -86,6 +93,9 @@ shadcn-nsh diff || echo "run shadcn-nsh add <item> --overwrite"
|
|
|
86
93
|
}
|
|
87
94
|
```
|
|
88
95
|
|
|
96
|
+
`theme` 是应用装的 `registry:theme` 条目(`theme`,或 `theme-shadcn`);文件里没有它就是
|
|
97
|
+
`theme`。
|
|
98
|
+
|
|
89
99
|
monorepo(`apps/*` + `packages/ui`)里它位于 `packages/ui`,`add -c apps/web` 装到那里。
|
|
90
100
|
|
|
91
101
|
## 本仓开发
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { Command as Command8 } from "commander";
|
|
|
6
6
|
// package.json
|
|
7
7
|
var package_default = {
|
|
8
8
|
name: "shadcn-nsh",
|
|
9
|
-
version: "0.
|
|
9
|
+
version: "0.3.0",
|
|
10
10
|
description: "Add styled NSH components (copy-into-app) from the shadcn-nsh registry",
|
|
11
11
|
keywords: [
|
|
12
12
|
"cli",
|
|
@@ -52,13 +52,27 @@ var package_default = {
|
|
|
52
52
|
};
|
|
53
53
|
|
|
54
54
|
// src/commands/add.ts
|
|
55
|
-
import
|
|
55
|
+
import path11 from "path";
|
|
56
56
|
import { Command as Command2 } from "commander";
|
|
57
57
|
|
|
58
58
|
// src/types.ts
|
|
59
59
|
var DEFAULT_REGISTRY = "https://raw.githubusercontent.com/aningdev/shadcn-nsh/main/public/r/{name}.json";
|
|
60
|
+
var DEFAULT_THEME = "theme";
|
|
61
|
+
var THEMES = {
|
|
62
|
+
default: DEFAULT_THEME,
|
|
63
|
+
shadcn: "theme-shadcn"
|
|
64
|
+
};
|
|
65
|
+
var isThemeName = (value) => value in THEMES;
|
|
66
|
+
function themeItem(name) {
|
|
67
|
+
if (!isThemeName(name)) {
|
|
68
|
+
const options = Object.keys(THEMES).join(", ");
|
|
69
|
+
throw new Error(`Unknown theme "${name}". Choose one of: ${options}.`);
|
|
70
|
+
}
|
|
71
|
+
return THEMES[name];
|
|
72
|
+
}
|
|
60
73
|
var DEFAULT_CONFIG = () => ({
|
|
61
74
|
registry: DEFAULT_REGISTRY,
|
|
75
|
+
theme: DEFAULT_THEME,
|
|
62
76
|
aliases: {
|
|
63
77
|
components: "./components",
|
|
64
78
|
ui: "./components/ui",
|
|
@@ -194,9 +208,223 @@ async function readApp(cwd, registry) {
|
|
|
194
208
|
return { ws, config, hasConfig: stored != null };
|
|
195
209
|
}
|
|
196
210
|
|
|
197
|
-
// src/utils/
|
|
211
|
+
// src/utils/deps.ts
|
|
212
|
+
import { spawnSync } from "child_process";
|
|
198
213
|
import fs4 from "fs/promises";
|
|
199
|
-
import
|
|
214
|
+
import path5 from "path";
|
|
215
|
+
|
|
216
|
+
// src/utils/registry.ts
|
|
217
|
+
import fs3 from "fs/promises";
|
|
218
|
+
import path4 from "path";
|
|
219
|
+
function itemUrl(config, name, registryOverride) {
|
|
220
|
+
const template = registryOverride ?? config.registry;
|
|
221
|
+
if (!template.includes("{name}")) {
|
|
222
|
+
throw new Error(`Registry URL must contain {name}: ${template}`);
|
|
223
|
+
}
|
|
224
|
+
return template.replaceAll("{name}", name);
|
|
225
|
+
}
|
|
226
|
+
function indexUrl(config, registryOverride) {
|
|
227
|
+
const template = registryOverride ?? config.registry;
|
|
228
|
+
if (!template.includes("{name}")) {
|
|
229
|
+
throw new Error(`Registry URL must contain {name}: ${template}`);
|
|
230
|
+
}
|
|
231
|
+
return template.replaceAll("{name}", "index");
|
|
232
|
+
}
|
|
233
|
+
async function fetchItem(config, name, registryOverride) {
|
|
234
|
+
const url = itemUrl(config, name, registryOverride);
|
|
235
|
+
const res = await fetch(url);
|
|
236
|
+
if (!res.ok) {
|
|
237
|
+
throw new Error(`Failed to fetch ${url}: ${res.status} ${res.statusText}`);
|
|
238
|
+
}
|
|
239
|
+
return await res.json();
|
|
240
|
+
}
|
|
241
|
+
async function fetchIndex(config, registryOverride) {
|
|
242
|
+
const url = indexUrl(config, registryOverride);
|
|
243
|
+
const res = await fetch(url);
|
|
244
|
+
if (!res.ok) {
|
|
245
|
+
throw new Error(
|
|
246
|
+
`Failed to fetch ${url}: ${res.status} ${res.statusText}. A registry needs an index.json (bun run registry:build writes one).`
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
return await res.json();
|
|
250
|
+
}
|
|
251
|
+
function resolveDependency(config, name) {
|
|
252
|
+
return name === DEFAULT_THEME && config.theme !== DEFAULT_THEME ? config.theme : name;
|
|
253
|
+
}
|
|
254
|
+
async function resolveTree(config, names, registryOverride) {
|
|
255
|
+
const seen = /* @__PURE__ */ new Set();
|
|
256
|
+
const out = [];
|
|
257
|
+
async function walk(name) {
|
|
258
|
+
if (seen.has(name)) return;
|
|
259
|
+
seen.add(name);
|
|
260
|
+
const item = await fetchItem(config, name, registryOverride);
|
|
261
|
+
for (const dep of item.registryDependencies ?? []) {
|
|
262
|
+
await walk(resolveDependency(config, dep));
|
|
263
|
+
}
|
|
264
|
+
out.push(item);
|
|
265
|
+
}
|
|
266
|
+
for (const name of names) {
|
|
267
|
+
await walk(name);
|
|
268
|
+
}
|
|
269
|
+
return out;
|
|
270
|
+
}
|
|
271
|
+
var IMPORT_RE = /(?:import|export)\s[^;]*?from\s+["']([^"']+)["']|import\s+["']([^"']+)["']/g;
|
|
272
|
+
var THEME_PACKAGE = "@shadcn-nsh/theme";
|
|
273
|
+
var RESOLVE_SUFFIXES = ["", ".ts", ".tsx", ".json", "/index.ts", "/index.tsx"];
|
|
274
|
+
var fromRuntime = (spec) => spec === "nsh" || spec.startsWith("nsh/");
|
|
275
|
+
function packageOf(spec) {
|
|
276
|
+
const parts = spec.split("/");
|
|
277
|
+
return spec.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0] ?? spec;
|
|
278
|
+
}
|
|
279
|
+
function declaredPackage(entry) {
|
|
280
|
+
const at = entry.indexOf("@", 1);
|
|
281
|
+
return at === -1 ? entry : entry.slice(0, at);
|
|
282
|
+
}
|
|
283
|
+
async function checkRegistryDependencies(registry, baseDir) {
|
|
284
|
+
const owner = /* @__PURE__ */ new Map();
|
|
285
|
+
const targets = /* @__PURE__ */ new Map();
|
|
286
|
+
for (const item of registry.items) {
|
|
287
|
+
const own = /* @__PURE__ */ new Set();
|
|
288
|
+
for (const file of item.files) {
|
|
289
|
+
owner.set(path4.resolve(baseDir, file.path), item.name);
|
|
290
|
+
if (file.target) own.add(file.target);
|
|
291
|
+
}
|
|
292
|
+
targets.set(item.name, own);
|
|
293
|
+
}
|
|
294
|
+
const themeItems = new Set(
|
|
295
|
+
registry.items.filter((item) => item.type === "registry:theme").map((item) => item.name)
|
|
296
|
+
);
|
|
297
|
+
const hasTheme = themeItems.has(DEFAULT_THEME);
|
|
298
|
+
const problems = [];
|
|
299
|
+
for (const item of registry.items) {
|
|
300
|
+
const used = /* @__PURE__ */ new Set();
|
|
301
|
+
const own = targets.get(item.name) ?? /* @__PURE__ */ new Set();
|
|
302
|
+
const packages = /* @__PURE__ */ new Set();
|
|
303
|
+
for (const file of item.files) {
|
|
304
|
+
const abs = path4.resolve(baseDir, file.path);
|
|
305
|
+
if (!/\.(ts|tsx)$/.test(abs)) continue;
|
|
306
|
+
const source = await fs3.readFile(abs, "utf8");
|
|
307
|
+
for (const match of source.matchAll(IMPORT_RE)) {
|
|
308
|
+
const spec = match[1] ?? match[2];
|
|
309
|
+
if (spec === THEME_PACKAGE && (hasTheme || themeItems.has(item.name))) {
|
|
310
|
+
used.add(themeItems.has(item.name) ? item.name : DEFAULT_THEME);
|
|
311
|
+
} else if (spec.startsWith(".")) {
|
|
312
|
+
const beside = file.target ? RESOLVE_SUFFIXES.map(
|
|
313
|
+
(suffix) => `${path4.posix.normalize(path4.posix.join(path4.posix.dirname(file.target), spec))}${suffix}`
|
|
314
|
+
) : [];
|
|
315
|
+
if (beside.some((candidate) => own.has(candidate))) continue;
|
|
316
|
+
const target = RESOLVE_SUFFIXES.map(
|
|
317
|
+
(x) => path4.resolve(path4.dirname(abs), spec) + x
|
|
318
|
+
).find((candidate) => owner.has(candidate));
|
|
319
|
+
if (target) used.add(owner.get(target));
|
|
320
|
+
else problems.push(`${item.name}: imports "${spec}", which no registry item ships`);
|
|
321
|
+
} else if (spec !== THEME_PACKAGE && !fromRuntime(spec)) packages.add(packageOf(spec));
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
used.delete(item.name);
|
|
325
|
+
const declared = new Set(item.registryDependencies ?? []);
|
|
326
|
+
for (const dep of used) {
|
|
327
|
+
if (!declared.has(dep))
|
|
328
|
+
problems.push(`${item.name}: imports "${dep}" but does not declare it`);
|
|
329
|
+
}
|
|
330
|
+
for (const dep of declared) {
|
|
331
|
+
if (!used.has(dep)) problems.push(`${item.name}: declares "${dep}" but never imports it`);
|
|
332
|
+
}
|
|
333
|
+
const installed = new Map(
|
|
334
|
+
(item.dependencies ?? []).map((entry) => [declaredPackage(entry), entry])
|
|
335
|
+
);
|
|
336
|
+
for (const name of packages) {
|
|
337
|
+
if (!installed.has(name))
|
|
338
|
+
problems.push(`${item.name}: imports "${name}" but does not declare it in dependencies`);
|
|
339
|
+
}
|
|
340
|
+
for (const [name, entry] of installed) {
|
|
341
|
+
if (!packages.has(name))
|
|
342
|
+
problems.push(`${item.name}: declares "${entry}" but never imports it`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return problems;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// src/utils/deps.ts
|
|
349
|
+
var LOCKFILES = [
|
|
350
|
+
["bun.lock", "bun"],
|
|
351
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
352
|
+
["yarn.lock", "yarn"]
|
|
353
|
+
];
|
|
354
|
+
async function exists2(file) {
|
|
355
|
+
try {
|
|
356
|
+
await fs4.access(file);
|
|
357
|
+
return true;
|
|
358
|
+
} catch {
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
async function projectOf(dir) {
|
|
363
|
+
const start = path5.resolve(dir);
|
|
364
|
+
let root;
|
|
365
|
+
let manager;
|
|
366
|
+
for (let at = start; ; at = path5.dirname(at)) {
|
|
367
|
+
for (const [lock, name] of LOCKFILES) {
|
|
368
|
+
if (manager == null && await exists2(path5.join(at, lock))) manager = name;
|
|
369
|
+
}
|
|
370
|
+
if (root == null && await exists2(path5.join(at, "package.json"))) root = at;
|
|
371
|
+
if (manager != null || path5.dirname(at) === at) {
|
|
372
|
+
return { root: root ?? start, manager: manager ?? "npm" };
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
function dependenciesOf(items) {
|
|
377
|
+
const packages = /* @__PURE__ */ new Map();
|
|
378
|
+
for (const item of items) {
|
|
379
|
+
for (const entry of item.dependencies ?? []) {
|
|
380
|
+
const name = declaredPackage(entry);
|
|
381
|
+
if (!packages.has(name)) packages.set(name, entry);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return [...packages.values()];
|
|
385
|
+
}
|
|
386
|
+
function packageName(dir) {
|
|
387
|
+
const name = path5.basename(dir).toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[^a-z0-9]+/, "");
|
|
388
|
+
return name === "" ? "app" : name;
|
|
389
|
+
}
|
|
390
|
+
async function ensurePackageJson(dir) {
|
|
391
|
+
const file = path5.join(dir, "package.json");
|
|
392
|
+
if (await exists2(file)) return null;
|
|
393
|
+
const content = { name: packageName(dir), private: true, dependencies: {} };
|
|
394
|
+
await fs4.writeFile(file, `${JSON.stringify(content, null, 2)}
|
|
395
|
+
`, "utf8");
|
|
396
|
+
return file;
|
|
397
|
+
}
|
|
398
|
+
async function installDependencies(dir, items, opts = {}) {
|
|
399
|
+
const dependencies = dependenciesOf(items);
|
|
400
|
+
if (dependencies.length === 0) return null;
|
|
401
|
+
const { root, manager } = await projectOf(dir);
|
|
402
|
+
const command = [manager, "add", ...dependencies];
|
|
403
|
+
const shown = command.join(" ");
|
|
404
|
+
const install = { manager, root, command: shown, ok: true };
|
|
405
|
+
if (opts.dryRun) {
|
|
406
|
+
console.log(`would install ${dependencies.join(" ")} (${shown})`);
|
|
407
|
+
return install;
|
|
408
|
+
}
|
|
409
|
+
const written = await ensurePackageJson(root);
|
|
410
|
+
if (written != null) {
|
|
411
|
+
console.log(`write ${path5.relative(path5.resolve(dir), written) || "package.json"}`);
|
|
412
|
+
}
|
|
413
|
+
const result = spawnSync(manager, command.slice(1), { cwd: root, stdio: "inherit" });
|
|
414
|
+
const failed = result.error ?? (result.status === 0 ? null : `exited ${result.status}`);
|
|
415
|
+
if (failed == null) {
|
|
416
|
+
console.log(`installed ${dependencies.join(" ")} (${shown})`);
|
|
417
|
+
return install;
|
|
418
|
+
}
|
|
419
|
+
const why = failed instanceof Error ? failed.message : failed;
|
|
420
|
+
console.error(`shadcn-nsh: \`${shown}\` failed in ${root}: ${why}`);
|
|
421
|
+
console.error(` run it yourself: cd ${root} && ${shown}`);
|
|
422
|
+
return { ...install, ok: false };
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// src/utils/install.ts
|
|
426
|
+
import fs6 from "fs/promises";
|
|
427
|
+
import path9 from "path";
|
|
200
428
|
|
|
201
429
|
// src/utils/diff.ts
|
|
202
430
|
function diffLines(before, after) {
|
|
@@ -249,25 +477,25 @@ function formatDiff(lines) {
|
|
|
249
477
|
}
|
|
250
478
|
|
|
251
479
|
// src/utils/plan.ts
|
|
252
|
-
import
|
|
480
|
+
import fs5 from "fs";
|
|
253
481
|
import fsPromises from "fs/promises";
|
|
254
|
-
import
|
|
482
|
+
import path8 from "path";
|
|
255
483
|
|
|
256
484
|
// src/utils/paths.ts
|
|
257
|
-
import
|
|
485
|
+
import path6 from "path";
|
|
258
486
|
function resolveAlias(cwd, alias) {
|
|
259
|
-
return
|
|
487
|
+
return path6.resolve(cwd, alias);
|
|
260
488
|
}
|
|
261
489
|
function relativeImport(fromFile, toModuleFile) {
|
|
262
|
-
let rel =
|
|
490
|
+
let rel = path6.relative(path6.dirname(fromFile), toModuleFile);
|
|
263
491
|
if (!rel.startsWith(".")) rel = `./${rel}`;
|
|
264
492
|
return rel.replace(/\\/g, "/").replace(/\.(tsx?|jsx?)$/, "");
|
|
265
493
|
}
|
|
266
494
|
function defaultTarget(file, aliases) {
|
|
267
495
|
if (file.target) return file.target;
|
|
268
|
-
const base =
|
|
269
|
-
if (file.type === "registry:ui") return
|
|
270
|
-
if (file.type === "registry:lib") return
|
|
496
|
+
const base = path6.basename(file.path);
|
|
497
|
+
if (file.type === "registry:ui") return path6.posix.join(aliases.ui.replace(/^\.\//, ""), base);
|
|
498
|
+
if (file.type === "registry:lib") return path6.posix.join(aliases.lib.replace(/^\.\//, ""), base);
|
|
271
499
|
return base;
|
|
272
500
|
}
|
|
273
501
|
|
|
@@ -293,7 +521,7 @@ function mergeTheme(existing, incoming) {
|
|
|
293
521
|
}
|
|
294
522
|
|
|
295
523
|
// src/utils/transform.ts
|
|
296
|
-
import
|
|
524
|
+
import path7 from "path";
|
|
297
525
|
function transformContent(content, absoluteTargetFile, config, cwd) {
|
|
298
526
|
const themeModule = resolveAlias(cwd, config.aliases.theme);
|
|
299
527
|
const themeImport = relativeImport(absoluteTargetFile, themeModule);
|
|
@@ -301,13 +529,13 @@ function transformContent(content, absoluteTargetFile, config, cwd) {
|
|
|
301
529
|
const rewritten = content.replaceAll(`from "@shadcn-nsh/theme"`, `from "${themeImport}"`).replaceAll(`from '@shadcn-nsh/theme'`, `from '${themeImport}'`);
|
|
302
530
|
return rewritten.replace(
|
|
303
531
|
/from (["'])\.\/lib\/([\w-]+)\1/g,
|
|
304
|
-
(_match, quote, name) => `from ${quote}${relativeImport(absoluteTargetFile,
|
|
532
|
+
(_match, quote, name) => `from ${quote}${relativeImport(absoluteTargetFile, path7.join(libDir, name))}${quote}`
|
|
305
533
|
);
|
|
306
534
|
}
|
|
307
535
|
|
|
308
536
|
// src/utils/plan.ts
|
|
309
537
|
function isThemeFile(file) {
|
|
310
|
-
return
|
|
538
|
+
return path8.basename(file.absolute) === "theme.json";
|
|
311
539
|
}
|
|
312
540
|
function hasChange(file) {
|
|
313
541
|
if (file.status === "merge") return true;
|
|
@@ -330,18 +558,19 @@ function themeMerge(existing, incoming) {
|
|
|
330
558
|
return null;
|
|
331
559
|
}
|
|
332
560
|
}
|
|
333
|
-
async function planItems(uiRoot, config, items) {
|
|
561
|
+
async function planItems(uiRoot, config, items, opts = {}) {
|
|
334
562
|
const planned = [];
|
|
563
|
+
const adopted = new Set(opts.adopt ?? []);
|
|
335
564
|
for (const item of items) {
|
|
336
565
|
for (const file of item.files) {
|
|
337
566
|
if (file.content == null) {
|
|
338
567
|
throw new Error(`Item ${item.name} file ${file.path} has no content (run registry:build)`);
|
|
339
568
|
}
|
|
340
569
|
const target = defaultTarget(file, config.aliases);
|
|
341
|
-
const absolute =
|
|
570
|
+
const absolute = path8.isAbsolute(target) ? target : path8.resolve(uiRoot, target);
|
|
342
571
|
const content = transformContent(file.content, absolute, config, uiRoot);
|
|
343
|
-
const existing = await readOrNull(absolute);
|
|
344
|
-
const relative =
|
|
572
|
+
const existing = adopted.has(item.name) ? null : await readOrNull(absolute);
|
|
573
|
+
const relative = path8.relative(uiRoot, absolute) || path8.basename(absolute);
|
|
345
574
|
const entry = {
|
|
346
575
|
item: item.name,
|
|
347
576
|
file,
|
|
@@ -356,7 +585,7 @@ async function planItems(uiRoot, config, items) {
|
|
|
356
585
|
if (existing != null) {
|
|
357
586
|
if (existing === content) {
|
|
358
587
|
entry.status = "same";
|
|
359
|
-
} else if (
|
|
588
|
+
} else if (path8.basename(absolute) === "theme.json") {
|
|
360
589
|
const merged2 = themeMerge(existing, content);
|
|
361
590
|
if (merged2 && merged2.added > 0) {
|
|
362
591
|
entry.status = "merge";
|
|
@@ -381,13 +610,13 @@ async function planItems(uiRoot, config, items) {
|
|
|
381
610
|
function isInstalled(uiRoot, config, item) {
|
|
382
611
|
return (item.files ?? []).some((file) => {
|
|
383
612
|
const target = defaultTarget(file, config.aliases);
|
|
384
|
-
return
|
|
613
|
+
return fs5.existsSync(path8.isAbsolute(target) ? target : path8.resolve(uiRoot, target));
|
|
385
614
|
});
|
|
386
615
|
}
|
|
387
616
|
|
|
388
617
|
// src/utils/install.ts
|
|
389
618
|
async function installItems(uiRoot, config, items, opts = {}) {
|
|
390
|
-
const planned = await planItems(uiRoot, config, items);
|
|
619
|
+
const planned = await planItems(uiRoot, config, items, opts);
|
|
391
620
|
const written = [];
|
|
392
621
|
for (const file of planned) {
|
|
393
622
|
const shown = file.path;
|
|
@@ -431,8 +660,8 @@ async function installItems(uiRoot, config, items, opts = {}) {
|
|
|
431
660
|
return { planned, written };
|
|
432
661
|
}
|
|
433
662
|
async function write(file, content) {
|
|
434
|
-
await
|
|
435
|
-
await
|
|
663
|
+
await fs6.mkdir(path9.dirname(file.absolute), { recursive: true });
|
|
664
|
+
await fs6.writeFile(file.absolute, content, "utf8");
|
|
436
665
|
}
|
|
437
666
|
async function merged(file) {
|
|
438
667
|
if (file.existing == null) return null;
|
|
@@ -448,105 +677,12 @@ async function merged(file) {
|
|
|
448
677
|
}
|
|
449
678
|
}
|
|
450
679
|
|
|
451
|
-
// src/utils/registry.ts
|
|
452
|
-
import fs5 from "fs/promises";
|
|
453
|
-
import path8 from "path";
|
|
454
|
-
function itemUrl(config, name, registryOverride) {
|
|
455
|
-
const template = registryOverride ?? config.registry;
|
|
456
|
-
if (!template.includes("{name}")) {
|
|
457
|
-
throw new Error(`Registry URL must contain {name}: ${template}`);
|
|
458
|
-
}
|
|
459
|
-
return template.replaceAll("{name}", name);
|
|
460
|
-
}
|
|
461
|
-
function indexUrl(config, registryOverride) {
|
|
462
|
-
const template = registryOverride ?? config.registry;
|
|
463
|
-
if (!template.includes("{name}")) {
|
|
464
|
-
throw new Error(`Registry URL must contain {name}: ${template}`);
|
|
465
|
-
}
|
|
466
|
-
return template.replaceAll("{name}", "index");
|
|
467
|
-
}
|
|
468
|
-
async function fetchItem(config, name, registryOverride) {
|
|
469
|
-
const url = itemUrl(config, name, registryOverride);
|
|
470
|
-
const res = await fetch(url);
|
|
471
|
-
if (!res.ok) {
|
|
472
|
-
throw new Error(`Failed to fetch ${url}: ${res.status} ${res.statusText}`);
|
|
473
|
-
}
|
|
474
|
-
return await res.json();
|
|
475
|
-
}
|
|
476
|
-
async function fetchIndex(config, registryOverride) {
|
|
477
|
-
const url = indexUrl(config, registryOverride);
|
|
478
|
-
const res = await fetch(url);
|
|
479
|
-
if (!res.ok) {
|
|
480
|
-
throw new Error(
|
|
481
|
-
`Failed to fetch ${url}: ${res.status} ${res.statusText}. A registry needs an index.json (bun run registry:build writes one).`
|
|
482
|
-
);
|
|
483
|
-
}
|
|
484
|
-
return await res.json();
|
|
485
|
-
}
|
|
486
|
-
async function resolveTree(config, names, registryOverride) {
|
|
487
|
-
const seen = /* @__PURE__ */ new Set();
|
|
488
|
-
const out = [];
|
|
489
|
-
async function walk(name) {
|
|
490
|
-
if (seen.has(name)) return;
|
|
491
|
-
seen.add(name);
|
|
492
|
-
const item = await fetchItem(config, name, registryOverride);
|
|
493
|
-
for (const dep of item.registryDependencies ?? []) {
|
|
494
|
-
await walk(dep);
|
|
495
|
-
}
|
|
496
|
-
out.push(item);
|
|
497
|
-
}
|
|
498
|
-
for (const name of names) {
|
|
499
|
-
await walk(name);
|
|
500
|
-
}
|
|
501
|
-
return out;
|
|
502
|
-
}
|
|
503
|
-
var IMPORT_RE = /(?:import|export)\s[^;]*?from\s+["']([^"']+)["']|import\s+["']([^"']+)["']/g;
|
|
504
|
-
var THEME_PACKAGE = "@shadcn-nsh/theme";
|
|
505
|
-
var RESOLVE_SUFFIXES = ["", ".ts", ".tsx", ".json", "/index.ts", "/index.tsx"];
|
|
506
|
-
async function checkRegistryDependencies(registry, baseDir) {
|
|
507
|
-
const owner = /* @__PURE__ */ new Map();
|
|
508
|
-
for (const item of registry.items) {
|
|
509
|
-
for (const file of item.files) owner.set(path8.resolve(baseDir, file.path), item.name);
|
|
510
|
-
}
|
|
511
|
-
const hasTheme = registry.items.some((item) => item.name === "theme");
|
|
512
|
-
const problems = [];
|
|
513
|
-
for (const item of registry.items) {
|
|
514
|
-
const used = /* @__PURE__ */ new Set();
|
|
515
|
-
for (const file of item.files) {
|
|
516
|
-
const abs = path8.resolve(baseDir, file.path);
|
|
517
|
-
if (!/\.(ts|tsx)$/.test(abs)) continue;
|
|
518
|
-
const source = await fs5.readFile(abs, "utf8");
|
|
519
|
-
for (const match of source.matchAll(IMPORT_RE)) {
|
|
520
|
-
const spec = match[1] ?? match[2];
|
|
521
|
-
if (spec === THEME_PACKAGE && hasTheme) used.add("theme");
|
|
522
|
-
else if (spec.startsWith(".")) {
|
|
523
|
-
const target = RESOLVE_SUFFIXES.map(
|
|
524
|
-
(x) => path8.resolve(path8.dirname(abs), spec) + x
|
|
525
|
-
).find((candidate) => owner.has(candidate));
|
|
526
|
-
if (target) used.add(owner.get(target));
|
|
527
|
-
else problems.push(`${item.name}: imports "${spec}", which no registry item ships`);
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
used.delete(item.name);
|
|
532
|
-
const declared = new Set(item.registryDependencies ?? []);
|
|
533
|
-
for (const dep of used) {
|
|
534
|
-
if (!declared.has(dep))
|
|
535
|
-
problems.push(`${item.name}: imports "${dep}" but does not declare it`);
|
|
536
|
-
}
|
|
537
|
-
for (const dep of declared) {
|
|
538
|
-
if (!used.has(dep)) problems.push(`${item.name}: declares "${dep}" but never imports it`);
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
return problems;
|
|
542
|
-
}
|
|
543
|
-
|
|
544
680
|
// src/commands/init.ts
|
|
545
|
-
import
|
|
546
|
-
import
|
|
681
|
+
import fs7 from "fs/promises";
|
|
682
|
+
import path10 from "path";
|
|
547
683
|
import { Command } from "commander";
|
|
548
684
|
async function runInit(opts) {
|
|
549
|
-
const cwd =
|
|
685
|
+
const cwd = path10.resolve(opts.cwd);
|
|
550
686
|
const ws = await resolveWorkspace(cwd);
|
|
551
687
|
if (ws.kind === "app") {
|
|
552
688
|
const nshJsonPath = await findNshJson(cwd);
|
|
@@ -562,10 +698,12 @@ async function runInit(opts) {
|
|
|
562
698
|
}
|
|
563
699
|
const config = DEFAULT_CONFIG();
|
|
564
700
|
if (opts.registry) config.registry = opts.registry;
|
|
701
|
+
config.theme = themeItem(opts.theme ?? "default");
|
|
565
702
|
await writeConfig(ws.uiRoot, config);
|
|
566
|
-
console.log(`write ${
|
|
567
|
-
const items = await resolveTree(config, [
|
|
703
|
+
console.log(`write ${path10.relative(cwd, path10.join(ws.uiRoot, "nsh-ui.json")) || "nsh-ui.json"}`);
|
|
704
|
+
const items = await resolveTree(config, [config.theme], opts.registry);
|
|
568
705
|
await installItems(ws.uiRoot, config, items, { overwrite: opts.overwrite });
|
|
706
|
+
await installDependencies(ws.uiRoot, items);
|
|
569
707
|
for (const appDir of ws.apps) {
|
|
570
708
|
const nshJsonPath = await findNshJson(appDir);
|
|
571
709
|
if (!nshJsonPath) continue;
|
|
@@ -573,13 +711,13 @@ async function runInit(opts) {
|
|
|
573
711
|
nsh.theme = ws.kind === "monorepo" ? themePathForApp(appDir, ws.uiRoot) : "./theme.json";
|
|
574
712
|
await writeJson(nshJsonPath, nsh);
|
|
575
713
|
console.log(
|
|
576
|
-
`update ${
|
|
714
|
+
`update ${path10.relative(cwd, nshJsonPath) || "nsh.json"} theme \u2192 ${String(nsh.theme)}`
|
|
577
715
|
);
|
|
578
716
|
}
|
|
579
717
|
if (ws.kind === "app") {
|
|
580
|
-
const tsconfig =
|
|
718
|
+
const tsconfig = path10.join(cwd, "tsconfig.json");
|
|
581
719
|
try {
|
|
582
|
-
await
|
|
720
|
+
await fs7.access(tsconfig);
|
|
583
721
|
} catch {
|
|
584
722
|
await writeJson(tsconfig, {
|
|
585
723
|
extends: "./.nsh/tsconfig.json",
|
|
@@ -590,14 +728,16 @@ async function runInit(opts) {
|
|
|
590
728
|
}
|
|
591
729
|
return config;
|
|
592
730
|
}
|
|
593
|
-
var init = new Command().name("init").description("Initialize shadcn-nsh (writes nsh-ui.json + theme; monorepo \u2192 packages/ui)").option("-c, --cwd <cwd>", "app or monorepo directory", process.cwd()).option("-y, --yes", "skip confirmation", false).option("--overwrite", "overwrite existing files", false).option("--registry <url>", "registry URL template containing {name}").action(
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
731
|
+
var init = new Command().name("init").description("Initialize shadcn-nsh (writes nsh-ui.json + theme; monorepo \u2192 packages/ui)").option("-c, --cwd <cwd>", "app or monorepo directory", process.cwd()).option("-y, --yes", "skip confirmation", false).option("--overwrite", "overwrite existing files", false).option("--theme <name>", "theme to install: default (the library's own) or shadcn", "default").option("--registry <url>", "registry URL template containing {name}").action(
|
|
732
|
+
async (opts) => {
|
|
733
|
+
await runInit(opts);
|
|
734
|
+
console.log("\nDone. Next: shadcn-nsh add button");
|
|
735
|
+
}
|
|
736
|
+
);
|
|
597
737
|
|
|
598
738
|
// src/commands/add.ts
|
|
599
739
|
async function runAdd(components, opts) {
|
|
600
|
-
const cwd =
|
|
740
|
+
const cwd = path11.resolve(opts.cwd);
|
|
601
741
|
const dryRun = opts.dryRun === true;
|
|
602
742
|
if (components.length === 0 && !opts.all) {
|
|
603
743
|
throw new Error("Name at least one component, or pass --all.");
|
|
@@ -629,7 +769,22 @@ async function runAdd(components, opts) {
|
|
|
629
769
|
throw new Error("The registry lists no registry:ui item to add.");
|
|
630
770
|
}
|
|
631
771
|
const items = await resolveTree(config, names, opts.registry);
|
|
632
|
-
|
|
772
|
+
const themes = items.filter((item) => item.type === "registry:theme").map((item) => item.name);
|
|
773
|
+
const named = names.filter((name) => themes.includes(name));
|
|
774
|
+
if (named.length > 1) {
|
|
775
|
+
throw new Error(`Name one theme, not ${named.join(", ")}: an app installs one.`);
|
|
776
|
+
}
|
|
777
|
+
const theme = named[0] === config.theme ? void 0 : named[0];
|
|
778
|
+
if (theme != null) {
|
|
779
|
+
config.theme = theme;
|
|
780
|
+
if (!dryRun) await writeConfig(ws.uiRoot, config);
|
|
781
|
+
}
|
|
782
|
+
await installItems(ws.uiRoot, config, items, {
|
|
783
|
+
overwrite: opts.overwrite,
|
|
784
|
+
dryRun,
|
|
785
|
+
adopt: theme == null ? [] : [theme]
|
|
786
|
+
});
|
|
787
|
+
const installed = await installDependencies(ws.uiRoot, items, { dryRun });
|
|
633
788
|
if (dryRun) {
|
|
634
789
|
const count = `${names.length} item${names.length === 1 ? "" : "s"}`;
|
|
635
790
|
console.log(`
|
|
@@ -642,9 +797,9 @@ Added: all ${names.length} registry:ui items` : `
|
|
|
642
797
|
Added: ${names.join(", ")}`
|
|
643
798
|
);
|
|
644
799
|
if (ws.kind === "monorepo") {
|
|
645
|
-
console.log(`(installed under ${
|
|
800
|
+
console.log(`(installed under ${path11.relative(ws.root, ws.uiRoot) || "packages/ui"})`);
|
|
646
801
|
}
|
|
647
|
-
return 0;
|
|
802
|
+
return installed?.ok === false ? 1 : 0;
|
|
648
803
|
}
|
|
649
804
|
var add = new Command2().name("add").description("Add registry components (monorepo: installs into packages/ui)").argument("[components...]", "component names (e.g. button)").option("-c, --cwd <cwd>", "app or monorepo directory", process.cwd()).option("-o, --overwrite", "replace files the app has changed", false).option("-a, --all", "every registry:ui item the registry lists", false).option("--dry-run", "print what would be written; write nothing", false).option("--registry <url>", "registry URL template containing {name}").action(
|
|
650
805
|
async (components, opts) => {
|
|
@@ -653,8 +808,8 @@ var add = new Command2().name("add").description("Add registry components (monor
|
|
|
653
808
|
);
|
|
654
809
|
|
|
655
810
|
// src/commands/build.ts
|
|
656
|
-
import
|
|
657
|
-
import
|
|
811
|
+
import fs8 from "fs/promises";
|
|
812
|
+
import path12 from "path";
|
|
658
813
|
import { Command as Command3 } from "commander";
|
|
659
814
|
|
|
660
815
|
// src/utils/registry-schema.ts
|
|
@@ -736,7 +891,7 @@ function parseRegistry(value, where) {
|
|
|
736
891
|
async function holdsItem(file, name) {
|
|
737
892
|
let value;
|
|
738
893
|
try {
|
|
739
|
-
value = JSON.parse(await
|
|
894
|
+
value = JSON.parse(await fs8.readFile(file, "utf8"));
|
|
740
895
|
} catch (error) {
|
|
741
896
|
if (error instanceof SyntaxError) return false;
|
|
742
897
|
throw error;
|
|
@@ -749,33 +904,33 @@ async function holdsItem(file, name) {
|
|
|
749
904
|
}
|
|
750
905
|
}
|
|
751
906
|
async function runBuild(registryFile, opts) {
|
|
752
|
-
const cwd =
|
|
753
|
-
const registryPath =
|
|
754
|
-
const outputDir =
|
|
755
|
-
const registry = parseRegistry(JSON.parse(await
|
|
756
|
-
const problems = await checkRegistryDependencies(registry,
|
|
907
|
+
const cwd = path12.resolve(opts.cwd);
|
|
908
|
+
const registryPath = path12.resolve(cwd, registryFile);
|
|
909
|
+
const outputDir = path12.resolve(cwd, opts.output);
|
|
910
|
+
const registry = parseRegistry(JSON.parse(await fs8.readFile(registryPath, "utf8")), registryPath);
|
|
911
|
+
const problems = await checkRegistryDependencies(registry, path12.dirname(registryPath));
|
|
757
912
|
if (problems.length > 0) {
|
|
758
913
|
for (const problem of problems) console.error(`registry: ${problem}`);
|
|
759
914
|
return 1;
|
|
760
915
|
}
|
|
761
|
-
await
|
|
916
|
+
await fs8.mkdir(outputDir, { recursive: true });
|
|
762
917
|
const catalog = [];
|
|
763
918
|
for (const item of registry.items) {
|
|
764
919
|
const built = {
|
|
765
920
|
...item,
|
|
766
921
|
files: await Promise.all(
|
|
767
922
|
item.files.map(async (file) => {
|
|
768
|
-
const abs =
|
|
769
|
-
const
|
|
923
|
+
const abs = path12.resolve(path12.dirname(registryPath), file.path);
|
|
924
|
+
const source = await fs8.readFile(abs, "utf8");
|
|
770
925
|
return {
|
|
771
926
|
...file,
|
|
772
|
-
content
|
|
927
|
+
content: opts.publish ? opts.publish(source, file.path) : source
|
|
773
928
|
};
|
|
774
929
|
})
|
|
775
930
|
)
|
|
776
931
|
};
|
|
777
|
-
const outFile =
|
|
778
|
-
await
|
|
932
|
+
const outFile = path12.join(outputDir, `${item.name}.json`);
|
|
933
|
+
await fs8.writeFile(outFile, `${JSON.stringify(built, null, 2)}
|
|
779
934
|
`, "utf8");
|
|
780
935
|
catalog.push({
|
|
781
936
|
name: item.name,
|
|
@@ -789,24 +944,24 @@ async function runBuild(registryFile, opts) {
|
|
|
789
944
|
target: file.target
|
|
790
945
|
}))
|
|
791
946
|
});
|
|
792
|
-
console.log(`built ${
|
|
947
|
+
console.log(`built ${path12.relative(cwd, outFile)}`);
|
|
793
948
|
}
|
|
794
|
-
const indexPath =
|
|
795
|
-
await
|
|
949
|
+
const indexPath = path12.join(outputDir, "index.json");
|
|
950
|
+
await fs8.writeFile(
|
|
796
951
|
indexPath,
|
|
797
952
|
`${JSON.stringify({ name: registry.name, homepage: registry.homepage, items: catalog }, null, 2)}
|
|
798
953
|
`,
|
|
799
954
|
"utf8"
|
|
800
955
|
);
|
|
801
|
-
console.log(`built ${
|
|
956
|
+
console.log(`built ${path12.relative(cwd, indexPath)}`);
|
|
802
957
|
const written = /* @__PURE__ */ new Set([...registry.items.map((item) => `${item.name}.json`), "index.json"]);
|
|
803
|
-
for (const entry of await
|
|
958
|
+
for (const entry of await fs8.readdir(outputDir, { withFileTypes: true })) {
|
|
804
959
|
if (!entry.isFile() || !entry.name.endsWith(".json") || written.has(entry.name)) continue;
|
|
805
|
-
const stale =
|
|
960
|
+
const stale = path12.join(outputDir, entry.name);
|
|
806
961
|
const name = entry.name.slice(0, -".json".length);
|
|
807
962
|
if (!await holdsItem(stale, name)) continue;
|
|
808
|
-
await
|
|
809
|
-
console.log(`removed ${
|
|
963
|
+
await fs8.rm(stale);
|
|
964
|
+
console.log(`removed ${path12.relative(cwd, stale)} (not in ${path12.basename(registryPath)})`);
|
|
810
965
|
}
|
|
811
966
|
return 0;
|
|
812
967
|
}
|
|
@@ -822,8 +977,9 @@ async function runDiff(names, opts) {
|
|
|
822
977
|
console.log("No nsh-ui.json \u2014 run init first.");
|
|
823
978
|
return 1;
|
|
824
979
|
}
|
|
980
|
+
const installedHere = (item) => isInstalled(ws.uiRoot, config, item) && (item.type !== "registry:theme" || item.name === config.theme);
|
|
825
981
|
const index = await fetchIndex(config, opts.registry);
|
|
826
|
-
const targets = names.length ? names : index.items.filter(
|
|
982
|
+
const targets = names.length ? names : index.items.filter(installedHere).map((item) => item.name);
|
|
827
983
|
if (targets.length === 0) {
|
|
828
984
|
console.log("Nothing installed.");
|
|
829
985
|
return 0;
|
|
@@ -866,12 +1022,13 @@ import { Command as Command5 } from "commander";
|
|
|
866
1022
|
async function runList(opts) {
|
|
867
1023
|
const { ws, config } = await readApp(opts.cwd, opts.registry);
|
|
868
1024
|
const index = await fetchIndex(config, opts.registry);
|
|
1025
|
+
const installedHere = (item) => isInstalled(ws.uiRoot, config, item) && (item.type !== "registry:theme" || item.name === config.theme);
|
|
869
1026
|
const rows = index.items.map((item) => ({
|
|
870
1027
|
name: item.name,
|
|
871
1028
|
type: item.type,
|
|
872
1029
|
title: item.title,
|
|
873
1030
|
description: item.description,
|
|
874
|
-
installed:
|
|
1031
|
+
installed: installedHere(item)
|
|
875
1032
|
}));
|
|
876
1033
|
if (opts.json) {
|
|
877
1034
|
console.log(
|
|
@@ -896,20 +1053,20 @@ var list = new Command5().name("list").description("List the registry's items, m
|
|
|
896
1053
|
});
|
|
897
1054
|
|
|
898
1055
|
// src/commands/serve.ts
|
|
899
|
-
import
|
|
1056
|
+
import fs9 from "fs/promises";
|
|
900
1057
|
import http from "http";
|
|
901
|
-
import
|
|
1058
|
+
import path13 from "path";
|
|
902
1059
|
import { Command as Command6 } from "commander";
|
|
903
1060
|
function fileUnder(root, pathname) {
|
|
904
|
-
const file =
|
|
905
|
-
const relative =
|
|
906
|
-
if (relative === "" || relative === ".." || relative.startsWith(`..${
|
|
1061
|
+
const file = path13.join(root, pathname);
|
|
1062
|
+
const relative = path13.relative(root, file);
|
|
1063
|
+
if (relative === "" || relative === ".." || relative.startsWith(`..${path13.sep}`) || path13.isAbsolute(relative)) {
|
|
907
1064
|
return null;
|
|
908
1065
|
}
|
|
909
1066
|
return file;
|
|
910
1067
|
}
|
|
911
1068
|
async function startRegistryServer(opts) {
|
|
912
|
-
const root =
|
|
1069
|
+
const root = path13.resolve(opts.dir);
|
|
913
1070
|
const port = opts.port ?? 0;
|
|
914
1071
|
const server = http.createServer(async (req, res) => {
|
|
915
1072
|
try {
|
|
@@ -925,7 +1082,7 @@ async function startRegistryServer(opts) {
|
|
|
925
1082
|
return;
|
|
926
1083
|
}
|
|
927
1084
|
try {
|
|
928
|
-
const body = await
|
|
1085
|
+
const body = await fs9.readFile(filePath);
|
|
929
1086
|
res.writeHead(200, {
|
|
930
1087
|
"content-type": "application/json; charset=utf-8",
|
|
931
1088
|
"access-control-allow-origin": "*"
|
|
@@ -956,7 +1113,7 @@ async function startRegistryServer(opts) {
|
|
|
956
1113
|
};
|
|
957
1114
|
}
|
|
958
1115
|
var serve = new Command6().name("serve").description("Serve public/r over HTTP for local CLI testing").option("-p, --port <port>", "port (0 picks a free one)", "4173").option("-d, --dir <path>", "registry directory", "./public/r").option("-c, --cwd <cwd>", "working directory", process.cwd()).action(async (opts) => {
|
|
959
|
-
const root =
|
|
1116
|
+
const root = path13.resolve(opts.cwd, opts.dir);
|
|
960
1117
|
const server = await startRegistryServer({ dir: root, port: Number(opts.port) });
|
|
961
1118
|
console.log(`Serving ${root}`);
|
|
962
1119
|
console.log(` ${server.url}/index.json`);
|