vitest-bats 0.0.1

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/runtime.js ADDED
@@ -0,0 +1,172 @@
1
+ import { executeBats } from "./bats-executor.js";
2
+ import { generateBatsFile } from "./bats-generator.js";
3
+ import { mkdirSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { resolve } from "node:path";
6
+
7
+ //#region src/runtime.ts
8
+ /**
9
+ * Symbol brand identifying a BatsResult instance. Used by matchers to
10
+ * detect BatsResult inputs in a way that survives:
11
+ * - Module-graph duplication (per-entry self-contained bundles produced as
12
+ * the workaround for savvy-web/rslib-builder#158, where each entry has
13
+ * its own copy of the BatsResult class — `instanceof` would fail).
14
+ * - Property name minification (a `constructor.name === "BatsResult"`
15
+ * check would break under any minifier that renames classes).
16
+ *
17
+ * Registered with `Symbol.for(...)` so the symbol is shared across module
18
+ * graphs (the global symbol registry is process-wide).
19
+ * @internal
20
+ */
21
+ const BATS_RESULT_BRAND = Symbol.for("vitest-bats.BatsResult");
22
+ /**
23
+ * The value returned by `ScriptBuilder.run()`/`.exec()`. Wraps the raw exit status,
24
+ * stdout/stderr (whole and split into lines), and recorded mock calls; assert
25
+ * against it with the `expect.extend` matchers from `vitest-bats/setup`.
26
+ * @public
27
+ */
28
+ var BatsResult = class {
29
+ /** @internal */
30
+ [BATS_RESULT_BRAND] = true;
31
+ status;
32
+ output;
33
+ stderr;
34
+ lines;
35
+ stderr_lines;
36
+ calls;
37
+ _jsonCache = null;
38
+ constructor(data) {
39
+ this.status = data.status;
40
+ this.output = data.output;
41
+ this.stderr = data.stderr;
42
+ this.lines = data.output === "" ? [] : data.output.split("\n");
43
+ this.stderr_lines = data.stderr === "" ? [] : data.stderr.split("\n");
44
+ this.calls = data.calls;
45
+ }
46
+ json() {
47
+ if (this._jsonCache) return this._jsonCache.value;
48
+ let parsed;
49
+ try {
50
+ parsed = JSON.parse(this.output);
51
+ } catch (err) {
52
+ const msg = err instanceof Error ? err.message : String(err);
53
+ throw new Error(`vitest-bats: result.output is not valid JSON: ${msg}`);
54
+ }
55
+ this._jsonCache = { value: parsed };
56
+ return parsed;
57
+ }
58
+ };
59
+ const builders = /* @__PURE__ */ new Set();
60
+ /**
61
+ * Resets every live `ScriptBuilder`'s `env()`/`flags()`/`mock()` configuration.
62
+ * Called automatically between tests by `vitest-bats/setup`'s `beforeEach` hook.
63
+ * @public
64
+ */
65
+ function resetAllBuilders() {
66
+ for (const b of builders) b.reset();
67
+ }
68
+ function loadDeps() {
69
+ return {
70
+ batsPath: process.env.BATS_PATH ?? "bats",
71
+ batsSupportPath: process.env.BATS_SUPPORT_PATH ?? ""
72
+ };
73
+ }
74
+ function loadKcov(scriptPath) {
75
+ if (process.env.__VITEST_BATS_KCOV__ !== "1") return void 0;
76
+ const cacheDir = process.env.__VITEST_BATS_CACHE_DIR__;
77
+ const kcovPath = process.env.KCOV_PATH;
78
+ if (!cacheDir || !kcovPath) return void 0;
79
+ const id = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
80
+ const scriptName = scriptPath.split("/").pop()?.replace(/\.sh$/, "") ?? "script";
81
+ const outputDir = resolve(cacheDir, "kcov", `${scriptName}-${id}`);
82
+ mkdirSync(outputDir, { recursive: true });
83
+ return {
84
+ kcovPath,
85
+ outputDir
86
+ };
87
+ }
88
+ function makeRecorderDir() {
89
+ const id = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
90
+ const dir = resolve(tmpdir(), `vitest-bats-${process.pid}`, "recorder", id);
91
+ mkdirSync(dir, { recursive: true });
92
+ return dir;
93
+ }
94
+ /**
95
+ * Test-writing API returned by a `.sh` import. Configure with `env()`/`flags()`/
96
+ * `mock()`, then terminate with `run(...args)` or `exec(shellExpr)` to get a
97
+ * {@link BatsResult}.
98
+ * @public
99
+ */
100
+ var ScriptBuilder = class {
101
+ path;
102
+ name;
103
+ _env = {};
104
+ _flags = "";
105
+ _stubs = [];
106
+ constructor(path, name) {
107
+ this.path = path;
108
+ this.name = name;
109
+ builders.add(this);
110
+ }
111
+ env(vars) {
112
+ this._env = {
113
+ ...this._env,
114
+ ...vars
115
+ };
116
+ return this;
117
+ }
118
+ flags(value) {
119
+ this._flags = value;
120
+ return this;
121
+ }
122
+ mock(cmd, responses = {}) {
123
+ this._stubs.push({
124
+ cmd,
125
+ responses
126
+ });
127
+ return this;
128
+ }
129
+ run(...args) {
130
+ return this.execute({ kind: "args" }, args);
131
+ }
132
+ exec(shellExpr) {
133
+ return this.execute({
134
+ kind: "shell",
135
+ expression: shellExpr
136
+ }, []);
137
+ }
138
+ reset() {
139
+ this._env = {};
140
+ this._flags = "";
141
+ this._stubs = [];
142
+ }
143
+ execute(mode, args) {
144
+ const recorderDir = makeRecorderDir();
145
+ const deps = loadDeps();
146
+ const kcov = loadKcov(this.path);
147
+ const batsContent = generateBatsFile({
148
+ scriptPath: this.path,
149
+ args,
150
+ env: this._env,
151
+ flags: this._flags,
152
+ stubs: this._stubs,
153
+ recorderDir,
154
+ deps,
155
+ mode,
156
+ ...kcov ? { kcov } : {}
157
+ });
158
+ try {
159
+ const data = executeBats({
160
+ batsPath: deps.batsPath,
161
+ batsContent,
162
+ recorderDir
163
+ });
164
+ return new BatsResult(data);
165
+ } finally {
166
+ this.reset();
167
+ }
168
+ }
169
+ };
170
+
171
+ //#endregion
172
+ export { BATS_RESULT_BRAND, BatsResult, ScriptBuilder, resetAllBuilders };
package/schema.js ADDED
@@ -0,0 +1,75 @@
1
+ import { Ajv } from "ajv";
2
+ import addFormatsModule from "ajv-formats";
3
+
4
+ //#region src/schema.ts
5
+ const addFormats = addFormatsModule.default;
6
+ let ajvSingleton = null;
7
+ function getAjv() {
8
+ if (!ajvSingleton) {
9
+ ajvSingleton = new Ajv({
10
+ strict: false,
11
+ allErrors: true
12
+ });
13
+ addFormats(ajvSingleton);
14
+ }
15
+ return ajvSingleton;
16
+ }
17
+ const compiledCache = /* @__PURE__ */ new WeakMap();
18
+ /**
19
+ * Detects whether `schema` implements the Standard Schema (`~standard`) interface,
20
+ * used by the `toMatchSchema` matcher to route to Standard Schema vs. JSON Schema
21
+ * validation.
22
+ * @internal
23
+ */
24
+ function isStandardSchema(schema) {
25
+ if (schema === null || typeof schema !== "object") return false;
26
+ if (!("~standard" in schema)) return false;
27
+ const std = schema["~standard"];
28
+ return std !== null && typeof std === "object" && typeof std.validate === "function";
29
+ }
30
+ function pathToString(path) {
31
+ if (!path || path.length === 0) return "(root)";
32
+ return path.map((seg) => {
33
+ const key = typeof seg === "object" && seg !== null && "key" in seg ? seg.key : seg;
34
+ return typeof key === "number" ? `[${key}]` : String(key);
35
+ }).reduce((acc, cur) => {
36
+ if (cur.startsWith("[")) return acc + cur;
37
+ return acc.length === 0 ? cur : `${acc}.${cur}`;
38
+ }, "");
39
+ }
40
+ function validateStandard(schema, value) {
41
+ const result = schema["~standard"].validate(value);
42
+ if (result instanceof Promise) throw new Error("vitest-bats: async Standard Schema validators are not supported. Await result.json() and use a built-in matcher (e.g., await expect(...).resolves.toEqual(...)) instead.");
43
+ if (!result.issues || result.issues.length === 0) return { ok: true };
44
+ return {
45
+ ok: false,
46
+ issues: result.issues.map((i) => `${pathToString(i.path)}: ${i.message}`)
47
+ };
48
+ }
49
+ function validateJsonSchema(schema, value) {
50
+ let compiled = compiledCache.get(schema);
51
+ if (!compiled) {
52
+ compiled = getAjv().compile(schema);
53
+ compiledCache.set(schema, compiled);
54
+ }
55
+ if (compiled(value)) return { ok: true };
56
+ return {
57
+ ok: false,
58
+ issues: (compiled.errors ?? []).map((err) => {
59
+ return `${err.instancePath || "(root)"}: ${err.message ?? "validation failed"}`;
60
+ })
61
+ };
62
+ }
63
+ /**
64
+ * Validates `value` against a Standard Schema validator or a raw JSON Schema
65
+ * object, backing the `toMatchSchema`/`toMatchJsonSchema` matchers.
66
+ * @internal
67
+ */
68
+ function validate(schema, value) {
69
+ if (isStandardSchema(schema)) return validateStandard(schema, value);
70
+ if (schema !== null && typeof schema === "object") return validateJsonSchema(schema, value);
71
+ throw new Error("vitest-bats: schema must be a Standard Schema validator or a JSON Schema object");
72
+ }
73
+
74
+ //#endregion
75
+ export { isStandardSchema, validate };
package/setup.d.ts ADDED
@@ -0,0 +1 @@
1
+ export {}
package/setup.js ADDED
@@ -0,0 +1,12 @@
1
+ import { resetAllBuilders } from "./runtime.js";
2
+ import { batsMatchers } from "./matchers.js";
3
+ import { beforeEach, expect } from "vitest";
4
+
5
+ //#region src/setup.ts
6
+ expect.extend(batsMatchers);
7
+ beforeEach(() => {
8
+ resetAllBuilders();
9
+ });
10
+
11
+ //#endregion
12
+ export { };
@@ -0,0 +1,11 @@
1
+ // This file is read by tools that parse documentation comments conforming to the TSDoc standard.
2
+ // It should be published with your NPM package. It should not be tracked by Git.
3
+ {
4
+ "tsdocVersion": "0.12",
5
+ "toolPackages": [
6
+ {
7
+ "packageName": "@microsoft/api-extractor",
8
+ "packageVersion": "7.59.0"
9
+ }
10
+ ]
11
+ }