@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.
Files changed (98) hide show
  1. package/LICENSE +21 -0
  2. package/dist/artifact-store.d.mts +19 -0
  3. package/dist/artifact-store.d.ts +20 -0
  4. package/dist/artifact-store.d.ts.map +1 -0
  5. package/dist/artifact-store.mjs +52 -0
  6. package/dist/debug.d.mts +5 -0
  7. package/dist/debug.d.ts +6 -0
  8. package/dist/debug.d.ts.map +1 -0
  9. package/dist/debug.mjs +22 -0
  10. package/dist/drivers/cli-framework.d.mts +57 -0
  11. package/dist/drivers/cli-framework.d.ts +64 -0
  12. package/dist/drivers/cli-framework.d.ts.map +1 -0
  13. package/dist/drivers/cli-framework.mjs +138 -0
  14. package/dist/drivers/command.d.mts +15 -0
  15. package/dist/drivers/command.d.ts +16 -0
  16. package/dist/drivers/command.d.ts.map +1 -0
  17. package/dist/drivers/command.mjs +70 -0
  18. package/dist/drivers/define-driver.d.mts +4 -0
  19. package/dist/drivers/define-driver.d.ts +5 -0
  20. package/dist/drivers/define-driver.d.ts.map +1 -0
  21. package/dist/drivers/define-driver.mjs +3 -0
  22. package/dist/drivers/host.d.mts +18 -0
  23. package/dist/drivers/host.d.ts +19 -0
  24. package/dist/drivers/host.d.ts.map +1 -0
  25. package/dist/drivers/host.mjs +31 -0
  26. package/dist/drivers/index.d.mts +12 -0
  27. package/dist/drivers/index.d.ts +13 -0
  28. package/dist/drivers/index.d.ts.map +1 -0
  29. package/dist/drivers/index.mjs +6 -0
  30. package/dist/drivers/node-entry.d.mts +13 -0
  31. package/dist/drivers/node-entry.d.ts +14 -0
  32. package/dist/drivers/node-entry.d.ts.map +1 -0
  33. package/dist/drivers/node-entry.mjs +55 -0
  34. package/dist/drivers/static-dir.d.mts +9 -0
  35. package/dist/drivers/static-dir.d.ts +10 -0
  36. package/dist/drivers/static-dir.d.ts.map +1 -0
  37. package/dist/drivers/static-dir.mjs +90 -0
  38. package/dist/harness.d.mts +4 -0
  39. package/dist/harness.d.ts +5 -0
  40. package/dist/harness.d.ts.map +1 -0
  41. package/dist/harness.mjs +18 -0
  42. package/dist/hash.d.mts +6 -0
  43. package/dist/hash.d.ts +7 -0
  44. package/dist/hash.d.ts.map +1 -0
  45. package/dist/hash.mjs +76 -0
  46. package/dist/identity.d.mts +9 -0
  47. package/dist/identity.d.ts +10 -0
  48. package/dist/identity.d.ts.map +1 -0
  49. package/dist/identity.mjs +44 -0
  50. package/dist/index.d.mts +29 -0
  51. package/dist/index.d.ts +30 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.mjs +22 -0
  54. package/dist/lock.d.mts +26 -0
  55. package/dist/lock.d.ts +27 -0
  56. package/dist/lock.d.ts.map +1 -0
  57. package/dist/lock.mjs +145 -0
  58. package/dist/orchestrator.d.mts +19 -0
  59. package/dist/orchestrator.d.ts +22 -0
  60. package/dist/orchestrator.d.ts.map +1 -0
  61. package/dist/orchestrator.mjs +343 -0
  62. package/dist/paths.d.mts +20 -0
  63. package/dist/paths.d.ts +21 -0
  64. package/dist/paths.d.ts.map +1 -0
  65. package/dist/paths.mjs +124 -0
  66. package/dist/ports.d.mts +2 -0
  67. package/dist/ports.d.ts +8 -0
  68. package/dist/ports.d.ts.map +1 -0
  69. package/dist/ports.mjs +38 -0
  70. package/dist/process.d.mts +31 -0
  71. package/dist/process.d.ts +36 -0
  72. package/dist/process.d.ts.map +1 -0
  73. package/dist/process.mjs +191 -0
  74. package/dist/progress.d.mts +45 -0
  75. package/dist/progress.d.ts +66 -0
  76. package/dist/progress.d.ts.map +1 -0
  77. package/dist/progress.mjs +205 -0
  78. package/dist/ready.d.mts +11 -0
  79. package/dist/ready.d.ts +12 -0
  80. package/dist/ready.d.ts.map +1 -0
  81. package/dist/ready.mjs +142 -0
  82. package/dist/recipes.d.mts +23 -0
  83. package/dist/recipes.d.ts +24 -0
  84. package/dist/recipes.d.ts.map +1 -0
  85. package/dist/recipes.mjs +94 -0
  86. package/dist/run-helper.d.mts +6 -0
  87. package/dist/run-helper.d.ts +7 -0
  88. package/dist/run-helper.d.ts.map +1 -0
  89. package/dist/run-helper.mjs +79 -0
  90. package/dist/target-registry.d.mts +27 -0
  91. package/dist/target-registry.d.ts +28 -0
  92. package/dist/target-registry.d.ts.map +1 -0
  93. package/dist/target-registry.mjs +103 -0
  94. package/dist/types.d.mts +92 -0
  95. package/dist/types.d.ts +93 -0
  96. package/dist/types.d.ts.map +1 -0
  97. package/dist/types.mjs +1 -0
  98. package/package.json +54 -0
