@mizen-labs/cli 0.0.0-stage → 0.1.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/README.md +351 -2
- package/dist/catalog.js +1 -0
- package/dist/catalog.json +8827 -0
- package/dist/cli.js +212 -0
- package/dist/discover.js +108 -0
- package/dist/doctor/doctor.js +44 -0
- package/dist/doctor/files.js +77 -0
- package/dist/doctor/packages.js +114 -0
- package/dist/doctor/project.js +133 -0
- package/dist/doctor/report.js +94 -0
- package/dist/doctor/setup.js +107 -0
- package/dist/doctor/syntax.js +148 -0
- package/dist/doctor/workspace-config.js +210 -0
- package/dist/doctor/workspace.js +169 -0
- package/dist/errors.js +81 -0
- package/dist/help.js +192 -0
- package/dist/init/config.js +75 -0
- package/dist/init/dependencies.js +130 -0
- package/dist/init/edit.js +119 -0
- package/dist/init/imports.js +83 -0
- package/dist/init/init.js +193 -0
- package/dist/init/plan.js +88 -0
- package/dist/init/release.js +60 -0
- package/dist/init/styles.js +96 -0
- package/dist/init/theme.js +176 -0
- package/dist/init/transaction.js +72 -0
- package/dist/output.js +78 -0
- package/dist/vite/bootstrap.js +43 -0
- package/dist/vite/doctor.js +50 -0
- package/dist/vite/entry.js +46 -0
- package/dist/vite/init.js +66 -0
- package/dist/vite/render.js +144 -0
- package/package.json +56 -3
- package/schema/v1.json +684 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { lstat } from "node:fs/promises";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { row } from "../output.js";
|
|
4
|
+
import { doctor } from "./doctor.js";
|
|
5
|
+
import { exists, manifestAt } from "./files.js";
|
|
6
|
+
import { frameworkOf } from "./project.js";
|
|
7
|
+
import { check, formatDiagnostic, report, summaryLine, } from "./report.js";
|
|
8
|
+
import { findWorkspace, linkedProject, memberPath, WorkspaceConfigError, workspaceMembers, } from "./workspace-config.js";
|
|
9
|
+
function skipped(workspace, project, name, reason, uncertain = false) {
|
|
10
|
+
return {
|
|
11
|
+
...(uncertain
|
|
12
|
+
? {
|
|
13
|
+
diagnostic: check("workspace.package", "unverified", reason, [project], "Inspect this package explicitly with mizen doctor --cwd <APP_DIR>."),
|
|
14
|
+
}
|
|
15
|
+
: {}),
|
|
16
|
+
skipped: {
|
|
17
|
+
name,
|
|
18
|
+
path: memberPath(workspace.project, project),
|
|
19
|
+
project,
|
|
20
|
+
reason,
|
|
21
|
+
status: uncertain ? "unverified" : "skipped",
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function uncertainFramework(manifest) {
|
|
26
|
+
const dependencies = {
|
|
27
|
+
...manifest.dependencies,
|
|
28
|
+
...manifest.devDependencies,
|
|
29
|
+
};
|
|
30
|
+
return [
|
|
31
|
+
"next",
|
|
32
|
+
"@tanstack/react-start",
|
|
33
|
+
"astro",
|
|
34
|
+
"@react-router/dev",
|
|
35
|
+
"@sveltejs/kit",
|
|
36
|
+
].some((name) => name in dependencies);
|
|
37
|
+
}
|
|
38
|
+
async function inspectMember(workspace, project, nodeRange) {
|
|
39
|
+
const path = join(project, "package.json");
|
|
40
|
+
try {
|
|
41
|
+
if ((await lstat(project)).isSymbolicLink() ||
|
|
42
|
+
(await linkedProject(project))) {
|
|
43
|
+
return skipped(workspace, project, null, "Linked package directories are not inspected implicitly.", true);
|
|
44
|
+
}
|
|
45
|
+
const manifest = await manifestAt(path);
|
|
46
|
+
if (!manifest) {
|
|
47
|
+
return {};
|
|
48
|
+
}
|
|
49
|
+
return inspectApp(workspace, project, manifest, nodeRange);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return {
|
|
53
|
+
diagnostic: check("workspace.manifest", "error", "A workspace package manifest or directory is invalid or unreadable.", [path], "Repair this package's manifest or directory, then rerun mizen doctor --workspace."),
|
|
54
|
+
skipped: {
|
|
55
|
+
name: null,
|
|
56
|
+
path: memberPath(workspace.project, project),
|
|
57
|
+
project,
|
|
58
|
+
reason: "The package cannot be classified because its metadata is unavailable.",
|
|
59
|
+
status: "unverified",
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async function inspectApp(workspace, project, manifest, nodeRange) {
|
|
65
|
+
const framework = frameworkOf(manifest);
|
|
66
|
+
if (framework === "unknown") {
|
|
67
|
+
return skipped(workspace, project, manifest.name ?? null, uncertainFramework(manifest)
|
|
68
|
+
? "The package declares an unsupported or ambiguous application framework."
|
|
69
|
+
: "No supported application framework is declared.", uncertainFramework(manifest));
|
|
70
|
+
}
|
|
71
|
+
if (framework === "vite" && !(await exists(join(project, "index.html")))) {
|
|
72
|
+
return skipped(workspace, project, manifest.name ?? null, "React and Vite are declared without index.html; this may be a library or a custom app.", true);
|
|
73
|
+
}
|
|
74
|
+
const result = await doctor(project, nodeRange, {
|
|
75
|
+
allowWorkspaceRoot: true,
|
|
76
|
+
}).catch(() => report(project, [
|
|
77
|
+
check("workspace.inspection", "error", "The application could not be inspected.", [project], "Run mizen doctor --cwd <APP_DIR> to inspect this failure separately."),
|
|
78
|
+
]));
|
|
79
|
+
return {
|
|
80
|
+
app: {
|
|
81
|
+
...result,
|
|
82
|
+
framework,
|
|
83
|
+
name: manifest.name ?? null,
|
|
84
|
+
path: memberPath(workspace.project, project),
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function aggregate(workspace, diagnostics, members) {
|
|
89
|
+
const apps = members.flatMap((member) => (member.app ? [member.app] : []));
|
|
90
|
+
const skippedPackages = members.flatMap((member) => member.skipped ? [member.skipped] : []);
|
|
91
|
+
const all = [
|
|
92
|
+
...diagnostics,
|
|
93
|
+
...members.flatMap((member) => member.diagnostic ? [member.diagnostic] : []),
|
|
94
|
+
...apps.flatMap((app) => app.diagnostics.map((item) => ({ ...item, project: app.project }))),
|
|
95
|
+
];
|
|
96
|
+
if (!apps.length) {
|
|
97
|
+
all.push(check("workspace.apps", "unverified", "No supported applications were inspected.", [workspace.source], "Check workspace patterns and skipped packages, or inspect an app with mizen doctor --cwd <APP_DIR>."));
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
...report(workspace.project, all),
|
|
101
|
+
apps,
|
|
102
|
+
skipped: skippedPackages,
|
|
103
|
+
workspace: {
|
|
104
|
+
kind: workspace.kind,
|
|
105
|
+
patterns: workspace.patterns,
|
|
106
|
+
source: workspace.source,
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
export async function workspaceDoctor(cwd, nodeRange) {
|
|
111
|
+
try {
|
|
112
|
+
const workspace = await findWorkspace(cwd);
|
|
113
|
+
const { diagnostics, projects } = await workspaceMembers(workspace);
|
|
114
|
+
const members = [];
|
|
115
|
+
for (const project of projects) {
|
|
116
|
+
// biome-ignore lint/performance/noAwaitInLoops: Keep filesystem inspection bounded and report apps independently.
|
|
117
|
+
members.push(await inspectMember(workspace, project, nodeRange));
|
|
118
|
+
}
|
|
119
|
+
return aggregate(workspace, diagnostics, members);
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
const diagnostic = error instanceof WorkspaceConfigError
|
|
123
|
+
? error.diagnostic
|
|
124
|
+
: check("workspace.discovery", "error", "The workspace could not be read.", [resolve(cwd)], "Check directory permissions and workspace configuration, then rerun mizen doctor --workspace.");
|
|
125
|
+
return {
|
|
126
|
+
...report(resolve(cwd), [diagnostic]),
|
|
127
|
+
apps: [],
|
|
128
|
+
skipped: [],
|
|
129
|
+
workspace: null,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function appLabel(app) {
|
|
134
|
+
return `${app.path} (${app.framework}${app.name ? `, ${app.name}` : ""})`;
|
|
135
|
+
}
|
|
136
|
+
export function formatWorkspace(style, result) {
|
|
137
|
+
const appFindings = result.apps.flatMap((app) => {
|
|
138
|
+
const findings = app.diagnostics.filter((item) => item.status !== "pass");
|
|
139
|
+
return findings.length
|
|
140
|
+
? [
|
|
141
|
+
style.bold(appLabel(app)),
|
|
142
|
+
...findings.flatMap((item) => formatDiagnostic(style, item, app.path)),
|
|
143
|
+
]
|
|
144
|
+
: [];
|
|
145
|
+
});
|
|
146
|
+
const rootFindings = result.diagnostics.filter((item) => !("project" in item) && item.status !== "pass");
|
|
147
|
+
return [
|
|
148
|
+
row("Workspace", result.project ?? "Not found"),
|
|
149
|
+
...(result.workspace ? [row("Declared in", result.workspace.source)] : []),
|
|
150
|
+
"",
|
|
151
|
+
...(result.apps.length
|
|
152
|
+
? [
|
|
153
|
+
style.bold("Apps"),
|
|
154
|
+
...result.apps.map((app) => row(`${app.status[0]?.toUpperCase()}${app.status.slice(1)}`, appLabel(app))),
|
|
155
|
+
"",
|
|
156
|
+
]
|
|
157
|
+
: []),
|
|
158
|
+
...rootFindings.flatMap((item) => formatDiagnostic(style, item)),
|
|
159
|
+
...appFindings,
|
|
160
|
+
...(result.skipped.length
|
|
161
|
+
? [
|
|
162
|
+
style.bold("Skipped"),
|
|
163
|
+
...result.skipped.map((item) => row(item.path, item.reason)),
|
|
164
|
+
"",
|
|
165
|
+
]
|
|
166
|
+
: []),
|
|
167
|
+
summaryLine(style, result),
|
|
168
|
+
].join("\n");
|
|
169
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { nextStep, row, status } from "./output.js";
|
|
2
|
+
const SAFE = /^[\w./:=@<>-]+$/u;
|
|
3
|
+
/** argv as one command line, quoting what the shell would split. */
|
|
4
|
+
export function commandLine(argv) {
|
|
5
|
+
return argv
|
|
6
|
+
.map((arg) => (SAFE.test(arg) ? arg : `'${arg.replaceAll("'", "'\\''")}'`))
|
|
7
|
+
.join(" ");
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* A failure with one class. `reason` is a stable snake_case code; the exit
|
|
11
|
+
* code is 2 for usage and missing input, 1 for every other failure.
|
|
12
|
+
*/
|
|
13
|
+
export class CliError extends Error {
|
|
14
|
+
reason;
|
|
15
|
+
exitCode;
|
|
16
|
+
detail;
|
|
17
|
+
constructor(reason, message, exitCode, detail = {}) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.reason = reason;
|
|
20
|
+
this.exitCode = exitCode;
|
|
21
|
+
this.detail = detail;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function seeHelp(command) {
|
|
25
|
+
return {
|
|
26
|
+
argv: ["mizen", ...(command ? [command] : []), "--help"],
|
|
27
|
+
benefit: "See usage",
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export function invalidUsage(message, command, reason = "invalid_arguments") {
|
|
31
|
+
throw new CliError(reason, message, 2, { next: seeHelp(command) });
|
|
32
|
+
}
|
|
33
|
+
const UNKNOWN_OPTION = /Unknown option '(?<option>[^']+)'/u;
|
|
34
|
+
export function normalizeError(error, command) {
|
|
35
|
+
if (error instanceof CliError) {
|
|
36
|
+
return error;
|
|
37
|
+
}
|
|
38
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
39
|
+
const code = error instanceof TypeError && "code" in error ? String(error.code) : "";
|
|
40
|
+
const where = `mizen${command ? ` ${command}` : ""}`;
|
|
41
|
+
if (code === "ERR_PARSE_ARGS_UNKNOWN_OPTION") {
|
|
42
|
+
const option = UNKNOWN_OPTION.exec(message)?.[1] ?? "option";
|
|
43
|
+
return new CliError("unknown_option", `Unknown option ${option} for ${where}.`, 2, { next: seeHelp(command) });
|
|
44
|
+
}
|
|
45
|
+
if (code.startsWith("ERR_PARSE_ARGS")) {
|
|
46
|
+
return new CliError("invalid_arguments", `Can't read the options for ${where}. ${message}`, 2, { next: seeHelp(command) });
|
|
47
|
+
}
|
|
48
|
+
return new CliError("internal_error", `Unexpected error in ${where}. This is a bug in Mizen CLI.`, 1, {
|
|
49
|
+
next: {
|
|
50
|
+
argv: ["mizen", "--version"],
|
|
51
|
+
benefit: "Report it with your version",
|
|
52
|
+
},
|
|
53
|
+
rows: [["Cause", message]],
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
/** The human error block, for stderr. */
|
|
57
|
+
export function formatError(style, failure) {
|
|
58
|
+
const { next, rows = [] } = failure.detail;
|
|
59
|
+
return [
|
|
60
|
+
status(style, "fail", failure.message),
|
|
61
|
+
...rows.map(([label, value]) => row(label, value)),
|
|
62
|
+
...(next
|
|
63
|
+
? ["", nextStep(style, next.benefit, commandLine(next.argv))]
|
|
64
|
+
: []),
|
|
65
|
+
].join("\n");
|
|
66
|
+
}
|
|
67
|
+
/** The machine error document: one line on stderr. */
|
|
68
|
+
export function errorDocument(failure) {
|
|
69
|
+
const { data, next } = failure.detail;
|
|
70
|
+
return {
|
|
71
|
+
error: {
|
|
72
|
+
...(data ? { data } : {}),
|
|
73
|
+
message: failure.message,
|
|
74
|
+
...(next
|
|
75
|
+
? { next: [{ argv: next.argv, command: commandLine(next.argv) }] }
|
|
76
|
+
: {}),
|
|
77
|
+
reason: failure.reason,
|
|
78
|
+
},
|
|
79
|
+
schemaVersion: 1,
|
|
80
|
+
};
|
|
81
|
+
}
|
package/dist/help.js
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { CliError, invalidUsage } from "./errors.js";
|
|
2
|
+
export const argumentOptions = {
|
|
3
|
+
cwd: { type: "string" },
|
|
4
|
+
"dry-run": { type: "boolean" },
|
|
5
|
+
feature: { multiple: true, type: "string" },
|
|
6
|
+
help: { short: "h", type: "boolean" },
|
|
7
|
+
json: { type: "boolean" },
|
|
8
|
+
"no-color": { type: "boolean" },
|
|
9
|
+
package: { type: "string" },
|
|
10
|
+
"package-manager": { type: "string" },
|
|
11
|
+
strict: { type: "boolean" },
|
|
12
|
+
version: { short: "v", type: "boolean" },
|
|
13
|
+
workspace: { type: "boolean" },
|
|
14
|
+
yes: { type: "boolean" },
|
|
15
|
+
};
|
|
16
|
+
const optionHelp = {
|
|
17
|
+
cwd: ["--cwd <DIR>", "Select the app directory (default: current directory)"],
|
|
18
|
+
"dry-run": [
|
|
19
|
+
"--dry-run",
|
|
20
|
+
"Print the plan without applying it, even with --yes",
|
|
21
|
+
],
|
|
22
|
+
feature: [
|
|
23
|
+
"--feature <FEATURE>",
|
|
24
|
+
"Add an optional feature, repeatable. Accepts: diagram",
|
|
25
|
+
],
|
|
26
|
+
help: ["-h, --help", "Show help"],
|
|
27
|
+
json: ["--json", "Print one JSON document (schema version 1)"],
|
|
28
|
+
"no-color": ["--no-color", "Turn off color"],
|
|
29
|
+
package: [
|
|
30
|
+
"--package <FILE>",
|
|
31
|
+
"Install @mizen-labs/ui from a local .tgz archive",
|
|
32
|
+
],
|
|
33
|
+
"package-manager": [
|
|
34
|
+
"--package-manager <NAME>",
|
|
35
|
+
"Use npm or pnpm when the app declares neither",
|
|
36
|
+
],
|
|
37
|
+
strict: ["--strict", "Fail when any check is unverified"],
|
|
38
|
+
version: ["-v, --version", "Show the version"],
|
|
39
|
+
workspace: ["--workspace", "Check every app in the nearest workspace"],
|
|
40
|
+
yes: ["--yes", "Apply the plan. Conflicts still stop init"],
|
|
41
|
+
};
|
|
42
|
+
const globals = ["help", "json", "no-color", "version"];
|
|
43
|
+
const commands = {
|
|
44
|
+
doctor: {
|
|
45
|
+
examples: [
|
|
46
|
+
"mizen doctor --cwd ./apps/web",
|
|
47
|
+
"mizen doctor --workspace --strict",
|
|
48
|
+
],
|
|
49
|
+
group: "Setup",
|
|
50
|
+
notes: [
|
|
51
|
+
"Checks Next.js App Router, TanStack Start, and React + Vite apps.",
|
|
52
|
+
"With --workspace, --cwd selects where to look for the workspace root.",
|
|
53
|
+
"Linked, external, and ambiguous packages stay unverified.",
|
|
54
|
+
"Exits 1 when a check fails, or with --strict when one is unverified.",
|
|
55
|
+
],
|
|
56
|
+
options: ["cwd", "strict", "workspace"],
|
|
57
|
+
summary: "Check an app's Mizen setup without changing files",
|
|
58
|
+
usage: "mizen doctor [options]",
|
|
59
|
+
},
|
|
60
|
+
info: {
|
|
61
|
+
examples: ["mizen info button", "mizen info button --json"],
|
|
62
|
+
group: "Components",
|
|
63
|
+
notes: [
|
|
64
|
+
"Accepts a component slug or name.",
|
|
65
|
+
"Describes the bundled catalog, which may differ from your installed version.",
|
|
66
|
+
"Works offline and never reads your project.",
|
|
67
|
+
],
|
|
68
|
+
options: [],
|
|
69
|
+
summary: "Show a component's API, imports, guidance, and examples",
|
|
70
|
+
usage: "mizen info <COMPONENT> [options]",
|
|
71
|
+
},
|
|
72
|
+
init: {
|
|
73
|
+
examples: [
|
|
74
|
+
"mizen init --cwd ./apps/web",
|
|
75
|
+
"mizen init --cwd ./apps/web --yes",
|
|
76
|
+
"mizen init --cwd ./apps/web --package ./mizen-ui-0.1.0.tgz --yes",
|
|
77
|
+
],
|
|
78
|
+
group: "Setup",
|
|
79
|
+
notes: [
|
|
80
|
+
"Prints a plan by default. Only --yes applies it, and --dry-run always wins.",
|
|
81
|
+
"Supports Next.js App Router, TanStack Start, and React + Vite apps.",
|
|
82
|
+
"The package manager must match the app's declaration and lockfile.",
|
|
83
|
+
"Installs skip lifecycle scripts.",
|
|
84
|
+
"Until @mizen-labs/ui is published, pass a matching local archive with --package.",
|
|
85
|
+
"--yes never overwrites a conflict. If init stops midway,",
|
|
86
|
+
".mizen-init-journal.json records each edit. Restore or finish them,",
|
|
87
|
+
"then remove the journal.",
|
|
88
|
+
"Exits 1 on a conflict, or when an install or check fails.",
|
|
89
|
+
],
|
|
90
|
+
options: ["cwd", "dry-run", "feature", "package", "package-manager", "yes"],
|
|
91
|
+
summary: "Plan or apply Mizen setup in an app",
|
|
92
|
+
usage: "mizen init [options]",
|
|
93
|
+
},
|
|
94
|
+
search: {
|
|
95
|
+
examples: ["mizen search dialog", "mizen search keyboard navigation"],
|
|
96
|
+
group: "Components",
|
|
97
|
+
notes: [
|
|
98
|
+
"Matches every query word, ignoring case. With no query, lists every component.",
|
|
99
|
+
"Works offline and never reads your project.",
|
|
100
|
+
],
|
|
101
|
+
options: [],
|
|
102
|
+
summary: "Find components in the bundled catalog",
|
|
103
|
+
usage: "mizen search [QUERY] [options]",
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
export function isCommand(value) {
|
|
107
|
+
return value !== undefined && Object.hasOwn(commands, value);
|
|
108
|
+
}
|
|
109
|
+
// Edit distance, for one close suggestion on a mistyped command.
|
|
110
|
+
function distance(a, b) {
|
|
111
|
+
let previous = Array.from({ length: b.length + 1 }, (_, index) => index);
|
|
112
|
+
for (const [i, letter] of [...a].entries()) {
|
|
113
|
+
const current = [i + 1];
|
|
114
|
+
for (const [j, other] of [...b].entries()) {
|
|
115
|
+
const substitution = (previous[j] ?? 0) + (letter === other ? 0 : 1);
|
|
116
|
+
current.push(Math.min((previous[j + 1] ?? 0) + 1, (current[j] ?? 0) + 1, substitution));
|
|
117
|
+
}
|
|
118
|
+
previous = current;
|
|
119
|
+
}
|
|
120
|
+
return previous[b.length] ?? 0;
|
|
121
|
+
}
|
|
122
|
+
/** The one known name within two edits of the input, if any. */
|
|
123
|
+
export function closest(input, names) {
|
|
124
|
+
const ranked = names
|
|
125
|
+
.map((name) => ({ name, score: distance(input, name) }))
|
|
126
|
+
.filter((item) => item.score <= 2)
|
|
127
|
+
.toSorted((a, b) => a.score - b.score);
|
|
128
|
+
return ranked[0]?.name;
|
|
129
|
+
}
|
|
130
|
+
export function unknownCommand(command) {
|
|
131
|
+
const suggestion = closest(command, Object.keys(commands));
|
|
132
|
+
throw new CliError("unknown_command", `Unknown command ${command} for mizen.${suggestion ? ` Did you mean ${suggestion}?` : ""}`, 2, {
|
|
133
|
+
next: {
|
|
134
|
+
argv: suggestion
|
|
135
|
+
? ["mizen", suggestion, "--help"]
|
|
136
|
+
: ["mizen", "--help"],
|
|
137
|
+
benefit: suggestion ? `See ${suggestion} usage` : "See usage",
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
const sorted = (options) => options.toSorted((a, b) => a.localeCompare(b));
|
|
142
|
+
function optionRows(options) {
|
|
143
|
+
const width = Math.max(...options.map((name) => optionHelp[name][0].length)) + 3;
|
|
144
|
+
return sorted(options).map((name) => {
|
|
145
|
+
const [label, description] = optionHelp[name];
|
|
146
|
+
return ` ${label.padEnd(width)}${description}`;
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
function section(style, title, lines) {
|
|
150
|
+
return lines.length ? ["", style.heading(`${title}:`), ...lines] : [];
|
|
151
|
+
}
|
|
152
|
+
function examples(style, list) {
|
|
153
|
+
return section(style, "Examples", list.map((command) => ` ${style.command(command)}`));
|
|
154
|
+
}
|
|
155
|
+
function rootHelp(style) {
|
|
156
|
+
const groups = ["Components", "Setup"];
|
|
157
|
+
return [
|
|
158
|
+
style.bold("mizen <COMMAND> [options]"),
|
|
159
|
+
"Find Mizen components and set up apps that use them",
|
|
160
|
+
...groups.flatMap((group) => section(style, group, Object.entries(commands)
|
|
161
|
+
.filter(([, item]) => item.group === group)
|
|
162
|
+
.map(([name, item]) => ` ${style.bold(name.padEnd(10))}${item.summary}`))),
|
|
163
|
+
...section(style, "Global Options", optionRows(globals)),
|
|
164
|
+
...examples(style, ["mizen search dialog", "mizen init --cwd ./apps/web"]),
|
|
165
|
+
].join("\n");
|
|
166
|
+
}
|
|
167
|
+
/** Help for the root, or for one command. Help never reads the project. */
|
|
168
|
+
export function help(style, command) {
|
|
169
|
+
if (!command) {
|
|
170
|
+
return rootHelp(style);
|
|
171
|
+
}
|
|
172
|
+
if (!isCommand(command)) {
|
|
173
|
+
return unknownCommand(command);
|
|
174
|
+
}
|
|
175
|
+
const entry = commands[command];
|
|
176
|
+
return [
|
|
177
|
+
style.bold(entry.usage),
|
|
178
|
+
entry.summary,
|
|
179
|
+
...section(style, "Options", entry.options.length ? optionRows(entry.options) : []),
|
|
180
|
+
"",
|
|
181
|
+
`${style.heading("Global Options:")} See mizen --help`,
|
|
182
|
+
...examples(style, entry.examples),
|
|
183
|
+
...section(style, "Notes", entry.notes.map((line) => ` ${line}`)),
|
|
184
|
+
].join("\n");
|
|
185
|
+
}
|
|
186
|
+
export function validateOptions(command, values) {
|
|
187
|
+
const allowed = new Set([...commands[command].options, ...globals]);
|
|
188
|
+
const invalid = Object.entries(values).find(([option, value]) => value !== undefined && !allowed.has(option))?.[0];
|
|
189
|
+
if (invalid) {
|
|
190
|
+
invalidUsage(`Unknown option --${invalid} for mizen ${command}.`, command, "unknown_option");
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { isNode, nameOf, parseSource, property, staticArrayContains, staticConfig, } from "../doctor/syntax.js";
|
|
2
|
+
import { addProperty, changes, conflict, fileEdit, position, sourceFile, } from "./edit.js";
|
|
3
|
+
function literalObject(value) {
|
|
4
|
+
if (!isNode(value) ||
|
|
5
|
+
value.type !== "ObjectExpression" ||
|
|
6
|
+
!Array.isArray(value.properties)) {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
return value.properties.every((item) => isNode(item) &&
|
|
10
|
+
!item.computed &&
|
|
11
|
+
["ObjectProperty", "ObjectMethod"].includes(item.type));
|
|
12
|
+
}
|
|
13
|
+
export function member(object, key) {
|
|
14
|
+
if (!literalObject(object)) {
|
|
15
|
+
return conflict("Computed keys or spreads can override configuration. Configure Mizen manually before init.");
|
|
16
|
+
}
|
|
17
|
+
const matches = object.properties.filter((item) => nameOf(item.key) === key);
|
|
18
|
+
if (matches.length > 1) {
|
|
19
|
+
return conflict(`Duplicate ${key} configuration requires manual review.`);
|
|
20
|
+
}
|
|
21
|
+
return matches.length === 1;
|
|
22
|
+
}
|
|
23
|
+
function arrayChange(value) {
|
|
24
|
+
const present = staticArrayContains(value, "@mizen-labs/ui");
|
|
25
|
+
if (present === null) {
|
|
26
|
+
return conflict("The bundling setting is not a literal string array. Configure Mizen manually.");
|
|
27
|
+
}
|
|
28
|
+
if (present) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const at = position(value, "start") + 1;
|
|
32
|
+
const node = value;
|
|
33
|
+
const comma = node.elements.length ? ", " : "";
|
|
34
|
+
return { end: at, start: at, text: `"@mizen-labs/ui"${comma}` };
|
|
35
|
+
}
|
|
36
|
+
function bundlingChange(_source, config, framework) {
|
|
37
|
+
if (framework === "next") {
|
|
38
|
+
if (!member(config, "transpilePackages")) {
|
|
39
|
+
return addProperty(config, "transpilePackages", '["@mizen-labs/ui"]');
|
|
40
|
+
}
|
|
41
|
+
return arrayChange(property(config, "transpilePackages"));
|
|
42
|
+
}
|
|
43
|
+
if (!member(config, "ssr")) {
|
|
44
|
+
return addProperty(config, "ssr", '{ noExternal: ["@mizen-labs/ui"] }');
|
|
45
|
+
}
|
|
46
|
+
const ssr = property(config, "ssr");
|
|
47
|
+
if (!literalObject(ssr)) {
|
|
48
|
+
return conflict("Computed ssr configuration needs manual setup.");
|
|
49
|
+
}
|
|
50
|
+
if (!member(ssr, "noExternal")) {
|
|
51
|
+
return addProperty(ssr, "noExternal", '["@mizen-labs/ui"]');
|
|
52
|
+
}
|
|
53
|
+
const external = property(ssr, "noExternal");
|
|
54
|
+
if (isNode(external) &&
|
|
55
|
+
external.type === "BooleanLiteral" &&
|
|
56
|
+
external.value === true) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return arrayChange(external);
|
|
60
|
+
}
|
|
61
|
+
export async function configEdit(project, framework) {
|
|
62
|
+
if (framework === "vite") {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
const prefix = framework === "next" ? "next.config" : "vite.config";
|
|
66
|
+
const template = framework === "next" ? "export default {};\n" : undefined;
|
|
67
|
+
const file = await sourceFile(project, ["mjs", "js", "ts", "mts", "cjs", "cts"].map((extension) => `${prefix}.${extension}`), template);
|
|
68
|
+
const config = staticConfig(parseSource(file.source));
|
|
69
|
+
if (!config) {
|
|
70
|
+
return conflict(`${file.path} has a wrapped or computed export. Configure Mizen bundling manually before init.`);
|
|
71
|
+
}
|
|
72
|
+
const edit = bundlingChange(file.source, config, framework);
|
|
73
|
+
const after = edit ? changes(file.source, [edit]) : file.source;
|
|
74
|
+
return fileEdit(file.path, file.before, after);
|
|
75
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import semver from "semver";
|
|
2
|
+
import { installedFrom, versionCheck, } from "../doctor/packages.js";
|
|
3
|
+
import { frameworkOf, packageManagerCheck } from "../doctor/project.js";
|
|
4
|
+
import { diagnosticDetail } from "../doctor/report.js";
|
|
5
|
+
import { supportsInitialization } from "../vite/render.js";
|
|
6
|
+
import { conflict } from "./edit.js";
|
|
7
|
+
import { releaseArchive } from "./release.js";
|
|
8
|
+
export async function managerFor(project, explicit) {
|
|
9
|
+
const diagnostic = await packageManagerCheck(project);
|
|
10
|
+
const lockfiles = diagnostic.evidence.filter((path) => !path.endsWith("package.json"));
|
|
11
|
+
if (diagnostic.status !== "pass" && lockfiles.length) {
|
|
12
|
+
return conflict("The package-manager declaration and lockfiles disagree.", diagnosticDetail(diagnostic));
|
|
13
|
+
}
|
|
14
|
+
const detected = diagnostic.packageManager;
|
|
15
|
+
if (!detected && diagnostic.evidence.length) {
|
|
16
|
+
return conflict("The package-manager declaration needs review.", diagnosticDetail(diagnostic));
|
|
17
|
+
}
|
|
18
|
+
const command = selectedManager(explicit, detected);
|
|
19
|
+
return { command, needsLockfile: !lockfiles.length };
|
|
20
|
+
}
|
|
21
|
+
async function compatiblePeers(project, manifest) {
|
|
22
|
+
const checks = await Promise.all(["react", "react-dom"].map(async (name) => {
|
|
23
|
+
const installed = await installedFrom(project, name);
|
|
24
|
+
if (!installed) {
|
|
25
|
+
return conflict(`Install ${name} in this app before init.`);
|
|
26
|
+
}
|
|
27
|
+
const check = versionCheck(name, installed.manifest.version, manifest.peerDependencies?.[name], [installed.path]);
|
|
28
|
+
if (check.status !== "pass") {
|
|
29
|
+
return conflict(`${name}: ${check.message} Resolve compatibility before init.`);
|
|
30
|
+
}
|
|
31
|
+
return installed.manifest.version;
|
|
32
|
+
}));
|
|
33
|
+
if (checks[0] !== checks[1]) {
|
|
34
|
+
conflict("React and ReactDOM versions must match before init.");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async function optionalPackages(project, manifest, ui, features) {
|
|
38
|
+
if (features.some((feature) => feature !== "diagram")) {
|
|
39
|
+
return conflict("The supported optional feature is diagram.");
|
|
40
|
+
}
|
|
41
|
+
if (!features.includes("diagram")) {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
const range = ui.peerDependencies?.pele;
|
|
45
|
+
if (!(range && semver.validRange(range))) {
|
|
46
|
+
return conflict("This Mizen archive has no valid Pele peer range.");
|
|
47
|
+
}
|
|
48
|
+
const pele = await installedFrom(project, "pele");
|
|
49
|
+
validatePele(pele, range);
|
|
50
|
+
const declared = { ...manifest.dependencies, ...manifest.devDependencies }
|
|
51
|
+
.pele;
|
|
52
|
+
return pele && declared ? [] : [`pele@${range}`];
|
|
53
|
+
}
|
|
54
|
+
function installCommand(command, packages, archive) {
|
|
55
|
+
const args = command === "npm"
|
|
56
|
+
? [
|
|
57
|
+
"install",
|
|
58
|
+
"--ignore-scripts",
|
|
59
|
+
"--no-audit",
|
|
60
|
+
"--no-fund",
|
|
61
|
+
"--save-exact",
|
|
62
|
+
"--include=dev",
|
|
63
|
+
...packages,
|
|
64
|
+
]
|
|
65
|
+
: [
|
|
66
|
+
packages.length ? "add" : "install",
|
|
67
|
+
"--ignore-scripts",
|
|
68
|
+
"--prod=false",
|
|
69
|
+
...packages,
|
|
70
|
+
];
|
|
71
|
+
return {
|
|
72
|
+
archive: archive ? { path: archive.path, sha256: archive.sha256 } : null,
|
|
73
|
+
args,
|
|
74
|
+
command,
|
|
75
|
+
packages,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
async function selectedRelease(project, version, specifier) {
|
|
79
|
+
const archive = specifier ? await releaseArchive(specifier, version) : null;
|
|
80
|
+
const installed = await installedFrom(project, "@mizen-labs/ui");
|
|
81
|
+
const ui = archive?.manifest ?? installed?.manifest;
|
|
82
|
+
if (!ui || ui.version !== version) {
|
|
83
|
+
return conflict(`Mizen is unpublished. Pass --package /path/to/mizen-ui.tgz for @mizen-labs/ui ${version}.`);
|
|
84
|
+
}
|
|
85
|
+
await compatiblePeers(project, ui);
|
|
86
|
+
return { archive, installed, ui };
|
|
87
|
+
}
|
|
88
|
+
function mizenPackage(manifest, release, version) {
|
|
89
|
+
const declared = { ...manifest.dependencies, ...manifest.devDependencies }["@mizen-labs/ui"];
|
|
90
|
+
if (release.installed &&
|
|
91
|
+
declared &&
|
|
92
|
+
release.installed.manifest.version === version &&
|
|
93
|
+
(frameworkOf(manifest) !== "vite" ||
|
|
94
|
+
supportsInitialization(release.installed.manifest))) {
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
if (!release.archive) {
|
|
98
|
+
return conflict("Declare Mizen in the target app with --package /path/to/mizen-ui.tgz.");
|
|
99
|
+
}
|
|
100
|
+
return [release.archive.path];
|
|
101
|
+
}
|
|
102
|
+
export async function installation(project, manifest, version, specifier, features, manager) {
|
|
103
|
+
const release = await selectedRelease(project, version, specifier);
|
|
104
|
+
if (frameworkOf(manifest) === "vite" && !supportsInitialization(release.ui)) {
|
|
105
|
+
return conflict("This UI build lacks synchronous client theme initialization. Rebuild and pack @mizen-labs/ui before running Vite init.");
|
|
106
|
+
}
|
|
107
|
+
const packages = [
|
|
108
|
+
...mizenPackage(manifest, release, version),
|
|
109
|
+
...(await optionalPackages(project, manifest, release.ui, features)),
|
|
110
|
+
];
|
|
111
|
+
if (!(packages.length || manager.needsLockfile)) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
return installCommand(manager.command, packages, release.archive);
|
|
115
|
+
}
|
|
116
|
+
function selectedManager(explicit, detected) {
|
|
117
|
+
if (explicit && detected && explicit !== detected) {
|
|
118
|
+
return conflict("The requested package manager conflicts with the project's lockfile or declaration.");
|
|
119
|
+
}
|
|
120
|
+
const selected = explicit ?? detected;
|
|
121
|
+
if (selected !== "npm" && selected !== "pnpm") {
|
|
122
|
+
return conflict("Init currently supports verified npm and pnpm writes. Select --package-manager npm or pnpm for an app without a lockfile, or configure this app manually.");
|
|
123
|
+
}
|
|
124
|
+
return selected;
|
|
125
|
+
}
|
|
126
|
+
function validatePele(pele, range) {
|
|
127
|
+
if (pele && !semver.satisfies(pele.manifest.version ?? "", range)) {
|
|
128
|
+
conflict("Resolve the existing Pele peer version before enabling diagrams.");
|
|
129
|
+
}
|
|
130
|
+
}
|