proby 0.12.0 → 0.13.1

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 CHANGED
@@ -5,8 +5,10 @@ Test runner for JavaScript runtimes.
5
5
  ## What is proby?
6
6
 
7
7
  A cross-runtime test runner that automatically discovers and runs test files.
8
- Works with both single repositories and monorepos. Uses `@rcompat/test` for
9
- writing tests. Works consistently across Node, Deno, and Bun.
8
+ Works with both single repositories and monorepos. Supports TypeScript source
9
+ execution in monorepos without a build step when your packages expose `source`
10
+ entries in `package.json`. Uses `@rcompat/test` for writing tests. Works
11
+ consistently across Node, Deno, and Bun.
10
12
 
11
13
  ## Installation
12
14
 
@@ -42,70 +44,266 @@ Run proby from your project root:
42
44
  npx proby
43
45
  ```
44
46
 
45
- ### Writing tests
47
+ Run a single file:
46
48
 
47
- Create test files with `.spec.ts` or `.spec.js` extension in your `src`
48
- directory:
49
+ ```bash
50
+ npx proby math.spec.ts
51
+ ```
49
52
 
50
- ```js
51
- // src/math.spec.ts
52
- import test from "@rcompat/test";
53
+ Run a single group within a file:
53
54
 
54
- test.case("addition", assert => {
55
- assert(1 + 1).equals(2);
56
- assert(2 + 2).equals(4);
57
- });
55
+ ```bash
56
+ npx proby math.spec.ts addition
57
+ ```
58
58
 
