as-test 0.5.3 → 1.0.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/bin/init.js DELETED
@@ -1,497 +0,0 @@
1
- import chalk from "chalk";
2
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
3
- import * as path from "path";
4
- import { createInterface } from "readline";
5
- import { getCliVersion, loadConfig } from "./util.js";
6
- const TARGETS = ["wasi", "bindings"];
7
- const EXAMPLE_MODES = ["minimal", "full", "none"];
8
- export async function init(rawArgs) {
9
- const options = parseInitArgs(rawArgs);
10
- const root = path.resolve(process.cwd(), options.dir);
11
- const rl = options.yes
12
- ? null
13
- : createInterface({
14
- input: process.stdin,
15
- output: process.stdout,
16
- });
17
- try {
18
- console.log(chalk.bold(`as-test init v${getCliVersion()}`) + "\n");
19
- const target = options.target ??
20
- (await askChoice("Select target", TARGETS, rl, "wasi"));
21
- const example = options.example ??
22
- (await askChoice("Select example mode", EXAMPLE_MODES, rl, "minimal"));
23
- printPlan(root, target, example);
24
- if (!options.yes) {
25
- const cont = (await ask("Continue? [Y/n] ", rl)).toLowerCase().trim();
26
- if (["n", "no"].includes(cont)) {
27
- console.log("Exiting.");
28
- return;
29
- }
30
- }
31
- const summary = applyInit(root, target, example, options.force);
32
- printSummary(summary);
33
- }
34
- finally {
35
- rl?.close();
36
- }
37
- }
38
- function parseInitArgs(rawArgs) {
39
- const options = {
40
- yes: false,
41
- force: false,
42
- dir: ".",
43
- };
44
- const positional = [];
45
- for (let i = 0; i < rawArgs.length; i++) {
46
- const arg = rawArgs[i];
47
- if (arg == "--yes" || arg == "-y") {
48
- options.yes = true;
49
- continue;
50
- }
51
- if (arg == "--force") {
52
- options.force = true;
53
- continue;
54
- }
55
- if (arg == "--target") {
56
- const next = rawArgs[i + 1];
57
- if (next && !next.startsWith("-")) {
58
- options.target = parseTarget(next);
59
- i++;
60
- continue;
61
- }
62
- throw new Error("--target requires a value: wasi|bindings");
63
- }
64
- if (arg.startsWith("--target=")) {
65
- options.target = parseTarget(arg.slice("--target=".length));
66
- continue;
67
- }
68
- if (arg == "--example") {
69
- const next = rawArgs[i + 1];
70
- if (next && !next.startsWith("-")) {
71
- options.example = parseExampleMode(next);
72
- i++;
73
- continue;
74
- }
75
- throw new Error("--example requires a value: minimal|full|none");
76
- }
77
- if (arg.startsWith("--example=")) {
78
- options.example = parseExampleMode(arg.slice("--example=".length));
79
- continue;
80
- }
81
- if (arg == "--dir") {
82
- const next = rawArgs[i + 1];
83
- if (next && !next.startsWith("-")) {
84
- options.dir = next;
85
- i++;
86
- continue;
87
- }
88
- throw new Error("--dir requires a path value");
89
- }
90
- if (arg.startsWith("--dir=")) {
91
- options.dir = arg.slice("--dir=".length);
92
- continue;
93
- }
94
- if (arg.startsWith("-")) {
95
- throw new Error(`Unknown init flag: ${arg}`);
96
- }
97
- positional.push(arg);
98
- }
99
- // First positional argument is always the target directory.
100
- if (positional.length > 0) {
101
- options.dir = positional.shift();
102
- }
103
- if (!options.target && positional.length > 0 && isTarget(positional[0])) {
104
- options.target = positional.shift();
105
- }
106
- if (!options.example && positional.length > 0 && isExampleMode(positional[0])) {
107
- options.example = positional.shift();
108
- }
109
- if (positional.length > 0) {
110
- throw new Error(`Unknown init argument(s): ${positional.join(", ")}. Usage: init [dir] [--target wasi|bindings] [--example minimal|full|none] [--yes] [--force] [--dir <path>]`);
111
- }
112
- return options;
113
- }
114
- function parseTarget(value) {
115
- if (!isTarget(value)) {
116
- throw new Error(`Invalid target "${value}". Expected wasi|bindings`);
117
- }
118
- return value;
119
- }
120
- function parseExampleMode(value) {
121
- if (!isExampleMode(value)) {
122
- throw new Error(`Invalid example mode "${value}". Expected minimal|full|none`);
123
- }
124
- return value;
125
- }
126
- function isTarget(value) {
127
- return TARGETS.includes(value);
128
- }
129
- function isExampleMode(value) {
130
- return EXAMPLE_MODES.includes(value);
131
- }
132
- function printPlan(root, target, example) {
133
- console.log(chalk.dim("Planned changes:\n"));
134
- console.log(chalk.dim(` target: ${target}`));
135
- console.log(chalk.dim(` example: ${example}`));
136
- console.log(chalk.dim(` root: ${root}\n`));
137
- console.log(chalk.dim(" directories:"));
138
- console.log(chalk.dim(" .as-test/build"));
139
- console.log(chalk.dim(" .as-test/logs"));
140
- console.log(chalk.dim(" .as-test/coverage"));
141
- console.log(chalk.dim(" .as-test/snapshots"));
142
- console.log(chalk.dim(" assembly/__tests__"));
143
- if (target == "wasi" || target == "bindings") {
144
- console.log(chalk.dim(" .as-test/runners"));
145
- }
146
- console.log(chalk.dim("\n files:"));
147
- console.log(chalk.dim(" as-test.config.json"));
148
- if (example != "none") {
149
- console.log(chalk.dim(" assembly/__tests__/example.spec.ts"));
150
- }
151
- if (target == "wasi" || target == "bindings") {
152
- console.log(chalk.dim(" .as-test/runners/default.wasi.js"));
153
- console.log(chalk.dim(" .as-test/runners/default.bindings.js"));
154
- }
155
- console.log(chalk.dim(" package.json"));
156
- console.log("");
157
- }
158
- function applyInit(root, target, example, force) {
159
- const summary = {
160
- created: [],
161
- updated: [],
162
- skipped: [],
163
- };
164
- ensureDir(root, ".as-test/build", summary);
165
- ensureDir(root, ".as-test/logs", summary);
166
- ensureDir(root, ".as-test/coverage", summary);
167
- ensureDir(root, ".as-test/snapshots", summary);
168
- ensureDir(root, "assembly/__tests__", summary);
169
- if (target == "wasi" || target == "bindings") {
170
- ensureDir(root, ".as-test/runners", summary);
171
- }
172
- ensureGitignoreIncludesAsTestDirs(root, summary);
173
- const configPath = path.join(root, "as-test.config.json");
174
- const config = loadConfig(configPath, false);
175
- config.$schema = "./node_modules/as-test/as-test.config.schema.json";
176
- config.buildOptions.target = target;
177
- config.runOptions.reporter = "default";
178
- if (target == "wasi") {
179
- config.runOptions.runtime.cmd = "node ./.as-test/runners/default.wasi.js <file>";
180
- }
181
- else {
182
- config.runOptions.runtime.cmd = "node ./.as-test/runners/default.bindings.js <file>";
183
- }
184
- writeJson(configPath, config, summary, "as-test.config.json");
185
- if (example != "none") {
186
- const examplePath = path.join(root, "assembly/__tests__/example.spec.ts");
187
- const content = example == "minimal" ? buildMinimalExampleSpec() : buildFullExampleSpec();
188
- writeManagedFile(examplePath, content, force, summary, "assembly/__tests__/example.spec.ts");
189
- }
190
- if (target == "wasi" || target == "bindings") {
191
- const runnerPath = path.join(root, ".as-test/runners/default.wasi.js");
192
- writeManagedFile(runnerPath, buildWasiRunner(), force, summary, ".as-test/runners/default.wasi.js");
193
- }
194
- if (target == "wasi" || target == "bindings") {
195
- const runnerPath = path.join(root, ".as-test/runners/default.bindings.js");
196
- writeManagedFile(runnerPath, buildBindingsRunner(), force, summary, ".as-test/runners/default.bindings.js");
197
- }
198
- const pkgPath = path.join(root, "package.json");
199
- const pkg = existsSync(pkgPath)
200
- ? JSON.parse(readFileSync(pkgPath, "utf8"))
201
- : {};
202
- if (!pkg.scripts || typeof pkg.scripts != "object") {
203
- pkg.scripts = {};
204
- }
205
- const scripts = pkg.scripts;
206
- if (!scripts.test) {
207
- scripts.test = "as-test test";
208
- }
209
- if (!pkg.type) {
210
- pkg.type = "module";
211
- }
212
- if (!pkg.devDependencies || typeof pkg.devDependencies != "object") {
213
- pkg.devDependencies = {};
214
- }
215
- const devDependencies = pkg.devDependencies;
216
- if (!devDependencies["as-test"]) {
217
- devDependencies["as-test"] = "^" + getCliVersion();
218
- }
219
- if (target == "wasi" && !devDependencies["@assemblyscript/wasi-shim"]) {
220
- devDependencies["@assemblyscript/wasi-shim"] = "^0.1.0";
221
- }
222
- if (target == "bindings" && !pkg.type) {
223
- pkg.type = "module";
224
- }
225
- writeJson(pkgPath, pkg, summary, "package.json");
226
- return summary;
227
- }
228
- function ensureDir(root, rel, summary) {
229
- const full = path.join(root, rel);
230
- if (existsSync(full))
231
- return;
232
- mkdirSync(full, { recursive: true });
233
- summary.created.push(rel + "/");
234
- }
235
- function ensureGitignoreIncludesAsTestDirs(root, summary) {
236
- const rel = ".gitignore";
237
- const fullPath = path.join(root, rel);
238
- const entries = ["!.as-test/runners/", "!.as-test/snapshots/"];
239
- const existed = existsSync(fullPath);
240
- const source = existed ? readFileSync(fullPath, "utf8") : "";
241
- const lines = source.split(/\r?\n/);
242
- const missing = entries.filter((entry) => !lines.some((line) => line.trim() == entry));
243
- if (!missing.length) {
244
- return;
245
- }
246
- const eol = source.includes("\r\n") ? "\r\n" : "\n";
247
- let output = source;
248
- if (output.length &&
249
- !output.endsWith("\n") &&
250
- !output.endsWith("\r\n")) {
251
- output += eol;
252
- }
253
- output += missing.join(eol) + eol;
254
- writeFileSync(fullPath, output);
255
- if (existed)
256
- summary.updated.push(rel);
257
- else
258
- summary.created.push(rel);
259
- }
260
- function writeJson(fullPath, value, summary, displayPath) {
261
- const rel = displayPath ??
262
- path.relative(process.cwd(), fullPath) ??
263
- path.basename(fullPath);
264
- const existed = existsSync(fullPath);
265
- const data = JSON.stringify(value, null, 2) + "\n";
266
- writeFileSync(fullPath, data);
267
- if (existed)
268
- summary.updated.push(rel);
269
- else
270
- summary.created.push(rel);
271
- }
272
- function writeManagedFile(fullPath, data, force, summary, displayPath) {
273
- const rel = displayPath ??
274
- path.relative(process.cwd(), fullPath) ??
275
- path.basename(fullPath);
276
- const existed = existsSync(fullPath);
277
- if (existed && !force) {
278
- summary.skipped.push(rel);
279
- return;
280
- }
281
- if (!existsSync(path.dirname(fullPath))) {
282
- mkdirSync(path.dirname(fullPath), { recursive: true });
283
- }
284
- writeFileSync(fullPath, data);
285
- if (existed)
286
- summary.updated.push(rel);
287
- else
288
- summary.created.push(rel);
289
- }
290
- function printSummary(summary) {
291
- console.log("");
292
- if (summary.created.length) {
293
- console.log(chalk.bold("Created:"));
294
- for (const item of summary.created) {
295
- console.log(` + ${item}`);
296
- }
297
- }
298
- if (summary.updated.length) {
299
- console.log(chalk.bold("Updated:"));
300
- for (const item of summary.updated) {
301
- console.log(` ~ ${item}`);
302
- }
303
- }
304
- if (summary.skipped.length) {
305
- console.log(chalk.bold("Skipped (exists, use --force to overwrite):"));
306
- for (const item of summary.skipped) {
307
- console.log(` = ${item}`);
308
- }
309
- }
310
- console.log("\nNow, install dependencies and run " + chalk.bold("as-test test") + "\n");
311
- }
312
- function ask(question, face) {
313
- if (!face) {
314
- throw new Error("interactive input is unavailable; pass --yes with options");
315
- }
316
- return new Promise((res) => {
317
- face.question(question, (answer) => {
318
- res(answer);
319
- });
320
- });
321
- }
322
- async function askChoice(label, choices, face, fallback) {
323
- if (!face) {
324
- return fallback;
325
- }
326
- const answer = (await ask(`${label} [${choices.join("/")}] (${fallback}) -> `, face))
327
- .trim()
328
- .toLowerCase();
329
- if (!answer.length)
330
- return fallback;
331
- if (choices.includes(answer))
332
- return answer;
333
- throw new Error(`Invalid choice "${answer}" for ${label}`);
334
- }
335
- function buildMinimalExampleSpec() {
336
- return `import { describe, expect, test, run } from "as-test";
337
-
338
- describe("example", () => {
339
- test("adds numbers", () => {
340
- expect(1 + 2).toBe(3);
341
- });
342
- });
343
-
344
- run();
345
- `;
346
- }
347
- function buildFullExampleSpec() {
348
- return `import { afterAll, beforeAll, describe, expect, it, log, run, test } from "as-test";
349
-
350
- beforeAll(() => {
351
- log("setup");
352
- });
353
-
354
- afterAll(() => {
355
- log("teardown");
356
- });
357
-
358
- describe("math", () => {
359
- test("addition", () => {
360
- expect(2 + 2).toBe(4);
361
- });
362
-
363
- test("comparisons", () => {
364
- expect(10).toBeGreaterThan(2);
365
- expect(2).toBeLessThan(10);
366
- });
367
- });
368
-
369
- describe("strings", () => {
370
- it("contains", () => {
371
- expect("assemblyscript").toContain("script");
372
- });
373
-
374
- test("prefix", () => {
375
- expect("as-test").toStartWith("as");
376
- });
377
- });
378
-
379
- run();
380
- `;
381
- }
382
- function buildWasiRunner() {
383
- return `import { readFileSync } from "fs";
384
- import { WASI } from "wasi";
385
-
386
- const originalEmitWarning = process.emitWarning.bind(process);
387
- process.emitWarning = ((warning, ...args) => {
388
- const type = typeof args[0] == "string" ? args[0] : "";
389
- const name = typeof warning?.name == "string" ? warning.name : type;
390
- const message =
391
- typeof warning == "string" ? warning : String(warning?.message ?? "");
392
- if (
393
- name == "ExperimentalWarning" &&
394
- message.includes("WASI is an experimental feature")
395
- ) {
396
- return;
397
- }
398
- return originalEmitWarning(warning, ...args);
399
- });
400
-
401
- const wasmPath = process.argv[2];
402
- if (!wasmPath) {
403
- process.stderr.write("usage: node ./.as-test/runners/default.wasi.js <file.wasm>\\n");
404
- process.exit(1);
405
- }
406
-
407
- try {
408
- const wasi = new WASI({
409
- version: "preview1",
410
- args: [wasmPath],
411
- env: process.env,
412
- preopens: {},
413
- });
414
-
415
- const binary = readFileSync(wasmPath);
416
- const module = new WebAssembly.Module(binary);
417
- const instance = new WebAssembly.Instance(module, {
418
- wasi_snapshot_preview1: wasi.wasiImport,
419
- });
420
- wasi.start(instance);
421
- } catch (error) {
422
- process.stderr.write("failed to run WASI module: " + String(error) + "\\n");
423
- process.exit(1);
424
- }
425
- `;
426
- }
427
- function buildBindingsRunner() {
428
- return `import fs from "fs";
429
- import path from "path";
430
- import { pathToFileURL } from "url";
431
-
432
- let patched = false;
433
-
434
- function readExact(length) {
435
- const out = Buffer.alloc(length);
436
- let offset = 0;
437
- while (offset < length) {
438
- let read = 0;
439
- try {
440
- read = fs.readSync(0, out, offset, length - offset, null);
441
- } catch (error) {
442
- if (error && error.code === "EAGAIN") {
443
- continue;
444
- }
445
- throw error;
446
- }
447
- if (!read) break;
448
- offset += read;
449
- }
450
- const view = out.subarray(0, offset);
451
- return view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength);
452
- }
453
-
454
- function writeRaw(data) {
455
- const view = Buffer.from(data);
456
- fs.writeSync(1, view);
457
- }
458
-
459
- function withNodeIo(imports = {}) {
460
- if (!patched) {
461
- patched = true;
462
- const originalWrite = process.stdout.write.bind(process.stdout);
463
- process.stdout.write = (chunk, ...args) => {
464
- if (chunk instanceof ArrayBuffer) {
465
- writeRaw(chunk);
466
- return true;
467
- }
468
- return originalWrite(chunk, ...args);
469
- };
470
- process.stdin.read = (size) => readExact(Number(size ?? 0));
471
- }
472
- return imports;
473
- }
474
-
475
- const wasmPathArg = process.argv[2];
476
- if (!wasmPathArg) {
477
- process.stderr.write("usage: node ./.as-test/runners/default.bindings.js <file.wasm>\\n");
478
- process.exit(1);
479
- }
480
-
481
- const wasmPath = path.resolve(process.cwd(), wasmPathArg);
482
- const jsPath = wasmPath.replace(/\\.wasm$/, ".js");
483
-
484
- try {
485
- const binary = fs.readFileSync(wasmPath);
486
- const module = new WebAssembly.Module(binary);
487
- const mod = await import(pathToFileURL(jsPath).href);
488
- if (typeof mod.instantiate !== "function") {
489
- throw new Error("bindings helper missing instantiate export");
490
- }
491
- mod.instantiate(module, withNodeIo({}));
492
- } catch (error) {
493
- process.stderr.write("failed to run bindings module: " + String(error) + "\\n");
494
- process.exit(1);
495
- }
496
- `;
497
- }