type-a-bin 0.1.2 → 0.1.3
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/README.md +289 -32
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/mock-bin-behaviour-runtime.d.ts +21 -0
- package/dist/mock-bin-behaviour-runtime.js +164 -0
- package/dist/mock-bin-behaviour.d.ts +84 -0
- package/dist/mock-bin-behaviour.js +109 -0
- package/dist/mock-bin-env.d.ts +22 -0
- package/dist/mock-bin-env.js +31 -0
- package/dist/mock-bin-preload.d.ts +1 -21
- package/dist/mock-bin-preload.js +23 -216
- package/dist/mock-bin-runtime.d.ts +41 -0
- package/dist/mock-bin-runtime.js +293 -0
- package/dist/mock-bin-tsx.d.ts +4 -0
- package/dist/mock-bin-tsx.js +30 -0
- package/dist/mock-bin-windows.d.ts +6 -4
- package/dist/mock-bin-windows.js +137 -67
- package/dist/mock-bin.d.ts +31 -3
- package/dist/mock-bin.js +50 -12
- package/dist/native/win32/arm64/type-a-bin-trampoline.exe +0 -0
- package/dist/native/win32/x64/type-a-bin-trampoline.exe +0 -0
- package/dist/rm-scratch.d.ts +11 -0
- package/dist/rm-scratch.js +25 -0
- package/package.json +2 -2
package/dist/mock-bin.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { MockBinBehaviour, MockBinHandle } from "./mock-bin-behaviour.js";
|
|
1
2
|
type MockBinCleanup = () => void;
|
|
2
3
|
interface MockBinConfig {
|
|
3
4
|
/** The name of the binary to mock (e.g., "gh", "git") */
|
|
@@ -21,16 +22,25 @@ interface MockBinScriptFile {
|
|
|
21
22
|
* original command, enabling conditional mocking where some subcommands
|
|
22
23
|
* are mocked while others pass through to the real binary.
|
|
23
24
|
*
|
|
24
|
-
* There are
|
|
25
|
+
* There are five calling conventions:
|
|
25
26
|
*
|
|
26
27
|
* 1. **Output shorthand** — pass the plain text the mock should print.
|
|
27
28
|
* The interpreter defaults to `bash` and the output is echoed.
|
|
28
29
|
* 2. **Full script** — pass an interpreter (`shebang`) and arbitrary
|
|
29
30
|
* script `code` to run when the mock binary is invoked.
|
|
30
|
-
* 3. **Script
|
|
31
|
+
* 3. **Script-file shorthand** — pass only a `{ file }` object and the
|
|
32
|
+
* interpreter is picked from the file's extension: TypeScript runs
|
|
33
|
+
* through the tsx loader (resolved to an absolute URL so the mock
|
|
34
|
+
* works from any working directory), `.js` through node, and `.sh`
|
|
35
|
+
* through bash.
|
|
36
|
+
* 4. **Script file** — pass an interpreter (`shebang`) and a
|
|
31
37
|
* `{ file }` object pointing at a script on disk. The file keeps its
|
|
32
38
|
* own extension, so extension-aware loaders work (e.g.
|
|
33
39
|
* `node --import tsx` with a `.ts` file).
|
|
40
|
+
* 5. **Scripted behaviour** — pass a `MockBinBehaviour` object instead
|
|
41
|
+
* of a script. The mock records every invocation on the returned
|
|
42
|
+
* handle's `calls`, and the object scripts the output, exit code and
|
|
43
|
+
* timing without writing a script at all.
|
|
34
44
|
*
|
|
35
45
|
* @param binNameOrConfig - Binary name or a config object with `binName`
|
|
36
46
|
* and an optional `pattern`
|
|
@@ -52,13 +62,31 @@ interface MockBinScriptFile {
|
|
|
52
62
|
*
|
|
53
63
|
* @example
|
|
54
64
|
* ```ts
|
|
55
|
-
* // Script
|
|
65
|
+
* // Script-file shorthand (the extension picks the interpreter)
|
|
66
|
+
* const cleanup = await mockBin("dragon", {
|
|
67
|
+
* file: "./src/tests/hoard-script.ts", // → node --import <absolute tsx>
|
|
68
|
+
* })
|
|
69
|
+
* ```
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```ts
|
|
73
|
+
* // Script file with an explicit interpreter (keeps its extension)
|
|
56
74
|
* const cleanup = await mockBin("dragon", "node --import tsx", {
|
|
57
75
|
* file: "./src/tests/hoard-script.ts",
|
|
58
76
|
* })
|
|
59
77
|
* ```
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```ts
|
|
81
|
+
* // Scripted behaviour, with the invocations recorded
|
|
82
|
+
* const mock = await mockBin("gh", { stdout: ["#1", "#2"], exitCode: 1 })
|
|
83
|
+
* expect(mock.calls[0]?.args).toEqual(["pr", "list"])
|
|
84
|
+
* mock() // The handle is the cleanup function
|
|
85
|
+
* ```
|
|
60
86
|
*/
|
|
61
87
|
declare function mockBin(binNameOrConfig: string | MockBinConfig, output: string): Promise<MockBinCleanup>;
|
|
88
|
+
declare function mockBin(binNameOrConfig: string | MockBinConfig, script: MockBinScriptFile): Promise<MockBinCleanup>;
|
|
89
|
+
declare function mockBin(binNameOrConfig: string | MockBinConfig, behaviour: MockBinBehaviour): Promise<MockBinHandle>;
|
|
62
90
|
declare function mockBin(binNameOrConfig: string | MockBinConfig, shebang: string, code: string): Promise<MockBinCleanup>;
|
|
63
91
|
declare function mockBin(binNameOrConfig: string | MockBinConfig, shebang: string, script: MockBinScriptFile): Promise<MockBinCleanup>;
|
|
64
92
|
export { type MockBinCleanup, type MockBinConfig, type MockBinScriptFile, mockBin, };
|
package/dist/mock-bin.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { rmSync } from "node:fs";
|
|
2
1
|
import { chmod, mkdtemp, stat, writeFile } from "node:fs/promises";
|
|
3
2
|
import { tmpdir } from "node:os";
|
|
4
3
|
import path from "node:path";
|
|
4
|
+
import { prepareBehaviour, withCalls } from "./mock-bin-behaviour.js";
|
|
5
|
+
import { isTypeScriptFile, resolveTsxImportUrl } from "./mock-bin-tsx.js";
|
|
5
6
|
import { mockBinWindows } from "./mock-bin-windows.js";
|
|
7
|
+
import { rmScratch } from "./rm-scratch.js";
|
|
6
8
|
/**
|
|
7
9
|
* Finds the path to a binary within the given PATH directories.
|
|
8
10
|
*
|
|
@@ -28,16 +30,20 @@ const findBinaryInPath = async (binName, pathDirs) => {
|
|
|
28
30
|
};
|
|
29
31
|
/** Wraps a bare interpreter in a `#!/usr/bin/env …` shebang line. */
|
|
30
32
|
const toShebangLine = (interpreter) => interpreter.startsWith("#!") ? interpreter : `#!/usr/bin/env ${interpreter}`;
|
|
33
|
+
const resolveExistingFile = async (file) => {
|
|
34
|
+
const resolvedFile = path.resolve(file);
|
|
35
|
+
const stats = await stat(resolvedFile).catch(() => null);
|
|
36
|
+
if (!stats?.isFile())
|
|
37
|
+
throw new Error(`mockBin: script file not found: ${file}`);
|
|
38
|
+
return resolvedFile;
|
|
39
|
+
};
|
|
31
40
|
/**
|
|
32
41
|
* Validates the script file exists, then builds an `exec` wrapper that
|
|
33
42
|
* delegates to it through the given interpreter. The file keeps its real
|
|
34
43
|
* extension so extension-aware loaders (e.g. tsx) parse it.
|
|
35
44
|
*/
|
|
36
45
|
const resolveScriptFile = async (shebang, { file }) => {
|
|
37
|
-
const resolvedFile =
|
|
38
|
-
const stats = await stat(resolvedFile).catch(() => null);
|
|
39
|
-
if (!stats?.isFile())
|
|
40
|
-
throw new Error(`mockBin: script file not found: ${file}`);
|
|
46
|
+
const resolvedFile = await resolveExistingFile(file);
|
|
41
47
|
// Accept either a bare interpreter ("node --import tsx") or a full
|
|
42
48
|
// shebang line ("#!/usr/bin/env node"); strip "#!" for the exec line.
|
|
43
49
|
const interpreter = shebang.startsWith("#!")
|
|
@@ -48,6 +54,33 @@ const resolveScriptFile = async (shebang, { file }) => {
|
|
|
48
54
|
body: `exec ${interpreter} "${resolvedFile}" "$@"`,
|
|
49
55
|
};
|
|
50
56
|
};
|
|
57
|
+
/** Discriminates the script-file shorthand from a behaviour object. */
|
|
58
|
+
const isScriptFile = (value) => typeof value === "object" &&
|
|
59
|
+
"file" in value &&
|
|
60
|
+
typeof value.file === "string";
|
|
61
|
+
/**
|
|
62
|
+
* Picks the interpreter for the script-file shorthand from the file's
|
|
63
|
+
* extension: TypeScript runs through the tsx loader resolved to an
|
|
64
|
+
* absolute file URL — so the mock works from any working directory —
|
|
65
|
+
* `.js` through node, `.sh` through bash. Anything else must name its
|
|
66
|
+
* interpreter explicitly.
|
|
67
|
+
*/
|
|
68
|
+
const scriptFileInterpreter = async (script) => {
|
|
69
|
+
const resolvedFile = await resolveExistingFile(script.file);
|
|
70
|
+
if (isTypeScriptFile(resolvedFile)) {
|
|
71
|
+
const tsxImportUrl = resolveTsxImportUrl(resolvedFile);
|
|
72
|
+
// Without tsx, node's native type stripping still parses erasable
|
|
73
|
+
// TypeScript.
|
|
74
|
+
return tsxImportUrl === null ? "node" : `node --import ${tsxImportUrl}`;
|
|
75
|
+
}
|
|
76
|
+
const extension = path.extname(resolvedFile).toLowerCase();
|
|
77
|
+
if ([".cjs", ".js", ".mjs"].includes(extension))
|
|
78
|
+
return "node";
|
|
79
|
+
if (extension === ".sh")
|
|
80
|
+
return "bash";
|
|
81
|
+
throw new Error(`mockBin: no interpreter known for '${extension || "no extension"}' script files; pass one explicitly, e.g. ` +
|
|
82
|
+
`mockBin(bin, "node", { file })`);
|
|
83
|
+
};
|
|
51
84
|
/**
|
|
52
85
|
* Resolves the shebang and body for inline code or the output shorthand.
|
|
53
86
|
* With no `code`, `shebangOrOutput` is echoed via bash.
|
|
@@ -62,6 +95,17 @@ async function mockBin(binNameOrConfig, shebangOrOutput, codeOrScript) {
|
|
|
62
95
|
? { binName: binNameOrConfig }
|
|
63
96
|
: binNameOrConfig;
|
|
64
97
|
const { binName, pattern } = config;
|
|
98
|
+
// Script-file shorthand: pick the interpreter, then take the explicit
|
|
99
|
+
// interpreter + script-file path.
|
|
100
|
+
if (isScriptFile(shebangOrOutput))
|
|
101
|
+
return mockBin(config, await scriptFileInterpreter(shebangOrOutput), shebangOrOutput);
|
|
102
|
+
// A scripted behaviour compiles to a node mock that carries its own
|
|
103
|
+
// pattern check, so it installs through the ordinary inline-code path
|
|
104
|
+
// on both platforms and only the call recorder is layered on top.
|
|
105
|
+
if (typeof shebangOrOutput === "object") {
|
|
106
|
+
const { code, recordDir } = await prepareBehaviour(binName, pattern, shebangOrOutput);
|
|
107
|
+
return withCalls(await mockBin(binName, "node", code), recordDir);
|
|
108
|
+
}
|
|
65
109
|
// Windows needs a real .exe on the PATH plus a NODE_OPTIONS preload
|
|
66
110
|
// that redirects the shim's entry to the mock script; everything else
|
|
67
111
|
// (pattern handling, cleanup contract) behaves the same.
|
|
@@ -134,13 +178,7 @@ fi
|
|
|
134
178
|
process.env.PATH = originalPath;
|
|
135
179
|
else
|
|
136
180
|
delete process.env.PATH;
|
|
137
|
-
|
|
138
|
-
rmSync(tempDir, { recursive: true });
|
|
139
|
-
}
|
|
140
|
-
catch (error) {
|
|
141
|
-
// Ignore cleanup errors - temp dir will be cleaned up eventually
|
|
142
|
-
console.warn(`Warning: Failed to remove mock-bin temp directory ${tempDir}: ${String(error)}`);
|
|
143
|
-
}
|
|
181
|
+
rmScratch(tempDir);
|
|
144
182
|
};
|
|
145
183
|
}
|
|
146
184
|
export { mockBin, };
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Removes a scratch directory tree without ever throwing. Deletion is
|
|
3
|
+
* retried because Windows — and busy filesystems generally — can
|
|
4
|
+
* transiently deny removing files a just-exited process still holds;
|
|
5
|
+
* a removal that still fails warns, since an unclean temp directory
|
|
6
|
+
* beats failing a suite that passed.
|
|
7
|
+
*
|
|
8
|
+
* @param dir - Directory to remove; missing paths are fine (`force`)
|
|
9
|
+
*/
|
|
10
|
+
declare const rmScratch: (dir: string) => void;
|
|
11
|
+
export { rmScratch };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { rmSync } from "node:fs";
|
|
2
|
+
const RM_OPTIONS = {
|
|
3
|
+
recursive: true,
|
|
4
|
+
force: true,
|
|
5
|
+
maxRetries: 40,
|
|
6
|
+
retryDelay: 250,
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Removes a scratch directory tree without ever throwing. Deletion is
|
|
10
|
+
* retried because Windows — and busy filesystems generally — can
|
|
11
|
+
* transiently deny removing files a just-exited process still holds;
|
|
12
|
+
* a removal that still fails warns, since an unclean temp directory
|
|
13
|
+
* beats failing a suite that passed.
|
|
14
|
+
*
|
|
15
|
+
* @param dir - Directory to remove; missing paths are fine (`force`)
|
|
16
|
+
*/
|
|
17
|
+
const rmScratch = (dir) => {
|
|
18
|
+
try {
|
|
19
|
+
rmSync(dir, RM_OPTIONS);
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
console.error(`warning: could not remove ${dir}: ${String(err)}`);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
export { rmScratch };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "type-a-bin",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Mock any executable binary for testing in Node.js",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"node": "==26"
|
|
25
25
|
},
|
|
26
26
|
"scripts": {
|
|
27
|
-
"build:lib": "del-cli dist && tsc && del-cli dist/tests",
|
|
27
|
+
"build:lib": "del-cli dist && tsc && del-cli dist/tests && node scripts/copy-native.mjs",
|
|
28
28
|
"build": "pnpm build:lib && pnpm -r build",
|
|
29
29
|
"format": "ts-canon format",
|
|
30
30
|
"lint": "ts-canon lint",
|