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.
- package/CHANGELOG.md +13 -0
- package/README.md +66 -5
- package/as-test.config.schema.json +31 -0
- package/assembly/src/expectation.ts +32 -9
- package/bin/{build.js → commands/build-core.js} +84 -32
- package/bin/commands/build.js +16 -0
- package/bin/commands/doctor-core.js +335 -0
- package/bin/commands/doctor.js +5 -0
- package/bin/commands/init-core.js +991 -0
- package/bin/commands/init.js +6 -0
- package/bin/{run.js → commands/run-core.js} +95 -30
- package/bin/commands/run.js +20 -0
- package/bin/commands/test.js +23 -0
- package/bin/commands/types.js +1 -0
- package/bin/index.js +410 -52
- package/bin/util.js +559 -8
- package/package.json +4 -2
- package/transform/lib/index.js +2 -1
- package/bin/init.js +0 -497
package/bin/util.js
CHANGED
|
@@ -33,8 +33,22 @@ export function loadConfig(CONFIG_PATH, warn = false) {
|
|
|
33
33
|
return new Config();
|
|
34
34
|
}
|
|
35
35
|
else {
|
|
36
|
-
const
|
|
36
|
+
const rawText = readFileSync(CONFIG_PATH, "utf8");
|
|
37
|
+
let parsed;
|
|
38
|
+
try {
|
|
39
|
+
parsed = JSON.parse(rawText);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
43
|
+
throw new Error(`invalid config JSON at ${CONFIG_PATH}\n${message}\nfix JSON syntax and rerun.`);
|
|
44
|
+
}
|
|
45
|
+
if (!parsed || typeof parsed != "object" || Array.isArray(parsed)) {
|
|
46
|
+
throw new Error(`invalid config at ${CONFIG_PATH}\nroot value must be an object. Example: { "input": ["./assembly/__tests__/*.spec.ts"] }`);
|
|
47
|
+
}
|
|
48
|
+
const raw = parsed;
|
|
49
|
+
validateConfig(raw, CONFIG_PATH);
|
|
37
50
|
const config = Object.assign(new Config(), raw);
|
|
51
|
+
applyOutputConfig(raw.output, raw, config);
|
|
38
52
|
config.env = parseEnvMap(raw.env);
|
|
39
53
|
const runOptionsRaw = raw.runOptions ?? {};
|
|
40
54
|
config.buildOptions = Object.assign(new BuildOptions(), raw.buildOptions ?? {});
|
|
@@ -44,7 +58,8 @@ export function loadConfig(CONFIG_PATH, warn = false) {
|
|
|
44
58
|
? config.buildOptions.args.filter((item) => typeof item == "string")
|
|
45
59
|
: [];
|
|
46
60
|
config.buildOptions.target =
|
|
47
|
-
typeof config.buildOptions.target == "string" &&
|
|
61
|
+
typeof config.buildOptions.target == "string" &&
|
|
62
|
+
config.buildOptions.target.length
|
|
48
63
|
? config.buildOptions.target
|
|
49
64
|
: "wasi";
|
|
50
65
|
config.runOptions = Object.assign(new RunOptions(), runOptionsRaw);
|
|
@@ -88,6 +103,491 @@ export function loadConfig(CONFIG_PATH, warn = false) {
|
|
|
88
103
|
return config;
|
|
89
104
|
}
|
|
90
105
|
}
|
|
106
|
+
const TOP_LEVEL_KEYS = new Set([
|
|
107
|
+
"$schema",
|
|
108
|
+
"input",
|
|
109
|
+
"output",
|
|
110
|
+
"outDir",
|
|
111
|
+
"logs",
|
|
112
|
+
"coverageDir",
|
|
113
|
+
"snapshotDir",
|
|
114
|
+
"config",
|
|
115
|
+
"coverage",
|
|
116
|
+
"env",
|
|
117
|
+
"buildOptions",
|
|
118
|
+
"modes",
|
|
119
|
+
"runOptions",
|
|
120
|
+
]);
|
|
121
|
+
const BUILD_OPTION_KEYS = new Set(["cmd", "args", "target"]);
|
|
122
|
+
const RUN_OPTION_KEYS = new Set(["runtime", "reporter", "run"]); // includes legacy "run"
|
|
123
|
+
const RUNTIME_OPTION_KEYS = new Set(["cmd", "run"]); // includes legacy "run"
|
|
124
|
+
const REPORTER_OPTION_KEYS = new Set(["name", "options", "outDir", "outFile"]);
|
|
125
|
+
const OUTPUT_OPTION_KEYS = new Set(["build", "logs", "coverage", "snapshots"]);
|
|
126
|
+
const MODE_KEYS = new Set([
|
|
127
|
+
"outDir",
|
|
128
|
+
"logs",
|
|
129
|
+
"coverageDir",
|
|
130
|
+
"snapshotDir",
|
|
131
|
+
"config",
|
|
132
|
+
"coverage",
|
|
133
|
+
"buildOptions",
|
|
134
|
+
"runOptions",
|
|
135
|
+
"env",
|
|
136
|
+
]);
|
|
137
|
+
function validateConfig(raw, configPath) {
|
|
138
|
+
const issues = [];
|
|
139
|
+
validateUnknownKeys(raw, TOP_LEVEL_KEYS, "$", issues);
|
|
140
|
+
validateStringField(raw, "$schema", "$", issues);
|
|
141
|
+
validateInputField(raw, "input", "$", issues);
|
|
142
|
+
validateOutputField(raw, "output", "$", issues);
|
|
143
|
+
validateStringField(raw, "outDir", "$", issues);
|
|
144
|
+
validateStringField(raw, "logs", "$", issues);
|
|
145
|
+
validateStringField(raw, "coverageDir", "$", issues);
|
|
146
|
+
validateStringField(raw, "snapshotDir", "$", issues);
|
|
147
|
+
validateStringField(raw, "config", "$", issues);
|
|
148
|
+
validateCoverageField(raw, "coverage", "$", issues);
|
|
149
|
+
validateEnvField(raw, "env", "$", issues);
|
|
150
|
+
validateBuildOptionsField(raw, "buildOptions", "$", issues);
|
|
151
|
+
validateRunOptionsField(raw, "runOptions", "$", issues);
|
|
152
|
+
validateModesField(raw, "modes", "$", issues);
|
|
153
|
+
if (!issues.length)
|
|
154
|
+
return;
|
|
155
|
+
const lines = issues.map((issue, index) => {
|
|
156
|
+
const suffix = issue.fix ? `\n fix: ${issue.fix}` : "";
|
|
157
|
+
return `${index + 1}. ${issue.path}: ${issue.message}${suffix}`;
|
|
158
|
+
});
|
|
159
|
+
throw new Error(`invalid config at ${configPath}\n${lines.join("\n")}\nrun "ast doctor" to check your setup.`);
|
|
160
|
+
}
|
|
161
|
+
function validateUnknownKeys(raw, allowed, pathPrefix, issues) {
|
|
162
|
+
for (const key of Object.keys(raw)) {
|
|
163
|
+
if (allowed.has(key))
|
|
164
|
+
continue;
|
|
165
|
+
const suggestion = resolveClosestKey(key, [...allowed]);
|
|
166
|
+
issues.push({
|
|
167
|
+
path: `${pathPrefix}.${key}`,
|
|
168
|
+
message: "unknown property",
|
|
169
|
+
fix: suggestion
|
|
170
|
+
? `use "${suggestion}" if that was intended, otherwise remove this property`
|
|
171
|
+
: `remove this property`,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function validateInputField(raw, key, pathPrefix, issues) {
|
|
176
|
+
if (!(key in raw) || raw[key] == undefined)
|
|
177
|
+
return;
|
|
178
|
+
const value = raw[key];
|
|
179
|
+
if (typeof value == "string") {
|
|
180
|
+
if (!value.length) {
|
|
181
|
+
issues.push({
|
|
182
|
+
path: `${pathPrefix}.${key}`,
|
|
183
|
+
message: "must not be an empty string",
|
|
184
|
+
fix: "set to a glob pattern or remove it to use the default input patterns",
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (!Array.isArray(value)) {
|
|
190
|
+
issues.push({
|
|
191
|
+
path: `${pathPrefix}.${key}`,
|
|
192
|
+
message: "must be a string or array of strings",
|
|
193
|
+
fix: 'example: "input": ["./assembly/__tests__/*.spec.ts"]',
|
|
194
|
+
});
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
for (let i = 0; i < value.length; i++) {
|
|
198
|
+
if (typeof value[i] == "string" && value[i].length)
|
|
199
|
+
continue;
|
|
200
|
+
issues.push({
|
|
201
|
+
path: `${pathPrefix}.${key}[${i}]`,
|
|
202
|
+
message: "must be a non-empty string",
|
|
203
|
+
fix: "remove invalid entries or replace them with valid glob strings",
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function validateStringField(raw, key, pathPrefix, issues) {
|
|
208
|
+
if (!(key in raw) || raw[key] == undefined)
|
|
209
|
+
return;
|
|
210
|
+
if (typeof raw[key] != "string") {
|
|
211
|
+
issues.push({
|
|
212
|
+
path: `${pathPrefix}.${key}`,
|
|
213
|
+
message: "must be a string",
|
|
214
|
+
fix: `set "${key}" to a string value`,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function validateOutputField(raw, key, pathPrefix, issues) {
|
|
219
|
+
if (!(key in raw) || raw[key] == undefined)
|
|
220
|
+
return;
|
|
221
|
+
const value = raw[key];
|
|
222
|
+
if (typeof value == "string") {
|
|
223
|
+
if (!value.length) {
|
|
224
|
+
issues.push({
|
|
225
|
+
path: `${pathPrefix}.${key}`,
|
|
226
|
+
message: "must not be an empty string",
|
|
227
|
+
fix: 'example: "output": "./.as-test/"',
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (!value || typeof value != "object" || Array.isArray(value)) {
|
|
233
|
+
issues.push({
|
|
234
|
+
path: `${pathPrefix}.${key}`,
|
|
235
|
+
message: "must be a string or object",
|
|
236
|
+
fix: 'example: "output": { "logs": "./.as-test/logs", "coverage": "./.as-test/coverage" }',
|
|
237
|
+
});
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const out = value;
|
|
241
|
+
validateUnknownKeys(out, OUTPUT_OPTION_KEYS, `${pathPrefix}.${key}`, issues);
|
|
242
|
+
if ("build" in out && (typeof out.build != "string" || !out.build.length)) {
|
|
243
|
+
issues.push({
|
|
244
|
+
path: `${pathPrefix}.${key}.build`,
|
|
245
|
+
message: "must be a non-empty string",
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
if ("snapshots" in out &&
|
|
249
|
+
(typeof out.snapshots != "string" || !out.snapshots.length)) {
|
|
250
|
+
issues.push({
|
|
251
|
+
path: `${pathPrefix}.${key}.snapshots`,
|
|
252
|
+
message: "must be a non-empty string",
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
if ("logs" in out && (typeof out.logs != "string" || !out.logs.length)) {
|
|
256
|
+
issues.push({
|
|
257
|
+
path: `${pathPrefix}.${key}.logs`,
|
|
258
|
+
message: 'must be a non-empty string or "none"',
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
if ("coverage" in out &&
|
|
262
|
+
(typeof out.coverage != "string" || !out.coverage.length)) {
|
|
263
|
+
issues.push({
|
|
264
|
+
path: `${pathPrefix}.${key}.coverage`,
|
|
265
|
+
message: 'must be a non-empty string or "none"',
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
function validateCoverageField(raw, key, pathPrefix, issues) {
|
|
270
|
+
if (!(key in raw) || raw[key] == undefined)
|
|
271
|
+
return;
|
|
272
|
+
validateCoverageValue(raw[key], `${pathPrefix}.${key}`, issues);
|
|
273
|
+
}
|
|
274
|
+
function validateCoverageValue(value, path, issues) {
|
|
275
|
+
if (typeof value == "boolean")
|
|
276
|
+
return;
|
|
277
|
+
if (!value || typeof value != "object" || Array.isArray(value)) {
|
|
278
|
+
issues.push({
|
|
279
|
+
path,
|
|
280
|
+
message: "must be a boolean or object",
|
|
281
|
+
fix: 'use true/false or { "enabled": true, "includeSpecs": false }',
|
|
282
|
+
});
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
const obj = value;
|
|
286
|
+
if ("enabled" in obj && typeof obj.enabled != "boolean") {
|
|
287
|
+
issues.push({
|
|
288
|
+
path: `${path}.enabled`,
|
|
289
|
+
message: "must be a boolean",
|
|
290
|
+
fix: "set to true or false",
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
if ("includeSpecs" in obj && typeof obj.includeSpecs != "boolean") {
|
|
294
|
+
issues.push({
|
|
295
|
+
path: `${path}.includeSpecs`,
|
|
296
|
+
message: "must be a boolean",
|
|
297
|
+
fix: "set to true or false",
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function validateEnvField(raw, key, pathPrefix, issues) {
|
|
302
|
+
if (!(key in raw) || raw[key] == undefined)
|
|
303
|
+
return;
|
|
304
|
+
const value = raw[key];
|
|
305
|
+
if (!value || typeof value != "object" || Array.isArray(value)) {
|
|
306
|
+
issues.push({
|
|
307
|
+
path: `${pathPrefix}.${key}`,
|
|
308
|
+
message: "must be an object of string values",
|
|
309
|
+
fix: 'example: "env": { "MY_FLAG": "1" }',
|
|
310
|
+
});
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
for (const [name, item] of Object.entries(value)) {
|
|
314
|
+
if (typeof item == "string")
|
|
315
|
+
continue;
|
|
316
|
+
issues.push({
|
|
317
|
+
path: `${pathPrefix}.${key}.${name}`,
|
|
318
|
+
message: "must be a string",
|
|
319
|
+
fix: "set environment values as strings",
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
function validateBuildOptionsField(raw, key, pathPrefix, issues) {
|
|
324
|
+
if (!(key in raw) || raw[key] == undefined)
|
|
325
|
+
return;
|
|
326
|
+
const value = raw[key];
|
|
327
|
+
if (!value || typeof value != "object" || Array.isArray(value)) {
|
|
328
|
+
issues.push({
|
|
329
|
+
path: `${pathPrefix}.${key}`,
|
|
330
|
+
message: "must be an object",
|
|
331
|
+
fix: 'example: "buildOptions": { "cmd": "", "args": [], "target": "wasi" }',
|
|
332
|
+
});
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
const obj = value;
|
|
336
|
+
validateUnknownKeys(obj, BUILD_OPTION_KEYS, `${pathPrefix}.${key}`, issues);
|
|
337
|
+
if ("cmd" in obj && typeof obj.cmd != "string") {
|
|
338
|
+
issues.push({
|
|
339
|
+
path: `${pathPrefix}.${key}.cmd`,
|
|
340
|
+
message: "must be a string",
|
|
341
|
+
fix: "set to an empty string or a command template",
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
if ("args" in obj && !isStringArray(obj.args)) {
|
|
345
|
+
issues.push({
|
|
346
|
+
path: `${pathPrefix}.${key}.args`,
|
|
347
|
+
message: "must be an array of strings",
|
|
348
|
+
fix: 'example: "args": ["--optimize"]',
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
if ("target" in obj) {
|
|
352
|
+
if (typeof obj.target != "string") {
|
|
353
|
+
issues.push({
|
|
354
|
+
path: `${pathPrefix}.${key}.target`,
|
|
355
|
+
message: "must be a string",
|
|
356
|
+
fix: 'set to "wasi" or "bindings"',
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
else if (obj.target != "wasi" && obj.target != "bindings") {
|
|
360
|
+
issues.push({
|
|
361
|
+
path: `${pathPrefix}.${key}.target`,
|
|
362
|
+
message: `must be "wasi" or "bindings"`,
|
|
363
|
+
fix: `received "${obj.target}"`,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
function validateRunOptionsField(raw, key, pathPrefix, issues) {
|
|
369
|
+
if (!(key in raw) || raw[key] == undefined)
|
|
370
|
+
return;
|
|
371
|
+
const value = raw[key];
|
|
372
|
+
if (!value || typeof value != "object" || Array.isArray(value)) {
|
|
373
|
+
issues.push({
|
|
374
|
+
path: `${pathPrefix}.${key}`,
|
|
375
|
+
message: "must be an object",
|
|
376
|
+
fix: 'example: "runOptions": { "runtime": { "cmd": "node ... <file>" } }',
|
|
377
|
+
});
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
const obj = value;
|
|
381
|
+
validateUnknownKeys(obj, RUN_OPTION_KEYS, `${pathPrefix}.${key}`, issues);
|
|
382
|
+
if ("run" in obj && typeof obj.run != "string") {
|
|
383
|
+
issues.push({
|
|
384
|
+
path: `${pathPrefix}.${key}.run`,
|
|
385
|
+
message: "must be a string",
|
|
386
|
+
fix: 'prefer "runtime.cmd"; legacy "run" must still be string',
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
if ("runtime" in obj && obj.runtime != undefined) {
|
|
390
|
+
const runtime = obj.runtime;
|
|
391
|
+
if (!runtime || typeof runtime != "object" || Array.isArray(runtime)) {
|
|
392
|
+
issues.push({
|
|
393
|
+
path: `${pathPrefix}.${key}.runtime`,
|
|
394
|
+
message: "must be an object",
|
|
395
|
+
fix: 'example: "runtime": { "cmd": "node ./.as-test/runners/default.wasi.js <file>" }',
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
const runtimeObj = runtime;
|
|
400
|
+
validateUnknownKeys(runtimeObj, RUNTIME_OPTION_KEYS, `${pathPrefix}.${key}.runtime`, issues);
|
|
401
|
+
if ("cmd" in runtimeObj && typeof runtimeObj.cmd != "string") {
|
|
402
|
+
issues.push({
|
|
403
|
+
path: `${pathPrefix}.${key}.runtime.cmd`,
|
|
404
|
+
message: "must be a string",
|
|
405
|
+
fix: 'set to a runtime command including "<file>"',
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
if ("run" in runtimeObj && typeof runtimeObj.run != "string") {
|
|
409
|
+
issues.push({
|
|
410
|
+
path: `${pathPrefix}.${key}.runtime.run`,
|
|
411
|
+
message: "must be a string",
|
|
412
|
+
fix: 'legacy "run" should be a command string',
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
if ("reporter" in obj && obj.reporter != undefined) {
|
|
418
|
+
const reporter = obj.reporter;
|
|
419
|
+
if (typeof reporter == "string")
|
|
420
|
+
return;
|
|
421
|
+
if (!reporter || typeof reporter != "object" || Array.isArray(reporter)) {
|
|
422
|
+
issues.push({
|
|
423
|
+
path: `${pathPrefix}.${key}.reporter`,
|
|
424
|
+
message: "must be a string or object",
|
|
425
|
+
fix: 'use "default", "tap", or { "name": "...", ... }',
|
|
426
|
+
});
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
const reporterObj = reporter;
|
|
430
|
+
validateUnknownKeys(reporterObj, REPORTER_OPTION_KEYS, `${pathPrefix}.${key}.reporter`, issues);
|
|
431
|
+
if ("name" in reporterObj && typeof reporterObj.name != "string") {
|
|
432
|
+
issues.push({
|
|
433
|
+
path: `${pathPrefix}.${key}.reporter.name`,
|
|
434
|
+
message: "must be a string",
|
|
435
|
+
fix: 'set to "default", "tap", or module path',
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
if (!("name" in reporterObj)) {
|
|
439
|
+
issues.push({
|
|
440
|
+
path: `${pathPrefix}.${key}.reporter`,
|
|
441
|
+
message: 'object reporter config requires "name"',
|
|
442
|
+
fix: 'example: { "name": "tap", "outDir": "./.as-test/reports" }',
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
if ("options" in reporterObj && !isStringArray(reporterObj.options)) {
|
|
446
|
+
issues.push({
|
|
447
|
+
path: `${pathPrefix}.${key}.reporter.options`,
|
|
448
|
+
message: "must be an array of strings",
|
|
449
|
+
fix: 'example: "options": ["single-file"]',
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
if ("outDir" in reporterObj && typeof reporterObj.outDir != "string") {
|
|
453
|
+
issues.push({
|
|
454
|
+
path: `${pathPrefix}.${key}.reporter.outDir`,
|
|
455
|
+
message: "must be a string",
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
if ("outFile" in reporterObj && typeof reporterObj.outFile != "string") {
|
|
459
|
+
issues.push({
|
|
460
|
+
path: `${pathPrefix}.${key}.reporter.outFile`,
|
|
461
|
+
message: "must be a string",
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
function validateModesField(raw, key, pathPrefix, issues) {
|
|
467
|
+
if (!(key in raw) || raw[key] == undefined)
|
|
468
|
+
return;
|
|
469
|
+
const value = raw[key];
|
|
470
|
+
if (!value || typeof value != "object" || Array.isArray(value)) {
|
|
471
|
+
issues.push({
|
|
472
|
+
path: `${pathPrefix}.${key}`,
|
|
473
|
+
message: "must be an object",
|
|
474
|
+
fix: 'example: "modes": { "wasi": { "buildOptions": { "target": "wasi" } } }',
|
|
475
|
+
});
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
for (const [modeName, modeRaw] of Object.entries(value)) {
|
|
479
|
+
if (!modeRaw || typeof modeRaw != "object" || Array.isArray(modeRaw)) {
|
|
480
|
+
issues.push({
|
|
481
|
+
path: `${pathPrefix}.${key}.${modeName}`,
|
|
482
|
+
message: "must be an object",
|
|
483
|
+
});
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
const modeObj = modeRaw;
|
|
487
|
+
const modePath = `${pathPrefix}.${key}.${modeName}`;
|
|
488
|
+
validateUnknownKeys(modeObj, MODE_KEYS, modePath, issues);
|
|
489
|
+
validateStringField(modeObj, "outDir", modePath, issues);
|
|
490
|
+
validateStringField(modeObj, "logs", modePath, issues);
|
|
491
|
+
validateStringField(modeObj, "coverageDir", modePath, issues);
|
|
492
|
+
validateStringField(modeObj, "snapshotDir", modePath, issues);
|
|
493
|
+
validateStringField(modeObj, "config", modePath, issues);
|
|
494
|
+
validateCoverageField(modeObj, "coverage", modePath, issues);
|
|
495
|
+
validateEnvField(modeObj, "env", modePath, issues);
|
|
496
|
+
validateBuildOptionsField(modeObj, "buildOptions", modePath, issues);
|
|
497
|
+
validateRunOptionsField(modeObj, "runOptions", modePath, issues);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
function isStringArray(value) {
|
|
501
|
+
return Array.isArray(value) && value.every((item) => typeof item == "string");
|
|
502
|
+
}
|
|
503
|
+
function resolveClosestKey(value, keys) {
|
|
504
|
+
let best = null;
|
|
505
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
506
|
+
for (const key of keys) {
|
|
507
|
+
const distance = levenshteinDistance(value, key);
|
|
508
|
+
if (distance < bestDistance) {
|
|
509
|
+
bestDistance = distance;
|
|
510
|
+
best = key;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
if (best && bestDistance <= 3)
|
|
514
|
+
return best;
|
|
515
|
+
return null;
|
|
516
|
+
}
|
|
517
|
+
function levenshteinDistance(left, right) {
|
|
518
|
+
if (left == right)
|
|
519
|
+
return 0;
|
|
520
|
+
if (!left.length)
|
|
521
|
+
return right.length;
|
|
522
|
+
if (!right.length)
|
|
523
|
+
return left.length;
|
|
524
|
+
const matrix = [];
|
|
525
|
+
for (let i = 0; i <= left.length; i++) {
|
|
526
|
+
matrix[i] = [i];
|
|
527
|
+
}
|
|
528
|
+
for (let j = 0; j <= right.length; j++) {
|
|
529
|
+
matrix[0][j] = j;
|
|
530
|
+
}
|
|
531
|
+
for (let i = 1; i <= left.length; i++) {
|
|
532
|
+
for (let j = 1; j <= right.length; j++) {
|
|
533
|
+
const cost = left[i - 1] == right[j - 1] ? 0 : 1;
|
|
534
|
+
matrix[i][j] = Math.min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j - 1] + cost);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return matrix[left.length][right.length];
|
|
538
|
+
}
|
|
539
|
+
function applyOutputConfig(rawOutput, rawConfig, config) {
|
|
540
|
+
if (rawOutput == undefined)
|
|
541
|
+
return;
|
|
542
|
+
if (typeof rawOutput == "string") {
|
|
543
|
+
applyOutputRoot(rawOutput, rawConfig, config);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
if (!rawOutput || typeof rawOutput != "object" || Array.isArray(rawOutput)) {
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
const output = rawOutput;
|
|
550
|
+
if ("build" in output &&
|
|
551
|
+
typeof output.build == "string" &&
|
|
552
|
+
output.build.length &&
|
|
553
|
+
!("outDir" in rawConfig)) {
|
|
554
|
+
config.outDir = output.build;
|
|
555
|
+
}
|
|
556
|
+
if ("logs" in output &&
|
|
557
|
+
typeof output.logs == "string" &&
|
|
558
|
+
output.logs.length &&
|
|
559
|
+
!("logs" in rawConfig)) {
|
|
560
|
+
config.logs = output.logs;
|
|
561
|
+
}
|
|
562
|
+
if ("coverage" in output &&
|
|
563
|
+
typeof output.coverage == "string" &&
|
|
564
|
+
output.coverage.length &&
|
|
565
|
+
!("coverageDir" in rawConfig)) {
|
|
566
|
+
config.coverageDir = output.coverage;
|
|
567
|
+
}
|
|
568
|
+
if ("snapshots" in output &&
|
|
569
|
+
typeof output.snapshots == "string" &&
|
|
570
|
+
output.snapshots.length &&
|
|
571
|
+
!("snapshotDir" in rawConfig)) {
|
|
572
|
+
config.snapshotDir = output.snapshots;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
function applyOutputRoot(root, rawConfig, config) {
|
|
576
|
+
if (!root.length)
|
|
577
|
+
return;
|
|
578
|
+
if (!("outDir" in rawConfig)) {
|
|
579
|
+
config.outDir = join(root, "build");
|
|
580
|
+
}
|
|
581
|
+
if (!("logs" in rawConfig)) {
|
|
582
|
+
config.logs = join(root, "logs");
|
|
583
|
+
}
|
|
584
|
+
if (!("coverageDir" in rawConfig)) {
|
|
585
|
+
config.coverageDir = join(root, "coverage");
|
|
586
|
+
}
|
|
587
|
+
if (!("snapshotDir" in rawConfig)) {
|
|
588
|
+
config.snapshotDir = join(root, "snapshots");
|
|
589
|
+
}
|
|
590
|
+
}
|
|
91
591
|
function parseModes(raw) {
|
|
92
592
|
if (!raw || typeof raw != "object" || Array.isArray(raw))
|
|
93
593
|
return {};
|
|
@@ -104,12 +604,10 @@ function parseModes(raw) {
|
|
|
104
604
|
if (typeof modeRaw.logs == "string" && modeRaw.logs.length) {
|
|
105
605
|
mode.logs = modeRaw.logs;
|
|
106
606
|
}
|
|
107
|
-
if (typeof modeRaw.coverageDir == "string" &&
|
|
108
|
-
modeRaw.coverageDir.length) {
|
|
607
|
+
if (typeof modeRaw.coverageDir == "string" && modeRaw.coverageDir.length) {
|
|
109
608
|
mode.coverageDir = modeRaw.coverageDir;
|
|
110
609
|
}
|
|
111
|
-
if (typeof modeRaw.snapshotDir == "string" &&
|
|
112
|
-
modeRaw.snapshotDir.length) {
|
|
610
|
+
if (typeof modeRaw.snapshotDir == "string" && modeRaw.snapshotDir.length) {
|
|
113
611
|
mode.snapshotDir = modeRaw.snapshotDir;
|
|
114
612
|
}
|
|
115
613
|
if (typeof modeRaw.config == "string" && modeRaw.config.length) {
|
|
@@ -144,8 +642,7 @@ function parseModes(raw) {
|
|
|
144
642
|
if (typeof runtimeRaw.cmd == "string" && runtimeRaw.cmd.length) {
|
|
145
643
|
runtime.cmd = runtimeRaw.cmd;
|
|
146
644
|
}
|
|
147
|
-
else if (typeof runtimeRaw.run == "string" &&
|
|
148
|
-
runtimeRaw.run.length) {
|
|
645
|
+
else if (typeof runtimeRaw.run == "string" && runtimeRaw.run.length) {
|
|
149
646
|
runtime.cmd = runtimeRaw.run;
|
|
150
647
|
}
|
|
151
648
|
else {
|
|
@@ -324,3 +821,57 @@ export function getExec(exec) {
|
|
|
324
821
|
}
|
|
325
822
|
return null;
|
|
326
823
|
}
|
|
824
|
+
export function tokenizeCommand(command) {
|
|
825
|
+
const out = [];
|
|
826
|
+
let current = "";
|
|
827
|
+
let quote = null;
|
|
828
|
+
let escaping = false;
|
|
829
|
+
for (let i = 0; i < command.length; i++) {
|
|
830
|
+
const ch = command[i];
|
|
831
|
+
if (escaping) {
|
|
832
|
+
current += ch;
|
|
833
|
+
escaping = false;
|
|
834
|
+
continue;
|
|
835
|
+
}
|
|
836
|
+
if (ch == "\\") {
|
|
837
|
+
if (quote == "'") {
|
|
838
|
+
current += ch;
|
|
839
|
+
}
|
|
840
|
+
else {
|
|
841
|
+
escaping = true;
|
|
842
|
+
}
|
|
843
|
+
continue;
|
|
844
|
+
}
|
|
845
|
+
if (quote) {
|
|
846
|
+
if (ch == quote) {
|
|
847
|
+
quote = null;
|
|
848
|
+
}
|
|
849
|
+
else {
|
|
850
|
+
current += ch;
|
|
851
|
+
}
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
if (ch == '"' || ch == "'") {
|
|
855
|
+
quote = ch;
|
|
856
|
+
continue;
|
|
857
|
+
}
|
|
858
|
+
if (/\s/.test(ch)) {
|
|
859
|
+
if (current.length) {
|
|
860
|
+
out.push(current);
|
|
861
|
+
current = "";
|
|
862
|
+
}
|
|
863
|
+
continue;
|
|
864
|
+
}
|
|
865
|
+
current += ch;
|
|
866
|
+
}
|
|
867
|
+
if (escaping) {
|
|
868
|
+
current += "\\";
|
|
869
|
+
}
|
|
870
|
+
if (quote) {
|
|
871
|
+
throw new Error(`unterminated quote in command: ${command}`);
|
|
872
|
+
}
|
|
873
|
+
if (current.length) {
|
|
874
|
+
out.push(current);
|
|
875
|
+
}
|
|
876
|
+
return out;
|
|
877
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "as-test",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.4",
|
|
4
4
|
"author": "Jairus Tanaka",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -63,9 +63,11 @@
|
|
|
63
63
|
},
|
|
64
64
|
"scripts": {
|
|
65
65
|
"test": "node ./bin/index.js test",
|
|
66
|
+
"typecheck": "tsc -p cli --noEmit && tsc -p transform --noEmit",
|
|
67
|
+
"lint": "eslint transform/src/**/*.ts tools/**/*.js eslint.config.js",
|
|
66
68
|
"build:transform": "tsc -p ./transform",
|
|
67
69
|
"build:cli": "tsc -p cli",
|
|
68
|
-
"build:run": "
|
|
70
|
+
"build:run": "npm run build:cli",
|
|
69
71
|
"prettier": "prettier -w .",
|
|
70
72
|
"release:check": "npm run build:cli && npm run build:transform && npm run test && npm pack --dry-run --cache /tmp/as-test-npm-cache",
|
|
71
73
|
"prepublishOnly": "npm run build:cli && npm run build:transform && npm run test"
|
package/transform/lib/index.js
CHANGED
|
@@ -26,7 +26,8 @@ export default class Transformer extends Transform {
|
|
|
26
26
|
return 0;
|
|
27
27
|
}
|
|
28
28
|
});
|
|
29
|
-
const
|
|
29
|
+
const entrySource = sources.find((v) => v.sourceKind == 1);
|
|
30
|
+
const entryFile = entrySource ? entrySource.simplePath : "unknown";
|
|
30
31
|
const mockedImportTargets = collectMockImportTargets(sources);
|
|
31
32
|
for (const target of mockedImportTargets) {
|
|
32
33
|
mock.importMocked.add(target);
|