@shadow-js/ui 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.md +3 -3
- package/dist/cli.mjs +453 -262
- package/dist/dev/theme/theme-provider.js +1 -1
- package/dist/dev/theme/theme-provider.js.map +1 -1
- package/dist/prod/theme/theme-provider.js +1 -1
- package/dist/prod/theme/theme-provider.js.map +1 -1
- package/package.json +11 -11
- package/registry/carousel/carousel.tsx +79 -15
- package/registry/chart/chart.tsx +5 -1
- package/registry/input-group/input-group.tsx +2 -1
- package/registry/resizable/resizable.tsx +175 -34
- package/registry.json +7 -3
package/dist/cli.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
|
-
import {
|
|
5
|
-
import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, readlinkSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
6
5
|
import { parseArgs } from "node:util";
|
|
6
|
+
import { createInterface } from "node:readline/promises";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
// src/cli/fs.ts
|
|
9
9
|
/**
|
|
@@ -11,6 +11,59 @@ import { fileURLToPath } from "node:url";
|
|
|
11
11
|
* directory or an in-memory tree, and so every path goes through one place.
|
|
12
12
|
* @packageDocumentation
|
|
13
13
|
*/
|
|
14
|
+
/** Symbolic links followed while resolving one path; the same bound the operating system uses. */
|
|
15
|
+
const MAX_LINK_HOPS = 40;
|
|
16
|
+
/** Folders `ancestors` climbs at most: far beyond any real project, so a path loop cannot spin. */
|
|
17
|
+
const MAX_LEVELS = 64;
|
|
18
|
+
/**
|
|
19
|
+
* `start` and its parent folders up to the filesystem root, nearest first (at most 64).
|
|
20
|
+
*
|
|
21
|
+
* @param start An absolute folder.
|
|
22
|
+
* @returns The folders, `start` first.
|
|
23
|
+
* @example
|
|
24
|
+
* ancestors("/a/b"); // ["/a/b", "/a", "/"]
|
|
25
|
+
* @since 1.0.0
|
|
26
|
+
*
|
|
27
|
+
* @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
|
|
28
|
+
*/
|
|
29
|
+
function ancestors(start) {
|
|
30
|
+
const folders = [start];
|
|
31
|
+
for (let folder = dirname(start); folders.length < MAX_LEVELS; folder = dirname(folder)) {
|
|
32
|
+
if (folder === folders[folders.length - 1]) break;
|
|
33
|
+
folders.push(folder);
|
|
34
|
+
}
|
|
35
|
+
return folders;
|
|
36
|
+
}
|
|
37
|
+
/** Whether a failed file call means "nothing is there" (or a file stands where a directory should). */
|
|
38
|
+
function isMissing(error) {
|
|
39
|
+
const code = error.code;
|
|
40
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The real path of `path`, where the part that does not exist yet is kept as written. A symbolic
|
|
44
|
+
* link whose target is missing is followed to that target: a write through it would create the
|
|
45
|
+
* target, so the guard has to see where it is.
|
|
46
|
+
*/
|
|
47
|
+
function resolveReal(path, hops) {
|
|
48
|
+
try {
|
|
49
|
+
return realpathSync(path);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (!isMissing(error)) throw error;
|
|
52
|
+
}
|
|
53
|
+
let link;
|
|
54
|
+
try {
|
|
55
|
+
link = readlinkSync(path);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (!isMissing(error) && error.code !== "EINVAL") throw error;
|
|
58
|
+
}
|
|
59
|
+
if (link !== void 0) {
|
|
60
|
+
if (hops >= MAX_LINK_HOPS) throw Object.assign(/* @__PURE__ */ new Error(`Too many symbolic links: ${path}`), { code: "ELOOP" });
|
|
61
|
+
return resolveReal(resolve(resolveReal(dirname(path), hops + 1), link), hops + 1);
|
|
62
|
+
}
|
|
63
|
+
const parent = dirname(path);
|
|
64
|
+
if (parent === path) return path;
|
|
65
|
+
return join(resolveReal(parent, hops), basename(path));
|
|
66
|
+
}
|
|
14
67
|
/**
|
|
15
68
|
* The real filesystem.
|
|
16
69
|
*
|
|
@@ -26,7 +79,7 @@ function nodeFs() {
|
|
|
26
79
|
exists: (path) => existsSync(path),
|
|
27
80
|
read(path) {
|
|
28
81
|
try {
|
|
29
|
-
return readFileSync(path, "utf8");
|
|
82
|
+
return readFileSync(path, "utf8").replace(/^\uFEFF/, "");
|
|
30
83
|
} catch (error) {
|
|
31
84
|
if (error.code === "ENOENT") return void 0;
|
|
32
85
|
throw error;
|
|
@@ -40,15 +93,7 @@ function nodeFs() {
|
|
|
40
93
|
rmSync(path, { force: true });
|
|
41
94
|
},
|
|
42
95
|
realpath(path) {
|
|
43
|
-
|
|
44
|
-
let current = path;
|
|
45
|
-
while (!existsSync(current)) {
|
|
46
|
-
const parent = dirname(current);
|
|
47
|
-
if (parent === current) return path;
|
|
48
|
-
tail.unshift(basename(current));
|
|
49
|
-
current = parent;
|
|
50
|
-
}
|
|
51
|
-
return join(realpathSync(current), ...tail);
|
|
96
|
+
return resolveReal(path, 0);
|
|
52
97
|
}
|
|
53
98
|
};
|
|
54
99
|
}
|
|
@@ -210,25 +255,37 @@ function installCrashHandlers(argv) {
|
|
|
210
255
|
process.on("uncaughtException", crash);
|
|
211
256
|
process.on("unhandledRejection", crash);
|
|
212
257
|
}
|
|
213
|
-
/** The
|
|
214
|
-
function
|
|
215
|
-
|
|
216
|
-
const m = b.length;
|
|
217
|
-
const max = n + m;
|
|
218
|
-
if (max > 2e4) return [...a.map((line) => ({
|
|
258
|
+
/** The whole of `a` deleted and the whole of `b` inserted: always correct, never minimal. */
|
|
259
|
+
function replacement(a, b) {
|
|
260
|
+
return [...a.map((line) => ({
|
|
219
261
|
kind: "delete",
|
|
220
262
|
line
|
|
221
263
|
})), ...b.map((line) => ({
|
|
222
264
|
kind: "insert",
|
|
223
265
|
line
|
|
224
266
|
}))];
|
|
267
|
+
}
|
|
268
|
+
/** The edit script turning `a` into `b`; one big replacement when the texts are too large or too different. */
|
|
269
|
+
function edits(a, b) {
|
|
270
|
+
const n = a.length;
|
|
271
|
+
const m = b.length;
|
|
272
|
+
const max = n + m;
|
|
273
|
+
if (max > 2e4) return replacement(a, b);
|
|
225
274
|
const offset = max;
|
|
226
|
-
|
|
275
|
+
const frontier = new Int32Array(2 * max + 2);
|
|
276
|
+
/** The frontier before each round, only the diagonals the next round can read: `[offset - d - 1, offset + d + 1]`. */
|
|
227
277
|
const trace = [];
|
|
278
|
+
let kept = 0;
|
|
228
279
|
let reached = -1;
|
|
229
280
|
for (let d = 0; d <= max && reached === -1; d += 1) {
|
|
230
|
-
|
|
231
|
-
const
|
|
281
|
+
const from = Math.max(0, offset - d - 1);
|
|
282
|
+
const cells = frontier.slice(from, Math.min(frontier.length, offset + d + 2));
|
|
283
|
+
kept += cells.length;
|
|
284
|
+
if (kept > 16e6) return replacement(a, b);
|
|
285
|
+
trace.push({
|
|
286
|
+
from,
|
|
287
|
+
cells
|
|
288
|
+
});
|
|
232
289
|
for (let k = -d; k <= d; k += 2) {
|
|
233
290
|
let x;
|
|
234
291
|
const below = frontier[offset + k - 1] ?? 0;
|
|
@@ -240,24 +297,24 @@ function edits(a, b) {
|
|
|
240
297
|
x += 1;
|
|
241
298
|
y += 1;
|
|
242
299
|
}
|
|
243
|
-
|
|
300
|
+
frontier[offset + k] = x;
|
|
244
301
|
if (x >= n && y >= m) {
|
|
245
302
|
reached = d;
|
|
246
303
|
break;
|
|
247
304
|
}
|
|
248
305
|
}
|
|
249
|
-
frontier = next;
|
|
250
306
|
}
|
|
251
307
|
const result = [];
|
|
252
308
|
let x = n;
|
|
253
309
|
let y = m;
|
|
254
310
|
for (let d = reached; d > 0; d -= 1) {
|
|
255
|
-
const previous = trace[d]
|
|
311
|
+
const previous = trace[d];
|
|
312
|
+
const at = (index) => previous === void 0 ? 0 : previous.cells[index - previous.from] ?? 0;
|
|
256
313
|
const k = x - y;
|
|
257
|
-
const below =
|
|
258
|
-
const above =
|
|
314
|
+
const below = at(offset + k - 1);
|
|
315
|
+
const above = at(offset + k + 1);
|
|
259
316
|
const previousK = k === -d || k !== d && below < above ? k + 1 : k - 1;
|
|
260
|
-
const previousX =
|
|
317
|
+
const previousX = at(offset + previousK);
|
|
261
318
|
const previousY = previousX - previousK;
|
|
262
319
|
while (x > previousX && y > previousY) {
|
|
263
320
|
x -= 1;
|
|
@@ -371,6 +428,186 @@ function unifiedDiff(before, after, labels, context = 3) {
|
|
|
371
428
|
...hunks
|
|
372
429
|
].join("\n");
|
|
373
430
|
}
|
|
431
|
+
// src/cli/schema.ts
|
|
432
|
+
/**
|
|
433
|
+
* The base colors, in documentation order.
|
|
434
|
+
* @since 1.0.0
|
|
435
|
+
*
|
|
436
|
+
* @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
|
|
437
|
+
*/
|
|
438
|
+
const BASE_COLORS = [
|
|
439
|
+
"neutral",
|
|
440
|
+
"zinc",
|
|
441
|
+
"slate",
|
|
442
|
+
"stone",
|
|
443
|
+
"gray"
|
|
444
|
+
];
|
|
445
|
+
/**
|
|
446
|
+
* The default aliases (shadcn's).
|
|
447
|
+
* @since 1.0.0
|
|
448
|
+
*
|
|
449
|
+
* @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
|
|
450
|
+
*/
|
|
451
|
+
const DEFAULT_ALIASES = {
|
|
452
|
+
components: "@/components",
|
|
453
|
+
ui: "@/components/ui",
|
|
454
|
+
lib: "@/lib",
|
|
455
|
+
utils: "@/lib/utils",
|
|
456
|
+
hooks: "@/hooks"
|
|
457
|
+
};
|
|
458
|
+
/** Registry item names are kebab-case; nothing else may reach a file path. */
|
|
459
|
+
const NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
460
|
+
/** A file name inside `registry/<name>/`. */
|
|
461
|
+
const FILE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
462
|
+
/**
|
|
463
|
+
* Whether `name` is a valid registry item name.
|
|
464
|
+
*
|
|
465
|
+
* @param name Any string.
|
|
466
|
+
* @returns `true` for kebab-case names such as `dropdown-menu`.
|
|
467
|
+
* @example
|
|
468
|
+
* isValidItemName("../etc/passwd"); // false
|
|
469
|
+
* @since 1.0.0
|
|
470
|
+
*
|
|
471
|
+
* @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
|
|
472
|
+
*/
|
|
473
|
+
function isValidItemName(name) {
|
|
474
|
+
return NAME_PATTERN.test(name);
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Thrown for malformed registry or configuration data.
|
|
478
|
+
* @since 1.0.0
|
|
479
|
+
*
|
|
480
|
+
* @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
|
|
481
|
+
*/
|
|
482
|
+
var SchemaError = class extends Error {
|
|
483
|
+
/** The stable code `--json` prints. */
|
|
484
|
+
code = "SHADOW_UI_INVALID_FILE";
|
|
485
|
+
constructor(message) {
|
|
486
|
+
super(message);
|
|
487
|
+
this.name = "SchemaError";
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
function record(value, where) {
|
|
491
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new SchemaError(`${where} must be an object`);
|
|
492
|
+
return value;
|
|
493
|
+
}
|
|
494
|
+
function text(value, where) {
|
|
495
|
+
if (typeof value !== "string" || value === "") throw new SchemaError(`${where} must be a string`);
|
|
496
|
+
return value;
|
|
497
|
+
}
|
|
498
|
+
function strings(value, where) {
|
|
499
|
+
if (!Array.isArray(value)) throw new SchemaError(`${where} must be an array`);
|
|
500
|
+
return value.map((entry, index) => text(entry, `${where}[${index}]`));
|
|
501
|
+
}
|
|
502
|
+
function itemType(value, where) {
|
|
503
|
+
if (value === "ui" || value === "lib" || value === "hook") return value;
|
|
504
|
+
throw new SchemaError(`${where} must be "ui", "lib" or "hook"`);
|
|
505
|
+
}
|
|
506
|
+
function parseItem(value, index) {
|
|
507
|
+
const where = `items[${index}]`;
|
|
508
|
+
const item = record(value, where);
|
|
509
|
+
const name = text(item["name"], `${where}.name`);
|
|
510
|
+
if (!isValidItemName(name)) throw new SchemaError(`${where}.name "${name}" is not a valid name`);
|
|
511
|
+
const files = item["files"];
|
|
512
|
+
if (!Array.isArray(files) || files.length === 0) throw new SchemaError(`${where}.files must be a non-empty array`);
|
|
513
|
+
const dependencies = record(item["dependencies"], `${where}.dependencies`);
|
|
514
|
+
const tailwind = record(item["tailwind"], `${where}.tailwind`);
|
|
515
|
+
return {
|
|
516
|
+
name,
|
|
517
|
+
type: itemType(item["type"], `${where}.type`),
|
|
518
|
+
title: text(item["title"], `${where}.title`),
|
|
519
|
+
description: text(item["description"], `${where}.description`),
|
|
520
|
+
files: files.map((file, fileIndex) => {
|
|
521
|
+
const entry = record(file, `${where}.files[${fileIndex}]`);
|
|
522
|
+
const path = text(entry["path"], `${where}.files[${fileIndex}].path`);
|
|
523
|
+
const segments = path.split("/");
|
|
524
|
+
const [root, folder, base] = segments;
|
|
525
|
+
if (segments.length !== 3 || root !== "registry" || folder !== name || base === void 0 || !FILE_PATTERN.test(base)) throw new SchemaError(`${where}.files[${fileIndex}].path "${path}" must be registry/${name}/<file>`);
|
|
526
|
+
return {
|
|
527
|
+
path,
|
|
528
|
+
type: itemType(entry["type"], `${where}.files[${fileIndex}].type`)
|
|
529
|
+
};
|
|
530
|
+
}),
|
|
531
|
+
registryDependencies: strings(item["registryDependencies"], `${where}.registryDependencies`),
|
|
532
|
+
dependencies: Object.fromEntries(Object.entries(dependencies).map(([pkg, version]) => [pkg, text(version, `${where}.dependencies.${pkg}`)])),
|
|
533
|
+
exports: strings(item["exports"], `${where}.exports`),
|
|
534
|
+
primitives: strings(item["primitives"], `${where}.primitives`),
|
|
535
|
+
tailwind: {
|
|
536
|
+
tokens: strings(tailwind["tokens"], `${where}.tailwind.tokens`),
|
|
537
|
+
animations: tailwind["animations"] === true
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Validates parsed `registry.json` data.
|
|
543
|
+
*
|
|
544
|
+
* @param value The parsed JSON.
|
|
545
|
+
* @returns The typed index.
|
|
546
|
+
* @throws SchemaError naming the first malformed field.
|
|
547
|
+
* @example
|
|
548
|
+
* const index = parseRegistryIndex(JSON.parse(readFileSync("registry.json", "utf8")));
|
|
549
|
+
* @since 1.0.0
|
|
550
|
+
*
|
|
551
|
+
* @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
|
|
552
|
+
*/
|
|
553
|
+
function parseRegistryIndex(value) {
|
|
554
|
+
const root = record(value, "registry");
|
|
555
|
+
const items = root["items"];
|
|
556
|
+
if (!Array.isArray(items)) throw new SchemaError("registry.items must be an array");
|
|
557
|
+
const parsed = items.map(parseItem);
|
|
558
|
+
const seen = /* @__PURE__ */ new Set();
|
|
559
|
+
for (const item of parsed) {
|
|
560
|
+
if (seen.has(item.name)) throw new SchemaError(`registry has two items named "${item.name}"`);
|
|
561
|
+
seen.add(item.name);
|
|
562
|
+
}
|
|
563
|
+
return {
|
|
564
|
+
name: text(root["name"], "registry.name"),
|
|
565
|
+
version: text(root["version"], "registry.version"),
|
|
566
|
+
frameworkDependencies: strings(root["frameworkDependencies"], "registry.frameworkDependencies"),
|
|
567
|
+
items: parsed
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Validates parsed `components.json` data.
|
|
572
|
+
*
|
|
573
|
+
* @param value The parsed JSON.
|
|
574
|
+
* @returns The typed configuration, with defaults for missing aliases.
|
|
575
|
+
* @throws SchemaError naming the first malformed field.
|
|
576
|
+
* @example
|
|
577
|
+
* const config = parseComponentsConfig(JSON.parse(readFileSync("components.json", "utf8")));
|
|
578
|
+
* @since 1.0.0
|
|
579
|
+
*
|
|
580
|
+
* @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
|
|
581
|
+
*/
|
|
582
|
+
function parseComponentsConfig(value) {
|
|
583
|
+
const root = record(value, "components.json");
|
|
584
|
+
const tailwind = record(root["tailwind"], "components.json tailwind");
|
|
585
|
+
const baseColor = tailwind["baseColor"] ?? "neutral";
|
|
586
|
+
if (!BASE_COLORS.includes(baseColor)) throw new SchemaError(`components.json tailwind.baseColor must be one of ${BASE_COLORS.join(", ")}`);
|
|
587
|
+
const given = root["aliases"] === void 0 ? {} : record(root["aliases"], "components.json aliases");
|
|
588
|
+
const alias = (key) => {
|
|
589
|
+
const entry = given[key];
|
|
590
|
+
if (entry === void 0) return DEFAULT_ALIASES[key];
|
|
591
|
+
const aliasText = text(entry, `components.json aliases.${key}`);
|
|
592
|
+
if (aliasText.includes("..") || /\s/.test(aliasText)) throw new SchemaError(`components.json aliases.${key} "${aliasText}" is not a valid alias`);
|
|
593
|
+
return aliasText;
|
|
594
|
+
};
|
|
595
|
+
const framework = root["framework"];
|
|
596
|
+
return {
|
|
597
|
+
framework: framework === "vite" || framework === "dusk" ? framework : "unknown",
|
|
598
|
+
tailwind: {
|
|
599
|
+
css: text(tailwind["css"], "components.json tailwind.css"),
|
|
600
|
+
baseColor
|
|
601
|
+
},
|
|
602
|
+
aliases: {
|
|
603
|
+
components: alias("components"),
|
|
604
|
+
ui: alias("ui"),
|
|
605
|
+
lib: alias("lib"),
|
|
606
|
+
utils: alias("utils"),
|
|
607
|
+
hooks: alias("hooks")
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
}
|
|
374
611
|
// src/cli/paths.ts
|
|
375
612
|
/**
|
|
376
613
|
* Path handling: reading `tsconfig` path aliases, mapping an import alias to a directory, and the
|
|
@@ -459,9 +696,59 @@ function parseJsonc(text) {
|
|
|
459
696
|
function asRecord(value) {
|
|
460
697
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
461
698
|
}
|
|
699
|
+
/** The `extends` links followed from one tsconfig: far beyond any real chain. */
|
|
700
|
+
const MAX_EXTENDS = 16;
|
|
701
|
+
/** The file an `extends` entry names, or `undefined`: a path, or a package looked up in `node_modules`. */
|
|
702
|
+
function extendsFile(fs, folder, entry) {
|
|
703
|
+
const candidates = (base) => base.endsWith(".json") ? [base] : [`${base}.json`, join(base, "tsconfig.json")];
|
|
704
|
+
if (entry.startsWith(".") || isAbsolute(entry)) return candidates(resolve(folder, entry)).find((file) => fs.exists(file));
|
|
705
|
+
for (const parent of ancestors(folder)) {
|
|
706
|
+
const found = candidates(join(parent, "node_modules", entry)).find((file) => fs.exists(file));
|
|
707
|
+
if (found !== void 0) return found;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* The `paths` and `baseUrl` of one tsconfig with its `extends` merged in, as TypeScript does: later
|
|
712
|
+
* bases override earlier ones, the config itself overrides its bases, and `paths` is replaced as a
|
|
713
|
+
* whole. `seen` holds the chain so far, so a cycle or a very long chain ends.
|
|
714
|
+
*/
|
|
715
|
+
function readOptions(fs, file, label, seen) {
|
|
716
|
+
const text = fs.read(file);
|
|
717
|
+
if (text === void 0) return void 0;
|
|
718
|
+
let parsed;
|
|
719
|
+
try {
|
|
720
|
+
parsed = parseJsonc(text);
|
|
721
|
+
} catch (error) {
|
|
722
|
+
if (error instanceof SyntaxError) throw new SchemaError(`${label} is not valid JSON: ${error.message}`);
|
|
723
|
+
throw error;
|
|
724
|
+
}
|
|
725
|
+
const config = asRecord(parsed);
|
|
726
|
+
const options = asRecord(config?.["compilerOptions"]);
|
|
727
|
+
const folder = dirname(file);
|
|
728
|
+
let merged = {};
|
|
729
|
+
const bases = [config?.["extends"]].flat();
|
|
730
|
+
for (const entry of seen.length < MAX_EXTENDS ? bases : []) {
|
|
731
|
+
const base = typeof entry === "string" ? extendsFile(fs, folder, entry) : void 0;
|
|
732
|
+
if (base !== void 0 && !seen.includes(base)) merged = {
|
|
733
|
+
...merged,
|
|
734
|
+
...readOptions(fs, base, base, [...seen, base])
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
const paths = asRecord(options?.["paths"]);
|
|
738
|
+
const baseUrl = options?.["baseUrl"];
|
|
739
|
+
return {
|
|
740
|
+
...merged,
|
|
741
|
+
...paths !== void 0 && {
|
|
742
|
+
paths,
|
|
743
|
+
pathsDir: folder
|
|
744
|
+
},
|
|
745
|
+
...typeof baseUrl === "string" && { baseUrl: resolve(folder, baseUrl) }
|
|
746
|
+
};
|
|
747
|
+
}
|
|
462
748
|
/**
|
|
463
749
|
* Reads the `compilerOptions.paths` of the project's tsconfig files (`tsconfig.json`,
|
|
464
|
-
* `tsconfig.app.json`)
|
|
750
|
+
* `tsconfig.app.json`), following their `extends` (relative files and packages in `node_modules`).
|
|
751
|
+
* Targets are resolved against `baseUrl` or the directory of the config that declares `paths`.
|
|
465
752
|
*
|
|
466
753
|
* @param fs The project filesystem.
|
|
467
754
|
* @param root The project root.
|
|
@@ -475,12 +762,10 @@ function asRecord(value) {
|
|
|
475
762
|
function readPathMappings(fs, root) {
|
|
476
763
|
const mappings = [];
|
|
477
764
|
for (const name of ["tsconfig.json", "tsconfig.app.json"]) {
|
|
478
|
-
const
|
|
479
|
-
|
|
480
|
-
const options = asRecord(asRecord(parseJsonc(text))?.["compilerOptions"]);
|
|
481
|
-
const paths = asRecord(options?.["paths"]);
|
|
765
|
+
const file = join(root, name);
|
|
766
|
+
const { paths, pathsDir, baseUrl } = readOptions(fs, file, name, [file]) ?? {};
|
|
482
767
|
if (paths === void 0) continue;
|
|
483
|
-
const base =
|
|
768
|
+
const base = baseUrl ?? pathsDir ?? root;
|
|
484
769
|
for (const [pattern, targets] of Object.entries(paths)) {
|
|
485
770
|
if (!Array.isArray(targets)) continue;
|
|
486
771
|
mappings.push({
|
|
@@ -507,19 +792,26 @@ function readPathMappings(fs, root) {
|
|
|
507
792
|
* @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
|
|
508
793
|
*/
|
|
509
794
|
function aliasToDirectory(alias, mappings) {
|
|
795
|
+
let best;
|
|
510
796
|
for (const { pattern, targets } of mappings) {
|
|
797
|
+
const target = targets[0];
|
|
798
|
+
if (target === void 0) continue;
|
|
511
799
|
const star = pattern.indexOf("*");
|
|
512
800
|
if (star === -1) {
|
|
513
|
-
if (pattern === alias
|
|
801
|
+
if (pattern === alias) return target;
|
|
514
802
|
continue;
|
|
515
803
|
}
|
|
516
804
|
const prefix = pattern.slice(0, star);
|
|
517
805
|
const suffix = pattern.slice(star + 1);
|
|
518
|
-
if (
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
806
|
+
if (alias.startsWith(prefix) && alias.endsWith(suffix) && alias.length >= prefix.length + suffix.length && (best === void 0 || prefix.length > best.length)) {
|
|
807
|
+
const rest = alias.slice(prefix.length, alias.length - suffix.length);
|
|
808
|
+
best = {
|
|
809
|
+
length: prefix.length,
|
|
810
|
+
directory: target.replace("*", rest)
|
|
811
|
+
};
|
|
812
|
+
}
|
|
522
813
|
}
|
|
814
|
+
if (best !== void 0) return best.directory;
|
|
523
815
|
throw new PathError(`The alias "${alias}" is not covered by "paths" in tsconfig.json. Add, for example:\n "compilerOptions": { "paths": { "@/*": ["./src/*"] } }`);
|
|
524
816
|
}
|
|
525
817
|
/**
|
|
@@ -569,12 +861,31 @@ function resolveInsideReal(fs, root, target) {
|
|
|
569
861
|
// src/cli/pm.ts
|
|
570
862
|
/**
|
|
571
863
|
* Package manager detection and the install command line. npm is the default; pnpm, yarn and bun
|
|
572
|
-
* are used when their lockfile (or the `packageManager` field) says so.
|
|
864
|
+
* are used when their lockfile (or the `packageManager` field) says so, in the project or a parent folder.
|
|
573
865
|
* @packageDocumentation
|
|
574
866
|
*/
|
|
867
|
+
/** The package manager one folder says it uses (field first, then lockfiles), or `undefined`. */
|
|
868
|
+
function managerOf(fs, folder) {
|
|
869
|
+
const manifest = fs.read(join(folder, "package.json"));
|
|
870
|
+
if (manifest !== void 0) try {
|
|
871
|
+
const field = JSON.parse(manifest).packageManager;
|
|
872
|
+
if (typeof field === "string") {
|
|
873
|
+
const name = field.split("@")[0];
|
|
874
|
+
if (name === "npm" || name === "pnpm" || name === "yarn" || name === "bun") return name;
|
|
875
|
+
}
|
|
876
|
+
} catch (error) {
|
|
877
|
+
if (!(error instanceof SyntaxError)) throw error;
|
|
878
|
+
}
|
|
879
|
+
if (fs.exists(join(folder, "pnpm-lock.yaml"))) return "pnpm";
|
|
880
|
+
if (fs.exists(join(folder, "yarn.lock"))) return "yarn";
|
|
881
|
+
if (fs.exists(join(folder, "bun.lock")) || fs.exists(join(folder, "bun.lockb"))) return "bun";
|
|
882
|
+
if (fs.exists(join(folder, "package-lock.json"))) return "npm";
|
|
883
|
+
}
|
|
575
884
|
/**
|
|
576
|
-
* Detects the project's package manager
|
|
577
|
-
*
|
|
885
|
+
* Detects the project's package manager. In the project folder and then each parent folder (a
|
|
886
|
+
* package inside a workspace is managed from the workspace root) it reads the `packageManager`
|
|
887
|
+
* field of `package.json`, then the lockfiles; the nearest folder that says anything wins, and npm
|
|
888
|
+
* is the default.
|
|
578
889
|
*
|
|
579
890
|
* @param fs The project filesystem.
|
|
580
891
|
* @param root The project root.
|
|
@@ -586,19 +897,10 @@ function resolveInsideReal(fs, root, target) {
|
|
|
586
897
|
* @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
|
|
587
898
|
*/
|
|
588
899
|
function detectPackageManager(fs, root) {
|
|
589
|
-
const
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
if (typeof field === "string") {
|
|
593
|
-
const name = field.split("@")[0];
|
|
594
|
-
if (name === "npm" || name === "pnpm" || name === "yarn" || name === "bun") return name;
|
|
595
|
-
}
|
|
596
|
-
} catch (error) {
|
|
597
|
-
if (!(error instanceof SyntaxError)) throw error;
|
|
900
|
+
for (const folder of ancestors(root)) {
|
|
901
|
+
const manager = managerOf(fs, folder);
|
|
902
|
+
if (manager !== void 0) return manager;
|
|
598
903
|
}
|
|
599
|
-
if (fs.exists(join(root, "pnpm-lock.yaml"))) return "pnpm";
|
|
600
|
-
if (fs.exists(join(root, "yarn.lock"))) return "yarn";
|
|
601
|
-
if (fs.exists(join(root, "bun.lock")) || fs.exists(join(root, "bun.lockb"))) return "bun";
|
|
602
904
|
return "npm";
|
|
603
905
|
}
|
|
604
906
|
/**
|
|
@@ -633,186 +935,6 @@ function installCommand(manager, packages) {
|
|
|
633
935
|
};
|
|
634
936
|
}
|
|
635
937
|
}
|
|
636
|
-
// src/cli/schema.ts
|
|
637
|
-
/**
|
|
638
|
-
* The base colors, in documentation order.
|
|
639
|
-
* @since 1.0.0
|
|
640
|
-
*
|
|
641
|
-
* @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
|
|
642
|
-
*/
|
|
643
|
-
const BASE_COLORS = [
|
|
644
|
-
"neutral",
|
|
645
|
-
"zinc",
|
|
646
|
-
"slate",
|
|
647
|
-
"stone",
|
|
648
|
-
"gray"
|
|
649
|
-
];
|
|
650
|
-
/**
|
|
651
|
-
* The default aliases (shadcn's).
|
|
652
|
-
* @since 1.0.0
|
|
653
|
-
*
|
|
654
|
-
* @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
|
|
655
|
-
*/
|
|
656
|
-
const DEFAULT_ALIASES = {
|
|
657
|
-
components: "@/components",
|
|
658
|
-
ui: "@/components/ui",
|
|
659
|
-
lib: "@/lib",
|
|
660
|
-
utils: "@/lib/utils",
|
|
661
|
-
hooks: "@/hooks"
|
|
662
|
-
};
|
|
663
|
-
/** Registry item names are kebab-case; nothing else may reach a file path. */
|
|
664
|
-
const NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
665
|
-
/** A file name inside `registry/<name>/`. */
|
|
666
|
-
const FILE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
667
|
-
/**
|
|
668
|
-
* Whether `name` is a valid registry item name.
|
|
669
|
-
*
|
|
670
|
-
* @param name Any string.
|
|
671
|
-
* @returns `true` for kebab-case names such as `dropdown-menu`.
|
|
672
|
-
* @example
|
|
673
|
-
* isValidItemName("../etc/passwd"); // false
|
|
674
|
-
* @since 1.0.0
|
|
675
|
-
*
|
|
676
|
-
* @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
|
|
677
|
-
*/
|
|
678
|
-
function isValidItemName(name) {
|
|
679
|
-
return NAME_PATTERN.test(name);
|
|
680
|
-
}
|
|
681
|
-
/**
|
|
682
|
-
* Thrown for malformed registry or configuration data.
|
|
683
|
-
* @since 1.0.0
|
|
684
|
-
*
|
|
685
|
-
* @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
|
|
686
|
-
*/
|
|
687
|
-
var SchemaError = class extends Error {
|
|
688
|
-
/** The stable code `--json` prints. */
|
|
689
|
-
code = "SHADOW_UI_INVALID_FILE";
|
|
690
|
-
constructor(message) {
|
|
691
|
-
super(message);
|
|
692
|
-
this.name = "SchemaError";
|
|
693
|
-
}
|
|
694
|
-
};
|
|
695
|
-
function record(value, where) {
|
|
696
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new SchemaError(`${where} must be an object`);
|
|
697
|
-
return value;
|
|
698
|
-
}
|
|
699
|
-
function text(value, where) {
|
|
700
|
-
if (typeof value !== "string" || value === "") throw new SchemaError(`${where} must be a string`);
|
|
701
|
-
return value;
|
|
702
|
-
}
|
|
703
|
-
function strings(value, where) {
|
|
704
|
-
if (!Array.isArray(value)) throw new SchemaError(`${where} must be an array`);
|
|
705
|
-
return value.map((entry, index) => text(entry, `${where}[${index}]`));
|
|
706
|
-
}
|
|
707
|
-
function itemType(value, where) {
|
|
708
|
-
if (value === "ui" || value === "lib" || value === "hook") return value;
|
|
709
|
-
throw new SchemaError(`${where} must be "ui", "lib" or "hook"`);
|
|
710
|
-
}
|
|
711
|
-
function parseItem(value, index) {
|
|
712
|
-
const where = `items[${index}]`;
|
|
713
|
-
const item = record(value, where);
|
|
714
|
-
const name = text(item["name"], `${where}.name`);
|
|
715
|
-
if (!isValidItemName(name)) throw new SchemaError(`${where}.name "${name}" is not a valid name`);
|
|
716
|
-
const files = item["files"];
|
|
717
|
-
if (!Array.isArray(files) || files.length === 0) throw new SchemaError(`${where}.files must be a non-empty array`);
|
|
718
|
-
const dependencies = record(item["dependencies"], `${where}.dependencies`);
|
|
719
|
-
const tailwind = record(item["tailwind"], `${where}.tailwind`);
|
|
720
|
-
return {
|
|
721
|
-
name,
|
|
722
|
-
type: itemType(item["type"], `${where}.type`),
|
|
723
|
-
title: text(item["title"], `${where}.title`),
|
|
724
|
-
description: text(item["description"], `${where}.description`),
|
|
725
|
-
files: files.map((file, fileIndex) => {
|
|
726
|
-
const entry = record(file, `${where}.files[${fileIndex}]`);
|
|
727
|
-
const path = text(entry["path"], `${where}.files[${fileIndex}].path`);
|
|
728
|
-
const segments = path.split("/");
|
|
729
|
-
const [root, folder, base] = segments;
|
|
730
|
-
if (segments.length !== 3 || root !== "registry" || folder !== name || base === void 0 || !FILE_PATTERN.test(base)) throw new SchemaError(`${where}.files[${fileIndex}].path "${path}" must be registry/${name}/<file>`);
|
|
731
|
-
return {
|
|
732
|
-
path,
|
|
733
|
-
type: itemType(entry["type"], `${where}.files[${fileIndex}].type`)
|
|
734
|
-
};
|
|
735
|
-
}),
|
|
736
|
-
registryDependencies: strings(item["registryDependencies"], `${where}.registryDependencies`),
|
|
737
|
-
dependencies: Object.fromEntries(Object.entries(dependencies).map(([pkg, version]) => [pkg, text(version, `${where}.dependencies.${pkg}`)])),
|
|
738
|
-
exports: strings(item["exports"], `${where}.exports`),
|
|
739
|
-
primitives: strings(item["primitives"], `${where}.primitives`),
|
|
740
|
-
tailwind: {
|
|
741
|
-
tokens: strings(tailwind["tokens"], `${where}.tailwind.tokens`),
|
|
742
|
-
animations: tailwind["animations"] === true
|
|
743
|
-
}
|
|
744
|
-
};
|
|
745
|
-
}
|
|
746
|
-
/**
|
|
747
|
-
* Validates parsed `registry.json` data.
|
|
748
|
-
*
|
|
749
|
-
* @param value The parsed JSON.
|
|
750
|
-
* @returns The typed index.
|
|
751
|
-
* @throws SchemaError naming the first malformed field.
|
|
752
|
-
* @example
|
|
753
|
-
* const index = parseRegistryIndex(JSON.parse(readFileSync("registry.json", "utf8")));
|
|
754
|
-
* @since 1.0.0
|
|
755
|
-
*
|
|
756
|
-
* @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
|
|
757
|
-
*/
|
|
758
|
-
function parseRegistryIndex(value) {
|
|
759
|
-
const root = record(value, "registry");
|
|
760
|
-
const items = root["items"];
|
|
761
|
-
if (!Array.isArray(items)) throw new SchemaError("registry.items must be an array");
|
|
762
|
-
const parsed = items.map(parseItem);
|
|
763
|
-
const seen = /* @__PURE__ */ new Set();
|
|
764
|
-
for (const item of parsed) {
|
|
765
|
-
if (seen.has(item.name)) throw new SchemaError(`registry has two items named "${item.name}"`);
|
|
766
|
-
seen.add(item.name);
|
|
767
|
-
}
|
|
768
|
-
return {
|
|
769
|
-
name: text(root["name"], "registry.name"),
|
|
770
|
-
version: text(root["version"], "registry.version"),
|
|
771
|
-
frameworkDependencies: strings(root["frameworkDependencies"], "registry.frameworkDependencies"),
|
|
772
|
-
items: parsed
|
|
773
|
-
};
|
|
774
|
-
}
|
|
775
|
-
/**
|
|
776
|
-
* Validates parsed `components.json` data.
|
|
777
|
-
*
|
|
778
|
-
* @param value The parsed JSON.
|
|
779
|
-
* @returns The typed configuration, with defaults for missing aliases.
|
|
780
|
-
* @throws SchemaError naming the first malformed field.
|
|
781
|
-
* @example
|
|
782
|
-
* const config = parseComponentsConfig(JSON.parse(readFileSync("components.json", "utf8")));
|
|
783
|
-
* @since 1.0.0
|
|
784
|
-
*
|
|
785
|
-
* @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
|
|
786
|
-
*/
|
|
787
|
-
function parseComponentsConfig(value) {
|
|
788
|
-
const root = record(value, "components.json");
|
|
789
|
-
const tailwind = record(root["tailwind"], "components.json tailwind");
|
|
790
|
-
const baseColor = tailwind["baseColor"] ?? "neutral";
|
|
791
|
-
if (!BASE_COLORS.includes(baseColor)) throw new SchemaError(`components.json tailwind.baseColor must be one of ${BASE_COLORS.join(", ")}`);
|
|
792
|
-
const given = root["aliases"] === void 0 ? {} : record(root["aliases"], "components.json aliases");
|
|
793
|
-
const alias = (key) => {
|
|
794
|
-
const entry = given[key];
|
|
795
|
-
if (entry === void 0) return DEFAULT_ALIASES[key];
|
|
796
|
-
const aliasText = text(entry, `components.json aliases.${key}`);
|
|
797
|
-
if (aliasText.includes("..") || /\s/.test(aliasText)) throw new SchemaError(`components.json aliases.${key} "${aliasText}" is not a valid alias`);
|
|
798
|
-
return aliasText;
|
|
799
|
-
};
|
|
800
|
-
const framework = root["framework"];
|
|
801
|
-
return {
|
|
802
|
-
framework: framework === "vite" || framework === "dusk" ? framework : "unknown",
|
|
803
|
-
tailwind: {
|
|
804
|
-
css: text(tailwind["css"], "components.json tailwind.css"),
|
|
805
|
-
baseColor
|
|
806
|
-
},
|
|
807
|
-
aliases: {
|
|
808
|
-
components: alias("components"),
|
|
809
|
-
ui: alias("ui"),
|
|
810
|
-
lib: alias("lib"),
|
|
811
|
-
utils: alias("utils"),
|
|
812
|
-
hooks: alias("hooks")
|
|
813
|
-
}
|
|
814
|
-
};
|
|
815
|
-
}
|
|
816
938
|
// src/cli/project.ts
|
|
817
939
|
/**
|
|
818
940
|
* The project the CLI works in: `components.json`, `package.json`, the stylesheet that imports
|
|
@@ -1243,6 +1365,7 @@ function dependentsOf(index, name, candidates) {
|
|
|
1243
1365
|
*
|
|
1244
1366
|
* @param specifier An import specifier such as `@/components/ui/button`.
|
|
1245
1367
|
* @param aliases The aliases from `components.json`.
|
|
1368
|
+
* @param options Rewrites besides the aliases (relative imports between ui components).
|
|
1246
1369
|
* @returns The rewritten specifier, or the input unchanged when it is not a registry alias.
|
|
1247
1370
|
* @example
|
|
1248
1371
|
* rewriteSpecifier("@/lib/utils", { ...aliases, utils: "~/helpers/cn" }); // "~/helpers/cn"
|
|
@@ -1250,9 +1373,12 @@ function dependentsOf(index, name, candidates) {
|
|
|
1250
1373
|
*
|
|
1251
1374
|
* @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
|
|
1252
1375
|
*/
|
|
1253
|
-
function rewriteSpecifier(specifier, aliases) {
|
|
1376
|
+
function rewriteSpecifier(specifier, aliases, options = {}) {
|
|
1254
1377
|
if (specifier === "@/lib/utils") return aliases.utils;
|
|
1255
|
-
if (specifier.startsWith("@/components/ui/"))
|
|
1378
|
+
if (specifier.startsWith("@/components/ui/")) {
|
|
1379
|
+
const rest = specifier.slice(16);
|
|
1380
|
+
return options.relativeUi === true ? `./${rest}` : `${aliases.ui}/${rest}`;
|
|
1381
|
+
}
|
|
1256
1382
|
if (specifier.startsWith("@/hooks/")) return `${aliases.hooks}/${specifier.slice(8)}`;
|
|
1257
1383
|
if (specifier.startsWith("@/lib/")) return `${aliases.lib}/${specifier.slice(6)}`;
|
|
1258
1384
|
return specifier;
|
|
@@ -1265,6 +1391,7 @@ const SPECIFIER = /(\bfrom\s*|\bimport\s*\(\s*|\bimport\s+)(["'])(@\/[^"'\n]+)\2
|
|
|
1265
1391
|
*
|
|
1266
1392
|
* @param source A registry file's text.
|
|
1267
1393
|
* @param aliases The aliases from `components.json`.
|
|
1394
|
+
* @param options Rewrites besides the aliases (relative imports between ui components).
|
|
1268
1395
|
* @returns The text with rewritten specifiers.
|
|
1269
1396
|
* @example
|
|
1270
1397
|
* rewriteImports('import { cn } from "@/lib/utils";', aliases);
|
|
@@ -1272,8 +1399,8 @@ const SPECIFIER = /(\bfrom\s*|\bimport\s*\(\s*|\bimport\s+)(["'])(@\/[^"'\n]+)\2
|
|
|
1272
1399
|
*
|
|
1273
1400
|
* @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
|
|
1274
1401
|
*/
|
|
1275
|
-
function rewriteImports(source, aliases) {
|
|
1276
|
-
return source.replace(SPECIFIER, (_match, lead, quote, specifier) => `${lead}${quote}${rewriteSpecifier(specifier, aliases)}${quote}`);
|
|
1402
|
+
function rewriteImports(source, aliases, options = {}) {
|
|
1403
|
+
return source.replace(SPECIFIER, (_match, lead, quote, specifier) => `${lead}${quote}${rewriteSpecifier(specifier, aliases, options)}${quote}`);
|
|
1277
1404
|
}
|
|
1278
1405
|
// src/cli/commands.ts
|
|
1279
1406
|
/**
|
|
@@ -1334,12 +1461,26 @@ function targetFor(item, file, config, mappings, ctx, pathOverride) {
|
|
|
1334
1461
|
if (item.type === "lib" && item.name === "utils") return resolveInsideReal(ctx.fs, ctx.cwd, `${aliasToDirectory(config.aliases.utils, mappings)}${extname(base)}`);
|
|
1335
1462
|
return resolveInsideReal(ctx.fs, ctx.cwd, join(directoryFor(item, config, mappings, ctx, pathOverride), base));
|
|
1336
1463
|
}
|
|
1464
|
+
/**
|
|
1465
|
+
* Whether ui components written with `--path` need relative imports between them: they do when that directory is not the one the
|
|
1466
|
+
* `ui` alias names (the alias would point at a directory that does not hold them), or when the alias names no directory at all.
|
|
1467
|
+
*/
|
|
1468
|
+
function importsBetweenUiAreRelative(ctx, config, mappings, pathOverride) {
|
|
1469
|
+
if (pathOverride === void 0) return false;
|
|
1470
|
+
try {
|
|
1471
|
+
return resolve(aliasToDirectory(config.aliases.ui, mappings)) !== resolve(ctx.cwd, pathOverride);
|
|
1472
|
+
} catch (error) {
|
|
1473
|
+
if (error instanceof PathError) return true;
|
|
1474
|
+
throw error;
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1337
1477
|
/** Builds the per-file plan for the items, reading what is on disk. */
|
|
1338
1478
|
function planFiles(ctx, config, items, pathOverride, mappings = readPathMappings(ctx.fs, ctx.cwd)) {
|
|
1339
1479
|
const plans = [];
|
|
1480
|
+
const relativeUi = importsBetweenUiAreRelative(ctx, config, mappings, pathOverride);
|
|
1340
1481
|
for (const item of items) for (const file of item.files) {
|
|
1341
1482
|
const raw = ctx.registry.readFile(file.path);
|
|
1342
|
-
const content = file.path.endsWith(".css") ? raw : rewriteImports(raw, config.aliases);
|
|
1483
|
+
const content = file.path.endsWith(".css") ? raw : rewriteImports(raw, config.aliases, { relativeUi: relativeUi && item.type === "ui" });
|
|
1343
1484
|
const target = targetFor(item, file, config, mappings, ctx, pathOverride);
|
|
1344
1485
|
const current = ctx.fs.read(target);
|
|
1345
1486
|
plans.push({
|
|
@@ -1536,6 +1677,11 @@ async function runInit(ctx, flags) {
|
|
|
1536
1677
|
recordFile(ctx, label, current === void 0 ? "create" : "update");
|
|
1537
1678
|
return "created";
|
|
1538
1679
|
};
|
|
1680
|
+
const configPath = resolveInsideReal(ctx.fs, ctx.cwd, "components.json");
|
|
1681
|
+
const themeFile = themePathFor(config.tailwind.css);
|
|
1682
|
+
resolveInsideReal(ctx.fs, ctx.cwd, themeFile);
|
|
1683
|
+
const cssPath = resolveInsideReal(ctx.fs, ctx.cwd, config.tailwind.css);
|
|
1684
|
+
const utils = planInitUtils(ctx, config);
|
|
1539
1685
|
ctx.report?.set("dryRun", flags.dryRun);
|
|
1540
1686
|
ctx.report?.set("config", {
|
|
1541
1687
|
baseColor,
|
|
@@ -1544,14 +1690,12 @@ async function runInit(ctx, flags) {
|
|
|
1544
1690
|
ctx.report?.set("files", []);
|
|
1545
1691
|
ctx.out("Setting up @shadow-js/ui");
|
|
1546
1692
|
const configText = serializeConfig(config);
|
|
1547
|
-
const configPath = resolveInsideReal(ctx.fs, ctx.cwd, "components.json");
|
|
1548
1693
|
const configNow = ctx.fs.read(configPath);
|
|
1549
1694
|
if (existing !== void 0 && !flags.overwrite && configNow !== void 0 && (config.tailwind.baseColor !== existing.tailwind.baseColor || config.tailwind.css !== existing.tailwind.css) && !sameContent(configNow, configText)) {
|
|
1550
1695
|
ctx.out(` ${flags.dryRun ? "would update" : "updated"} components.json (base color ${config.tailwind.baseColor}, stylesheet ${config.tailwind.css})`);
|
|
1551
1696
|
if (!flags.dryRun) ctx.fs.write(configPath, configText);
|
|
1552
1697
|
recordFile(ctx, "components.json", "update");
|
|
1553
1698
|
} else write("components.json", configText, "components.json");
|
|
1554
|
-
const themeFile = themePathFor(config.tailwind.css);
|
|
1555
1699
|
const themeLabel = themeFile.replaceAll("\\", "/");
|
|
1556
1700
|
const themeText = buildThemeCss(ctx.registry, config.tailwind.baseColor);
|
|
1557
1701
|
const themeCurrent = ctx.fs.read(resolveInsideReal(ctx.fs, ctx.cwd, themeFile));
|
|
@@ -1564,7 +1708,6 @@ async function runInit(ctx, flags) {
|
|
|
1564
1708
|
ctx.out(` kept ${themeLabel} (edited; pass --overwrite to regenerate it for ${config.tailwind.baseColor})`);
|
|
1565
1709
|
recordFile(ctx, themeLabel, "kept");
|
|
1566
1710
|
} else write(themeFile, themeText, themeLabel);
|
|
1567
|
-
const cssPath = resolveInsideReal(ctx.fs, ctx.cwd, config.tailwind.css);
|
|
1568
1711
|
const css = ctx.fs.read(cssPath);
|
|
1569
1712
|
if (css === void 0) write(config.tailwind.css, `@import "tailwindcss";\n${THEME_IMPORT_LINE}\n`, config.tailwind.css);
|
|
1570
1713
|
else {
|
|
@@ -1584,7 +1727,7 @@ async function runInit(ctx, flags) {
|
|
|
1584
1727
|
recordFile(ctx, config.tailwind.css, "skipped");
|
|
1585
1728
|
}
|
|
1586
1729
|
}
|
|
1587
|
-
const added = await runAddForInit(ctx,
|
|
1730
|
+
const added = await runAddForInit(ctx, utils, flags);
|
|
1588
1731
|
if (added !== OK) return added;
|
|
1589
1732
|
const tooling = [];
|
|
1590
1733
|
if (!info.hasTailwind) tooling.push("tailwindcss");
|
|
@@ -1596,14 +1739,21 @@ async function runInit(ctx, flags) {
|
|
|
1596
1739
|
return failed(ctx, error);
|
|
1597
1740
|
}
|
|
1598
1741
|
}
|
|
1599
|
-
/**
|
|
1600
|
-
|
|
1742
|
+
/** Plans `utils` through the same pipeline as `add`. */
|
|
1743
|
+
function planInitUtils(ctx, config) {
|
|
1601
1744
|
const plan = resolveItems(ctx.registry.index, ["utils"]);
|
|
1602
1745
|
const mappings = [...readPathMappings(ctx.fs, ctx.cwd), {
|
|
1603
1746
|
pattern: "@/*",
|
|
1604
1747
|
targets: [join(ctx.cwd, "src", "*")]
|
|
1605
1748
|
}];
|
|
1606
|
-
|
|
1749
|
+
return {
|
|
1750
|
+
plan,
|
|
1751
|
+
entries: planFiles(ctx, config, plan.items, void 0, mappings)
|
|
1752
|
+
};
|
|
1753
|
+
}
|
|
1754
|
+
/** `init` copies `utils` through the same pipeline as `add`. */
|
|
1755
|
+
async function runAddForInit(ctx, { plan, entries }, flags) {
|
|
1756
|
+
for (const entry of entries) {
|
|
1607
1757
|
const where = display(ctx.cwd, entry.target);
|
|
1608
1758
|
if (entry.status === "unchanged") {
|
|
1609
1759
|
ctx.out(` unchanged ${where}`);
|
|
@@ -1759,7 +1909,8 @@ function runDiff(ctx, names) {
|
|
|
1759
1909
|
}
|
|
1760
1910
|
/**
|
|
1761
1911
|
* Deletes components' files from the project. Refuses when another component that is in the
|
|
1762
|
-
* project imports one of them (
|
|
1912
|
+
* project imports one of them, and keeps (exit 1) a file that differs from the registry, because
|
|
1913
|
+
* the user edited it; `force` overrides both. It never removes npm packages.
|
|
1763
1914
|
*
|
|
1764
1915
|
* @param ctx The CLI context.
|
|
1765
1916
|
* @param names Component names.
|
|
@@ -1786,6 +1937,7 @@ function runRemove(ctx, names, flags) {
|
|
|
1786
1937
|
ctx.report?.set("dryRun", flags.dryRun);
|
|
1787
1938
|
ctx.report?.set("removed", []);
|
|
1788
1939
|
let code = OK;
|
|
1940
|
+
let kept = 0;
|
|
1789
1941
|
for (const name of removing) {
|
|
1790
1942
|
const blockers = dependentsOf(ctx.registry.index, name, installedNames).filter((other) => !removing.includes(other));
|
|
1791
1943
|
if (blockers.length > 0 && !flags.force) {
|
|
@@ -1799,18 +1951,30 @@ function runRemove(ctx, names, flags) {
|
|
|
1799
1951
|
}
|
|
1800
1952
|
const item = findItem(ctx.registry.index, name);
|
|
1801
1953
|
if (item === void 0) continue;
|
|
1802
|
-
for (const
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1954
|
+
for (const entry of planFiles(ctx, config, [item], void 0, mappings)) {
|
|
1955
|
+
if (entry.status === "create") continue;
|
|
1956
|
+
const where = display(ctx.cwd, entry.target);
|
|
1957
|
+
if (entry.status === "differs" && !flags.force) {
|
|
1958
|
+
kept += 1;
|
|
1959
|
+
ctx.out(` kept ${where} (differs from the registry; pass --force to remove it)`);
|
|
1960
|
+
ctx.report?.push("kept", {
|
|
1961
|
+
name,
|
|
1962
|
+
path: where
|
|
1963
|
+
});
|
|
1964
|
+
continue;
|
|
1965
|
+
}
|
|
1806
1966
|
ctx.out(` ${flags.dryRun ? "would remove" : "remove"} ${where}`);
|
|
1807
|
-
if (!flags.dryRun) ctx.fs.remove(target);
|
|
1967
|
+
if (!flags.dryRun) ctx.fs.remove(entry.target);
|
|
1808
1968
|
ctx.report?.push("removed", {
|
|
1809
1969
|
name,
|
|
1810
1970
|
path: where
|
|
1811
1971
|
});
|
|
1812
1972
|
}
|
|
1813
1973
|
}
|
|
1974
|
+
if (kept > 0) {
|
|
1975
|
+
problem(ctx, "SHADOW_UI_FILES_DIFFER", `${kept} file(s) differ from the registry and were kept; pass --force to remove them.`);
|
|
1976
|
+
code = FAILED;
|
|
1977
|
+
}
|
|
1814
1978
|
return code;
|
|
1815
1979
|
} catch (error) {
|
|
1816
1980
|
return failed(ctx, error);
|
|
@@ -2160,6 +2324,42 @@ async function dispatch(ctx, command, rest, values, common) {
|
|
|
2160
2324
|
}
|
|
2161
2325
|
}
|
|
2162
2326
|
}
|
|
2327
|
+
// src/cli/prompt.ts
|
|
2328
|
+
/**
|
|
2329
|
+
* The yes/no question of the executable, kept apart from `bin.ts` (which runs on import) so a test
|
|
2330
|
+
* can drive it with in-memory streams.
|
|
2331
|
+
* @packageDocumentation
|
|
2332
|
+
*/
|
|
2333
|
+
/**
|
|
2334
|
+
* Asks a yes/no question and resolves `true` only for `y` or `yes` (any case). An empty answer is
|
|
2335
|
+
* no, and so is the end of input (Ctrl-D, a closed pipe): readline never settles `question()` when
|
|
2336
|
+
* its input ends, which left Node to exit with code 13 ("unsettled top-level await").
|
|
2337
|
+
*
|
|
2338
|
+
* @param question The question, without the `(y/N)` suffix.
|
|
2339
|
+
* @param input The stream the answer is read from.
|
|
2340
|
+
* @param output The stream the question is written to.
|
|
2341
|
+
* @returns Whether the user said yes.
|
|
2342
|
+
* @example
|
|
2343
|
+
* const yes = await askYesNo("Install packages?", process.stdin, process.stdout);
|
|
2344
|
+
* @since 1.0.0
|
|
2345
|
+
*
|
|
2346
|
+
* @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
|
|
2347
|
+
*/
|
|
2348
|
+
async function askYesNo(question, input, output) {
|
|
2349
|
+
const prompt = createInterface({
|
|
2350
|
+
input,
|
|
2351
|
+
output
|
|
2352
|
+
});
|
|
2353
|
+
try {
|
|
2354
|
+
const answer = await new Promise((settle, fail) => {
|
|
2355
|
+
prompt.once("close", () => settle(""));
|
|
2356
|
+
prompt.question(`${question} (y/N) `).then(settle, fail);
|
|
2357
|
+
});
|
|
2358
|
+
return /^y(es)?$/i.test(answer.trim());
|
|
2359
|
+
} finally {
|
|
2360
|
+
prompt.close();
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2163
2363
|
// src/cli/registry-source.ts
|
|
2164
2364
|
/**
|
|
2165
2365
|
* Where the CLI reads the registry from. The registry ships inside the package, so nothing is
|
|
@@ -2243,21 +2443,12 @@ function realContext(cwd) {
|
|
|
2243
2443
|
cwd: resolve(cwd ?? process.cwd()),
|
|
2244
2444
|
fs: nodeFs(),
|
|
2245
2445
|
registry: loadRegistrySource(),
|
|
2246
|
-
version: "1.0.
|
|
2446
|
+
version: "1.0.3",
|
|
2247
2447
|
out: (line) => process.stdout.write(`${line}\n`),
|
|
2248
2448
|
err: (line) => process.stderr.write(`${line}\n`),
|
|
2249
2449
|
async confirm(question) {
|
|
2250
2450
|
if (!process.stdin.isTTY) return false;
|
|
2251
|
-
|
|
2252
|
-
input: process.stdin,
|
|
2253
|
-
output: process.stdout
|
|
2254
|
-
});
|
|
2255
|
-
try {
|
|
2256
|
-
const answer = await prompt.question(`${question} (y/N) `);
|
|
2257
|
-
return /^y(es)?$/i.test(answer.trim());
|
|
2258
|
-
} finally {
|
|
2259
|
-
prompt.close();
|
|
2260
|
-
}
|
|
2451
|
+
return askYesNo(question, process.stdin, process.stdout);
|
|
2261
2452
|
},
|
|
2262
2453
|
install: ({ cwd: directory, manager, packages, output }) => new Promise((done, reject) => {
|
|
2263
2454
|
const args = manager === "npm" ? ["install", ...packages] : ["add", ...packages];
|