nxds-cli 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +235 -88
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { Command, CommanderError, Option } from "commander";
5
- import path11 from "path";
5
+ import path12 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.2.0"}. Run \`${CLI_COMMAND}\` to get the newest one.`
247
+ `${release} needs a newer nxds-cli than ${"0.3.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) return 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
- 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.`
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 setupVariant(catalog, framework) {
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 variantOf(setup, framework);
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, framework) {
311
+ function releaseFiles(catalog, target) {
272
312
  return new Map(
273
- catalog.items.flatMap(
274
- (item) => (item.variants[framework]?.files ?? []).map(
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(cwd, registry) {
461
- return loadRelease(registry, readConfig(cwd)?.version ?? "latest");
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 items = (await appRelease(cwd, registry)).items.filter(
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 items = (await appRelease(cwd, registry)).items.filter((item) => item.name !== SETUP).flatMap((item) => {
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(cwd, catalog) {
505
- const configured = readConfig(cwd)?.framework;
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 catalog = await appRelease(cwd, registry);
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 = variantOf(item, projectFramework(cwd, catalog));
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 existsSync7, readFileSync as readFileSync4 } from "fs";
577
- import path8 from "path";
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 existsSync5, mkdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
582
- import path6 from "path";
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 = path6.join(root, config.dir, file);
608
- return existsSync5(target) && hasConflictMarkers(readFileSync2(target, "utf8"));
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 = path6.join(root, config.dir, file.path);
613
- if (!existsSync5(target)) return "missing";
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 = path6.join(root, config.dir, file);
638
- mkdirSync(path6.dirname(target), { recursive: true });
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 = path6.join(root, config.dir, file.path);
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 = path6.relative(path6.dirname(fromFile), toFile).split(path6.sep).join("/");
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 existsSync6, readFileSync as readFileSync3 } from "fs";
691
- import path7 from "path";
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 parse(source, {
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) => existsSync6(path7.join(project.root, candidate, "layout.tsx"))
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(path7.join(project.root, file), "utf8");
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") && !existsSync6(path7.join(project.root, "nxds.vite.ts"));
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 = existsSync6(path7.join(project.root, configFile)) ? read(project, configFile) : "";
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 (existsSync6(path7.join(project.root, file))) return NO_CHANGES;
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 latest = await loadRelease(registry, "latest");
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.framework);
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) => variantOf(item, config.framework).files.map((file) => {
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 || !existsSync7(path8.join(root, file))) return "";
1290
- return readFileSync4(path8.join(root, file), "utf8");
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 = path8.posix.dirname(config.css);
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 = path8.posix.join(entryDir2, base).replace(/\/+$/, "");
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) => variantOf(item, config.framework).packages.npm)
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 = path8.basename(config.css);
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) => variantOf(item, config.framework).files.filter(
1397
- (file) => !existsSync7(path8.join(cwd, config.dir, file.path))
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) => variantOf(item, config.framework).files.map((file) => file.path)
1529
+ (item) => variantFor(item, config).files.map((file) => file.path)
1410
1530
  )
1411
1531
  ).map((file) => `${config.dir}/${file}`);
1412
1532
  check(
@@ -1437,12 +1557,12 @@ function report(checks) {
1437
1557
  }
1438
1558
 
1439
1559
  // src/commands/install.ts
1440
- import { existsSync as existsSync9, mkdirSync as mkdirSync2, writeFileSync as writeFileSync4 } from "fs";
1441
- import path10 from "path";
1560
+ import { existsSync as existsSync10, mkdirSync as mkdirSync2, writeFileSync as writeFileSync4 } from "fs";
1561
+ import path11 from "path";
1442
1562
 
1443
1563
  // src/install.ts
1444
- import { existsSync as existsSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
1445
- import path9 from "path";
1564
+ import { existsSync as existsSync9, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
1565
+ import path10 from "path";
1446
1566
  function emptyInstallResult() {
1447
1567
  return {
1448
1568
  items: [],
@@ -1458,8 +1578,8 @@ function emptyInstallResult() {
1458
1578
  }
1459
1579
  function addCssImports(project, config, files, dryRun) {
1460
1580
  if (files.length === 0) return [];
1461
- const entry = path9.join(project.root, config.css);
1462
- const current = existsSync8(entry) ? readFileSync5(entry, "utf8") : "";
1581
+ const entry = path10.join(project.root, config.css);
1582
+ const current = existsSync9(entry) ? readFileSync5(entry, "utf8") : "";
1463
1583
  const added = [...new Set(files)].map((file) => cssImportLine(config, file)).filter((line) => !current.includes(line));
1464
1584
  if (added.length > 0 && !dryRun) {
1465
1585
  writeFileSync3(entry, `${current.trimEnd()}
@@ -1470,7 +1590,7 @@ ${added.join("\n")}
1470
1590
  }
1471
1591
  function installItems(catalog, project, config, names, previous, options) {
1472
1592
  const items = resolveItems(catalog, config.framework, names);
1473
- const variants = items.map((item) => variantOf(item, config.framework));
1593
+ const variants = items.map((item) => variantFor(item, config));
1474
1594
  const files = new Map(
1475
1595
  variants.flatMap((variant) => variant.files).map((file) => [file.path, file])
1476
1596
  );
@@ -1536,6 +1656,12 @@ function installLines(config, result, options) {
1536
1656
  function settled(result) {
1537
1657
  return result.skipped.length === 0 && result.conflicted.length === 0 && result.unresolved.length === 0;
1538
1658
  }
1659
+ function assertFilesMatchReact(config, react) {
1660
+ if (config.react === react) return;
1661
+ throw new CliError(
1662
+ `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.`
1663
+ );
1664
+ }
1539
1665
  function nxdsDir(dir) {
1540
1666
  const normalized = relativePath(dir);
1541
1667
  if (normalized) return normalized;
@@ -1568,11 +1694,17 @@ async function init(cwd, registry, options) {
1568
1694
  "nxds components are TypeScript. Add a tsconfig.json to your app, then run init again."
1569
1695
  );
1570
1696
  }
1697
+ const react = reactMajor(project);
1571
1698
  const addTailwind = !("tailwindcss" in project.dependencies);
1572
1699
  if (!addTailwind) assertTailwind4(project);
1573
1700
  const dir = options.dir ? nxdsDir(options.dir) : void 0;
1574
1701
  const existing = readConfig(cwd);
1575
- const catalog = await loadRelease(registry, existing?.version ?? "latest");
1702
+ const catalog = await loadRelease(
1703
+ registry,
1704
+ existing?.version ?? "latest",
1705
+ react
1706
+ );
1707
+ if (existing) assertFilesMatchReact(existing, react);
1576
1708
  if (existing && dir && dir !== existing.dir) {
1577
1709
  throw new CliError(
1578
1710
  `nxds is already set up in ${existing.dir}. Run init without --dir, or move the folder and change "dir" in nxds.json.`
@@ -1580,12 +1712,13 @@ async function init(cwd, registry, options) {
1580
1712
  }
1581
1713
  const config = existing ?? {
1582
1714
  framework: "react",
1715
+ react,
1583
1716
  version: catalog.version,
1584
1717
  dir: dir ?? defaultDir(project),
1585
1718
  css: cssEntryPath(project),
1586
1719
  items: []
1587
1720
  };
1588
- const setup = setupVariant(catalog, config.framework);
1721
+ const setup = setupVariant(catalog, config);
1589
1722
  const wiring = planWiring(project, config, addTailwind, setup.steps);
1590
1723
  const tailwind = packagesToInstall(
1591
1724
  project,
@@ -1604,8 +1737,8 @@ async function init(cwd, registry, options) {
1604
1737
  );
1605
1738
  if (!options.dryRun) {
1606
1739
  for (const change of wiring.changes) {
1607
- const target = path10.join(project.root, change.path);
1608
- mkdirSync2(path10.dirname(target), { recursive: true });
1740
+ const target = path11.join(project.root, change.path);
1741
+ mkdirSync2(path11.dirname(target), { recursive: true });
1609
1742
  writeFileSync4(target, change.content);
1610
1743
  }
1611
1744
  writeConfig(cwd, config);
@@ -1616,6 +1749,7 @@ async function init(cwd, registry, options) {
1616
1749
  return {
1617
1750
  data: {
1618
1751
  setup: project.setup,
1752
+ react: config.react,
1619
1753
  version: config.version,
1620
1754
  dir: config.dir,
1621
1755
  css: config.css,
@@ -1627,7 +1761,7 @@ async function init(cwd, registry, options) {
1627
1761
  manual: wiring.manual
1628
1762
  },
1629
1763
  lines: [
1630
- `nxds ${config.version} in ${config.dir}`,
1764
+ `nxds ${config.version} in ${config.dir}, for React ${config.react}`,
1631
1765
  ...installLines(config, { ...installed, packages }, options),
1632
1766
  ...wiring.changes.map(
1633
1767
  (change) => `${change.created ? createVerb : editVerb} ${change.path}`
@@ -1649,10 +1783,12 @@ function manualLines(manual) {
1649
1783
  }
1650
1784
  async function add(cwd, registry, names, options) {
1651
1785
  const config = requireConfig(cwd);
1652
- const catalog = await loadRelease(registry, config.version);
1786
+ const project = readProject(cwd);
1787
+ assertFilesMatchReact(config, reactMajor(project));
1788
+ const catalog = await loadRelease(registry, config.version, config.react);
1653
1789
  const installed = installItems(
1654
1790
  catalog,
1655
- readProject(cwd),
1791
+ project,
1656
1792
  config,
1657
1793
  names,
1658
1794
  null,
@@ -1670,32 +1806,34 @@ async function add(cwd, registry, names, options) {
1670
1806
  ok: settled(installed)
1671
1807
  };
1672
1808
  }
1673
- function filesOf(catalog, framework, names) {
1809
+ function filesOf(catalog, target, names) {
1674
1810
  return new Set(
1675
- resolveItems(catalog, framework, names).flatMap(
1676
- (item) => variantOf(item, framework).files.map((file) => file.path)
1811
+ resolveItems(catalog, target.framework, names).flatMap(
1812
+ (item) => variantFor(item, target).files.map((file) => file.path)
1677
1813
  )
1678
1814
  );
1679
1815
  }
1680
1816
  async function update(cwd, registry, options) {
1681
1817
  const config = requireConfig(cwd);
1818
+ const project = readProject(cwd);
1682
1819
  const from = config.version;
1683
- const latest = await loadRelease(registry, "latest");
1820
+ const fromReact = config.react;
1821
+ const react = reactMajor(project);
1822
+ const latest = await loadRelease(registry, "latest", react);
1684
1823
  const { known, removed } = splitInstalled(latest, config.items);
1685
- const current = filesOf(latest, config.framework, known);
1824
+ const current = filesOf(latest, { ...config, react }, known);
1686
1825
  const previous = await fetchRelease(registry, from);
1687
- const installedBefore = previous ? filesOf(
1688
- previous,
1689
- config.framework,
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]));
1826
+ const installedBefore = previous ? filesOf(previous, config, splitInstalled(previous, config.items).known) : /* @__PURE__ */ new Set();
1827
+ const unchanged = from === latest.version && fromReact === react;
1828
+ const unresolved = unchanged || options.overwrite ? [] : conflictedFiles(cwd, config, /* @__PURE__ */ new Set([...current, ...installedBefore]));
1693
1829
  if (unresolved.length > 0) {
1694
1830
  const data2 = {
1695
1831
  ...emptyInstallResult(),
1696
1832
  dryRun: options.dryRun,
1697
1833
  from,
1698
1834
  version: from,
1835
+ fromReact,
1836
+ react: fromReact,
1699
1837
  unresolved,
1700
1838
  removed: [],
1701
1839
  orphaned: []
@@ -1710,24 +1848,30 @@ async function update(cwd, registry, options) {
1710
1848
  };
1711
1849
  }
1712
1850
  const orphaned = [...installedBefore].filter(
1713
- (file) => !current.has(file) && existsSync9(path10.join(cwd, config.dir, file))
1851
+ (file) => !current.has(file) && existsSync10(path11.join(cwd, config.dir, file))
1714
1852
  );
1853
+ const previousFiles = previous && releaseFiles(previous, config);
1715
1854
  config.items = known;
1716
1855
  const installed = installItems(
1717
1856
  latest,
1718
- readProject(cwd),
1719
- config,
1857
+ project,
1858
+ { ...config, react },
1720
1859
  known,
1721
- previous && releaseFiles(previous, config.framework),
1860
+ previousFiles,
1722
1861
  options
1723
1862
  );
1724
- const stays = from !== latest.version && installed.skipped.length > 0;
1725
- if (!stays) config.version = latest.version;
1863
+ const stays = !unchanged && installed.skipped.length > 0;
1864
+ if (!stays) {
1865
+ config.version = latest.version;
1866
+ config.react = react;
1867
+ }
1726
1868
  if (!options.dryRun) writeConfig(cwd, config);
1727
1869
  const data = {
1728
1870
  dryRun: options.dryRun,
1729
1871
  from,
1730
1872
  version: config.version,
1873
+ fromReact,
1874
+ react: config.react,
1731
1875
  ...installed,
1732
1876
  removed,
1733
1877
  orphaned
@@ -1745,6 +1889,9 @@ async function update(cwd, registry, options) {
1745
1889
  ...orphaned.map(
1746
1890
  (file) => `No installed item uses ${config.dir}/${file} any more. Delete it once nothing in your app imports it.`
1747
1891
  ),
1892
+ ...fromReact !== react && !stays ? [
1893
+ `${options.dryRun ? "Would switch" : "Switched"} the files from React ${fromReact} to React ${react}.`
1894
+ ] : [],
1748
1895
  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
1896
  ],
1750
1897
  ok: settled(installed)
@@ -1755,7 +1902,7 @@ async function update(cwd, registry, options) {
1755
1902
  function context(command) {
1756
1903
  const { cwd, registry } = command.optsWithGlobals();
1757
1904
  return {
1758
- cwd: path11.resolve(cwd ?? process.cwd()),
1905
+ cwd: path12.resolve(cwd ?? process.cwd()),
1759
1906
  registry: resolveRegistry(registry)
1760
1907
  };
1761
1908
  }
@@ -1777,7 +1924,7 @@ function program(done, write) {
1777
1924
  // Errors are printed once, by `main`, as text or JSON.
1778
1925
  outputError: () => {
1779
1926
  }
1780
- }).exitOverride().name(CLI_COMMAND).description("Add nxds to your app.").version("0.2.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(
1927
+ }).exitOverride().name(CLI_COMMAND).description("Add nxds to your app.").version("0.3.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
1928
  new Option(
1782
1929
  "--registry <url|path>",
1783
1930
  "where to get nxds releases (default: https://nxds.dev/r)"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nxds-cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "description": "Install nxds components, blocks and setup into your app.",
6
6
  "keywords": [