@owlmeans/test 0.1.18-rc.34 → 0.1.18-rc.35
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 +4 -4
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/testing-unit/SKILL.md +1 -1
- package/build/env.d.ts +3 -14
- package/build/env.d.ts.map +1 -1
- package/build/env.js +60 -55
- package/build/env.js.map +1 -1
- package/build/gates.d.ts +1 -5
- package/build/gates.d.ts.map +1 -1
- package/build/gates.js +2 -2
- package/build/gates.js.map +1 -1
- package/build/index.d.ts +1 -0
- package/build/index.d.ts.map +1 -1
- package/build/types.d.ts +31 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +2 -0
- package/build/types.js.map +1 -0
- package/package.json +2 -2
- package/src/env.ts +54 -57
- package/src/gates.ts +3 -7
- package/src/index.ts +1 -0
- package/src/types.ts +37 -0
package/README.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Foundation helpers shared by the OwlMeans test packages and individual package test suites.
|
|
4
4
|
|
|
5
|
-
- `loadEnv(opts?)` — locate `<monorepo-root>/.env`, parse it, merge into `process.env` (idempotent; no-op when Bun already loaded the file via `--env-file`).
|
|
6
|
-
- `requireEnv(keys)` — returns `{ ok: true }` or `{ skip: true, reason }` so callers can `test.skip(...)` when an integration variable is missing.
|
|
7
|
-
- `hasEnv(key)` — boolean shorthand for conditional service provisioning in `tests/context.ts`.
|
|
5
|
+
- `envHelper.loadEnv(opts?)` — locate `<monorepo-root>/.env`, parse it, merge into `process.env` (idempotent; no-op when Bun already loaded the file via `--env-file`).
|
|
6
|
+
- `envHelper.requireEnv(keys)` — returns `{ ok: true }` or `{ skip: true, reason }` so callers can `test.skip(...)` when an integration variable is missing.
|
|
7
|
+
- `envHelper.hasEnv(key)` — boolean shorthand for conditional service provisioning in `tests/context.ts`.
|
|
8
8
|
- `makeGates(spec)` — builds a frozen record of `{ <name>: { skip, reason } }` so a per-package `tests/context.ts` exposes one source of truth and individual specs can self-skip.
|
|
9
9
|
- `loadFixture<T>(relPath)` — JSON fixture loader rooted at the consuming package's `tests/fixtures/`.
|
|
10
10
|
|
|
@@ -18,7 +18,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
|
|
|
18
18
|
your project's skill store (`.agents/skills/`):
|
|
19
19
|
|
|
20
20
|
```sh
|
|
21
|
-
npx @owlmeans/agent-skills@^0.1.18-rc.
|
|
21
|
+
npx @owlmeans/agent-skills@^0.1.18-rc.49
|
|
22
22
|
```
|
|
23
23
|
|
|
24
24
|
The embedded files are version-matched to this package release. Do not edit them
|
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"package": "@owlmeans/test",
|
|
4
|
-
"version": "0.1.18-rc.
|
|
5
|
-
"generatedAt": "2026-10-
|
|
4
|
+
"version": "0.1.18-rc.35",
|
|
5
|
+
"generatedAt": "2026-10-05T21:03:06.380Z",
|
|
6
6
|
"canonicalRepo": "https://github.com/owlmeans/common",
|
|
7
7
|
"entries": [
|
|
8
8
|
{
|
|
@@ -6,7 +6,7 @@ description: Category-A unit tests for OwlMeans Common packages — no mocks, re
|
|
|
6
6
|
|
|
7
7
|
# Unit Tests — Category A (no mocks)
|
|
8
8
|
|
|
9
|
-
**Install:** `"@owlmeans/test": "^0.1.18-rc.
|
|
9
|
+
**Install:** `"@owlmeans/test": "^0.1.18-rc.35"` in `devDependencies`
|
|
10
10
|
|
|
11
11
|
Apply this skill when adding tests to packages in category A (see `testing-overview`). The list includes core abstractions (`context`, `config`, `error`, `entrypoint`, `route`, `resource`, …) and platform-agnostic services (`api`, `state`, `flow`, `i18n`, `client-flow`, `client-socket`, `client-job`, `server-route`, `server-context`, `web-db`, …).
|
|
12
12
|
|
package/build/env.d.ts
CHANGED
|
@@ -1,15 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export interface EnvSkip {
|
|
5
|
-
skip: true;
|
|
6
|
-
reason: string;
|
|
7
|
-
}
|
|
8
|
-
export type EnvGate = EnvOk | EnvSkip;
|
|
9
|
-
export declare const loadEnv: (opts?: {
|
|
10
|
-
force?: boolean;
|
|
11
|
-
file?: string;
|
|
12
|
-
}) => void;
|
|
13
|
-
export declare const hasEnv: (key: string) => boolean;
|
|
14
|
-
export declare const requireEnv: (keys: string[]) => EnvGate;
|
|
1
|
+
import type { EnvHelper } from './types.js';
|
|
2
|
+
export declare const createEnvHelper: () => EnvHelper;
|
|
3
|
+
export declare const envHelper: EnvHelper;
|
|
15
4
|
//# sourceMappingURL=env.d.ts.map
|
package/build/env.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAW,SAAS,EAAkB,MAAM,YAAY,CAAA;AAKpE,eAAO,MAAM,eAAe,QAAO,SA4DlC,CAAA;AAED,eAAO,MAAM,SAAS,WAAoB,CAAA"}
|
package/build/env.js
CHANGED
|
@@ -1,63 +1,68 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { dirname, resolve } from 'node:path';
|
|
3
|
+
// Process-wide on purpose: the file is merged into `process.env`, which every helper instance shares.
|
|
3
4
|
let loaded = false;
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
export const createEnvHelper = () => {
|
|
6
|
+
const findRoot = (start) => {
|
|
7
|
+
let dir = start;
|
|
8
|
+
for (let i = 0; i < 10; i++) {
|
|
9
|
+
if (existsSync(resolve(dir, 'bun.lock')) || existsSync(resolve(dir, 'package.json')) && existsSync(resolve(dir, 'packages'))) {
|
|
10
|
+
return dir;
|
|
11
|
+
}
|
|
12
|
+
const parent = dirname(dir);
|
|
13
|
+
if (parent === dir)
|
|
14
|
+
return null;
|
|
15
|
+
dir = parent;
|
|
9
16
|
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
let val = line.slice(eq + 1).trim();
|
|
28
|
-
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
29
|
-
val = val.slice(1, -1);
|
|
17
|
+
return null;
|
|
18
|
+
};
|
|
19
|
+
const parseEnv = (text) => {
|
|
20
|
+
const out = {};
|
|
21
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
22
|
+
const line = raw.trim();
|
|
23
|
+
if (line === '' || line.startsWith('#'))
|
|
24
|
+
continue;
|
|
25
|
+
const eq = line.indexOf('=');
|
|
26
|
+
if (eq < 1)
|
|
27
|
+
continue;
|
|
28
|
+
const key = line.slice(0, eq).trim();
|
|
29
|
+
let val = line.slice(eq + 1).trim();
|
|
30
|
+
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
31
|
+
val = val.slice(1, -1);
|
|
32
|
+
}
|
|
33
|
+
out[key] = val;
|
|
30
34
|
}
|
|
31
|
-
out
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
process.env[k] = v;
|
|
35
|
+
return out;
|
|
36
|
+
};
|
|
37
|
+
const loadEnv = (opts) => {
|
|
38
|
+
if (loaded && opts?.force !== true)
|
|
39
|
+
return;
|
|
40
|
+
loaded = true;
|
|
41
|
+
const root = findRoot(process.cwd());
|
|
42
|
+
const envPath = opts?.file ?? (root != null ? resolve(root, '.env') : null);
|
|
43
|
+
if (envPath == null || !existsSync(envPath))
|
|
44
|
+
return;
|
|
45
|
+
const text = readFileSync(envPath, 'utf8');
|
|
46
|
+
const parsed = parseEnv(text);
|
|
47
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
48
|
+
if (process.env[k] == null || process.env[k] === '') {
|
|
49
|
+
process.env[k] = v;
|
|
50
|
+
}
|
|
48
51
|
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return {
|
|
61
|
-
|
|
52
|
+
};
|
|
53
|
+
const hasEnv = (key) => {
|
|
54
|
+
loadEnv();
|
|
55
|
+
const v = process.env[key];
|
|
56
|
+
return v != null && v !== '';
|
|
57
|
+
};
|
|
58
|
+
const requireEnv = (keys) => {
|
|
59
|
+
loadEnv();
|
|
60
|
+
const missing = keys.filter(k => !hasEnv(k));
|
|
61
|
+
if (missing.length === 0)
|
|
62
|
+
return { ok: true };
|
|
63
|
+
return { skip: true, reason: `Missing env: ${missing.join(', ')}` };
|
|
64
|
+
};
|
|
65
|
+
return { loadEnv, hasEnv, requireEnv };
|
|
62
66
|
};
|
|
67
|
+
export const envHelper = createEnvHelper();
|
|
63
68
|
//# sourceMappingURL=env.js.map
|
package/build/env.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"env.js","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAClD,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;
|
|
1
|
+
{"version":3,"file":"env.js","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":"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"}
|
package/build/gates.d.ts
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import type { EnvGate } from './
|
|
2
|
-
export type GateSpec = Record<string, string[]>;
|
|
3
|
-
export type Gates<S extends GateSpec> = {
|
|
4
|
-
readonly [K in keyof S]: EnvGate;
|
|
5
|
-
};
|
|
1
|
+
import type { EnvGate, GateSpec, Gates } from './types.js';
|
|
6
2
|
/**
|
|
7
3
|
* Build a frozen record of gates from a spec of `{ name: [envKey, ...] }`.
|
|
8
4
|
* Use in `tests/context.ts` so a single source of truth describes which
|
package/build/gates.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gates.d.ts","sourceRoot":"","sources":["../src/gates.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"gates.d.ts","sourceRoot":"","sources":["../src/gates.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AAE1D;;;;;GAKG;AACH,eAAO,MAAM,SAAS,GAAI,CAAC,SAAS,QAAQ,QAAQ,CAAC,KAAG,KAAK,CAAC,CAAC,CAM9D,CAAA;AAED,eAAO,MAAM,MAAM,SAAU,OAAO,KAAG,IAAI,IAAI;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAClC,CAAA"}
|
package/build/gates.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { envHelper } from './env.js';
|
|
2
2
|
/**
|
|
3
3
|
* Build a frozen record of gates from a spec of `{ name: [envKey, ...] }`.
|
|
4
4
|
* Use in `tests/context.ts` so a single source of truth describes which
|
|
@@ -8,7 +8,7 @@ import { requireEnv } from './env.js';
|
|
|
8
8
|
export const makeGates = (spec) => {
|
|
9
9
|
const out = {};
|
|
10
10
|
for (const [name, keys] of Object.entries(spec)) {
|
|
11
|
-
out[name] = requireEnv(keys);
|
|
11
|
+
out[name] = envHelper.requireEnv(keys);
|
|
12
12
|
}
|
|
13
13
|
return Object.freeze(out);
|
|
14
14
|
};
|
package/build/gates.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gates.js","sourceRoot":"","sources":["../src/gates.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"gates.js","sourceRoot":"","sources":["../src/gates.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AAGpC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,CAAqB,IAAO,EAAY,EAAE;IACjE,MAAM,GAAG,GAAG,EAA6B,CAAA;IACzC,KAAK,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QAChD,GAAG,CAAC,IAAI,CAAC,GAAG,SAAS,CAAC,UAAU,CAAC,IAAI,CAAC,CAAA;IACxC,CAAC;IACD,OAAO,MAAM,CAAC,MAAM,CAAC,GAAG,CAAa,CAAA;AACvC,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,IAAa,EAA0C,EAAE,CAC7E,IAAwB,CAAC,IAAI,KAAK,IAAI,CAAA"}
|
package/build/index.d.ts
CHANGED
package/build/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAA;AACxB,cAAc,YAAY,CAAA;AAC1B,cAAc,eAAe,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAA;AACxB,cAAc,YAAY,CAAA;AAC1B,cAAc,eAAe,CAAA;AAC7B,mBAAmB,YAAY,CAAA"}
|
package/build/types.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface EnvOk {
|
|
2
|
+
ok: true;
|
|
3
|
+
}
|
|
4
|
+
export interface EnvSkip {
|
|
5
|
+
skip: true;
|
|
6
|
+
reason: string;
|
|
7
|
+
}
|
|
8
|
+
export type EnvGate = EnvOk | EnvSkip;
|
|
9
|
+
export type GateSpec = Record<string, string[]>;
|
|
10
|
+
export type Gates<S extends GateSpec> = {
|
|
11
|
+
readonly [K in keyof S]: EnvGate;
|
|
12
|
+
};
|
|
13
|
+
export interface LoadEnvOptions {
|
|
14
|
+
/** Re-read the file even when it was already loaded once in this process. */
|
|
15
|
+
force?: boolean;
|
|
16
|
+
/** The file to read instead of `<monorepo-root>/.env`. */
|
|
17
|
+
file?: string;
|
|
18
|
+
}
|
|
19
|
+
/** The test environment: `<monorepo-root>/.env` merged into `process.env`, and gates over it. */
|
|
20
|
+
export interface EnvHelper {
|
|
21
|
+
/**
|
|
22
|
+
* Locate `<monorepo-root>/.env`, parse it and merge it into `process.env` — a variable that is
|
|
23
|
+
* already set and non-empty wins. Idempotent per process unless `force` is passed.
|
|
24
|
+
*/
|
|
25
|
+
loadEnv: (opts?: LoadEnvOptions) => void;
|
|
26
|
+
/** Whether a variable is set to a non-empty value (after {@link EnvHelper.loadEnv}). */
|
|
27
|
+
hasEnv: (key: string) => boolean;
|
|
28
|
+
/** `{ ok: true }` when every key is set, otherwise `{ skip: true, reason }` naming the missing ones. */
|
|
29
|
+
requireEnv: (keys: string[]) => EnvGate;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,IAAI,CAAA;CACT;AAED,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,IAAI,CAAA;IACV,MAAM,EAAE,MAAM,CAAA;CACf;AAGD,MAAM,MAAM,OAAO,GAAG,KAAK,GAAG,OAAO,CAAA;AAGrC,MAAM,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAA;AAG/C,MAAM,MAAM,KAAK,CAAC,CAAC,SAAS,QAAQ,IAAI;IAAE,QAAQ,EAAE,CAAC,IAAI,MAAM,CAAC,GAAG,OAAO;CAAE,CAAA;AAE5E,MAAM,WAAW,cAAc;IAC7B,6EAA6E;IAC7E,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,0DAA0D;IAC1D,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,iGAAiG;AACjG,MAAM,WAAW,SAAS;IACxB;;;OAGG;IACH,OAAO,EAAE,CAAC,IAAI,CAAC,EAAE,cAAc,KAAK,IAAI,CAAA;IACxC,wFAAwF;IACxF,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAA;IAChC,wGAAwG;IACxG,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAA;CACxC"}
|
package/build/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owlmeans/test",
|
|
3
|
-
"version": "0.1.18-rc.
|
|
3
|
+
"version": "0.1.18-rc.35",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
}
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@owlmeans/context": "^0.1.18-rc.
|
|
24
|
+
"@owlmeans/context": "^0.1.18-rc.34"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@owlmeans/dep-config": "^0.1.18-rc.6",
|
package/src/env.ts
CHANGED
|
@@ -1,73 +1,70 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs'
|
|
2
2
|
import { dirname, resolve } from 'node:path'
|
|
3
|
+
import type { EnvGate, EnvHelper, LoadEnvOptions } from './types.js'
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
ok: true
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export interface EnvSkip {
|
|
9
|
-
skip: true
|
|
10
|
-
reason: string
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export type EnvGate = EnvOk | EnvSkip
|
|
14
|
-
|
|
5
|
+
// Process-wide on purpose: the file is merged into `process.env`, which every helper instance shares.
|
|
15
6
|
let loaded = false
|
|
16
7
|
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
8
|
+
export const createEnvHelper = (): EnvHelper => {
|
|
9
|
+
const findRoot = (start: string): string | null => {
|
|
10
|
+
let dir = start
|
|
11
|
+
for (let i = 0; i < 10; i++) {
|
|
12
|
+
if (existsSync(resolve(dir, 'bun.lock')) || existsSync(resolve(dir, 'package.json')) && existsSync(resolve(dir, 'packages'))) {
|
|
13
|
+
return dir
|
|
14
|
+
}
|
|
15
|
+
const parent = dirname(dir)
|
|
16
|
+
if (parent === dir) return null
|
|
17
|
+
dir = parent
|
|
22
18
|
}
|
|
23
|
-
|
|
24
|
-
if (parent === dir) return null
|
|
25
|
-
dir = parent
|
|
19
|
+
return null
|
|
26
20
|
}
|
|
27
|
-
return null
|
|
28
|
-
}
|
|
29
21
|
|
|
30
|
-
const parseEnv = (text: string): Record<string, string> => {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
22
|
+
const parseEnv = (text: string): Record<string, string> => {
|
|
23
|
+
const out: Record<string, string> = {}
|
|
24
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
25
|
+
const line = raw.trim()
|
|
26
|
+
if (line === '' || line.startsWith('#')) continue
|
|
27
|
+
const eq = line.indexOf('=')
|
|
28
|
+
if (eq < 1) continue
|
|
29
|
+
const key = line.slice(0, eq).trim()
|
|
30
|
+
let val = line.slice(eq + 1).trim()
|
|
31
|
+
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
32
|
+
val = val.slice(1, -1)
|
|
33
|
+
}
|
|
34
|
+
out[key] = val
|
|
41
35
|
}
|
|
42
|
-
out
|
|
36
|
+
return out
|
|
43
37
|
}
|
|
44
|
-
return out
|
|
45
|
-
}
|
|
46
38
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
39
|
+
const loadEnv = (opts?: LoadEnvOptions): void => {
|
|
40
|
+
if (loaded && opts?.force !== true) return
|
|
41
|
+
loaded = true
|
|
42
|
+
const root = findRoot(process.cwd())
|
|
43
|
+
const envPath = opts?.file ?? (root != null ? resolve(root, '.env') : null)
|
|
44
|
+
if (envPath == null || !existsSync(envPath)) return
|
|
45
|
+
const text = readFileSync(envPath, 'utf8')
|
|
46
|
+
const parsed = parseEnv(text)
|
|
47
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
48
|
+
if (process.env[k] == null || process.env[k] === '') {
|
|
49
|
+
process.env[k] = v
|
|
50
|
+
}
|
|
58
51
|
}
|
|
59
52
|
}
|
|
60
|
-
}
|
|
61
53
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
54
|
+
const hasEnv = (key: string): boolean => {
|
|
55
|
+
loadEnv()
|
|
56
|
+
const v = process.env[key]
|
|
57
|
+
return v != null && v !== ''
|
|
58
|
+
}
|
|
67
59
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
60
|
+
const requireEnv = (keys: string[]): EnvGate => {
|
|
61
|
+
loadEnv()
|
|
62
|
+
const missing = keys.filter(k => !hasEnv(k))
|
|
63
|
+
if (missing.length === 0) return { ok: true }
|
|
64
|
+
return { skip: true, reason: `Missing env: ${missing.join(', ')}` }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return { loadEnv, hasEnv, requireEnv }
|
|
73
68
|
}
|
|
69
|
+
|
|
70
|
+
export const envHelper = createEnvHelper()
|
package/src/gates.ts
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { EnvGate } from './
|
|
3
|
-
|
|
4
|
-
export type GateSpec = Record<string, string[]>
|
|
5
|
-
|
|
6
|
-
export type Gates<S extends GateSpec> = { readonly [K in keyof S]: EnvGate }
|
|
1
|
+
import { envHelper } from './env.js'
|
|
2
|
+
import type { EnvGate, GateSpec, Gates } from './types.js'
|
|
7
3
|
|
|
8
4
|
/**
|
|
9
5
|
* Build a frozen record of gates from a spec of `{ name: [envKey, ...] }`.
|
|
@@ -14,7 +10,7 @@ export type Gates<S extends GateSpec> = { readonly [K in keyof S]: EnvGate }
|
|
|
14
10
|
export const makeGates = <S extends GateSpec>(spec: S): Gates<S> => {
|
|
15
11
|
const out = {} as Record<string, EnvGate>
|
|
16
12
|
for (const [name, keys] of Object.entries(spec)) {
|
|
17
|
-
out[name] = requireEnv(keys)
|
|
13
|
+
out[name] = envHelper.requireEnv(keys)
|
|
18
14
|
}
|
|
19
15
|
return Object.freeze(out) as Gates<S>
|
|
20
16
|
}
|
package/src/index.ts
CHANGED
package/src/types.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export interface EnvOk {
|
|
2
|
+
ok: true
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export interface EnvSkip {
|
|
6
|
+
skip: true
|
|
7
|
+
reason: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Kept as a type: a union of the two gate outcomes.
|
|
11
|
+
export type EnvGate = EnvOk | EnvSkip
|
|
12
|
+
|
|
13
|
+
// Kept as a type: a record over arbitrary gate names (an implicit index signature).
|
|
14
|
+
export type GateSpec = Record<string, string[]>
|
|
15
|
+
|
|
16
|
+
// Kept as a type: mapped over the spec's gate names.
|
|
17
|
+
export type Gates<S extends GateSpec> = { readonly [K in keyof S]: EnvGate }
|
|
18
|
+
|
|
19
|
+
export interface LoadEnvOptions {
|
|
20
|
+
/** Re-read the file even when it was already loaded once in this process. */
|
|
21
|
+
force?: boolean
|
|
22
|
+
/** The file to read instead of `<monorepo-root>/.env`. */
|
|
23
|
+
file?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The test environment: `<monorepo-root>/.env` merged into `process.env`, and gates over it. */
|
|
27
|
+
export interface EnvHelper {
|
|
28
|
+
/**
|
|
29
|
+
* Locate `<monorepo-root>/.env`, parse it and merge it into `process.env` — a variable that is
|
|
30
|
+
* already set and non-empty wins. Idempotent per process unless `force` is passed.
|
|
31
|
+
*/
|
|
32
|
+
loadEnv: (opts?: LoadEnvOptions) => void
|
|
33
|
+
/** Whether a variable is set to a non-empty value (after {@link EnvHelper.loadEnv}). */
|
|
34
|
+
hasEnv: (key: string) => boolean
|
|
35
|
+
/** `{ ok: true }` when every key is set, otherwise `{ skip: true, reason }` naming the missing ones. */
|
|
36
|
+
requireEnv: (keys: string[]) => EnvGate
|
|
37
|
+
}
|