@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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 s00d
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,19 @@
1
+ import { SCHEMA_VERSION } from "./types.mjs";
2
+ export interface BuildMeta {
3
+ schemaVersion: typeof SCHEMA_VERSION;
4
+ hash: string;
5
+ identity: string;
6
+ createdAt: number;
7
+ }
8
+ export declare class ArtifactStore {
9
+ private readonly artifactsRoot;
10
+ constructor(artifactsRoot: string);
11
+ buildDir(identity: string): string;
12
+ hashFile(identity: string): string;
13
+ readyFile(identity: string): string;
14
+ metaFile(identity: string): string;
15
+ ensureDir(identity: string): Promise<string>;
16
+ isWarm(identity: string, hash: string): Promise<boolean>;
17
+ commit(identity: string, hash: string): Promise<void>;
18
+ invalidate(identity: string): Promise<void>;
19
+ }
@@ -0,0 +1,20 @@
1
+ import { SCHEMA_VERSION } from './types';
2
+ export interface BuildMeta {
3
+ schemaVersion: typeof SCHEMA_VERSION;
4
+ hash: string;
5
+ identity: string;
6
+ createdAt: number;
7
+ }
8
+ export declare class ArtifactStore {
9
+ private readonly artifactsRoot;
10
+ constructor(artifactsRoot: string);
11
+ buildDir(identity: string): string;
12
+ hashFile(identity: string): string;
13
+ readyFile(identity: string): string;
14
+ metaFile(identity: string): string;
15
+ ensureDir(identity: string): Promise<string>;
16
+ isWarm(identity: string, hash: string): Promise<boolean>;
17
+ commit(identity: string, hash: string): Promise<void>;
18
+ invalidate(identity: string): Promise<void>;
19
+ }
20
+ //# sourceMappingURL=artifact-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"artifact-store.d.ts","sourceRoot":"","sources":["../src/artifact-store.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAKzC,MAAM,WAAW,SAAS;IACxB,aAAa,EAAE,OAAO,cAAc,CAAC;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,qBAAa,aAAa;IACZ,OAAO,CAAC,QAAQ,CAAC,aAAa;gBAAb,aAAa,EAAE,MAAM;IAElD,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM;IAIlC,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM;IAIlC,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM;IAInC,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM;IAI5B,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAO5C,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAOxD,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAarD,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAGlD"}
@@ -0,0 +1,52 @@
1
+ import { existsSync } from "node:fs";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { join } from "pathe";
4
+ import { SCHEMA_VERSION } from "./types.mjs";
5
+ import { isReady, markReady, removePath } from "./lock.mjs";
6
+ import { debug } from "./debug.mjs";
7
+ import { refuseArtifactsInsidePackage } from "./paths.mjs";
8
+ export class ArtifactStore {
9
+ artifactsRoot;
10
+ constructor(artifactsRoot) {
11
+ this.artifactsRoot = artifactsRoot;
12
+ }
13
+ buildDir(identity) {
14
+ return join(this.artifactsRoot, "builds", identity);
15
+ }
16
+ hashFile(identity) {
17
+ return join(this.buildDir(identity), ".build-hash");
18
+ }
19
+ readyFile(identity) {
20
+ return join(this.buildDir(identity), ".ready");
21
+ }
22
+ metaFile(identity) {
23
+ return join(this.buildDir(identity), ".meta.json");
24
+ }
25
+ async ensureDir(identity) {
26
+ const dir = this.buildDir(identity);
27
+ refuseArtifactsInsidePackage(dir);
28
+ await mkdir(dir, { recursive: true });
29
+ return dir;
30
+ }
31
+ async isWarm(identity, hash) {
32
+ if (!await isReady(this.readyFile(identity))) return false;
33
+ if (!existsSync(this.hashFile(identity))) return false;
34
+ const stored = (await readFile(this.hashFile(identity), "utf8")).trim();
35
+ return stored === hash;
36
+ }
37
+ async commit(identity, hash) {
38
+ const meta = {
39
+ schemaVersion: SCHEMA_VERSION,
40
+ hash,
41
+ identity,
42
+ createdAt: Date.now()
43
+ };
44
+ await writeFile(this.hashFile(identity), hash, "utf8");
45
+ await writeFile(this.metaFile(identity), JSON.stringify(meta, null, 2), "utf8");
46
+ await markReady(this.readyFile(identity));
47
+ debug("artifact", `committed ${identity}`);
48
+ }
49
+ async invalidate(identity) {
50
+ await removePath(this.buildDir(identity));
51
+ }
52
+ }
@@ -0,0 +1,5 @@
1
+ export declare function isDebug(): boolean;
2
+ export declare function debug(scope: string, message: string, extra?: unknown): void;
3
+ /** Non-lifecycle messages — stdout so Vitest does not label as [error]. */
4
+ export declare function log(scope: string, message: string): void;
5
+ export declare function envFlag(name: string, defaultValue?: boolean): boolean;
@@ -0,0 +1,6 @@
1
+ export declare function isDebug(): boolean;
2
+ export declare function debug(scope: string, message: string, extra?: unknown): void;
3
+ /** Non-lifecycle messages — stdout so Vitest does not label as [error]. */
4
+ export declare function log(scope: string, message: string): void;
5
+ export declare function envFlag(name: string, defaultValue?: boolean): boolean;
6
+ //# sourceMappingURL=debug.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"debug.d.ts","sourceRoot":"","sources":["../src/debug.ts"],"names":[],"mappings":"AAEA,wBAAgB,OAAO,IAAI,OAAO,CAEjC;AAED,wBAAgB,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,GAAG,IAAI,CAQ3E;AAED,2EAA2E;AAC3E,wBAAgB,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAExD;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,UAAQ,GAAG,OAAO,CAInE"}
package/dist/debug.mjs ADDED
@@ -0,0 +1,22 @@
1
+ const PREFIX = "[untestutils";
2
+ export function isDebug() {
3
+ return process.env.UNTESTUTILS_DEBUG === "1" || process.env.UNTESTUTILS_DEBUG === "true";
4
+ }
5
+ export function debug(scope, message, extra) {
6
+ if (!isDebug()) return;
7
+
8
+ if (extra !== undefined) {
9
+ console.error(`${PREFIX}:${scope}] ${message}`, extra);
10
+ } else {
11
+ console.error(`${PREFIX}:${scope}] ${message}`);
12
+ }
13
+ }
14
+
15
+ export function log(scope, message) {
16
+ console.log(`${PREFIX}:${scope}] ${message}`);
17
+ }
18
+ export function envFlag(name, defaultValue = false) {
19
+ const v = process.env[name];
20
+ if (v === undefined) return defaultValue;
21
+ return v === "1" || v === "true";
22
+ }
@@ -0,0 +1,57 @@
1
+ import { type Recipe, type PrepareCtx, type StartCtx, type SharePolicy } from "../index.mjs";
2
+ export interface ResolveBinOptions {
3
+ roots: string[];
4
+ packageJsonIds?: string[];
5
+ binRelative?: string[];
6
+ directIds?: string[];
7
+ label: string;
8
+ }
9
+ /**
10
+ * Resolve a CLI entry from the app under test (or cwd).
11
+ */
12
+ export declare function resolveBin(opts: ResolveBinOptions): string;
13
+ export type SpawnSpec = {
14
+ command: string;
15
+ args: string[];
16
+ env?: Record<string, string>;
17
+ cwd?: string;
18
+ };
19
+ export interface CliFrameworkRecipeOptions {
20
+ id: string;
21
+ root: string;
22
+ label: string;
23
+ share?: SharePolicy;
24
+ env?: Record<string, string>;
25
+ hashInputs?: string[];
26
+ readyPath?: string;
27
+ readyTimeoutMs?: number;
28
+ prepare?: (ctx: PrepareCtx & {
29
+ root: string;
30
+ }) => Promise<SpawnSpec | void> | SpawnSpec | void;
31
+ start: (ctx: StartCtx & {
32
+ root: string;
33
+ }) => SpawnSpec | Promise<SpawnSpec>;
34
+ verifyAfterPrepare?: (ctx: PrepareCtx & {
35
+ root: string;
36
+ }) => Promise<void>;
37
+ }
38
+ export type FrameworkBaseOptions = {
39
+ id?: string;
40
+ root: string;
41
+ env?: Record<string, string>;
42
+ hashInputs?: string[];
43
+ readyPath?: string;
44
+ readyTimeoutMs?: number;
45
+ };
46
+ export declare function frameworkRoots(root: string): string[];
47
+ /**
48
+ * Fail early with a clear message when a fixture root (or its package.json) is missing.
49
+ * Call from prepare/start — not at Recipe factory time (hashInputs may use placeholder roots).
50
+ */
51
+ export declare function assertAppRoot(root: string, label: string, opts?: {
52
+ requirePackageJson?: boolean;
53
+ }): string;
54
+ /**
55
+ * Shared Recipe shell for framework CLI drivers (vite/next/astro/…).
56
+ */
57
+ export declare function cliFrameworkRecipe(opts: CliFrameworkRecipeOptions): Recipe;
@@ -0,0 +1,64 @@
1
+ import { runCommand, type Recipe, type PrepareCtx, type StartCtx, type SharePolicy } from '..';
2
+ export interface ResolveBinOptions {
3
+ roots: string[];
4
+ packageJsonIds?: string[];
5
+ binRelative?: string[];
6
+ directIds?: string[];
7
+ label: string;
8
+ }
9
+ /**
10
+ * Resolve a CLI entry from the app under test (or cwd).
11
+ */
12
+ export declare function resolveBin(opts: ResolveBinOptions): string;
13
+ export type SpawnSpec = {
14
+ command: string;
15
+ args: string[];
16
+ env?: Record<string, string>;
17
+ cwd?: string;
18
+ };
19
+ export interface CliFrameworkRecipeOptions {
20
+ id: string;
21
+ root: string;
22
+ label: string;
23
+ share?: SharePolicy;
24
+ env?: Record<string, string>;
25
+ hashInputs?: string[];
26
+ readyPath?: string;
27
+ readyTimeoutMs?: number;
28
+ prepare?: (ctx: PrepareCtx & {
29
+ root: string;
30
+ }) => Promise<SpawnSpec | void> | SpawnSpec | void;
31
+ start: (ctx: StartCtx & {
32
+ root: string;
33
+ }) => SpawnSpec | Promise<SpawnSpec>;
34
+ verifyAfterPrepare?: (ctx: PrepareCtx & {
35
+ root: string;
36
+ }) => Promise<void>;
37
+ }
38
+ export type FrameworkBaseOptions = {
39
+ id?: string;
40
+ root: string;
41
+ env?: Record<string, string>;
42
+ hashInputs?: string[];
43
+ readyPath?: string;
44
+ readyTimeoutMs?: number;
45
+ };
46
+ export declare function frameworkRoots(root: string): string[];
47
+ /**
48
+ * Fail early with a clear message when a fixture root (or its package.json) is missing.
49
+ * Call from prepare/start — not at Recipe factory time (hashInputs may use placeholder roots).
50
+ */
51
+ export declare function assertAppRoot(root: string, label: string, opts?: {
52
+ requirePackageJson?: boolean;
53
+ }): string;
54
+ /**
55
+ * Shared Recipe shell for framework CLI drivers (vite/next/astro/…).
56
+ */
57
+ export declare function cliFrameworkRecipe(opts: CliFrameworkRecipeOptions): Recipe;
58
+ /** @internal */
59
+ export declare const _cliInternals: {
60
+ resolveBin: typeof resolveBin;
61
+ runCommand: typeof runCommand;
62
+ assertAppRoot: typeof assertAppRoot;
63
+ };
64
+ //# sourceMappingURL=cli-framework.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-framework.d.ts","sourceRoot":"","sources":["../../src/drivers/cli-framework.ts"],"names":[],"mappings":"AAGA,OAAO,EAKL,UAAU,EACV,KAAK,MAAM,EACX,KAAK,UAAU,EACf,KAAK,QAAQ,EACb,KAAK,WAAW,EACjB,MAAM,IAAI,CAAC;AAEZ,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,iBAAiB,GAAG,MAAM,CAmC1D;AAED,MAAM,MAAM,SAAS,GAAG;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,WAAW,yBAAyB;IACxC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,UAAU,GAAG;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,SAAS,GAAG,IAAI,CAAC;IAC/F,KAAK,EAAE,CAAC,GAAG,EAAE,QAAQ,GAAG;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,KAAK,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;IAC5E,kBAAkB,CAAC,EAAE,CAAC,GAAG,EAAE,UAAU,GAAG;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5E;AAED,MAAM,MAAM,oBAAoB,GAAG;IACjC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAErD;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,IAAI,GAAE;IAAE,kBAAkB,CAAC,EAAE,OAAO,CAAA;CAAO,GAC1C,MAAM,CAYR;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,yBAAyB,GAAG,MAAM,CAuE1E;AAED,gBAAgB;AAChB,eAAO,MAAM,aAAa;;;;CAA4C,CAAC"}
@@ -0,0 +1,138 @@
1
+ import { createRequire } from "node:module";
2
+ import { existsSync } from "node:fs";
3
+ import { dirname, join, resolve } from "pathe";
4
+ import { defineRecipe, loopbackUrl, waitForHttpReady, spawnManaged, runCommand } from "../index.mjs";
5
+
6
+ export function resolveBin(opts) {
7
+ const seen = new Set();
8
+ for (const root of opts.roots) {
9
+ const key = resolve(root);
10
+ if (seen.has(key)) continue;
11
+ seen.add(key);
12
+ const pkgPath = join(key, "package.json");
13
+ if (!existsSync(pkgPath)) continue;
14
+ let req;
15
+ try {
16
+ req = createRequire(pkgPath);
17
+ } catch {
18
+ continue;
19
+ }
20
+ for (const id of opts.directIds ?? []) {
21
+ try {
22
+ return req.resolve(id);
23
+ } catch {}
24
+ }
25
+ for (const pkgId of opts.packageJsonIds ?? []) {
26
+ try {
27
+ const pkgJson = req.resolve(pkgId);
28
+ const pkgRoot = dirname(pkgJson);
29
+ for (const rel of opts.binRelative ?? []) {
30
+ const bin = join(pkgRoot, rel);
31
+ if (existsSync(bin)) return bin;
32
+ }
33
+ } catch {}
34
+ }
35
+ }
36
+ throw new Error(`[untestutils/${opts.label}] cannot resolve CLI from ${opts.roots.join(", ")}`);
37
+ }
38
+ export function frameworkRoots(root) {
39
+ return [root, process.cwd()];
40
+ }
41
+
42
+ export function assertAppRoot(root, label, opts = {}) {
43
+ const resolved = resolve(root);
44
+ if (!existsSync(resolved)) {
45
+ throw new Error(`[untestutils/${label}] root not found: ${resolved}`);
46
+ }
47
+ if (opts.requirePackageJson !== false) {
48
+ const pkg = join(resolved, "package.json");
49
+ if (!existsSync(pkg)) {
50
+ throw new Error(`[untestutils/${label}] package.json not found in root: ${resolved}`);
51
+ }
52
+ }
53
+ return resolved;
54
+ }
55
+
56
+ export function cliFrameworkRecipe(opts) {
57
+ const root = resolve(opts.root);
58
+ const ensureRoot = () => assertAppRoot(root, opts.label);
59
+ const recipe = defineRecipe({
60
+ id: opts.id,
61
+ share: opts.share ?? (opts.prepare ? "always" : "never"),
62
+ hashInputs: async () => {
63
+ const inputs = [...opts.hashInputs ?? [root]];
64
+ if (opts.env && Object.keys(opts.env).length) {
65
+ inputs.push(`env:${JSON.stringify(opts.env)}`);
66
+ }
67
+ return inputs;
68
+ },
69
+ ready: async () => {},
70
+ prepare: opts.prepare ? async (ctx) => {
71
+ ensureRoot();
72
+ const enriched = {
73
+ ...ctx,
74
+ root
75
+ };
76
+ const spec = await opts.prepare(enriched);
77
+ if (spec) {
78
+ const result = await runCommand(spec.command, spec.args, {
79
+ cwd: spec.cwd ?? root,
80
+ env: {
81
+ ...process.env,
82
+ ...opts.env,
83
+ ...spec.env,
84
+ OUT_DIR: ctx.outDir
85
+ },
86
+ timeoutMs: opts.readyTimeoutMs ?? 3e5
87
+ });
88
+ if (result.exitCode !== 0) {
89
+ throw new Error(`[untestutils/${opts.label}] prepare failed (${result.exitCode}):\n${result.stderr.slice(-2e3)}\n${result.stdout.slice(-1e3)}`);
90
+ }
91
+ }
92
+ if (opts.verifyAfterPrepare) await opts.verifyAfterPrepare(enriched);
93
+ } : undefined,
94
+ start: async (ctx) => {
95
+ ensureRoot();
96
+ const spec = await opts.start({
97
+ ...ctx,
98
+ root
99
+ });
100
+ const managed = spawnManaged(spec.command, spec.args, {
101
+ cwd: spec.cwd ?? root,
102
+ env: {
103
+ ...process.env,
104
+ ...opts.env,
105
+ ...spec.env,
106
+ PORT: String(ctx.port),
107
+ HOST: ctx.host
108
+ },
109
+ captureLogs: true
110
+ });
111
+ const url = loopbackUrl(ctx.port, "/", ctx.host);
112
+ try {
113
+ await waitForHttpReady(url, {
114
+ path: opts.readyPath ?? "/",
115
+ timeoutMs: opts.readyTimeoutMs ?? 12e4
116
+ });
117
+ } catch (e) {
118
+ await managed.stop();
119
+ throw new Error(`[untestutils/${opts.label}] ready failed:\n${e}\n--- logs ---\n${managed.logs().slice(-4e3)}`);
120
+ }
121
+ return {
122
+ kind: "url+dir",
123
+ url,
124
+ dir: ctx.outDir,
125
+ stop: managed.stop,
126
+ pid: managed.pid
127
+ };
128
+ }
129
+ });
130
+ recipe.root = root;
131
+ return recipe;
132
+ }
133
+
134
+ export const _cliInternals = {
135
+ resolveBin,
136
+ runCommand,
137
+ assertAppRoot
138
+ };
@@ -0,0 +1,15 @@
1
+ import { type Recipe } from "../index.mjs";
2
+ export interface CommandOptions {
3
+ id: string;
4
+ cwd?: string;
5
+ prepare?: string;
6
+ /** Shell command; `$PORT`, `$HOST`, and `$OUT_DIR` are substituted */
7
+ start?: string;
8
+ readyPath?: string;
9
+ readyTimeoutMs?: number;
10
+ hashInputs?: string[];
11
+ share?: Recipe["share"];
12
+ env?: Record<string, string>;
13
+ }
14
+ export declare function command(opts: CommandOptions): Recipe;
15
+ export default command;
@@ -0,0 +1,16 @@
1
+ import { type Recipe } from '..';
2
+ export interface CommandOptions {
3
+ id: string;
4
+ cwd?: string;
5
+ prepare?: string;
6
+ /** Shell command; `$PORT`, `$HOST`, and `$OUT_DIR` are substituted */
7
+ start?: string;
8
+ readyPath?: string;
9
+ readyTimeoutMs?: number;
10
+ hashInputs?: string[];
11
+ share?: Recipe['share'];
12
+ env?: Record<string, string>;
13
+ }
14
+ export declare function command(opts: CommandOptions): Recipe;
15
+ export default command;
16
+ //# sourceMappingURL=command.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"command.d.ts","sourceRoot":"","sources":["../../src/drivers/command.ts"],"names":[],"mappings":"AACA,OAAO,EAKL,KAAK,MAAM,EACZ,MAAM,IAAI,CAAC;AAEZ,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,sEAAsE;IACtE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC9B;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,cAAc,GAAG,MAAM,CA6DpD;AAED,eAAe,OAAO,CAAC"}
@@ -0,0 +1,70 @@
1
+ import { resolve } from "pathe";
2
+ import { defineRecipe, loopbackUrl, waitForHttpReady, spawnManaged } from "../index.mjs";
3
+ export function command(opts) {
4
+ const cwd = opts.cwd ? resolve(opts.cwd) : undefined;
5
+ const recipe = defineRecipe({
6
+ id: opts.id,
7
+ share: opts.share ?? (opts.start ? "always" : "prepare-only"),
8
+ hashInputs: async () => {
9
+ if (opts.hashInputs?.length) return opts.hashInputs.map((p) => resolve(cwd ?? process.cwd(), p));
10
+ return cwd ? [cwd] : [];
11
+ },
12
+ ready: opts.start ? async () => {} : undefined,
13
+ prepare: opts.prepare ? async ({ run, outDir }) => {
14
+ const result = await run.command(opts.prepare, {
15
+ cwd,
16
+ env: {
17
+ ...process.env,
18
+ ...opts.env,
19
+ OUT_DIR: outDir
20
+ }
21
+ });
22
+ if (result.exitCode !== 0) {
23
+ throw new Error(`[untestutils/command] prepare failed (${result.exitCode}):\n${result.stderr.slice(-2e3)}`);
24
+ }
25
+ } : undefined,
26
+ start: async ({ port, host, outDir }) => {
27
+ if (!opts.start) {
28
+ return {
29
+ kind: "dir",
30
+ dir: outDir
31
+ };
32
+ }
33
+ const rendered = opts.start.replaceAll("$PORT", String(port)).replaceAll("$HOST", host).replaceAll("$OUT_DIR", outDir);
34
+ const parts = rendered.trim().split(/\s+/);
35
+ const cmd = parts[0];
36
+ const args = parts.slice(1);
37
+ const managed = spawnManaged(cmd, args, {
38
+ cwd,
39
+ env: {
40
+ ...process.env,
41
+ ...opts.env,
42
+ PORT: String(port),
43
+ HOST: host,
44
+ OUT_DIR: outDir
45
+ },
46
+ captureLogs: true
47
+ });
48
+ const url = loopbackUrl(port, "/", host);
49
+ try {
50
+ await waitForHttpReady(url, {
51
+ path: opts.readyPath ?? "/",
52
+ timeoutMs: opts.readyTimeoutMs
53
+ });
54
+ } catch (e) {
55
+ await managed.stop();
56
+ throw new Error(`${e}\n--- logs ---\n${managed.logs().slice(-4e3)}`);
57
+ }
58
+ return {
59
+ kind: "url+dir",
60
+ url,
61
+ dir: outDir,
62
+ stop: managed.stop,
63
+ pid: managed.pid
64
+ };
65
+ }
66
+ });
67
+ if (cwd) recipe.root = cwd;
68
+ return recipe;
69
+ }
70
+ export default command;
@@ -0,0 +1,4 @@
1
+ import type { Recipe } from "../index.mjs";
2
+ /** Framework/app adapter → Recipe. Core never imports frameworks. */
3
+ export type Driver<Opts = unknown> = (opts: Opts) => Recipe;
4
+ export declare function defineDriver<Opts>(impl: Driver<Opts>): Driver<Opts>;
@@ -0,0 +1,5 @@
1
+ import type { Recipe } from '..';
2
+ /** Framework/app adapter → Recipe. Core never imports frameworks. */
3
+ export type Driver<Opts = unknown> = (opts: Opts) => Recipe;
4
+ export declare function defineDriver<Opts>(impl: Driver<Opts>): Driver<Opts>;
5
+ //# sourceMappingURL=define-driver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"define-driver.d.ts","sourceRoot":"","sources":["../../src/drivers/define-driver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,IAAI,CAAC;AAEjC,qEAAqE;AACrE,MAAM,MAAM,MAAM,CAAC,IAAI,GAAG,OAAO,IAAI,CAAC,IAAI,EAAE,IAAI,KAAK,MAAM,CAAC;AAE5D,wBAAgB,YAAY,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,CAEnE"}
@@ -0,0 +1,3 @@
1
+ export function defineDriver(impl) {
2
+ return impl;
3
+ }
@@ -0,0 +1,18 @@
1
+ import { type Recipe } from "../index.mjs";
2
+ import { type Driver } from "./define-driver.mjs";
3
+ export interface HostOptions {
4
+ id: string;
5
+ /** Absolute or env-resolved URL of an already-deployed app (staging/prod). */
6
+ url: string;
7
+ readyPath?: string;
8
+ readyTimeoutMs?: number;
9
+ /** Skip HTTP ready check (default false). */
10
+ skipReady?: boolean;
11
+ share?: Recipe["share"];
12
+ }
13
+ /**
14
+ * Attach to an already-running remote/staging/prod URL — no local prepare/start.
15
+ * Use for post-deploy smoke with Playwright/Vitest.
16
+ */
17
+ export declare const host: Driver<HostOptions>;
18
+ export default host;
@@ -0,0 +1,19 @@
1
+ import { type Recipe } from '..';
2
+ import { type Driver } from './define-driver';
3
+ export interface HostOptions {
4
+ id: string;
5
+ /** Absolute or env-resolved URL of an already-deployed app (staging/prod). */
6
+ url: string;
7
+ readyPath?: string;
8
+ readyTimeoutMs?: number;
9
+ /** Skip HTTP ready check (default false). */
10
+ skipReady?: boolean;
11
+ share?: Recipe['share'];
12
+ }
13
+ /**
14
+ * Attach to an already-running remote/staging/prod URL — no local prepare/start.
15
+ * Use for post-deploy smoke with Playwright/Vitest.
16
+ */
17
+ export declare const host: Driver<HostOptions>;
18
+ export default host;
19
+ //# sourceMappingURL=host.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"host.d.ts","sourceRoot":"","sources":["../../src/drivers/host.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoD,KAAK,MAAM,EAAE,MAAM,IAAI,CAAC;AACnF,OAAO,EAAgB,KAAK,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAE5D,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,8EAA8E;IAC9E,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,6CAA6C;IAC7C,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;CACzB;AAED;;;GAGG;AACH,eAAO,MAAM,IAAI,EAAE,MAAM,CAAC,WAAW,CA0BnC,CAAC;AAEH,eAAe,IAAI,CAAC"}
@@ -0,0 +1,31 @@
1
+ import { defineRecipe, normalizeBaseUrl, waitForHttpReady } from "../index.mjs";
2
+ import { defineDriver } from "./define-driver.mjs";
3
+
4
+ export const host = defineDriver((opts) => {
5
+ if (!opts.url?.trim()) {
6
+ throw new Error("[untestutils/host] url is required");
7
+ }
8
+ const url = normalizeBaseUrl(opts.url);
9
+ return defineRecipe({
10
+ id: opts.id,
11
+ share: opts.share ?? "always",
12
+
13
+ ready: async () => {},
14
+ start: async () => {
15
+ if (!opts.skipReady) {
16
+ await waitForHttpReady(url, {
17
+ path: opts.readyPath ?? "/",
18
+ timeoutMs: opts.readyTimeoutMs
19
+ });
20
+ }
21
+ return {
22
+ kind: "url",
23
+ url,
24
+ stop: async () => {
25
+
26
+ }
27
+ };
28
+ }
29
+ });
30
+ });
31
+ export default host;
@@ -0,0 +1,12 @@
1
+ export type { Driver } from "./define-driver.mjs";
2
+ export { defineDriver } from "./define-driver.mjs";
3
+ export { command } from "./command.mjs";
4
+ export type { CommandOptions } from "./command.mjs";
5
+ export { staticDir } from "./static-dir.mjs";
6
+ export type { StaticDirOptions } from "./static-dir.mjs";
7
+ export { nodeEntry } from "./node-entry.mjs";
8
+ export type { NodeEntryOptions } from "./node-entry.mjs";
9
+ export { host } from "./host.mjs";
10
+ export type { HostOptions } from "./host.mjs";
11
+ export { resolveBin, assertAppRoot, cliFrameworkRecipe, frameworkRoots, _cliInternals } from "./cli-framework.mjs";
12
+ export type { ResolveBinOptions, SpawnSpec, CliFrameworkRecipeOptions, FrameworkBaseOptions } from "./cli-framework.mjs";
@@ -0,0 +1,13 @@
1
+ export type { Driver } from './define-driver';
2
+ export { defineDriver } from './define-driver';
3
+ export { command } from './command';
4
+ export type { CommandOptions } from './command';
5
+ export { staticDir } from './static-dir';
6
+ export type { StaticDirOptions } from './static-dir';
7
+ export { nodeEntry } from './node-entry';
8
+ export type { NodeEntryOptions } from './node-entry';
9
+ export { host } from './host';
10
+ export type { HostOptions } from './host';
11
+ export { resolveBin, assertAppRoot, cliFrameworkRecipe, frameworkRoots, _cliInternals, } from './cli-framework';
12
+ export type { ResolveBinOptions, SpawnSpec, CliFrameworkRecipeOptions, FrameworkBaseOptions, } from './cli-framework';
13
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/drivers/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,YAAY,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAChD,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,YAAY,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,YAAY,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAC9B,YAAY,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAC1C,OAAO,EACL,UAAU,EACV,aAAa,EACb,kBAAkB,EAClB,cAAc,EACd,aAAa,GACd,MAAM,iBAAiB,CAAC;AACzB,YAAY,EACV,iBAAiB,EACjB,SAAS,EACT,yBAAyB,EACzB,oBAAoB,GACrB,MAAM,iBAAiB,CAAC"}