as-test 1.2.0 → 1.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/CHANGELOG.md +19 -0
- package/as-test.config.schema.json +15 -0
- package/assembly/coverage.ts +22 -26
- package/assembly/index.ts +2 -0
- package/assembly/src/expectation.ts +111 -38
- package/assembly/src/mode.ts +55 -0
- package/bin/commands/build-core.js +152 -7
- package/bin/commands/build.js +3 -1
- package/bin/commands/init-core.js +253 -5
- package/bin/commands/test.js +1 -1
- package/bin/index.js +60 -20
- package/bin/types.js +7 -0
- package/bin/util.js +43 -0
- package/package.json +3 -3
- package/transform/lib/index.js +26 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync } from "fs";
|
|
2
|
+
import { INTERNAL_FEATURE_NAMES, normalizeFeatureName } from "../types.js";
|
|
2
3
|
import { glob } from "glob";
|
|
3
4
|
import chalk from "chalk";
|
|
4
5
|
import { spawn } from "child_process";
|
|
@@ -67,6 +68,7 @@ export async function build(
|
|
|
67
68
|
a.localeCompare(b),
|
|
68
69
|
);
|
|
69
70
|
await assertNoArtifactCollisions(sourceInputPatterns);
|
|
71
|
+
warnOnUnknownModeReferences(inputFiles, loadedConfig.modes ?? {});
|
|
70
72
|
const coverageEnabled = resolveCoverageEnabled(
|
|
71
73
|
config.coverage,
|
|
72
74
|
featureToggles.coverage,
|
|
@@ -75,6 +77,7 @@ export async function build(
|
|
|
75
77
|
...mode.env,
|
|
76
78
|
...config.buildOptions.env,
|
|
77
79
|
AS_TEST_COVERAGE_ENABLED: coverageEnabled ? "1" : "0",
|
|
80
|
+
AS_TEST_MODE_NAME: modeName ?? "default",
|
|
78
81
|
};
|
|
79
82
|
if (
|
|
80
83
|
!resolvedConfig &&
|
|
@@ -259,6 +262,7 @@ export async function getBuildReuseInfo(
|
|
|
259
262
|
...mode.env,
|
|
260
263
|
...config.buildOptions.env,
|
|
261
264
|
AS_TEST_COVERAGE_ENABLED: coverageEnabled ? "1" : "0",
|
|
265
|
+
AS_TEST_MODE_NAME: modeName ?? "default",
|
|
262
266
|
};
|
|
263
267
|
return {
|
|
264
268
|
signature: JSON.stringify({
|
|
@@ -273,6 +277,129 @@ export async function getBuildReuseInfo(
|
|
|
273
277
|
function hasCustomBuildCommand(config) {
|
|
274
278
|
return !!config.buildOptions.cmd.trim().length;
|
|
275
279
|
}
|
|
280
|
+
// Scans input spec files for `mode([...], fn)` calls whose entries reference
|
|
281
|
+
// mode names not present in the configured set. Collects all (file, name)
|
|
282
|
+
// hits and prints a single formatted block to stdout. The implicit "default"
|
|
283
|
+
// name is only valid when no configured mode has `default: true` — otherwise
|
|
284
|
+
// a named mode always runs and `AS_TEST_MODE_NAME` is never literal "default".
|
|
285
|
+
const MODE_CALL_RE = /\bmode\s*\(\s*\[([^\]]*)\]/g;
|
|
286
|
+
const MODE_STRING_RE = /["']([^"']*)["']/g;
|
|
287
|
+
const STRIP_COMMENTS_RE = /\/\*[\s\S]*?\*\/|\/\/.*$/gm;
|
|
288
|
+
const reportedModeWarnings = new Set();
|
|
289
|
+
const pendingModeWarningsByFile = new Map();
|
|
290
|
+
// Scans input spec files for `mode([...], fn)` calls whose entries reference
|
|
291
|
+
// mode names not present in the configured set, and buffers them for later
|
|
292
|
+
// printing via flushModeWarnings(). Called as early as possible (before the
|
|
293
|
+
// reporter starts streaming progress). De-duplicates across invocations.
|
|
294
|
+
export function warnOnUnknownModeReferences(files, configuredModes) {
|
|
295
|
+
const modeEntries = Object.entries(configuredModes ?? {});
|
|
296
|
+
const fallsBackToImplicitDefault =
|
|
297
|
+
modeEntries.length === 0 ||
|
|
298
|
+
modeEntries.every(([, mode]) => mode?.default === false);
|
|
299
|
+
const knownModes = new Set(modeEntries.map(([name]) => name));
|
|
300
|
+
if (fallsBackToImplicitDefault) knownModes.add("default");
|
|
301
|
+
const knownList = [...knownModes].sort();
|
|
302
|
+
for (const file of files) {
|
|
303
|
+
let text;
|
|
304
|
+
try {
|
|
305
|
+
text = readFileSync(file, "utf8");
|
|
306
|
+
} catch {
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
text = text.replace(STRIP_COMMENTS_RE, "");
|
|
310
|
+
for (const callMatch of text.matchAll(MODE_CALL_RE)) {
|
|
311
|
+
const arrayContents = callMatch[1] ?? "";
|
|
312
|
+
for (const strMatch of arrayContents.matchAll(MODE_STRING_RE)) {
|
|
313
|
+
let value = strMatch[1] ?? "";
|
|
314
|
+
if (value.length === 0) continue;
|
|
315
|
+
if (value.charCodeAt(0) === 33 /* '!' */) value = value.slice(1);
|
|
316
|
+
if (value.length === 0) continue;
|
|
317
|
+
if (knownModes.has(value)) continue;
|
|
318
|
+
const key = `${file}\x1f${value}`;
|
|
319
|
+
if (reportedModeWarnings.has(key)) continue;
|
|
320
|
+
reportedModeWarnings.add(key);
|
|
321
|
+
const warning = {
|
|
322
|
+
name: value,
|
|
323
|
+
suggestion: closestKnownMode(value, knownList),
|
|
324
|
+
};
|
|
325
|
+
const list = pendingModeWarningsByFile.get(file);
|
|
326
|
+
if (list) list.push(warning);
|
|
327
|
+
else pendingModeWarningsByFile.set(file, [warning]);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
// Drains buffered mode warnings. When `showAll` is true, prints the full
|
|
333
|
+
// per-warning block; otherwise prints a one-line summary that tells the user
|
|
334
|
+
// to re-run with `--show-warnings`. No-op when there are no warnings.
|
|
335
|
+
export function flushModeWarnings(showAll) {
|
|
336
|
+
if (pendingModeWarningsByFile.size === 0) return;
|
|
337
|
+
const hits = [];
|
|
338
|
+
for (const [file, list] of pendingModeWarningsByFile) {
|
|
339
|
+
for (const w of list) {
|
|
340
|
+
hits.push({ file, name: w.name, suggestion: w.suggestion });
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
pendingModeWarningsByFile.clear();
|
|
344
|
+
if (hits.length === 0) return;
|
|
345
|
+
if (!showAll) {
|
|
346
|
+
const count = hits.length;
|
|
347
|
+
const noun = count === 1 ? "warning" : "warnings";
|
|
348
|
+
process.stdout.write(
|
|
349
|
+
`\nFound ${chalk.yellow.bold(count)} ${noun}. Run with ${chalk.dim("--show-warnings")} to view.\n`,
|
|
350
|
+
);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const lines = [chalk.yellow.bold("WARNINGS:")];
|
|
354
|
+
for (const hit of hits) {
|
|
355
|
+
let line = ` - unknown mode reference ${chalk.bold(`"${hit.name}"`)} in ${chalk.dim(hit.file)}`;
|
|
356
|
+
if (hit.suggestion) {
|
|
357
|
+
line += ` - did you mean ${chalk.cyan(`"${hit.suggestion}"`)}?`;
|
|
358
|
+
}
|
|
359
|
+
lines.push(line);
|
|
360
|
+
}
|
|
361
|
+
process.stdout.write("\n" + lines.join("\n") + "\n");
|
|
362
|
+
}
|
|
363
|
+
// Returns the configured mode whose Levenshtein distance to `name` is below
|
|
364
|
+
// a small threshold (proportional to the longer string's length). Returns
|
|
365
|
+
// null when nothing's close enough — avoids suggesting wildly different names.
|
|
366
|
+
function closestKnownMode(name, candidates) {
|
|
367
|
+
let best = null;
|
|
368
|
+
let bestDist = Infinity;
|
|
369
|
+
for (const candidate of candidates) {
|
|
370
|
+
const d = levenshtein(name, candidate);
|
|
371
|
+
const threshold = Math.max(
|
|
372
|
+
2,
|
|
373
|
+
Math.floor(Math.max(name.length, candidate.length) * 0.4),
|
|
374
|
+
);
|
|
375
|
+
if (d < bestDist && d <= threshold) {
|
|
376
|
+
bestDist = d;
|
|
377
|
+
best = candidate;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return best;
|
|
381
|
+
}
|
|
382
|
+
function levenshtein(a, b) {
|
|
383
|
+
const m = a.length;
|
|
384
|
+
const n = b.length;
|
|
385
|
+
if (m === 0) return n;
|
|
386
|
+
if (n === 0) return m;
|
|
387
|
+
let prev = new Array(n + 1);
|
|
388
|
+
let curr = new Array(n + 1);
|
|
389
|
+
for (let j = 0; j <= n; j++) prev[j] = j;
|
|
390
|
+
for (let i = 1; i <= m; i++) {
|
|
391
|
+
curr[0] = i;
|
|
392
|
+
for (let j = 1; j <= n; j++) {
|
|
393
|
+
const cost = a.charCodeAt(i - 1) === b.charCodeAt(j - 1) ? 0 : 1;
|
|
394
|
+
const del = prev[j] + 1;
|
|
395
|
+
const ins = curr[j - 1] + 1;
|
|
396
|
+
const sub = prev[j - 1] + cost;
|
|
397
|
+
curr[j] = Math.min(del, ins, sub);
|
|
398
|
+
}
|
|
399
|
+
[prev, curr] = [curr, prev];
|
|
400
|
+
}
|
|
401
|
+
return prev[n];
|
|
402
|
+
}
|
|
276
403
|
function getBuildCommand(
|
|
277
404
|
config,
|
|
278
405
|
pkgRunner,
|
|
@@ -570,7 +697,8 @@ function getBuildStdout(error) {
|
|
|
570
697
|
}
|
|
571
698
|
function getDefaultBuildArgs(config, featureToggles) {
|
|
572
699
|
const buildArgs = [];
|
|
573
|
-
const
|
|
700
|
+
const effectiveFeatures = resolveEffectiveFeatures(config, featureToggles);
|
|
701
|
+
const tryAsEnabled = resolveTryAsEnabled(effectiveFeatures.has("try-as"));
|
|
574
702
|
buildArgs.push("--transform", "as-test/transform");
|
|
575
703
|
if (
|
|
576
704
|
resolveProjectModule("json-as/transform") &&
|
|
@@ -587,6 +715,10 @@ function getDefaultBuildArgs(config, featureToggles) {
|
|
|
587
715
|
if (tryAsEnabled) {
|
|
588
716
|
buildArgs.push("--use", "AS_TEST_TRY_AS=1");
|
|
589
717
|
}
|
|
718
|
+
for (const feature of effectiveFeatures) {
|
|
719
|
+
if (INTERNAL_FEATURE_NAMES.has(feature)) continue;
|
|
720
|
+
buildArgs.push("--enable", feature);
|
|
721
|
+
}
|
|
590
722
|
// Should also strip any bindings-enabling from asconfig
|
|
591
723
|
if (
|
|
592
724
|
config.buildOptions.target == "bindings" ||
|
|
@@ -685,16 +817,29 @@ function transformsContainJsonAs(value) {
|
|
|
685
817
|
}
|
|
686
818
|
return false;
|
|
687
819
|
}
|
|
688
|
-
function
|
|
689
|
-
const
|
|
690
|
-
|
|
691
|
-
|
|
820
|
+
function resolveEffectiveFeatures(config, featureToggles) {
|
|
821
|
+
const effective = new Set();
|
|
822
|
+
for (const name of config.features) {
|
|
823
|
+
effective.add(normalizeFeatureName(name));
|
|
824
|
+
}
|
|
825
|
+
const overrides = featureToggles.featureOverrides ?? {};
|
|
826
|
+
for (const [name, enabled] of Object.entries(overrides)) {
|
|
827
|
+
const key = normalizeFeatureName(name);
|
|
828
|
+
if (!key.length) continue;
|
|
829
|
+
if (enabled) effective.add(key);
|
|
830
|
+
else effective.delete(key);
|
|
831
|
+
}
|
|
832
|
+
effective.delete("");
|
|
833
|
+
return effective;
|
|
834
|
+
}
|
|
835
|
+
function resolveTryAsEnabled(enabled) {
|
|
836
|
+
if (!enabled) return false;
|
|
837
|
+
if (!hasTryAsRuntime()) {
|
|
692
838
|
throw new Error(
|
|
693
839
|
'try-as feature was enabled, but package "try-as" is not installed',
|
|
694
840
|
);
|
|
695
841
|
}
|
|
696
|
-
|
|
697
|
-
return false;
|
|
842
|
+
return true;
|
|
698
843
|
}
|
|
699
844
|
function resolveCoverageEnabled(rawCoverage, override) {
|
|
700
845
|
if (override != undefined) return override;
|
package/bin/commands/build.js
CHANGED
|
@@ -5,6 +5,8 @@ export {
|
|
|
5
5
|
formatInvocation,
|
|
6
6
|
getBuildInvocationPreview,
|
|
7
7
|
getBuildReuseInfo,
|
|
8
|
+
warnOnUnknownModeReferences,
|
|
9
|
+
flushModeWarnings,
|
|
8
10
|
} from "./build-core.js";
|
|
9
11
|
export async function executeBuildCommand(
|
|
10
12
|
rawArgs,
|
|
@@ -17,8 +19,8 @@ export async function executeBuildCommand(
|
|
|
17
19
|
const featureToggles = deps.resolveFeatureToggles(rawArgs, "build");
|
|
18
20
|
const parallel = deps.resolveBuildParallelJobs(rawArgs);
|
|
19
21
|
const buildFeatureToggles = {
|
|
20
|
-
tryAs: featureToggles.tryAs,
|
|
21
22
|
coverage: featureToggles.coverage,
|
|
23
|
+
featureOverrides: featureToggles.featureOverrides,
|
|
22
24
|
};
|
|
23
25
|
const modeTargets = deps.resolveExecutionModes(configPath, selectedModes);
|
|
24
26
|
if (listFlags.list || listFlags.listModes) {
|
|
@@ -7,6 +7,12 @@ import { getCliVersion } from "../util.js";
|
|
|
7
7
|
import { buildWebRunnerSource } from "./web-runner-source.js";
|
|
8
8
|
const TARGETS = ["wasi", "bindings", "web"];
|
|
9
9
|
const EXAMPLE_MODES = ["minimal", "full", "none"];
|
|
10
|
+
const FEATURE_KEYS = ["coverage", "tryAs"];
|
|
11
|
+
const FEATURE_LABELS = {
|
|
12
|
+
coverage: "coverage (runtime coverage points + report)",
|
|
13
|
+
tryAs:
|
|
14
|
+
"try-as (try/catch/finally + toThrow assertions + throwable rewriting)",
|
|
15
|
+
};
|
|
10
16
|
export async function init(rawArgs) {
|
|
11
17
|
const options = parseInitArgs(rawArgs);
|
|
12
18
|
const rl = options.yes
|
|
@@ -23,6 +29,10 @@ export async function init(rawArgs) {
|
|
|
23
29
|
target: options.target ?? "wasi",
|
|
24
30
|
example: options.example ?? "minimal",
|
|
25
31
|
fuzzExample: options.fuzzExample ?? false,
|
|
32
|
+
features: resolveFeatures(options.features, {
|
|
33
|
+
coverage: false,
|
|
34
|
+
tryAs: false,
|
|
35
|
+
}),
|
|
26
36
|
installDependenciesNow: options.install ?? false,
|
|
27
37
|
}
|
|
28
38
|
: await runInteractiveOnboarding(options, rl);
|
|
@@ -35,6 +45,7 @@ export async function init(rawArgs) {
|
|
|
35
45
|
answers.target,
|
|
36
46
|
answers.example,
|
|
37
47
|
answers.fuzzExample,
|
|
48
|
+
answers.features,
|
|
38
49
|
answers.installDependenciesNow,
|
|
39
50
|
);
|
|
40
51
|
if (!options.yes) {
|
|
@@ -49,6 +60,7 @@ export async function init(rawArgs) {
|
|
|
49
60
|
answers.target,
|
|
50
61
|
answers.example,
|
|
51
62
|
answers.fuzzExample,
|
|
63
|
+
answers.features,
|
|
52
64
|
options.force,
|
|
53
65
|
);
|
|
54
66
|
printSummary(summary);
|
|
@@ -67,6 +79,7 @@ export async function init(rawArgs) {
|
|
|
67
79
|
}
|
|
68
80
|
function parseInitArgs(rawArgs) {
|
|
69
81
|
const options = {
|
|
82
|
+
features: {},
|
|
70
83
|
yes: false,
|
|
71
84
|
force: false,
|
|
72
85
|
dir: ".",
|
|
@@ -95,6 +108,30 @@ function parseInitArgs(rawArgs) {
|
|
|
95
108
|
options.fuzzExample = false;
|
|
96
109
|
continue;
|
|
97
110
|
}
|
|
111
|
+
if (arg == "--enable" || arg == "--disable") {
|
|
112
|
+
const next = rawArgs[i + 1];
|
|
113
|
+
if (!next || next.startsWith("-")) {
|
|
114
|
+
throw new Error(`${arg} requires a value: coverage|try-as`);
|
|
115
|
+
}
|
|
116
|
+
for (const name of splitInitFeatureList(next)) {
|
|
117
|
+
applyInitFeatureToggle(options.features, name, arg == "--enable");
|
|
118
|
+
}
|
|
119
|
+
i++;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (arg.startsWith("--enable=") || arg.startsWith("--disable=")) {
|
|
123
|
+
const eq = arg.indexOf("=");
|
|
124
|
+
const flag = arg.slice(0, eq);
|
|
125
|
+
const value = arg.slice(eq + 1);
|
|
126
|
+
const names = splitInitFeatureList(value);
|
|
127
|
+
if (!names.length) {
|
|
128
|
+
throw new Error(`${flag} requires a value: coverage|try-as`);
|
|
129
|
+
}
|
|
130
|
+
for (const name of names) {
|
|
131
|
+
applyInitFeatureToggle(options.features, name, flag == "--enable");
|
|
132
|
+
}
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
98
135
|
if (arg == "--target") {
|
|
99
136
|
const next = rawArgs[i + 1];
|
|
100
137
|
if (next && !next.startsWith("-")) {
|
|
@@ -158,11 +195,37 @@ function parseInitArgs(rawArgs) {
|
|
|
158
195
|
}
|
|
159
196
|
if (positional.length > 0) {
|
|
160
197
|
throw new Error(
|
|
161
|
-
`Unknown init argument(s): ${positional.join(", ")}. Usage: init [dir] [--target wasi|bindings|web] [--example minimal|full|none] [--fuzz-example|--no-fuzz-example] [--install] [--yes] [--force] [--dir <path>]`,
|
|
198
|
+
`Unknown init argument(s): ${positional.join(", ")}. Usage: init [dir] [--target wasi|bindings|web] [--example minimal|full|none] [--fuzz-example|--no-fuzz-example] [--enable coverage|try-as] [--disable coverage|try-as] [--install] [--yes] [--force] [--dir <path>]`,
|
|
162
199
|
);
|
|
163
200
|
}
|
|
164
201
|
return options;
|
|
165
202
|
}
|
|
203
|
+
function splitInitFeatureList(value) {
|
|
204
|
+
return value
|
|
205
|
+
.split(",")
|
|
206
|
+
.map((part) => part.trim())
|
|
207
|
+
.filter((part) => part.length > 0);
|
|
208
|
+
}
|
|
209
|
+
function applyInitFeatureToggle(out, rawFeature, enabled) {
|
|
210
|
+
const key = rawFeature.trim().toLowerCase();
|
|
211
|
+
if (key == "coverage") {
|
|
212
|
+
out.coverage = enabled;
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (key == "try-as" || key == "try_as" || key == "tryas") {
|
|
216
|
+
out.tryAs = enabled;
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
throw new Error(
|
|
220
|
+
`unknown feature "${rawFeature}". Supported features: coverage, try-as`,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
function resolveFeatures(overrides, defaults) {
|
|
224
|
+
return {
|
|
225
|
+
coverage: overrides.coverage ?? defaults.coverage,
|
|
226
|
+
tryAs: overrides.tryAs ?? defaults.tryAs,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
166
229
|
async function runInteractiveOnboarding(options, face) {
|
|
167
230
|
printOnboardingIntro();
|
|
168
231
|
const acknowledged = await askYesNo(
|
|
@@ -231,6 +294,25 @@ async function runInteractiveOnboarding(options, face) {
|
|
|
231
294
|
if (options.target || onboardingMode == "quick") {
|
|
232
295
|
printPromptAndSelectionLine("Build target", target);
|
|
233
296
|
}
|
|
297
|
+
const featureDefaults = { coverage: false, tryAs: false };
|
|
298
|
+
const explicitFeatures =
|
|
299
|
+
options.features.coverage !== undefined ||
|
|
300
|
+
options.features.tryAs !== undefined;
|
|
301
|
+
const features =
|
|
302
|
+
explicitFeatures || onboardingMode == "quick"
|
|
303
|
+
? resolveFeatures(options.features, featureDefaults)
|
|
304
|
+
: await askMultiToggle(
|
|
305
|
+
"Features (↑/↓ to move, space to toggle, enter to confirm)",
|
|
306
|
+
FEATURE_KEYS.map((key) => ({
|
|
307
|
+
value: key,
|
|
308
|
+
label: FEATURE_LABELS[key],
|
|
309
|
+
})),
|
|
310
|
+
face,
|
|
311
|
+
resolveFeatures(options.features, featureDefaults),
|
|
312
|
+
);
|
|
313
|
+
if (explicitFeatures || onboardingMode == "quick") {
|
|
314
|
+
printPromptAndSelectionLine("Features", formatFeatureSelection(features));
|
|
315
|
+
}
|
|
234
316
|
const example =
|
|
235
317
|
options.example ??
|
|
236
318
|
(onboardingMode == "quick"
|
|
@@ -275,9 +357,16 @@ async function runInteractiveOnboarding(options, face) {
|
|
|
275
357
|
target,
|
|
276
358
|
example,
|
|
277
359
|
fuzzExample,
|
|
360
|
+
features,
|
|
278
361
|
installDependenciesNow,
|
|
279
362
|
};
|
|
280
363
|
}
|
|
364
|
+
function formatFeatureSelection(features) {
|
|
365
|
+
const labels = [];
|
|
366
|
+
if (features.coverage) labels.push("coverage");
|
|
367
|
+
if (features.tryAs) labels.push("try-as");
|
|
368
|
+
return labels.length ? labels.join(", ") : "none";
|
|
369
|
+
}
|
|
281
370
|
function printOnboardingHeader() {
|
|
282
371
|
// console.log(
|
|
283
372
|
// chalk.bold.cyan(
|
|
@@ -375,7 +464,7 @@ function isTarget(value) {
|
|
|
375
464
|
function isExampleMode(value) {
|
|
376
465
|
return EXAMPLE_MODES.includes(value);
|
|
377
466
|
}
|
|
378
|
-
function printPlan(root, target, example, fuzzExample, install) {
|
|
467
|
+
function printPlan(root, target, example, fuzzExample, features, install) {
|
|
379
468
|
const displayRoot = () => {
|
|
380
469
|
const rel = path.relative(process.cwd(), root).split(path.sep).join("/");
|
|
381
470
|
if (!rel || rel == ".") return "./";
|
|
@@ -483,6 +572,9 @@ function printPlan(root, target, example, fuzzExample, install) {
|
|
|
483
572
|
console.log(
|
|
484
573
|
"│" + chalk.dim(` - Fuzzer example: ${fuzzExample ? "yes" : "no"}`),
|
|
485
574
|
);
|
|
575
|
+
console.log(
|
|
576
|
+
"│" + chalk.dim(` - Features: ${formatFeatureSelection(features)}`),
|
|
577
|
+
);
|
|
486
578
|
console.log("│" + chalk.dim(` - Directory: ${displayRoot()}`));
|
|
487
579
|
console.log(
|
|
488
580
|
"│" + chalk.dim(` - Install dependencies: ${install ? "yes" : "no"}`),
|
|
@@ -494,7 +586,7 @@ function printPlan(root, target, example, fuzzExample, install) {
|
|
|
494
586
|
}
|
|
495
587
|
console.log("│");
|
|
496
588
|
}
|
|
497
|
-
function applyInit(root, target, example, fuzzExample, force) {
|
|
589
|
+
function applyInit(root, target, example, fuzzExample, features, force) {
|
|
498
590
|
const summary = {
|
|
499
591
|
created: [],
|
|
500
592
|
updated: [],
|
|
@@ -518,13 +610,16 @@ function applyInit(root, target, example, fuzzExample, force) {
|
|
|
518
610
|
summary,
|
|
519
611
|
"assembly/tsconfig.json",
|
|
520
612
|
);
|
|
613
|
+
const featuresArray = [];
|
|
614
|
+
if (features.tryAs) featuresArray.push("try-as");
|
|
521
615
|
const configPath = path.join(root, "as-test.config.json");
|
|
522
616
|
const config = {
|
|
523
617
|
$schema: "node_modules/as-test/as-test.config.schema.json",
|
|
524
618
|
input: ["assembly/__tests__/*.spec.ts"],
|
|
525
619
|
output: ".as-test/",
|
|
526
620
|
config: "none",
|
|
527
|
-
coverage:
|
|
621
|
+
coverage: features.coverage,
|
|
622
|
+
features: featuresArray,
|
|
528
623
|
env: {},
|
|
529
624
|
...(fuzzExample
|
|
530
625
|
? {
|
|
@@ -659,6 +754,9 @@ function applyInit(root, target, example, fuzzExample, force) {
|
|
|
659
754
|
if (target == "wasi" && !devDependencies["@assemblyscript/wasi-shim"]) {
|
|
660
755
|
devDependencies["@assemblyscript/wasi-shim"] = "^0.1.0";
|
|
661
756
|
}
|
|
757
|
+
if (features.tryAs && !hasDependency(pkg, "try-as")) {
|
|
758
|
+
devDependencies["try-as"] = "^1.1.0";
|
|
759
|
+
}
|
|
662
760
|
if (target == "bindings" && !pkg.type) {
|
|
663
761
|
pkg.type = "module";
|
|
664
762
|
}
|
|
@@ -814,6 +912,21 @@ async function askMenuChoice(label, choices, face, fallback) {
|
|
|
814
912
|
}
|
|
815
913
|
return askMenuChoiceWithArrows(label, choices, face, fallbackValue);
|
|
816
914
|
}
|
|
915
|
+
async function askMultiToggle(label, choices, face, initial) {
|
|
916
|
+
if (!face) return { ...initial };
|
|
917
|
+
if (canUseArrowMenu(face)) {
|
|
918
|
+
return askMultiToggleWithArrows(label, choices, face, initial);
|
|
919
|
+
}
|
|
920
|
+
const result = { ...initial };
|
|
921
|
+
for (const choice of choices) {
|
|
922
|
+
result[choice.value] = await askYesNo(
|
|
923
|
+
`${label} — enable ${choice.label}?`,
|
|
924
|
+
face,
|
|
925
|
+
initial[choice.value],
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
return result;
|
|
929
|
+
}
|
|
817
930
|
async function askYesNo(label, face, fallback) {
|
|
818
931
|
if (!face) return fallback;
|
|
819
932
|
if (canUseArrowMenu(face)) {
|
|
@@ -887,7 +1000,7 @@ async function askMenuChoiceWithArrows(label, choices, face, fallback) {
|
|
|
887
1000
|
}
|
|
888
1001
|
const totalLineCount = Math.max(lines.length, renderedLineCount);
|
|
889
1002
|
for (let i = 0; i < totalLineCount; i++) {
|
|
890
|
-
process.stdout.write("\x1b[2K");
|
|
1003
|
+
process.stdout.write("\r\x1b[2K");
|
|
891
1004
|
if (i < lines.length) {
|
|
892
1005
|
process.stdout.write(lines[i]);
|
|
893
1006
|
}
|
|
@@ -987,6 +1100,141 @@ async function askMenuChoiceWithArrows(label, choices, face, fallback) {
|
|
|
987
1100
|
writeLines(menuLines());
|
|
988
1101
|
});
|
|
989
1102
|
}
|
|
1103
|
+
async function askMultiToggleWithArrows(label, choices, face, initial) {
|
|
1104
|
+
const stdin = process.stdin;
|
|
1105
|
+
const stdout = process.stdout;
|
|
1106
|
+
const selected = { ...initial };
|
|
1107
|
+
let cursorIndex = 0;
|
|
1108
|
+
let renderedLineCount = 0;
|
|
1109
|
+
const previousRawMode = Boolean(stdin.isRaw);
|
|
1110
|
+
const lineWidth = Math.max(20, (stdout.columns ?? 80) - 2);
|
|
1111
|
+
const clamp = (value, max) => {
|
|
1112
|
+
if (value.length <= max) return value;
|
|
1113
|
+
if (max <= 1) return value.slice(0, max);
|
|
1114
|
+
return `${value.slice(0, max - 1)}…`;
|
|
1115
|
+
};
|
|
1116
|
+
const titleLine = () =>
|
|
1117
|
+
chalk.bold.blue(`◆ ${clamp(label, Math.max(8, lineWidth - 3))}`);
|
|
1118
|
+
const menuLines = () => {
|
|
1119
|
+
const lines = [titleLine()];
|
|
1120
|
+
for (let i = 0; i < choices.length; i++) {
|
|
1121
|
+
const choice = choices[i];
|
|
1122
|
+
const isOn = Boolean(selected[choice.value]);
|
|
1123
|
+
const cursor = i == cursorIndex ? chalk.blue("›") : " ";
|
|
1124
|
+
const marker = isOn ? chalk.blue("●") : chalk.dim("○");
|
|
1125
|
+
const text = clamp(choice.label, Math.max(8, lineWidth - 6));
|
|
1126
|
+
const painted = i == cursorIndex ? chalk.bold(text) : text;
|
|
1127
|
+
lines.push(`│ ${cursor} ${marker} ${painted}`);
|
|
1128
|
+
}
|
|
1129
|
+
lines.push("│");
|
|
1130
|
+
return lines;
|
|
1131
|
+
};
|
|
1132
|
+
const collapsedLines = () => {
|
|
1133
|
+
const enabled = choices
|
|
1134
|
+
.filter((choice) => selected[choice.value])
|
|
1135
|
+
.map((choice) => choice.value);
|
|
1136
|
+
const summary = enabled.length ? enabled.join(", ") : "none";
|
|
1137
|
+
return [`│ ${chalk.gray(clamp(summary, Math.max(8, lineWidth - 4)))}`];
|
|
1138
|
+
};
|
|
1139
|
+
const writeLines = (lines) => {
|
|
1140
|
+
if (renderedLineCount > 0) {
|
|
1141
|
+
process.stdout.write(`\x1b[${renderedLineCount}A`);
|
|
1142
|
+
}
|
|
1143
|
+
const totalLineCount = Math.max(lines.length, renderedLineCount);
|
|
1144
|
+
for (let i = 0; i < totalLineCount; i++) {
|
|
1145
|
+
process.stdout.write("\r\x1b[2K");
|
|
1146
|
+
if (i < lines.length) {
|
|
1147
|
+
process.stdout.write(lines[i]);
|
|
1148
|
+
}
|
|
1149
|
+
process.stdout.write("\n");
|
|
1150
|
+
}
|
|
1151
|
+
renderedLineCount = lines.length;
|
|
1152
|
+
};
|
|
1153
|
+
const collapseInPlace = () => {
|
|
1154
|
+
const lines = collapsedLines();
|
|
1155
|
+
if (renderedLineCount > 0) {
|
|
1156
|
+
process.stdout.write(`\x1b[${renderedLineCount}A`);
|
|
1157
|
+
}
|
|
1158
|
+
const totalLineCount = Math.max(renderedLineCount, lines.length);
|
|
1159
|
+
for (let i = 0; i < totalLineCount; i++) {
|
|
1160
|
+
process.stdout.write("\r\x1b[2K");
|
|
1161
|
+
if (i < lines.length) {
|
|
1162
|
+
process.stdout.write(lines[i]);
|
|
1163
|
+
}
|
|
1164
|
+
process.stdout.write("\n");
|
|
1165
|
+
}
|
|
1166
|
+
const extraLines = totalLineCount - lines.length;
|
|
1167
|
+
if (extraLines > 0) {
|
|
1168
|
+
process.stdout.write(`\x1b[${extraLines}A`);
|
|
1169
|
+
}
|
|
1170
|
+
renderedLineCount = 0;
|
|
1171
|
+
};
|
|
1172
|
+
return new Promise((resolve, reject) => {
|
|
1173
|
+
let settled = false;
|
|
1174
|
+
const cleanup = () => {
|
|
1175
|
+
stdin.off("data", onData);
|
|
1176
|
+
if (stdin.isTTY) {
|
|
1177
|
+
stdin.setRawMode(previousRawMode);
|
|
1178
|
+
}
|
|
1179
|
+
const isClosed = Boolean(face.closed);
|
|
1180
|
+
if (!isClosed) {
|
|
1181
|
+
try {
|
|
1182
|
+
face.resume();
|
|
1183
|
+
} catch {
|
|
1184
|
+
// noop: readline may already be closed during shutdown/cancel paths.
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
};
|
|
1188
|
+
const finish = () => {
|
|
1189
|
+
if (settled) return;
|
|
1190
|
+
settled = true;
|
|
1191
|
+
collapseInPlace();
|
|
1192
|
+
cleanup();
|
|
1193
|
+
resolve(selected);
|
|
1194
|
+
};
|
|
1195
|
+
const fail = (error) => {
|
|
1196
|
+
if (settled) return;
|
|
1197
|
+
settled = true;
|
|
1198
|
+
cleanup();
|
|
1199
|
+
reject(error);
|
|
1200
|
+
};
|
|
1201
|
+
const onData = (chunk) => {
|
|
1202
|
+
const input = typeof chunk == "string" ? chunk : chunk.toString("utf8");
|
|
1203
|
+
if (!input.length) return;
|
|
1204
|
+
if (input == "\u0003") {
|
|
1205
|
+
fail(new Error(chalk.bold.red("◆ Cancelled")));
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
if (input == "\x1b[A" || input == "\x1bOA") {
|
|
1209
|
+
cursorIndex = (cursorIndex - 1 + choices.length) % choices.length;
|
|
1210
|
+
writeLines(menuLines());
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
if (input == "\x1b[B" || input == "\x1bOB") {
|
|
1214
|
+
cursorIndex = (cursorIndex + 1) % choices.length;
|
|
1215
|
+
writeLines(menuLines());
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
if (input == " ") {
|
|
1219
|
+
const key = choices[cursorIndex].value;
|
|
1220
|
+
selected[key] = !selected[key];
|
|
1221
|
+
writeLines(menuLines());
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
if (input == "\r" || input == "\n") {
|
|
1225
|
+
finish();
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
};
|
|
1229
|
+
face.pause();
|
|
1230
|
+
if (stdin.isTTY) {
|
|
1231
|
+
stdin.setRawMode(true);
|
|
1232
|
+
}
|
|
1233
|
+
stdin.resume();
|
|
1234
|
+
stdin.on("data", onData);
|
|
1235
|
+
writeLines(menuLines());
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
990
1238
|
function installDependencies(root) {
|
|
991
1239
|
const install = resolveInstallCommand(root);
|
|
992
1240
|
console.log(
|
package/bin/commands/test.js
CHANGED
|
@@ -11,8 +11,8 @@ export async function executeTestCommand(
|
|
|
11
11
|
const listFlags = deps.resolveListFlags(rawArgs, "test");
|
|
12
12
|
const featureToggles = deps.resolveFeatureToggles(rawArgs, "test");
|
|
13
13
|
const buildFeatureToggles = {
|
|
14
|
-
tryAs: featureToggles.tryAs,
|
|
15
14
|
coverage: featureToggles.coverage,
|
|
15
|
+
featureOverrides: featureToggles.featureOverrides,
|
|
16
16
|
};
|
|
17
17
|
const showCoverageMode = deps.resolveShowCoverageMode(rawArgs, "test");
|
|
18
18
|
const runFlags = {
|