@owlmeans/test 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,11 @@
1
+ # @owlmeans/test
2
+
3
+ Foundation helpers shared by the OwlMeans test packages and individual package test suites.
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`.
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
+ - `loadFixture<T>(relPath)` — JSON fixture loader rooted at the consuming package's `tests/fixtures/`.
10
+
11
+ Tests must be located at `packages/<pkg>/tests/`, named `*.spec.ts`. See the `testing-overview` skill for the full strategy.
package/build/env.d.ts ADDED
@@ -0,0 +1,15 @@
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 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;
15
+ //# sourceMappingURL=env.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,IAAI,CAAA;CACT;AAED,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,IAAI,CAAA;IACV,MAAM,EAAE,MAAM,CAAA;CACf;AAED,MAAM,MAAM,OAAO,GAAG,KAAK,GAAG,OAAO,CAAA;AAkCrC,eAAO,MAAM,OAAO,GAAI,OAAO;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,KAAG,IAanE,CAAA;AAED,eAAO,MAAM,MAAM,GAAI,KAAK,MAAM,KAAG,OAIpC,CAAA;AAED,eAAO,MAAM,UAAU,GAAI,MAAM,MAAM,EAAE,KAAG,OAK3C,CAAA"}
package/build/env.js ADDED
@@ -0,0 +1,63 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { dirname, resolve } from 'node:path';
3
+ let loaded = false;
4
+ const findRoot = (start) => {
5
+ let dir = start;
6
+ for (let i = 0; i < 10; i++) {
7
+ if (existsSync(resolve(dir, 'bun.lock')) || existsSync(resolve(dir, 'package.json')) && existsSync(resolve(dir, 'packages'))) {
8
+ return dir;
9
+ }
10
+ const parent = dirname(dir);
11
+ if (parent === dir)
12
+ return null;
13
+ dir = parent;
14
+ }
15
+ return null;
16
+ };
17
+ const parseEnv = (text) => {
18
+ const out = {};
19
+ for (const raw of text.split(/\r?\n/)) {
20
+ const line = raw.trim();
21
+ if (line === '' || line.startsWith('#'))
22
+ continue;
23
+ const eq = line.indexOf('=');
24
+ if (eq < 1)
25
+ continue;
26
+ const key = line.slice(0, eq).trim();
27
+ let val = line.slice(eq + 1).trim();
28
+ if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
29
+ val = val.slice(1, -1);
30
+ }
31
+ out[key] = val;
32
+ }
33
+ return out;
34
+ };
35
+ export const loadEnv = (opts) => {
36
+ if (loaded && opts?.force !== true)
37
+ return;
38
+ loaded = true;
39
+ const root = findRoot(process.cwd());
40
+ const envPath = opts?.file ?? (root != null ? resolve(root, '.env') : null);
41
+ if (envPath == null || !existsSync(envPath))
42
+ return;
43
+ const text = readFileSync(envPath, 'utf8');
44
+ const parsed = parseEnv(text);
45
+ for (const [k, v] of Object.entries(parsed)) {
46
+ if (process.env[k] == null || process.env[k] === '') {
47
+ process.env[k] = v;
48
+ }
49
+ }
50
+ };
51
+ export const hasEnv = (key) => {
52
+ loadEnv();
53
+ const v = process.env[key];
54
+ return v != null && v !== '';
55
+ };
56
+ export const requireEnv = (keys) => {
57
+ loadEnv();
58
+ const missing = keys.filter(k => !hasEnv(k));
59
+ if (missing.length === 0)
60
+ return { ok: true };
61
+ return { skip: true, reason: `Missing env: ${missing.join(', ')}` };
62
+ };
63
+ //# sourceMappingURL=env.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env.js","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Load a JSON fixture relative to the consuming package's `tests/fixtures/`
3
+ * directory. The caller passes a path relative to its own `tests/`; the
4
+ * loader resolves it against `process.cwd()`, which `bun test` sets to the
5
+ * package root.
6
+ */
7
+ export declare const loadFixture: <T = unknown>(relPath: string) => T;
8
+ //# sourceMappingURL=fixtures.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fixtures.d.ts","sourceRoot":"","sources":["../src/fixtures.ts"],"names":[],"mappings":"AAGA;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GAAI,CAAC,GAAG,OAAO,EAAE,SAAS,MAAM,KAAG,CAI1D,CAAA"}
@@ -0,0 +1,14 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ /**
4
+ * Load a JSON fixture relative to the consuming package's `tests/fixtures/`
5
+ * directory. The caller passes a path relative to its own `tests/`; the
6
+ * loader resolves it against `process.cwd()`, which `bun test` sets to the
7
+ * package root.
8
+ */
9
+ export const loadFixture = (relPath) => {
10
+ const path = resolve(process.cwd(), 'tests', relPath);
11
+ const text = readFileSync(path, 'utf8');
12
+ return JSON.parse(text);
13
+ };
14
+ //# sourceMappingURL=fixtures.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fixtures.js","sourceRoot":"","sources":["../src/fixtures.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AACtC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAEnC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CAAc,OAAe,EAAK,EAAE;IAC7D,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,OAAO,CAAC,CAAA;IACrD,MAAM,IAAI,GAAG,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;IACvC,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAM,CAAA;AAC9B,CAAC,CAAA"}
@@ -0,0 +1,17 @@
1
+ import type { EnvGate } from './env.js';
2
+ export type GateSpec = Record<string, string[]>;
3
+ export type Gates<S extends GateSpec> = {
4
+ readonly [K in keyof S]: EnvGate;
5
+ };
6
+ /**
7
+ * Build a frozen record of gates from a spec of `{ name: [envKey, ...] }`.
8
+ * Use in `tests/context.ts` so a single source of truth describes which
9
+ * external dependencies are available, and individual specs read
10
+ * `gates.<name>.skip` to self-skip when their dependency is missing.
11
+ */
12
+ export declare const makeGates: <S extends GateSpec>(spec: S) => Gates<S>;
13
+ export declare const isSkip: (gate: EnvGate) => gate is {
14
+ skip: true;
15
+ reason: string;
16
+ };
17
+ //# sourceMappingURL=gates.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gates.d.ts","sourceRoot":"","sources":["../src/gates.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAA;AAEvC,MAAM,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAA;AAE/C,MAAM,MAAM,KAAK,CAAC,CAAC,SAAS,QAAQ,IAAI;IAAE,QAAQ,EAAE,CAAC,IAAI,MAAM,CAAC,GAAG,OAAO;CAAE,CAAA;AAE5E;;;;;GAKG;AACH,eAAO,MAAM,SAAS,GAAI,CAAC,SAAS,QAAQ,EAAE,MAAM,CAAC,KAAG,KAAK,CAAC,CAAC,CAM9D,CAAA;AAED,eAAO,MAAM,MAAM,GAAI,MAAM,OAAO,KAAG,IAAI,IAAI;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAClC,CAAA"}
package/build/gates.js ADDED
@@ -0,0 +1,16 @@
1
+ import { requireEnv } from './env.js';
2
+ /**
3
+ * Build a frozen record of gates from a spec of `{ name: [envKey, ...] }`.
4
+ * Use in `tests/context.ts` so a single source of truth describes which
5
+ * external dependencies are available, and individual specs read
6
+ * `gates.<name>.skip` to self-skip when their dependency is missing.
7
+ */
8
+ export const makeGates = (spec) => {
9
+ const out = {};
10
+ for (const [name, keys] of Object.entries(spec)) {
11
+ out[name] = requireEnv(keys);
12
+ }
13
+ return Object.freeze(out);
14
+ };
15
+ export const isSkip = (gate) => gate.skip === true;
16
+ //# sourceMappingURL=gates.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gates.js","sourceRoot":"","sources":["../src/gates.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAOrC;;;;;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,UAAU,CAAC,IAAI,CAAC,CAAA;IAC9B,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"}
@@ -0,0 +1,4 @@
1
+ export * from './env.js';
2
+ export * from './gates.js';
3
+ export * from './fixtures.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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"}
package/build/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export * from './env.js';
2
+ export * from './gates.js';
3
+ export * from './fixtures.js';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAA;AACxB,cAAc,YAAY,CAAA;AAC1B,cAAc,eAAe,CAAA"}
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@owlmeans/test",
3
+ "version": "0.1.3",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "scripts": {
7
+ "build": "tsc -b",
8
+ "dev": "nodemon -e ts,tsx,json --watch src --exec \"tsc -p ./tsconfig.json\"",
9
+ "watch": "tsc -b -w --preserveWatchOutput --pretty"
10
+ },
11
+ "main": "build/index.js",
12
+ "module": "build/index.js",
13
+ "types": "build/index.d.ts",
14
+ "exports": {
15
+ ".": {
16
+ "import": "./build/index.js",
17
+ "require": "./build/index.js",
18
+ "default": "./build/index.js",
19
+ "module": "./build/index.js",
20
+ "types": "./build/index.d.ts"
21
+ }
22
+ },
23
+ "dependencies": {
24
+ "@owlmeans/context": "^0.1.3"
25
+ },
26
+ "devDependencies": {
27
+ "@owlmeans/dep-config": "^0.1.3",
28
+ "@types/bun": "^1.3.0",
29
+ "nodemon": "^3.1.11",
30
+ "typescript": "^6.0.2"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public"
34
+ }
35
+ }
package/src/env.ts ADDED
@@ -0,0 +1,73 @@
1
+ import { existsSync, readFileSync } from 'node:fs'
2
+ import { dirname, resolve } from 'node:path'
3
+
4
+ export interface EnvOk {
5
+ ok: true
6
+ }
7
+
8
+ export interface EnvSkip {
9
+ skip: true
10
+ reason: string
11
+ }
12
+
13
+ export type EnvGate = EnvOk | EnvSkip
14
+
15
+ let loaded = false
16
+
17
+ const findRoot = (start: string): string | null => {
18
+ let dir = start
19
+ for (let i = 0; i < 10; i++) {
20
+ if (existsSync(resolve(dir, 'bun.lock')) || existsSync(resolve(dir, 'package.json')) && existsSync(resolve(dir, 'packages'))) {
21
+ return dir
22
+ }
23
+ const parent = dirname(dir)
24
+ if (parent === dir) return null
25
+ dir = parent
26
+ }
27
+ return null
28
+ }
29
+
30
+ const parseEnv = (text: string): Record<string, string> => {
31
+ const out: Record<string, string> = {}
32
+ for (const raw of text.split(/\r?\n/)) {
33
+ const line = raw.trim()
34
+ if (line === '' || line.startsWith('#')) continue
35
+ const eq = line.indexOf('=')
36
+ if (eq < 1) continue
37
+ const key = line.slice(0, eq).trim()
38
+ let val = line.slice(eq + 1).trim()
39
+ if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
40
+ val = val.slice(1, -1)
41
+ }
42
+ out[key] = val
43
+ }
44
+ return out
45
+ }
46
+
47
+ export const loadEnv = (opts?: { force?: boolean, file?: string }): void => {
48
+ if (loaded && opts?.force !== true) return
49
+ loaded = true
50
+ const root = findRoot(process.cwd())
51
+ const envPath = opts?.file ?? (root != null ? resolve(root, '.env') : null)
52
+ if (envPath == null || !existsSync(envPath)) return
53
+ const text = readFileSync(envPath, 'utf8')
54
+ const parsed = parseEnv(text)
55
+ for (const [k, v] of Object.entries(parsed)) {
56
+ if (process.env[k] == null || process.env[k] === '') {
57
+ process.env[k] = v
58
+ }
59
+ }
60
+ }
61
+
62
+ export const hasEnv = (key: string): boolean => {
63
+ loadEnv()
64
+ const v = process.env[key]
65
+ return v != null && v !== ''
66
+ }
67
+
68
+ export const requireEnv = (keys: string[]): EnvGate => {
69
+ loadEnv()
70
+ const missing = keys.filter(k => !hasEnv(k))
71
+ if (missing.length === 0) return { ok: true }
72
+ return { skip: true, reason: `Missing env: ${missing.join(', ')}` }
73
+ }
@@ -0,0 +1,14 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import { resolve } from 'node:path'
3
+
4
+ /**
5
+ * Load a JSON fixture relative to the consuming package's `tests/fixtures/`
6
+ * directory. The caller passes a path relative to its own `tests/`; the
7
+ * loader resolves it against `process.cwd()`, which `bun test` sets to the
8
+ * package root.
9
+ */
10
+ export const loadFixture = <T = unknown>(relPath: string): T => {
11
+ const path = resolve(process.cwd(), 'tests', relPath)
12
+ const text = readFileSync(path, 'utf8')
13
+ return JSON.parse(text) as T
14
+ }
package/src/gates.ts ADDED
@@ -0,0 +1,23 @@
1
+ import { requireEnv } from './env.js'
2
+ import type { EnvGate } from './env.js'
3
+
4
+ export type GateSpec = Record<string, string[]>
5
+
6
+ export type Gates<S extends GateSpec> = { readonly [K in keyof S]: EnvGate }
7
+
8
+ /**
9
+ * Build a frozen record of gates from a spec of `{ name: [envKey, ...] }`.
10
+ * Use in `tests/context.ts` so a single source of truth describes which
11
+ * external dependencies are available, and individual specs read
12
+ * `gates.<name>.skip` to self-skip when their dependency is missing.
13
+ */
14
+ export const makeGates = <S extends GateSpec>(spec: S): Gates<S> => {
15
+ const out = {} as Record<string, EnvGate>
16
+ for (const [name, keys] of Object.entries(spec)) {
17
+ out[name] = requireEnv(keys)
18
+ }
19
+ return Object.freeze(out) as Gates<S>
20
+ }
21
+
22
+ export const isSkip = (gate: EnvGate): gate is { skip: true, reason: string } =>
23
+ (gate as { skip?: true }).skip === true
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from './env.js'
2
+ export * from './gates.js'
3
+ export * from './fixtures.js'
package/tsconfig.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": [
3
+ "@owlmeans/dep-config/tsconfig.base.json",
4
+ "@owlmeans/dep-config/tsconfig.bun.json"
5
+ ],
6
+ "compilerOptions": {
7
+ "rootDir": "./src/",
8
+ "outDir": "./build/"
9
+ },
10
+ "exclude": ["./dist/**/*", "./build/**/*", "./*.ts"]
11
+ }