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