@@ -0,0 +1,205 @@
1
+ import { envFlag, isDebug } from "./debug.mjs";
2
+ const ID_WIDTH = 22;
3
+
4
+ export function isCi() {
5
+ if (envFlag("CI", false)) return true;
6
+ const keys = [
7
+ "GITHUB_ACTIONS",
8
+ "GITLAB_CI",
9
+ "CIRCLECI",
10
+ "BUILDKITE",
11
+ "TF_BUILD",
12
+ "TEAMCITY_VERSION",
13
+ "JENKINS_URL"
14
+ ];
15
+ return keys.some((k) => {
16
+ const v = process.env[k];
17
+ return v !== undefined && v !== "" && v !== "0" && v !== "false";
18
+ });
19
+ }
20
+
21
+ export function isProgressEnabled() {
22
+ if (envFlag("UNTESTUTILS_QUIET", false)) return false;
23
+ const explicit = process.env.UNTESTUTILS_PROGRESS;
24
+ if (explicit === "0" || explicit === "false") return false;
25
+ if (explicit === "1" || explicit === "true") return true;
26
+ if (isCi()) return false;
27
+ return true;
28
+ }
29
+
30
+ export function isQuiet() {
31
+ return !isProgressEnabled();
32
+ }
33
+ function useFancy() {
34
+ if (!isProgressEnabled()) return false;
35
+ if (isCi()) return false;
36
+ return process.stdout.isTTY === true;
37
+ }
38
+
39
+ export const progressIo = {
40
+ lines: [],
41
+ capture: false,
42
+ bannerShown: false,
43
+ wave: null,
44
+ write(line) {
45
+ process.stdout.write(`${line}\n`);
46
+ },
47
+ writeErr(line) {
48
+ process.stderr.write(`${line}\n`);
49
+ },
50
+ reset() {
51
+ this.lines = [];
52
+ this.capture = false;
53
+ this.bannerShown = false;
54
+ this.wave = null;
55
+ }
56
+ };
57
+ function padId(s) {
58
+ if (s.length > ID_WIDTH) return `${s.slice(0, ID_WIDTH - 1)}…`;
59
+ return s.padEnd(ID_WIDTH);
60
+ }
61
+ function emit(line) {
62
+ if (!isProgressEnabled()) return;
63
+ if (progressIo.capture) {
64
+ progressIo.lines.push(line);
65
+ return;
66
+ }
67
+ progressIo.write(line);
68
+ }
69
+ function ensureBanner() {
70
+ if (progressIo.bannerShown || !isProgressEnabled()) return;
71
+ progressIo.bannerShown = true;
72
+ emit(useFancy() ? "◆ untestutils · mass e2e on live targets" : "[untestutils] mass e2e on live targets");
73
+ }
74
+ function formatDuration(ms) {
75
+ if (ms < 1e3) return `${ms}ms`;
76
+ if (ms < 1e4) return `${(ms / 1e3).toFixed(1)}s`;
77
+ return `${Math.round(ms / 1e3)}s`;
78
+ }
79
+ function errMessage(err) {
80
+ if (err instanceof Error) return err.message;
81
+ return String(err);
82
+ }
83
+ function check(ok = true) {
84
+ if (!useFancy()) return ok ? "+" : "x";
85
+ return ok ? "✔" : "✖";
86
+ }
87
+ function arrow() {
88
+ return useFancy() ? "▸" : ">";
89
+ }
90
+
91
+ export const progress = {
92
+
93
+ waveStart(ids) {
94
+ ensureBanner();
95
+ const total = ids.length;
96
+ progressIo.wave = {
97
+ total,
98
+ ready: 0,
99
+ building: new Set(),
100
+ active: true,
101
+ startedAt: Date.now(),
102
+ massRunShown: false
103
+ };
104
+ emit("");
105
+ emit(`${arrow()} prepare once` + (useFancy() ? " — every worker reuses the same builds" : ` (${total} recipes, shared across workers)`));
106
+ if (total) {
107
+ emit(` ${total} recipes in the wave`);
108
+ }
109
+ },
110
+
111
+ prewarm(ids) {
112
+ progress.waveStart(ids);
113
+ },
114
+ prepareStart(id) {
115
+ ensureBanner();
116
+ if (progressIo.wave?.active) {
117
+ progressIo.wave.building.add(id);
118
+ emit(` ${padId(id)} building…`);
119
+ return;
120
+ }
121
+ emit(` ${useFancy() ? "●" : "*"} ${padId(id)} building…`);
122
+ },
123
+ prepareDone(id, ms) {
124
+ ensureBanner();
125
+ if (progressIo.wave?.active) {
126
+ progressIo.wave.building.delete(id);
127
+ progressIo.wave.ready += 1;
128
+ emit(` ${check()} ${padId(id)} built · ${formatDuration(ms)}`);
129
+ return;
130
+ }
131
+ emit(` ${check()} ${padId(id)} built · ${formatDuration(ms)}`);
132
+ },
133
+ prepareCache(id) {
134
+ ensureBanner();
135
+ if (progressIo.wave?.active) {
136
+ progressIo.wave.building.delete(id);
137
+ progressIo.wave.ready += 1;
138
+ emit(` ${check()} ${padId(id)} cached`);
139
+ return;
140
+ }
141
+ emit(` ${check()} ${padId(id)} cached`);
142
+ },
143
+
144
+ waveReady() {
145
+ const wave = progressIo.wave;
146
+ if (!wave?.active) return;
147
+ wave.active = false;
148
+ const ready = wave.ready || wave.total;
149
+ const total = wave.total || ready;
150
+ emit(` ${check()} ${ready}/${total} warm` + (useFancy() ? " · shared host registry" : " · shared hosts"));
151
+ },
152
+
153
+ massRun() {
154
+ ensureBanner();
155
+ if (progressIo.wave?.massRunShown) return;
156
+ if (progressIo.wave) progressIo.wave.massRunShown = true;
157
+ emit("");
158
+ emit(`${arrow()} mass run` + (useFancy() ? " live URLs · real cookies/SEO/$fetch · no per-file rebuild" : " live URLs · shared prepare"));
159
+ },
160
+ start(id, url) {
161
+ ensureBanner();
162
+
163
+ if (progressIo.wave?.active) return;
164
+
165
+ if (progressIo.wave && !progressIo.wave.massRunShown) {
166
+ progress.massRun();
167
+ }
168
+ emit(` ${useFancy() ? "→" : ">"} ${padId(id)} ${url}`);
169
+ },
170
+ teardown() {
171
+ ensureBanner();
172
+ const wave = progressIo.wave;
173
+ emit("");
174
+ if (wave && wave.total > 0) {
175
+ const elapsed = formatDuration(Date.now() - wave.startedAt);
176
+ emit(` ${wave.ready || wave.total} recipes prepared once` + (useFancy() ? ` · session ${elapsed}` : ` · ${elapsed}`));
177
+ }
178
+ emit(useFancy() ? " prepare once → many workers → real URLs & data" : " teardown stop");
179
+ emit(` ${useFancy() ? "■" : "="} teardown`);
180
+ },
181
+
182
+ fail(id, err) {
183
+ const line = ` ${check(false)} ${padId(id)} ${errMessage(err)}`;
184
+ if (progressIo.capture) {
185
+ progressIo.lines.push(line);
186
+ return;
187
+ }
188
+ progressIo.writeErr(line);
189
+ if (err instanceof Error && err.stack && isDebug()) {
190
+ progressIo.writeErr(err.stack);
191
+ }
192
+ }
193
+ };
194
+
195
+ export async function withQuietLogger(fn) {
196
+ if (isDebug() || !isProgressEnabled()) return fn();
197
+ const { consola, LogLevels } = await import("consola");
198
+ const prev = consola.level;
199
+ consola.level = LogLevels.error;
200
+ try {
201
+ return await fn();
202
+ } finally {
203
+ consola.level = prev;
204
+ }
205
+ }
@@ -0,0 +1,11 @@
1
+ import type { Running } from "./types.mjs";
2
+ export interface ReadyOptions {
3
+ timeoutMs?: number;
4
+ path?: string;
5
+ acceptStatuses?: number[];
6
+ rejectBodyIncludes?: string[];
7
+ /** Skip TCP probe (default: skip for non-loopback hosts). */
8
+ skipTcp?: boolean;
9
+ }
10
+ export declare function waitForHttpReady(url: string, opts?: ReadyOptions): Promise<void>;
11
+ export declare function defaultReady(running: Running, opts?: ReadyOptions): Promise<void>;
@@ -0,0 +1,12 @@
1
+ import type { Running } from './types';
2
+ export interface ReadyOptions {
3
+ timeoutMs?: number;
4
+ path?: string;
5
+ acceptStatuses?: number[];
6
+ rejectBodyIncludes?: string[];
7
+ /** Skip TCP probe (default: skip for non-loopback hosts). */
8
+ skipTcp?: boolean;
9
+ }
10
+ export declare function waitForHttpReady(url: string, opts?: ReadyOptions): Promise<void>;
11
+ export declare function defaultReady(running: Running, opts?: ReadyOptions): Promise<void>;
12
+ //# sourceMappingURL=ready.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ready.d.ts","sourceRoot":"","sources":["../src/ready.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAKvC,MAAM,WAAW,YAAY;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC9B,6DAA6D;IAC7D,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,GAAE,YAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAgF1F;AAED,wBAAsB,YAAY,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAKvF"}
package/dist/ready.mjs ADDED
@@ -0,0 +1,142 @@
1
+ import { ofetch } from "ofetch";
2
+ import { debug } from "./debug.mjs";
3
+ import { LOOPBACK_HOST } from "./paths.mjs";
4
+ import { waitForPort } from "./ports.mjs";
5
+ export async function waitForHttpReady(url, opts = {}) {
6
+ const timeoutMs = opts.timeoutMs ?? 6e4;
7
+ const path = opts.path ?? "/";
8
+ const accept = new Set(opts.acceptStatuses ?? [
9
+ 200,
10
+ 301,
11
+ 302,
12
+ 304,
13
+ 307,
14
+ 308
15
+ ]);
16
+ const rejectBody = opts.rejectBodyIncludes ?? ["__NUXT_LOADING__"];
17
+ const target = new URL(path, url).toString();
18
+ const deadline = Date.now() + timeoutMs;
19
+ let lastStatus = 0;
20
+ let lastBody = "";
21
+ let lastError = "";
22
+ let phase = "http";
23
+ const endpoint = parseHostPort(target);
24
+ const skipTcp = opts.skipTcp ?? (endpoint ? !isLoopbackHost(endpoint.host) : true);
25
+ if (endpoint && !skipTcp) {
26
+ phase = "tcp";
27
+ const tcpBudget = Math.max(0, deadline - Date.now());
28
+ if (tcpBudget <= 0) {
29
+ throw readinessError(target, timeoutMs, {
30
+ phase,
31
+ lastStatus,
32
+ lastBody,
33
+ lastError: "timeout before tcp probe"
34
+ });
35
+ }
36
+ try {
37
+ await waitForPort(endpoint.port, endpoint.host, tcpBudget);
38
+ debug("ready", `tcp open ${endpoint.host}:${endpoint.port}`);
39
+ } catch (e) {
40
+ lastError = errMessage(e);
41
+ throw readinessError(target, timeoutMs, {
42
+ phase: "tcp",
43
+ lastStatus: 0,
44
+ lastBody: "",
45
+ lastError,
46
+ hint: `Nothing accepted TCP on ${endpoint.host}:${endpoint.port}. Check the server binds that host (set HOST / --host, or UNTESTUTILS_BIND_HOST).`
47
+ });
48
+ }
49
+ }
50
+ phase = "http";
51
+ while (Date.now() < deadline) {
52
+ try {
53
+ const res = await ofetch.raw(target, {
54
+ method: "GET",
55
+ redirect: "manual",
56
+ ignoreResponseError: true,
57
+ responseType: "text",
58
+ timeout: 5e3
59
+ });
60
+ lastStatus = res.status;
61
+ lastBody = typeof res._data === "string" ? res._data.slice(0, 500) : "";
62
+ lastError = "";
63
+ if (res.status === 503) {
64
+ await sleep(200);
65
+ continue;
66
+ }
67
+ if (rejectBody.some((s) => lastBody.includes(s))) {
68
+ await sleep(200);
69
+ continue;
70
+ }
71
+ if (accept.has(res.status)) {
72
+ debug("ready", `ok ${target} status=${res.status}`);
73
+ return;
74
+ }
75
+ lastError = `unexpected status ${res.status} (accept ${[...accept].join(",")})`;
76
+ } catch (e) {
77
+ lastError = errMessage(e);
78
+ }
79
+ await sleep(200);
80
+ }
81
+ throw readinessError(target, timeoutMs, {
82
+ phase,
83
+ lastStatus,
84
+ lastBody,
85
+ lastError,
86
+ hint: httpReadyHint(endpoint, skipTcp)
87
+ });
88
+ }
89
+ export async function defaultReady(running, opts) {
90
+ if (running.kind === "dir") return;
91
+ const url = running.kind === "url" || running.kind === "url+dir" ? running.url : undefined;
92
+ if (!url) return;
93
+ await waitForHttpReady(url, opts);
94
+ }
95
+ function httpReadyHint(endpoint, skippedTcp) {
96
+ if (!endpoint) return undefined;
97
+ if (skippedTcp) {
98
+ return `Remote/non-loopback probe timed out. Raise readyTimeoutMs, set readyPath, or skipReady: true for host().`;
99
+ }
100
+ return `HTTP never became ready on ${endpoint.host}:${endpoint.port}. Confirm the process binds HOST (see UNTESTUTILS_BIND_HOST) and readyPath returns an accepted status.`;
101
+ }
102
+ function parseHostPort(href) {
103
+ try {
104
+ const u = new URL(href);
105
+ const host = u.hostname.replace(/^\[|\]$/g, "");
106
+ let port = u.port ? Number(u.port) : NaN;
107
+ if (!Number.isFinite(port) || port <= 0) {
108
+ if (u.protocol === "https:") port = 443;
109
+ else if (u.protocol === "http:") port = 80;
110
+ else return null;
111
+ }
112
+ if (!host) return null;
113
+ return {
114
+ host,
115
+ port
116
+ };
117
+ } catch {
118
+ return null;
119
+ }
120
+ }
121
+ function isLoopbackHost(host) {
122
+ const h = host.replace(/^\[|\]$/g, "").toLowerCase();
123
+ return h === LOOPBACK_HOST || h === "127.0.0.1" || h === "localhost" || h === "::1";
124
+ }
125
+ function errMessage(e) {
126
+ if (e instanceof Error) return e.message;
127
+ return String(e);
128
+ }
129
+ function readinessError(target, timeoutMs, detail) {
130
+ const parts = [
131
+ `[untestutils] readiness failed for ${target} after ${timeoutMs}ms`,
132
+ `phase=${detail.phase}`,
133
+ `lastStatus=${detail.lastStatus}`
134
+ ];
135
+ if (detail.lastError) parts.push(`lastError=${JSON.stringify(detail.lastError)}`);
136
+ if (detail.lastBody) parts.push(`body=${JSON.stringify(detail.lastBody.slice(0, 200))}`);
137
+ if (detail.hint) parts.push(detail.hint);
138
+ return new Error(parts.join(" · "));
139
+ }
140
+ function sleep(ms) {
141
+ return new Promise((r) => setTimeout(r, ms));
142
+ }
@@ -0,0 +1,23 @@
1
+ import type { Recipe, RecipeRegistry } from "./types.mjs";
2
+ /** Validate a recipe object (does not register). Prefer wrapping with defineRecipes. */
3
+ export declare function defineRecipe(recipe: Recipe): Recipe;
4
+ /**
5
+ * Register recipes. Optionally pass `import.meta.url` so workers/globalSetup can
6
+ * re-import the same file — otherwise the caller path is inferred from the stack.
7
+ *
8
+ * Prefer: `export const recipes = defineRecipes({ … }, import.meta.url)`
9
+ *
10
+ * Re-imports are idempotent (config + globalSetup/setup-file often load recipes.ts twice
11
+ * under different module URLs). Duplicates *within* one call still throw.
12
+ */
13
+ export declare function defineRecipes<T extends Record<string, Recipe>>(recipes: T, moduleUrl?: string | URL): T & RecipeRegistry;
14
+ /**
15
+ * Idempotent register for plugin options: skip ids already registered
16
+ * (typical when recipes.ts already called defineRecipes and config passes the same map).
17
+ */
18
+ export declare function ensureRecipes(recipes: Record<string, Recipe>): void;
19
+ /** Absolute path to the recipes source module (for child processes). */
20
+ export declare function getRecipesModulePath(): string | undefined;
21
+ export declare function getRegisteredRecipe(id: string): Recipe | undefined;
22
+ export declare function listRegisteredRecipes(): Recipe[];
23
+ export declare function clearRegisteredRecipes(): void;
@@ -0,0 +1,24 @@
1
+ import type { Recipe, RecipeRegistry } from './types';
2
+ /** Validate a recipe object (does not register). Prefer wrapping with defineRecipes. */
3
+ export declare function defineRecipe(recipe: Recipe): Recipe;
4
+ /**
5
+ * Register recipes. Optionally pass `import.meta.url` so workers/globalSetup can
6
+ * re-import the same file — otherwise the caller path is inferred from the stack.
7
+ *
8
+ * Prefer: `export const recipes = defineRecipes({ … }, import.meta.url)`
9
+ *
10
+ * Re-imports are idempotent (config + globalSetup/setup-file often load recipes.ts twice
11
+ * under different module URLs). Duplicates *within* one call still throw.
12
+ */
13
+ export declare function defineRecipes<T extends Record<string, Recipe>>(recipes: T, moduleUrl?: string | URL): T & RecipeRegistry;
14
+ /**
15
+ * Idempotent register for plugin options: skip ids already registered
16
+ * (typical when recipes.ts already called defineRecipes and config passes the same map).
17
+ */
18
+ export declare function ensureRecipes(recipes: Record<string, Recipe>): void;
19
+ /** Absolute path to the recipes source module (for child processes). */
20
+ export declare function getRecipesModulePath(): string | undefined;
21
+ export declare function getRegisteredRecipe(id: string): Recipe | undefined;
22
+ export declare function listRegisteredRecipes(): Recipe[];
23
+ export declare function clearRegisteredRecipes(): void;
24
+ //# sourceMappingURL=recipes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recipes.d.ts","sourceRoot":"","sources":["../src/recipes.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAMtD,wFAAwF;AACxF,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAQnD;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5D,OAAO,EAAE,CAAC,EACV,SAAS,CAAC,EAAE,MAAM,GAAG,GAAG,GACvB,CAAC,GAAG,cAAc,CAoBpB;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAQnE;AAED,wEAAwE;AACxE,wBAAgB,oBAAoB,IAAI,MAAM,GAAG,SAAS,CAEzD;AAED,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAElE;AAED,wBAAgB,qBAAqB,IAAI,MAAM,EAAE,CAEhD;AAED,wBAAgB,sBAAsB,IAAI,IAAI,CAG7C"}
@@ -0,0 +1,94 @@
1
+ import { fileURLToPath } from "node:url";
2
+ const globalRecipes = new Map();
3
+
4
+ let recipesModulePath;
5
+
6
+ export function defineRecipe(recipe) {
7
+ if (!recipe.id) {
8
+ throw new Error("[untestutils] defineRecipe requires recipe.id");
9
+ }
10
+ if (!recipe.start) {
11
+ throw new Error("[untestutils] defineRecipe requires recipe.start");
12
+ }
13
+ return recipe;
14
+ }
15
+
16
+ export function defineRecipes(recipes, moduleUrl) {
17
+ rememberRecipesModule(moduleUrl);
18
+ const ids = new Set();
19
+ for (const [key, recipe] of Object.entries(recipes)) {
20
+ const id = recipe.id ?? key;
21
+ if (ids.has(id)) {
22
+ throw new Error(`[untestutils] duplicate recipe id "${id}" in defineRecipes`);
23
+ }
24
+ ids.add(id);
25
+ if (globalRecipes.has(id)) {
26
+
27
+ continue;
28
+ }
29
+ if (!recipe.id) recipe.id = key;
30
+ if (!recipe.start) {
31
+ throw new Error(`[untestutils] recipe "${id}" requires start()`);
32
+ }
33
+ globalRecipes.set(recipe.id, recipe);
34
+ }
35
+ return recipes;
36
+ }
37
+
38
+ export function ensureRecipes(recipes) {
39
+ const pending = {};
40
+ for (const [key, recipe] of Object.entries(recipes)) {
41
+ const id = recipe.id ?? key;
42
+ if (globalRecipes.has(id)) continue;
43
+ pending[key] = recipe;
44
+ }
45
+ if (Object.keys(pending).length) defineRecipes(pending);
46
+ }
47
+
48
+ export function getRecipesModulePath() {
49
+ return recipesModulePath;
50
+ }
51
+ export function getRegisteredRecipe(id) {
52
+ return globalRecipes.get(id);
53
+ }
54
+ export function listRegisteredRecipes() {
55
+ return [...globalRecipes.values()];
56
+ }
57
+ export function clearRegisteredRecipes() {
58
+ globalRecipes.clear();
59
+ recipesModulePath = undefined;
60
+ }
61
+ function rememberRecipesModule(moduleUrl) {
62
+ if (moduleUrl) {
63
+ recipesModulePath = toFsPath(moduleUrl);
64
+ return;
65
+ }
66
+ if (recipesModulePath) return;
67
+ const inferred = captureCallerPath();
68
+ if (inferred) recipesModulePath = inferred;
69
+ }
70
+ function toFsPath(moduleUrl) {
71
+ const href = typeof moduleUrl === "string" ? moduleUrl : moduleUrl.href;
72
+ return href.startsWith("file:") ? fileURLToPath(href) : href;
73
+ }
74
+
75
+ function captureCallerPath() {
76
+ const previous = Error.prepareStackTrace;
77
+ try {
78
+ Error.prepareStackTrace = (_err, stack) => stack;
79
+ const err = new Error();
80
+ const stack = err.stack;
81
+ for (let i = 2; i < stack.length; i++) {
82
+ const file = stack[i]?.getFileName();
83
+ if (!file || file.startsWith("node:")) continue;
84
+ if (/[/\\]recipes\.[cm]?[jt]s$/.test(file) && /[/\\](core|@untestutils)[/\\]/.test(file)) {
85
+ continue;
86
+ }
87
+ if (file.includes(`${"node_modules"}/@untestutils/core`)) continue;
88
+ return file.startsWith("file:") ? fileURLToPath(file) : file;
89
+ }
90
+ } catch {} finally {
91
+ Error.prepareStackTrace = previous;
92
+ }
93
+ return undefined;
94
+ }
@@ -0,0 +1,6 @@
1
+ import type { RunHelper, RunResult } from "./types.mjs";
2
+ export declare function createRunHelper(defaults?: {
3
+ cwd?: string;
4
+ env?: NodeJS.ProcessEnv;
5
+ }): RunHelper;
6
+ export type { RunResult };
@@ -0,0 +1,7 @@
1
+ import type { RunHelper, RunResult } from './types';
2
+ export declare function createRunHelper(defaults?: {
3
+ cwd?: string;
4
+ env?: NodeJS.ProcessEnv;
5
+ }): RunHelper;
6
+ export type { RunResult };
7
+ //# sourceMappingURL=run-helper.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-helper.d.ts","sourceRoot":"","sources":["../src/run-helper.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAmB,SAAS,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAsBrE,wBAAgB,eAAe,CAC7B,QAAQ,GAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAA;CAAO,GACvD,SAAS,CA6CX;AAED,YAAY,EAAE,SAAS,EAAE,CAAC"}
@@ -0,0 +1,79 @@
1
+ import { spawn } from "node:child_process";
2
+ import { killProcessTree, runCommand, scrubTestEnv } from "./process.mjs";
3
+ import { platform } from "node:os";
4
+ function interpolate(strings, values) {
5
+ let out = "";
6
+ for (let i = 0; i < strings.length; i++) {
7
+ out += strings[i];
8
+ if (i < values.length) out += String(values[i]);
9
+ }
10
+ return out.trim();
11
+ }
12
+ function splitCommand(cmd) {
13
+
14
+ const parts = cmd.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) ?? [];
15
+ const cleaned = parts.map((p) => p.replace(/^['"]|['"]$/g, ""));
16
+ const [command, ...args] = cleaned;
17
+ if (!command) throw new Error("[untestutils] empty command");
18
+ return {
19
+ command,
20
+ args
21
+ };
22
+ }
23
+ export function createRunHelper(defaults = {}) {
24
+ const run = (async (strings, ...values) => {
25
+ const cmd = interpolate(strings, values);
26
+ const { command, args } = splitCommand(cmd);
27
+ const result = await runCommand(command, args, {
28
+ cwd: defaults.cwd,
29
+ env: {
30
+ ...scrubTestEnv(),
31
+ ...defaults.env
32
+ }
33
+ });
34
+ return result;
35
+ });
36
+ run.command = async (cmd, opts = {}) => {
37
+ const { command, args } = splitCommand(cmd);
38
+ return runCommand(command, args, {
39
+ cwd: opts.cwd ?? defaults.cwd,
40
+ env: {
41
+ ...scrubTestEnv(),
42
+ ...defaults.env,
43
+ ...opts.env
44
+ }
45
+ });
46
+ };
47
+ run.detached = async (strings, ...values) => {
48
+ const cmd = interpolate(strings, values);
49
+ const { command, args } = splitCommand(cmd);
50
+ const env = {
51
+ ...scrubTestEnv(),
52
+ ...defaults.env
53
+ };
54
+ let output = "";
55
+ const child = spawn(command, args, {
56
+ cwd: defaults.cwd,
57
+ env,
58
+ stdio: [
59
+ "ignore",
60
+ "pipe",
61
+ "pipe"
62
+ ],
63
+ detached: platform() !== "win32"
64
+ });
65
+ child.stdout?.on("data", (c) => {
66
+ output += c.toString();
67
+ });
68
+ child.stderr?.on("data", (c) => {
69
+ output += c.toString();
70
+ });
71
+ const proc = {
72
+ pid: child.pid,
73
+ stop: async () => killProcessTree(child),
74
+ logs: () => output
75
+ };
76
+ return proc;
77
+ };
78
+ return run;
79
+ }
@@ -0,0 +1,27 @@
1
+ export interface TargetEntry {
2
+ id: string;
3
+ url?: string;
4
+ dir?: string;
5
+ identity: string;
6
+ /** Managed server pid — written by the starter worker so global teardown can kill orphans. */
7
+ pid?: number;
8
+ }
9
+ export type TargetMap = Record<string, TargetEntry>;
10
+ export declare class TargetRegistry {
11
+ private readonly artifactsRoot;
12
+ constructor(artifactsRoot: string);
13
+ get filePath(): string;
14
+ private lockPath;
15
+ private withLock;
16
+ read(): Promise<TargetMap>;
17
+ write(map: TargetMap): Promise<void>;
18
+ set(entry: TargetEntry): Promise<TargetEntry | undefined>;
19
+ get(id: string): Promise<TargetEntry | undefined>;
20
+ applyAllToEnv(map: TargetMap): void;
21
+ applyEnv(entry: TargetEntry): void;
22
+ clear(): Promise<void>;
23
+ /** Atomically read + clear — used by global teardown so no pid is lost to races. */
24
+ drain(): Promise<TargetMap>;
25
+ }
26
+ export declare function envKey(id: string): string;
27
+ export declare function envDirKey(id: string): string;
@@ -0,0 +1,28 @@
1
+ export interface TargetEntry {
2
+ id: string;
3
+ url?: string;
4
+ dir?: string;
5
+ identity: string;
6
+ /** Managed server pid — written by the starter worker so global teardown can kill orphans. */
7
+ pid?: number;
8
+ }
9
+ export type TargetMap = Record<string, TargetEntry>;
10
+ export declare class TargetRegistry {
11
+ private readonly artifactsRoot;
12
+ constructor(artifactsRoot: string);
13
+ get filePath(): string;
14
+ private lockPath;
15
+ private withLock;
16
+ read(): Promise<TargetMap>;
17
+ write(map: TargetMap): Promise<void>;
18
+ set(entry: TargetEntry): Promise<TargetEntry | undefined>;
19
+ get(id: string): Promise<TargetEntry | undefined>;
20
+ applyAllToEnv(map: TargetMap): void;
21
+ applyEnv(entry: TargetEntry): void;
22
+ clear(): Promise<void>;
23
+ /** Atomically read + clear — used by global teardown so no pid is lost to races. */
24
+ drain(): Promise<TargetMap>;
25
+ }
26
+ export declare function envKey(id: string): string;
27
+ export declare function envDirKey(id: string): string;
28
+ //# sourceMappingURL=target-registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"target-registry.d.ts","sourceRoot":"","sources":["../src/target-registry.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,8FAA8F;IAC9F,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;AAEpD,qBAAa,cAAc;IACb,OAAO,CAAC,QAAQ,CAAC,aAAa;gBAAb,aAAa,EAAE,MAAM;IAElD,IAAI,QAAQ,IAAI,MAAM,CAErB;IAED,OAAO,CAAC,QAAQ;YAIF,QAAQ;IAUhB,IAAI,IAAI,OAAO,CAAC,SAAS,CAAC;IAS1B,KAAK,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAKpC,GAAG,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC;IAgBzD,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC;IAiBvD,aAAa,CAAC,GAAG,EAAE,SAAS,GAAG,IAAI;IAInC,QAAQ,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI;IAK5B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAM5B,oFAAoF;IAC9E,KAAK,IAAI,OAAO,CAAC,SAAS,CAAC;CAOlC;AAED,wBAAgB,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAEzC;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAE5C"}