as-test 0.5.3 → 0.5.4

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.
@@ -0,0 +1,335 @@
1
+ import chalk from "chalk";
2
+ import { existsSync } from "fs";
3
+ import { glob } from "glob";
4
+ import * as path from "path";
5
+ import { applyMode, getExec, loadConfig, tokenizeCommand } from "../util.js";
6
+ import { Config } from "../types.js";
7
+ const DEFAULT_CONFIG_PATH = path.join(process.cwd(), "./as-test.config.json");
8
+ export async function doctor(configPath = DEFAULT_CONFIG_PATH, selectedModes = []) {
9
+ const checks = [];
10
+ const resolvedConfigPath = configPath ?? DEFAULT_CONFIG_PATH;
11
+ const configExists = existsSync(resolvedConfigPath);
12
+ const loadedConfig = tryLoadConfig(resolvedConfigPath);
13
+ const config = loadedConfig.config;
14
+ if (!configExists) {
15
+ checks.push({
16
+ status: "warn",
17
+ scope: "config",
18
+ label: "Config file not found",
19
+ details: `No config at ${resolvedConfigPath}; default settings will be used.`,
20
+ fix: `Create a config with "ast init" or add ${path.basename(resolvedConfigPath)}.`,
21
+ });
22
+ }
23
+ else if (!loadedConfig.loaded) {
24
+ checks.push({
25
+ status: "error",
26
+ scope: "config",
27
+ label: "Config parse failed",
28
+ details: `Could not parse ${resolvedConfigPath}.`,
29
+ fix: "Fix JSON syntax errors, then rerun `ast doctor`.",
30
+ });
31
+ }
32
+ else {
33
+ checks.push({
34
+ status: "ok",
35
+ scope: "config",
36
+ label: "Config loaded",
37
+ details: resolvedConfigPath,
38
+ });
39
+ }
40
+ checks.push(checkNodeVersion());
41
+ checks.push(checkDependency("assemblyscript", true));
42
+ const selected = selectedModes.length
43
+ ? selectedModes
44
+ : Object.keys(config.modes).length
45
+ ? Object.keys(config.modes)
46
+ : [undefined];
47
+ if (selectedModes.length) {
48
+ for (const modeName of selectedModes) {
49
+ if (!config.modes[modeName]) {
50
+ checks.push({
51
+ status: "error",
52
+ scope: "modes",
53
+ label: `Unknown mode "${modeName}"`,
54
+ details: `Available modes: ${Object.keys(config.modes).join(", ") || "(none)"}`,
55
+ fix: `Use "--mode <name>" with one of the configured modes, or remove "--mode ${modeName}".`,
56
+ });
57
+ }
58
+ }
59
+ }
60
+ for (const modeName of selected) {
61
+ const scope = modeName ?? "default";
62
+ const modeCheck = await checkMode(config, modeName, scope);
63
+ checks.push(...modeCheck);
64
+ }
65
+ renderChecks(checks, resolvedConfigPath, selected);
66
+ const hasErrors = checks.some((check) => check.status == "error");
67
+ if (hasErrors) {
68
+ throw new Error("doctor checks failed");
69
+ }
70
+ }
71
+ function tryLoadConfig(configPath) {
72
+ try {
73
+ return { loaded: true, config: loadConfig(configPath, false) };
74
+ }
75
+ catch {
76
+ return { loaded: false, config: new Config() };
77
+ }
78
+ }
79
+ async function checkMode(config, modeName, scope) {
80
+ const checks = [];
81
+ let applied;
82
+ try {
83
+ applied = applyMode(config, modeName);
84
+ }
85
+ catch (error) {
86
+ checks.push({
87
+ status: "error",
88
+ scope,
89
+ label: "Mode merge failed",
90
+ details: error instanceof Error ? error.message : String(error),
91
+ fix: `Fix mode "${scope}" in as-test.config.json.`,
92
+ });
93
+ return checks;
94
+ }
95
+ const active = applied.config;
96
+ const runtimeCommand = active.runOptions.runtime.cmd;
97
+ const target = active.buildOptions.target;
98
+ if (target == "wasi") {
99
+ checks.push(checkDependency("@assemblyscript/wasi-shim", true, scope));
100
+ }
101
+ checks.push(...checkRuntimeCommand(runtimeCommand, target, scope), ...(await checkInputPatterns(active.input, scope)));
102
+ return checks;
103
+ }
104
+ function checkRuntimeCommand(runtimeCommand, target, scope) {
105
+ const checks = [];
106
+ if (!runtimeCommand.trim().length) {
107
+ checks.push({
108
+ status: "error",
109
+ scope,
110
+ label: "Runtime command missing",
111
+ details: "runOptions.runtime.cmd is empty.",
112
+ fix: 'Set "runOptions.runtime.cmd" in as-test.config.json (for example: node ./.as-test/runners/default.wasi.js <file>).',
113
+ });
114
+ return checks;
115
+ }
116
+ let tokens;
117
+ try {
118
+ tokens = tokenizeCommand(runtimeCommand);
119
+ }
120
+ catch (error) {
121
+ checks.push({
122
+ status: "error",
123
+ scope,
124
+ label: "Runtime command parsing failed",
125
+ details: error instanceof Error ? error.message : String(error),
126
+ fix: "Fix quotes/escaping in runOptions.runtime.cmd.",
127
+ });
128
+ return checks;
129
+ }
130
+ if (!tokens.length) {
131
+ checks.push({
132
+ status: "error",
133
+ scope,
134
+ label: "Runtime command empty",
135
+ details: "Command parsed to zero tokens.",
136
+ fix: "Provide a runtime command executable and args.",
137
+ });
138
+ return checks;
139
+ }
140
+ const execToken = tokens[0];
141
+ const execPath = getExec(execToken);
142
+ if (!execPath) {
143
+ checks.push({
144
+ status: "error",
145
+ scope,
146
+ label: `Runtime executable not found: ${execToken}`,
147
+ details: "Executable is not available in PATH.",
148
+ fix: `Install "${execToken}" or update runOptions.runtime.cmd.`,
149
+ });
150
+ }
151
+ else {
152
+ checks.push({
153
+ status: "ok",
154
+ scope,
155
+ label: "Runtime executable",
156
+ details: `${execToken} -> ${execPath}`,
157
+ });
158
+ }
159
+ if (!tokens.some((token) => token.includes("<file>"))) {
160
+ checks.push({
161
+ status: "error",
162
+ scope,
163
+ label: "Runtime command missing <file> placeholder",
164
+ details: `Runtime command for target "${target}" cannot receive the wasm artifact path.`,
165
+ fix: 'Add "<file>" to runOptions.runtime.cmd.',
166
+ });
167
+ }
168
+ if (isScriptHostRuntime(execToken)) {
169
+ const scriptPath = extractRuntimeScriptPath(tokens.slice(1));
170
+ if (scriptPath) {
171
+ const resolved = path.isAbsolute(scriptPath)
172
+ ? scriptPath
173
+ : path.join(process.cwd(), scriptPath);
174
+ if (!existsSync(resolved)) {
175
+ checks.push({
176
+ status: "warn",
177
+ scope,
178
+ label: "Runtime script path not found",
179
+ details: `${scriptPath} does not exist.`,
180
+ fix: "Create the runner script, or use `ast init` to scaffold default runners.",
181
+ });
182
+ }
183
+ else {
184
+ checks.push({
185
+ status: "ok",
186
+ scope,
187
+ label: "Runtime script path",
188
+ details: scriptPath,
189
+ });
190
+ }
191
+ }
192
+ }
193
+ return checks;
194
+ }
195
+ async function checkInputPatterns(input, scope) {
196
+ const patterns = Array.isArray(input) ? input : [input];
197
+ const files = await glob(patterns);
198
+ const specs = files.filter((file) => file.endsWith(".spec.ts"));
199
+ if (!specs.length) {
200
+ return [
201
+ {
202
+ status: "warn",
203
+ scope,
204
+ label: "No spec files matched input patterns",
205
+ details: patterns.join(", "),
206
+ fix: 'Update "input" patterns or add `*.spec.ts` files.',
207
+ },
208
+ ];
209
+ }
210
+ return [
211
+ {
212
+ status: "ok",
213
+ scope,
214
+ label: "Spec file discovery",
215
+ details: `${specs.length} spec file(s) matched input patterns.`,
216
+ },
217
+ ];
218
+ }
219
+ function checkNodeVersion() {
220
+ const version = process.versions.node;
221
+ const major = Number(version.split(".")[0] ?? "0");
222
+ if (!Number.isFinite(major) || major < 18) {
223
+ return {
224
+ status: "warn",
225
+ scope: "env",
226
+ label: "Node.js version is old",
227
+ details: `Detected v${version}.`,
228
+ fix: "Use Node.js 18+ for the best compatibility.",
229
+ };
230
+ }
231
+ return {
232
+ status: "ok",
233
+ scope: "env",
234
+ label: "Node.js version",
235
+ details: `v${version}`,
236
+ };
237
+ }
238
+ function checkDependency(pkg, required, scope = "deps") {
239
+ const pkgJson = path.join(process.cwd(), "node_modules", pkg, "package.json");
240
+ if (!existsSync(pkgJson)) {
241
+ return {
242
+ status: required ? "error" : "warn",
243
+ scope,
244
+ label: `Dependency missing: ${pkg}`,
245
+ details: `${pkg} is not installed in node_modules.`,
246
+ fix: `Install with: npm i -D ${pkg}`,
247
+ };
248
+ }
249
+ return {
250
+ status: "ok",
251
+ scope,
252
+ label: `Dependency present: ${pkg}`,
253
+ details: pkgJson,
254
+ };
255
+ }
256
+ function isScriptHostRuntime(execToken) {
257
+ const token = path.basename(execToken).toLowerCase();
258
+ return (token == "node" ||
259
+ token == "node.exe" ||
260
+ token == "node.cmd" ||
261
+ token == "bun" ||
262
+ token == "bun.exe" ||
263
+ token == "bun.cmd" ||
264
+ token == "deno" ||
265
+ token == "deno.exe" ||
266
+ token == "deno.cmd" ||
267
+ token == "tsx" ||
268
+ token == "tsx.cmd" ||
269
+ token == "ts-node" ||
270
+ token == "ts-node.cmd");
271
+ }
272
+ function extractRuntimeScriptPath(args) {
273
+ for (let i = 0; i < args.length; i++) {
274
+ const token = args[i];
275
+ if (token == "--") {
276
+ const next = args[i + 1];
277
+ if (next && isLikelyScriptPath(next))
278
+ return next;
279
+ return null;
280
+ }
281
+ if (token.startsWith("-"))
282
+ continue;
283
+ if (isLikelyScriptPath(token))
284
+ return token;
285
+ return null;
286
+ }
287
+ return null;
288
+ }
289
+ function isLikelyScriptPath(token) {
290
+ if (!token.length)
291
+ return false;
292
+ if (token == "<file>" || token == "<name>")
293
+ return false;
294
+ if (token.includes("://"))
295
+ return false;
296
+ if (token.startsWith("-"))
297
+ return false;
298
+ if (token.startsWith("./"))
299
+ return true;
300
+ if (token.startsWith("../"))
301
+ return true;
302
+ if (token.startsWith("/"))
303
+ return true;
304
+ if (token.startsWith(".\\"))
305
+ return true;
306
+ if (token.startsWith("..\\"))
307
+ return true;
308
+ if (/^[A-Za-z]:[\\/]/.test(token))
309
+ return true;
310
+ return /\.(mjs|cjs|js|ts)$/.test(token);
311
+ }
312
+ function renderChecks(checks, configPath, selectedModes) {
313
+ const errors = checks.filter((check) => check.status == "error").length;
314
+ const warnings = checks.filter((check) => check.status == "warn").length;
315
+ const oks = checks.filter((check) => check.status == "ok").length;
316
+ process.stdout.write(chalk.bold.blueBright("as-test doctor") + "\n");
317
+ process.stdout.write(chalk.dim(`config: ${configPath}`) + "\n");
318
+ process.stdout.write(chalk.dim(`modes: ${selectedModes.length
319
+ ? selectedModes.map((mode) => mode ?? "default").join(", ")
320
+ : "default"}`) + "\n\n");
321
+ for (const check of checks) {
322
+ const badge = check.status == "ok"
323
+ ? chalk.bgGreenBright.black(" OK ")
324
+ : check.status == "warn"
325
+ ? chalk.bgYellow.black(" WARN ")
326
+ : chalk.bgRed.white(" ERROR ");
327
+ process.stdout.write(`${badge} ${chalk.bold(`[${check.scope}]`)} ${check.label}\n`);
328
+ process.stdout.write(` ${check.details}\n`);
329
+ if (check.fix?.length) {
330
+ process.stdout.write(chalk.dim(` fix: ${check.fix}\n`));
331
+ }
332
+ }
333
+ process.stdout.write("\n");
334
+ process.stdout.write(`${chalk.bold("Summary:")} ${chalk.greenBright(`${oks} ok`)}, ${warnings ? chalk.yellowBright(`${warnings} warn`) : chalk.gray("0 warn")}, ${errors ? chalk.redBright(`${errors} error`) : chalk.greenBright("0 error")}\n`);
335
+ }
@@ -0,0 +1,5 @@
1
+ import { doctor } from "./doctor-core.js";
2
+ export { doctor } from "./doctor-core.js";
3
+ export async function executeDoctorCommand(configPath, selectedModes) {
4
+ await doctor(configPath, selectedModes);
5
+ }