nxds-cli 0.0.0-stage → 0.0.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 (4) hide show
  1. package/LICENSE +21 -0
  2. package/dist/index.js +1670 -0
  3. package/package.json +48 -4
  4. package/README.md +0 -3
package/dist/index.js ADDED
@@ -0,0 +1,1670 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import path10 from "path";
5
+ import { parseArgs } from "util";
6
+
7
+ // src/catalog.ts
8
+ import { existsSync as existsSync2 } from "fs";
9
+ import path2 from "path";
10
+ import { valid as valid2, validRange } from "semver";
11
+
12
+ // src/config.ts
13
+ import { existsSync, writeFileSync } from "fs";
14
+ import path from "path";
15
+ import { valid } from "semver";
16
+
17
+ // src/errors.ts
18
+ import { readFileSync } from "fs";
19
+ var CliError = class extends Error {
20
+ };
21
+ function readJson(file) {
22
+ const text2 = readFileSync(file, "utf8");
23
+ try {
24
+ return JSON.parse(text2);
25
+ } catch (error) {
26
+ throw new CliError(
27
+ `${file} is not valid JSON (${error.message}). Fix it, then run the command again.`
28
+ );
29
+ }
30
+ }
31
+
32
+ // src/config.ts
33
+ var CONFIG_FILE = "nxds.json";
34
+ function configPath(root) {
35
+ return path.join(root, CONFIG_FILE);
36
+ }
37
+ function projectPath(value) {
38
+ const normalized = path.posix.normalize(value.replaceAll("\\", "/")).replace(/\/+$/, "");
39
+ if (path.posix.isAbsolute(normalized) || /^[A-Za-z]:/.test(normalized) || normalized === "." || normalized === ".." || normalized.startsWith("../")) {
40
+ return null;
41
+ }
42
+ return normalized;
43
+ }
44
+ function invalidField(value) {
45
+ if (value.framework !== "react") return "framework";
46
+ if (typeof value.version !== "string" || valid(value.version) !== value.version)
47
+ return "version";
48
+ for (const field of ["dir", "css"]) {
49
+ const file = value[field];
50
+ if (typeof file !== "string" || projectPath(file) !== file) return field;
51
+ }
52
+ const { items } = value;
53
+ if (!Array.isArray(items) || !items.every((item) => typeof item === "string"))
54
+ return "items";
55
+ return null;
56
+ }
57
+ function readConfig(root) {
58
+ const file = configPath(root);
59
+ if (!existsSync(file)) return null;
60
+ const value = readJson(file);
61
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
62
+ throw new CliError(`${CONFIG_FILE} must be a JSON object.`);
63
+ }
64
+ const field = invalidField(value);
65
+ if (field === null) return value;
66
+ throw new CliError(
67
+ `${CONFIG_FILE} has a missing or invalid "${field}". Fix it, or delete ${CONFIG_FILE} and run \`nxds init\` again.`
68
+ );
69
+ }
70
+ function requireConfig(root) {
71
+ const config = readConfig(root);
72
+ if (config) return config;
73
+ throw new CliError(`nxds is not set up here. Run \`nxds init\` first.`);
74
+ }
75
+ function writeConfig(root, config) {
76
+ writeFileSync(configPath(root), `${JSON.stringify(config, null, 2)}
77
+ `);
78
+ }
79
+
80
+ // src/catalog.ts
81
+ var SETUP = "setup";
82
+ var DEFAULT_REGISTRY = "https://nxds.dev/r";
83
+ function isUrl(registry) {
84
+ return /^https?:\/\//.test(registry);
85
+ }
86
+ function resolveRegistry(option) {
87
+ if (!option) return DEFAULT_REGISTRY;
88
+ return isUrl(option) ? option.replace(/\/+$/, "") : path2.resolve(option);
89
+ }
90
+ async function fetchJson(url) {
91
+ let response;
92
+ try {
93
+ response = await fetch(url);
94
+ } catch (error) {
95
+ throw new CliError(
96
+ `Could not reach ${url} (${error.message}). Check your connection, then run the command again.`
97
+ );
98
+ }
99
+ if (response.status === 404) return null;
100
+ if (!response.ok) {
101
+ throw new CliError(
102
+ `${url} answered ${response.status}. Run the command again later.`
103
+ );
104
+ }
105
+ try {
106
+ return await response.json();
107
+ } catch (error) {
108
+ throw new CliError(
109
+ `${url} is not valid JSON (${error.message}). Run the command again later.`
110
+ );
111
+ }
112
+ }
113
+ function registryLocation(registry, file) {
114
+ return isUrl(registry) ? `${registry}/${file}` : path2.join(registry, file);
115
+ }
116
+ async function readRegistryFile(registry, file) {
117
+ const location = registryLocation(registry, file);
118
+ if (isUrl(registry)) return fetchJson(location);
119
+ return existsSync2(location) ? readJson(location) : null;
120
+ }
121
+ var KINDS = /* @__PURE__ */ new Set(["setup", "component", "block"]);
122
+ var STEPS = new Set(
123
+ Object.keys({
124
+ "add-css-import": true,
125
+ "add-head-script": true,
126
+ "wrap-root-with-provider": true
127
+ })
128
+ );
129
+ var NPM_NAME = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
130
+ function isRange(value) {
131
+ return typeof value === "string" && /^[ -~]+$/.test(value) && value === value.trim() && validRange(value) !== null;
132
+ }
133
+ function isRecord(value) {
134
+ return typeof value === "object" && value !== null && !Array.isArray(value);
135
+ }
136
+ function isStrings(value) {
137
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string");
138
+ }
139
+ function fileProblem(file) {
140
+ if (!isRecord(file) || typeof file.content !== "string") {
141
+ return "has a file without content";
142
+ }
143
+ if (typeof file.path !== "string" || projectPath(file.path) !== file.path) {
144
+ return `has a file path outside the nxds folder: ${String(file.path)}`;
145
+ }
146
+ return null;
147
+ }
148
+ function variantProblem(variant, kind) {
149
+ if (!isRecord(variant)) return "has a variant that is not an object";
150
+ const { packages } = variant;
151
+ if (!isRecord(packages) || !isRecord(packages.npm) || !Object.entries(packages.npm).every(
152
+ ([name, range]) => NPM_NAME.test(name) && isRange(range)
153
+ ) || kind === "setup" && !isRange(packages.tailwind)) {
154
+ return 'has invalid "packages"';
155
+ }
156
+ for (const field of ["items", "css"]) {
157
+ if (!isStrings(variant[field])) return `has invalid "${field}"`;
158
+ }
159
+ const { steps } = variant;
160
+ if (!Array.isArray(steps) || !steps.every((step) => STEPS.has(step))) {
161
+ return 'has invalid "steps"';
162
+ }
163
+ if (!Array.isArray(variant.files)) return 'has invalid "files"';
164
+ return variant.files.map(fileProblem).find(Boolean) ?? null;
165
+ }
166
+ function itemProblem(item) {
167
+ if (!isRecord(item) || typeof item.name !== "string") {
168
+ return "has an item without a name";
169
+ }
170
+ const fields = ["title", "description", "docs"];
171
+ if (!KINDS.has(item.kind) || fields.some((field) => typeof item[field] !== "string") || !isRecord(item.variants)) {
172
+ return `"${item.name}" is not a catalog item`;
173
+ }
174
+ const problem = Object.values(item.variants).map((variant) => variantProblem(variant, item.kind)).find(Boolean);
175
+ return problem ? `"${item.name}" ${problem}` : null;
176
+ }
177
+ function releaseProblem(value, version) {
178
+ if (!isRecord(value)) return "it is not a JSON object";
179
+ if (value.schema !== 1) return 'its "schema" is not 1';
180
+ if (typeof value.version !== "string" || valid2(value.version) !== value.version) {
181
+ return 'its "version" is not a release version';
182
+ }
183
+ if (version !== "latest" && value.version !== version) {
184
+ return `it holds nxds ${value.version}`;
185
+ }
186
+ if (!isStrings(value.frameworks)) return 'its "frameworks" is invalid';
187
+ if (!Array.isArray(value.items)) return 'its "items" is invalid';
188
+ return value.items.map(itemProblem).find(Boolean) ?? null;
189
+ }
190
+ async function fetchRelease(registry, version) {
191
+ const file = `${version}.json`;
192
+ const value = await readRegistryFile(registry, file);
193
+ if (value === null) return null;
194
+ if (isRecord(value) && typeof value.schema === "number" && value.schema > 1) {
195
+ throw new CliError(
196
+ `nxds ${version} needs a newer CLI. Run \`npx nxds@latest\` instead.`
197
+ );
198
+ }
199
+ const problem = releaseProblem(value, version);
200
+ if (problem === null) return value;
201
+ throw new CliError(
202
+ `${registryLocation(registry, file)} is not a valid nxds release: ${problem}. Check \`--registry\`, or run the command again later.`
203
+ );
204
+ }
205
+ async function loadRelease(registry, version) {
206
+ const catalog = await fetchRelease(registry, version);
207
+ if (catalog) return catalog;
208
+ throw new CliError(
209
+ version === "latest" ? `${registry} has no nxds release.` : `${registry} has no nxds ${version}, the release this app is on. Run \`nxds update\` to move to the latest release.`
210
+ );
211
+ }
212
+ function findItem(catalog, name) {
213
+ const item = catalog.items.find((candidate) => candidate.name === name);
214
+ if (item) return item;
215
+ throw new CliError(
216
+ `nxds ${catalog.version} has no item named "${name}". Run \`nxds search ${name}\` to find the right name, or \`nxds update\` if it's newer.`
217
+ );
218
+ }
219
+ function variantOf(item, framework) {
220
+ const variant = item.variants[framework];
221
+ if (variant) return variant;
222
+ throw new CliError(`${item.title} is not available for ${framework} yet.`);
223
+ }
224
+ function setupVariant(catalog, framework) {
225
+ const setup = catalog.items.find((item) => item.name === SETUP);
226
+ if (setup?.kind !== "setup") {
227
+ throw new Error(`nxds ${catalog.version} has no "${SETUP}" setup item.`);
228
+ }
229
+ return variantOf(setup, framework);
230
+ }
231
+ function resolveItems(catalog, framework, names) {
232
+ const ordered = [];
233
+ const seen = /* @__PURE__ */ new Set();
234
+ function visit(name) {
235
+ if (seen.has(name)) return;
236
+ seen.add(name);
237
+ const item = findItem(catalog, name);
238
+ variantOf(item, framework).items.forEach(visit);
239
+ ordered.push(item);
240
+ }
241
+ names.forEach(visit);
242
+ return ordered;
243
+ }
244
+ function releaseFiles(catalog, framework) {
245
+ return new Map(
246
+ catalog.items.flatMap(
247
+ (item) => (item.variants[framework]?.files ?? []).map(
248
+ (file) => [file.path, file.content]
249
+ )
250
+ )
251
+ );
252
+ }
253
+ function splitInstalled(catalog, installed) {
254
+ const names = new Set(catalog.items.map((item) => item.name));
255
+ return {
256
+ known: installed.filter((name) => names.has(name)),
257
+ removed: installed.filter((name) => !names.has(name))
258
+ };
259
+ }
260
+
261
+ // src/project.ts
262
+ import { existsSync as existsSync4 } from "fs";
263
+ import path4 from "path";
264
+ import { major, minVersion as minVersion2, validRange as validRange3 } from "semver";
265
+
266
+ // src/packages.ts
267
+ import { spawnSync } from "child_process";
268
+ import { existsSync as existsSync3 } from "fs";
269
+ import path3 from "path";
270
+ import { minVersion, satisfies, validRange as validRange2 } from "semver";
271
+ var ADD_COMMAND = {
272
+ npm: ["install"],
273
+ pnpm: ["add"],
274
+ yarn: ["add"],
275
+ bun: ["add"]
276
+ };
277
+ function declaresRange(project, name, range) {
278
+ const declared = project.dependencies[name];
279
+ if (declared === void 0) return false;
280
+ if (validRange2(declared) === null) return true;
281
+ const lowest = minVersion(declared);
282
+ return lowest !== null && satisfies(lowest, range);
283
+ }
284
+ function packagesToInstall(project, packages) {
285
+ return Object.entries(packages).filter(([name, range]) => !declaresRange(project, name, range)).map(([name, range]) => `${name}@${range}`);
286
+ }
287
+ function installedVersion(project, name) {
288
+ let dir = project.root;
289
+ for (; ; ) {
290
+ const manifest = path3.join(dir, "node_modules", name, "package.json");
291
+ if (existsSync3(manifest)) {
292
+ return readJson(manifest).version ?? null;
293
+ }
294
+ const parent = path3.dirname(dir);
295
+ if (parent === dir) return null;
296
+ dir = parent;
297
+ }
298
+ }
299
+ function installPackages(project, specs, { dev }) {
300
+ if (specs.length === 0) return;
301
+ const args = [
302
+ ...ADD_COMMAND[project.packageManager],
303
+ ...dev ? ["-D"] : [],
304
+ ...specs
305
+ ];
306
+ const options = {
307
+ cwd: project.root,
308
+ stdio: ["ignore", 2, 2]
309
+ };
310
+ const run2 = process.platform === "win32" ? spawnSync(
311
+ [project.packageManager, ...args.map((arg) => `"${arg}"`)].join(" "),
312
+ { ...options, shell: true }
313
+ ) : spawnSync(project.packageManager, args, options);
314
+ if (run2.status !== 0) {
315
+ throw new CliError(
316
+ `\`${project.packageManager} ${args.join(" ")}\` failed. Fix the error above, then run the same nxds command again.`
317
+ );
318
+ }
319
+ for (const spec of specs) {
320
+ const name = spec.slice(0, spec.lastIndexOf("@"));
321
+ project.dependencies[name] = spec.slice(name.length + 1);
322
+ }
323
+ }
324
+
325
+ // src/project.ts
326
+ var LOCKFILES = [
327
+ ["pnpm-lock.yaml", "pnpm"],
328
+ ["yarn.lock", "yarn"],
329
+ ["bun.lock", "bun"],
330
+ ["bun.lockb", "bun"],
331
+ ["package-lock.json", "npm"]
332
+ ];
333
+ var APP_DIRS = ["app", "src/app"];
334
+ var VITE_ENTRIES = ["src/main.tsx", "src/main.jsx"];
335
+ var VITE_CONFIGS = [
336
+ "vite.config.ts",
337
+ "vite.config.mts",
338
+ "vite.config.js"
339
+ ];
340
+ function firstExisting(root, candidates) {
341
+ return candidates.find((file) => existsSync4(path4.join(root, file)));
342
+ }
343
+ function detectSetup(root, dependencies) {
344
+ if ("next" in dependencies && firstExisting(
345
+ root,
346
+ APP_DIRS.map((dir) => `${dir}/layout.tsx`)
347
+ )) {
348
+ return "next-app";
349
+ }
350
+ if ("vite" in dependencies && "react" in dependencies && firstExisting(root, VITE_ENTRIES)) {
351
+ return "vite-react";
352
+ }
353
+ return null;
354
+ }
355
+ function readProject(root) {
356
+ const manifestPath = path4.join(root, "package.json");
357
+ if (!existsSync4(manifestPath)) {
358
+ throw new CliError(
359
+ `No package.json in ${root}. Run nxds from your app's root folder, or pass --cwd.`
360
+ );
361
+ }
362
+ const manifest = readJson(manifestPath);
363
+ const dependencies = {
364
+ ...manifest.devDependencies,
365
+ ...manifest.dependencies
366
+ };
367
+ return {
368
+ root,
369
+ setup: detectSetup(root, dependencies),
370
+ packageManager: LOCKFILES.find(([file]) => existsSync4(path4.join(root, file)))?.[1] ?? "npm",
371
+ usesSrc: existsSync4(path4.join(root, "src")),
372
+ typescript: existsSync4(path4.join(root, "tsconfig.json")),
373
+ dependencies
374
+ };
375
+ }
376
+ function findFile(project, candidates) {
377
+ return firstExisting(project.root, candidates);
378
+ }
379
+ function majorVersion(project, name) {
380
+ const declared = project.dependencies[name];
381
+ if (declared === void 0) return null;
382
+ if (validRange3(declared) !== null) return minVersion2(declared)?.major ?? null;
383
+ const installed = installedVersion(project, name);
384
+ return installed === null ? null : major(installed);
385
+ }
386
+
387
+ // src/commands/browse.ts
388
+ var KINDS2 = ["component", "block"];
389
+ function appRelease(cwd, registry) {
390
+ return loadRelease(registry, readConfig(cwd)?.version ?? "latest");
391
+ }
392
+ function summary(item) {
393
+ return {
394
+ name: item.name,
395
+ kind: item.kind,
396
+ title: item.title,
397
+ description: item.description
398
+ };
399
+ }
400
+ function firstSentence(text2) {
401
+ return text2.split(/(?<=\.)\s/)[0]?.replaceAll("`", "") ?? text2;
402
+ }
403
+ function summaryLines(items) {
404
+ const width = Math.max(...items.map((item) => item.name.length));
405
+ return items.map(
406
+ (item) => `${item.name.padEnd(width)} ${firstSentence(item.description)}`
407
+ );
408
+ }
409
+ async function list(cwd, registry, kind) {
410
+ if (kind !== void 0 && !KINDS2.includes(kind)) {
411
+ throw new CliError(`--kind takes ${KINDS2.join(" or ")}.`);
412
+ }
413
+ const items = (await appRelease(cwd, registry)).items.filter(
414
+ (item) => item.name !== SETUP && (kind === void 0 || item.kind === kind)
415
+ );
416
+ return { data: items.map(summary), lines: summaryLines(items), ok: true };
417
+ }
418
+ async function search(cwd, registry, query) {
419
+ if (!query)
420
+ throw new CliError("Give a search term, such as `nxds search date`.");
421
+ const needle = query.toLowerCase();
422
+ const rank = (item) => {
423
+ if (item.name.includes(needle)) return 0;
424
+ if (item.title.toLowerCase().includes(needle)) return 1;
425
+ if (item.description.toLowerCase().includes(needle)) return 2;
426
+ return void 0;
427
+ };
428
+ const items = (await appRelease(cwd, registry)).items.filter((item) => item.name !== SETUP).flatMap((item) => {
429
+ const score = rank(item);
430
+ return score === void 0 ? [] : [{ item, score }];
431
+ }).sort((a, b) => a.score - b.score).map(({ item }) => item);
432
+ return {
433
+ data: items.map(summary),
434
+ lines: items.length > 0 ? summaryLines(items) : [`Nothing matches "${query}".`],
435
+ ok: true
436
+ };
437
+ }
438
+ function projectFramework(cwd, catalog) {
439
+ const configured = readConfig(cwd)?.framework;
440
+ if (configured) return configured;
441
+ const [only, ...others] = catalog.frameworks;
442
+ if (only && others.length === 0) return only;
443
+ throw new CliError(
444
+ "This release has several frameworks. Run `nxds init` first, so nxds knows which one your app uses."
445
+ );
446
+ }
447
+ async function view(cwd, registry, name) {
448
+ if (!name) throw new CliError("Name an item, such as `nxds view button`.");
449
+ const catalog = await appRelease(cwd, registry);
450
+ const item = findItem(catalog, name);
451
+ const variant = variantOf(item, projectFramework(cwd, catalog));
452
+ const packages = Object.entries(variant.packages.npm).map(
453
+ ([pkg, range]) => `${pkg}@${range}`
454
+ );
455
+ const needs = variant.items.filter((other) => other !== SETUP);
456
+ return {
457
+ data: {
458
+ ...summary(item),
459
+ docs: item.docs,
460
+ items: variant.items,
461
+ packages,
462
+ files: variant.files.map((file) => file.path)
463
+ },
464
+ lines: [
465
+ `${item.title} (${item.kind})`,
466
+ item.description,
467
+ `Docs: ${item.docs}`,
468
+ ...needs.length > 0 ? [`Also installs: ${needs.join(", ")}`] : [],
469
+ ...packages.length > 0 ? [`Packages: ${packages.join(" ")}`] : [],
470
+ "Files:",
471
+ ...variant.files.map((file) => ` ${file.path}`)
472
+ ],
473
+ ok: true
474
+ };
475
+ }
476
+ function info(cwd) {
477
+ const project = readProject(cwd);
478
+ const config = readConfig(cwd);
479
+ const setupLabel = {
480
+ "next-app": "Next.js App Router",
481
+ "vite-react": "Vite + React"
482
+ };
483
+ return {
484
+ data: {
485
+ setup: project.setup,
486
+ packageManager: project.packageManager,
487
+ typescript: project.typescript,
488
+ tailwind: project.dependencies.tailwindcss ?? null,
489
+ initialized: config !== null,
490
+ version: config?.version ?? null,
491
+ dir: config?.dir ?? null,
492
+ css: config?.css ?? null,
493
+ items: config?.items ?? []
494
+ },
495
+ lines: [
496
+ `App: ${project.setup ? setupLabel[project.setup] : "not supported by init (see https://nxds.dev/getting-started/install)"}`,
497
+ `Package manager: ${project.packageManager}`,
498
+ `TypeScript: ${project.typescript ? "yes" : "no"}`,
499
+ `Tailwind: ${project.dependencies.tailwindcss ?? "not installed"}`,
500
+ config ? `nxds: ${config.version} in ${config.dir}, CSS entry ${config.css}` : "nxds: not set up. Run `nxds init`.",
501
+ ...config && config.items.length > 0 ? [
502
+ `Installed: ${config.items.filter((item) => item !== SETUP).join(", ") || "setup only"}`
503
+ ] : []
504
+ ],
505
+ ok: true
506
+ };
507
+ }
508
+
509
+ // src/commands/check.ts
510
+ import { spawnSync as spawnSync2 } from "child_process";
511
+ import { existsSync as existsSync7, readFileSync as readFileSync4 } from "fs";
512
+ import path7 from "path";
513
+ import { satisfies as satisfies2 } from "semver";
514
+
515
+ // src/files.ts
516
+ import { existsSync as existsSync5, mkdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
517
+ import path5 from "path";
518
+ var NO_PREVIOUS = /* @__PURE__ */ new Map();
519
+ function fileState(root, config, file, previous) {
520
+ const target = path5.join(root, config.dir, file.path);
521
+ if (!existsSync5(target)) return "missing";
522
+ const onDisk = readFileSync2(target, "utf8");
523
+ if (onDisk === file.content) return "current";
524
+ return previous.get(file.path) === onDisk ? "outdated" : "changed";
525
+ }
526
+ function writeFiles(root, config, files, previous, { overwrite, dryRun }) {
527
+ const result = { written: [], unchanged: [], skipped: [] };
528
+ for (const file of files) {
529
+ const state = fileState(root, config, file, previous);
530
+ if (state === "current") {
531
+ result.unchanged.push(file.path);
532
+ continue;
533
+ }
534
+ if (state === "changed" && !overwrite) {
535
+ result.skipped.push(file.path);
536
+ continue;
537
+ }
538
+ result.written.push(file.path);
539
+ if (dryRun) continue;
540
+ const target = path5.join(root, config.dir, file.path);
541
+ mkdirSync(path5.dirname(target), { recursive: true });
542
+ writeFileSync2(target, file.content);
543
+ }
544
+ return result;
545
+ }
546
+ function relativeImport(fromFile, toFile) {
547
+ const relative = path5.relative(path5.dirname(fromFile), toFile).split(path5.sep).join("/");
548
+ return relative.startsWith(".") ? relative : `./${relative}`;
549
+ }
550
+
551
+ // src/setup/react.ts
552
+ import {
553
+ isArrayExpression,
554
+ isCallExpression,
555
+ isExportDefaultDeclaration,
556
+ isIdentifier,
557
+ isJSXElement as isJSXElement2,
558
+ isMemberExpression,
559
+ isObjectExpression,
560
+ isObjectProperty,
561
+ isSpreadElement
562
+ } from "@babel/types";
563
+ import { existsSync as existsSync6, readFileSync as readFileSync3 } from "fs";
564
+ import path6 from "path";
565
+
566
+ // src/setup/source-edit.ts
567
+ import { parse } from "@babel/parser";
568
+ import {
569
+ isImportDeclaration,
570
+ isJSXAttribute,
571
+ isJSXElement,
572
+ isJSXIdentifier,
573
+ traverseFast
574
+ } from "@babel/types";
575
+ function parseModule(source, file) {
576
+ try {
577
+ return parse(source, {
578
+ sourceType: "module",
579
+ plugins: ["typescript", "jsx"]
580
+ }).program;
581
+ } catch (error) {
582
+ throw new CliError(
583
+ `Could not parse ${file}: ${error.message}. Fix the syntax error, then run the command again.`
584
+ );
585
+ }
586
+ }
587
+ function startOf(node) {
588
+ return node.start;
589
+ }
590
+ function endOf(node) {
591
+ return node.end;
592
+ }
593
+ function findNode(root, matches) {
594
+ let found;
595
+ traverseFast(root, (node) => {
596
+ if (!matches(node)) return void 0;
597
+ found = node;
598
+ return traverseFast.stop;
599
+ });
600
+ return found;
601
+ }
602
+ function jsxName(element) {
603
+ const { name } = element.openingElement;
604
+ return isJSXIdentifier(name) ? name.name : void 0;
605
+ }
606
+ function findJsx(root, name) {
607
+ return findNode(
608
+ root,
609
+ (node) => isJSXElement(node) && jsxName(node) === name
610
+ );
611
+ }
612
+ function applyInsertions(source, insertions) {
613
+ return [...insertions].sort((a, b) => b.at - a.at).reduce(
614
+ (text2, { at, text: inserted }) => text2.slice(0, at) + inserted + text2.slice(at),
615
+ source
616
+ );
617
+ }
618
+ function importInsertion(source, program, lines) {
619
+ const imports = program.body.filter((node) => isImportDeclaration(node));
620
+ const first = imports[0];
621
+ const quote = first && source[startOf(first.source)];
622
+ const semicolons = !first || source[endOf(first) - 1] === ";";
623
+ const text2 = lines.map((line) => quote === '"' ? line.replaceAll("'", '"') : line).map((line) => semicolons ? line : line.replace(/;$/, "")).join("\n");
624
+ const lastImport = imports.at(-1);
625
+ if (lastImport) return { at: endOf(lastImport), text: `
626
+ ${text2}` };
627
+ const lastDirective = program.directives.at(-1);
628
+ if (lastDirective) return { at: endOf(lastDirective), text: `
629
+
630
+ ${text2}` };
631
+ return { at: 0, text: `${text2}
632
+
633
+ ` };
634
+ }
635
+ function lineIndent(source, offset) {
636
+ const lineStart = source.lastIndexOf("\n", offset - 1) + 1;
637
+ return /^[ \t]*/.exec(source.slice(lineStart))?.[0] ?? "";
638
+ }
639
+ function beforeWhitespace(source, offset) {
640
+ let at = offset;
641
+ while (at > 0 && /\s/.test(source.charAt(at - 1))) at -= 1;
642
+ return at;
643
+ }
644
+ function openingTagEnd(element) {
645
+ return endOf(element.openingElement);
646
+ }
647
+ function hasAttribute(element, name) {
648
+ return element.openingElement.attributes.some(
649
+ (attribute) => isJSXAttribute(attribute) && isJSXIdentifier(attribute.name) && attribute.name.name === name
650
+ );
651
+ }
652
+ function attributeInsertion(element, name) {
653
+ const opening = element.openingElement;
654
+ const tagEnd = endOf(opening) - (opening.selfClosing ? 2 : 1);
655
+ return { at: tagEnd, text: ` ${name}` };
656
+ }
657
+
658
+ // src/setup/react.ts
659
+ var STORAGE_KEY = "nxds-appearance";
660
+ var PROVIDER = "components/appearance/provider";
661
+ var TAILWIND_PLUGIN = {
662
+ "next-app": "@tailwindcss/postcss",
663
+ "vite-react": "@tailwindcss/vite"
664
+ };
665
+ function tailwindPackages(setup, range) {
666
+ return { tailwindcss: range, [TAILWIND_PLUGIN[setup]]: range };
667
+ }
668
+ function appDir(project) {
669
+ const dir = APP_DIRS.find(
670
+ (candidate) => existsSync6(path6.join(project.root, candidate, "layout.tsx"))
671
+ );
672
+ if (!dir)
673
+ throw new CliError("No app/layout.tsx or src/app/layout.tsx found.");
674
+ return dir;
675
+ }
676
+ function entryDir(project) {
677
+ return project.setup === "next-app" ? appDir(project) : "src";
678
+ }
679
+ function defaultDir(project) {
680
+ return project.usesSrc ? "src/components/nxds" : "components/nxds";
681
+ }
682
+ function cssEntryPath(project) {
683
+ return `${entryDir(project)}/nxds.css`;
684
+ }
685
+ function read(project, file) {
686
+ return readFileSync3(path6.join(project.root, file), "utf8");
687
+ }
688
+ function cssEntry(config, withPreflight) {
689
+ const tokens = relativeImport(config.css, `${config.dir}/tailwind/nxds.css`);
690
+ return [
691
+ ...withPreflight ? [
692
+ "@layer theme, base, components, utilities;",
693
+ "@import 'tailwindcss/preflight.css' layer(base);"
694
+ ] : [],
695
+ `@import '${tokens}';`,
696
+ "@import 'tw-animate-css';",
697
+ ""
698
+ ].join("\n");
699
+ }
700
+ function cssImportLine(config, file) {
701
+ return `@import '${relativeImport(config.css, `${config.dir}/${file}`)}';`;
702
+ }
703
+ var APPEARANCE_STATE = `import {
704
+ DEFAULT_NXDS_APPEARANCE,
705
+ type NxdsAppearanceState,
706
+ } from '@nxds/core';
707
+
708
+ export const defaultAppearance = {
709
+ ...DEFAULT_NXDS_APPEARANCE,
710
+ } satisfies NxdsAppearanceState;
711
+ `;
712
+ function providerImport(fromFile, config, file = "") {
713
+ return relativeImport(fromFile, `${config.dir}/${PROVIDER}${file}`);
714
+ }
715
+ function editedFile(file, content) {
716
+ return { path: file, content, created: false };
717
+ }
718
+ var NO_CHANGES = { changes: [], manual: [] };
719
+ var PROVIDER_PROPS = `storageKey="${STORAGE_KEY}" defaultState={defaultAppearance}`;
720
+ var SCRIPT_ELEMENT = `<NxdsAppearanceScript ${PROVIDER_PROPS} />`;
721
+ function wireNextLayout(project, config, steps) {
722
+ const layout = `${appDir(project)}/layout.tsx`;
723
+ const source = read(project, layout);
724
+ if (/NxdsAppearance(Provider|Script)/.test(source)) return NO_CHANGES;
725
+ const program = parseModule(source, layout);
726
+ const html = findJsx(program, "html");
727
+ const body = html && findJsx(html, "body");
728
+ const head = html && findJsx(html, "head");
729
+ if (!html || !body?.closingElement || head && !head.closingElement) {
730
+ return {
731
+ changes: [],
732
+ manual: [
733
+ {
734
+ file: layout,
735
+ step: `Add ${SCRIPT_ELEMENT} inside <head>, and wrap the content of <body> in <NxdsAppearanceProvider ${PROVIDER_PROPS}>.`
736
+ }
737
+ ]
738
+ };
739
+ }
740
+ const step = " ";
741
+ const imports = [];
742
+ const insertions = [];
743
+ if (steps.has("add-css-import")) imports.push("import './nxds.css';");
744
+ if (steps.has("wrap-root-with-provider")) {
745
+ const bodyIndent = lineIndent(source, startOf(body));
746
+ imports.push(
747
+ `import { NxdsAppearanceProvider } from '${providerImport(layout, config)}';`
748
+ );
749
+ insertions.push(
750
+ {
751
+ at: openingTagEnd(body),
752
+ text: `
753
+ ${bodyIndent}${step}<NxdsAppearanceProvider ${PROVIDER_PROPS}>`
754
+ },
755
+ {
756
+ at: beforeWhitespace(source, startOf(body.closingElement)),
757
+ text: `
758
+ ${bodyIndent}${step}</NxdsAppearanceProvider>`
759
+ }
760
+ );
761
+ }
762
+ if (steps.has("add-head-script")) {
763
+ const htmlIndent = lineIndent(source, startOf(html));
764
+ imports.push(
765
+ `import { NxdsAppearanceScript } from '${providerImport(layout, config, "/server")}';`
766
+ );
767
+ insertions.push(
768
+ head ? {
769
+ at: openingTagEnd(head),
770
+ text: `
771
+ ${lineIndent(source, startOf(head))}${step}${SCRIPT_ELEMENT}`
772
+ } : {
773
+ at: openingTagEnd(html),
774
+ text: `
775
+ ${htmlIndent}${step}<head>
776
+ ${htmlIndent}${step}${step}${SCRIPT_ELEMENT}
777
+ ${htmlIndent}${step}</head>`
778
+ }
779
+ );
780
+ if (!hasAttribute(html, "suppressHydrationWarning")) {
781
+ insertions.push(attributeInsertion(html, "suppressHydrationWarning"));
782
+ }
783
+ }
784
+ if (insertions.length > 0) {
785
+ imports.push("import { defaultAppearance } from './appearance-state';");
786
+ }
787
+ if (imports.length === 0) return NO_CHANGES;
788
+ insertions.push(importInsertion(source, program, imports));
789
+ return {
790
+ changes: [editedFile(layout, applyInsertions(source, insertions))],
791
+ manual: []
792
+ };
793
+ }
794
+ function renderedElement(program) {
795
+ const call = findNode(
796
+ program,
797
+ (node) => isCallExpression(node) && isMemberExpression(node.callee) && isIdentifier(node.callee.property, { name: "render" }) && isJSXElement2(node.arguments[0])
798
+ );
799
+ return call?.arguments[0];
800
+ }
801
+ function wireViteEntry(project, config, steps) {
802
+ const entry = findFile(project, VITE_ENTRIES);
803
+ if (!entry) throw new CliError("No src/main.tsx found.");
804
+ const source = read(project, entry);
805
+ if (source.includes("NxdsAppearanceProvider")) return NO_CHANGES;
806
+ const program = parseModule(source, entry);
807
+ const imports = [];
808
+ const insertions = [];
809
+ if (steps.has("add-css-import")) imports.push("import './nxds.css';");
810
+ if (steps.has("wrap-root-with-provider")) {
811
+ const app = renderedElement(program);
812
+ if (!app) {
813
+ return {
814
+ changes: [],
815
+ manual: [
816
+ {
817
+ file: entry,
818
+ step: `Wrap the element you render in <NxdsAppearanceProvider ${PROVIDER_PROPS}>.`
819
+ }
820
+ ]
821
+ };
822
+ }
823
+ const indent = lineIndent(source, startOf(app));
824
+ imports.push(
825
+ `import { NxdsAppearanceProvider } from '${providerImport(entry, config)}';`,
826
+ "import { defaultAppearance } from './appearance-state';"
827
+ );
828
+ insertions.push(
829
+ {
830
+ at: startOf(app),
831
+ text: `<NxdsAppearanceProvider ${PROVIDER_PROPS}>
832
+ ${indent}`
833
+ },
834
+ { at: endOf(app), text: `
835
+ ${indent}</NxdsAppearanceProvider>` }
836
+ );
837
+ }
838
+ if (imports.length === 0) return NO_CHANGES;
839
+ insertions.push(importInsertion(source, program, imports));
840
+ return {
841
+ changes: [editedFile(entry, applyInsertions(source, insertions))],
842
+ manual: []
843
+ };
844
+ }
845
+ var VITE_PLUGIN = `import {
846
+ createNxdsAppearanceBootstrapScript,
847
+ createNxdsAppearanceSnapshotFromState,
848
+ } from '@nxds/core';
849
+ import type { Plugin } from 'vite';
850
+
851
+ import { defaultAppearance } from './src/appearance-state';
852
+
853
+ /** Adds the nxds theme script to index.html, so the page doesn't flash the wrong theme. */
854
+ export function nxdsAppearance(): Plugin {
855
+ const script = createNxdsAppearanceBootstrapScript({
856
+ storageKey: '${STORAGE_KEY}',
857
+ defaultSnapshot: createNxdsAppearanceSnapshotFromState(defaultAppearance),
858
+ });
859
+ return {
860
+ name: 'nxds-appearance',
861
+ transformIndexHtml: (html) =>
862
+ html.replace('</head>', \`<script>\${script}</script></head>\`),
863
+ };
864
+ }
865
+ `;
866
+ function configObject(program) {
867
+ const declaration = program.body.find(
868
+ (node) => isExportDefaultDeclaration(node)
869
+ )?.declaration;
870
+ if (isObjectExpression(declaration)) return declaration;
871
+ if (!isCallExpression(declaration)) return void 0;
872
+ const [argument] = declaration.arguments;
873
+ return isObjectExpression(argument) ? argument : void 0;
874
+ }
875
+ function hasProperty(object, name) {
876
+ return object.properties.some(
877
+ (property) => !isSpreadElement(property) && isIdentifier(property.key, { name })
878
+ );
879
+ }
880
+ function pluginsArray(object) {
881
+ const property = object.properties.find(
882
+ (candidate) => isObjectProperty(candidate) && isIdentifier(candidate.key, { name: "plugins" })
883
+ );
884
+ return isObjectProperty(property) && isArrayExpression(property.value) ? property.value : void 0;
885
+ }
886
+ var TAILWIND_VITE_PLUGIN = {
887
+ call: "tailwindcss()",
888
+ importLine: "import tailwindcss from '@tailwindcss/vite';"
889
+ };
890
+ var NXDS_VITE_PLUGIN = {
891
+ call: "nxdsAppearance()",
892
+ importLine: "import { nxdsAppearance } from './nxds.vite';"
893
+ };
894
+ var VITE_ALIAS = "{ '@': '/src' }";
895
+ function wireViteConfig(project, addTailwind, steps) {
896
+ const configFile = findFile(project, VITE_CONFIGS) ?? "vite.config.ts";
897
+ const addScript = steps.has("add-head-script") && !existsSync6(path6.join(project.root, "nxds.vite.ts"));
898
+ const changes = addScript ? [{ path: "nxds.vite.ts", content: VITE_PLUGIN, created: true }] : [];
899
+ const source = existsSync6(path6.join(project.root, configFile)) ? read(project, configFile) : "";
900
+ const program = source ? parseModule(source, configFile) : void 0;
901
+ const object = program && configObject(program);
902
+ const plugins = object && pluginsArray(object);
903
+ const newPlugins = [
904
+ ...addTailwind ? [TAILWIND_VITE_PLUGIN] : [],
905
+ ...steps.has("add-head-script") && !source.includes("nxdsAppearance") ? [NXDS_VITE_PLUGIN] : []
906
+ ];
907
+ const needsAlias = !/['"]@['"]/.test(source);
908
+ const manual = [];
909
+ const insertions = [];
910
+ const imports = [];
911
+ if (newPlugins.length > 0 && plugins) {
912
+ const calls = newPlugins.map((plugin) => plugin.call).join(", ");
913
+ const last = plugins.elements.at(-1);
914
+ insertions.push(
915
+ last ? { at: endOf(last), text: `, ${calls}` } : { at: startOf(plugins) + 1, text: calls }
916
+ );
917
+ imports.push(...newPlugins.map((plugin) => plugin.importLine));
918
+ } else if (newPlugins.length > 0) {
919
+ manual.push({
920
+ file: configFile,
921
+ step: `Add ${newPlugins.map((plugin) => `${plugin.call} (${plugin.importLine})`).join(" and ")} to the plugins array.`
922
+ });
923
+ }
924
+ if (needsAlias && object && !hasProperty(object, "resolve")) {
925
+ const last = object.properties.at(-1);
926
+ const alias = `resolve: {
927
+ alias: ${VITE_ALIAS},
928
+ }`;
929
+ insertions.push(
930
+ last ? { at: endOf(last), text: `,
931
+ ${alias}` } : { at: startOf(object) + 1, text: `
932
+ ${alias},
933
+ ` }
934
+ );
935
+ } else if (needsAlias) {
936
+ manual.push({
937
+ file: configFile,
938
+ step: `Add resolve.alias ${VITE_ALIAS}, so imports like @/components/nxds/... resolve.`
939
+ });
940
+ }
941
+ if (program && insertions.length > 0) {
942
+ insertions.push(importInsertion(source, program, imports));
943
+ changes.push(editedFile(configFile, applyInsertions(source, insertions)));
944
+ }
945
+ return { changes, manual };
946
+ }
947
+ var ALIAS_PATTERN = /"@\/\*"/;
948
+ var PATHS_PATTERN = /"paths"\s*:/;
949
+ var COMPILER_OPTIONS = /"compilerOptions"\s*:\s*\{/;
950
+ function wireTsconfigAlias(project) {
951
+ const file = findFile(project, ["tsconfig.app.json", "tsconfig.json"]);
952
+ if (!file) throw new CliError("No tsconfig.json found.");
953
+ const source = read(project, file);
954
+ if (ALIAS_PATTERN.test(source)) return NO_CHANGES;
955
+ const target = project.usesSrc ? "./src/*" : "./*";
956
+ const match = COMPILER_OPTIONS.exec(source);
957
+ if (!match || PATHS_PATTERN.test(source)) {
958
+ return {
959
+ changes: [],
960
+ manual: [
961
+ {
962
+ file,
963
+ step: `Add "@/*": ["${target}"] to compilerOptions.paths.`
964
+ }
965
+ ]
966
+ };
967
+ }
968
+ const at = match.index + match[0].length;
969
+ return {
970
+ changes: [
971
+ editedFile(
972
+ file,
973
+ applyInsertions(source, [
974
+ { at, text: `
975
+ "paths": { "@/*": ["${target}"] },` }
976
+ ])
977
+ )
978
+ ],
979
+ manual: []
980
+ };
981
+ }
982
+ var ESLINT_CONFIGS = [
983
+ "eslint.config.js",
984
+ "eslint.config.mjs",
985
+ "eslint.config.ts"
986
+ ];
987
+ function exportedConfigs(program) {
988
+ const declaration = program.body.find(
989
+ (node) => isExportDefaultDeclaration(node)
990
+ )?.declaration;
991
+ if (isArrayExpression(declaration)) return declaration.elements;
992
+ if (!isCallExpression(declaration)) return [];
993
+ const [first] = declaration.arguments;
994
+ return isArrayExpression(first) ? first.elements : declaration.arguments;
995
+ }
996
+ function wireViteEslint(project, config) {
997
+ const file = findFile(project, ESLINT_CONFIGS);
998
+ if (!file) return NO_CHANGES;
999
+ const source = read(project, file);
1000
+ if (!source.includes("react-refresh") || source.includes(config.dir)) {
1001
+ return NO_CHANGES;
1002
+ }
1003
+ const rule = `{
1004
+ files: ['${config.dir}/**/*.{ts,tsx}'],
1005
+ rules: { 'react-refresh/only-export-components': 'off' },
1006
+ }`;
1007
+ const last = exportedConfigs(parseModule(source, file)).at(-1);
1008
+ if (!last) {
1009
+ return {
1010
+ changes: [],
1011
+ manual: [
1012
+ {
1013
+ file,
1014
+ step: `Add ${rule.replace(/\s+/g, " ")} to the exported config.`
1015
+ }
1016
+ ]
1017
+ };
1018
+ }
1019
+ return {
1020
+ changes: [
1021
+ editedFile(
1022
+ file,
1023
+ applyInsertions(source, [{ at: endOf(last), text: `,
1024
+ ${rule}` }])
1025
+ )
1026
+ ],
1027
+ manual: []
1028
+ };
1029
+ }
1030
+ var POSTCSS_CONFIG = `export default {
1031
+ plugins: { '@tailwindcss/postcss': {} },
1032
+ };
1033
+ `;
1034
+ var POSTCSS_CONFIGS = [
1035
+ "postcss.config.mjs",
1036
+ "postcss.config.js",
1037
+ "postcss.config.cjs"
1038
+ ];
1039
+ function wireNextTailwind(project) {
1040
+ const existing = findFile(project, POSTCSS_CONFIGS);
1041
+ if (!existing) {
1042
+ return {
1043
+ changes: [
1044
+ { path: "postcss.config.mjs", content: POSTCSS_CONFIG, created: true }
1045
+ ],
1046
+ manual: []
1047
+ };
1048
+ }
1049
+ return {
1050
+ changes: [],
1051
+ manual: [
1052
+ {
1053
+ file: existing,
1054
+ step: "Add '@tailwindcss/postcss': {} to the plugins."
1055
+ }
1056
+ ]
1057
+ };
1058
+ }
1059
+ function createdUnlessExists(project, file, content) {
1060
+ if (existsSync6(path6.join(project.root, file))) return NO_CHANGES;
1061
+ return { changes: [{ path: file, content, created: true }], manual: [] };
1062
+ }
1063
+ function planWiring(project, config, addTailwind, setupSteps) {
1064
+ const steps = new Set(setupSteps);
1065
+ const plans = [wireTsconfigAlias(project)];
1066
+ if (steps.has("add-css-import")) {
1067
+ plans.push(
1068
+ createdUnlessExists(project, config.css, cssEntry(config, addTailwind))
1069
+ );
1070
+ }
1071
+ if (steps.has("add-head-script") || steps.has("wrap-root-with-provider")) {
1072
+ plans.push(
1073
+ createdUnlessExists(
1074
+ project,
1075
+ `${entryDir(project)}/appearance-state.ts`,
1076
+ APPEARANCE_STATE
1077
+ )
1078
+ );
1079
+ }
1080
+ if (project.setup === "next-app") {
1081
+ plans.push(wireNextLayout(project, config, steps));
1082
+ if (addTailwind) plans.push(wireNextTailwind(project));
1083
+ } else {
1084
+ plans.push(wireViteEntry(project, config, steps));
1085
+ plans.push(wireViteConfig(project, addTailwind, steps));
1086
+ plans.push(wireViteEslint(project, config));
1087
+ }
1088
+ return {
1089
+ changes: plans.flatMap((plan) => plan.changes),
1090
+ manual: plans.flatMap((plan) => plan.manual)
1091
+ };
1092
+ }
1093
+
1094
+ // src/commands/check.ts
1095
+ async function diff(cwd, registry, names) {
1096
+ const config = requireConfig(cwd);
1097
+ const latest = await loadRelease(registry, "latest");
1098
+ const previous = await fetchRelease(registry, config.version);
1099
+ const previousFiles = previous ? releaseFiles(previous, config.framework) : NO_PREVIOUS;
1100
+ const { known, removed } = splitInstalled(latest, config.items);
1101
+ const items = resolveItems(
1102
+ latest,
1103
+ config.framework,
1104
+ names.length > 0 ? names : known
1105
+ );
1106
+ const files = items.flatMap(
1107
+ (item) => variantOf(item, config.framework).files.map((file) => ({
1108
+ item: item.name,
1109
+ path: file.path,
1110
+ state: fileState(cwd, config, file, previousFiles)
1111
+ }))
1112
+ );
1113
+ const differing = files.filter((file) => file.state !== "current");
1114
+ const advice = {
1115
+ outdated: "`nxds update` replaces it",
1116
+ changed: "differs from nxds's copy; `nxds update --overwrite` replaces it",
1117
+ missing: "`nxds update` restores it"
1118
+ };
1119
+ return {
1120
+ data: { version: config.version, latest: latest.version, files, removed },
1121
+ lines: [
1122
+ ...differing.map(
1123
+ (file) => `${file.state.padEnd(8)} ${file.path} (${advice[file.state]})`
1124
+ ),
1125
+ ...removed.map(
1126
+ (name) => `removed ${name} (no longer in nxds; \`nxds update\` drops it)`
1127
+ ),
1128
+ ...differing.length === 0 && removed.length === 0 ? [`Every file matches nxds ${latest.version}.`] : []
1129
+ ],
1130
+ ok: true
1131
+ };
1132
+ }
1133
+ function tailwindMessage(project) {
1134
+ const declared = project.dependencies.tailwindcss;
1135
+ if (declared === void 0) {
1136
+ return "Tailwind 4 is not installed. Run `nxds init`.";
1137
+ }
1138
+ const tailwindMajor = majorVersion(project, "tailwindcss");
1139
+ if (tailwindMajor === 4) return `tailwindcss ${declared}`;
1140
+ if (tailwindMajor === null) {
1141
+ return `tailwindcss ${declared} names no version, and none is installed. Install your dependencies, then run doctor again.`;
1142
+ }
1143
+ return `tailwindcss ${declared}. nxds needs Tailwind CSS 4.`;
1144
+ }
1145
+ function read2(root, file) {
1146
+ if (!file || !existsSync7(path7.join(root, file))) return "";
1147
+ return readFileSync4(path7.join(root, file), "utf8");
1148
+ }
1149
+ function sourcesNxdsDir(config, css) {
1150
+ const entryDir2 = path7.posix.dirname(config.css);
1151
+ return [...css.matchAll(/@source\s+(['"])(.*?)\1/g)].some(([, , source]) => {
1152
+ const base = (source ?? "").split(/[*{[]/)[0] ?? "";
1153
+ const folder = path7.posix.join(entryDir2, base).replace(/\/+$/, "");
1154
+ return folder === "." || /^\.\.(\/\.\.)*$/.test(folder) || config.dir === folder || config.dir.startsWith(`${folder}/`);
1155
+ });
1156
+ }
1157
+ function unscannedReason(cwd, config, css) {
1158
+ if (/source\(\s*none\s*\)/.test(css) && !sourcesNxdsDir(config, css)) {
1159
+ return `${config.css} turns off Tailwind's source detection with source(none), and no @source covers ${config.dir}.`;
1160
+ }
1161
+ const ignored = spawnSync2("git", ["check-ignore", "-q", config.dir], { cwd });
1162
+ if (ignored.status === 0) {
1163
+ return `${config.dir} is git-ignored, so Tailwind skips it. Commit the nxds folder.`;
1164
+ }
1165
+ return null;
1166
+ }
1167
+ function packageProblems(project, config, items) {
1168
+ const needed = Object.assign(
1169
+ {},
1170
+ ...items.map((item) => variantOf(item, config.framework).packages.npm)
1171
+ );
1172
+ return Object.entries(needed).flatMap(([name, range]) => {
1173
+ if (!(name in project.dependencies))
1174
+ return [`${name} is not in package.json`];
1175
+ const version = installedVersion(project, name);
1176
+ if (version === null) return [`${name} is not installed`];
1177
+ if (!satisfies2(version, range)) {
1178
+ return [
1179
+ `${name} ${version} is installed, and this release needs ${range}`
1180
+ ];
1181
+ }
1182
+ return [];
1183
+ });
1184
+ }
1185
+ async function doctor(cwd, registry) {
1186
+ const project = readProject(cwd);
1187
+ const config = readConfig(cwd);
1188
+ const checks = [];
1189
+ const check = (name, ok, message) => checks.push({ name, ok, message });
1190
+ check(
1191
+ "config",
1192
+ config !== null,
1193
+ config ? "nxds.json found" : "No nxds.json. Run `nxds init`."
1194
+ );
1195
+ if (!project.setup) {
1196
+ check("app", false, "Not a Next.js App Router or Vite + React app.");
1197
+ }
1198
+ if (!config || !project.setup) return report(checks);
1199
+ const catalog = await fetchRelease(registry, config.version);
1200
+ check(
1201
+ "release",
1202
+ catalog !== null,
1203
+ catalog ? `Files come from nxds ${config.version}` : `${registry} has no nxds ${config.version}. Run \`nxds update\`.`
1204
+ );
1205
+ check(
1206
+ "tailwind",
1207
+ majorVersion(project, "tailwindcss") === 4,
1208
+ tailwindMessage(project)
1209
+ );
1210
+ const css = read2(cwd, config.css);
1211
+ const importsTokens = css.includes("tailwind/nxds.css");
1212
+ check(
1213
+ "css entry",
1214
+ importsTokens,
1215
+ !css ? `${config.css} is missing. Run \`nxds init\`.` : importsTokens ? `${config.css} imports the nxds tokens` : `${config.css} does not import ${config.dir}/tailwind/nxds.css. Add the @import back.`
1216
+ );
1217
+ const unscanned = unscannedReason(cwd, config, css);
1218
+ check(
1219
+ "classes",
1220
+ unscanned === null,
1221
+ unscanned ?? `Tailwind scans ${config.dir}, so its nx: classes are generated`
1222
+ );
1223
+ const entry = project.setup === "next-app" ? `${entryDir(project)}/layout.tsx` : findFile(project, VITE_ENTRIES);
1224
+ const entrySource = read2(cwd, entry);
1225
+ const cssFile = path7.basename(config.css);
1226
+ const importsCss = entrySource.includes(cssFile);
1227
+ check(
1228
+ "css import",
1229
+ importsCss,
1230
+ `${entry} ${importsCss ? "imports" : "does not import"} ${cssFile}`
1231
+ );
1232
+ check(
1233
+ "provider",
1234
+ entrySource.includes("NxdsAppearanceProvider"),
1235
+ `${entry} ${entrySource.includes("NxdsAppearanceProvider") ? "renders" : "does not render"} NxdsAppearanceProvider`
1236
+ );
1237
+ const scriptSource = project.setup === "next-app" ? entrySource : read2(cwd, findFile(project, VITE_CONFIGS));
1238
+ const scriptMarker = project.setup === "next-app" ? "NxdsAppearanceScript" : "nxdsAppearance";
1239
+ check(
1240
+ "theme script",
1241
+ scriptSource.includes(scriptMarker),
1242
+ scriptSource.includes(scriptMarker) ? "The theme script runs before first paint" : "The theme script is missing, so the page can flash the wrong theme. Run `nxds init`."
1243
+ );
1244
+ if (!catalog) return report(checks);
1245
+ const { known, removed } = splitInstalled(catalog, config.items);
1246
+ check(
1247
+ "items",
1248
+ removed.length === 0,
1249
+ removed.length === 0 ? `${known.length} installed items, all in this release` : `No longer in nxds: ${removed.join(", ")}. Run \`nxds update\`.`
1250
+ );
1251
+ const items = resolveItems(catalog, config.framework, known);
1252
+ const missingFiles = items.flatMap(
1253
+ (item) => variantOf(item, config.framework).files.filter(
1254
+ (file) => !existsSync7(path7.join(cwd, config.dir, file.path))
1255
+ ).map((file) => `${item.name}: ${file.path}`)
1256
+ );
1257
+ check(
1258
+ "files",
1259
+ missingFiles.length === 0,
1260
+ missingFiles.length === 0 ? `All files for ${known.length} installed items are present` : `Missing ${missingFiles.join(", ")}. Run \`nxds add\` for those items.`
1261
+ );
1262
+ const problems = packageProblems(project, config, items);
1263
+ check(
1264
+ "packages",
1265
+ problems.length === 0,
1266
+ problems.length === 0 ? "Every package the installed items need is installed in range, @nxds/core included" : `${problems.join("; ")}. Run \`nxds update\`, then your package manager's install.`
1267
+ );
1268
+ return report(checks);
1269
+ }
1270
+ function report(checks) {
1271
+ const ok = checks.every((item) => item.ok);
1272
+ return {
1273
+ data: { checks },
1274
+ lines: [
1275
+ ...checks.map(
1276
+ (item) => `${item.ok ? "ok " : "FAIL"} ${item.name}: ${item.message}`
1277
+ ),
1278
+ ok ? "nxds is set up correctly." : "Fix the failed checks above."
1279
+ ],
1280
+ ok
1281
+ };
1282
+ }
1283
+
1284
+ // src/commands/install.ts
1285
+ import { existsSync as existsSync9, mkdirSync as mkdirSync2, writeFileSync as writeFileSync4 } from "fs";
1286
+ import path9 from "path";
1287
+
1288
+ // src/install.ts
1289
+ import { existsSync as existsSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
1290
+ import path8 from "path";
1291
+ function addCssImports(project, config, files, dryRun) {
1292
+ if (files.length === 0) return [];
1293
+ const entry = path8.join(project.root, config.css);
1294
+ const current = existsSync8(entry) ? readFileSync5(entry, "utf8") : "";
1295
+ const added = [...new Set(files)].map((file) => cssImportLine(config, file)).filter((line) => !current.includes(line));
1296
+ if (added.length > 0 && !dryRun) {
1297
+ writeFileSync3(entry, `${current.trimEnd()}
1298
+ ${added.join("\n")}
1299
+ `);
1300
+ }
1301
+ return added;
1302
+ }
1303
+ function installItems(catalog, project, config, names, previous, options) {
1304
+ const items = resolveItems(catalog, config.framework, names);
1305
+ const variants = items.map((item) => variantOf(item, config.framework));
1306
+ const files = new Map(
1307
+ variants.flatMap((variant) => variant.files).map((file) => [file.path, file])
1308
+ );
1309
+ const written = writeFiles(
1310
+ project.root,
1311
+ config,
1312
+ [...files.values()],
1313
+ previous,
1314
+ options
1315
+ );
1316
+ const packages = packagesToInstall(
1317
+ project,
1318
+ Object.assign({}, ...variants.map((variant) => variant.packages.npm))
1319
+ );
1320
+ if (options.install && !options.dryRun) {
1321
+ installPackages(project, packages, { dev: false });
1322
+ }
1323
+ const cssImports = addCssImports(
1324
+ project,
1325
+ config,
1326
+ variants.flatMap((variant) => variant.css),
1327
+ options.dryRun
1328
+ );
1329
+ for (const item of items) {
1330
+ if (!config.items.includes(item.name)) config.items.push(item.name);
1331
+ }
1332
+ return {
1333
+ ...written,
1334
+ items: items.map((item) => item.name),
1335
+ packages,
1336
+ cssImports
1337
+ };
1338
+ }
1339
+
1340
+ // src/commands/install.ts
1341
+ function packagesVerb({ dryRun, install }) {
1342
+ if (dryRun) return "Would install";
1343
+ return install ? "Installed" : "Not installed (--no-install):";
1344
+ }
1345
+ function installLines(config, result, options) {
1346
+ const { dryRun } = options;
1347
+ const verb = dryRun ? "Would write" : "Wrote";
1348
+ return [
1349
+ ...result.written.map((file) => `${verb} ${config.dir}/${file}`),
1350
+ ...result.skipped.map(
1351
+ (file) => `Kept ${config.dir}/${file}: it differs from nxds's copy, so it may hold your edits. Pass --overwrite to replace it.`
1352
+ ),
1353
+ ...result.packages.length > 0 ? [`${packagesVerb(options)} ${result.packages.join(" ")}`] : [],
1354
+ ...result.cssImports.map(
1355
+ (line) => `${dryRun ? "Would add" : "Added"} ${line}`
1356
+ )
1357
+ ];
1358
+ }
1359
+ function nxdsDir(dir) {
1360
+ const normalized = projectPath(dir);
1361
+ if (normalized) return normalized;
1362
+ throw new CliError(
1363
+ `--dir takes a folder inside your app, such as components/nxds, not "${dir}".`
1364
+ );
1365
+ }
1366
+ function assertTailwind4(project) {
1367
+ const declared = project.dependencies.tailwindcss;
1368
+ const tailwindMajor = majorVersion(project, "tailwindcss");
1369
+ if (tailwindMajor === 4) return;
1370
+ if (tailwindMajor === null) {
1371
+ throw new CliError(
1372
+ `This app declares tailwindcss ${declared}, which names no version, and none is installed. Install your dependencies, then run init again.`
1373
+ );
1374
+ }
1375
+ throw new CliError(
1376
+ `nxds needs Tailwind CSS 4, and this app declares tailwindcss ${declared}. Upgrade it (https://tailwindcss.com/docs/upgrade-guide), then run init again.`
1377
+ );
1378
+ }
1379
+ async function init(cwd, registry, options) {
1380
+ const project = readProject(cwd);
1381
+ if (!project.setup) {
1382
+ throw new CliError(
1383
+ "nxds init supports Next.js App Router and Vite + React apps. For other setups, follow the manual steps at https://nxds.dev/getting-started/install."
1384
+ );
1385
+ }
1386
+ if (!project.typescript) {
1387
+ throw new CliError(
1388
+ "nxds components are TypeScript. Add a tsconfig.json to your app, then run init again."
1389
+ );
1390
+ }
1391
+ const addTailwind = !("tailwindcss" in project.dependencies);
1392
+ if (!addTailwind) assertTailwind4(project);
1393
+ const dir = options.dir ? nxdsDir(options.dir) : void 0;
1394
+ const existing = readConfig(cwd);
1395
+ const catalog = await loadRelease(registry, existing?.version ?? "latest");
1396
+ if (existing && dir && dir !== existing.dir) {
1397
+ throw new CliError(
1398
+ `nxds is already set up in ${existing.dir}. Run init without --dir, or move the folder and change "dir" in nxds.json.`
1399
+ );
1400
+ }
1401
+ const config = existing ?? {
1402
+ framework: "react",
1403
+ version: catalog.version,
1404
+ dir: dir ?? defaultDir(project),
1405
+ css: cssEntryPath(project),
1406
+ items: []
1407
+ };
1408
+ const setup = setupVariant(catalog, config.framework);
1409
+ const wiring = planWiring(project, config, addTailwind, setup.steps);
1410
+ const tailwind = packagesToInstall(
1411
+ project,
1412
+ addTailwind ? tailwindPackages(project.setup, setup.packages.tailwind) : {}
1413
+ );
1414
+ if (options.install && !options.dryRun) {
1415
+ installPackages(project, tailwind, { dev: true });
1416
+ }
1417
+ const installed = installItems(
1418
+ catalog,
1419
+ project,
1420
+ config,
1421
+ [SETUP],
1422
+ NO_PREVIOUS,
1423
+ options
1424
+ );
1425
+ if (!options.dryRun) {
1426
+ for (const change of wiring.changes) {
1427
+ const target = path9.join(project.root, change.path);
1428
+ mkdirSync2(path9.dirname(target), { recursive: true });
1429
+ writeFileSync4(target, change.content);
1430
+ }
1431
+ writeConfig(cwd, config);
1432
+ }
1433
+ const packages = [...tailwind, ...installed.packages];
1434
+ const [createVerb, editVerb] = options.dryRun ? ["Would create", "Would edit"] : ["Created", "Edited"];
1435
+ const done = wiring.manual.length === 0;
1436
+ return {
1437
+ data: {
1438
+ setup: project.setup,
1439
+ version: config.version,
1440
+ dir: config.dir,
1441
+ css: config.css,
1442
+ dryRun: options.dryRun,
1443
+ ...installed,
1444
+ packages,
1445
+ created: wiring.changes.filter((change) => change.created).map((change) => change.path),
1446
+ edited: wiring.changes.filter((change) => !change.created).map((change) => change.path),
1447
+ manual: wiring.manual
1448
+ },
1449
+ lines: [
1450
+ `nxds ${config.version} in ${config.dir}`,
1451
+ ...installLines(config, { ...installed, packages }, options),
1452
+ ...wiring.changes.map(
1453
+ (change) => `${change.created ? createVerb : editVerb} ${change.path}`
1454
+ ),
1455
+ ...manualLines(wiring.manual),
1456
+ ...done && !options.dryRun ? ["nxds is set up. Add a component with `nxds add button`."] : []
1457
+ ],
1458
+ ok: done
1459
+ };
1460
+ }
1461
+ function manualLines(manual) {
1462
+ if (manual.length === 0) return [];
1463
+ return [
1464
+ "Finish these steps by hand, then run `nxds doctor`:",
1465
+ ...manual.map(({ file, step }) => ` ${file}: ${step}`)
1466
+ ];
1467
+ }
1468
+ async function add(cwd, registry, names, options) {
1469
+ if (names.length === 0) {
1470
+ throw new CliError("Name at least one item, such as `nxds add button`.");
1471
+ }
1472
+ const config = requireConfig(cwd);
1473
+ const catalog = await loadRelease(registry, config.version);
1474
+ const installed = installItems(
1475
+ catalog,
1476
+ readProject(cwd),
1477
+ config,
1478
+ names,
1479
+ NO_PREVIOUS,
1480
+ options
1481
+ );
1482
+ if (!options.dryRun) writeConfig(cwd, config);
1483
+ return {
1484
+ data: { dryRun: options.dryRun, ...installed },
1485
+ lines: [
1486
+ ...installLines(config, installed, options),
1487
+ ...installed.written.length === 0 && installed.skipped.length === 0 ? [
1488
+ `${names.join(", ")} ${names.length === 1 ? "is" : "are"} already up to date.`
1489
+ ] : []
1490
+ ],
1491
+ ok: true
1492
+ };
1493
+ }
1494
+ function filesOf(catalog, framework, names) {
1495
+ return new Set(
1496
+ resolveItems(catalog, framework, names).flatMap(
1497
+ (item) => variantOf(item, framework).files.map((file) => file.path)
1498
+ )
1499
+ );
1500
+ }
1501
+ async function update(cwd, registry, names, options) {
1502
+ if (names.length > 0) {
1503
+ throw new CliError(
1504
+ "`nxds update` updates every installed item, so one app never mixes two releases. Run it without item names."
1505
+ );
1506
+ }
1507
+ const config = requireConfig(cwd);
1508
+ const latest = await loadRelease(registry, "latest");
1509
+ const previous = await fetchRelease(registry, config.version);
1510
+ const { known, removed } = splitInstalled(latest, config.items);
1511
+ const current = filesOf(latest, config.framework, known);
1512
+ const installedBefore = previous ? filesOf(
1513
+ previous,
1514
+ config.framework,
1515
+ splitInstalled(previous, config.items).known
1516
+ ) : /* @__PURE__ */ new Set();
1517
+ const orphaned = [...installedBefore].filter(
1518
+ (file) => !current.has(file) && existsSync9(path9.join(cwd, config.dir, file))
1519
+ );
1520
+ config.items = known;
1521
+ const installed = installItems(
1522
+ latest,
1523
+ readProject(cwd),
1524
+ config,
1525
+ known,
1526
+ previous ? releaseFiles(previous, config.framework) : NO_PREVIOUS,
1527
+ options
1528
+ );
1529
+ const from = config.version;
1530
+ const stays = from !== latest.version && installed.skipped.length > 0;
1531
+ if (!stays) config.version = latest.version;
1532
+ if (!options.dryRun) writeConfig(cwd, config);
1533
+ return {
1534
+ data: {
1535
+ dryRun: options.dryRun,
1536
+ from,
1537
+ version: config.version,
1538
+ ...installed,
1539
+ removed,
1540
+ orphaned
1541
+ },
1542
+ lines: [
1543
+ ...previous ? [] : [
1544
+ `${registry} has no nxds ${from}, so every file that differs from ${latest.version} counts as changed.`
1545
+ ],
1546
+ ...installLines(config, installed, options),
1547
+ ...removed.length > 0 ? [
1548
+ `No longer in nxds, so dropped from nxds.json: ${removed.join(", ")}`
1549
+ ] : [],
1550
+ ...orphaned.map(
1551
+ (file) => `No installed item uses ${config.dir}/${file} any more. Delete it once nothing in your app imports it.`
1552
+ ),
1553
+ stays ? `nxds.json stays on ${from} until every kept file is replaced.` : `nxds ${from === latest.version ? "is on" : `${options.dryRun ? "would move" : "moved"} from ${from} to`} ${latest.version}.`
1554
+ ],
1555
+ ok: !stays
1556
+ };
1557
+ }
1558
+
1559
+ // src/index.ts
1560
+ var HELP = `nxds: add nxds to your app.
1561
+
1562
+ Usage
1563
+ nxds init Set up nxds: token CSS, provider, and the layout wiring
1564
+ nxds add <item...> Add components or blocks, and everything they need
1565
+ nxds list [--kind k] List components and blocks (k: component, block)
1566
+ nxds search <term> Find items by name or description
1567
+ nxds view <item> Show an item's files, packages and docs link
1568
+ nxds info Show what the CLI detects in this app
1569
+ nxds update Update every installed item to the latest release
1570
+ nxds diff [item...] Compare your nxds files with the latest release
1571
+ nxds doctor Check that nxds is wired up correctly
1572
+
1573
+ Options
1574
+ --json Print machine-readable JSON
1575
+ --dry-run Show what would change, without changing anything
1576
+ --overwrite Replace files that differ from nxds's copy (add, update)
1577
+ --no-install Skip installing npm packages (init, add, update)
1578
+ --dir <path> nxds folder for init (default: components/nxds, or src/components/nxds)
1579
+ --cwd <path> Run in another folder
1580
+ --registry <url|path> Where to get nxds releases (default: https://nxds.dev/r)
1581
+ -v, --version Print the version
1582
+
1583
+ Docs: https://nxds.dev/getting-started/install`;
1584
+ var OPTIONS = {
1585
+ json: { type: "boolean", default: false },
1586
+ "dry-run": { type: "boolean", default: false },
1587
+ overwrite: { type: "boolean", default: false },
1588
+ "no-install": { type: "boolean", default: false },
1589
+ dir: { type: "string" },
1590
+ cwd: { type: "string" },
1591
+ registry: { type: "string" },
1592
+ kind: { type: "string" },
1593
+ help: { type: "boolean", short: "h", default: false },
1594
+ version: { type: "boolean", short: "v", default: false }
1595
+ };
1596
+ function parseCommandLine() {
1597
+ try {
1598
+ return parseArgs({ allowPositionals: true, options: OPTIONS });
1599
+ } catch (error) {
1600
+ throw new CliError(
1601
+ `${error.message}
1602
+ Run \`nxds --help\` for the options.`
1603
+ );
1604
+ }
1605
+ }
1606
+ function text(data, output) {
1607
+ return { data, lines: [output], ok: true };
1608
+ }
1609
+ async function run(values, positionals) {
1610
+ if (values.version) return text({ version: "0.0.0" }, "0.0.0");
1611
+ if (values.help || positionals[0] === "help")
1612
+ return text({ help: HELP }, HELP);
1613
+ const [command, ...args] = positionals;
1614
+ const cwd = path10.resolve(values.cwd ?? process.cwd());
1615
+ const registry = resolveRegistry(values.registry);
1616
+ const installOptions = {
1617
+ overwrite: values.overwrite,
1618
+ dryRun: values["dry-run"],
1619
+ install: !values["no-install"]
1620
+ };
1621
+ switch (command) {
1622
+ case "init":
1623
+ return init(cwd, registry, { ...installOptions, dir: values.dir });
1624
+ case "add":
1625
+ return add(cwd, registry, args, installOptions);
1626
+ case "update":
1627
+ return update(cwd, registry, args, installOptions);
1628
+ case "list":
1629
+ return list(cwd, registry, values.kind);
1630
+ case "search":
1631
+ return search(cwd, registry, args.join(" "));
1632
+ case "view":
1633
+ return view(cwd, registry, args[0]);
1634
+ case "info":
1635
+ return info(cwd);
1636
+ case "diff":
1637
+ return diff(cwd, registry, args);
1638
+ case "doctor":
1639
+ return doctor(cwd, registry);
1640
+ default:
1641
+ throw new CliError(
1642
+ command ? `Unknown command "${command}". Run \`nxds --help\`.` : HELP
1643
+ );
1644
+ }
1645
+ }
1646
+ async function main() {
1647
+ const json = process.argv.includes("--json");
1648
+ try {
1649
+ const { values, positionals } = parseCommandLine();
1650
+ const result = await run(values, positionals);
1651
+ const output = json ? JSON.stringify({ ok: result.ok, data: result.data }, null, 2) : result.lines.join("\n");
1652
+ process.stdout.write(`${output}
1653
+ `);
1654
+ process.exitCode = result.ok ? 0 : 1;
1655
+ } catch (error) {
1656
+ process.exitCode = 1;
1657
+ const message = error instanceof Error ? error.message : String(error);
1658
+ if (json) {
1659
+ process.stdout.write(
1660
+ `${JSON.stringify({ ok: false, error: message }, null, 2)}
1661
+ `
1662
+ );
1663
+ } else if (error instanceof CliError) {
1664
+ console.error(message);
1665
+ } else {
1666
+ console.error(error);
1667
+ }
1668
+ }
1669
+ }
1670
+ await main();