as-test 1.1.10 → 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.
@@ -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: false,
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(