59
- test.case("multiplication", assert => {
60
- assert(2 * 3).equals(6);
61
- assert(4 * 5).equals(20);
59
+ ## How proby resolves source files
60
+
61
+ Proby relaunches itself with runtime conditions before running tests. By
62
+ default it uses the `source` condition, which lets runtimes resolve package
63
+ `imports` and `exports` to your TypeScript source files instead of built
64
+ JavaScript.
65
+
66
+ This is especially useful in monorepos where packages depend on each other and
67
+ you want to run tests against source directly.
68
+
69
+ To use this pattern, expose `source` entries in your packages.
70
+
71
+ Example:
72
+
73
+ ```json
74
+ {
75
+ "imports": {
76
+ "#*": {
77
+ "source": "./src/*.ts",
78
+ "default": "./lib/*.js"
79
+ }
80
+ },
81
+ "exports": {
82
+ ".": {
83
+ "source": "./src/index.ts",
84
+ "default": "./lib/index.js"
85
+ }
86
+ }
87
+ }
88
+ ```
89
+
90
+ With this setup, proby can run against your TypeScript source without requiring
91
+ an upfront build step.
92
+
93
+ ## Configuration
94
+
95
+ Create `proby.config.ts` or `proby.config.js` in your project root.
96
+
97
+ ```ts
98
+ import config from "proby/config";
99
+
100
+ export default config({
101
+ monorepo: true,
102
+ packages: "packages",
103
+ include: ["src"],
104
+ conditions: ["source"],
62
105
  });
63
106
  ```
64
107
 
65
- ### Test output
108
+ ### Config options
66
109
 
67
- Proby displays colored output:
68
- - Green `o` for passing tests
69
- - Red `x` for failing tests
110
+ #### `monorepo`
70
111
 
112
+ ```ts
113
+ boolean
71
114
  ```
72
- oooooxoo
73
- src/math.spec.ts division
74
- expected 5
75
- actual 4
115
+
116
+ Whether proby should scan package directories inside a monorepo.
117
+
118
+ Default:
119
+
120
+ ```ts
121
+ false
122
+ ```
123
+
124
+ #### `packages`
125
+
126
+ ```ts
127
+ string
128
+ ```
129
+
130
+ Directory containing package folders when `monorepo` is enabled.
131
+
132
+ Default:
133
+
134
+ ```ts
135
+ "packages"
136
+ ```
137
+
138
+ #### `include`
139
+
140
+ ```ts
141
+ string[]
142
+ ```
143
+
144
+ Directories to scan for spec files.
145
+
146
+ Default:
147
+
148
+ ```ts
149
+ ["src"]
76
150
  ```
77
151
 
78
- ### Project structure
152
+ #### `conditions`
79
153
 
80
- Proby automatically detects your project structure:
154
+ ```ts
155
+ string[]
156
+ ```
157
+
158
+ Runtime conditions used when proby relaunches itself.
159
+
160
+ Default:
81
161
 
82
- **Single repository:**
162
+ ```ts
163
+ ["source"]
83
164
  ```
165
+
166
+ ## Project structure
167
+
168
+ Proby automatically detects your project structure.
169
+
170
+ ### Single repository
171
+
172
+ ```text
84
173
  my-project/
85
174
  ├── src/
86
175
  │ ├── utils.ts
87
- │ ├── utils.spec.ts # ← Test file
176
+ │ ├── utils.spec.ts
88
177
  │ ├── math.ts
89
- │ └── math.spec.ts # ← Test file
178
+ │ └── math.spec.ts
90
179
  └── package.json
91
180
  ```
92
181
 
93
- **Monorepo:**
94
- ```
182
+ ### Monorepo
183
+
184
+ ```text
95
185
  my-monorepo/
96
186
  ├── packages/
97
187
  │ ├── core/
98
188
  │ │ └── src/
99
189
  │ │ ├── index.ts
100
- │ │ └── index.spec.ts # ← Test file
190
+ │ │ └── index.spec.ts
101
191
  │ └── utils/
102
192
  │ └── src/
103
193
  │ ├── helpers.ts
104
- │ └── helpers.spec.ts # ← Test file
194
+ │ └── helpers.spec.ts
105
195
  └── package.json
106
196
  ```
107
197
 
108
- ### npm scripts
198
+ ## Test file conventions
199
+
200
+ - Files must end with `.spec.ts` or `.spec.js`
201
+ - Files must be in one of the configured include directories
202
+ - Use `@rcompat/test` to write tests
203
+ - Use `test.group` to organize cases into named groups targetable by proby
204
+
205
+ ## Static mock files
206
+
207
+ Proby supports preloading sibling mock files before a spec file is evaluated.
208
+ This allows module mocks to be registered before the spec's static imports are
209
+ read.
210
+
211
+ Pair files by matching the spec extension exactly:
212
+
213
+ - `math.spec.ts` pairs with `math.mock.ts`
214
+ - `math.spec.js` pairs with `math.mock.js`
215
+
216
+ If a sibling mock file exists, proby loads it before the spec file.
217
+
218
+ Example:
219
+
220
+ ```ts
221
+ // math.ts
222
+ export function add(a: number, b: number) {
223
+ return a + b;
224
+ }
225
+ ```
226
+
227
+ ```ts
228
+ // math.mock.ts
229
+ import test from "@rcompat/test";
230
+
231
+ test.mock("./math.ts", () => ({
232
+ add: (a: number, b: number) => 99,
233
+ }));
234
+ ```
235
+
236
+ ```ts
237
+ // math.spec.ts
238
+ import test from "@rcompat/test";
239
+ import { add } from "./math.ts";
240
+
241
+ test.case("uses the preloaded mock", assert => {
242
+ assert(add(1, 2)).equals(99);
243
+ });
244
+ ```
245
+
246
+ Static mocks are file-scoped. A mock loaded for one spec file does not leak
247
+ into later spec files.
248
+
249
+ ## Grouping tests
250
+
251
+ Use `test.group` in your spec files to cluster related cases. Groups can then
252
+ be targeted individually when running proby.
253
+
254
+ ```js
255
+ import test from "@rcompat/test";
256
+
257
+ test.group("addition", () => {
258
+ test.case("integers", assert => {
259
+ assert(1 + 1).equals(2);
260
+ });
261
+ });
262
+
263
+ test.group("multiplication", () => {
264
+ test.case("integers", assert => {
265
+ assert(2 * 3).equals(6);
266
+ });
267
+ });
268
+ ```
269
+
270
+ ```bash
271
+ npx proby math.spec.ts addition
272
+ ```
273
+
274
+ ## Writing tests
275
+
276
+ Create test files with `.spec.ts` or `.spec.js`:
277
+
278
+ ```js
279
+ import test from "@rcompat/test";
280
+
281
+ test.case("addition", assert => {
282
+ assert(1 + 1).equals(2);
283
+ assert(2 + 2).equals(4);
284
+ });
285
+
286
+ test.case("multiplication", assert => {
287
+ assert(2 * 3).equals(6);
288
+ assert(4 * 5).equals(20);
289
+ });
290
+ ```
291
+
292
+ ## Test output
293
+
294
+ Proby displays colored output:
295
+
296
+ - Green `o` for passing tests
297
+ - Red `x` for failing tests
298
+
299
+ ```text
300
+ oooooxoo
301
+ src/math.spec.ts division
302
+ expected 5
303
+ actual 4
304
+ ```
305
+
306
+ ## npm scripts
109
307
 
110
308
  Add proby to your `package.json`:
111
309
 
@@ -123,12 +321,6 @@ Then run:
123
321
  npm test
124
322
  ```
125
323
 
126
- ## Test file conventions
127
-
128
- - Files must end with `.spec.ts` or `.spec.js`
129
- - Files must be in the `src` directory (or `packages/*/src` for monorepos)
130
- - Use `@rcompat/test` to write tests
131
-
132
324
  ## Examples
133
325
 
134
326
  ### Basic assertions
@@ -137,17 +329,10 @@ npm test
137
329
  import test from "@rcompat/test";
138
330
 
139
331
  test.case("basic assertions", assert => {
140
- // equality
141
332
  assert(value).equals(expected);
142
-
143
- // truthiness
144
333
  assert(condition).true();
145
334
  assert(condition).false();
146
-
147
- // type checking
148
335
  assert(value).type<string>();
149
-
150
- // throws
151
336
  assert(() => throwingFunction()).throws();
152
337
  assert(() => safeFunction()).tries();
153
338
  });
@@ -168,7 +353,7 @@ test.case("async operations", async assert => {
168
353
 
169
354
  ```js
170
355
  // src/calculator.ts
171
- export function add(a: number, b: number) {
356
+ export function add(a, b) {
172
357
  return a + b;
173
358
  }
174
359
 
@@ -186,13 +371,11 @@ test.case("add function", assert => {
186
371
  ## Cross-Runtime Compatibility
187
372
 
188
373
  | Runtime | Supported |
189
- |---------|-----------|
374
+ | ------- | --------- |
190
375
  | Node.js | ✓ |
191
376
  | Deno | ✓ |
192
377
  | Bun | ✓ |
193
378
 
194
- No configuration required — just run `npx proby`.
195
-
196
379
  ## License
197
380
 
198
381
  MIT
@@ -0,0 +1,8 @@
1
+ declare const Schema: import("pema").ObjectType<{
2
+ monorepo: import("pema").DefaultType<import("pema").BooleanType, false>;
3
+ packages: import("pema").DefaultType<import("pema").StringType, "packages">;
4
+ include: import("pema").DefaultType<import("pema").ArrayType<import("pema").StringType>, string[]>;
5
+ conditions: import("pema").DefaultType<import("pema").ArrayType<import("pema").StringType>, string[]>;
6
+ }>;
7
+ export default Schema;
8
+ //# sourceMappingURL=Schema.d.ts.map
package/lib/Schema.js ADDED
@@ -0,0 +1,9 @@
1
+ import p from "pema";
2
+ const Schema = p({
3
+ monorepo: p.boolean.default(false),
4
+ packages: p.string.default("packages"),
5
+ include: p.array(p.string).default(["src"]),
6
+ conditions: p.array(p.string).default(["source"]),
7
+ });
8
+ export default Schema;
9
+ //# sourceMappingURL=Schema.js.map
package/lib/bin.js CHANGED
@@ -1,37 +1,51 @@
1
1
  #!/usr/bin/env node
2
- import args from "@rcompat/args";
3
- import color from "@rcompat/cli/color";
4
- import print from "@rcompat/cli/print";
2
+ import p from "pema";
3
+ import env from "@rcompat/env";
5
4
  import fs from "@rcompat/fs";
6
- import run from "./run.js";
5
+ import io from "@rcompat/io";
6
+ import is from "@rcompat/is";
7
+ import runtime from "@rcompat/runtime";
8
+ const Schema = p({
9
+ monorepo: p.boolean.default(false),
10
+ packages: p.string.default("packages"),
11
+ include: p.array(p.string).default(["src"]),
12
+ conditions: p.array(p.string).default(["source"]),
13
+ });
7
14
  const root = await fs.project.root();
8
- const spec_json = root.join("spec.json");
9
- if (await spec_json.exists()) {
10
- // console.log(`spec.json exists, reading`);
15
+ const ts_config_file = root.join("proby.config.ts");
16
+ const js_config_file = root.join("proby.config.js");
17
+ const user_config = await ts_config_file.exists()
18
+ ? (await ts_config_file.import("default"))
19
+ : await js_config_file.exists()
20
+ ? (await js_config_file.import("default"))
21
+ : {};
22
+ const { include, packages, conditions, monorepo } = Schema.parse(user_config);
23
+ const conditions_flag = conditions.length > 0
24
+ ? ` --conditions=${conditions.join(",")}`
25
+ : "";
26
+ const script = runtime.script;
27
+ const args = runtime.args.join(" ");
28
+ if (!is.defined(env.try("PROBY_RELAUNCHED"))) {
29
+ await io.spawn(`${runtime.bin} ${conditions_flag} ${script} ${args}`, {
30
+ inherit: true,
31
+ env: { ...process.env, PROBY_RELAUNCHED: "1" },
32
+ });
33
+ runtime.exit(0);
11
34
  }
12
- else {
13
- // console.log(`spec.json missing, continuing with defaults`);
14
- }
15
- const [file] = args;
16
- const type = await (async (base) => {
17
- if (await base.join("packages").exists()) {
18
- return "monorepo";
19
- }
20
- if (await base.join("src").exists()) {
21
- return "repo";
22
- }
23
- })(root);
24
- if (type === "monorepo") {
25
- for (const repo of await root.join("packages").list({
35
+ import run from "#run";
36
+ const [file, group] = runtime.args;
37
+ if (monorepo) {
38
+ for (const repo of await root.join(packages).list({
26
39
  filter: info => info.kind === "directory",
27
40
  })) {
28
- await run(repo.join("src"), repo.name, file);
41
+ for (const dir of include) {
42
+ await run(repo.join(dir), repo.name, file, group);
43
+ }
29
44
  }
30
45
  }
31
- else if (type === "repo") {
32
- await run(root.join("src"), undefined, file);
33
- }
34
46
  else {
35
- print(`${color.red("src")} or ${color.red("packages")} not found\n`);
47
+ for (const dir of include) {
48
+ await run(root.join(dir), undefined, file, group);
49
+ }
36
50
  }
37
51
  //# sourceMappingURL=bin.js.map
@@ -0,0 +1,9 @@
1
+ import type Schema from "#Schema";
2
+ declare const _default: (input: typeof Schema.input) => {
3
+ monorepo?: boolean | undefined;
4
+ packages?: string | undefined;
5
+ include?: string[] | undefined;
6
+ conditions?: string[] | undefined;
7
+ } | undefined;
8
+ export default _default;
9
+ //# sourceMappingURL=config.d.ts.map
package/lib/config.js ADDED
@@ -0,0 +1,2 @@
1
+ export default (input) => input;
2
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1,2 @@
1
+ export declare function add(a: number, b: number): number;
2
+ //# sourceMappingURL=math.d.ts.map
@@ -0,0 +1,4 @@
1
+ export function add(a, b) {
2
+ return a + b;
3
+ }
4
+ //# sourceMappingURL=math.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=math.mock.d.ts.map
@@ -0,0 +1,5 @@
1
+ import test from "@rcompat/test";
2
+ test.mock("#fixtures/static-mock/math", () => ({
3
+ add: (a, b) => 99,
4
+ }));
5
+ //# sourceMappingURL=math.mock.js.map
package/lib/run.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  import type { FileRef } from "@rcompat/fs";
2
- declare const _default: (root: FileRef, subrepo?: string, target?: string) => Promise<void>;
2
+ declare const _default: (root: FileRef, subrepo?: string, target?: string, group?: string) => Promise<void>;
3
3
  export default _default;
4
4
  //# sourceMappingURL=run.d.ts.map
package/lib/run.js CHANGED
@@ -44,7 +44,8 @@ async function run_in_worker(spec, env) {
44
44
  });
45
45
  const context = await env_module.setup?.();
46
46
  return new Promise((resolve, reject) => {
47
- const worker = new Worker(new URL("./worker.js", import.meta.url), {
47
+ const worker_url = new URL(import.meta.url.endsWith(".ts") ? "./worker.ts" : "./worker.js", import.meta.url);
48
+ const worker = new Worker(worker_url, {
48
49
  workerData: {
49
50
  spec: spec.path,
50
51
  env: env.path,
@@ -72,7 +73,7 @@ async function run_in_worker(spec, env) {
72
73
  });
73
74
  });
74
75
  }
75
- export default async (root, subrepo, target) => {
76
+ export default async (root, subrepo, target, group) => {
76
77
  const files = await root.list({
77
78
  recursive: true,
78
79
  filter: info => extensions.some(extension => info.path.endsWith(extension)),
@@ -89,33 +90,41 @@ export default async (root, subrepo, target) => {
89
90
  await run_in_worker(file, env_file);
90
91
  continue;
91
92
  }
93
+ const mock_file = await file.sibling(file.name.replace(/\.spec\.(ts|js)$/, ".mock.$1")).or(() => null);
92
94
  repository.suite(file);
93
- await file.import();
94
- }
95
- for (const suite of repository.next()) {
96
- const failed = [];
97
- for await (const test of suite.run()) {
98
- for (const result of test.results) {
99
- if (result.passed) {
100
- print(color.green("o"));
95
+ const suite = repository.next().next().value;
96
+ try {
97
+ if (mock_file !== null)
98
+ await mock_file.import();
99
+ await file.import();
100
+ const failed = [];
101
+ for await (const test of suite.run()) {
102
+ if (group !== undefined && test.group !== group)
103
+ continue;
104
+ for (const result of test.results) {
105
+ if (result.passed) {
106
+ print(color.green("o"));
107
+ }
108
+ else {
109
+ failed.push([test, result]);
110
+ print(color.red("x"));
111
+ }
101
112
  }
102
- else {
103
- failed.push([test, result]);
104
- print(color.red("x"));
113
+ }
114
+ await suite.end();
115
+ if (failed.length > 0) {
116
+ print("\n");
117
+ for (const [test, result] of failed) {
118
+ print(`${suite.file.debase(root)} ${color.red(test.name)} \n`);
119
+ print(` expected ${stringify(result.expected)}\n`);
120
+ print(` actual ${stringify(result.actual)}\n`);
105
121
  }
106
122
  }
107
123
  }
108
- await suite.end();
109
- if (failed.length > 0) {
110
- print("\n");
111
- for (const [test, result] of failed) {
112
- print(`${suite.file.debase(root)} ${color.red(test.name)} \n`);
113
- print(` expected ${stringify(result.expected)}\n`);
114
- print(` actual ${stringify(result.actual)}\n`);
115
- }
124
+ finally {
125
+ repository.reset();
116
126
  }
117
127
  }
118
128
  print("\n");
119
- repository.reset();
120
129
  };
121
130
  //# sourceMappingURL=run.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "proby",
3
- "version": "0.12.0",
3
+ "version": "0.13.1",
4
4
  "description": "Standard library test runner",
5
5
  "bugs": "https://github.com/rcompat/rcompat/issues",
6
6
  "license": "MIT",
@@ -16,13 +16,17 @@
16
16
  "directory": "packages/proby"
17
17
  },
18
18
  "dependencies": {
19
- "@rcompat/args": "^0.12.0",
20
- "@rcompat/cli": "^0.17.0",
21
- "@rcompat/fs": "^0.27.1",
22
- "@rcompat/assert": "^0.7.0"
19
+ "pema": "^0.5.0",
20
+ "@rcompat/assert": "^0.8.0",
21
+ "@rcompat/cli": "^0.18.0",
22
+ "@rcompat/env": "^0.17.0",
23
+ "@rcompat/fs": "^0.28.0",
24
+ "@rcompat/io": "^0.5.0",
25
+ "@rcompat/is": "^0.6.0",
26
+ "@rcompat/runtime": "^0.11.0"
23
27
  },
24
28
  "peerDependencies": {
25
- "@rcompat/test": "^0.11.2"
29
+ "@rcompat/test": "^0.12.0"
26
30
  },
27
31
  "peerDependenciesMeta": {
28
32
  "@rcompat/test": {
@@ -32,10 +36,16 @@
32
36
  "type": "module",
33
37
  "imports": {
34
38
  "#*": {
35
- "apekit": "./src/*.ts",
39
+ "source": "./src/*.ts",
36
40
  "default": "./lib/*.js"
37
41
  }
38
42
  },
43
+ "exports": {
44
+ "./config": {
45
+ "source": "./src/config.ts",
46
+ "default": "./lib/config.js"
47
+ }
48
+ },
39
49
  "scripts": {
40
50
  "build": "npm run clean && tsc",
41
51
  "clean": "rm -rf ./lib",