@untestutils/core 0.5.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/LICENSE +21 -0
- package/dist/artifact-store.d.mts +19 -0
- package/dist/artifact-store.d.ts +20 -0
- package/dist/artifact-store.d.ts.map +1 -0
- package/dist/artifact-store.mjs +52 -0
- package/dist/debug.d.mts +5 -0
- package/dist/debug.d.ts +6 -0
- package/dist/debug.d.ts.map +1 -0
- package/dist/debug.mjs +22 -0
- package/dist/drivers/cli-framework.d.mts +57 -0
- package/dist/drivers/cli-framework.d.ts +64 -0
- package/dist/drivers/cli-framework.d.ts.map +1 -0
- package/dist/drivers/cli-framework.mjs +138 -0
- package/dist/drivers/command.d.mts +15 -0
- package/dist/drivers/command.d.ts +16 -0
- package/dist/drivers/command.d.ts.map +1 -0
- package/dist/drivers/command.mjs +70 -0
- package/dist/drivers/define-driver.d.mts +4 -0
- package/dist/drivers/define-driver.d.ts +5 -0
- package/dist/drivers/define-driver.d.ts.map +1 -0
- package/dist/drivers/define-driver.mjs +3 -0
- package/dist/drivers/host.d.mts +18 -0
- package/dist/drivers/host.d.ts +19 -0
- package/dist/drivers/host.d.ts.map +1 -0
- package/dist/drivers/host.mjs +31 -0
- package/dist/drivers/index.d.mts +12 -0
- package/dist/drivers/index.d.ts +13 -0
- package/dist/drivers/index.d.ts.map +1 -0
- package/dist/drivers/index.mjs +6 -0
- package/dist/drivers/node-entry.d.mts +13 -0
- package/dist/drivers/node-entry.d.ts +14 -0
- package/dist/drivers/node-entry.d.ts.map +1 -0
- package/dist/drivers/node-entry.mjs +55 -0
- package/dist/drivers/static-dir.d.mts +9 -0
- package/dist/drivers/static-dir.d.ts +10 -0
- package/dist/drivers/static-dir.d.ts.map +1 -0
- package/dist/drivers/static-dir.mjs +90 -0
- package/dist/harness.d.mts +4 -0
- package/dist/harness.d.ts +5 -0
- package/dist/harness.d.ts.map +1 -0
- package/dist/harness.mjs +18 -0
- package/dist/hash.d.mts +6 -0
- package/dist/hash.d.ts +7 -0
- package/dist/hash.d.ts.map +1 -0
- package/dist/hash.mjs +76 -0
- package/dist/identity.d.mts +9 -0
- package/dist/identity.d.ts +10 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.mjs +44 -0
- package/dist/index.d.mts +29 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +22 -0
- package/dist/lock.d.mts +26 -0
- package/dist/lock.d.ts +27 -0
- package/dist/lock.d.ts.map +1 -0
- package/dist/lock.mjs +145 -0
- package/dist/orchestrator.d.mts +19 -0
- package/dist/orchestrator.d.ts +22 -0
- package/dist/orchestrator.d.ts.map +1 -0
- package/dist/orchestrator.mjs +343 -0
- package/dist/paths.d.mts +20 -0
- package/dist/paths.d.ts +21 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.mjs +124 -0
- package/dist/ports.d.mts +2 -0
- package/dist/ports.d.ts +8 -0
- package/dist/ports.d.ts.map +1 -0
- package/dist/ports.mjs +38 -0
- package/dist/process.d.mts +31 -0
- package/dist/process.d.ts +36 -0
- package/dist/process.d.ts.map +1 -0
- package/dist/process.mjs +191 -0
- package/dist/progress.d.mts +45 -0
- package/dist/progress.d.ts +66 -0
- package/dist/progress.d.ts.map +1 -0
- package/dist/progress.mjs +205 -0
- package/dist/ready.d.mts +11 -0
- package/dist/ready.d.ts +12 -0
- package/dist/ready.d.ts.map +1 -0
- package/dist/ready.mjs +142 -0
- package/dist/recipes.d.mts +23 -0
- package/dist/recipes.d.ts +24 -0
- package/dist/recipes.d.ts.map +1 -0
- package/dist/recipes.mjs +94 -0
- package/dist/run-helper.d.mts +6 -0
- package/dist/run-helper.d.ts +7 -0
- package/dist/run-helper.d.ts.map +1 -0
- package/dist/run-helper.mjs +79 -0
- package/dist/target-registry.d.mts +27 -0
- package/dist/target-registry.d.ts +28 -0
- package/dist/target-registry.d.ts.map +1 -0
- package/dist/target-registry.mjs +103 -0
- package/dist/types.d.mts +92 -0
- package/dist/types.d.ts +93 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.mjs +1 -0
- package/package.json +54 -0
package/dist/paths.mjs
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, isAbsolute, join, normalize, resolve } from "pathe";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
export const LOOPBACK_HOST = "127.0.0.1";
|
|
7
|
+
|
|
8
|
+
export function resolveBindHost() {
|
|
9
|
+
const raw = process.env.UNTESTUTILS_BIND_HOST?.trim();
|
|
10
|
+
if (!raw) return LOOPBACK_HOST;
|
|
11
|
+
if (raw === "localhost" || raw === "::1" || raw === "[::1]") return LOOPBACK_HOST;
|
|
12
|
+
return raw;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function resolveProbeHost(bindHost = resolveBindHost()) {
|
|
16
|
+
const h = bindHost.replace(/^\[|\]$/g, "").toLowerCase();
|
|
17
|
+
if (h === "0.0.0.0" || h === "*" || h === "::" || h === "::0") return LOOPBACK_HOST;
|
|
18
|
+
return bindHost;
|
|
19
|
+
}
|
|
20
|
+
export function loopbackUrl(port, path = "/", host = resolveBindHost()) {
|
|
21
|
+
const probe = resolveProbeHost(host);
|
|
22
|
+
const p = path.startsWith("/") ? path : `/${path}`;
|
|
23
|
+
return `http://${probe}:${port}${p}`;
|
|
24
|
+
}
|
|
25
|
+
export function normalizeBaseUrl(url) {
|
|
26
|
+
try {
|
|
27
|
+
const u = new URL(url);
|
|
28
|
+
const host = u.hostname.replace(/^\[|\]$/g, "");
|
|
29
|
+
if (host === "localhost" || host === "::1") {
|
|
30
|
+
u.hostname = LOOPBACK_HOST;
|
|
31
|
+
}
|
|
32
|
+
let href = u.toString();
|
|
33
|
+
if (u.pathname === "/" && !href.endsWith("/")) href += "/";
|
|
34
|
+
return href;
|
|
35
|
+
} catch {
|
|
36
|
+
return url.replace("localhost", LOOPBACK_HOST).replace("[::1]", LOOPBACK_HOST);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function resolveArtifactsRoot(cwd = process.cwd()) {
|
|
40
|
+
if (process.env.UNTESTUTILS_ARTIFACTS_DIR) {
|
|
41
|
+
return resolve(cwd, process.env.UNTESTUTILS_ARTIFACTS_DIR);
|
|
42
|
+
}
|
|
43
|
+
const repoRoot = findRepoRoot(cwd);
|
|
44
|
+
return join(repoRoot, ".untestutils");
|
|
45
|
+
}
|
|
46
|
+
export function findRepoRoot(start = process.cwd()) {
|
|
47
|
+
let dir = resolve(start);
|
|
48
|
+
for (;;) {
|
|
49
|
+
if (existsSync(join(dir, "pnpm-workspace.yaml")) || existsSync(join(dir, ".git"))) {
|
|
50
|
+
return dir;
|
|
51
|
+
}
|
|
52
|
+
const parent = dirname(dir);
|
|
53
|
+
if (parent === dir) return start;
|
|
54
|
+
dir = parent;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export function assertNotInsideWorkspacePackage(artifactsRoot, workspaceRoots) {
|
|
58
|
+
const normalized = normalize(resolve(artifactsRoot));
|
|
59
|
+
for (const root of workspaceRoots) {
|
|
60
|
+
const pkgRoot = normalize(resolve(root));
|
|
61
|
+
if (normalized === pkgRoot || normalized.startsWith(pkgRoot + "/")) {
|
|
62
|
+
|
|
63
|
+
const pkgJson = join(pkgRoot, "package.json");
|
|
64
|
+
if (!existsSync(pkgJson)) continue;
|
|
65
|
+
try {
|
|
66
|
+
const name = JSON.parse(readFileSync(pkgJson, "utf8")).name;
|
|
67
|
+
|
|
68
|
+
if (name && name !== "untestutils" && !normalized.includes("/.untestutils")) {
|
|
69
|
+
throw new Error(`[untestutils] artifactsDir must not live inside workspace package "${name}" (${pkgRoot}). Use <repo>/.untestutils instead.`);
|
|
70
|
+
}
|
|
71
|
+
} catch (e) {
|
|
72
|
+
|
|
73
|
+
if (e instanceof Error && e.message.startsWith("[untestutils]")) throw e;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function refuseArtifactsInsidePackage(outDir) {
|
|
80
|
+
const target = normalize(resolve(outDir));
|
|
81
|
+
if (/(^|\/|\\)\.untestutils(\/|\\|$)/.test(target)) return;
|
|
82
|
+
let dir = target;
|
|
83
|
+
for (let i = 0; i < 12; i++) {
|
|
84
|
+
const pkgPath = join(dir, "package.json");
|
|
85
|
+
if (existsSync(pkgPath)) {
|
|
86
|
+
let name;
|
|
87
|
+
try {
|
|
88
|
+
name = JSON.parse(readFileSync(pkgPath, "utf8")).name;
|
|
89
|
+
} catch {
|
|
90
|
+
name = undefined;
|
|
91
|
+
}
|
|
92
|
+
if (name && name !== "untestutils") {
|
|
93
|
+
let walk = dir;
|
|
94
|
+
for (let j = 0; j < 12; j++) {
|
|
95
|
+
if (existsSync(join(walk, "pnpm-workspace.yaml"))) {
|
|
96
|
+
throw new Error(`[untestutils] refusing to write artifacts inside workspace package "${name}" (${dir}). Use <repo>/.untestutils/builds.`);
|
|
97
|
+
}
|
|
98
|
+
const parent = dirname(walk);
|
|
99
|
+
|
|
100
|
+
if (parent === walk) break;
|
|
101
|
+
walk = parent;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
const parent = dirname(dir);
|
|
107
|
+
if (parent === dir) break;
|
|
108
|
+
dir = parent;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
export function isWorkspacePackageDir(dir) {
|
|
112
|
+
return existsSync(join(dir, "package.json"));
|
|
113
|
+
}
|
|
114
|
+
export function toFilePath(urlOrPath) {
|
|
115
|
+
if (typeof urlOrPath === "string") {
|
|
116
|
+
if (urlOrPath.startsWith("file:")) return fileURLToPath(urlOrPath);
|
|
117
|
+
return isAbsolute(urlOrPath) ? urlOrPath : resolve(urlOrPath);
|
|
118
|
+
}
|
|
119
|
+
return fileURLToPath(urlOrPath);
|
|
120
|
+
}
|
|
121
|
+
export function expandHome(path) {
|
|
122
|
+
if (path.startsWith("~/")) return join(homedir(), path.slice(2));
|
|
123
|
+
return path;
|
|
124
|
+
}
|
package/dist/ports.d.mts
ADDED
package/dist/ports.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import net from 'node:net';
|
|
2
|
+
/** @internal injectable for port allocation edge cases */
|
|
3
|
+
export declare const portNet: {
|
|
4
|
+
createServer: (options?: net.ServerOpts | undefined, connectionListener?: ((socket: net.Socket) => void) | undefined) => net.Server;
|
|
5
|
+
};
|
|
6
|
+
export declare function getFreePort(host?: string): Promise<number>;
|
|
7
|
+
export declare function waitForPort(port: number, host?: string, timeoutMs?: number): Promise<void>;
|
|
8
|
+
//# sourceMappingURL=ports.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ports.d.ts","sourceRoot":"","sources":["../src/ports.ts"],"names":[],"mappings":"AAAA,OAAO,GAAG,MAAM,UAAU,CAAC;AAG3B,0DAA0D;AAC1D,eAAO,MAAM,OAAO;;CAEnB,CAAC;AAEF,wBAAgB,WAAW,CAAC,IAAI,GAAE,MAAsB,GAAG,OAAO,CAAC,MAAM,CAAC,CAezE;AAED,wBAAgB,WAAW,CACzB,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,MAAsB,EAC5B,SAAS,GAAE,MAAe,GACzB,OAAO,CAAC,IAAI,CAAC,CAiBf"}
|
package/dist/ports.mjs
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import net from "node:net";
|
|
2
|
+
import { LOOPBACK_HOST } from "./paths.mjs";
|
|
3
|
+
|
|
4
|
+
export const portNet = { createServer: (...args) => net.createServer(...args) };
|
|
5
|
+
export function getFreePort(host = LOOPBACK_HOST) {
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
const server = portNet.createServer();
|
|
8
|
+
server.once("error", reject);
|
|
9
|
+
server.listen(0, host, () => {
|
|
10
|
+
const addr = server.address();
|
|
11
|
+
if (!addr || typeof addr === "string") {
|
|
12
|
+
server.close();
|
|
13
|
+
reject(new Error("failed to allocate port"));
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const { port } = addr;
|
|
17
|
+
server.close((err) => err ? reject(err) : resolve(port));
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
export function waitForPort(port, host = LOOPBACK_HOST, timeoutMs = 3e4) {
|
|
22
|
+
const deadline = Date.now() + timeoutMs;
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
const attempt = () => {
|
|
25
|
+
const socket = net.connect(port, host);
|
|
26
|
+
socket.once("connect", () => {
|
|
27
|
+
socket.destroy();
|
|
28
|
+
resolve();
|
|
29
|
+
});
|
|
30
|
+
socket.once("error", () => {
|
|
31
|
+
socket.destroy();
|
|
32
|
+
if (Date.now() > deadline) reject(new Error(`port ${port} not ready after ${timeoutMs}ms`));
|
|
33
|
+
else setTimeout(attempt, 150);
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
attempt();
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type ChildProcess, spawn } from "node:child_process";
|
|
2
|
+
export declare function scrubTestEnv(env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
3
|
+
export interface SpawnOpts {
|
|
4
|
+
cwd?: string;
|
|
5
|
+
env?: NodeJS.ProcessEnv;
|
|
6
|
+
captureLogs?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface ManagedProcess {
|
|
9
|
+
child: ChildProcess;
|
|
10
|
+
pid?: number;
|
|
11
|
+
stop: () => Promise<void>;
|
|
12
|
+
logs: () => string;
|
|
13
|
+
}
|
|
14
|
+
export declare const processIo: {
|
|
15
|
+
spawn: typeof spawn;
|
|
16
|
+
kill: typeof process.kill;
|
|
17
|
+
};
|
|
18
|
+
export declare function currentPlatform(): NodeJS.Platform;
|
|
19
|
+
export declare function spawnManaged(command: string, args: string[], opts?: SpawnOpts): ManagedProcess;
|
|
20
|
+
/** True when `pid` refers to a live process (signal 0). */
|
|
21
|
+
export declare function isPidAlive(pid: number): boolean;
|
|
22
|
+
/** Kill a process group by pid (detached spawn) without needing the ChildProcess handle. */
|
|
23
|
+
export declare function killPidTree(pid: number): Promise<void>;
|
|
24
|
+
export declare function killProcessTree(child: ChildProcess): Promise<void>;
|
|
25
|
+
export declare function runCommand(command: string, args: string[], opts?: SpawnOpts & {
|
|
26
|
+
timeoutMs?: number;
|
|
27
|
+
}): Promise<{
|
|
28
|
+
exitCode: number;
|
|
29
|
+
stdout: string;
|
|
30
|
+
stderr: string;
|
|
31
|
+
}>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type ChildProcess, spawn } from 'node:child_process';
|
|
2
|
+
export declare function scrubTestEnv(env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
3
|
+
export interface SpawnOpts {
|
|
4
|
+
cwd?: string;
|
|
5
|
+
env?: NodeJS.ProcessEnv;
|
|
6
|
+
captureLogs?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface ManagedProcess {
|
|
9
|
+
child: ChildProcess;
|
|
10
|
+
pid?: number;
|
|
11
|
+
stop: () => Promise<void>;
|
|
12
|
+
logs: () => string;
|
|
13
|
+
}
|
|
14
|
+
/** @internal mutable seams — tests replace these; never call real kill/spawn from unit tests */
|
|
15
|
+
export declare const processPlatform: {
|
|
16
|
+
current: () => NodeJS.Platform;
|
|
17
|
+
};
|
|
18
|
+
export declare const processIo: {
|
|
19
|
+
spawn: typeof spawn;
|
|
20
|
+
kill: typeof process.kill;
|
|
21
|
+
};
|
|
22
|
+
export declare function currentPlatform(): NodeJS.Platform;
|
|
23
|
+
export declare function spawnManaged(command: string, args: string[], opts?: SpawnOpts): ManagedProcess;
|
|
24
|
+
/** True when `pid` refers to a live process (signal 0). */
|
|
25
|
+
export declare function isPidAlive(pid: number): boolean;
|
|
26
|
+
/** Kill a process group by pid (detached spawn) without needing the ChildProcess handle. */
|
|
27
|
+
export declare function killPidTree(pid: number): Promise<void>;
|
|
28
|
+
export declare function killProcessTree(child: ChildProcess): Promise<void>;
|
|
29
|
+
export declare function runCommand(command: string, args: string[], opts?: SpawnOpts & {
|
|
30
|
+
timeoutMs?: number;
|
|
31
|
+
}): Promise<{
|
|
32
|
+
exitCode: number;
|
|
33
|
+
stdout: string;
|
|
34
|
+
stderr: string;
|
|
35
|
+
}>;
|
|
36
|
+
//# sourceMappingURL=process.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"process.d.ts","sourceRoot":"","sources":["../src/process.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,YAAY,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAa9D,wBAAgB,YAAY,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAAC,UAAU,CAIpF;AAED,MAAM,WAAW,SAAS;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,YAAY,CAAC;IACpB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,IAAI,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,gGAAgG;AAChG,eAAO,MAAM,eAAe;mBACb,MAAM,CAAC,QAAQ;CAC7B,CAAC;AAEF,eAAO,MAAM,SAAS;WAC8C,OAAO,KAAK;UAE9C,OAAO,OAAO,CAAC,IAAI;CACpD,CAAC;AAEF,wBAAgB,eAAe,IAAI,MAAM,CAAC,QAAQ,CAEjD;AAED,wBAAgB,YAAY,CAC1B,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,EACd,IAAI,GAAE,SAAc,GACnB,cAAc,CAmChB;AAMD,2DAA2D;AAC3D,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAQ/C;AAED,4FAA4F;AAC5F,wBAAsB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAgD5D;AAED,wBAAsB,eAAe,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAqCxE;AAED,wBAAsB,UAAU,CAC9B,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,EACd,IAAI,GAAE,SAAS,GAAG;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GAC5C,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAgC/D"}
|
package/dist/process.mjs
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { platform } from "node:os";
|
|
3
|
+
import { debug } from "./debug.mjs";
|
|
4
|
+
const STRIP_ENV = [
|
|
5
|
+
"VITEST",
|
|
6
|
+
"VITEST_WORKER_ID",
|
|
7
|
+
"VITEST_POOL_ID",
|
|
8
|
+
"TEST",
|
|
9
|
+
"JEST_WORKER_ID",
|
|
10
|
+
"JEST"
|
|
11
|
+
];
|
|
12
|
+
export function scrubTestEnv(env = process.env) {
|
|
13
|
+
const next = { ...env };
|
|
14
|
+
for (const key of STRIP_ENV) delete next[key];
|
|
15
|
+
return next;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const processPlatform = { current: () => platform() };
|
|
19
|
+
export const processIo = {
|
|
20
|
+
spawn: ((...args) => spawn(...args)),
|
|
21
|
+
kill: ((pid, signal) => process.kill(pid, signal))
|
|
22
|
+
};
|
|
23
|
+
export function currentPlatform() {
|
|
24
|
+
return processPlatform.current();
|
|
25
|
+
}
|
|
26
|
+
export function spawnManaged(command, args, opts = {}) {
|
|
27
|
+
const env = scrubTestEnv(opts.env ?? process.env);
|
|
28
|
+
const capture = opts.captureLogs ?? envFlagCapture();
|
|
29
|
+
let output = "";
|
|
30
|
+
debug("spawn", `${command} ${args.join(" ")}`, { cwd: opts.cwd });
|
|
31
|
+
const child = processIo.spawn(command, args, {
|
|
32
|
+
cwd: opts.cwd,
|
|
33
|
+
env,
|
|
34
|
+
stdio: capture ? [
|
|
35
|
+
"ignore",
|
|
36
|
+
"pipe",
|
|
37
|
+
"pipe"
|
|
38
|
+
] : "inherit",
|
|
39
|
+
detached: currentPlatform() !== "win32"
|
|
40
|
+
});
|
|
41
|
+
if (capture) {
|
|
42
|
+
child.stdout?.on("data", (c) => {
|
|
43
|
+
output += c.toString();
|
|
44
|
+
});
|
|
45
|
+
child.stderr?.on("data", (c) => {
|
|
46
|
+
output += c.toString();
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
const stop = async () => {
|
|
50
|
+
|
|
51
|
+
if (!child.pid) return;
|
|
52
|
+
await killProcessTree(child);
|
|
53
|
+
};
|
|
54
|
+
return {
|
|
55
|
+
child,
|
|
56
|
+
pid: child.pid,
|
|
57
|
+
stop,
|
|
58
|
+
logs: () => output
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function envFlagCapture() {
|
|
62
|
+
return process.env.UNTESTUTILS_CAPTURE_LOGS === "1";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function isPidAlive(pid) {
|
|
66
|
+
if (!pid || pid <= 1) return false;
|
|
67
|
+
try {
|
|
68
|
+
processIo.kill(pid, 0);
|
|
69
|
+
return true;
|
|
70
|
+
} catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function killPidTree(pid) {
|
|
76
|
+
|
|
77
|
+
if (!pid || pid <= 1 || pid === process.pid) return;
|
|
78
|
+
if (currentPlatform() === "win32") {
|
|
79
|
+
await new Promise((resolve) => {
|
|
80
|
+
const killer = processIo.spawn("taskkill", [
|
|
81
|
+
"/T",
|
|
82
|
+
"/F",
|
|
83
|
+
"/PID",
|
|
84
|
+
String(pid)
|
|
85
|
+
], { stdio: "ignore" });
|
|
86
|
+
killer.on("exit", () => resolve());
|
|
87
|
+
killer.on("error", () => resolve());
|
|
88
|
+
setTimeout(resolve, 5e3).unref();
|
|
89
|
+
});
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
await new Promise((resolve) => {
|
|
93
|
+
try {
|
|
94
|
+
processIo.kill(-pid, "SIGTERM");
|
|
95
|
+
} catch {
|
|
96
|
+
try {
|
|
97
|
+
processIo.kill(pid, "SIGTERM");
|
|
98
|
+
} catch {}
|
|
99
|
+
}
|
|
100
|
+
const t = setTimeout(() => {
|
|
101
|
+
try {
|
|
102
|
+
processIo.kill(-pid, "SIGKILL");
|
|
103
|
+
} catch {
|
|
104
|
+
try {
|
|
105
|
+
processIo.kill(pid, "SIGKILL");
|
|
106
|
+
} catch {}
|
|
107
|
+
}
|
|
108
|
+
resolve();
|
|
109
|
+
}, 2e3);
|
|
110
|
+
t.unref();
|
|
111
|
+
const poll = setInterval(() => {
|
|
112
|
+
if (!isPidAlive(pid)) {
|
|
113
|
+
clearInterval(poll);
|
|
114
|
+
clearTimeout(t);
|
|
115
|
+
resolve();
|
|
116
|
+
}
|
|
117
|
+
}, 50);
|
|
118
|
+
poll.unref();
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
export async function killProcessTree(child) {
|
|
122
|
+
const pid = child.pid;
|
|
123
|
+
if (!pid || pid <= 1 || pid === process.pid) return;
|
|
124
|
+
if (currentPlatform() === "win32") {
|
|
125
|
+
await killPidTree(pid);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
await new Promise((resolve) => {
|
|
129
|
+
try {
|
|
130
|
+
processIo.kill(-pid, "SIGTERM");
|
|
131
|
+
} catch {
|
|
132
|
+
try {
|
|
133
|
+
child.kill("SIGTERM");
|
|
134
|
+
} catch {}
|
|
135
|
+
}
|
|
136
|
+
const t = setTimeout(() => {
|
|
137
|
+
try {
|
|
138
|
+
processIo.kill(-pid, "SIGKILL");
|
|
139
|
+
} catch {
|
|
140
|
+
try {
|
|
141
|
+
child.kill("SIGKILL");
|
|
142
|
+
} catch {}
|
|
143
|
+
}
|
|
144
|
+
resolve();
|
|
145
|
+
}, 5e3);
|
|
146
|
+
t.unref();
|
|
147
|
+
child.once("exit", () => {
|
|
148
|
+
clearTimeout(t);
|
|
149
|
+
resolve();
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
export async function runCommand(command, args, opts = {}) {
|
|
154
|
+
const env = scrubTestEnv(opts.env ?? process.env);
|
|
155
|
+
return new Promise((resolve, reject) => {
|
|
156
|
+
const child = processIo.spawn(command, args, {
|
|
157
|
+
cwd: opts.cwd,
|
|
158
|
+
env,
|
|
159
|
+
stdio: [
|
|
160
|
+
"ignore",
|
|
161
|
+
"pipe",
|
|
162
|
+
"pipe"
|
|
163
|
+
]
|
|
164
|
+
});
|
|
165
|
+
let stdout = "";
|
|
166
|
+
let stderr = "";
|
|
167
|
+
child.stdout?.on("data", (c) => {
|
|
168
|
+
stdout += c.toString();
|
|
169
|
+
if (process.env.UNTESTUTILS_DEBUG === "1") process.stderr.write(c);
|
|
170
|
+
});
|
|
171
|
+
child.stderr?.on("data", (c) => {
|
|
172
|
+
stderr += c.toString();
|
|
173
|
+
if (process.env.UNTESTUTILS_DEBUG === "1") process.stderr.write(c);
|
|
174
|
+
});
|
|
175
|
+
const timer = opts.timeoutMs ? setTimeout(() => {
|
|
176
|
+
void killProcessTree(child);
|
|
177
|
+
reject(new Error(`[untestutils] command timed out: ${command} ${args.join(" ")}`));
|
|
178
|
+
}, opts.timeoutMs) : undefined;
|
|
179
|
+
child.on("error", (err) => {
|
|
180
|
+
reject(new Error(`[untestutils] failed to spawn ${command}: ${err.message}`, { cause: err }));
|
|
181
|
+
});
|
|
182
|
+
child.on("exit", (code) => {
|
|
183
|
+
if (timer) clearTimeout(timer);
|
|
184
|
+
resolve({
|
|
185
|
+
exitCode: code ?? 1,
|
|
186
|
+
stdout,
|
|
187
|
+
stderr
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** Common CI / non-interactive runners (GitHub, GitLab, Jenkins, …). */
|
|
2
|
+
export declare function isCi(): boolean;
|
|
3
|
+
/**
|
|
4
|
+
* Compact harness progress (prepare wave + mass-run banner).
|
|
5
|
+
* Off when: `UNTESTUTILS_QUIET=1`, `UNTESTUTILS_PROGRESS=0`, or CI (unless `UNTESTUTILS_PROGRESS=1`).
|
|
6
|
+
*/
|
|
7
|
+
export declare function isProgressEnabled(): boolean;
|
|
8
|
+
/** @deprecated prefer isProgressEnabled — QUIET alone */
|
|
9
|
+
export declare function isQuiet(): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Harness console UX — mirrors the README demo:
|
|
12
|
+
* prepare once → shared hosts → mass run on live URLs.
|
|
13
|
+
*/
|
|
14
|
+
export declare const progress: {
|
|
15
|
+
/**
|
|
16
|
+
* Start a prepare wave (typically `prewarm` in globalSetup).
|
|
17
|
+
* Groups many recipe prepares under one banner.
|
|
18
|
+
*/
|
|
19
|
+
waveStart(ids: string[]): void;
|
|
20
|
+
/** @deprecated use waveStart — kept for older call sites */
|
|
21
|
+
prewarm(ids: string[]): void;
|
|
22
|
+
prepareStart(id: string): void;
|
|
23
|
+
prepareDone(id: string, ms: number): void;
|
|
24
|
+
prepareCache(id: string): void;
|
|
25
|
+
/**
|
|
26
|
+
* Close the prepare wave and announce shared hosts are warm.
|
|
27
|
+
* Call after all prewarm `ensurePrepared` calls finish.
|
|
28
|
+
*/
|
|
29
|
+
waveReady(): void;
|
|
30
|
+
/**
|
|
31
|
+
* Banner before specs hit live URLs (after prepare wave).
|
|
32
|
+
* Safe to call multiple times — only prints once per process.
|
|
33
|
+
*/
|
|
34
|
+
massRun(): void;
|
|
35
|
+
start(id: string, url: string): void;
|
|
36
|
+
teardown(): void;
|
|
37
|
+
/** Always surfaces failures — never swallowed by quiet/CI. */
|
|
38
|
+
fail(id: string, err: unknown): void;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* While pretty progress is on, mute noisy info/success from Nuxt/Vite/Nitro.
|
|
42
|
+
* Errors (consola level ≥ error) stay visible. Never mutes in CI / quiet / debug.
|
|
43
|
+
* Always restores level and rethrows — does not swallow.
|
|
44
|
+
*/
|
|
45
|
+
export declare function withQuietLogger<T>(fn: () => Promise<T>): Promise<T>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** Common CI / non-interactive runners (GitHub, GitLab, Jenkins, …). */
|
|
2
|
+
export declare function isCi(): boolean;
|
|
3
|
+
/**
|
|
4
|
+
* Compact harness progress (prepare wave + mass-run banner).
|
|
5
|
+
* Off when: `UNTESTUTILS_QUIET=1`, `UNTESTUTILS_PROGRESS=0`, or CI (unless `UNTESTUTILS_PROGRESS=1`).
|
|
6
|
+
*/
|
|
7
|
+
export declare function isProgressEnabled(): boolean;
|
|
8
|
+
/** @deprecated prefer isProgressEnabled — QUIET alone */
|
|
9
|
+
export declare function isQuiet(): boolean;
|
|
10
|
+
type WaveState = {
|
|
11
|
+
total: number;
|
|
12
|
+
ready: number;
|
|
13
|
+
building: Set<string>;
|
|
14
|
+
/** Suppress per-id `→ start` noise while the prepare wave is active. */
|
|
15
|
+
active: boolean;
|
|
16
|
+
startedAt: number;
|
|
17
|
+
massRunShown: boolean;
|
|
18
|
+
};
|
|
19
|
+
/** @internal mutable seam for unit tests */
|
|
20
|
+
export declare const progressIo: {
|
|
21
|
+
lines: string[];
|
|
22
|
+
capture: boolean;
|
|
23
|
+
bannerShown: boolean;
|
|
24
|
+
wave: WaveState | null;
|
|
25
|
+
write(line: string): void;
|
|
26
|
+
writeErr(line: string): void;
|
|
27
|
+
reset(): void;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Harness console UX — mirrors the README demo:
|
|
31
|
+
* prepare once → shared hosts → mass run on live URLs.
|
|
32
|
+
*/
|
|
33
|
+
export declare const progress: {
|
|
34
|
+
/**
|
|
35
|
+
* Start a prepare wave (typically `prewarm` in globalSetup).
|
|
36
|
+
* Groups many recipe prepares under one banner.
|
|
37
|
+
*/
|
|
38
|
+
waveStart(ids: string[]): void;
|
|
39
|
+
/** @deprecated use waveStart — kept for older call sites */
|
|
40
|
+
prewarm(ids: string[]): void;
|
|
41
|
+
prepareStart(id: string): void;
|
|
42
|
+
prepareDone(id: string, ms: number): void;
|
|
43
|
+
prepareCache(id: string): void;
|
|
44
|
+
/**
|
|
45
|
+
* Close the prepare wave and announce shared hosts are warm.
|
|
46
|
+
* Call after all prewarm `ensurePrepared` calls finish.
|
|
47
|
+
*/
|
|
48
|
+
waveReady(): void;
|
|
49
|
+
/**
|
|
50
|
+
* Banner before specs hit live URLs (after prepare wave).
|
|
51
|
+
* Safe to call multiple times — only prints once per process.
|
|
52
|
+
*/
|
|
53
|
+
massRun(): void;
|
|
54
|
+
start(id: string, url: string): void;
|
|
55
|
+
teardown(): void;
|
|
56
|
+
/** Always surfaces failures — never swallowed by quiet/CI. */
|
|
57
|
+
fail(id: string, err: unknown): void;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* While pretty progress is on, mute noisy info/success from Nuxt/Vite/Nitro.
|
|
61
|
+
* Errors (consola level ≥ error) stay visible. Never mutes in CI / quiet / debug.
|
|
62
|
+
* Always restores level and rethrows — does not swallow.
|
|
63
|
+
*/
|
|
64
|
+
export declare function withQuietLogger<T>(fn: () => Promise<T>): Promise<T>;
|
|
65
|
+
export {};
|
|
66
|
+
//# sourceMappingURL=progress.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"progress.d.ts","sourceRoot":"","sources":["../src/progress.ts"],"names":[],"mappings":"AAIA,wEAAwE;AACxE,wBAAgB,IAAI,IAAI,OAAO,CAe9B;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,OAAO,CAO3C;AAED,yDAAyD;AACzD,wBAAgB,OAAO,IAAI,OAAO,CAEjC;AAQD,KAAK,SAAS,GAAG;IACf,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACtB,wEAAwE;IACxE,MAAM,EAAE,OAAO,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,OAAO,CAAC;CACvB,CAAC;AAEF,4CAA4C;AAC5C,eAAO,MAAM,UAAU;WACR,MAAM,EAAE;;;UAGP,SAAS,GAAG,IAAI;gBAClB,MAAM;mBAGH,MAAM;;CAStB,CAAC;AA8CF;;;GAGG;AACH,eAAO,MAAM,QAAQ;IACnB;;;OAGG;mBACY,MAAM,EAAE,GAAG,IAAI;IAuB9B,4DAA4D;iBAC/C,MAAM,EAAE,GAAG,IAAI;qBAIX,MAAM,GAAG,IAAI;oBAUd,MAAM,MAAM,MAAM,GAAG,IAAI;qBAWxB,MAAM,GAAG,IAAI;IAW9B;;;OAGG;iBACU,IAAI;IAYjB;;;OAGG;eACQ,IAAI;cAaL,MAAM,OAAO,MAAM,GAAG,IAAI;gBAWxB,IAAI;IAehB,8DAA8D;aACrD,MAAM,OAAO,OAAO,GAAG,IAAI;CAWrC,CAAC;AAEF;;;;GAIG;AACH,wBAAsB,eAAe,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAUzE"}
|