nxds-cli 0.2.0 → 0.4.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/dist/index.js +594 -91
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { Command, CommanderError, Option } from "commander";
|
|
5
|
-
import
|
|
4
|
+
import { Argument, Command, CommanderError, Option } from "commander";
|
|
5
|
+
import path13 from "path";
|
|
6
6
|
|
|
7
7
|
// ../catalog/scripts/catalog-schema.ts
|
|
8
8
|
import path from "path";
|
|
@@ -49,6 +49,11 @@ var setupStepSchema = z.enum([
|
|
|
49
49
|
"wrap-root-with-provider"
|
|
50
50
|
]);
|
|
51
51
|
var npmPackagesSchema = z.record(npmNameSchema, rangeSchema);
|
|
52
|
+
var reactMajorSchema = z.union([z.literal(18), z.literal(19)]);
|
|
53
|
+
var react18Schema = z.object({
|
|
54
|
+
files: z.array(catalogFileSchema),
|
|
55
|
+
packages: z.object({ npm: npmPackagesSchema })
|
|
56
|
+
});
|
|
52
57
|
function variantWith(packages) {
|
|
53
58
|
return z.object({
|
|
54
59
|
/** Other items this one needs, installed first. */
|
|
@@ -57,12 +62,35 @@ function variantWith(packages) {
|
|
|
57
62
|
files: z.array(catalogFileSchema),
|
|
58
63
|
/** Files in `files` the nxds CSS entry imports. */
|
|
59
64
|
css: z.array(relativePathSchema),
|
|
60
|
-
steps: z.array(setupStepSchema)
|
|
65
|
+
steps: z.array(setupStepSchema),
|
|
66
|
+
react18: react18Schema.optional()
|
|
61
67
|
}).refine(
|
|
62
68
|
({ css, files }) => css.every((entry) => files.some((file) => file.path === entry)),
|
|
63
69
|
{ path: ["css"], message: "must name files in `files`" }
|
|
70
|
+
).refine(
|
|
71
|
+
({ files, react18 }) => (react18?.files ?? []).every(
|
|
72
|
+
(file) => files.some((own) => own.path === file.path)
|
|
73
|
+
),
|
|
74
|
+
{
|
|
75
|
+
path: ["react18", "files"],
|
|
76
|
+
message: "must replace files in `files`"
|
|
77
|
+
}
|
|
64
78
|
);
|
|
65
79
|
}
|
|
80
|
+
function variantForReact(variant, react) {
|
|
81
|
+
if (react === 19 || !variant.react18) return variant;
|
|
82
|
+
const replaced = new Map(
|
|
83
|
+
variant.react18.files.map((file) => [file.path, file])
|
|
84
|
+
);
|
|
85
|
+
return {
|
|
86
|
+
...variant,
|
|
87
|
+
files: variant.files.map((file) => replaced.get(file.path) ?? file),
|
|
88
|
+
packages: {
|
|
89
|
+
...variant.packages,
|
|
90
|
+
npm: { ...variant.packages.npm, ...variant.react18.packages.npm }
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
66
94
|
var variantSchema = variantWith(z.object({ npm: npmPackagesSchema }));
|
|
67
95
|
var setupVariantSchema = variantWith(
|
|
68
96
|
z.object({
|
|
@@ -97,6 +125,8 @@ var catalogSchema = z.object({
|
|
|
97
125
|
version: versionSchema,
|
|
98
126
|
/** The frameworks the CLI installs. The docs copy also carries variants for others; a release catalog drops them. Names this version doesn't know are dropped too. */
|
|
99
127
|
frameworks: z.array(z.string()).transform((frameworks) => frameworks.filter(isFramework)),
|
|
128
|
+
/** The React majors this release has files for. Releases from before React 18 support leave it out, and have React 19 files only. */
|
|
129
|
+
reactMajors: z.array(reactMajorSchema).nonempty().default([19]),
|
|
100
130
|
items: z.array(catalogItemSchema)
|
|
101
131
|
});
|
|
102
132
|
function lines(issues, prefix) {
|
|
@@ -214,7 +244,7 @@ async function fetchRelease(registry, version) {
|
|
|
214
244
|
if (isNewerSchema(value)) {
|
|
215
245
|
const release = version === "latest" ? "The latest nxds release" : `nxds ${version}`;
|
|
216
246
|
throw new CliError(
|
|
217
|
-
`${release} needs a newer nxds-cli than ${"0.
|
|
247
|
+
`${release} needs a newer nxds-cli than ${"0.4.0"}. Run \`${CLI_COMMAND}\` to get the newest one.`
|
|
218
248
|
);
|
|
219
249
|
}
|
|
220
250
|
const invalid = (problems) => new CliError(
|
|
@@ -229,11 +259,18 @@ Check \`--registry\`, or run the command again later.`
|
|
|
229
259
|
}
|
|
230
260
|
return parsed.data;
|
|
231
261
|
}
|
|
232
|
-
async function loadRelease(registry, version) {
|
|
262
|
+
async function loadRelease(registry, version, react) {
|
|
233
263
|
const catalog = await fetchRelease(registry, version);
|
|
234
|
-
if (catalog)
|
|
264
|
+
if (!catalog) {
|
|
265
|
+
throw new CliError(
|
|
266
|
+
version === "latest" ? `${registry} has no nxds release.` : `${registry} has no nxds ${version}, the release this app is on. Run \`${CLI_COMMAND} update\` to move to the latest release.`
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
if (catalog.reactMajors.includes(react)) return catalog;
|
|
270
|
+
const latest = version === "latest" ? null : await fetchRelease(registry, "latest");
|
|
271
|
+
const fix = latest?.reactMajors.includes(react) ? `Upgrade the app to React 19, or run \`${CLI_COMMAND} update\` to move to nxds ${latest.version}, which has React ${react} files.` : "Upgrade the app to React 19.";
|
|
235
272
|
throw new CliError(
|
|
236
|
-
|
|
273
|
+
`nxds ${catalog.version} has no React ${react} files. ${fix}`
|
|
237
274
|
);
|
|
238
275
|
}
|
|
239
276
|
function findItem(catalog, name) {
|
|
@@ -248,12 +285,15 @@ function variantOf(item, framework) {
|
|
|
248
285
|
if (variant) return variant;
|
|
249
286
|
throw new CliError(`${item.title} is not available for ${framework} yet.`);
|
|
250
287
|
}
|
|
251
|
-
function
|
|
288
|
+
function variantFor(item, target) {
|
|
289
|
+
return variantForReact(variantOf(item, target.framework), target.react);
|
|
290
|
+
}
|
|
291
|
+
function setupVariant(catalog, target) {
|
|
252
292
|
const setup = catalog.items.find((item) => item.name === SETUP);
|
|
253
293
|
if (setup?.kind !== "setup") {
|
|
254
294
|
throw new Error(`nxds ${catalog.version} has no "${SETUP}" setup item.`);
|
|
255
295
|
}
|
|
256
|
-
return
|
|
296
|
+
return variantFor(setup, target);
|
|
257
297
|
}
|
|
258
298
|
function resolveItems(catalog, framework, names) {
|
|
259
299
|
const ordered = [];
|
|
@@ -268,10 +308,10 @@ function resolveItems(catalog, framework, names) {
|
|
|
268
308
|
names.forEach(visit);
|
|
269
309
|
return ordered;
|
|
270
310
|
}
|
|
271
|
-
function releaseFiles(catalog,
|
|
311
|
+
function releaseFiles(catalog, target) {
|
|
272
312
|
return new Map(
|
|
273
|
-
catalog.items.flatMap(
|
|
274
|
-
(item) => (item
|
|
313
|
+
catalog.items.filter((item) => item.variants[target.framework]).flatMap(
|
|
314
|
+
(item) => variantFor(item, target).files.map(
|
|
275
315
|
(file) => [file.path, file.content]
|
|
276
316
|
)
|
|
277
317
|
)
|
|
@@ -285,6 +325,10 @@ function splitInstalled(catalog, installed) {
|
|
|
285
325
|
};
|
|
286
326
|
}
|
|
287
327
|
|
|
328
|
+
// src/commands/browse.ts
|
|
329
|
+
import { existsSync as existsSync5 } from "fs";
|
|
330
|
+
import path6 from "path";
|
|
331
|
+
|
|
288
332
|
// src/config.ts
|
|
289
333
|
import { existsSync as existsSync2, writeFileSync } from "fs";
|
|
290
334
|
import path3 from "path";
|
|
@@ -293,6 +337,8 @@ var CONFIG_FILE = "nxds.json";
|
|
|
293
337
|
var configSchema = z2.looseObject({
|
|
294
338
|
/** The CLI installs React only. */
|
|
295
339
|
framework: z2.literal("react"),
|
|
340
|
+
/** The React major the installed files are for. */
|
|
341
|
+
react: reactMajorSchema,
|
|
296
342
|
/** The nxds release every installed file comes from. */
|
|
297
343
|
version: versionSchema,
|
|
298
344
|
/** The nxds folder, relative to the project root. */
|
|
@@ -333,7 +379,7 @@ function writeConfig(root, config) {
|
|
|
333
379
|
// src/project.ts
|
|
334
380
|
import { existsSync as existsSync4 } from "fs";
|
|
335
381
|
import path5 from "path";
|
|
336
|
-
import { major, minVersion as minVersion2, validRange as validRange3 } from "semver";
|
|
382
|
+
import { major, minVersion as minVersion2, parse, validRange as validRange3 } from "semver";
|
|
337
383
|
|
|
338
384
|
// src/packages.ts
|
|
339
385
|
import { spawnSync } from "child_process";
|
|
@@ -448,6 +494,27 @@ function readProject(root) {
|
|
|
448
494
|
function findFile(project, candidates) {
|
|
449
495
|
return firstExisting(project.root, candidates);
|
|
450
496
|
}
|
|
497
|
+
function reactMajor(project) {
|
|
498
|
+
const declared = project.dependencies.react;
|
|
499
|
+
if (declared === void 0) {
|
|
500
|
+
throw new CliError(
|
|
501
|
+
"This app has no react dependency. nxds needs React 18.3 or 19."
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
const installed = installedVersion(project, "react");
|
|
505
|
+
const range = validRange3(declared) === null ? null : minVersion2(declared);
|
|
506
|
+
const version = installed === null ? range : parse(installed);
|
|
507
|
+
if (version === null) {
|
|
508
|
+
throw new CliError(
|
|
509
|
+
`This app declares react ${declared}, which names no version, and none is installed. Install your dependencies, then run the command again.`
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
if (version.major >= 19) return 19;
|
|
513
|
+
if (version.major === 18 && version.minor >= 3) return 18;
|
|
514
|
+
throw new CliError(
|
|
515
|
+
`nxds needs React 18.3 or 19, and this app uses react ${version.version}. Upgrade React, then run the command again.`
|
|
516
|
+
);
|
|
517
|
+
}
|
|
451
518
|
function majorVersion(project, name) {
|
|
452
519
|
const declared = project.dependencies[name];
|
|
453
520
|
if (declared === void 0) return null;
|
|
@@ -457,8 +524,8 @@ function majorVersion(project, name) {
|
|
|
457
524
|
}
|
|
458
525
|
|
|
459
526
|
// src/commands/browse.ts
|
|
460
|
-
function appRelease(
|
|
461
|
-
return loadRelease(registry,
|
|
527
|
+
function appRelease(registry, config, react) {
|
|
528
|
+
return loadRelease(registry, config?.version ?? "latest", react);
|
|
462
529
|
}
|
|
463
530
|
function summary(item) {
|
|
464
531
|
return {
|
|
@@ -478,7 +545,12 @@ function summaryLines(items) {
|
|
|
478
545
|
);
|
|
479
546
|
}
|
|
480
547
|
async function list(cwd, registry, kind) {
|
|
481
|
-
const
|
|
548
|
+
const catalog = await appRelease(
|
|
549
|
+
registry,
|
|
550
|
+
readConfig(cwd),
|
|
551
|
+
projectReact(cwd)
|
|
552
|
+
);
|
|
553
|
+
const items = catalog.items.filter(
|
|
482
554
|
(item) => item.name !== SETUP && (kind === void 0 || item.kind === kind)
|
|
483
555
|
);
|
|
484
556
|
return { data: items.map(summary), lines: summaryLines(items), ok: true };
|
|
@@ -491,7 +563,12 @@ async function search(cwd, registry, query) {
|
|
|
491
563
|
if (item.description.toLowerCase().includes(needle)) return 2;
|
|
492
564
|
return void 0;
|
|
493
565
|
};
|
|
494
|
-
const
|
|
566
|
+
const catalog = await appRelease(
|
|
567
|
+
registry,
|
|
568
|
+
readConfig(cwd),
|
|
569
|
+
projectReact(cwd)
|
|
570
|
+
);
|
|
571
|
+
const items = catalog.items.filter((item) => item.name !== SETUP).flatMap((item) => {
|
|
495
572
|
const score = rank(item);
|
|
496
573
|
return score === void 0 ? [] : [{ item, score }];
|
|
497
574
|
}).sort((a, b) => a.score - b.score).map(({ item }) => item);
|
|
@@ -501,19 +578,28 @@ async function search(cwd, registry, query) {
|
|
|
501
578
|
ok: true
|
|
502
579
|
};
|
|
503
580
|
}
|
|
504
|
-
function projectFramework(
|
|
505
|
-
|
|
506
|
-
if (configured) return configured;
|
|
581
|
+
function projectFramework(config, catalog) {
|
|
582
|
+
if (config) return config.framework;
|
|
507
583
|
const [only, ...others] = catalog.frameworks;
|
|
508
584
|
if (only && others.length === 0) return only;
|
|
509
585
|
throw new CliError(
|
|
510
586
|
`This release has several frameworks. Run \`${CLI_COMMAND} init\` first, so nxds knows which one your app uses.`
|
|
511
587
|
);
|
|
512
588
|
}
|
|
589
|
+
function projectReact(cwd) {
|
|
590
|
+
if (!existsSync5(path6.join(cwd, "package.json"))) return 19;
|
|
591
|
+
const project = readProject(cwd);
|
|
592
|
+
return "react" in project.dependencies ? reactMajor(project) : 19;
|
|
593
|
+
}
|
|
513
594
|
async function view(cwd, registry, name) {
|
|
514
|
-
const
|
|
595
|
+
const config = readConfig(cwd);
|
|
596
|
+
const react = projectReact(cwd);
|
|
597
|
+
const catalog = await appRelease(registry, config, react);
|
|
515
598
|
const item = findItem(catalog, name);
|
|
516
|
-
const variant =
|
|
599
|
+
const variant = variantFor(item, {
|
|
600
|
+
framework: projectFramework(config, catalog),
|
|
601
|
+
react
|
|
602
|
+
});
|
|
517
603
|
const packages = Object.entries(variant.packages.npm).map(
|
|
518
604
|
([pkg, range]) => `${pkg}@${range}`
|
|
519
605
|
);
|
|
@@ -573,13 +659,13 @@ function info(cwd) {
|
|
|
573
659
|
|
|
574
660
|
// src/commands/check.ts
|
|
575
661
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
576
|
-
import { existsSync as
|
|
577
|
-
import
|
|
578
|
-
import { satisfies as satisfies2 } from "semver";
|
|
662
|
+
import { existsSync as existsSync8, readFileSync as readFileSync4 } from "fs";
|
|
663
|
+
import path9 from "path";
|
|
664
|
+
import { major as major2, satisfies as satisfies2 } from "semver";
|
|
579
665
|
|
|
580
666
|
// src/files.ts
|
|
581
|
-
import { existsSync as
|
|
582
|
-
import
|
|
667
|
+
import { existsSync as existsSync6, mkdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
668
|
+
import path7 from "path";
|
|
583
669
|
import { merge } from "node-diff3";
|
|
584
670
|
function baseOf(previous, file) {
|
|
585
671
|
if (!previous) return void 0;
|
|
@@ -604,13 +690,13 @@ function hasConflictMarkers(content) {
|
|
|
604
690
|
}
|
|
605
691
|
function conflictedFiles(root, config, files) {
|
|
606
692
|
return [...files].filter((file) => {
|
|
607
|
-
const target =
|
|
608
|
-
return
|
|
693
|
+
const target = path7.join(root, config.dir, file);
|
|
694
|
+
return existsSync6(target) && hasConflictMarkers(readFileSync2(target, "utf8"));
|
|
609
695
|
});
|
|
610
696
|
}
|
|
611
697
|
function fileState(root, config, file, previous) {
|
|
612
|
-
const target =
|
|
613
|
-
if (!
|
|
698
|
+
const target = path7.join(root, config.dir, file.path);
|
|
699
|
+
if (!existsSync6(target)) return "missing";
|
|
614
700
|
const onDisk = readFileSync2(target, "utf8");
|
|
615
701
|
if (hasConflictMarkers(onDisk)) return "conflicted";
|
|
616
702
|
if (sameContent(onDisk, file.content)) return "current";
|
|
@@ -634,8 +720,8 @@ function writeFiles(root, config, files, previous, { overwrite, dryRun, version
|
|
|
634
720
|
};
|
|
635
721
|
const write = (file, content) => {
|
|
636
722
|
if (dryRun) return;
|
|
637
|
-
const target =
|
|
638
|
-
mkdirSync(
|
|
723
|
+
const target = path7.join(root, config.dir, file);
|
|
724
|
+
mkdirSync(path7.dirname(target), { recursive: true });
|
|
639
725
|
writeFileSync2(target, content);
|
|
640
726
|
};
|
|
641
727
|
for (const file of files) {
|
|
@@ -658,7 +744,7 @@ function writeFiles(root, config, files, previous, { overwrite, dryRun, version
|
|
|
658
744
|
result.skipped.push(file.path);
|
|
659
745
|
continue;
|
|
660
746
|
}
|
|
661
|
-
const target =
|
|
747
|
+
const target = path7.join(root, config.dir, file.path);
|
|
662
748
|
const ours = readFileSync2(target, "utf8");
|
|
663
749
|
const merged = mergeFile(ours, base, file.content, version);
|
|
664
750
|
if (sameContent(ours, merged.content)) {
|
|
@@ -671,7 +757,7 @@ function writeFiles(root, config, files, previous, { overwrite, dryRun, version
|
|
|
671
757
|
return result;
|
|
672
758
|
}
|
|
673
759
|
function relativeImport(fromFile, toFile) {
|
|
674
|
-
const relative =
|
|
760
|
+
const relative = path7.relative(path7.dirname(fromFile), toFile).split(path7.sep).join("/");
|
|
675
761
|
return relative.startsWith(".") ? relative : `./${relative}`;
|
|
676
762
|
}
|
|
677
763
|
|
|
@@ -687,11 +773,11 @@ import {
|
|
|
687
773
|
isObjectProperty,
|
|
688
774
|
isSpreadElement
|
|
689
775
|
} from "@babel/types";
|
|
690
|
-
import { existsSync as
|
|
691
|
-
import
|
|
776
|
+
import { existsSync as existsSync7, readFileSync as readFileSync3 } from "fs";
|
|
777
|
+
import path8 from "path";
|
|
692
778
|
|
|
693
779
|
// src/setup/source-edit.ts
|
|
694
|
-
import { parse } from "@babel/parser";
|
|
780
|
+
import { parse as parse2 } from "@babel/parser";
|
|
695
781
|
import {
|
|
696
782
|
isImportDeclaration,
|
|
697
783
|
isJSXAttribute,
|
|
@@ -701,7 +787,7 @@ import {
|
|
|
701
787
|
} from "@babel/types";
|
|
702
788
|
function parseModule(source, file) {
|
|
703
789
|
try {
|
|
704
|
-
return
|
|
790
|
+
return parse2(source, {
|
|
705
791
|
sourceType: "module",
|
|
706
792
|
plugins: ["typescript", "jsx"]
|
|
707
793
|
}).program;
|
|
@@ -794,7 +880,7 @@ function tailwindPackages(setup, range) {
|
|
|
794
880
|
}
|
|
795
881
|
function appDir(project) {
|
|
796
882
|
const dir = APP_DIRS.find(
|
|
797
|
-
(candidate) =>
|
|
883
|
+
(candidate) => existsSync7(path8.join(project.root, candidate, "layout.tsx"))
|
|
798
884
|
);
|
|
799
885
|
if (!dir)
|
|
800
886
|
throw new CliError("No app/layout.tsx or src/app/layout.tsx found.");
|
|
@@ -810,7 +896,7 @@ function cssEntryPath(project) {
|
|
|
810
896
|
return `${entryDir(project)}/nxds.css`;
|
|
811
897
|
}
|
|
812
898
|
function read(project, file) {
|
|
813
|
-
return readFileSync3(
|
|
899
|
+
return readFileSync3(path8.join(project.root, file), "utf8");
|
|
814
900
|
}
|
|
815
901
|
function cssEntry(config, withPreflight) {
|
|
816
902
|
const tokens = relativeImport(config.css, `${config.dir}/tailwind/nxds.css`);
|
|
@@ -1021,9 +1107,9 @@ var NXDS_VITE_PLUGIN = {
|
|
|
1021
1107
|
var VITE_ALIAS = "{ '@': '/src' }";
|
|
1022
1108
|
function wireViteConfig(project, addTailwind, steps) {
|
|
1023
1109
|
const configFile = findFile(project, VITE_CONFIGS) ?? "vite.config.ts";
|
|
1024
|
-
const addScript = steps.has("add-head-script") && !
|
|
1110
|
+
const addScript = steps.has("add-head-script") && !existsSync7(path8.join(project.root, "nxds.vite.ts"));
|
|
1025
1111
|
const changes = addScript ? [{ path: "nxds.vite.ts", content: VITE_PLUGIN, created: true }] : [];
|
|
1026
|
-
const source =
|
|
1112
|
+
const source = existsSync7(path8.join(project.root, configFile)) ? read(project, configFile) : "";
|
|
1027
1113
|
const program2 = source ? parseModule(source, configFile) : void 0;
|
|
1028
1114
|
const object = program2 && configObject(program2);
|
|
1029
1115
|
const plugins = object && pluginsArray(object);
|
|
@@ -1184,7 +1270,7 @@ function wireNextTailwind(project) {
|
|
|
1184
1270
|
};
|
|
1185
1271
|
}
|
|
1186
1272
|
function createdUnlessExists(project, file, content) {
|
|
1187
|
-
if (
|
|
1273
|
+
if (existsSync7(path8.join(project.root, file))) return NO_CHANGES;
|
|
1188
1274
|
return { changes: [{ path: file, content, created: true }], manual: [] };
|
|
1189
1275
|
}
|
|
1190
1276
|
function planWiring(project, config, addTailwind, setupSteps) {
|
|
@@ -1221,9 +1307,10 @@ function planWiring(project, config, addTailwind, setupSteps) {
|
|
|
1221
1307
|
// src/commands/check.ts
|
|
1222
1308
|
async function diff(cwd, registry, names) {
|
|
1223
1309
|
const config = requireConfig(cwd);
|
|
1224
|
-
const
|
|
1310
|
+
const target = { ...config, react: reactMajor(readProject(cwd)) };
|
|
1311
|
+
const latest = await loadRelease(registry, "latest", target.react);
|
|
1225
1312
|
const previous = await fetchRelease(registry, config.version);
|
|
1226
|
-
const previousFiles = previous && releaseFiles(previous, config
|
|
1313
|
+
const previousFiles = previous && releaseFiles(previous, config);
|
|
1227
1314
|
const { known, removed } = splitInstalled(latest, config.items);
|
|
1228
1315
|
const items = resolveItems(
|
|
1229
1316
|
latest,
|
|
@@ -1248,7 +1335,7 @@ async function diff(cwd, registry, names) {
|
|
|
1248
1335
|
return `has your edits; ${update2} merges nxds's changes into it, ${overwrite} replaces it`;
|
|
1249
1336
|
}
|
|
1250
1337
|
const files = items.flatMap(
|
|
1251
|
-
(item) =>
|
|
1338
|
+
(item) => variantFor(item, target).files.map((file) => {
|
|
1252
1339
|
const state = fileState(cwd, config, file, previousFiles);
|
|
1253
1340
|
return {
|
|
1254
1341
|
item: item.name,
|
|
@@ -1286,14 +1373,14 @@ function tailwindMessage(project) {
|
|
|
1286
1373
|
return `tailwindcss ${declared}. nxds needs Tailwind CSS 4.`;
|
|
1287
1374
|
}
|
|
1288
1375
|
function read2(root, file) {
|
|
1289
|
-
if (!file || !
|
|
1290
|
-
return readFileSync4(
|
|
1376
|
+
if (!file || !existsSync8(path9.join(root, file))) return "";
|
|
1377
|
+
return readFileSync4(path9.join(root, file), "utf8");
|
|
1291
1378
|
}
|
|
1292
1379
|
function sourcesNxdsDir(config, css) {
|
|
1293
|
-
const entryDir2 =
|
|
1380
|
+
const entryDir2 = path9.posix.dirname(config.css);
|
|
1294
1381
|
return [...css.matchAll(/@source\s+(['"])(.*?)\1/g)].some(([, , source]) => {
|
|
1295
1382
|
const base = (source ?? "").split(/[*{[]/)[0] ?? "";
|
|
1296
|
-
const folder =
|
|
1383
|
+
const folder = path9.posix.join(entryDir2, base).replace(/\/+$/, "");
|
|
1297
1384
|
return folder === "." || /^\.\.(\/\.\.)*$/.test(folder) || config.dir === folder || config.dir.startsWith(`${folder}/`);
|
|
1298
1385
|
});
|
|
1299
1386
|
}
|
|
@@ -1307,10 +1394,42 @@ function unscannedReason(cwd, config, css) {
|
|
|
1307
1394
|
}
|
|
1308
1395
|
return null;
|
|
1309
1396
|
}
|
|
1397
|
+
function installedMajor(project, name) {
|
|
1398
|
+
const version = installedVersion(project, name);
|
|
1399
|
+
return version === null ? null : major2(version);
|
|
1400
|
+
}
|
|
1401
|
+
function reactChecks(project, config) {
|
|
1402
|
+
let react;
|
|
1403
|
+
try {
|
|
1404
|
+
react = reactMajor(project);
|
|
1405
|
+
} catch (error) {
|
|
1406
|
+
if (!(error instanceof CliError)) throw error;
|
|
1407
|
+
return [{ name: "react", ok: false, message: error.message }];
|
|
1408
|
+
}
|
|
1409
|
+
const checks = [
|
|
1410
|
+
{
|
|
1411
|
+
name: "react",
|
|
1412
|
+
ok: config.react === react,
|
|
1413
|
+
message: config.react === react ? `The files are for React ${react}, which this app uses` : `The files are for React ${config.react}, and this app uses React ${react}. Run \`${CLI_COMMAND} update\` to switch them.`
|
|
1414
|
+
}
|
|
1415
|
+
];
|
|
1416
|
+
const reactInstalled = installedMajor(project, "react");
|
|
1417
|
+
if (reactInstalled === null) return checks;
|
|
1418
|
+
for (const name of ["react-dom", "@types/react", "@types/react-dom"]) {
|
|
1419
|
+
if (name.startsWith("@types/") && !(name in project.dependencies)) continue;
|
|
1420
|
+
const installed = installedMajor(project, name);
|
|
1421
|
+
checks.push({
|
|
1422
|
+
name,
|
|
1423
|
+
ok: installed === reactInstalled,
|
|
1424
|
+
message: installed === null ? `${name} is not installed` : installed === reactInstalled ? `${name} ${installed} matches react ${reactInstalled}` : `${name} ${installed} is installed with react ${reactInstalled}. Install the same major as react.`
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
return checks;
|
|
1428
|
+
}
|
|
1310
1429
|
function packageProblems(project, config, items) {
|
|
1311
1430
|
const needed = Object.assign(
|
|
1312
1431
|
{},
|
|
1313
|
-
...items.map((item) =>
|
|
1432
|
+
...items.map((item) => variantFor(item, config).packages.npm)
|
|
1314
1433
|
);
|
|
1315
1434
|
return Object.entries(needed).flatMap(([name, range]) => {
|
|
1316
1435
|
if (!(name in project.dependencies))
|
|
@@ -1350,6 +1469,7 @@ async function doctor(cwd, registry) {
|
|
|
1350
1469
|
majorVersion(project, "tailwindcss") === 4,
|
|
1351
1470
|
tailwindMessage(project)
|
|
1352
1471
|
);
|
|
1472
|
+
checks.push(...reactChecks(project, config));
|
|
1353
1473
|
const css = read2(cwd, config.css);
|
|
1354
1474
|
const importsTokens = css.includes("tailwind/nxds.css");
|
|
1355
1475
|
check(
|
|
@@ -1365,7 +1485,7 @@ async function doctor(cwd, registry) {
|
|
|
1365
1485
|
);
|
|
1366
1486
|
const entry = project.setup === "next-app" ? `${entryDir(project)}/layout.tsx` : findFile(project, VITE_ENTRIES);
|
|
1367
1487
|
const entrySource = read2(cwd, entry);
|
|
1368
|
-
const cssFile =
|
|
1488
|
+
const cssFile = path9.basename(config.css);
|
|
1369
1489
|
const importsCss = entrySource.includes(cssFile);
|
|
1370
1490
|
check(
|
|
1371
1491
|
"css import",
|
|
@@ -1393,8 +1513,8 @@ async function doctor(cwd, registry) {
|
|
|
1393
1513
|
);
|
|
1394
1514
|
const items = resolveItems(catalog, config.framework, known);
|
|
1395
1515
|
const missingFiles = items.flatMap(
|
|
1396
|
-
(item) =>
|
|
1397
|
-
(file) => !
|
|
1516
|
+
(item) => variantFor(item, config).files.filter(
|
|
1517
|
+
(file) => !existsSync8(path9.join(cwd, config.dir, file.path))
|
|
1398
1518
|
).map((file) => `${item.name}: ${file.path}`)
|
|
1399
1519
|
);
|
|
1400
1520
|
check(
|
|
@@ -1406,7 +1526,7 @@ async function doctor(cwd, registry) {
|
|
|
1406
1526
|
cwd,
|
|
1407
1527
|
config,
|
|
1408
1528
|
items.flatMap(
|
|
1409
|
-
(item) =>
|
|
1529
|
+
(item) => variantFor(item, config).files.map((file) => file.path)
|
|
1410
1530
|
)
|
|
1411
1531
|
).map((file) => `${config.dir}/${file}`);
|
|
1412
1532
|
check(
|
|
@@ -1436,13 +1556,347 @@ function report(checks) {
|
|
|
1436
1556
|
};
|
|
1437
1557
|
}
|
|
1438
1558
|
|
|
1439
|
-
// src/
|
|
1440
|
-
|
|
1559
|
+
// ../claude-design/src/system-index.ts
|
|
1560
|
+
var IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
1561
|
+
var PAGE_PATHS = ["manifest.json", "api/"];
|
|
1562
|
+
function mergeSystemIndex(current, exported) {
|
|
1563
|
+
return {
|
|
1564
|
+
...current,
|
|
1565
|
+
namespace: exported.namespace,
|
|
1566
|
+
editing: exported.editing,
|
|
1567
|
+
libraries: exported.libraries,
|
|
1568
|
+
lastChange: exported.lastChange
|
|
1569
|
+
};
|
|
1570
|
+
}
|
|
1571
|
+
function isPageGenerated(file) {
|
|
1572
|
+
return PAGE_PATHS.some((page) => file === page || file.startsWith(page));
|
|
1573
|
+
}
|
|
1574
|
+
function validateIndex(index) {
|
|
1575
|
+
const where = "design-system.json";
|
|
1576
|
+
const errors = [];
|
|
1577
|
+
if (index["v"] !== 3 || index["layout"] !== "files") {
|
|
1578
|
+
errors.push(`${where}: needs "v": 3 and "layout": "files".`);
|
|
1579
|
+
}
|
|
1580
|
+
if (!index["createdOnFiles"] && !index["convertedFrom"]) {
|
|
1581
|
+
errors.push(`${where}: needs the createdOnFiles marker.`);
|
|
1582
|
+
}
|
|
1583
|
+
if (typeof index["title"] !== "string" || !index["title"]) {
|
|
1584
|
+
errors.push(`${where}: needs a title.`);
|
|
1585
|
+
}
|
|
1586
|
+
if (typeof index["namespace"] !== "string" || !IDENTIFIER.test(index["namespace"])) {
|
|
1587
|
+
errors.push(`${where}: namespace must be a JS identifier.`);
|
|
1588
|
+
}
|
|
1589
|
+
const lastChange = index["lastChange"];
|
|
1590
|
+
if (!lastChange || typeof lastChange["by"] !== "string" || Number.isNaN(Date.parse(String(lastChange["at"])))) {
|
|
1591
|
+
errors.push(`${where}: lastChange needs "by" and an ISO-8601 "at".`);
|
|
1592
|
+
}
|
|
1593
|
+
return errors;
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
// src/commands/export.ts
|
|
1597
|
+
import { createHash } from "crypto";
|
|
1598
|
+
import {
|
|
1599
|
+
existsSync as existsSync9,
|
|
1600
|
+
lstatSync,
|
|
1601
|
+
mkdirSync as mkdirSync2,
|
|
1602
|
+
mkdtempSync,
|
|
1603
|
+
readdirSync,
|
|
1604
|
+
readFileSync as readFileSync5,
|
|
1605
|
+
renameSync,
|
|
1606
|
+
rmSync,
|
|
1607
|
+
writeFileSync as writeFileSync3
|
|
1608
|
+
} from "fs";
|
|
1609
|
+
import { tmpdir } from "os";
|
|
1441
1610
|
import path10 from "path";
|
|
1611
|
+
import { x as extract } from "tar";
|
|
1612
|
+
import { EnvHttpProxyAgent as EnvHttpProxyAgent2, fetch as fetch2 } from "undici";
|
|
1613
|
+
var CLAUDE_DESIGN_PACKAGE = "@nxds/claude-design";
|
|
1614
|
+
var DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org";
|
|
1615
|
+
var DEFAULT_EXPORT_DIR = "nxds-design-system";
|
|
1616
|
+
var FETCH_TIMEOUT_MS2 = 6e4;
|
|
1617
|
+
var dispatcher2 = new EnvHttpProxyAgent2();
|
|
1618
|
+
async function download(url) {
|
|
1619
|
+
let response;
|
|
1620
|
+
try {
|
|
1621
|
+
response = await fetch2(url, {
|
|
1622
|
+
dispatcher: dispatcher2,
|
|
1623
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS2)
|
|
1624
|
+
});
|
|
1625
|
+
} catch (error) {
|
|
1626
|
+
const { message, cause } = error;
|
|
1627
|
+
const reason = cause instanceof Error ? cause.message : message;
|
|
1628
|
+
throw new CliError(
|
|
1629
|
+
`Could not reach ${url} (${reason}). Check your connection, or set HTTPS_PROXY if you are behind a proxy, then run the command again.`
|
|
1630
|
+
);
|
|
1631
|
+
}
|
|
1632
|
+
if (!response.ok) {
|
|
1633
|
+
throw new CliError(
|
|
1634
|
+
`${url} answered ${response.status}. Run the command again later.`
|
|
1635
|
+
);
|
|
1636
|
+
}
|
|
1637
|
+
return Buffer.from(await response.arrayBuffer());
|
|
1638
|
+
}
|
|
1639
|
+
function packageUrl(registry) {
|
|
1640
|
+
return `${registry.replace(/\/+$/, "")}/${CLAUDE_DESIGN_PACKAGE.replace("/", "%2f")}`;
|
|
1641
|
+
}
|
|
1642
|
+
async function resolveTarball(registry, release) {
|
|
1643
|
+
const url = packageUrl(registry);
|
|
1644
|
+
const body = (await download(url)).toString("utf8");
|
|
1645
|
+
let packument;
|
|
1646
|
+
try {
|
|
1647
|
+
packument = JSON.parse(body);
|
|
1648
|
+
} catch {
|
|
1649
|
+
throw new CliError(
|
|
1650
|
+
`${url} didn't answer with JSON. Check --npm-registry, or your proxy, then run the command again.`
|
|
1651
|
+
);
|
|
1652
|
+
}
|
|
1653
|
+
const version = packument["dist-tags"]?.[release] ?? release;
|
|
1654
|
+
const dist = packument.versions?.[version]?.dist;
|
|
1655
|
+
if (!dist?.tarball || !dist.integrity) {
|
|
1656
|
+
const known = Object.keys(packument.versions ?? {}).slice(-5).join(", ");
|
|
1657
|
+
throw new UsageError(
|
|
1658
|
+
`${CLAUDE_DESIGN_PACKAGE} has no release ${release}. Recent releases: ${known || "none"}.`
|
|
1659
|
+
);
|
|
1660
|
+
}
|
|
1661
|
+
return { version, tarball: dist.tarball, integrity: dist.integrity };
|
|
1662
|
+
}
|
|
1663
|
+
function checkIntegrity(bytes, integrity, tarball) {
|
|
1664
|
+
const expected = integrity.split(/\s+/).find((entry) => entry.startsWith("sha512-"));
|
|
1665
|
+
if (!expected) {
|
|
1666
|
+
throw new CliError(
|
|
1667
|
+
`${tarball} has no sha512 integrity to check it against.`
|
|
1668
|
+
);
|
|
1669
|
+
}
|
|
1670
|
+
const actual = `sha512-${createHash("sha512").update(bytes).digest("base64")}`;
|
|
1671
|
+
if (actual !== expected) {
|
|
1672
|
+
throw new CliError(
|
|
1673
|
+
`${tarball} doesn't match its published integrity. Run the command again; if it keeps failing, report it.`
|
|
1674
|
+
);
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
async function fetchPackage(registry, release) {
|
|
1678
|
+
const { version, tarball, integrity } = await resolveTarball(
|
|
1679
|
+
registry,
|
|
1680
|
+
release
|
|
1681
|
+
);
|
|
1682
|
+
const bytes = await download(tarball);
|
|
1683
|
+
checkIntegrity(bytes, integrity, tarball);
|
|
1684
|
+
const folder = mkdtempSync(path10.join(tmpdir(), "nxds-claude-design-"));
|
|
1685
|
+
const cleanup = () => rmSync(folder, { recursive: true, force: true });
|
|
1686
|
+
try {
|
|
1687
|
+
const file = path10.join(folder, "package.tgz");
|
|
1688
|
+
writeFileSync3(file, bytes);
|
|
1689
|
+
const unpacked = path10.join(folder, "package");
|
|
1690
|
+
mkdirSync2(unpacked);
|
|
1691
|
+
await extract({
|
|
1692
|
+
file,
|
|
1693
|
+
cwd: unpacked,
|
|
1694
|
+
strip: 1,
|
|
1695
|
+
filter: (_, entry) => "type" in entry && (entry.type === "File" || entry.type === "Directory")
|
|
1696
|
+
});
|
|
1697
|
+
return { version, root: unpacked, cleanup };
|
|
1698
|
+
} catch (error) {
|
|
1699
|
+
cleanup();
|
|
1700
|
+
throw error;
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
function localPackage(source) {
|
|
1704
|
+
if (!existsSync9(path10.join(source, "project"))) {
|
|
1705
|
+
throw new UsageError(
|
|
1706
|
+
`${source} has no project/ folder. Point --source at a built @nxds/claude-design dist folder.`
|
|
1707
|
+
);
|
|
1708
|
+
}
|
|
1709
|
+
const manifest = existsSync9(path10.join(source, "package.json")) ? readJson(path10.join(source, "package.json")) : {};
|
|
1710
|
+
return {
|
|
1711
|
+
version: manifest.version ?? "local",
|
|
1712
|
+
root: source,
|
|
1713
|
+
cleanup: () => {
|
|
1714
|
+
}
|
|
1715
|
+
};
|
|
1716
|
+
}
|
|
1717
|
+
function assertWritable(out, overwrite) {
|
|
1718
|
+
if (!existsSync9(out) || readdirSync(out).length === 0 || overwrite) return;
|
|
1719
|
+
throw new UsageError(
|
|
1720
|
+
`${out} already has files. Pass --overwrite to replace its project/ folder, or choose another --out.`
|
|
1721
|
+
);
|
|
1722
|
+
}
|
|
1723
|
+
function readPaths(file) {
|
|
1724
|
+
const value = readJson(file);
|
|
1725
|
+
if (!Array.isArray(value) || !value.every((entry) => typeof entry === "string")) {
|
|
1726
|
+
throw new UsageError(
|
|
1727
|
+
`${file} must be a JSON array of the system's file paths.`
|
|
1728
|
+
);
|
|
1729
|
+
}
|
|
1730
|
+
return value;
|
|
1731
|
+
}
|
|
1732
|
+
function stalePaths(existing, exported) {
|
|
1733
|
+
return existing.filter((file) => file.startsWith("project/")).filter((file) => !isPageGenerated(file.slice("project/".length))).filter((file) => !exported.has(file)).sort();
|
|
1734
|
+
}
|
|
1735
|
+
function readManifest(root) {
|
|
1736
|
+
const file = path10.join(root, "files.json");
|
|
1737
|
+
if (!existsSync9(file)) {
|
|
1738
|
+
throw new CliError(
|
|
1739
|
+
`${root} has no files.json. Export from an @nxds/claude-design release or a built dist folder.`
|
|
1740
|
+
);
|
|
1741
|
+
}
|
|
1742
|
+
const value = readJson(file);
|
|
1743
|
+
if (!Array.isArray(value) || !value.every(
|
|
1744
|
+
(entry) => typeof entry?.path === "string" && typeof entry.sha256 === "string"
|
|
1745
|
+
)) {
|
|
1746
|
+
throw new CliError(
|
|
1747
|
+
`${file} must be a JSON array of { path, sha256 } entries.`
|
|
1748
|
+
);
|
|
1749
|
+
}
|
|
1750
|
+
return value;
|
|
1751
|
+
}
|
|
1752
|
+
function readProject2(root) {
|
|
1753
|
+
const projectDir = path10.join(root, "project");
|
|
1754
|
+
const files = /* @__PURE__ */ new Map();
|
|
1755
|
+
for (const entry of readManifest(root)) {
|
|
1756
|
+
const file = path10.join(projectDir, entry.path);
|
|
1757
|
+
const relative = path10.relative(projectDir, file);
|
|
1758
|
+
if (relative.split(path10.sep)[0] === ".." || path10.isAbsolute(relative)) {
|
|
1759
|
+
throw new CliError(`files.json lists ${entry.path}, outside project/.`);
|
|
1760
|
+
}
|
|
1761
|
+
if (!existsSync9(file) || !lstatSync(file).isFile()) {
|
|
1762
|
+
throw new CliError(
|
|
1763
|
+
`project/${entry.path} is missing from the release, or isn't a regular file.`
|
|
1764
|
+
);
|
|
1765
|
+
}
|
|
1766
|
+
const bytes = readFileSync5(file);
|
|
1767
|
+
if (createHash("sha256").update(bytes).digest("hex") !== entry.sha256) {
|
|
1768
|
+
throw new CliError(
|
|
1769
|
+
`project/${entry.path} doesn't match the release's files.json, so nothing was written.`
|
|
1770
|
+
);
|
|
1771
|
+
}
|
|
1772
|
+
files.set(entry.path, bytes);
|
|
1773
|
+
}
|
|
1774
|
+
return files;
|
|
1775
|
+
}
|
|
1776
|
+
function mergeIndex(files, currentFile) {
|
|
1777
|
+
const exported = files.get("design-system.json");
|
|
1778
|
+
if (!exported) throw new CliError("The release has no design-system.json.");
|
|
1779
|
+
const merged = mergeSystemIndex(
|
|
1780
|
+
readJson(currentFile),
|
|
1781
|
+
JSON.parse(exported.toString("utf8"))
|
|
1782
|
+
);
|
|
1783
|
+
const errors = validateIndex(merged);
|
|
1784
|
+
if (errors.length > 0) {
|
|
1785
|
+
throw new UsageError(
|
|
1786
|
+
`${currentFile} merges into an index the page can't read, so nothing was written:
|
|
1787
|
+
${errors.map((error) => ` ${error}`).join("\n")}`
|
|
1788
|
+
);
|
|
1789
|
+
}
|
|
1790
|
+
files.set(
|
|
1791
|
+
"design-system.json",
|
|
1792
|
+
Buffer.from(`${JSON.stringify(merged, null, 2)}
|
|
1793
|
+
`)
|
|
1794
|
+
);
|
|
1795
|
+
}
|
|
1796
|
+
function writeProject(out, files) {
|
|
1797
|
+
mkdirSync2(out, { recursive: true });
|
|
1798
|
+
const staging = makeStaging(path10.dirname(out), out);
|
|
1799
|
+
const next = path10.join(staging, "next");
|
|
1800
|
+
const previous = path10.join(staging, "previous");
|
|
1801
|
+
const projectDir = path10.join(out, "project");
|
|
1802
|
+
try {
|
|
1803
|
+
for (const [file, bytes] of files) {
|
|
1804
|
+
mkdirSync2(path10.dirname(path10.join(next, file)), { recursive: true });
|
|
1805
|
+
writeFileSync3(path10.join(next, file), bytes);
|
|
1806
|
+
}
|
|
1807
|
+
if (existsSync9(projectDir)) renameSync(projectDir, previous);
|
|
1808
|
+
renameSync(next, projectDir);
|
|
1809
|
+
} catch (error) {
|
|
1810
|
+
restore(previous, projectDir, error);
|
|
1811
|
+
rmSync(staging, { recursive: true, force: true });
|
|
1812
|
+
throw renameError(error, out);
|
|
1813
|
+
}
|
|
1814
|
+
rmSync(staging, { recursive: true, force: true });
|
|
1815
|
+
}
|
|
1816
|
+
function makeStaging(parent, out) {
|
|
1817
|
+
try {
|
|
1818
|
+
return mkdtempSync(path10.join(parent, ".nxds-project-"));
|
|
1819
|
+
} catch (error) {
|
|
1820
|
+
const { code, message } = error;
|
|
1821
|
+
if (code !== "EACCES" && code !== "EPERM" && code !== "EROFS") throw error;
|
|
1822
|
+
throw new UsageError(
|
|
1823
|
+
`The export stages project/ in ${parent}, beside ${out}, so ${parent} must be writable (${message}).`
|
|
1824
|
+
);
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
function restore(previous, projectDir, cause) {
|
|
1828
|
+
if (!existsSync9(previous)) return;
|
|
1829
|
+
try {
|
|
1830
|
+
renameSync(previous, projectDir);
|
|
1831
|
+
} catch (error) {
|
|
1832
|
+
throw new CliError(
|
|
1833
|
+
`The export failed (${cause.message}) and couldn't put the old project/ back (${error.message}). It is in ${previous}.`
|
|
1834
|
+
);
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
function renameError(error, out) {
|
|
1838
|
+
if (error.code !== "EXDEV") return error;
|
|
1839
|
+
return new UsageError(
|
|
1840
|
+
`${out} is on another filesystem from its parent folder, so project/ can't be moved into it. Pass an --out folder inside ${out}.`
|
|
1841
|
+
);
|
|
1842
|
+
}
|
|
1843
|
+
async function exportClaudeDesign(cwd, options) {
|
|
1844
|
+
const out = path10.resolve(cwd, options.out ?? DEFAULT_EXPORT_DIR);
|
|
1845
|
+
assertWritable(out, options.overwrite);
|
|
1846
|
+
const pkg = options.source ? localPackage(path10.resolve(cwd, options.source)) : await fetchPackage(
|
|
1847
|
+
options.npmRegistry ?? DEFAULT_NPM_REGISTRY,
|
|
1848
|
+
options.release ?? "latest"
|
|
1849
|
+
);
|
|
1850
|
+
try {
|
|
1851
|
+
const files = readProject2(pkg.root);
|
|
1852
|
+
if (options.index) mergeIndex(files, path10.resolve(cwd, options.index));
|
|
1853
|
+
const exported = new Set(
|
|
1854
|
+
[...files.keys()].map((file) => `project/${file}`)
|
|
1855
|
+
);
|
|
1856
|
+
const removed = options.existing ? stalePaths(readPaths(path10.resolve(cwd, options.existing)), exported) : [];
|
|
1857
|
+
const coverageFile = path10.join(pkg.root, "coverage.md");
|
|
1858
|
+
const coverage = existsSync9(coverageFile) ? readFileSync5(coverageFile) : void 0;
|
|
1859
|
+
writeProject(out, files);
|
|
1860
|
+
if (coverage) writeFileSync3(path10.join(out, "coverage.md"), coverage);
|
|
1861
|
+
if (options.existing) {
|
|
1862
|
+
writeFileSync3(
|
|
1863
|
+
path10.join(out, "removed.json"),
|
|
1864
|
+
`${JSON.stringify(removed, null, 2)}
|
|
1865
|
+
`
|
|
1866
|
+
);
|
|
1867
|
+
}
|
|
1868
|
+
const relative = path10.relative(cwd, out) || ".";
|
|
1869
|
+
const lines2 = [
|
|
1870
|
+
`Exported nxds ${pkg.version} as a Claude Design System: ${files.size} files in ${path10.join(relative, "project")}.`,
|
|
1871
|
+
`Publish ${relative} as a Design System artifact in Claude, with ${relative} as the root.`
|
|
1872
|
+
];
|
|
1873
|
+
if (removed.length > 0) {
|
|
1874
|
+
lines2.push(
|
|
1875
|
+
`Remove the ${removed.length} files the system has and this release doesn't: they're listed in ${path10.join(relative, "removed.json")}.`
|
|
1876
|
+
);
|
|
1877
|
+
}
|
|
1878
|
+
if (coverage) {
|
|
1879
|
+
lines2.push(
|
|
1880
|
+
`The props the canvas can't set are in ${path10.join(relative, "coverage.md")}.`
|
|
1881
|
+
);
|
|
1882
|
+
}
|
|
1883
|
+
return {
|
|
1884
|
+
ok: true,
|
|
1885
|
+
lines: lines2,
|
|
1886
|
+
data: { version: pkg.version, out, files: files.size, removed }
|
|
1887
|
+
};
|
|
1888
|
+
} finally {
|
|
1889
|
+
pkg.cleanup();
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
// src/commands/install.ts
|
|
1894
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync3, writeFileSync as writeFileSync5 } from "fs";
|
|
1895
|
+
import path12 from "path";
|
|
1442
1896
|
|
|
1443
1897
|
// src/install.ts
|
|
1444
|
-
import { existsSync as
|
|
1445
|
-
import
|
|
1898
|
+
import { existsSync as existsSync10, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
|
|
1899
|
+
import path11 from "path";
|
|
1446
1900
|
function emptyInstallResult() {
|
|
1447
1901
|
return {
|
|
1448
1902
|
items: [],
|
|
@@ -1458,11 +1912,11 @@ function emptyInstallResult() {
|
|
|
1458
1912
|
}
|
|
1459
1913
|
function addCssImports(project, config, files, dryRun) {
|
|
1460
1914
|
if (files.length === 0) return [];
|
|
1461
|
-
const entry =
|
|
1462
|
-
const current =
|
|
1915
|
+
const entry = path11.join(project.root, config.css);
|
|
1916
|
+
const current = existsSync10(entry) ? readFileSync6(entry, "utf8") : "";
|
|
1463
1917
|
const added = [...new Set(files)].map((file) => cssImportLine(config, file)).filter((line) => !current.includes(line));
|
|
1464
1918
|
if (added.length > 0 && !dryRun) {
|
|
1465
|
-
|
|
1919
|
+
writeFileSync4(entry, `${current.trimEnd()}
|
|
1466
1920
|
${added.join("\n")}
|
|
1467
1921
|
`);
|
|
1468
1922
|
}
|
|
@@ -1470,7 +1924,7 @@ ${added.join("\n")}
|
|
|
1470
1924
|
}
|
|
1471
1925
|
function installItems(catalog, project, config, names, previous, options) {
|
|
1472
1926
|
const items = resolveItems(catalog, config.framework, names);
|
|
1473
|
-
const variants = items.map((item) =>
|
|
1927
|
+
const variants = items.map((item) => variantFor(item, config));
|
|
1474
1928
|
const files = new Map(
|
|
1475
1929
|
variants.flatMap((variant) => variant.files).map((file) => [file.path, file])
|
|
1476
1930
|
);
|
|
@@ -1536,6 +1990,12 @@ function installLines(config, result, options) {
|
|
|
1536
1990
|
function settled(result) {
|
|
1537
1991
|
return result.skipped.length === 0 && result.conflicted.length === 0 && result.unresolved.length === 0;
|
|
1538
1992
|
}
|
|
1993
|
+
function assertFilesMatchReact(config, react) {
|
|
1994
|
+
if (config.react === react) return;
|
|
1995
|
+
throw new CliError(
|
|
1996
|
+
`nxds.json has React ${config.react} files, and this app uses React ${react}. Run \`${CLI_COMMAND} update\` to switch them, then run this command again.`
|
|
1997
|
+
);
|
|
1998
|
+
}
|
|
1539
1999
|
function nxdsDir(dir) {
|
|
1540
2000
|
const normalized = relativePath(dir);
|
|
1541
2001
|
if (normalized) return normalized;
|
|
@@ -1568,11 +2028,17 @@ async function init(cwd, registry, options) {
|
|
|
1568
2028
|
"nxds components are TypeScript. Add a tsconfig.json to your app, then run init again."
|
|
1569
2029
|
);
|
|
1570
2030
|
}
|
|
2031
|
+
const react = reactMajor(project);
|
|
1571
2032
|
const addTailwind = !("tailwindcss" in project.dependencies);
|
|
1572
2033
|
if (!addTailwind) assertTailwind4(project);
|
|
1573
2034
|
const dir = options.dir ? nxdsDir(options.dir) : void 0;
|
|
1574
2035
|
const existing = readConfig(cwd);
|
|
1575
|
-
const catalog = await loadRelease(
|
|
2036
|
+
const catalog = await loadRelease(
|
|
2037
|
+
registry,
|
|
2038
|
+
existing?.version ?? "latest",
|
|
2039
|
+
react
|
|
2040
|
+
);
|
|
2041
|
+
if (existing) assertFilesMatchReact(existing, react);
|
|
1576
2042
|
if (existing && dir && dir !== existing.dir) {
|
|
1577
2043
|
throw new CliError(
|
|
1578
2044
|
`nxds is already set up in ${existing.dir}. Run init without --dir, or move the folder and change "dir" in nxds.json.`
|
|
@@ -1580,12 +2046,13 @@ async function init(cwd, registry, options) {
|
|
|
1580
2046
|
}
|
|
1581
2047
|
const config = existing ?? {
|
|
1582
2048
|
framework: "react",
|
|
2049
|
+
react,
|
|
1583
2050
|
version: catalog.version,
|
|
1584
2051
|
dir: dir ?? defaultDir(project),
|
|
1585
2052
|
css: cssEntryPath(project),
|
|
1586
2053
|
items: []
|
|
1587
2054
|
};
|
|
1588
|
-
const setup = setupVariant(catalog, config
|
|
2055
|
+
const setup = setupVariant(catalog, config);
|
|
1589
2056
|
const wiring = planWiring(project, config, addTailwind, setup.steps);
|
|
1590
2057
|
const tailwind = packagesToInstall(
|
|
1591
2058
|
project,
|
|
@@ -1604,9 +2071,9 @@ async function init(cwd, registry, options) {
|
|
|
1604
2071
|
);
|
|
1605
2072
|
if (!options.dryRun) {
|
|
1606
2073
|
for (const change of wiring.changes) {
|
|
1607
|
-
const target =
|
|
1608
|
-
|
|
1609
|
-
|
|
2074
|
+
const target = path12.join(project.root, change.path);
|
|
2075
|
+
mkdirSync3(path12.dirname(target), { recursive: true });
|
|
2076
|
+
writeFileSync5(target, change.content);
|
|
1610
2077
|
}
|
|
1611
2078
|
writeConfig(cwd, config);
|
|
1612
2079
|
}
|
|
@@ -1616,6 +2083,7 @@ async function init(cwd, registry, options) {
|
|
|
1616
2083
|
return {
|
|
1617
2084
|
data: {
|
|
1618
2085
|
setup: project.setup,
|
|
2086
|
+
react: config.react,
|
|
1619
2087
|
version: config.version,
|
|
1620
2088
|
dir: config.dir,
|
|
1621
2089
|
css: config.css,
|
|
@@ -1627,7 +2095,7 @@ async function init(cwd, registry, options) {
|
|
|
1627
2095
|
manual: wiring.manual
|
|
1628
2096
|
},
|
|
1629
2097
|
lines: [
|
|
1630
|
-
`nxds ${config.version} in ${config.dir}`,
|
|
2098
|
+
`nxds ${config.version} in ${config.dir}, for React ${config.react}`,
|
|
1631
2099
|
...installLines(config, { ...installed, packages }, options),
|
|
1632
2100
|
...wiring.changes.map(
|
|
1633
2101
|
(change) => `${change.created ? createVerb : editVerb} ${change.path}`
|
|
@@ -1649,10 +2117,12 @@ function manualLines(manual) {
|
|
|
1649
2117
|
}
|
|
1650
2118
|
async function add(cwd, registry, names, options) {
|
|
1651
2119
|
const config = requireConfig(cwd);
|
|
1652
|
-
const
|
|
2120
|
+
const project = readProject(cwd);
|
|
2121
|
+
assertFilesMatchReact(config, reactMajor(project));
|
|
2122
|
+
const catalog = await loadRelease(registry, config.version, config.react);
|
|
1653
2123
|
const installed = installItems(
|
|
1654
2124
|
catalog,
|
|
1655
|
-
|
|
2125
|
+
project,
|
|
1656
2126
|
config,
|
|
1657
2127
|
names,
|
|
1658
2128
|
null,
|
|
@@ -1670,32 +2140,34 @@ async function add(cwd, registry, names, options) {
|
|
|
1670
2140
|
ok: settled(installed)
|
|
1671
2141
|
};
|
|
1672
2142
|
}
|
|
1673
|
-
function filesOf(catalog,
|
|
2143
|
+
function filesOf(catalog, target, names) {
|
|
1674
2144
|
return new Set(
|
|
1675
|
-
resolveItems(catalog, framework, names).flatMap(
|
|
1676
|
-
(item) =>
|
|
2145
|
+
resolveItems(catalog, target.framework, names).flatMap(
|
|
2146
|
+
(item) => variantFor(item, target).files.map((file) => file.path)
|
|
1677
2147
|
)
|
|
1678
2148
|
);
|
|
1679
2149
|
}
|
|
1680
2150
|
async function update(cwd, registry, options) {
|
|
1681
2151
|
const config = requireConfig(cwd);
|
|
2152
|
+
const project = readProject(cwd);
|
|
1682
2153
|
const from = config.version;
|
|
1683
|
-
const
|
|
2154
|
+
const fromReact = config.react;
|
|
2155
|
+
const react = reactMajor(project);
|
|
2156
|
+
const latest = await loadRelease(registry, "latest", react);
|
|
1684
2157
|
const { known, removed } = splitInstalled(latest, config.items);
|
|
1685
|
-
const current = filesOf(latest, config
|
|
2158
|
+
const current = filesOf(latest, { ...config, react }, known);
|
|
1686
2159
|
const previous = await fetchRelease(registry, from);
|
|
1687
|
-
const installedBefore = previous ? filesOf(
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
splitInstalled(previous, config.items).known
|
|
1691
|
-
) : /* @__PURE__ */ new Set();
|
|
1692
|
-
const unresolved = from === latest.version || options.overwrite ? [] : conflictedFiles(cwd, config, /* @__PURE__ */ new Set([...current, ...installedBefore]));
|
|
2160
|
+
const installedBefore = previous ? filesOf(previous, config, splitInstalled(previous, config.items).known) : /* @__PURE__ */ new Set();
|
|
2161
|
+
const unchanged = from === latest.version && fromReact === react;
|
|
2162
|
+
const unresolved = unchanged || options.overwrite ? [] : conflictedFiles(cwd, config, /* @__PURE__ */ new Set([...current, ...installedBefore]));
|
|
1693
2163
|
if (unresolved.length > 0) {
|
|
1694
2164
|
const data2 = {
|
|
1695
2165
|
...emptyInstallResult(),
|
|
1696
2166
|
dryRun: options.dryRun,
|
|
1697
2167
|
from,
|
|
1698
2168
|
version: from,
|
|
2169
|
+
fromReact,
|
|
2170
|
+
react: fromReact,
|
|
1699
2171
|
unresolved,
|
|
1700
2172
|
removed: [],
|
|
1701
2173
|
orphaned: []
|
|
@@ -1710,24 +2182,30 @@ async function update(cwd, registry, options) {
|
|
|
1710
2182
|
};
|
|
1711
2183
|
}
|
|
1712
2184
|
const orphaned = [...installedBefore].filter(
|
|
1713
|
-
(file) => !current.has(file) &&
|
|
2185
|
+
(file) => !current.has(file) && existsSync11(path12.join(cwd, config.dir, file))
|
|
1714
2186
|
);
|
|
2187
|
+
const previousFiles = previous && releaseFiles(previous, config);
|
|
1715
2188
|
config.items = known;
|
|
1716
2189
|
const installed = installItems(
|
|
1717
2190
|
latest,
|
|
1718
|
-
|
|
1719
|
-
config,
|
|
2191
|
+
project,
|
|
2192
|
+
{ ...config, react },
|
|
1720
2193
|
known,
|
|
1721
|
-
|
|
2194
|
+
previousFiles,
|
|
1722
2195
|
options
|
|
1723
2196
|
);
|
|
1724
|
-
const stays =
|
|
1725
|
-
if (!stays)
|
|
2197
|
+
const stays = !unchanged && installed.skipped.length > 0;
|
|
2198
|
+
if (!stays) {
|
|
2199
|
+
config.version = latest.version;
|
|
2200
|
+
config.react = react;
|
|
2201
|
+
}
|
|
1726
2202
|
if (!options.dryRun) writeConfig(cwd, config);
|
|
1727
2203
|
const data = {
|
|
1728
2204
|
dryRun: options.dryRun,
|
|
1729
2205
|
from,
|
|
1730
2206
|
version: config.version,
|
|
2207
|
+
fromReact,
|
|
2208
|
+
react: config.react,
|
|
1731
2209
|
...installed,
|
|
1732
2210
|
removed,
|
|
1733
2211
|
orphaned
|
|
@@ -1745,6 +2223,9 @@ async function update(cwd, registry, options) {
|
|
|
1745
2223
|
...orphaned.map(
|
|
1746
2224
|
(file) => `No installed item uses ${config.dir}/${file} any more. Delete it once nothing in your app imports it.`
|
|
1747
2225
|
),
|
|
2226
|
+
...fromReact !== react && !stays ? [
|
|
2227
|
+
`${options.dryRun ? "Would switch" : "Switched"} the files from React ${fromReact} to React ${react}.`
|
|
2228
|
+
] : [],
|
|
1748
2229
|
stays ? `nxds.json stays on ${from} until every file above is resolved or replaced.` : `nxds ${from === latest.version ? "is on" : `${options.dryRun ? "would move" : "moved"} from ${from} to`} ${latest.version}.`
|
|
1749
2230
|
],
|
|
1750
2231
|
ok: settled(installed)
|
|
@@ -1755,7 +2236,7 @@ async function update(cwd, registry, options) {
|
|
|
1755
2236
|
function context(command) {
|
|
1756
2237
|
const { cwd, registry } = command.optsWithGlobals();
|
|
1757
2238
|
return {
|
|
1758
|
-
cwd:
|
|
2239
|
+
cwd: path13.resolve(cwd ?? process.cwd()),
|
|
1759
2240
|
registry: resolveRegistry(registry)
|
|
1760
2241
|
};
|
|
1761
2242
|
}
|
|
@@ -1777,7 +2258,7 @@ function program(done, write) {
|
|
|
1777
2258
|
// Errors are printed once, by `main`, as text or JSON.
|
|
1778
2259
|
outputError: () => {
|
|
1779
2260
|
}
|
|
1780
|
-
}).exitOverride().name(CLI_COMMAND).description("Add nxds to your app.").version("0.
|
|
2261
|
+
}).exitOverride().name(CLI_COMMAND).description("Add nxds to your app.").version("0.4.0", "-v, --version", "print the version").helpOption("-h, --help", "show help").helpCommand("help [command]", "show help for a command").option("--json", "print machine-readable JSON").option("--cwd <path>", "run in another folder").addOption(
|
|
1781
2262
|
new Option(
|
|
1782
2263
|
"--registry <url|path>",
|
|
1783
2264
|
"where to get nxds releases (default: https://nxds.dev/r)"
|
|
@@ -1837,6 +2318,28 @@ Docs: https://nxds.dev/getting-started/install`
|
|
|
1837
2318
|
const { cwd, registry } = context(command);
|
|
1838
2319
|
done(await diff(cwd, registry, items));
|
|
1839
2320
|
});
|
|
2321
|
+
cli.command("export").description(
|
|
2322
|
+
"export nxds as a Claude Design System folder, ready to publish"
|
|
2323
|
+
).addArgument(
|
|
2324
|
+
new Argument("<target>", "what to export").choices(["claude-design"])
|
|
2325
|
+
).option("--out <path>", `folder to write (default: ${DEFAULT_EXPORT_DIR})`).option("--release <version>", "nxds release to export (default: latest)").option(
|
|
2326
|
+
"--index <path>",
|
|
2327
|
+
"the system's current design-system.json, to keep its title and creation date"
|
|
2328
|
+
).option(
|
|
2329
|
+
"--existing <path>",
|
|
2330
|
+
"a JSON list of the system's current file paths, to list the ones this release removes"
|
|
2331
|
+
).option("--overwrite", "replace the project/ folder in --out", false).option(
|
|
2332
|
+
"--source <path>",
|
|
2333
|
+
"a built @nxds/claude-design dist folder, instead of the npm release"
|
|
2334
|
+
).addOption(
|
|
2335
|
+
new Option(
|
|
2336
|
+
"--npm-registry <url>",
|
|
2337
|
+
`npm registry to download from (default: ${DEFAULT_NPM_REGISTRY})`
|
|
2338
|
+
).env("npm_config_registry")
|
|
2339
|
+
).action(async (_target, _options, command) => {
|
|
2340
|
+
const { cwd } = context(command);
|
|
2341
|
+
done(await exportClaudeDesign(cwd, command.opts()));
|
|
2342
|
+
});
|
|
1840
2343
|
cli.command("doctor").description("check that nxds is wired up correctly").action(async (_options, command) => {
|
|
1841
2344
|
const { cwd, registry } = context(command);
|
|
1842
2345
|
done(await doctor(cwd, registry));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nxds-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Install nxds components, blocks and setup into your app.",
|
|
6
6
|
"keywords": [
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"commander": "^14.0.3",
|
|
37
37
|
"node-diff3": "^3.2.1",
|
|
38
38
|
"semver": "^7.8.5",
|
|
39
|
+
"tar": "^7.5.22",
|
|
39
40
|
"undici": "^7.30.1",
|
|
40
41
|
"zod": "^4.6.7"
|
|
41
42
|
},
|
|
@@ -44,7 +45,8 @@
|
|
|
44
45
|
"@types/semver": "^7.8.0",
|
|
45
46
|
"tsup": "^8.5.1",
|
|
46
47
|
"typescript": "^5.9.3",
|
|
47
|
-
"@nxds/catalog": "0.0.0"
|
|
48
|
+
"@nxds/catalog": "0.0.0",
|
|
49
|
+
"@nxds/claude-design": "0.6.0"
|
|
48
50
|
},
|
|
49
51
|
"scripts": {
|
|
50
52
|
"build": "tsup",
|