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.
- package/LICENSE +21 -0
- package/dist/index.js +1670 -0
- package/package.json +48 -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();
|