nxds-cli 0.0.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +833 -428
- package/package.json +15 -11
package/dist/index.js
CHANGED
|
@@ -1,27 +1,176 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// src/
|
|
4
|
-
import
|
|
5
|
-
import
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { Command, CommanderError, Option } from "commander";
|
|
5
|
+
import path12 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
|
+
var reactMajorSchema = z.union([z.literal(18), z.literal(19)]);
|
|
53
|
+
var react18Schema = z.object({
|
|
54
|
+
files: z.array(catalogFileSchema),
|
|
55
|
+
packages: z.object({ npm: npmPackagesSchema })
|
|
56
|
+
});
|
|
57
|
+
function variantWith(packages) {
|
|
58
|
+
return z.object({
|
|
59
|
+
/** Other items this one needs, installed first. */
|
|
60
|
+
items: z.array(z.string()),
|
|
61
|
+
packages,
|
|
62
|
+
files: z.array(catalogFileSchema),
|
|
63
|
+
/** Files in `files` the nxds CSS entry imports. */
|
|
64
|
+
css: z.array(relativePathSchema),
|
|
65
|
+
steps: z.array(setupStepSchema),
|
|
66
|
+
react18: react18Schema.optional()
|
|
67
|
+
}).refine(
|
|
68
|
+
({ css, files }) => css.every((entry) => files.some((file) => file.path === entry)),
|
|
69
|
+
{ path: ["css"], message: "must name files in `files`" }
|
|
70
|
+
).refine(
|
|
71
|
+
({ files, react18 }) => (react18?.files ?? []).every(
|
|
72
|
+
(file) => files.some((own) => own.path === file.path)
|
|
73
|
+
),
|
|
74
|
+
{
|
|
75
|
+
path: ["react18", "files"],
|
|
76
|
+
message: "must replace files in `files`"
|
|
77
|
+
}
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
function variantForReact(variant, react) {
|
|
81
|
+
if (react === 19 || !variant.react18) return variant;
|
|
82
|
+
const replaced = new Map(
|
|
83
|
+
variant.react18.files.map((file) => [file.path, file])
|
|
84
|
+
);
|
|
85
|
+
return {
|
|
86
|
+
...variant,
|
|
87
|
+
files: variant.files.map((file) => replaced.get(file.path) ?? file),
|
|
88
|
+
packages: {
|
|
89
|
+
...variant.packages,
|
|
90
|
+
npm: { ...variant.packages.npm, ...variant.react18.packages.npm }
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
var variantSchema = variantWith(z.object({ npm: npmPackagesSchema }));
|
|
95
|
+
var setupVariantSchema = variantWith(
|
|
96
|
+
z.object({
|
|
97
|
+
npm: npmPackagesSchema,
|
|
98
|
+
/** The Tailwind range `init` installs, with the app's build plugin, when the app has no Tailwind. */
|
|
99
|
+
tailwind: rangeSchema
|
|
100
|
+
})
|
|
101
|
+
);
|
|
102
|
+
var itemInfo = {
|
|
103
|
+
name: z.string(),
|
|
104
|
+
title: z.string(),
|
|
105
|
+
description: z.string(),
|
|
106
|
+
docs: z.string()
|
|
107
|
+
};
|
|
108
|
+
var setupItemSchema = z.object({
|
|
109
|
+
...itemInfo,
|
|
110
|
+
kind: z.literal("setup"),
|
|
111
|
+
variants: variantsSchema(setupVariantSchema)
|
|
112
|
+
});
|
|
113
|
+
var catalogItemSchema = z.discriminatedUnion("kind", [
|
|
114
|
+
setupItemSchema,
|
|
115
|
+
z.object({
|
|
116
|
+
...itemInfo,
|
|
117
|
+
kind: z.enum(["component", "block"]),
|
|
118
|
+
variants: variantsSchema(variantSchema)
|
|
119
|
+
})
|
|
120
|
+
]);
|
|
121
|
+
var CATALOG_SCHEMA = 1;
|
|
122
|
+
var catalogSchema = z.object({
|
|
123
|
+
schema: z.literal(CATALOG_SCHEMA),
|
|
124
|
+
/** The `@nxds/react` version this catalog was built from. */
|
|
125
|
+
version: versionSchema,
|
|
126
|
+
/** The frameworks the CLI installs. The docs copy also carries variants for others; a release catalog drops them. Names this version doesn't know are dropped too. */
|
|
127
|
+
frameworks: z.array(z.string()).transform((frameworks) => frameworks.filter(isFramework)),
|
|
128
|
+
/** The React majors this release has files for. Releases from before React 18 support leave it out, and have React 19 files only. */
|
|
129
|
+
reactMajors: z.array(reactMajorSchema).nonempty().default([19]),
|
|
130
|
+
items: z.array(catalogItemSchema)
|
|
131
|
+
});
|
|
132
|
+
function lines(issues, prefix) {
|
|
133
|
+
return issues.flatMap((issue) => {
|
|
134
|
+
const at = [...prefix, ...issue.path];
|
|
135
|
+
if (issue.code === "invalid_key") return lines(issue.issues, at);
|
|
136
|
+
return [
|
|
137
|
+
`${at.length > 0 ? at.map(String).join(".") : "(root)"}: ${issue.message}`
|
|
138
|
+
];
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
function issueLines(error) {
|
|
142
|
+
return lines(error.issues, []);
|
|
143
|
+
}
|
|
6
144
|
|
|
7
145
|
// src/catalog.ts
|
|
8
|
-
import { existsSync
|
|
146
|
+
import { existsSync } from "fs";
|
|
9
147
|
import path2 from "path";
|
|
10
|
-
import {
|
|
148
|
+
import { EnvHttpProxyAgent, fetch } from "undici";
|
|
11
149
|
|
|
12
|
-
// src/
|
|
13
|
-
|
|
14
|
-
import path from "path";
|
|
15
|
-
import { valid } from "semver";
|
|
150
|
+
// src/cli-command.ts
|
|
151
|
+
var CLI_COMMAND = "npx nxds-cli@latest";
|
|
16
152
|
|
|
17
153
|
// src/errors.ts
|
|
18
154
|
import { readFileSync } from "fs";
|
|
155
|
+
var EXIT_CODES = {
|
|
156
|
+
ok: 0,
|
|
157
|
+
/** A network or registry failure, an invalid file, or a bug in nxds. */
|
|
158
|
+
error: 1,
|
|
159
|
+
/** A bad flag, unknown command or item, or a missing argument. */
|
|
160
|
+
usage: 2,
|
|
161
|
+
/** The command ran, and left something for the person to do: conflicts, kept files, manual steps or failed checks. */
|
|
162
|
+
attention: 3
|
|
163
|
+
};
|
|
19
164
|
var CliError = class extends Error {
|
|
165
|
+
exitCode = EXIT_CODES.error;
|
|
166
|
+
};
|
|
167
|
+
var UsageError = class extends CliError {
|
|
168
|
+
exitCode = EXIT_CODES.usage;
|
|
20
169
|
};
|
|
21
170
|
function readJson(file) {
|
|
22
|
-
const
|
|
171
|
+
const text = readFileSync(file, "utf8");
|
|
23
172
|
try {
|
|
24
|
-
return JSON.parse(
|
|
173
|
+
return JSON.parse(text);
|
|
25
174
|
} catch (error) {
|
|
26
175
|
throw new CliError(
|
|
27
176
|
`${file} is not valid JSON (${error.message}). Fix it, then run the command again.`
|
|
@@ -29,57 +178,11 @@ function readJson(file) {
|
|
|
29
178
|
}
|
|
30
179
|
}
|
|
31
180
|
|
|
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
181
|
// src/catalog.ts
|
|
81
182
|
var SETUP = "setup";
|
|
82
183
|
var DEFAULT_REGISTRY = "https://nxds.dev/r";
|
|
184
|
+
var FETCH_TIMEOUT_MS = 3e4;
|
|
185
|
+
var dispatcher = new EnvHttpProxyAgent();
|
|
83
186
|
function isUrl(registry) {
|
|
84
187
|
return /^https?:\/\//.test(registry);
|
|
85
188
|
}
|
|
@@ -87,13 +190,26 @@ function resolveRegistry(option) {
|
|
|
87
190
|
if (!option) return DEFAULT_REGISTRY;
|
|
88
191
|
return isUrl(option) ? option.replace(/\/+$/, "") : path2.resolve(option);
|
|
89
192
|
}
|
|
193
|
+
function timeoutError(url, error) {
|
|
194
|
+
if (!(error instanceof Error) || error.name !== "TimeoutError") return null;
|
|
195
|
+
return new CliError(
|
|
196
|
+
`${url} did not answer within ${FETCH_TIMEOUT_MS / 1e3}s. Check your connection, then run the command again.`
|
|
197
|
+
);
|
|
198
|
+
}
|
|
90
199
|
async function fetchJson(url) {
|
|
91
200
|
let response;
|
|
92
201
|
try {
|
|
93
|
-
response = await fetch(url
|
|
202
|
+
response = await fetch(url, {
|
|
203
|
+
dispatcher,
|
|
204
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
205
|
+
});
|
|
94
206
|
} catch (error) {
|
|
207
|
+
const timeout = timeoutError(url, error);
|
|
208
|
+
if (timeout) throw timeout;
|
|
209
|
+
const { message, cause } = error;
|
|
210
|
+
const reason = cause instanceof Error ? cause.message : message;
|
|
95
211
|
throw new CliError(
|
|
96
|
-
`Could not reach ${url} (${
|
|
212
|
+
`Could not reach ${url} (${reason}). Check your connection, or set HTTPS_PROXY if you are behind a proxy, then run the command again.`
|
|
97
213
|
);
|
|
98
214
|
}
|
|
99
215
|
if (response.status === 404) return null;
|
|
@@ -105,7 +221,7 @@ async function fetchJson(url) {
|
|
|
105
221
|
try {
|
|
106
222
|
return await response.json();
|
|
107
223
|
} catch (error) {
|
|
108
|
-
throw new CliError(
|
|
224
|
+
throw timeoutError(url, error) ?? new CliError(
|
|
109
225
|
`${url} is not valid JSON (${error.message}). Run the command again later.`
|
|
110
226
|
);
|
|
111
227
|
}
|
|
@@ -116,104 +232,52 @@ function registryLocation(registry, file) {
|
|
|
116
232
|
async function readRegistryFile(registry, file) {
|
|
117
233
|
const location = registryLocation(registry, file);
|
|
118
234
|
if (isUrl(registry)) return fetchJson(location);
|
|
119
|
-
return
|
|
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;
|
|
235
|
+
return existsSync(location) ? readJson(location) : null;
|
|
147
236
|
}
|
|
148
|
-
function
|
|
149
|
-
|
|
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;
|
|
237
|
+
function isNewerSchema(value) {
|
|
238
|
+
return typeof value === "object" && value !== null && "schema" in value && typeof value.schema === "number" && value.schema > CATALOG_SCHEMA;
|
|
189
239
|
}
|
|
190
240
|
async function fetchRelease(registry, version) {
|
|
191
241
|
const file = `${version}.json`;
|
|
192
242
|
const value = await readRegistryFile(registry, file);
|
|
193
243
|
if (value === null) return null;
|
|
194
|
-
if (
|
|
244
|
+
if (isNewerSchema(value)) {
|
|
245
|
+
const release = version === "latest" ? "The latest nxds release" : `nxds ${version}`;
|
|
195
246
|
throw new CliError(
|
|
196
|
-
|
|
247
|
+
`${release} needs a newer nxds-cli than ${"0.3.0"}. Run \`${CLI_COMMAND}\` to get the newest one.`
|
|
197
248
|
);
|
|
198
249
|
}
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
250
|
+
const invalid = (problems) => new CliError(
|
|
251
|
+
`${registryLocation(registry, file)} is not a valid nxds release:
|
|
252
|
+
${problems.map((line) => ` ${line}`).join("\n")}
|
|
253
|
+
Check \`--registry\`, or run the command again later.`
|
|
203
254
|
);
|
|
255
|
+
const parsed = catalogSchema.safeParse(value);
|
|
256
|
+
if (!parsed.success) throw invalid(issueLines(parsed.error));
|
|
257
|
+
if (version !== "latest" && parsed.data.version !== version) {
|
|
258
|
+
throw invalid([`version: it holds nxds ${parsed.data.version}`]);
|
|
259
|
+
}
|
|
260
|
+
return parsed.data;
|
|
204
261
|
}
|
|
205
|
-
async function loadRelease(registry, version) {
|
|
262
|
+
async function loadRelease(registry, version, react) {
|
|
206
263
|
const catalog = await fetchRelease(registry, version);
|
|
207
|
-
if (catalog)
|
|
264
|
+
if (!catalog) {
|
|
265
|
+
throw new CliError(
|
|
266
|
+
version === "latest" ? `${registry} has no nxds release.` : `${registry} has no nxds ${version}, the release this app is on. Run \`${CLI_COMMAND} update\` to move to the latest release.`
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
if (catalog.reactMajors.includes(react)) return catalog;
|
|
270
|
+
const latest = version === "latest" ? null : await fetchRelease(registry, "latest");
|
|
271
|
+
const fix = latest?.reactMajors.includes(react) ? `Upgrade the app to React 19, or run \`${CLI_COMMAND} update\` to move to nxds ${latest.version}, which has React ${react} files.` : "Upgrade the app to React 19.";
|
|
208
272
|
throw new CliError(
|
|
209
|
-
|
|
273
|
+
`nxds ${catalog.version} has no React ${react} files. ${fix}`
|
|
210
274
|
);
|
|
211
275
|
}
|
|
212
276
|
function findItem(catalog, name) {
|
|
213
277
|
const item = catalog.items.find((candidate) => candidate.name === name);
|
|
214
278
|
if (item) return item;
|
|
215
|
-
throw new
|
|
216
|
-
`nxds ${catalog.version} has no item named "${name}". Run
|
|
279
|
+
throw new UsageError(
|
|
280
|
+
`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.`
|
|
217
281
|
);
|
|
218
282
|
}
|
|
219
283
|
function variantOf(item, framework) {
|
|
@@ -221,12 +285,15 @@ function variantOf(item, framework) {
|
|
|
221
285
|
if (variant) return variant;
|
|
222
286
|
throw new CliError(`${item.title} is not available for ${framework} yet.`);
|
|
223
287
|
}
|
|
224
|
-
function
|
|
288
|
+
function variantFor(item, target) {
|
|
289
|
+
return variantForReact(variantOf(item, target.framework), target.react);
|
|
290
|
+
}
|
|
291
|
+
function setupVariant(catalog, target) {
|
|
225
292
|
const setup = catalog.items.find((item) => item.name === SETUP);
|
|
226
293
|
if (setup?.kind !== "setup") {
|
|
227
294
|
throw new Error(`nxds ${catalog.version} has no "${SETUP}" setup item.`);
|
|
228
295
|
}
|
|
229
|
-
return
|
|
296
|
+
return variantFor(setup, target);
|
|
230
297
|
}
|
|
231
298
|
function resolveItems(catalog, framework, names) {
|
|
232
299
|
const ordered = [];
|
|
@@ -241,10 +308,10 @@ function resolveItems(catalog, framework, names) {
|
|
|
241
308
|
names.forEach(visit);
|
|
242
309
|
return ordered;
|
|
243
310
|
}
|
|
244
|
-
function releaseFiles(catalog,
|
|
311
|
+
function releaseFiles(catalog, target) {
|
|
245
312
|
return new Map(
|
|
246
|
-
catalog.items.flatMap(
|
|
247
|
-
(item) => (item
|
|
313
|
+
catalog.items.filter((item) => item.variants[target.framework]).flatMap(
|
|
314
|
+
(item) => variantFor(item, target).files.map(
|
|
248
315
|
(file) => [file.path, file.content]
|
|
249
316
|
)
|
|
250
317
|
)
|
|
@@ -258,15 +325,66 @@ function splitInstalled(catalog, installed) {
|
|
|
258
325
|
};
|
|
259
326
|
}
|
|
260
327
|
|
|
328
|
+
// src/commands/browse.ts
|
|
329
|
+
import { existsSync as existsSync5 } from "fs";
|
|
330
|
+
import path6 from "path";
|
|
331
|
+
|
|
332
|
+
// src/config.ts
|
|
333
|
+
import { existsSync as existsSync2, writeFileSync } from "fs";
|
|
334
|
+
import path3 from "path";
|
|
335
|
+
import { z as z2 } from "zod";
|
|
336
|
+
var CONFIG_FILE = "nxds.json";
|
|
337
|
+
var configSchema = z2.looseObject({
|
|
338
|
+
/** The CLI installs React only. */
|
|
339
|
+
framework: z2.literal("react"),
|
|
340
|
+
/** The React major the installed files are for. */
|
|
341
|
+
react: reactMajorSchema,
|
|
342
|
+
/** The nxds release every installed file comes from. */
|
|
343
|
+
version: versionSchema,
|
|
344
|
+
/** The nxds folder, relative to the project root. */
|
|
345
|
+
dir: relativePathSchema,
|
|
346
|
+
/** The nxds CSS entry, relative to the project root. */
|
|
347
|
+
css: relativePathSchema,
|
|
348
|
+
/** Installed items, in install order. */
|
|
349
|
+
items: z2.array(z2.string())
|
|
350
|
+
});
|
|
351
|
+
function configPath(root) {
|
|
352
|
+
return path3.join(root, CONFIG_FILE);
|
|
353
|
+
}
|
|
354
|
+
function readConfig(root) {
|
|
355
|
+
const file = configPath(root);
|
|
356
|
+
if (!existsSync2(file)) return null;
|
|
357
|
+
const parsed = configSchema.safeParse(readJson(file));
|
|
358
|
+
if (parsed.success) return parsed.data;
|
|
359
|
+
throw new CliError(
|
|
360
|
+
`${CONFIG_FILE} is not valid:
|
|
361
|
+
${issueLines(parsed.error).map((line) => ` ${line}`).join(
|
|
362
|
+
"\n"
|
|
363
|
+
)}
|
|
364
|
+
Fix it, or delete ${CONFIG_FILE} and run \`${CLI_COMMAND} init\` again.`
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
function requireConfig(root) {
|
|
368
|
+
const config = readConfig(root);
|
|
369
|
+
if (config) return config;
|
|
370
|
+
throw new CliError(
|
|
371
|
+
`nxds is not set up here. Run \`${CLI_COMMAND} init\` first.`
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
function writeConfig(root, config) {
|
|
375
|
+
writeFileSync(configPath(root), `${JSON.stringify(config, null, 2)}
|
|
376
|
+
`);
|
|
377
|
+
}
|
|
378
|
+
|
|
261
379
|
// src/project.ts
|
|
262
380
|
import { existsSync as existsSync4 } from "fs";
|
|
263
|
-
import
|
|
264
|
-
import { major, minVersion as minVersion2, validRange as validRange3 } from "semver";
|
|
381
|
+
import path5 from "path";
|
|
382
|
+
import { major, minVersion as minVersion2, parse, validRange as validRange3 } from "semver";
|
|
265
383
|
|
|
266
384
|
// src/packages.ts
|
|
267
385
|
import { spawnSync } from "child_process";
|
|
268
386
|
import { existsSync as existsSync3 } from "fs";
|
|
269
|
-
import
|
|
387
|
+
import path4 from "path";
|
|
270
388
|
import { minVersion, satisfies, validRange as validRange2 } from "semver";
|
|
271
389
|
var ADD_COMMAND = {
|
|
272
390
|
npm: ["install"],
|
|
@@ -287,11 +405,11 @@ function packagesToInstall(project, packages) {
|
|
|
287
405
|
function installedVersion(project, name) {
|
|
288
406
|
let dir = project.root;
|
|
289
407
|
for (; ; ) {
|
|
290
|
-
const manifest =
|
|
408
|
+
const manifest = path4.join(dir, "node_modules", name, "package.json");
|
|
291
409
|
if (existsSync3(manifest)) {
|
|
292
410
|
return readJson(manifest).version ?? null;
|
|
293
411
|
}
|
|
294
|
-
const parent =
|
|
412
|
+
const parent = path4.dirname(dir);
|
|
295
413
|
if (parent === dir) return null;
|
|
296
414
|
dir = parent;
|
|
297
415
|
}
|
|
@@ -307,11 +425,11 @@ function installPackages(project, specs, { dev }) {
|
|
|
307
425
|
cwd: project.root,
|
|
308
426
|
stdio: ["ignore", 2, 2]
|
|
309
427
|
};
|
|
310
|
-
const
|
|
428
|
+
const run = process.platform === "win32" ? spawnSync(
|
|
311
429
|
[project.packageManager, ...args.map((arg) => `"${arg}"`)].join(" "),
|
|
312
430
|
{ ...options, shell: true }
|
|
313
431
|
) : spawnSync(project.packageManager, args, options);
|
|
314
|
-
if (
|
|
432
|
+
if (run.status !== 0) {
|
|
315
433
|
throw new CliError(
|
|
316
434
|
`\`${project.packageManager} ${args.join(" ")}\` failed. Fix the error above, then run the same nxds command again.`
|
|
317
435
|
);
|
|
@@ -338,7 +456,7 @@ var VITE_CONFIGS = [
|
|
|
338
456
|
"vite.config.js"
|
|
339
457
|
];
|
|
340
458
|
function firstExisting(root, candidates) {
|
|
341
|
-
return candidates.find((file) => existsSync4(
|
|
459
|
+
return candidates.find((file) => existsSync4(path5.join(root, file)));
|
|
342
460
|
}
|
|
343
461
|
function detectSetup(root, dependencies) {
|
|
344
462
|
if ("next" in dependencies && firstExisting(
|
|
@@ -353,10 +471,10 @@ function detectSetup(root, dependencies) {
|
|
|
353
471
|
return null;
|
|
354
472
|
}
|
|
355
473
|
function readProject(root) {
|
|
356
|
-
const manifestPath =
|
|
474
|
+
const manifestPath = path5.join(root, "package.json");
|
|
357
475
|
if (!existsSync4(manifestPath)) {
|
|
358
476
|
throw new CliError(
|
|
359
|
-
`No package.json in ${root}. Run
|
|
477
|
+
`No package.json in ${root}. Run \`${CLI_COMMAND}\` from your app's root folder, or pass --cwd.`
|
|
360
478
|
);
|
|
361
479
|
}
|
|
362
480
|
const manifest = readJson(manifestPath);
|
|
@@ -367,15 +485,36 @@ function readProject(root) {
|
|
|
367
485
|
return {
|
|
368
486
|
root,
|
|
369
487
|
setup: detectSetup(root, dependencies),
|
|
370
|
-
packageManager: LOCKFILES.find(([file]) => existsSync4(
|
|
371
|
-
usesSrc: existsSync4(
|
|
372
|
-
typescript: existsSync4(
|
|
488
|
+
packageManager: LOCKFILES.find(([file]) => existsSync4(path5.join(root, file)))?.[1] ?? "npm",
|
|
489
|
+
usesSrc: existsSync4(path5.join(root, "src")),
|
|
490
|
+
typescript: existsSync4(path5.join(root, "tsconfig.json")),
|
|
373
491
|
dependencies
|
|
374
492
|
};
|
|
375
493
|
}
|
|
376
494
|
function findFile(project, candidates) {
|
|
377
495
|
return firstExisting(project.root, candidates);
|
|
378
496
|
}
|
|
497
|
+
function reactMajor(project) {
|
|
498
|
+
const declared = project.dependencies.react;
|
|
499
|
+
if (declared === void 0) {
|
|
500
|
+
throw new CliError(
|
|
501
|
+
"This app has no react dependency. nxds needs React 18.3 or 19."
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
const installed = installedVersion(project, "react");
|
|
505
|
+
const range = validRange3(declared) === null ? null : minVersion2(declared);
|
|
506
|
+
const version = installed === null ? range : parse(installed);
|
|
507
|
+
if (version === null) {
|
|
508
|
+
throw new CliError(
|
|
509
|
+
`This app declares react ${declared}, which names no version, and none is installed. Install your dependencies, then run the command again.`
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
if (version.major >= 19) return 19;
|
|
513
|
+
if (version.major === 18 && version.minor >= 3) return 18;
|
|
514
|
+
throw new CliError(
|
|
515
|
+
`nxds needs React 18.3 or 19, and this app uses react ${version.version}. Upgrade React, then run the command again.`
|
|
516
|
+
);
|
|
517
|
+
}
|
|
379
518
|
function majorVersion(project, name) {
|
|
380
519
|
const declared = project.dependencies[name];
|
|
381
520
|
if (declared === void 0) return null;
|
|
@@ -385,9 +524,8 @@ function majorVersion(project, name) {
|
|
|
385
524
|
}
|
|
386
525
|
|
|
387
526
|
// src/commands/browse.ts
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
return loadRelease(registry, readConfig(cwd)?.version ?? "latest");
|
|
527
|
+
function appRelease(registry, config, react) {
|
|
528
|
+
return loadRelease(registry, config?.version ?? "latest", react);
|
|
391
529
|
}
|
|
392
530
|
function summary(item) {
|
|
393
531
|
return {
|
|
@@ -397,8 +535,8 @@ function summary(item) {
|
|
|
397
535
|
description: item.description
|
|
398
536
|
};
|
|
399
537
|
}
|
|
400
|
-
function firstSentence(
|
|
401
|
-
return
|
|
538
|
+
function firstSentence(text) {
|
|
539
|
+
return text.split(/(?<=\.)\s/)[0]?.replaceAll("`", "") ?? text;
|
|
402
540
|
}
|
|
403
541
|
function summaryLines(items) {
|
|
404
542
|
const width = Math.max(...items.map((item) => item.name.length));
|
|
@@ -407,17 +545,17 @@ function summaryLines(items) {
|
|
|
407
545
|
);
|
|
408
546
|
}
|
|
409
547
|
async function list(cwd, registry, kind) {
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
548
|
+
const catalog = await appRelease(
|
|
549
|
+
registry,
|
|
550
|
+
readConfig(cwd),
|
|
551
|
+
projectReact(cwd)
|
|
552
|
+
);
|
|
553
|
+
const items = catalog.items.filter(
|
|
414
554
|
(item) => item.name !== SETUP && (kind === void 0 || item.kind === kind)
|
|
415
555
|
);
|
|
416
556
|
return { data: items.map(summary), lines: summaryLines(items), ok: true };
|
|
417
557
|
}
|
|
418
558
|
async function search(cwd, registry, query) {
|
|
419
|
-
if (!query)
|
|
420
|
-
throw new CliError("Give a search term, such as `nxds search date`.");
|
|
421
559
|
const needle = query.toLowerCase();
|
|
422
560
|
const rank = (item) => {
|
|
423
561
|
if (item.name.includes(needle)) return 0;
|
|
@@ -425,7 +563,12 @@ async function search(cwd, registry, query) {
|
|
|
425
563
|
if (item.description.toLowerCase().includes(needle)) return 2;
|
|
426
564
|
return void 0;
|
|
427
565
|
};
|
|
428
|
-
const
|
|
566
|
+
const catalog = await appRelease(
|
|
567
|
+
registry,
|
|
568
|
+
readConfig(cwd),
|
|
569
|
+
projectReact(cwd)
|
|
570
|
+
);
|
|
571
|
+
const items = catalog.items.filter((item) => item.name !== SETUP).flatMap((item) => {
|
|
429
572
|
const score = rank(item);
|
|
430
573
|
return score === void 0 ? [] : [{ item, score }];
|
|
431
574
|
}).sort((a, b) => a.score - b.score).map(({ item }) => item);
|
|
@@ -435,20 +578,28 @@ async function search(cwd, registry, query) {
|
|
|
435
578
|
ok: true
|
|
436
579
|
};
|
|
437
580
|
}
|
|
438
|
-
function projectFramework(
|
|
439
|
-
|
|
440
|
-
if (configured) return configured;
|
|
581
|
+
function projectFramework(config, catalog) {
|
|
582
|
+
if (config) return config.framework;
|
|
441
583
|
const [only, ...others] = catalog.frameworks;
|
|
442
584
|
if (only && others.length === 0) return only;
|
|
443
585
|
throw new CliError(
|
|
444
|
-
|
|
586
|
+
`This release has several frameworks. Run \`${CLI_COMMAND} init\` first, so nxds knows which one your app uses.`
|
|
445
587
|
);
|
|
446
588
|
}
|
|
589
|
+
function projectReact(cwd) {
|
|
590
|
+
if (!existsSync5(path6.join(cwd, "package.json"))) return 19;
|
|
591
|
+
const project = readProject(cwd);
|
|
592
|
+
return "react" in project.dependencies ? reactMajor(project) : 19;
|
|
593
|
+
}
|
|
447
594
|
async function view(cwd, registry, name) {
|
|
448
|
-
|
|
449
|
-
const
|
|
595
|
+
const config = readConfig(cwd);
|
|
596
|
+
const react = projectReact(cwd);
|
|
597
|
+
const catalog = await appRelease(registry, config, react);
|
|
450
598
|
const item = findItem(catalog, name);
|
|
451
|
-
const variant =
|
|
599
|
+
const variant = variantFor(item, {
|
|
600
|
+
framework: projectFramework(config, catalog),
|
|
601
|
+
react
|
|
602
|
+
});
|
|
452
603
|
const packages = Object.entries(variant.packages.npm).map(
|
|
453
604
|
([pkg, range]) => `${pkg}@${range}`
|
|
454
605
|
);
|
|
@@ -497,7 +648,7 @@ function info(cwd) {
|
|
|
497
648
|
`Package manager: ${project.packageManager}`,
|
|
498
649
|
`TypeScript: ${project.typescript ? "yes" : "no"}`,
|
|
499
650
|
`Tailwind: ${project.dependencies.tailwindcss ?? "not installed"}`,
|
|
500
|
-
config ? `nxds: ${config.version} in ${config.dir}, CSS entry ${config.css}` :
|
|
651
|
+
config ? `nxds: ${config.version} in ${config.dir}, CSS entry ${config.css}` : `nxds: not set up. Run \`${CLI_COMMAND} init\`.`,
|
|
501
652
|
...config && config.items.length > 0 ? [
|
|
502
653
|
`Installed: ${config.items.filter((item) => item !== SETUP).join(", ") || "setup only"}`
|
|
503
654
|
] : []
|
|
@@ -508,43 +659,105 @@ function info(cwd) {
|
|
|
508
659
|
|
|
509
660
|
// src/commands/check.ts
|
|
510
661
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
511
|
-
import { existsSync as
|
|
512
|
-
import
|
|
513
|
-
import { satisfies as satisfies2 } from "semver";
|
|
662
|
+
import { existsSync as existsSync8, readFileSync as readFileSync4 } from "fs";
|
|
663
|
+
import path9 from "path";
|
|
664
|
+
import { major as major2, satisfies as satisfies2 } from "semver";
|
|
514
665
|
|
|
515
666
|
// src/files.ts
|
|
516
|
-
import { existsSync as
|
|
517
|
-
import
|
|
518
|
-
|
|
667
|
+
import { existsSync as existsSync6, mkdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
668
|
+
import path7 from "path";
|
|
669
|
+
import { merge } from "node-diff3";
|
|
670
|
+
function baseOf(previous, file) {
|
|
671
|
+
if (!previous) return void 0;
|
|
672
|
+
return previous.get(file) ?? "";
|
|
673
|
+
}
|
|
674
|
+
function withLf(content) {
|
|
675
|
+
return `${content.replace(/\r\n?/g, "\n").replace(/\n+$/, "")}
|
|
676
|
+
`;
|
|
677
|
+
}
|
|
678
|
+
function normalize(content) {
|
|
679
|
+
const lines2 = withLf(content).split("\n").map((line) => line.trimEnd());
|
|
680
|
+
return `${lines2.join("\n").trimEnd()}
|
|
681
|
+
`;
|
|
682
|
+
}
|
|
683
|
+
function sameContent(a, b) {
|
|
684
|
+
return b !== void 0 && normalize(a) === normalize(b);
|
|
685
|
+
}
|
|
686
|
+
var OURS_LABEL = "your file";
|
|
687
|
+
var CONFLICT_START = `<<<<<<< ${OURS_LABEL}`;
|
|
688
|
+
function hasConflictMarkers(content) {
|
|
689
|
+
return content.split(/\r?\n/).includes(CONFLICT_START);
|
|
690
|
+
}
|
|
691
|
+
function conflictedFiles(root, config, files) {
|
|
692
|
+
return [...files].filter((file) => {
|
|
693
|
+
const target = path7.join(root, config.dir, file);
|
|
694
|
+
return existsSync6(target) && hasConflictMarkers(readFileSync2(target, "utf8"));
|
|
695
|
+
});
|
|
696
|
+
}
|
|
519
697
|
function fileState(root, config, file, previous) {
|
|
520
|
-
const target =
|
|
521
|
-
if (!
|
|
698
|
+
const target = path7.join(root, config.dir, file.path);
|
|
699
|
+
if (!existsSync6(target)) return "missing";
|
|
522
700
|
const onDisk = readFileSync2(target, "utf8");
|
|
523
|
-
if (onDisk
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
701
|
+
if (hasConflictMarkers(onDisk)) return "conflicted";
|
|
702
|
+
if (sameContent(onDisk, file.content)) return "current";
|
|
703
|
+
return sameContent(onDisk, previous?.get(file.path)) ? "outdated" : "changed";
|
|
704
|
+
}
|
|
705
|
+
function mergeFile(ours, base, theirs, version) {
|
|
706
|
+
const lines2 = (content) => withLf(content).split("\n");
|
|
707
|
+
const { conflict, result } = merge(lines2(ours), lines2(base), lines2(theirs), {
|
|
708
|
+
label: { a: OURS_LABEL, b: `nxds ${version}` }
|
|
709
|
+
});
|
|
710
|
+
return { content: result.join("\n"), conflict };
|
|
711
|
+
}
|
|
712
|
+
function writeFiles(root, config, files, previous, { overwrite, dryRun, version }) {
|
|
713
|
+
const result = {
|
|
714
|
+
written: [],
|
|
715
|
+
unchanged: [],
|
|
716
|
+
merged: [],
|
|
717
|
+
conflicted: [],
|
|
718
|
+
unresolved: [],
|
|
719
|
+
skipped: []
|
|
720
|
+
};
|
|
721
|
+
const write = (file, content) => {
|
|
722
|
+
if (dryRun) return;
|
|
723
|
+
const target = path7.join(root, config.dir, file);
|
|
724
|
+
mkdirSync(path7.dirname(target), { recursive: true });
|
|
725
|
+
writeFileSync2(target, content);
|
|
726
|
+
};
|
|
528
727
|
for (const file of files) {
|
|
529
728
|
const state = fileState(root, config, file, previous);
|
|
530
729
|
if (state === "current") {
|
|
531
730
|
result.unchanged.push(file.path);
|
|
532
731
|
continue;
|
|
533
732
|
}
|
|
534
|
-
if (state === "
|
|
733
|
+
if (overwrite || state === "missing" || state === "outdated") {
|
|
734
|
+
result.written.push(file.path);
|
|
735
|
+
write(file.path, file.content);
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
if (state === "conflicted") {
|
|
739
|
+
result.unresolved.push(file.path);
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
742
|
+
const base = baseOf(previous, file.path);
|
|
743
|
+
if (base === void 0) {
|
|
535
744
|
result.skipped.push(file.path);
|
|
536
745
|
continue;
|
|
537
746
|
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
const
|
|
541
|
-
|
|
542
|
-
|
|
747
|
+
const target = path7.join(root, config.dir, file.path);
|
|
748
|
+
const ours = readFileSync2(target, "utf8");
|
|
749
|
+
const merged = mergeFile(ours, base, file.content, version);
|
|
750
|
+
if (sameContent(ours, merged.content)) {
|
|
751
|
+
result.unchanged.push(file.path);
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
result[merged.conflict ? "conflicted" : "merged"].push(file.path);
|
|
755
|
+
write(file.path, merged.content);
|
|
543
756
|
}
|
|
544
757
|
return result;
|
|
545
758
|
}
|
|
546
759
|
function relativeImport(fromFile, toFile) {
|
|
547
|
-
const relative =
|
|
760
|
+
const relative = path7.relative(path7.dirname(fromFile), toFile).split(path7.sep).join("/");
|
|
548
761
|
return relative.startsWith(".") ? relative : `./${relative}`;
|
|
549
762
|
}
|
|
550
763
|
|
|
@@ -560,11 +773,11 @@ import {
|
|
|
560
773
|
isObjectProperty,
|
|
561
774
|
isSpreadElement
|
|
562
775
|
} from "@babel/types";
|
|
563
|
-
import { existsSync as
|
|
564
|
-
import
|
|
776
|
+
import { existsSync as existsSync7, readFileSync as readFileSync3 } from "fs";
|
|
777
|
+
import path8 from "path";
|
|
565
778
|
|
|
566
779
|
// src/setup/source-edit.ts
|
|
567
|
-
import { parse } from "@babel/parser";
|
|
780
|
+
import { parse as parse2 } from "@babel/parser";
|
|
568
781
|
import {
|
|
569
782
|
isImportDeclaration,
|
|
570
783
|
isJSXAttribute,
|
|
@@ -574,7 +787,7 @@ import {
|
|
|
574
787
|
} from "@babel/types";
|
|
575
788
|
function parseModule(source, file) {
|
|
576
789
|
try {
|
|
577
|
-
return
|
|
790
|
+
return parse2(source, {
|
|
578
791
|
sourceType: "module",
|
|
579
792
|
plugins: ["typescript", "jsx"]
|
|
580
793
|
}).program;
|
|
@@ -611,24 +824,24 @@ function findJsx(root, name) {
|
|
|
611
824
|
}
|
|
612
825
|
function applyInsertions(source, insertions) {
|
|
613
826
|
return [...insertions].sort((a, b) => b.at - a.at).reduce(
|
|
614
|
-
(
|
|
827
|
+
(text, { at, text: inserted }) => text.slice(0, at) + inserted + text.slice(at),
|
|
615
828
|
source
|
|
616
829
|
);
|
|
617
830
|
}
|
|
618
|
-
function importInsertion(source,
|
|
619
|
-
const imports =
|
|
831
|
+
function importInsertion(source, program2, lines2) {
|
|
832
|
+
const imports = program2.body.filter((node) => isImportDeclaration(node));
|
|
620
833
|
const first = imports[0];
|
|
621
834
|
const quote = first && source[startOf(first.source)];
|
|
622
835
|
const semicolons = !first || source[endOf(first) - 1] === ";";
|
|
623
|
-
const
|
|
836
|
+
const text = lines2.map((line) => quote === '"' ? line.replaceAll("'", '"') : line).map((line) => semicolons ? line : line.replace(/;$/, "")).join("\n");
|
|
624
837
|
const lastImport = imports.at(-1);
|
|
625
838
|
if (lastImport) return { at: endOf(lastImport), text: `
|
|
626
|
-
${
|
|
627
|
-
const lastDirective =
|
|
839
|
+
${text}` };
|
|
840
|
+
const lastDirective = program2.directives.at(-1);
|
|
628
841
|
if (lastDirective) return { at: endOf(lastDirective), text: `
|
|
629
842
|
|
|
630
|
-
${
|
|
631
|
-
return { at: 0, text: `${
|
|
843
|
+
${text}` };
|
|
844
|
+
return { at: 0, text: `${text}
|
|
632
845
|
|
|
633
846
|
` };
|
|
634
847
|
}
|
|
@@ -667,7 +880,7 @@ function tailwindPackages(setup, range) {
|
|
|
667
880
|
}
|
|
668
881
|
function appDir(project) {
|
|
669
882
|
const dir = APP_DIRS.find(
|
|
670
|
-
(candidate) =>
|
|
883
|
+
(candidate) => existsSync7(path8.join(project.root, candidate, "layout.tsx"))
|
|
671
884
|
);
|
|
672
885
|
if (!dir)
|
|
673
886
|
throw new CliError("No app/layout.tsx or src/app/layout.tsx found.");
|
|
@@ -683,7 +896,7 @@ function cssEntryPath(project) {
|
|
|
683
896
|
return `${entryDir(project)}/nxds.css`;
|
|
684
897
|
}
|
|
685
898
|
function read(project, file) {
|
|
686
|
-
return readFileSync3(
|
|
899
|
+
return readFileSync3(path8.join(project.root, file), "utf8");
|
|
687
900
|
}
|
|
688
901
|
function cssEntry(config, withPreflight) {
|
|
689
902
|
const tokens = relativeImport(config.css, `${config.dir}/tailwind/nxds.css`);
|
|
@@ -722,8 +935,8 @@ function wireNextLayout(project, config, steps) {
|
|
|
722
935
|
const layout = `${appDir(project)}/layout.tsx`;
|
|
723
936
|
const source = read(project, layout);
|
|
724
937
|
if (/NxdsAppearance(Provider|Script)/.test(source)) return NO_CHANGES;
|
|
725
|
-
const
|
|
726
|
-
const html = findJsx(
|
|
938
|
+
const program2 = parseModule(source, layout);
|
|
939
|
+
const html = findJsx(program2, "html");
|
|
727
940
|
const body = html && findJsx(html, "body");
|
|
728
941
|
const head = html && findJsx(html, "head");
|
|
729
942
|
if (!html || !body?.closingElement || head && !head.closingElement) {
|
|
@@ -785,15 +998,15 @@ ${htmlIndent}${step}</head>`
|
|
|
785
998
|
imports.push("import { defaultAppearance } from './appearance-state';");
|
|
786
999
|
}
|
|
787
1000
|
if (imports.length === 0) return NO_CHANGES;
|
|
788
|
-
insertions.push(importInsertion(source,
|
|
1001
|
+
insertions.push(importInsertion(source, program2, imports));
|
|
789
1002
|
return {
|
|
790
1003
|
changes: [editedFile(layout, applyInsertions(source, insertions))],
|
|
791
1004
|
manual: []
|
|
792
1005
|
};
|
|
793
1006
|
}
|
|
794
|
-
function renderedElement(
|
|
1007
|
+
function renderedElement(program2) {
|
|
795
1008
|
const call = findNode(
|
|
796
|
-
|
|
1009
|
+
program2,
|
|
797
1010
|
(node) => isCallExpression(node) && isMemberExpression(node.callee) && isIdentifier(node.callee.property, { name: "render" }) && isJSXElement2(node.arguments[0])
|
|
798
1011
|
);
|
|
799
1012
|
return call?.arguments[0];
|
|
@@ -803,12 +1016,12 @@ function wireViteEntry(project, config, steps) {
|
|
|
803
1016
|
if (!entry) throw new CliError("No src/main.tsx found.");
|
|
804
1017
|
const source = read(project, entry);
|
|
805
1018
|
if (source.includes("NxdsAppearanceProvider")) return NO_CHANGES;
|
|
806
|
-
const
|
|
1019
|
+
const program2 = parseModule(source, entry);
|
|
807
1020
|
const imports = [];
|
|
808
1021
|
const insertions = [];
|
|
809
1022
|
if (steps.has("add-css-import")) imports.push("import './nxds.css';");
|
|
810
1023
|
if (steps.has("wrap-root-with-provider")) {
|
|
811
|
-
const app = renderedElement(
|
|
1024
|
+
const app = renderedElement(program2);
|
|
812
1025
|
if (!app) {
|
|
813
1026
|
return {
|
|
814
1027
|
changes: [],
|
|
@@ -836,7 +1049,7 @@ ${indent}</NxdsAppearanceProvider>` }
|
|
|
836
1049
|
);
|
|
837
1050
|
}
|
|
838
1051
|
if (imports.length === 0) return NO_CHANGES;
|
|
839
|
-
insertions.push(importInsertion(source,
|
|
1052
|
+
insertions.push(importInsertion(source, program2, imports));
|
|
840
1053
|
return {
|
|
841
1054
|
changes: [editedFile(entry, applyInsertions(source, insertions))],
|
|
842
1055
|
manual: []
|
|
@@ -863,8 +1076,8 @@ export function nxdsAppearance(): Plugin {
|
|
|
863
1076
|
};
|
|
864
1077
|
}
|
|
865
1078
|
`;
|
|
866
|
-
function configObject(
|
|
867
|
-
const declaration =
|
|
1079
|
+
function configObject(program2) {
|
|
1080
|
+
const declaration = program2.body.find(
|
|
868
1081
|
(node) => isExportDefaultDeclaration(node)
|
|
869
1082
|
)?.declaration;
|
|
870
1083
|
if (isObjectExpression(declaration)) return declaration;
|
|
@@ -894,11 +1107,11 @@ var NXDS_VITE_PLUGIN = {
|
|
|
894
1107
|
var VITE_ALIAS = "{ '@': '/src' }";
|
|
895
1108
|
function wireViteConfig(project, addTailwind, steps) {
|
|
896
1109
|
const configFile = findFile(project, VITE_CONFIGS) ?? "vite.config.ts";
|
|
897
|
-
const addScript = steps.has("add-head-script") && !
|
|
1110
|
+
const addScript = steps.has("add-head-script") && !existsSync7(path8.join(project.root, "nxds.vite.ts"));
|
|
898
1111
|
const changes = addScript ? [{ path: "nxds.vite.ts", content: VITE_PLUGIN, created: true }] : [];
|
|
899
|
-
const source =
|
|
900
|
-
const
|
|
901
|
-
const object =
|
|
1112
|
+
const source = existsSync7(path8.join(project.root, configFile)) ? read(project, configFile) : "";
|
|
1113
|
+
const program2 = source ? parseModule(source, configFile) : void 0;
|
|
1114
|
+
const object = program2 && configObject(program2);
|
|
902
1115
|
const plugins = object && pluginsArray(object);
|
|
903
1116
|
const newPlugins = [
|
|
904
1117
|
...addTailwind ? [TAILWIND_VITE_PLUGIN] : [],
|
|
@@ -938,8 +1151,8 @@ function wireViteConfig(project, addTailwind, steps) {
|
|
|
938
1151
|
step: `Add resolve.alias ${VITE_ALIAS}, so imports like @/components/nxds/... resolve.`
|
|
939
1152
|
});
|
|
940
1153
|
}
|
|
941
|
-
if (
|
|
942
|
-
insertions.push(importInsertion(source,
|
|
1154
|
+
if (program2 && insertions.length > 0) {
|
|
1155
|
+
insertions.push(importInsertion(source, program2, imports));
|
|
943
1156
|
changes.push(editedFile(configFile, applyInsertions(source, insertions)));
|
|
944
1157
|
}
|
|
945
1158
|
return { changes, manual };
|
|
@@ -984,8 +1197,8 @@ var ESLINT_CONFIGS = [
|
|
|
984
1197
|
"eslint.config.mjs",
|
|
985
1198
|
"eslint.config.ts"
|
|
986
1199
|
];
|
|
987
|
-
function exportedConfigs(
|
|
988
|
-
const declaration =
|
|
1200
|
+
function exportedConfigs(program2) {
|
|
1201
|
+
const declaration = program2.body.find(
|
|
989
1202
|
(node) => isExportDefaultDeclaration(node)
|
|
990
1203
|
)?.declaration;
|
|
991
1204
|
if (isArrayExpression(declaration)) return declaration.elements;
|
|
@@ -1057,7 +1270,7 @@ function wireNextTailwind(project) {
|
|
|
1057
1270
|
};
|
|
1058
1271
|
}
|
|
1059
1272
|
function createdUnlessExists(project, file, content) {
|
|
1060
|
-
if (
|
|
1273
|
+
if (existsSync7(path8.join(project.root, file))) return NO_CHANGES;
|
|
1061
1274
|
return { changes: [{ path: file, content, created: true }], manual: [] };
|
|
1062
1275
|
}
|
|
1063
1276
|
function planWiring(project, config, addTailwind, setupSteps) {
|
|
@@ -1094,36 +1307,53 @@ function planWiring(project, config, addTailwind, setupSteps) {
|
|
|
1094
1307
|
// src/commands/check.ts
|
|
1095
1308
|
async function diff(cwd, registry, names) {
|
|
1096
1309
|
const config = requireConfig(cwd);
|
|
1097
|
-
const
|
|
1310
|
+
const target = { ...config, react: reactMajor(readProject(cwd)) };
|
|
1311
|
+
const latest = await loadRelease(registry, "latest", target.react);
|
|
1098
1312
|
const previous = await fetchRelease(registry, config.version);
|
|
1099
|
-
const previousFiles = previous
|
|
1313
|
+
const previousFiles = previous && releaseFiles(previous, config);
|
|
1100
1314
|
const { known, removed } = splitInstalled(latest, config.items);
|
|
1101
1315
|
const items = resolveItems(
|
|
1102
1316
|
latest,
|
|
1103
1317
|
config.framework,
|
|
1104
1318
|
names.length > 0 ? names : known
|
|
1105
1319
|
);
|
|
1320
|
+
const update2 = `\`${CLI_COMMAND} update\``;
|
|
1321
|
+
const overwrite = `\`${CLI_COMMAND} update --overwrite\``;
|
|
1322
|
+
function advice(state, file) {
|
|
1323
|
+
if (state === "outdated") return `${update2} replaces it`;
|
|
1324
|
+
if (state === "missing") return `${update2} restores it`;
|
|
1325
|
+
if (state === "conflicted") {
|
|
1326
|
+
return `has conflict markers; resolve them, or ${overwrite} replaces it`;
|
|
1327
|
+
}
|
|
1328
|
+
const base = baseOf(previousFiles, file.path);
|
|
1329
|
+
if (base === void 0) {
|
|
1330
|
+
return `has your edits; ${update2} keeps it, ${overwrite} replaces it`;
|
|
1331
|
+
}
|
|
1332
|
+
if (sameContent(base, file.content)) {
|
|
1333
|
+
return `has your edits; nxds hasn't changed it since ${config.version}`;
|
|
1334
|
+
}
|
|
1335
|
+
return `has your edits; ${update2} merges nxds's changes into it, ${overwrite} replaces it`;
|
|
1336
|
+
}
|
|
1106
1337
|
const files = items.flatMap(
|
|
1107
|
-
(item) =>
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1338
|
+
(item) => variantFor(item, target).files.map((file) => {
|
|
1339
|
+
const state = fileState(cwd, config, file, previousFiles);
|
|
1340
|
+
return {
|
|
1341
|
+
item: item.name,
|
|
1342
|
+
path: file.path,
|
|
1343
|
+
state,
|
|
1344
|
+
advice: state === "current" ? null : advice(state, file)
|
|
1345
|
+
};
|
|
1346
|
+
})
|
|
1112
1347
|
);
|
|
1113
1348
|
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
1349
|
return {
|
|
1120
1350
|
data: { version: config.version, latest: latest.version, files, removed },
|
|
1121
1351
|
lines: [
|
|
1122
1352
|
...differing.map(
|
|
1123
|
-
(file) => `${file.state.padEnd(
|
|
1353
|
+
(file) => `${file.state.padEnd(10)} ${file.path} (${file.advice})`
|
|
1124
1354
|
),
|
|
1125
1355
|
...removed.map(
|
|
1126
|
-
(name) =>
|
|
1356
|
+
(name) => `${"removed".padEnd(10)} ${name} (no longer in nxds; \`${CLI_COMMAND} update\` drops it)`
|
|
1127
1357
|
),
|
|
1128
1358
|
...differing.length === 0 && removed.length === 0 ? [`Every file matches nxds ${latest.version}.`] : []
|
|
1129
1359
|
],
|
|
@@ -1133,7 +1363,7 @@ async function diff(cwd, registry, names) {
|
|
|
1133
1363
|
function tailwindMessage(project) {
|
|
1134
1364
|
const declared = project.dependencies.tailwindcss;
|
|
1135
1365
|
if (declared === void 0) {
|
|
1136
|
-
return
|
|
1366
|
+
return `Tailwind 4 is not installed. Run \`${CLI_COMMAND} init\`.`;
|
|
1137
1367
|
}
|
|
1138
1368
|
const tailwindMajor = majorVersion(project, "tailwindcss");
|
|
1139
1369
|
if (tailwindMajor === 4) return `tailwindcss ${declared}`;
|
|
@@ -1143,14 +1373,14 @@ function tailwindMessage(project) {
|
|
|
1143
1373
|
return `tailwindcss ${declared}. nxds needs Tailwind CSS 4.`;
|
|
1144
1374
|
}
|
|
1145
1375
|
function read2(root, file) {
|
|
1146
|
-
if (!file || !
|
|
1147
|
-
return readFileSync4(
|
|
1376
|
+
if (!file || !existsSync8(path9.join(root, file))) return "";
|
|
1377
|
+
return readFileSync4(path9.join(root, file), "utf8");
|
|
1148
1378
|
}
|
|
1149
1379
|
function sourcesNxdsDir(config, css) {
|
|
1150
|
-
const entryDir2 =
|
|
1380
|
+
const entryDir2 = path9.posix.dirname(config.css);
|
|
1151
1381
|
return [...css.matchAll(/@source\s+(['"])(.*?)\1/g)].some(([, , source]) => {
|
|
1152
1382
|
const base = (source ?? "").split(/[*{[]/)[0] ?? "";
|
|
1153
|
-
const folder =
|
|
1383
|
+
const folder = path9.posix.join(entryDir2, base).replace(/\/+$/, "");
|
|
1154
1384
|
return folder === "." || /^\.\.(\/\.\.)*$/.test(folder) || config.dir === folder || config.dir.startsWith(`${folder}/`);
|
|
1155
1385
|
});
|
|
1156
1386
|
}
|
|
@@ -1164,10 +1394,42 @@ function unscannedReason(cwd, config, css) {
|
|
|
1164
1394
|
}
|
|
1165
1395
|
return null;
|
|
1166
1396
|
}
|
|
1397
|
+
function installedMajor(project, name) {
|
|
1398
|
+
const version = installedVersion(project, name);
|
|
1399
|
+
return version === null ? null : major2(version);
|
|
1400
|
+
}
|
|
1401
|
+
function reactChecks(project, config) {
|
|
1402
|
+
let react;
|
|
1403
|
+
try {
|
|
1404
|
+
react = reactMajor(project);
|
|
1405
|
+
} catch (error) {
|
|
1406
|
+
if (!(error instanceof CliError)) throw error;
|
|
1407
|
+
return [{ name: "react", ok: false, message: error.message }];
|
|
1408
|
+
}
|
|
1409
|
+
const checks = [
|
|
1410
|
+
{
|
|
1411
|
+
name: "react",
|
|
1412
|
+
ok: config.react === react,
|
|
1413
|
+
message: config.react === react ? `The files are for React ${react}, which this app uses` : `The files are for React ${config.react}, and this app uses React ${react}. Run \`${CLI_COMMAND} update\` to switch them.`
|
|
1414
|
+
}
|
|
1415
|
+
];
|
|
1416
|
+
const reactInstalled = installedMajor(project, "react");
|
|
1417
|
+
if (reactInstalled === null) return checks;
|
|
1418
|
+
for (const name of ["react-dom", "@types/react", "@types/react-dom"]) {
|
|
1419
|
+
if (name.startsWith("@types/") && !(name in project.dependencies)) continue;
|
|
1420
|
+
const installed = installedMajor(project, name);
|
|
1421
|
+
checks.push({
|
|
1422
|
+
name,
|
|
1423
|
+
ok: installed === reactInstalled,
|
|
1424
|
+
message: installed === null ? `${name} is not installed` : installed === reactInstalled ? `${name} ${installed} matches react ${reactInstalled}` : `${name} ${installed} is installed with react ${reactInstalled}. Install the same major as react.`
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
return checks;
|
|
1428
|
+
}
|
|
1167
1429
|
function packageProblems(project, config, items) {
|
|
1168
1430
|
const needed = Object.assign(
|
|
1169
1431
|
{},
|
|
1170
|
-
...items.map((item) =>
|
|
1432
|
+
...items.map((item) => variantFor(item, config).packages.npm)
|
|
1171
1433
|
);
|
|
1172
1434
|
return Object.entries(needed).flatMap(([name, range]) => {
|
|
1173
1435
|
if (!(name in project.dependencies))
|
|
@@ -1190,7 +1452,7 @@ async function doctor(cwd, registry) {
|
|
|
1190
1452
|
check(
|
|
1191
1453
|
"config",
|
|
1192
1454
|
config !== null,
|
|
1193
|
-
config ? "nxds.json found" :
|
|
1455
|
+
config ? "nxds.json found" : `No nxds.json. Run \`${CLI_COMMAND} init\`.`
|
|
1194
1456
|
);
|
|
1195
1457
|
if (!project.setup) {
|
|
1196
1458
|
check("app", false, "Not a Next.js App Router or Vite + React app.");
|
|
@@ -1200,19 +1462,20 @@ async function doctor(cwd, registry) {
|
|
|
1200
1462
|
check(
|
|
1201
1463
|
"release",
|
|
1202
1464
|
catalog !== null,
|
|
1203
|
-
catalog ? `Files come from nxds ${config.version}` : `${registry} has no nxds ${config.version}. Run
|
|
1465
|
+
catalog ? `Files come from nxds ${config.version}` : `${registry} has no nxds ${config.version}. Run \`${CLI_COMMAND} update\`.`
|
|
1204
1466
|
);
|
|
1205
1467
|
check(
|
|
1206
1468
|
"tailwind",
|
|
1207
1469
|
majorVersion(project, "tailwindcss") === 4,
|
|
1208
1470
|
tailwindMessage(project)
|
|
1209
1471
|
);
|
|
1472
|
+
checks.push(...reactChecks(project, config));
|
|
1210
1473
|
const css = read2(cwd, config.css);
|
|
1211
1474
|
const importsTokens = css.includes("tailwind/nxds.css");
|
|
1212
1475
|
check(
|
|
1213
1476
|
"css entry",
|
|
1214
1477
|
importsTokens,
|
|
1215
|
-
!css ? `${config.css} is missing. Run
|
|
1478
|
+
!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.`
|
|
1216
1479
|
);
|
|
1217
1480
|
const unscanned = unscannedReason(cwd, config, css);
|
|
1218
1481
|
check(
|
|
@@ -1222,7 +1485,7 @@ async function doctor(cwd, registry) {
|
|
|
1222
1485
|
);
|
|
1223
1486
|
const entry = project.setup === "next-app" ? `${entryDir(project)}/layout.tsx` : findFile(project, VITE_ENTRIES);
|
|
1224
1487
|
const entrySource = read2(cwd, entry);
|
|
1225
|
-
const cssFile =
|
|
1488
|
+
const cssFile = path9.basename(config.css);
|
|
1226
1489
|
const importsCss = entrySource.includes(cssFile);
|
|
1227
1490
|
check(
|
|
1228
1491
|
"css import",
|
|
@@ -1239,31 +1502,43 @@ async function doctor(cwd, registry) {
|
|
|
1239
1502
|
check(
|
|
1240
1503
|
"theme script",
|
|
1241
1504
|
scriptSource.includes(scriptMarker),
|
|
1242
|
-
scriptSource.includes(scriptMarker) ? "The theme script runs before first paint" :
|
|
1505
|
+
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\`.`
|
|
1243
1506
|
);
|
|
1244
1507
|
if (!catalog) return report(checks);
|
|
1245
1508
|
const { known, removed } = splitInstalled(catalog, config.items);
|
|
1246
1509
|
check(
|
|
1247
1510
|
"items",
|
|
1248
1511
|
removed.length === 0,
|
|
1249
|
-
removed.length === 0 ? `${known.length} installed items, all in this release` : `No longer in nxds: ${removed.join(", ")}. Run
|
|
1512
|
+
removed.length === 0 ? `${known.length} installed items, all in this release` : `No longer in nxds: ${removed.join(", ")}. Run \`${CLI_COMMAND} update\`.`
|
|
1250
1513
|
);
|
|
1251
1514
|
const items = resolveItems(catalog, config.framework, known);
|
|
1252
1515
|
const missingFiles = items.flatMap(
|
|
1253
|
-
(item) =>
|
|
1254
|
-
(file) => !
|
|
1516
|
+
(item) => variantFor(item, config).files.filter(
|
|
1517
|
+
(file) => !existsSync8(path9.join(cwd, config.dir, file.path))
|
|
1255
1518
|
).map((file) => `${item.name}: ${file.path}`)
|
|
1256
1519
|
);
|
|
1257
1520
|
check(
|
|
1258
1521
|
"files",
|
|
1259
1522
|
missingFiles.length === 0,
|
|
1260
|
-
missingFiles.length === 0 ? `All files for ${known.length} installed items are present` : `Missing ${missingFiles.join(", ")}. Run
|
|
1523
|
+
missingFiles.length === 0 ? `All files for ${known.length} installed items are present` : `Missing ${missingFiles.join(", ")}. Run \`${CLI_COMMAND} add\` for those items.`
|
|
1524
|
+
);
|
|
1525
|
+
const conflicted = conflictedFiles(
|
|
1526
|
+
cwd,
|
|
1527
|
+
config,
|
|
1528
|
+
items.flatMap(
|
|
1529
|
+
(item) => variantFor(item, config).files.map((file) => file.path)
|
|
1530
|
+
)
|
|
1531
|
+
).map((file) => `${config.dir}/${file}`);
|
|
1532
|
+
check(
|
|
1533
|
+
"conflicts",
|
|
1534
|
+
conflicted.length === 0,
|
|
1535
|
+
conflicted.length === 0 ? "No conflict markers left from an update" : `Resolve the <<<<<<< markers in ${conflicted.join(", ")}.`
|
|
1261
1536
|
);
|
|
1262
1537
|
const problems = packageProblems(project, config, items);
|
|
1263
1538
|
check(
|
|
1264
1539
|
"packages",
|
|
1265
1540
|
problems.length === 0,
|
|
1266
|
-
problems.length === 0 ? "Every package the installed items need is installed in range, @nxds/core included" : `${problems.join("; ")}. Run
|
|
1541
|
+
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.`
|
|
1267
1542
|
);
|
|
1268
1543
|
return report(checks);
|
|
1269
1544
|
}
|
|
@@ -1282,16 +1557,29 @@ function report(checks) {
|
|
|
1282
1557
|
}
|
|
1283
1558
|
|
|
1284
1559
|
// src/commands/install.ts
|
|
1285
|
-
import { existsSync as
|
|
1286
|
-
import
|
|
1560
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
1561
|
+
import path11 from "path";
|
|
1287
1562
|
|
|
1288
1563
|
// src/install.ts
|
|
1289
|
-
import { existsSync as
|
|
1290
|
-
import
|
|
1564
|
+
import { existsSync as existsSync9, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
1565
|
+
import path10 from "path";
|
|
1566
|
+
function emptyInstallResult() {
|
|
1567
|
+
return {
|
|
1568
|
+
items: [],
|
|
1569
|
+
written: [],
|
|
1570
|
+
unchanged: [],
|
|
1571
|
+
merged: [],
|
|
1572
|
+
conflicted: [],
|
|
1573
|
+
unresolved: [],
|
|
1574
|
+
skipped: [],
|
|
1575
|
+
packages: [],
|
|
1576
|
+
cssImports: []
|
|
1577
|
+
};
|
|
1578
|
+
}
|
|
1291
1579
|
function addCssImports(project, config, files, dryRun) {
|
|
1292
1580
|
if (files.length === 0) return [];
|
|
1293
|
-
const entry =
|
|
1294
|
-
const current =
|
|
1581
|
+
const entry = path10.join(project.root, config.css);
|
|
1582
|
+
const current = existsSync9(entry) ? readFileSync5(entry, "utf8") : "";
|
|
1295
1583
|
const added = [...new Set(files)].map((file) => cssImportLine(config, file)).filter((line) => !current.includes(line));
|
|
1296
1584
|
if (added.length > 0 && !dryRun) {
|
|
1297
1585
|
writeFileSync3(entry, `${current.trimEnd()}
|
|
@@ -1302,7 +1590,7 @@ ${added.join("\n")}
|
|
|
1302
1590
|
}
|
|
1303
1591
|
function installItems(catalog, project, config, names, previous, options) {
|
|
1304
1592
|
const items = resolveItems(catalog, config.framework, names);
|
|
1305
|
-
const variants = items.map((item) =>
|
|
1593
|
+
const variants = items.map((item) => variantFor(item, config));
|
|
1306
1594
|
const files = new Map(
|
|
1307
1595
|
variants.flatMap((variant) => variant.files).map((file) => [file.path, file])
|
|
1308
1596
|
);
|
|
@@ -1311,7 +1599,7 @@ function installItems(catalog, project, config, names, previous, options) {
|
|
|
1311
1599
|
config,
|
|
1312
1600
|
[...files.values()],
|
|
1313
1601
|
previous,
|
|
1314
|
-
options
|
|
1602
|
+
{ ...options, version: catalog.version }
|
|
1315
1603
|
);
|
|
1316
1604
|
const packages = packagesToInstall(
|
|
1317
1605
|
project,
|
|
@@ -1347,6 +1635,15 @@ function installLines(config, result, options) {
|
|
|
1347
1635
|
const verb = dryRun ? "Would write" : "Wrote";
|
|
1348
1636
|
return [
|
|
1349
1637
|
...result.written.map((file) => `${verb} ${config.dir}/${file}`),
|
|
1638
|
+
...result.merged.map(
|
|
1639
|
+
(file) => `${dryRun ? "Would merge" : "Merged"} nxds's changes into your edits in ${config.dir}/${file}`
|
|
1640
|
+
),
|
|
1641
|
+
...result.conflicted.map(
|
|
1642
|
+
(file) => `${dryRun ? "Would conflict" : "Conflict"} in ${config.dir}/${file}: nxds changed lines you edited. Resolve the <<<<<<< markers, then run \`${CLI_COMMAND} update\` again.`
|
|
1643
|
+
),
|
|
1644
|
+
...result.unresolved.map(
|
|
1645
|
+
(file) => `Still conflicted: ${config.dir}/${file}. Resolve the <<<<<<< markers, then run \`${CLI_COMMAND} update\` again.`
|
|
1646
|
+
),
|
|
1350
1647
|
...result.skipped.map(
|
|
1351
1648
|
(file) => `Kept ${config.dir}/${file}: it differs from nxds's copy, so it may hold your edits. Pass --overwrite to replace it.`
|
|
1352
1649
|
),
|
|
@@ -1356,10 +1653,19 @@ function installLines(config, result, options) {
|
|
|
1356
1653
|
)
|
|
1357
1654
|
];
|
|
1358
1655
|
}
|
|
1656
|
+
function settled(result) {
|
|
1657
|
+
return result.skipped.length === 0 && result.conflicted.length === 0 && result.unresolved.length === 0;
|
|
1658
|
+
}
|
|
1659
|
+
function assertFilesMatchReact(config, react) {
|
|
1660
|
+
if (config.react === react) return;
|
|
1661
|
+
throw new CliError(
|
|
1662
|
+
`nxds.json has React ${config.react} files, and this app uses React ${react}. Run \`${CLI_COMMAND} update\` to switch them, then run this command again.`
|
|
1663
|
+
);
|
|
1664
|
+
}
|
|
1359
1665
|
function nxdsDir(dir) {
|
|
1360
|
-
const normalized =
|
|
1666
|
+
const normalized = relativePath(dir);
|
|
1361
1667
|
if (normalized) return normalized;
|
|
1362
|
-
throw new
|
|
1668
|
+
throw new UsageError(
|
|
1363
1669
|
`--dir takes a folder inside your app, such as components/nxds, not "${dir}".`
|
|
1364
1670
|
);
|
|
1365
1671
|
}
|
|
@@ -1380,7 +1686,7 @@ async function init(cwd, registry, options) {
|
|
|
1380
1686
|
const project = readProject(cwd);
|
|
1381
1687
|
if (!project.setup) {
|
|
1382
1688
|
throw new CliError(
|
|
1383
|
-
|
|
1689
|
+
`\`${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.`
|
|
1384
1690
|
);
|
|
1385
1691
|
}
|
|
1386
1692
|
if (!project.typescript) {
|
|
@@ -1388,11 +1694,17 @@ async function init(cwd, registry, options) {
|
|
|
1388
1694
|
"nxds components are TypeScript. Add a tsconfig.json to your app, then run init again."
|
|
1389
1695
|
);
|
|
1390
1696
|
}
|
|
1697
|
+
const react = reactMajor(project);
|
|
1391
1698
|
const addTailwind = !("tailwindcss" in project.dependencies);
|
|
1392
1699
|
if (!addTailwind) assertTailwind4(project);
|
|
1393
1700
|
const dir = options.dir ? nxdsDir(options.dir) : void 0;
|
|
1394
1701
|
const existing = readConfig(cwd);
|
|
1395
|
-
const catalog = await loadRelease(
|
|
1702
|
+
const catalog = await loadRelease(
|
|
1703
|
+
registry,
|
|
1704
|
+
existing?.version ?? "latest",
|
|
1705
|
+
react
|
|
1706
|
+
);
|
|
1707
|
+
if (existing) assertFilesMatchReact(existing, react);
|
|
1396
1708
|
if (existing && dir && dir !== existing.dir) {
|
|
1397
1709
|
throw new CliError(
|
|
1398
1710
|
`nxds is already set up in ${existing.dir}. Run init without --dir, or move the folder and change "dir" in nxds.json.`
|
|
@@ -1400,12 +1712,13 @@ async function init(cwd, registry, options) {
|
|
|
1400
1712
|
}
|
|
1401
1713
|
const config = existing ?? {
|
|
1402
1714
|
framework: "react",
|
|
1715
|
+
react,
|
|
1403
1716
|
version: catalog.version,
|
|
1404
1717
|
dir: dir ?? defaultDir(project),
|
|
1405
1718
|
css: cssEntryPath(project),
|
|
1406
1719
|
items: []
|
|
1407
1720
|
};
|
|
1408
|
-
const setup = setupVariant(catalog, config
|
|
1721
|
+
const setup = setupVariant(catalog, config);
|
|
1409
1722
|
const wiring = planWiring(project, config, addTailwind, setup.steps);
|
|
1410
1723
|
const tailwind = packagesToInstall(
|
|
1411
1724
|
project,
|
|
@@ -1419,23 +1732,24 @@ async function init(cwd, registry, options) {
|
|
|
1419
1732
|
project,
|
|
1420
1733
|
config,
|
|
1421
1734
|
[SETUP],
|
|
1422
|
-
|
|
1735
|
+
null,
|
|
1423
1736
|
options
|
|
1424
1737
|
);
|
|
1425
1738
|
if (!options.dryRun) {
|
|
1426
1739
|
for (const change of wiring.changes) {
|
|
1427
|
-
const target =
|
|
1428
|
-
mkdirSync2(
|
|
1740
|
+
const target = path11.join(project.root, change.path);
|
|
1741
|
+
mkdirSync2(path11.dirname(target), { recursive: true });
|
|
1429
1742
|
writeFileSync4(target, change.content);
|
|
1430
1743
|
}
|
|
1431
1744
|
writeConfig(cwd, config);
|
|
1432
1745
|
}
|
|
1433
1746
|
const packages = [...tailwind, ...installed.packages];
|
|
1434
1747
|
const [createVerb, editVerb] = options.dryRun ? ["Would create", "Would edit"] : ["Created", "Edited"];
|
|
1435
|
-
const done = wiring.manual.length === 0;
|
|
1748
|
+
const done = wiring.manual.length === 0 && settled(installed);
|
|
1436
1749
|
return {
|
|
1437
1750
|
data: {
|
|
1438
1751
|
setup: project.setup,
|
|
1752
|
+
react: config.react,
|
|
1439
1753
|
version: config.version,
|
|
1440
1754
|
dir: config.dir,
|
|
1441
1755
|
css: config.css,
|
|
@@ -1447,13 +1761,15 @@ async function init(cwd, registry, options) {
|
|
|
1447
1761
|
manual: wiring.manual
|
|
1448
1762
|
},
|
|
1449
1763
|
lines: [
|
|
1450
|
-
`nxds ${config.version} in ${config.dir}`,
|
|
1764
|
+
`nxds ${config.version} in ${config.dir}, for React ${config.react}`,
|
|
1451
1765
|
...installLines(config, { ...installed, packages }, options),
|
|
1452
1766
|
...wiring.changes.map(
|
|
1453
1767
|
(change) => `${change.created ? createVerb : editVerb} ${change.path}`
|
|
1454
1768
|
),
|
|
1455
1769
|
...manualLines(wiring.manual),
|
|
1456
|
-
...done && !options.dryRun ? [
|
|
1770
|
+
...done && !options.dryRun ? [
|
|
1771
|
+
`nxds is set up. Add a component with \`${CLI_COMMAND} add button\`.`
|
|
1772
|
+
] : []
|
|
1457
1773
|
],
|
|
1458
1774
|
ok: done
|
|
1459
1775
|
};
|
|
@@ -1461,22 +1777,21 @@ async function init(cwd, registry, options) {
|
|
|
1461
1777
|
function manualLines(manual) {
|
|
1462
1778
|
if (manual.length === 0) return [];
|
|
1463
1779
|
return [
|
|
1464
|
-
|
|
1780
|
+
`Finish these steps by hand, then run \`${CLI_COMMAND} doctor\`:`,
|
|
1465
1781
|
...manual.map(({ file, step }) => ` ${file}: ${step}`)
|
|
1466
1782
|
];
|
|
1467
1783
|
}
|
|
1468
1784
|
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
1785
|
const config = requireConfig(cwd);
|
|
1473
|
-
const
|
|
1786
|
+
const project = readProject(cwd);
|
|
1787
|
+
assertFilesMatchReact(config, reactMajor(project));
|
|
1788
|
+
const catalog = await loadRelease(registry, config.version, config.react);
|
|
1474
1789
|
const installed = installItems(
|
|
1475
1790
|
catalog,
|
|
1476
|
-
|
|
1791
|
+
project,
|
|
1477
1792
|
config,
|
|
1478
1793
|
names,
|
|
1479
|
-
|
|
1794
|
+
null,
|
|
1480
1795
|
options
|
|
1481
1796
|
);
|
|
1482
1797
|
if (!options.dryRun) writeConfig(cwd, config);
|
|
@@ -1484,61 +1799,85 @@ async function add(cwd, registry, names, options) {
|
|
|
1484
1799
|
data: { dryRun: options.dryRun, ...installed },
|
|
1485
1800
|
lines: [
|
|
1486
1801
|
...installLines(config, installed, options),
|
|
1487
|
-
...installed.written.length === 0 && installed
|
|
1802
|
+
...installed.written.length === 0 && settled(installed) ? [
|
|
1488
1803
|
`${names.join(", ")} ${names.length === 1 ? "is" : "are"} already up to date.`
|
|
1489
1804
|
] : []
|
|
1490
1805
|
],
|
|
1491
|
-
ok:
|
|
1806
|
+
ok: settled(installed)
|
|
1492
1807
|
};
|
|
1493
1808
|
}
|
|
1494
|
-
function filesOf(catalog,
|
|
1809
|
+
function filesOf(catalog, target, names) {
|
|
1495
1810
|
return new Set(
|
|
1496
|
-
resolveItems(catalog, framework, names).flatMap(
|
|
1497
|
-
(item) =>
|
|
1811
|
+
resolveItems(catalog, target.framework, names).flatMap(
|
|
1812
|
+
(item) => variantFor(item, target).files.map((file) => file.path)
|
|
1498
1813
|
)
|
|
1499
1814
|
);
|
|
1500
1815
|
}
|
|
1501
|
-
async function update(cwd, registry,
|
|
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
|
-
}
|
|
1816
|
+
async function update(cwd, registry, options) {
|
|
1507
1817
|
const config = requireConfig(cwd);
|
|
1508
|
-
const
|
|
1509
|
-
const
|
|
1818
|
+
const project = readProject(cwd);
|
|
1819
|
+
const from = config.version;
|
|
1820
|
+
const fromReact = config.react;
|
|
1821
|
+
const react = reactMajor(project);
|
|
1822
|
+
const latest = await loadRelease(registry, "latest", react);
|
|
1510
1823
|
const { known, removed } = splitInstalled(latest, config.items);
|
|
1511
|
-
const current = filesOf(latest, config
|
|
1512
|
-
const
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1824
|
+
const current = filesOf(latest, { ...config, react }, known);
|
|
1825
|
+
const previous = await fetchRelease(registry, from);
|
|
1826
|
+
const installedBefore = previous ? filesOf(previous, config, splitInstalled(previous, config.items).known) : /* @__PURE__ */ new Set();
|
|
1827
|
+
const unchanged = from === latest.version && fromReact === react;
|
|
1828
|
+
const unresolved = unchanged || options.overwrite ? [] : conflictedFiles(cwd, config, /* @__PURE__ */ new Set([...current, ...installedBefore]));
|
|
1829
|
+
if (unresolved.length > 0) {
|
|
1830
|
+
const data2 = {
|
|
1831
|
+
...emptyInstallResult(),
|
|
1832
|
+
dryRun: options.dryRun,
|
|
1833
|
+
from,
|
|
1834
|
+
version: from,
|
|
1835
|
+
fromReact,
|
|
1836
|
+
react: fromReact,
|
|
1837
|
+
unresolved,
|
|
1838
|
+
removed: [],
|
|
1839
|
+
orphaned: []
|
|
1840
|
+
};
|
|
1841
|
+
return {
|
|
1842
|
+
data: data2,
|
|
1843
|
+
lines: [
|
|
1844
|
+
...unresolved.map((file) => `Still conflicted: ${config.dir}/${file}`),
|
|
1845
|
+
`Changed nothing, and nxds.json stays on ${from}. Resolve the <<<<<<< markers, then run \`${CLI_COMMAND} update\` again.`
|
|
1846
|
+
],
|
|
1847
|
+
ok: false
|
|
1848
|
+
};
|
|
1849
|
+
}
|
|
1517
1850
|
const orphaned = [...installedBefore].filter(
|
|
1518
|
-
(file) => !current.has(file) &&
|
|
1851
|
+
(file) => !current.has(file) && existsSync10(path11.join(cwd, config.dir, file))
|
|
1519
1852
|
);
|
|
1853
|
+
const previousFiles = previous && releaseFiles(previous, config);
|
|
1520
1854
|
config.items = known;
|
|
1521
1855
|
const installed = installItems(
|
|
1522
1856
|
latest,
|
|
1523
|
-
|
|
1524
|
-
config,
|
|
1857
|
+
project,
|
|
1858
|
+
{ ...config, react },
|
|
1525
1859
|
known,
|
|
1526
|
-
|
|
1860
|
+
previousFiles,
|
|
1527
1861
|
options
|
|
1528
1862
|
);
|
|
1529
|
-
const
|
|
1530
|
-
|
|
1531
|
-
|
|
1863
|
+
const stays = !unchanged && installed.skipped.length > 0;
|
|
1864
|
+
if (!stays) {
|
|
1865
|
+
config.version = latest.version;
|
|
1866
|
+
config.react = react;
|
|
1867
|
+
}
|
|
1532
1868
|
if (!options.dryRun) writeConfig(cwd, config);
|
|
1869
|
+
const data = {
|
|
1870
|
+
dryRun: options.dryRun,
|
|
1871
|
+
from,
|
|
1872
|
+
version: config.version,
|
|
1873
|
+
fromReact,
|
|
1874
|
+
react: config.react,
|
|
1875
|
+
...installed,
|
|
1876
|
+
removed,
|
|
1877
|
+
orphaned
|
|
1878
|
+
};
|
|
1533
1879
|
return {
|
|
1534
|
-
data
|
|
1535
|
-
dryRun: options.dryRun,
|
|
1536
|
-
from,
|
|
1537
|
-
version: config.version,
|
|
1538
|
-
...installed,
|
|
1539
|
-
removed,
|
|
1540
|
-
orphaned
|
|
1541
|
-
},
|
|
1880
|
+
data,
|
|
1542
1881
|
lines: [
|
|
1543
1882
|
...previous ? [] : [
|
|
1544
1883
|
`${registry} has no nxds ${from}, so every file that differs from ${latest.version} counts as changed.`
|
|
@@ -1550,121 +1889,187 @@ async function update(cwd, registry, names, options) {
|
|
|
1550
1889
|
...orphaned.map(
|
|
1551
1890
|
(file) => `No installed item uses ${config.dir}/${file} any more. Delete it once nothing in your app imports it.`
|
|
1552
1891
|
),
|
|
1553
|
-
|
|
1892
|
+
...fromReact !== react && !stays ? [
|
|
1893
|
+
`${options.dryRun ? "Would switch" : "Switched"} the files from React ${fromReact} to React ${react}.`
|
|
1894
|
+
] : [],
|
|
1895
|
+
stays ? `nxds.json stays on ${from} until every file above is resolved or replaced.` : `nxds ${from === latest.version ? "is on" : `${options.dryRun ? "would move" : "moved"} from ${from} to`} ${latest.version}.`
|
|
1554
1896
|
],
|
|
1555
|
-
ok:
|
|
1897
|
+
ok: settled(installed)
|
|
1556
1898
|
};
|
|
1557
1899
|
}
|
|
1558
1900
|
|
|
1559
|
-
// src/
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
--overwrite
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
json
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
);
|
|
1604
|
-
|
|
1901
|
+
// src/cli.ts
|
|
1902
|
+
function context(command) {
|
|
1903
|
+
const { cwd, registry } = command.optsWithGlobals();
|
|
1904
|
+
return {
|
|
1905
|
+
cwd: path12.resolve(cwd ?? process.cwd()),
|
|
1906
|
+
registry: resolveRegistry(registry)
|
|
1907
|
+
};
|
|
1908
|
+
}
|
|
1909
|
+
function installOptions(command) {
|
|
1910
|
+
const flags = command.opts();
|
|
1911
|
+
return {
|
|
1912
|
+
overwrite: flags.overwrite ?? false,
|
|
1913
|
+
dryRun: flags.dryRun ?? false,
|
|
1914
|
+
install: flags.install
|
|
1915
|
+
};
|
|
1916
|
+
}
|
|
1917
|
+
function withInstallFlags(command) {
|
|
1918
|
+
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");
|
|
1919
|
+
}
|
|
1920
|
+
function program(done, write) {
|
|
1921
|
+
const cli = new Command().configureOutput({
|
|
1922
|
+
writeOut: write,
|
|
1923
|
+
writeErr: write,
|
|
1924
|
+
// Errors are printed once, by `main`, as text or JSON.
|
|
1925
|
+
outputError: () => {
|
|
1926
|
+
}
|
|
1927
|
+
}).exitOverride().name(CLI_COMMAND).description("Add nxds to your app.").version("0.3.0", "-v, --version", "print the version").helpOption("-h, --help", "show help").helpCommand("help [command]", "show help for a command").option("--json", "print machine-readable JSON").option("--cwd <path>", "run in another folder").addOption(
|
|
1928
|
+
new Option(
|
|
1929
|
+
"--registry <url|path>",
|
|
1930
|
+
"where to get nxds releases (default: https://nxds.dev/r)"
|
|
1931
|
+
).env("NXDS_REGISTRY_URL")
|
|
1932
|
+
).configureHelp({ showGlobalOptions: true }).addHelpText(
|
|
1933
|
+
"after",
|
|
1934
|
+
`
|
|
1935
|
+
Exit codes: 0 done, 1 error, 2 usage error, 3 done with something left for you (conflicts, kept files, manual steps, failed checks).
|
|
1936
|
+
Docs: https://nxds.dev/getting-started/install`
|
|
1937
|
+
);
|
|
1938
|
+
withInstallFlags(
|
|
1939
|
+
cli.command("init").description("set up nxds: token CSS, provider, and the layout wiring").option(
|
|
1940
|
+
"--dir <path>",
|
|
1941
|
+
"nxds folder (default: components/nxds, or src/components/nxds)"
|
|
1942
|
+
)
|
|
1943
|
+
).action(async (_options, command) => {
|
|
1944
|
+
const { cwd, registry } = context(command);
|
|
1945
|
+
const { dir } = command.opts();
|
|
1946
|
+
done(await init(cwd, registry, { ...installOptions(command), dir }));
|
|
1947
|
+
});
|
|
1948
|
+
withInstallFlags(
|
|
1949
|
+
cli.command("add").description("add components or blocks, and everything they need").argument("<items...>", "item names, such as button")
|
|
1950
|
+
).action(async (items, _options, command) => {
|
|
1951
|
+
const { cwd, registry } = context(command);
|
|
1952
|
+
done(await add(cwd, registry, items, installOptions(command)));
|
|
1953
|
+
});
|
|
1954
|
+
withInstallFlags(
|
|
1955
|
+
cli.command("update").description(
|
|
1956
|
+
"update every installed item to the latest release, merging nxds changes into your edits"
|
|
1957
|
+
)
|
|
1958
|
+
).action(async (_options, command) => {
|
|
1959
|
+
const { cwd, registry } = context(command);
|
|
1960
|
+
done(await update(cwd, registry, installOptions(command)));
|
|
1961
|
+
});
|
|
1962
|
+
cli.command("list").description("list components and blocks").addOption(
|
|
1963
|
+
new Option("--kind <kind>", "only this kind").choices([
|
|
1964
|
+
"component",
|
|
1965
|
+
"block"
|
|
1966
|
+
])
|
|
1967
|
+
).action(async (_options, command) => {
|
|
1968
|
+
const { cwd, registry } = context(command);
|
|
1969
|
+
const { kind } = command.opts();
|
|
1970
|
+
done(await list(cwd, registry, kind));
|
|
1971
|
+
});
|
|
1972
|
+
cli.command("search").description("find items by name or description").argument("<term...>", "words to look for").action(async (term, _options, command) => {
|
|
1973
|
+
const { cwd, registry } = context(command);
|
|
1974
|
+
done(await search(cwd, registry, term.join(" ")));
|
|
1975
|
+
});
|
|
1976
|
+
cli.command("view").description("show an item's files, packages and docs link").argument("<item>", "item name").action(async (item, _options, command) => {
|
|
1977
|
+
const { cwd, registry } = context(command);
|
|
1978
|
+
done(await view(cwd, registry, item));
|
|
1979
|
+
});
|
|
1980
|
+
cli.command("info").description("show what the CLI detects in this app").action((_options, command) => {
|
|
1981
|
+
done(info(context(command).cwd));
|
|
1982
|
+
});
|
|
1983
|
+
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) => {
|
|
1984
|
+
const { cwd, registry } = context(command);
|
|
1985
|
+
done(await diff(cwd, registry, items));
|
|
1986
|
+
});
|
|
1987
|
+
cli.command("doctor").description("check that nxds is wired up correctly").action(async (_options, command) => {
|
|
1988
|
+
const { cwd, registry } = context(command);
|
|
1989
|
+
done(await doctor(cwd, registry));
|
|
1990
|
+
});
|
|
1991
|
+
return cli;
|
|
1605
1992
|
}
|
|
1606
|
-
function text
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
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"]
|
|
1993
|
+
function printed(text, key) {
|
|
1994
|
+
const trimmed = text.trimEnd();
|
|
1995
|
+
return {
|
|
1996
|
+
exitCode: EXIT_CODES.ok,
|
|
1997
|
+
data: { [key]: trimmed },
|
|
1998
|
+
lines: [trimmed]
|
|
1620
1999
|
};
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
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
|
-
);
|
|
2000
|
+
}
|
|
2001
|
+
function commanderOutcome(error, printedText) {
|
|
2002
|
+
if (error.code === "commander.version") {
|
|
2003
|
+
return printed(printedText, "version");
|
|
1644
2004
|
}
|
|
2005
|
+
if (error.exitCode === 0) return printed(printedText, "help");
|
|
2006
|
+
if (error.code === "commander.help") {
|
|
2007
|
+
return { exitCode: EXIT_CODES.usage, error: printedText.trimEnd() };
|
|
2008
|
+
}
|
|
2009
|
+
return {
|
|
2010
|
+
exitCode: EXIT_CODES.usage,
|
|
2011
|
+
error: `${error.message.replace(/^error: /, "")}
|
|
2012
|
+
Run \`${CLI_COMMAND} --help\` for the commands and options.`
|
|
2013
|
+
};
|
|
1645
2014
|
}
|
|
1646
|
-
async function
|
|
1647
|
-
|
|
2015
|
+
async function outcome(argv) {
|
|
2016
|
+
let result;
|
|
2017
|
+
let printedText = "";
|
|
2018
|
+
const cli = program(
|
|
2019
|
+
(done) => {
|
|
2020
|
+
result = done;
|
|
2021
|
+
},
|
|
2022
|
+
(text) => {
|
|
2023
|
+
printedText += text;
|
|
2024
|
+
}
|
|
2025
|
+
);
|
|
1648
2026
|
try {
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
2027
|
+
await cli.parseAsync(argv, { from: "user" });
|
|
2028
|
+
if (!result) throw new Error("The command stored no result.");
|
|
2029
|
+
return {
|
|
2030
|
+
exitCode: result.ok ? EXIT_CODES.ok : EXIT_CODES.attention,
|
|
2031
|
+
data: result.data,
|
|
2032
|
+
lines: result.lines
|
|
2033
|
+
};
|
|
1655
2034
|
} catch (error) {
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
`
|
|
1662
|
-
);
|
|
1663
|
-
} else if (error instanceof CliError) {
|
|
1664
|
-
console.error(message);
|
|
1665
|
-
} else {
|
|
1666
|
-
console.error(error);
|
|
2035
|
+
if (error instanceof CommanderError) {
|
|
2036
|
+
return commanderOutcome(error, printedText);
|
|
2037
|
+
}
|
|
2038
|
+
if (error instanceof CliError) {
|
|
2039
|
+
return { exitCode: error.exitCode, error: error.message };
|
|
1667
2040
|
}
|
|
2041
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2042
|
+
const stack = error instanceof Error ? error.stack : void 0;
|
|
2043
|
+
return {
|
|
2044
|
+
exitCode: EXIT_CODES.error,
|
|
2045
|
+
error: message,
|
|
2046
|
+
stack: stack ?? message
|
|
2047
|
+
};
|
|
1668
2048
|
}
|
|
1669
2049
|
}
|
|
1670
|
-
|
|
2050
|
+
async function main(argv, output) {
|
|
2051
|
+
const json = argv.includes("--json");
|
|
2052
|
+
const result = await outcome(argv);
|
|
2053
|
+
const ok = result.exitCode === EXIT_CODES.ok;
|
|
2054
|
+
if (json) {
|
|
2055
|
+
const body = "error" in result ? { ok: false, exitCode: result.exitCode, error: result.error } : { ok, exitCode: result.exitCode, data: result.data };
|
|
2056
|
+
output.out(`${JSON.stringify(body, null, 2)}
|
|
2057
|
+
`);
|
|
2058
|
+
} else if ("lines" in result) {
|
|
2059
|
+
output.out(`${result.lines.join("\n")}
|
|
2060
|
+
`);
|
|
2061
|
+
} else if (result.stack !== void 0) {
|
|
2062
|
+
output.err(`${result.stack}
|
|
2063
|
+
`);
|
|
2064
|
+
} else {
|
|
2065
|
+
output.err(`${result.error}
|
|
2066
|
+
`);
|
|
2067
|
+
}
|
|
2068
|
+
return result.exitCode;
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
// src/index.ts
|
|
2072
|
+
process.exitCode = await main(process.argv.slice(2), {
|
|
2073
|
+
out: (text) => process.stdout.write(text),
|
|
2074
|
+
err: (text) => process.stderr.write(text)
|
|
2075
|
+
});
|