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
|
@@ -0,0 +1,991 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { spawnSync } from "child_process";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
import { createInterface } from "readline";
|
|
6
|
+
import { getCliVersion } from "../util.js";
|
|
7
|
+
const TARGETS = ["wasi", "bindings"];
|
|
8
|
+
const EXAMPLE_MODES = ["minimal", "full", "none"];
|
|
9
|
+
export async function init(rawArgs) {
|
|
10
|
+
const options = parseInitArgs(rawArgs);
|
|
11
|
+
const rl = options.yes
|
|
12
|
+
? null
|
|
13
|
+
: createInterface({
|
|
14
|
+
input: process.stdin,
|
|
15
|
+
output: process.stdout,
|
|
16
|
+
});
|
|
17
|
+
try {
|
|
18
|
+
printOnboardingHeader();
|
|
19
|
+
const answers = options.yes
|
|
20
|
+
? {
|
|
21
|
+
root: path.resolve(process.cwd(), options.dir),
|
|
22
|
+
target: options.target ?? "wasi",
|
|
23
|
+
example: options.example ?? "minimal",
|
|
24
|
+
installDependenciesNow: options.install ?? false,
|
|
25
|
+
}
|
|
26
|
+
: await runInteractiveOnboarding(options, rl);
|
|
27
|
+
if (!answers) {
|
|
28
|
+
console.log(chalk.bold.red("◆ Cancelled"));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
printPlan(answers.root, answers.target, answers.example, answers.installDependenciesNow);
|
|
32
|
+
if (!options.yes) {
|
|
33
|
+
const cont = await askYesNo("Continue with these changes?", rl, true);
|
|
34
|
+
if (!cont) {
|
|
35
|
+
console.log(chalk.bold.red("◆ Cancelled"));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const summary = applyInit(answers.root, answers.target, answers.example, options.force);
|
|
40
|
+
printSummary(summary);
|
|
41
|
+
console.log(chalk.bold.green("◆ Finished!"));
|
|
42
|
+
if (answers.installDependenciesNow) {
|
|
43
|
+
installDependencies(answers.root);
|
|
44
|
+
console.log("\nNow, run " + chalk.italic.bold("npm test") + "\n");
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
console.log("\nNow, run " + chalk.italic.bold("npm i && npm test") + "\n");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
rl?.close();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function parseInitArgs(rawArgs) {
|
|
55
|
+
const options = {
|
|
56
|
+
yes: false,
|
|
57
|
+
force: false,
|
|
58
|
+
dir: ".",
|
|
59
|
+
dirExplicit: false,
|
|
60
|
+
};
|
|
61
|
+
const positional = [];
|
|
62
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
63
|
+
const arg = rawArgs[i];
|
|
64
|
+
if (arg == "--yes" || arg == "-y") {
|
|
65
|
+
options.yes = true;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (arg == "--force") {
|
|
69
|
+
options.force = true;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (arg == "--install") {
|
|
73
|
+
options.install = true;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (arg == "--target") {
|
|
77
|
+
const next = rawArgs[i + 1];
|
|
78
|
+
if (next && !next.startsWith("-")) {
|
|
79
|
+
options.target = parseTarget(next);
|
|
80
|
+
i++;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
throw new Error("--target requires a value: wasi|bindings");
|
|
84
|
+
}
|
|
85
|
+
if (arg.startsWith("--target=")) {
|
|
86
|
+
options.target = parseTarget(arg.slice("--target=".length));
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (arg == "--example") {
|
|
90
|
+
const next = rawArgs[i + 1];
|
|
91
|
+
if (next && !next.startsWith("-")) {
|
|
92
|
+
options.example = parseExampleMode(next);
|
|
93
|
+
i++;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
throw new Error("--example requires a value: minimal|full|none");
|
|
97
|
+
}
|
|
98
|
+
if (arg.startsWith("--example=")) {
|
|
99
|
+
options.example = parseExampleMode(arg.slice("--example=".length));
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (arg == "--dir") {
|
|
103
|
+
const next = rawArgs[i + 1];
|
|
104
|
+
if (next && !next.startsWith("-")) {
|
|
105
|
+
options.dir = next;
|
|
106
|
+
options.dirExplicit = true;
|
|
107
|
+
i++;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
throw new Error("--dir requires a path value");
|
|
111
|
+
}
|
|
112
|
+
if (arg.startsWith("--dir=")) {
|
|
113
|
+
options.dir = arg.slice("--dir=".length);
|
|
114
|
+
options.dirExplicit = true;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (arg.startsWith("-")) {
|
|
118
|
+
throw new Error(`Unknown init flag: ${arg}`);
|
|
119
|
+
}
|
|
120
|
+
positional.push(arg);
|
|
121
|
+
}
|
|
122
|
+
// First positional argument is always the target directory.
|
|
123
|
+
if (positional.length > 0) {
|
|
124
|
+
options.dir = positional.shift();
|
|
125
|
+
options.dirExplicit = true;
|
|
126
|
+
}
|
|
127
|
+
if (!options.target && positional.length > 0 && isTarget(positional[0])) {
|
|
128
|
+
options.target = positional.shift();
|
|
129
|
+
}
|
|
130
|
+
if (!options.example &&
|
|
131
|
+
positional.length > 0 &&
|
|
132
|
+
isExampleMode(positional[0])) {
|
|
133
|
+
options.example = positional.shift();
|
|
134
|
+
}
|
|
135
|
+
if (positional.length > 0) {
|
|
136
|
+
throw new Error(`Unknown init argument(s): ${positional.join(", ")}. Usage: init [dir] [--target wasi|bindings] [--example minimal|full|none] [--install] [--yes] [--force] [--dir <path>]`);
|
|
137
|
+
}
|
|
138
|
+
return options;
|
|
139
|
+
}
|
|
140
|
+
async function runInteractiveOnboarding(options, face) {
|
|
141
|
+
printOnboardingIntro();
|
|
142
|
+
const acknowledged = await askYesNo("I understand this command writes files and can run package manager installs. Continue?", face, true);
|
|
143
|
+
if (!acknowledged)
|
|
144
|
+
return null;
|
|
145
|
+
const onboardingMode = await askMenuChoice("Onboarding mode", [
|
|
146
|
+
{ value: "manual", label: "Manual (guided prompts)" },
|
|
147
|
+
{ value: "quick", label: "Quick (sensible defaults)" },
|
|
148
|
+
], face, "manual");
|
|
149
|
+
const workspacePrompt = "What do you want to set up? (default: ./)";
|
|
150
|
+
const defaultRoot = options.dir;
|
|
151
|
+
let selectedDir = defaultRoot;
|
|
152
|
+
if (options.dirExplicit || onboardingMode == "quick") {
|
|
153
|
+
selectedDir = options.dir;
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
const defaultDisplay = defaultRoot == "." ? "./" : defaultRoot;
|
|
157
|
+
const enteredDir = (await ask(`${chalk.bold.blue(`◇ ${workspacePrompt}`)}\n│ `, face, defaultDisplay)).trim();
|
|
158
|
+
selectedDir = enteredDir.length ? enteredDir : defaultRoot;
|
|
159
|
+
}
|
|
160
|
+
const resolvedRoot = path.resolve(process.cwd(), selectedDir);
|
|
161
|
+
if (options.dirExplicit || onboardingMode == "quick") {
|
|
162
|
+
printPromptAndSelectionLine(workspacePrompt, resolvedRoot);
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
printSelectionLine(resolvedRoot);
|
|
166
|
+
}
|
|
167
|
+
const target = options.target ??
|
|
168
|
+
(onboardingMode == "quick"
|
|
169
|
+
? "wasi"
|
|
170
|
+
: await askMenuChoice("Build target", [
|
|
171
|
+
{
|
|
172
|
+
value: "wasi",
|
|
173
|
+
label: "wasi (default runner: node .as-test/runners/default.wasi.js)",
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
value: "bindings",
|
|
177
|
+
label: "bindings (default runner: node .as-test/runners/default.bindings.js)",
|
|
178
|
+
},
|
|
179
|
+
], face, "wasi"));
|
|
180
|
+
if (options.target || onboardingMode == "quick") {
|
|
181
|
+
printPromptAndSelectionLine("Build target", target);
|
|
182
|
+
}
|
|
183
|
+
const example = options.example ??
|
|
184
|
+
(onboardingMode == "quick"
|
|
185
|
+
? "minimal"
|
|
186
|
+
: await askMenuChoice("Example template", [
|
|
187
|
+
{ value: "minimal", label: "minimal (one short starter spec)" },
|
|
188
|
+
{ value: "full", label: "full (hooks, assertions, logs, suites)" },
|
|
189
|
+
{ value: "none", label: "none (config/runners only)" },
|
|
190
|
+
], face, "minimal"));
|
|
191
|
+
if (options.example || onboardingMode == "quick") {
|
|
192
|
+
printPromptAndSelectionLine("Example template", example);
|
|
193
|
+
}
|
|
194
|
+
const installDependenciesNow = options.install ??
|
|
195
|
+
(onboardingMode == "quick"
|
|
196
|
+
? false
|
|
197
|
+
: await askYesNo("Install dependencies now?", face, false));
|
|
198
|
+
if (options.install !== undefined || onboardingMode == "quick") {
|
|
199
|
+
printPromptAndSelectionLine("Install dependencies now?", installDependenciesNow ? "Yes" : "No");
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
root: resolvedRoot,
|
|
203
|
+
target,
|
|
204
|
+
example,
|
|
205
|
+
installDependenciesNow,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function printOnboardingHeader() {
|
|
209
|
+
// console.log(
|
|
210
|
+
// chalk.bold.cyan(
|
|
211
|
+
// `as-test ${getCliVersion()} — AssemblyScript testing without runtime guesswork.`,
|
|
212
|
+
// ) + "\n",
|
|
213
|
+
// );
|
|
214
|
+
}
|
|
215
|
+
function printOnboardingIntro() {
|
|
216
|
+
console.log(chalk.cyan("╔═╗ ╔═╗ ╔═╗ ╔═╗ ╔═╗ ╔═╗"));
|
|
217
|
+
console.log(chalk.cyan("╠═╣ ╚═╗ ══ ║ ╠═ ╚═╗ ║ "));
|
|
218
|
+
console.log(chalk.cyan("╩ ╩ ╚═╝ ╩ ╚═╝ ╚═╝ ╩ "));
|
|
219
|
+
console.log("");
|
|
220
|
+
// console.log(chalk.bold("┌") + " " + chalk.bold.blueBright(""));
|
|
221
|
+
// console.log("│");
|
|
222
|
+
// printPanel("Security", [
|
|
223
|
+
// "Security warning — please read.",
|
|
224
|
+
// "",
|
|
225
|
+
// "as-test is a local developer tool and executes build/runtime commands from your project config.",
|
|
226
|
+
// "If the config is untrusted, those commands can run arbitrary programs on your machine.",
|
|
227
|
+
// "",
|
|
228
|
+
// "Recommended baseline:",
|
|
229
|
+
// "- Keep this tool scoped to trusted repositories.",
|
|
230
|
+
// "- Review runOptions.runtime.cmd and buildOptions.cmd before running.",
|
|
231
|
+
// "- Prefer least-privilege shells/environments for shared machines and CI.",
|
|
232
|
+
// "",
|
|
233
|
+
// "Run regularly: ast doctor and ast test --list",
|
|
234
|
+
// "Read docs: README.md (Configuration + Setup Diagnostics sections).",
|
|
235
|
+
// ]);
|
|
236
|
+
// console.log("│");
|
|
237
|
+
}
|
|
238
|
+
// function printPanel(title: string, lines: string[]): void {
|
|
239
|
+
// const innerWidth = Math.max(32, (process.stdout.columns ?? 80) - 6);
|
|
240
|
+
// const heading = `◇ ${title} `;
|
|
241
|
+
// const rule = "─".repeat(Math.max(8, innerWidth - heading.length));
|
|
242
|
+
// console.log(chalk.bold.blue(`${heading}${rule}`));
|
|
243
|
+
// for (const line of lines) {
|
|
244
|
+
// if (!line.length) {
|
|
245
|
+
// console.log("│");
|
|
246
|
+
// continue;
|
|
247
|
+
// }
|
|
248
|
+
// for (const wrapped of wrapText(line, innerWidth)) {
|
|
249
|
+
// console.log(`│ ${wrapped}`);
|
|
250
|
+
// }
|
|
251
|
+
// }
|
|
252
|
+
// console.log(`├${"─".repeat(Math.max(8, innerWidth))}`);
|
|
253
|
+
// }
|
|
254
|
+
// function wrapText(value: string, width: number): string[] {
|
|
255
|
+
// if (width < 1) return [value];
|
|
256
|
+
// const words = value.split(/\s+/).filter((part) => part.length > 0);
|
|
257
|
+
// if (!words.length) return [""];
|
|
258
|
+
// const lines: string[] = [];
|
|
259
|
+
// let current = "";
|
|
260
|
+
// for (const word of words) {
|
|
261
|
+
// if (!current.length) {
|
|
262
|
+
// current = word;
|
|
263
|
+
// continue;
|
|
264
|
+
// }
|
|
265
|
+
// if (current.length + 1 + word.length <= width) {
|
|
266
|
+
// current += ` ${word}`;
|
|
267
|
+
// continue;
|
|
268
|
+
// }
|
|
269
|
+
// lines.push(current);
|
|
270
|
+
// current = word;
|
|
271
|
+
// }
|
|
272
|
+
// if (current.length) {
|
|
273
|
+
// lines.push(current);
|
|
274
|
+
// }
|
|
275
|
+
// return lines;
|
|
276
|
+
// }
|
|
277
|
+
function printPromptAndSelectionLine(prompt, answer) {
|
|
278
|
+
console.log(chalk.bold.blue(`◇ ${prompt}`));
|
|
279
|
+
printSelectionLine(answer);
|
|
280
|
+
}
|
|
281
|
+
function printSelectionLine(answer) {
|
|
282
|
+
console.log(`│ ${chalk.gray(answer)}`);
|
|
283
|
+
console.log("│");
|
|
284
|
+
}
|
|
285
|
+
function parseTarget(value) {
|
|
286
|
+
if (!isTarget(value)) {
|
|
287
|
+
throw new Error(`Invalid target "${value}". Expected wasi|bindings`);
|
|
288
|
+
}
|
|
289
|
+
return value;
|
|
290
|
+
}
|
|
291
|
+
function parseExampleMode(value) {
|
|
292
|
+
if (!isExampleMode(value)) {
|
|
293
|
+
throw new Error(`Invalid example mode "${value}". Expected minimal|full|none`);
|
|
294
|
+
}
|
|
295
|
+
return value;
|
|
296
|
+
}
|
|
297
|
+
function isTarget(value) {
|
|
298
|
+
return TARGETS.includes(value);
|
|
299
|
+
}
|
|
300
|
+
function isExampleMode(value) {
|
|
301
|
+
return EXAMPLE_MODES.includes(value);
|
|
302
|
+
}
|
|
303
|
+
function printPlan(root, target, example, install) {
|
|
304
|
+
const displayRoot = () => {
|
|
305
|
+
const rel = path.relative(process.cwd(), root).split(path.sep).join("/");
|
|
306
|
+
if (!rel || rel == ".")
|
|
307
|
+
return "./";
|
|
308
|
+
if (rel.startsWith(".."))
|
|
309
|
+
return rel;
|
|
310
|
+
return `./${rel}`;
|
|
311
|
+
};
|
|
312
|
+
const statusColor = (relPath) => existsSync(path.join(root, relPath)) ? chalk.hex("#d29922") : chalk.green;
|
|
313
|
+
const paintNode = (node) => statusColor(node.relPath)(node.isDir ? `${node.name}/` : node.name);
|
|
314
|
+
const ensureChild = (parent, name, relPath, isDir) => {
|
|
315
|
+
let child = parent.children.find((entry) => entry.name == name);
|
|
316
|
+
if (!child) {
|
|
317
|
+
child = { name, relPath, isDir, children: [] };
|
|
318
|
+
parent.children.push(child);
|
|
319
|
+
return child;
|
|
320
|
+
}
|
|
321
|
+
if (isDir) {
|
|
322
|
+
child.isDir = true;
|
|
323
|
+
}
|
|
324
|
+
return child;
|
|
325
|
+
};
|
|
326
|
+
const buildTree = (entries) => {
|
|
327
|
+
const rootNode = {
|
|
328
|
+
name: "",
|
|
329
|
+
relPath: "",
|
|
330
|
+
isDir: true,
|
|
331
|
+
children: [],
|
|
332
|
+
};
|
|
333
|
+
for (const entry of entries) {
|
|
334
|
+
const parts = entry.path.split("/").filter((part) => part.length > 0);
|
|
335
|
+
let cursor = rootNode;
|
|
336
|
+
let relPath = "";
|
|
337
|
+
for (let i = 0; i < parts.length; i++) {
|
|
338
|
+
const part = parts[i];
|
|
339
|
+
relPath = relPath ? `${relPath}/${part}` : part;
|
|
340
|
+
const isLeaf = i == parts.length - 1;
|
|
341
|
+
cursor = ensureChild(cursor, part, relPath, isLeaf ? entry.isDir : true);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return rootNode;
|
|
345
|
+
};
|
|
346
|
+
const renderBranch = (nodes, prefix) => {
|
|
347
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
348
|
+
const node = nodes[i];
|
|
349
|
+
const isLast = i == nodes.length - 1;
|
|
350
|
+
const branch = isLast ? "└── " : "├── ";
|
|
351
|
+
const treeGlyphs = chalk.dim(`${prefix}${branch}`);
|
|
352
|
+
console.log(`│ ${treeGlyphs}${paintNode(node)}`);
|
|
353
|
+
if (node.children.length > 0) {
|
|
354
|
+
const childPrefix = `${prefix}${isLast ? " " : "│ "}`;
|
|
355
|
+
renderBranch(node.children, childPrefix);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
const fileEntries = [
|
|
360
|
+
{ path: ".as-test", isDir: true },
|
|
361
|
+
{ path: ".as-test/build", isDir: true },
|
|
362
|
+
{ path: ".as-test/logs", isDir: true },
|
|
363
|
+
{ path: ".as-test/coverage", isDir: true },
|
|
364
|
+
{ path: ".as-test/snapshots", isDir: true },
|
|
365
|
+
{ path: "assembly", isDir: true },
|
|
366
|
+
{ path: "assembly/__tests__", isDir: true },
|
|
367
|
+
{ path: "as-test.config.json", isDir: false },
|
|
368
|
+
{ path: "package.json", isDir: false },
|
|
369
|
+
];
|
|
370
|
+
if (target == "wasi" || target == "bindings") {
|
|
371
|
+
fileEntries.push({ path: ".as-test/runners", isDir: true });
|
|
372
|
+
fileEntries.push({
|
|
373
|
+
path: ".as-test/runners/default.bindings.js",
|
|
374
|
+
isDir: false,
|
|
375
|
+
});
|
|
376
|
+
fileEntries.push({
|
|
377
|
+
path: ".as-test/runners/default.wasi.js",
|
|
378
|
+
isDir: false,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
if (example != "none") {
|
|
382
|
+
fileEntries.push({
|
|
383
|
+
path: "assembly/__tests__/example.spec.ts",
|
|
384
|
+
isDir: false,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
const treeRoot = buildTree(fileEntries);
|
|
388
|
+
console.log(chalk.bold.blue("◇ Planned Changes"));
|
|
389
|
+
console.log("│" + chalk.dim(` - Target: ${target}`));
|
|
390
|
+
console.log("│" + chalk.dim(` - Example: ${example}`));
|
|
391
|
+
console.log("│" + chalk.dim(` - Directory: ${displayRoot()}`));
|
|
392
|
+
console.log("│" + chalk.dim(` - Install dependencies: ${install ? "yes" : "no"}`));
|
|
393
|
+
console.log("│" + chalk.bold.blue(" File Changes"));
|
|
394
|
+
for (const topLevelNode of treeRoot.children) {
|
|
395
|
+
console.log(`│ ${paintNode(topLevelNode)}`);
|
|
396
|
+
renderBranch(topLevelNode.children, "");
|
|
397
|
+
}
|
|
398
|
+
console.log("│");
|
|
399
|
+
}
|
|
400
|
+
function applyInit(root, target, example, force) {
|
|
401
|
+
const summary = {
|
|
402
|
+
created: [],
|
|
403
|
+
updated: [],
|
|
404
|
+
skipped: [],
|
|
405
|
+
};
|
|
406
|
+
ensureDir(root, ".as-test/build", summary);
|
|
407
|
+
ensureDir(root, ".as-test/logs", summary);
|
|
408
|
+
ensureDir(root, ".as-test/coverage", summary);
|
|
409
|
+
ensureDir(root, ".as-test/snapshots", summary);
|
|
410
|
+
ensureDir(root, "assembly/__tests__", summary);
|
|
411
|
+
if (target == "wasi" || target == "bindings") {
|
|
412
|
+
ensureDir(root, ".as-test/runners", summary);
|
|
413
|
+
}
|
|
414
|
+
ensureGitignoreIncludesAsTestDirs(root, summary);
|
|
415
|
+
const configPath = path.join(root, "as-test.config.json");
|
|
416
|
+
const config = {
|
|
417
|
+
$schema: "node_modules/as-test/as-test.config.schema.json",
|
|
418
|
+
input: ["assembly/__tests__/*.spec.ts"],
|
|
419
|
+
output: ".as-test/",
|
|
420
|
+
config: "none",
|
|
421
|
+
coverage: true,
|
|
422
|
+
env: {},
|
|
423
|
+
buildOptions: {
|
|
424
|
+
target,
|
|
425
|
+
},
|
|
426
|
+
runOptions: {
|
|
427
|
+
runtime: {
|
|
428
|
+
cmd: target == "wasi"
|
|
429
|
+
? "node .as-test/runners/default.wasi.js <file>"
|
|
430
|
+
: "node .as-test/runners/default.bindings.js <file>",
|
|
431
|
+
},
|
|
432
|
+
reporter: "default",
|
|
433
|
+
},
|
|
434
|
+
modes: {},
|
|
435
|
+
};
|
|
436
|
+
writeJson(configPath, config, summary, "as-test.config.json");
|
|
437
|
+
if (example != "none") {
|
|
438
|
+
const examplePath = path.join(root, "assembly/__tests__/example.spec.ts");
|
|
439
|
+
const content = example == "minimal" ? buildMinimalExampleSpec() : buildFullExampleSpec();
|
|
440
|
+
writeManagedFile(examplePath, content, force, summary, "assembly/__tests__/example.spec.ts");
|
|
441
|
+
}
|
|
442
|
+
if (target == "wasi" || target == "bindings") {
|
|
443
|
+
const runnerPath = path.join(root, ".as-test/runners/default.wasi.js");
|
|
444
|
+
writeManagedFile(runnerPath, buildWasiRunner(), force, summary, ".as-test/runners/default.wasi.js");
|
|
445
|
+
}
|
|
446
|
+
if (target == "wasi" || target == "bindings") {
|
|
447
|
+
const runnerPath = path.join(root, ".as-test/runners/default.bindings.js");
|
|
448
|
+
writeManagedFile(runnerPath, buildBindingsRunner(), force, summary, ".as-test/runners/default.bindings.js");
|
|
449
|
+
}
|
|
450
|
+
const pkgPath = path.join(root, "package.json");
|
|
451
|
+
const pkg = existsSync(pkgPath)
|
|
452
|
+
? JSON.parse(readFileSync(pkgPath, "utf8"))
|
|
453
|
+
: {};
|
|
454
|
+
if (!pkg.scripts || typeof pkg.scripts != "object") {
|
|
455
|
+
pkg.scripts = {};
|
|
456
|
+
}
|
|
457
|
+
const scripts = pkg.scripts;
|
|
458
|
+
if (!scripts.test) {
|
|
459
|
+
scripts.test = "ast test";
|
|
460
|
+
}
|
|
461
|
+
if (!pkg.type) {
|
|
462
|
+
pkg.type = "module";
|
|
463
|
+
}
|
|
464
|
+
if (!pkg.devDependencies || typeof pkg.devDependencies != "object") {
|
|
465
|
+
pkg.devDependencies = {};
|
|
466
|
+
}
|
|
467
|
+
const devDependencies = pkg.devDependencies;
|
|
468
|
+
if (!devDependencies["as-test"]) {
|
|
469
|
+
devDependencies["as-test"] = "^" + getCliVersion();
|
|
470
|
+
}
|
|
471
|
+
if (!hasDependency(pkg, "assemblyscript")) {
|
|
472
|
+
devDependencies["assemblyscript"] = "^0.28.9";
|
|
473
|
+
}
|
|
474
|
+
if (target == "wasi" && !devDependencies["@assemblyscript/wasi-shim"]) {
|
|
475
|
+
devDependencies["@assemblyscript/wasi-shim"] = "^0.1.0";
|
|
476
|
+
}
|
|
477
|
+
if (target == "bindings" && !pkg.type) {
|
|
478
|
+
pkg.type = "module";
|
|
479
|
+
}
|
|
480
|
+
writeJson(pkgPath, pkg, summary, "package.json");
|
|
481
|
+
return summary;
|
|
482
|
+
}
|
|
483
|
+
function hasDependency(pkg, dependency) {
|
|
484
|
+
const sections = ["dependencies", "devDependencies", "peerDependencies"];
|
|
485
|
+
for (const section of sections) {
|
|
486
|
+
const value = pkg[section];
|
|
487
|
+
if (!value || typeof value != "object" || Array.isArray(value))
|
|
488
|
+
continue;
|
|
489
|
+
if (dependency in value)
|
|
490
|
+
return true;
|
|
491
|
+
}
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
494
|
+
function ensureDir(root, rel, summary) {
|
|
495
|
+
const full = path.join(root, rel);
|
|
496
|
+
if (existsSync(full))
|
|
497
|
+
return;
|
|
498
|
+
mkdirSync(full, { recursive: true });
|
|
499
|
+
summary.created.push(rel + "/");
|
|
500
|
+
}
|
|
501
|
+
function ensureGitignoreIncludesAsTestDirs(root, summary) {
|
|
502
|
+
const rel = ".gitignore";
|
|
503
|
+
const fullPath = path.join(root, rel);
|
|
504
|
+
const entries = ["!.as-test/runners/", "!.as-test/snapshots/"];
|
|
505
|
+
const existed = existsSync(fullPath);
|
|
506
|
+
const source = existed ? readFileSync(fullPath, "utf8") : "";
|
|
507
|
+
const lines = source.split(/\r?\n/);
|
|
508
|
+
const missing = entries.filter((entry) => !lines.some((line) => line.trim() == entry));
|
|
509
|
+
if (!missing.length) {
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
const eol = source.includes("\r\n") ? "\r\n" : "\n";
|
|
513
|
+
let output = source;
|
|
514
|
+
if (output.length && !output.endsWith("\n") && !output.endsWith("\r\n")) {
|
|
515
|
+
output += eol;
|
|
516
|
+
}
|
|
517
|
+
output += missing.join(eol) + eol;
|
|
518
|
+
writeFileSync(fullPath, output);
|
|
519
|
+
if (existed)
|
|
520
|
+
summary.updated.push(rel);
|
|
521
|
+
else
|
|
522
|
+
summary.created.push(rel);
|
|
523
|
+
}
|
|
524
|
+
function writeJson(fullPath, value, summary, displayPath) {
|
|
525
|
+
const rel = displayPath ??
|
|
526
|
+
path.relative(process.cwd(), fullPath) ??
|
|
527
|
+
path.basename(fullPath);
|
|
528
|
+
const existed = existsSync(fullPath);
|
|
529
|
+
const data = JSON.stringify(value, null, 2) + "\n";
|
|
530
|
+
writeFileSync(fullPath, data);
|
|
531
|
+
if (existed)
|
|
532
|
+
summary.updated.push(rel);
|
|
533
|
+
else
|
|
534
|
+
summary.created.push(rel);
|
|
535
|
+
}
|
|
536
|
+
function writeManagedFile(fullPath, data, force, summary, displayPath) {
|
|
537
|
+
const rel = displayPath ??
|
|
538
|
+
path.relative(process.cwd(), fullPath) ??
|
|
539
|
+
path.basename(fullPath);
|
|
540
|
+
const existed = existsSync(fullPath);
|
|
541
|
+
if (existed && !force) {
|
|
542
|
+
summary.skipped.push(rel);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
if (!existsSync(path.dirname(fullPath))) {
|
|
546
|
+
mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
547
|
+
}
|
|
548
|
+
writeFileSync(fullPath, data);
|
|
549
|
+
if (existed)
|
|
550
|
+
summary.updated.push(rel);
|
|
551
|
+
else
|
|
552
|
+
summary.created.push(rel);
|
|
553
|
+
}
|
|
554
|
+
function printSummary(summary) {
|
|
555
|
+
console.log("│");
|
|
556
|
+
if (summary.created.length) {
|
|
557
|
+
console.log(chalk.bold("│ Created:"));
|
|
558
|
+
for (const item of summary.created) {
|
|
559
|
+
console.log(`│ + ${item}`);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
if (summary.updated.length) {
|
|
563
|
+
console.log(chalk.bold("│ Updated:"));
|
|
564
|
+
for (const item of summary.updated) {
|
|
565
|
+
console.log(`│ ~ ${item}`);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
if (summary.skipped.length) {
|
|
569
|
+
console.log(chalk.bold("│ Skipped (exists, use --force to overwrite):"));
|
|
570
|
+
for (const item of summary.skipped) {
|
|
571
|
+
console.log(`│ = ${item}`);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
console.log("│");
|
|
575
|
+
}
|
|
576
|
+
function ask(question, face, initialValue) {
|
|
577
|
+
if (!face) {
|
|
578
|
+
throw new Error("interactive input is unavailable; pass --yes with options");
|
|
579
|
+
}
|
|
580
|
+
return new Promise((res) => {
|
|
581
|
+
face.question(question, (answer) => {
|
|
582
|
+
const stdout = process.stdout;
|
|
583
|
+
if (stdout.isTTY) {
|
|
584
|
+
stdout.write("\x1b[1A");
|
|
585
|
+
stdout.write("\x1b[2K");
|
|
586
|
+
stdout.write("\r");
|
|
587
|
+
}
|
|
588
|
+
res(answer);
|
|
589
|
+
});
|
|
590
|
+
if (initialValue && initialValue.length) {
|
|
591
|
+
face.write(initialValue);
|
|
592
|
+
}
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
async function askChoice(label, choices, face, fallback) {
|
|
596
|
+
if (!face) {
|
|
597
|
+
return fallback;
|
|
598
|
+
}
|
|
599
|
+
const answer = (await ask(`${label} [${choices.join("/")}] (${fallback}) -> `, face, fallback))
|
|
600
|
+
.trim()
|
|
601
|
+
.toLowerCase();
|
|
602
|
+
if (!answer.length)
|
|
603
|
+
return fallback;
|
|
604
|
+
if (choices.includes(answer))
|
|
605
|
+
return answer;
|
|
606
|
+
throw new Error(`Invalid choice "${answer}" for ${label}`);
|
|
607
|
+
}
|
|
608
|
+
async function askMenuChoice(label, choices, face, fallback) {
|
|
609
|
+
const fallbackValue = choices.some((choice) => choice.value == fallback)
|
|
610
|
+
? fallback
|
|
611
|
+
: choices[0].value;
|
|
612
|
+
if (!face)
|
|
613
|
+
return fallbackValue;
|
|
614
|
+
if (!canUseArrowMenu(face)) {
|
|
615
|
+
const values = choices.map((choice) => choice.value);
|
|
616
|
+
return askChoice(label, values, face, fallbackValue);
|
|
617
|
+
}
|
|
618
|
+
return askMenuChoiceWithArrows(label, choices, face, fallbackValue);
|
|
619
|
+
}
|
|
620
|
+
async function askYesNo(label, face, fallback) {
|
|
621
|
+
if (!face)
|
|
622
|
+
return fallback;
|
|
623
|
+
if (canUseArrowMenu(face)) {
|
|
624
|
+
const selected = await askMenuChoice(label, [
|
|
625
|
+
{ value: "yes", label: "Yes" },
|
|
626
|
+
{ value: "no", label: "No" },
|
|
627
|
+
], face, fallback ? "yes" : "no");
|
|
628
|
+
return selected == "yes";
|
|
629
|
+
}
|
|
630
|
+
const suffix = fallback ? "[Y/n]" : "[y/N]";
|
|
631
|
+
const defaultValue = fallback ? "yes" : "no";
|
|
632
|
+
const answer = (await ask(`${label} ${suffix} `, face, defaultValue))
|
|
633
|
+
.trim()
|
|
634
|
+
.toLowerCase();
|
|
635
|
+
if (!answer.length)
|
|
636
|
+
return fallback;
|
|
637
|
+
if (answer == "y" || answer == "yes")
|
|
638
|
+
return true;
|
|
639
|
+
if (answer == "n" || answer == "no")
|
|
640
|
+
return false;
|
|
641
|
+
throw new Error(`Invalid answer "${answer}". Expected yes or no.`);
|
|
642
|
+
}
|
|
643
|
+
function canUseArrowMenu(face) {
|
|
644
|
+
if (!face)
|
|
645
|
+
return false;
|
|
646
|
+
const stdin = process.stdin;
|
|
647
|
+
const stdout = process.stdout;
|
|
648
|
+
return (Boolean(stdin.isTTY) &&
|
|
649
|
+
Boolean(stdout.isTTY) &&
|
|
650
|
+
typeof stdin.setRawMode == "function");
|
|
651
|
+
}
|
|
652
|
+
async function askMenuChoiceWithArrows(label, choices, face, fallback) {
|
|
653
|
+
const stdin = process.stdin;
|
|
654
|
+
const stdout = process.stdout;
|
|
655
|
+
const fallbackIndex = choices.findIndex((choice) => choice.value == fallback);
|
|
656
|
+
let selectedIndex = fallbackIndex == -1 ? 0 : fallbackIndex;
|
|
657
|
+
let renderedLineCount = 0;
|
|
658
|
+
const previousRawMode = Boolean(stdin.isRaw);
|
|
659
|
+
const lineWidth = Math.max(20, (stdout.columns ?? 80) - 2);
|
|
660
|
+
const clamp = (value, max) => {
|
|
661
|
+
if (value.length <= max)
|
|
662
|
+
return value;
|
|
663
|
+
if (max <= 1)
|
|
664
|
+
return value.slice(0, max);
|
|
665
|
+
return `${value.slice(0, max - 1)}…`;
|
|
666
|
+
};
|
|
667
|
+
const titleLine = () => chalk.bold.blue(`◆ ${clamp(label, Math.max(8, lineWidth - 3))}`);
|
|
668
|
+
const menuLines = () => {
|
|
669
|
+
const lines = [titleLine()];
|
|
670
|
+
for (let i = 0; i < choices.length; i++) {
|
|
671
|
+
const choice = choices[i];
|
|
672
|
+
const marker = i == selectedIndex ? chalk.blue("●") : chalk.dim("○");
|
|
673
|
+
lines.push(`│ ${marker} ${clamp(choice.label, Math.max(8, lineWidth - 6))}`);
|
|
674
|
+
}
|
|
675
|
+
lines.push("│");
|
|
676
|
+
return lines;
|
|
677
|
+
};
|
|
678
|
+
const collapsedLines = () => {
|
|
679
|
+
const selected = choices[selectedIndex];
|
|
680
|
+
return [
|
|
681
|
+
`│ ${chalk.gray(clamp(selected.label, Math.max(8, lineWidth - 4)))}`,
|
|
682
|
+
];
|
|
683
|
+
};
|
|
684
|
+
const writeLines = (lines, collapse = false) => {
|
|
685
|
+
if (renderedLineCount > 0) {
|
|
686
|
+
process.stdout.write(`\x1b[${renderedLineCount}A`);
|
|
687
|
+
}
|
|
688
|
+
const totalLineCount = Math.max(lines.length, renderedLineCount);
|
|
689
|
+
for (let i = 0; i < totalLineCount; i++) {
|
|
690
|
+
process.stdout.write("\x1b[2K");
|
|
691
|
+
if (i < lines.length) {
|
|
692
|
+
process.stdout.write(lines[i]);
|
|
693
|
+
}
|
|
694
|
+
process.stdout.write("\n");
|
|
695
|
+
}
|
|
696
|
+
renderedLineCount = lines.length;
|
|
697
|
+
if (collapse) {
|
|
698
|
+
renderedLineCount = 0;
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
const collapseInPlace = () => {
|
|
702
|
+
const lines = collapsedLines();
|
|
703
|
+
if (renderedLineCount > 0) {
|
|
704
|
+
process.stdout.write(`\x1b[${renderedLineCount}A`);
|
|
705
|
+
}
|
|
706
|
+
const totalLineCount = Math.max(renderedLineCount, lines.length);
|
|
707
|
+
for (let i = 0; i < totalLineCount; i++) {
|
|
708
|
+
process.stdout.write("\r\x1b[2K");
|
|
709
|
+
if (i < lines.length) {
|
|
710
|
+
process.stdout.write(lines[i]);
|
|
711
|
+
}
|
|
712
|
+
process.stdout.write("\n");
|
|
713
|
+
}
|
|
714
|
+
const extraLines = totalLineCount - lines.length;
|
|
715
|
+
if (extraLines > 0) {
|
|
716
|
+
process.stdout.write(`\x1b[${extraLines}A`);
|
|
717
|
+
}
|
|
718
|
+
renderedLineCount = 0;
|
|
719
|
+
};
|
|
720
|
+
return new Promise((resolve, reject) => {
|
|
721
|
+
let settled = false;
|
|
722
|
+
const cleanup = () => {
|
|
723
|
+
stdin.off("data", onData);
|
|
724
|
+
if (stdin.isTTY) {
|
|
725
|
+
stdin.setRawMode(previousRawMode);
|
|
726
|
+
}
|
|
727
|
+
const isClosed = Boolean(face.closed);
|
|
728
|
+
if (!isClosed) {
|
|
729
|
+
try {
|
|
730
|
+
face.resume();
|
|
731
|
+
}
|
|
732
|
+
catch {
|
|
733
|
+
// noop: readline may already be closed during shutdown/cancel paths.
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
const finish = (value) => {
|
|
738
|
+
if (settled)
|
|
739
|
+
return;
|
|
740
|
+
settled = true;
|
|
741
|
+
collapseInPlace();
|
|
742
|
+
cleanup();
|
|
743
|
+
resolve(value);
|
|
744
|
+
};
|
|
745
|
+
const fail = (error) => {
|
|
746
|
+
if (settled)
|
|
747
|
+
return;
|
|
748
|
+
settled = true;
|
|
749
|
+
cleanup();
|
|
750
|
+
reject(error);
|
|
751
|
+
};
|
|
752
|
+
const onData = (chunk) => {
|
|
753
|
+
const input = typeof chunk == "string" ? chunk : chunk.toString("utf8");
|
|
754
|
+
if (!input.length)
|
|
755
|
+
return;
|
|
756
|
+
if (input == "\u0003") {
|
|
757
|
+
fail(new Error(chalk.bold.red("◆ Cancelled")));
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
if (input == "\x1b[A" ||
|
|
761
|
+
input == "\x1bOA" ||
|
|
762
|
+
input == "\x1b[D" ||
|
|
763
|
+
input == "\x1bOD") {
|
|
764
|
+
selectedIndex = (selectedIndex - 1 + choices.length) % choices.length;
|
|
765
|
+
writeLines(menuLines());
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
if (input == "\x1b[B" ||
|
|
769
|
+
input == "\x1bOB" ||
|
|
770
|
+
input == "\x1b[C" ||
|
|
771
|
+
input == "\x1bOC") {
|
|
772
|
+
selectedIndex = (selectedIndex + 1) % choices.length;
|
|
773
|
+
writeLines(menuLines());
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
if (input == "\r" || input == "\n") {
|
|
777
|
+
finish(choices[selectedIndex].value);
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
};
|
|
781
|
+
face.pause();
|
|
782
|
+
if (stdin.isTTY) {
|
|
783
|
+
stdin.setRawMode(true);
|
|
784
|
+
}
|
|
785
|
+
stdin.resume();
|
|
786
|
+
stdin.on("data", onData);
|
|
787
|
+
writeLines(menuLines());
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
function installDependencies(root) {
|
|
791
|
+
const install = resolveInstallCommand(root);
|
|
792
|
+
console.log("\n" +
|
|
793
|
+
chalk.dim(`Installing dependencies with: ${install.command} ${install.args.join(" ")}`));
|
|
794
|
+
const child = spawnSync(install.command, install.args, {
|
|
795
|
+
cwd: root,
|
|
796
|
+
stdio: "inherit",
|
|
797
|
+
shell: process.platform == "win32",
|
|
798
|
+
});
|
|
799
|
+
if (child.error) {
|
|
800
|
+
throw new Error(`failed to run dependency install: ${child.error.message}`);
|
|
801
|
+
}
|
|
802
|
+
if (child.status !== 0) {
|
|
803
|
+
throw new Error(`dependency installation failed with exit code ${String(child.status)}`);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
function resolveInstallCommand(root) {
|
|
807
|
+
if (existsSync(path.join(root, "pnpm-lock.yaml"))) {
|
|
808
|
+
return { command: "pnpm", args: ["install"] };
|
|
809
|
+
}
|
|
810
|
+
if (existsSync(path.join(root, "yarn.lock"))) {
|
|
811
|
+
return { command: "yarn", args: ["install"] };
|
|
812
|
+
}
|
|
813
|
+
if (existsSync(path.join(root, "bun.lockb")) ||
|
|
814
|
+
existsSync(path.join(root, "bun.lock"))) {
|
|
815
|
+
return { command: "bun", args: ["install"] };
|
|
816
|
+
}
|
|
817
|
+
const userAgent = process.env.npm_config_user_agent ?? "";
|
|
818
|
+
if (userAgent.startsWith("pnpm")) {
|
|
819
|
+
return { command: "pnpm", args: ["install"] };
|
|
820
|
+
}
|
|
821
|
+
if (userAgent.startsWith("yarn")) {
|
|
822
|
+
return { command: "yarn", args: ["install"] };
|
|
823
|
+
}
|
|
824
|
+
if (userAgent.startsWith("bun")) {
|
|
825
|
+
return { command: "bun", args: ["install"] };
|
|
826
|
+
}
|
|
827
|
+
return { command: "npm", args: ["install"] };
|
|
828
|
+
}
|
|
829
|
+
function buildMinimalExampleSpec() {
|
|
830
|
+
return `import { describe, expect, test, run } from "as-test";
|
|
831
|
+
|
|
832
|
+
describe("example", () => {
|
|
833
|
+
test("adds numbers", () => {
|
|
834
|
+
expect(1 + 2).toBe(3);
|
|
835
|
+
});
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
run();
|
|
839
|
+
`;
|
|
840
|
+
}
|
|
841
|
+
function buildFullExampleSpec() {
|
|
842
|
+
return `import { afterAll, beforeAll, describe, expect, it, log, run, test } from "as-test";
|
|
843
|
+
|
|
844
|
+
beforeAll(() => {
|
|
845
|
+
log("setup");
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
afterAll(() => {
|
|
849
|
+
log("teardown");
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
describe("math", () => {
|
|
853
|
+
test("addition", () => {
|
|
854
|
+
expect(2 + 2).toBe(4);
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
test("comparisons", () => {
|
|
858
|
+
expect(10).toBeGreaterThan(2);
|
|
859
|
+
expect(2).toBeLessThan(10);
|
|
860
|
+
});
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
describe("strings", () => {
|
|
864
|
+
it("contains", () => {
|
|
865
|
+
expect("assemblyscript").toContain("script");
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
test("prefix", () => {
|
|
869
|
+
expect("as-test").toStartWith("as");
|
|
870
|
+
});
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
run();
|
|
874
|
+
`;
|
|
875
|
+
}
|
|
876
|
+
function buildWasiRunner() {
|
|
877
|
+
return `import { readFileSync } from "fs";
|
|
878
|
+
import { WASI } from "wasi";
|
|
879
|
+
|
|
880
|
+
const originalEmitWarning = process.emitWarning.bind(process);
|
|
881
|
+
process.emitWarning = ((warning, ...args) => {
|
|
882
|
+
const type = typeof args[0] == "string" ? args[0] : "";
|
|
883
|
+
const name = typeof warning?.name == "string" ? warning.name : type;
|
|
884
|
+
const message =
|
|
885
|
+
typeof warning == "string" ? warning : String(warning?.message ?? "");
|
|
886
|
+
if (
|
|
887
|
+
name == "ExperimentalWarning" &&
|
|
888
|
+
message.includes("WASI is an experimental feature")
|
|
889
|
+
) {
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
return originalEmitWarning(warning, ...args);
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
const wasmPath = process.argv[2];
|
|
896
|
+
if (!wasmPath) {
|
|
897
|
+
process.stderr.write("usage: node ./.as-test/runners/default.wasi.js <file.wasm>\\n");
|
|
898
|
+
process.exit(1);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
try {
|
|
902
|
+
const wasi = new WASI({
|
|
903
|
+
version: "preview1",
|
|
904
|
+
args: [wasmPath],
|
|
905
|
+
env: process.env,
|
|
906
|
+
preopens: {},
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
const binary = readFileSync(wasmPath);
|
|
910
|
+
const module = new WebAssembly.Module(binary);
|
|
911
|
+
const instance = new WebAssembly.Instance(module, {
|
|
912
|
+
wasi_snapshot_preview1: wasi.wasiImport,
|
|
913
|
+
});
|
|
914
|
+
wasi.start(instance);
|
|
915
|
+
} catch (error) {
|
|
916
|
+
process.stderr.write("failed to run WASI module: " + String(error) + "\\n");
|
|
917
|
+
process.exit(1);
|
|
918
|
+
}
|
|
919
|
+
`;
|
|
920
|
+
}
|
|
921
|
+
function buildBindingsRunner() {
|
|
922
|
+
return `import fs from "fs";
|
|
923
|
+
import path from "path";
|
|
924
|
+
import { pathToFileURL } from "url";
|
|
925
|
+
|
|
926
|
+
let patched = false;
|
|
927
|
+
|
|
928
|
+
function readExact(length) {
|
|
929
|
+
const out = Buffer.alloc(length);
|
|
930
|
+
let offset = 0;
|
|
931
|
+
while (offset < length) {
|
|
932
|
+
let read = 0;
|
|
933
|
+
try {
|
|
934
|
+
read = fs.readSync(0, out, offset, length - offset, null);
|
|
935
|
+
} catch (error) {
|
|
936
|
+
if (error && error.code === "EAGAIN") {
|
|
937
|
+
continue;
|
|
938
|
+
}
|
|
939
|
+
throw error;
|
|
940
|
+
}
|
|
941
|
+
if (!read) break;
|
|
942
|
+
offset += read;
|
|
943
|
+
}
|
|
944
|
+
const view = out.subarray(0, offset);
|
|
945
|
+
return view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength);
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function writeRaw(data) {
|
|
949
|
+
const view = Buffer.from(data);
|
|
950
|
+
fs.writeSync(1, view);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
function withNodeIo(imports = {}) {
|
|
954
|
+
if (!patched) {
|
|
955
|
+
patched = true;
|
|
956
|
+
const originalWrite = process.stdout.write.bind(process.stdout);
|
|
957
|
+
process.stdout.write = (chunk, ...args) => {
|
|
958
|
+
if (chunk instanceof ArrayBuffer) {
|
|
959
|
+
writeRaw(chunk);
|
|
960
|
+
return true;
|
|
961
|
+
}
|
|
962
|
+
return originalWrite(chunk, ...args);
|
|
963
|
+
};
|
|
964
|
+
process.stdin.read = (size) => readExact(Number(size ?? 0));
|
|
965
|
+
}
|
|
966
|
+
return imports;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
const wasmPathArg = process.argv[2];
|
|
970
|
+
if (!wasmPathArg) {
|
|
971
|
+
process.stderr.write("usage: node ./.as-test/runners/default.bindings.js <file.wasm>\\n");
|
|
972
|
+
process.exit(1);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
const wasmPath = path.resolve(process.cwd(), wasmPathArg);
|
|
976
|
+
const jsPath = wasmPath.replace(/\\.wasm$/, ".js");
|
|
977
|
+
|
|
978
|
+
try {
|
|
979
|
+
const binary = fs.readFileSync(wasmPath);
|
|
980
|
+
const module = new WebAssembly.Module(binary);
|
|
981
|
+
const mod = await import(pathToFileURL(jsPath).href);
|
|
982
|
+
if (typeof mod.instantiate !== "function") {
|
|
983
|
+
throw new Error("bindings helper missing instantiate export");
|
|
984
|
+
}
|
|
985
|
+
mod.instantiate(module, withNodeIo({}));
|
|
986
|
+
} catch (error) {
|
|
987
|
+
process.stderr.write("failed to run bindings module: " + String(error) + "\\n");
|
|
988
|
+
process.exit(1);
|
|
989
|
+
}
|
|
990
|
+
`;
|
|
991
|
+
}
|