@unrift/core 0.2.1 → 0.4.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 (68) hide show
  1. package/bin/unrift.mjs +2 -1
  2. package/dist/esm/cli.js +19 -13
  3. package/dist/esm/context.d.ts +1 -2
  4. package/dist/esm/context.d.ts.map +1 -1
  5. package/dist/esm/expect.d.ts +21 -1
  6. package/dist/esm/expect.d.ts.map +1 -1
  7. package/dist/esm/index.d.ts +3 -0
  8. package/dist/esm/index.d.ts.map +1 -1
  9. package/dist/esm/index.js +3 -0
  10. package/dist/esm/interface.d.ts +3 -3
  11. package/dist/esm/interface.d.ts.map +1 -1
  12. package/dist/esm/interface.js +11 -6
  13. package/dist/esm/matchers.d.ts.map +1 -1
  14. package/dist/esm/matchers.js +261 -1
  15. package/dist/esm/mock/auto.d.ts +27 -0
  16. package/dist/esm/mock/auto.d.ts.map +1 -0
  17. package/dist/esm/mock/auto.js +108 -0
  18. package/dist/esm/mock/fetch.d.ts +47 -0
  19. package/dist/esm/mock/fetch.d.ts.map +1 -0
  20. package/dist/esm/mock/fetch.js +149 -0
  21. package/dist/esm/mock/fs.d.ts +54 -0
  22. package/dist/esm/mock/fs.d.ts.map +1 -0
  23. package/dist/esm/mock/fs.js +125 -0
  24. package/dist/esm/mock/index.d.ts +70 -0
  25. package/dist/esm/mock/index.d.ts.map +1 -0
  26. package/dist/esm/mock/index.js +70 -0
  27. package/dist/esm/mock/module-loader.d.ts +66 -0
  28. package/dist/esm/mock/module-loader.d.ts.map +1 -0
  29. package/dist/esm/mock/module-loader.js +123 -0
  30. package/dist/esm/mock/module.d.ts +99 -0
  31. package/dist/esm/mock/module.d.ts.map +1 -0
  32. package/dist/esm/mock/module.js +198 -0
  33. package/dist/esm/mock/register-loader.d.ts +17 -0
  34. package/dist/esm/mock/register-loader.d.ts.map +1 -0
  35. package/dist/esm/mock/register-loader.js +45 -0
  36. package/dist/esm/mock/spy.d.ts +73 -0
  37. package/dist/esm/mock/spy.d.ts.map +1 -0
  38. package/dist/esm/mock/spy.js +307 -0
  39. package/dist/esm/mock/stub.d.ts +26 -0
  40. package/dist/esm/mock/stub.d.ts.map +1 -0
  41. package/dist/esm/mock/stub.js +82 -0
  42. package/dist/esm/mock/timers.d.ts +51 -0
  43. package/dist/esm/mock/timers.d.ts.map +1 -0
  44. package/dist/esm/mock/timers.js +581 -0
  45. package/dist/esm/runner.d.ts +1 -0
  46. package/dist/esm/runner.d.ts.map +1 -1
  47. package/dist/esm/runner.js +138 -28
  48. package/dist/esm/test.d.ts +3 -2
  49. package/dist/esm/test.d.ts.map +1 -1
  50. package/dist/esm/test.js +17 -7
  51. package/dist/esm/types.d.ts +0 -1
  52. package/dist/esm/types.d.ts.map +1 -1
  53. package/dist/esm/types.js +0 -1
  54. package/dist/esm/utils/compareEngine.d.ts.map +1 -1
  55. package/dist/esm/utils/compareEngine.js +32 -1
  56. package/dist/esm/utils/defineConfig.d.ts +0 -1
  57. package/dist/esm/utils/defineConfig.d.ts.map +1 -1
  58. package/dist/esm/utils/diff.d.ts.map +1 -1
  59. package/dist/esm/utils/diff.js +31 -0
  60. package/dist/esm/utils/discoverTestFiles.js +1 -1
  61. package/dist/esm/utils/helpers.d.ts +21 -0
  62. package/dist/esm/utils/helpers.d.ts.map +1 -1
  63. package/dist/esm/utils/helpers.js +97 -0
  64. package/dist/esm/utils/loadConfig.d.ts.map +1 -1
  65. package/dist/esm/utils/loadConfig.js +2 -2
  66. package/dist/esm/utils/toThrow.d.ts.map +1 -1
  67. package/dist/esm/utils/toThrow.js +2 -1
  68. package/package.json +8 -3
@@ -1,38 +1,33 @@
1
- import { relative, resolve } from "node:path";
1
+ import { watch as fsWatch, readdirSync } from "node:fs";
2
+ import { relative, resolve, join } from "node:path";
2
3
  import { loadConfig, loadConfigFromPath, } from "./utils/loadConfig.js";
3
4
  import { cleanUnriftCaches } from "./utils/transform.js";
4
5
  import { colors } from "./utils/colors.js";
5
6
  import { discoverTestFiles } from "./utils/discoverTestFiles.js";
6
7
  import { runEngine } from "./run.js";
7
8
  import { normalizePath } from "./utils/normalizePath.js";
9
+ import { safeRegExp, isGlobPattern, globToRegex } from "./utils/helpers.js";
8
10
  import { TaskStatus } from "./types.js";
11
+ /**
12
+ * Captured at module load so the runner's own timing measurements
13
+ * aren't affected by user mocks (e.g. mock.useFakeTimers patches performance.now).
14
+ */
15
+ const realPerformanceNow = performance.now.bind(performance);
9
16
  function debugLog(enabled, json, ...args) {
10
17
  // Keep stdout clean for --json mode; use stderr for debug.
11
18
  if (!enabled)
12
19
  return;
13
20
  (json ? console.error : console.log)(...args);
14
21
  }
15
- function safeRegExp(source) {
16
- try {
17
- return new RegExp(source);
18
- }
19
- catch (err) {
20
- const msg = err instanceof Error ? err.message : String(err);
21
- throw new Error(`Invalid regex "${source}": ${msg}`);
22
- }
23
- }
24
22
  const regExpCache = new Map();
25
23
  function getCachedRegExp(pattern) {
26
24
  let re = regExpCache.get(pattern);
27
25
  if (!re) {
28
- re = safeRegExp(pattern);
26
+ re = isGlobPattern(pattern) ? globToRegex(pattern) : safeRegExp(pattern);
29
27
  regExpCache.set(pattern, re);
30
28
  }
31
29
  return re;
32
30
  }
33
- /**
34
- * @TODO Add glob support
35
- */
36
31
  function matchesAnyPattern(fileAbs, patterns, baseDir) {
37
32
  const absolutePath = normalizePath(fileAbs);
38
33
  const relativePath = normalizePath(relative(baseDir, fileAbs));
@@ -51,6 +46,32 @@ function filterByIncludesExcludes(files, baseDir, includes, excludes) {
51
46
  }
52
47
  return filtered;
53
48
  }
49
+ function filterStack(stack) {
50
+ const lines = stack.split("\n");
51
+ const message = [];
52
+ const frames = [];
53
+ for (const line of lines) {
54
+ if (line.trimStart().startsWith("at ")) {
55
+ frames.push(line);
56
+ }
57
+ else {
58
+ message.push(line);
59
+ }
60
+ }
61
+ const filtered = frames.filter((line) => {
62
+ if (line.includes("node:"))
63
+ return false;
64
+ if (line.includes("/@unrift/core/dist/"))
65
+ return false;
66
+ if (line.includes("/unrift/dist/"))
67
+ return false;
68
+ if (line.includes("/dist/esm/"))
69
+ return false;
70
+ return true;
71
+ });
72
+ const kept = filtered.length > 0 ? filtered : frames;
73
+ return [...message, ...kept].join("\n");
74
+ }
54
75
  function formatDuration(ms) {
55
76
  if (ms < 1000)
56
77
  return `${ms.toFixed(0)}ms`;
@@ -127,7 +148,8 @@ function extractTestName(description) {
127
148
  return description.replace(/^.*?›\s*/, "");
128
149
  }
129
150
  export async function runTestsCLI(options = {}) {
130
- const runStart = performance.now();
151
+ regExpCache.clear();
152
+ const runStart = realPerformanceNow();
131
153
  // Cache clean mode
132
154
  if (options.cacheClean) {
133
155
  const projectRoot = process.cwd();
@@ -142,7 +164,7 @@ export async function runTestsCLI(options = {}) {
142
164
  if (result.tempCacheDeleted)
143
165
  lines.push("✔ deleted temp cache");
144
166
  if (lines.length === 0)
145
- lines.push("— nothing to clean");
167
+ lines.push("- nothing to clean");
146
168
  console.log(lines.join("\n"));
147
169
  }
148
170
  return;
@@ -162,8 +184,10 @@ export async function runTestsCLI(options = {}) {
162
184
  // Test dir is relative to config dir if config is loaded
163
185
  const testDir = resolve(configDir, config?.testDir ?? options.testDir ?? "test");
164
186
  let files = discoverTestFiles(testDir);
165
- if (options.pattern) {
166
- files = files.filter((f) => options.pattern.test(normalizePath(f)));
187
+ const pattern = options.pattern ??
188
+ (config?.pattern ? safeRegExp(config.pattern) : undefined);
189
+ if (pattern) {
190
+ files = files.filter((f) => pattern.test(normalizePath(f)));
167
191
  }
168
192
  files = filterByIncludesExcludes(files, testDir, config?.includes, config?.excludes).sort((a, b) => normalizePath(a).localeCompare(normalizePath(b)));
169
193
  debugLog(options.debug, options.json, "Discovered test files:", files);
@@ -196,7 +220,7 @@ export async function runTestsCLI(options = {}) {
196
220
  testDir: dir,
197
221
  configPath: resolvedConfigPath,
198
222
  files: [],
199
- durationMs: performance.now() - runStart,
223
+ durationMs: realPerformanceNow() - runStart,
200
224
  results: [],
201
225
  };
202
226
  console.log(JSON.stringify(report, null, 2));
@@ -246,7 +270,7 @@ export async function runTestsCLI(options = {}) {
246
270
  });
247
271
  }
248
272
  const ok = failed === 0;
249
- const durationMs = performance.now() - runStart;
273
+ const durationMs = realPerformanceNow() - runStart;
250
274
  // JSON mode: stdout must be JSON only
251
275
  if (options.json) {
252
276
  const report = {
@@ -269,7 +293,7 @@ export async function runTestsCLI(options = {}) {
269
293
  process.exitCode = 1;
270
294
  return;
271
295
  }
272
- // ── Pretty reporting ──────────────────────────────────────────────
296
+ // Pretty reporting
273
297
  const groups = groupResultsByFile(results);
274
298
  // File-by-file results
275
299
  for (const group of groups) {
@@ -296,14 +320,13 @@ export async function runTestsCLI(options = {}) {
296
320
  const parts = testName.split(" › ");
297
321
  const leaf = parts[parts.length - 1];
298
322
  const suitePath = parts.slice(0, -1).join(" › ");
299
- // Print suite name when it changes
300
- if (suitePath && suitePath !== lastSuitePath) {
301
- const depth = parts.length - 1;
323
+ const depth = parts.length - 1;
324
+ // Print suite name when it changes (depth > 0 guards against top-level it() tests)
325
+ if (depth > 0 && suitePath && suitePath !== lastSuitePath) {
302
326
  const suiteIndent = " " + " ".repeat(Math.max(0, depth - 1));
303
327
  console.log(`${suiteIndent}${colors.dim("›")} ${colors.bold(parts[depth - 1])}`);
304
328
  lastSuitePath = suitePath;
305
329
  }
306
- const depth = parts.length - 1;
307
330
  const indent = " " + " ".repeat(depth);
308
331
  if (result.status === TaskStatus.Pass) {
309
332
  const dur = colors.dim(formatDuration(result.durationMs ?? 0));
@@ -325,14 +348,14 @@ export async function runTestsCLI(options = {}) {
325
348
  }
326
349
  }
327
350
  }
328
- // ── Failure details ────────────────────────────────────────────────
351
+ // Failure details
329
352
  if (failures.length > 0) {
330
353
  console.log(`\n${horizontalRule(colors.boldRed(" FAILURES "))}\n`);
331
354
  failures.forEach((r, i) => {
332
355
  const testName = extractTestName(r.description);
333
356
  console.log(colors.boldRed(` ${i + 1}) ${testName}`));
334
357
  if (r.error) {
335
- const errText = r.error.stack ?? r.error.message;
358
+ const errText = filterStack(r.error.stack ?? r.error.message);
336
359
  const lines = errText.split("\n");
337
360
  for (const line of lines) {
338
361
  // Highlight the "Received/Expected" lines differently
@@ -355,7 +378,7 @@ export async function runTestsCLI(options = {}) {
355
378
  });
356
379
  console.log(`\n${horizontalRule()}`);
357
380
  }
358
- // ── Summary ────────────────────────────────────────────────────────
381
+ // Summary
359
382
  console.log("");
360
383
  const summaryParts = [];
361
384
  if (passed > 0)
@@ -373,3 +396,90 @@ export async function runTestsCLI(options = {}) {
373
396
  if (!ok)
374
397
  process.exitCode = 1;
375
398
  }
399
+ /**
400
+ * Watch a directory tree for changes, calling `onChange` after a debounce.
401
+ * Uses individual `fs.watch()` calls per directory so it works on all
402
+ * platforms (Linux `fs.watch` does not support `recursive: true` before Node 22).
403
+ * Returns a cleanup function to stop watching.
404
+ */
405
+ function watchRecursive(dir, onChange, debounceMs = 150) {
406
+ let timer = null;
407
+ let lastFile = "";
408
+ function trigger(filename) {
409
+ lastFile = filename;
410
+ if (timer)
411
+ clearTimeout(timer);
412
+ timer = setTimeout(() => onChange(lastFile), debounceMs);
413
+ }
414
+ const watchers = [];
415
+ function watchDir(d) {
416
+ try {
417
+ watchers.push(fsWatch(d, (_event, filename) => trigger(filename ?? d)));
418
+ }
419
+ catch {
420
+ return;
421
+ }
422
+ let entries;
423
+ try {
424
+ entries = readdirSync(d, { withFileTypes: true });
425
+ }
426
+ catch {
427
+ return;
428
+ }
429
+ for (const entry of entries) {
430
+ if (entry.isDirectory() &&
431
+ !entry.name.startsWith(".") &&
432
+ entry.name !== "node_modules") {
433
+ watchDir(join(d, entry.name));
434
+ }
435
+ }
436
+ }
437
+ watchDir(dir);
438
+ return () => {
439
+ if (timer)
440
+ clearTimeout(timer);
441
+ for (const w of watchers) {
442
+ try {
443
+ w.close();
444
+ }
445
+ catch {
446
+ // ignore
447
+ }
448
+ }
449
+ };
450
+ }
451
+ export async function watchTestsCLI(options = {}) {
452
+ // Run once immediately
453
+ await runTestsCLI(options);
454
+ // Resolve the directory to watch (same logic as runTestsCLI)
455
+ const loaded = options.configPath
456
+ ? await import("./utils/loadConfig").then((m) => m.loadConfigFromPath(options.configPath))
457
+ : await import("./utils/loadConfig").then((m) => m.loadConfig(process.cwd()));
458
+ const config = loaded?.config ?? null;
459
+ const configDir = loaded?.configDir ?? process.cwd();
460
+ const testDir = resolve(configDir, config?.testDir ?? options.testDir ?? "test");
461
+ console.log(`\n ${colors.dim("↺")} ${colors.dim("Watching")} ${colors.bold(shortenPath(testDir))} ${colors.dim("for changes… Ctrl+C to quit")}\n`);
462
+ let running = false;
463
+ const stop = watchRecursive(testDir, async (filename) => {
464
+ if (running)
465
+ return;
466
+ running = true;
467
+ const shortFile = shortenPath(filename);
468
+ console.log(`\n${colors.dim("─".repeat(Math.min(process.stdout.columns || 80, 80)))}`);
469
+ console.log(` ${colors.dim("↺")} ${colors.dim(`${shortFile} changed - re-running…`)}\n`);
470
+ // Reset exit code so each run is evaluated independently
471
+ process.exitCode = 0;
472
+ try {
473
+ await runTestsCLI(options);
474
+ }
475
+ finally {
476
+ running = false;
477
+ }
478
+ console.log(` ${colors.dim("↺")} ${colors.dim("Watching for changes… Ctrl+C to quit")}\n`);
479
+ });
480
+ // Keep process alive and clean up on exit
481
+ process.on("SIGINT", () => {
482
+ stop();
483
+ process.exit(0);
484
+ });
485
+ }
@@ -6,7 +6,8 @@ export declare class Test {
6
6
  fn: PromisableFn<void>;
7
7
  mode: TaskMode;
8
8
  status: TaskStatus;
9
- constructor(description: string, fn: PromisableFn<void>, mode?: TaskMode);
10
- run(timeoutMs?: number): Promise<void>;
9
+ timeoutMs?: number;
10
+ constructor(description: string, fn: PromisableFn<void>, mode?: TaskMode, timeoutMs?: number);
11
+ run(globalTimeoutMs?: number): Promise<void>;
11
12
  }
12
13
  //# sourceMappingURL=test.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"test.d.ts","sourceRoot":"","sources":["../../src/test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,KAAK,YAAY,EAAE,MAAM,SAAS,CAAC;AAGlE,qBAAa,IAAI;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAK;IACvB,KAAK,CAAC,EAAE,KAAK,CAAC;IAEd,EAAE,EAAE,YAAY,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,EAAE,QAAQ,CAAC;IAEf,MAAM,EAAE,UAAU,CAAsB;gBAGtC,WAAW,EAAE,MAAM,EACnB,EAAE,EAAE,YAAY,CAAC,IAAI,CAAC,EACtB,IAAI,GAAE,QAA2B;IAO7B,GAAG,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAsD7C"}
1
+ {"version":3,"file":"test.d.ts","sourceRoot":"","sources":["../../src/test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,KAAK,YAAY,EAAE,MAAM,SAAS,CAAC;AASlE,qBAAa,IAAI;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAK;IACvB,KAAK,CAAC,EAAE,KAAK,CAAC;IAEd,EAAE,EAAE,YAAY,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,EAAE,QAAQ,CAAC;IAEf,MAAM,EAAE,UAAU,CAAsB;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;gBAGjB,WAAW,EAAE,MAAM,EACnB,EAAE,EAAE,YAAY,CAAC,IAAI,CAAC,EACtB,IAAI,GAAE,QAA2B,EACjC,SAAS,CAAC,EAAE,MAAM;IAQd,GAAG,CAAC,eAAe,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAyDnD"}
package/dist/esm/test.js CHANGED
@@ -1,5 +1,10 @@
1
1
  import { TaskMode, TaskStatus } from "./types.js";
2
2
  import { toError } from "./utils/toError.js";
3
+ /**
4
+ * Captured at module load so the runner's own timing measurements are
5
+ * immune to user mocks (e.g. mock.useFakeTimers patches performance.now).
6
+ */
7
+ const realPerformanceNow = performance.now.bind(performance);
3
8
  export class Test {
4
9
  description;
5
10
  durationMs = 0;
@@ -7,12 +12,15 @@ export class Test {
7
12
  fn;
8
13
  mode;
9
14
  status = TaskStatus.Pending;
10
- constructor(description, fn, mode = TaskMode.Default) {
15
+ timeoutMs;
16
+ constructor(description, fn, mode = TaskMode.Default, timeoutMs) {
11
17
  this.description = description;
12
18
  this.fn = fn;
13
19
  this.mode = mode;
20
+ this.timeoutMs = timeoutMs;
14
21
  }
15
- async run(timeoutMs) {
22
+ async run(globalTimeoutMs) {
23
+ const timeoutMs = this.timeoutMs ?? globalTimeoutMs;
16
24
  if (this.mode === TaskMode.Skip) {
17
25
  this.status = TaskStatus.Skipped;
18
26
  this.durationMs = 0;
@@ -23,7 +31,7 @@ export class Test {
23
31
  this.durationMs = 0;
24
32
  return;
25
33
  }
26
- const start = performance.now();
34
+ const start = realPerformanceNow();
27
35
  this.status = TaskStatus.Running;
28
36
  const exec = Promise.resolve().then(() => this.fn());
29
37
  try {
@@ -34,9 +42,11 @@ export class Test {
34
42
  reject(new Error(`Test "${this.description}" timed out after ${timeoutMs} ms`));
35
43
  }, timeoutMs);
36
44
  });
37
- // NOTE: Promise.race does not cancel the losing promise. A timed-out test
38
- // function continues running in the background. True cancellation would
39
- // require AbortController integration, which is a larger future change.
45
+ /**
46
+ * NOTE: Promise.race does not cancel the losing promise. A timed-out test
47
+ * function continues running in the background. True cancellation would
48
+ * require AbortController integration, which is a larger future change.
49
+ */
40
50
  try {
41
51
  await Promise.race([exec, timeoutFn]);
42
52
  }
@@ -55,7 +65,7 @@ export class Test {
55
65
  this.status = TaskStatus.Fail;
56
66
  }
57
67
  finally {
58
- this.durationMs = performance.now() - start;
68
+ this.durationMs = realPerformanceNow() - start;
59
69
  }
60
70
  }
61
71
  }
@@ -5,7 +5,6 @@ export declare enum TaskStatus {
5
5
  Running = "running",
6
6
  Pass = "pass",
7
7
  Fail = "fail",
8
- Only = "only",
9
8
  Skipped = "skipped",
10
9
  Todo = "todo"
11
10
  }
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;AAE3C,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI,MAAM,UAAU,CAAC,CAAC,CAAC,CAAC;AAElD,oBAAY,UAAU;IACpB,OAAO,YAAY;IACnB,OAAO,YAAY;IACnB,IAAI,SAAS;IACb,IAAI,SAAS;IACb,IAAI,SAAS;IACb,OAAO,YAAY;IACnB,IAAI,SAAS;CACd;AAED,oBAAY,QAAQ;IAClB,OAAO,YAAY;IACnB,IAAI,SAAS;IACb,IAAI,SAAS;IACb,IAAI,SAAS;CACd;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,OAAO,CAAC;IACf,IAAI,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;CACtC;AAED,MAAM,MAAM,SAAS,GAAG,CACtB,IAAI,EAAE,cAAc,EACpB,QAAQ,EAAE,OAAO,EACjB,GAAG,IAAI,EAAE,SAAS,OAAO,EAAE,KACxB,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAE1B,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;AAE3C,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI,MAAM,UAAU,CAAC,CAAC,CAAC,CAAC;AAElD,oBAAY,UAAU;IACpB,OAAO,YAAY;IACnB,OAAO,YAAY;IACnB,IAAI,SAAS;IACb,IAAI,SAAS;IACb,OAAO,YAAY;IACnB,IAAI,SAAS;CACd;AAED,oBAAY,QAAQ;IAClB,OAAO,YAAY;IACnB,IAAI,SAAS;IACb,IAAI,SAAS;IACb,IAAI,SAAS;CACd;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,OAAO,CAAC;IACf,IAAI,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;CACtC;AAED,MAAM,MAAM,SAAS,GAAG,CACtB,IAAI,EAAE,cAAc,EACpB,QAAQ,EAAE,OAAO,EACjB,GAAG,IAAI,EAAE,SAAS,OAAO,EAAE,KACxB,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAE1B,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC"}
package/dist/esm/types.js CHANGED
@@ -4,7 +4,6 @@ export var TaskStatus;
4
4
  TaskStatus["Running"] = "running";
5
5
  TaskStatus["Pass"] = "pass";
6
6
  TaskStatus["Fail"] = "fail";
7
- TaskStatus["Only"] = "only";
8
7
  TaskStatus["Skipped"] = "skipped";
9
8
  TaskStatus["Todo"] = "todo";
10
9
  })(TaskStatus || (TaskStatus = {}));
@@ -1 +1 @@
1
- {"version":3,"file":"compareEngine.d.ts","sourceRoot":"","sources":["../../../src/utils/compareEngine.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,cAAc;IAC7B,qEAAqE;IACrE,cAAc,EAAE,OAAO,CAAC;IACxB,0EAA0E;IAC1E,sBAAsB,EAAE,OAAO,CAAC;IAChC,yDAAyD;IACzD,mBAAmB,EAAE,OAAO,CAAC;CAC9B;AAID,wBAAgB,eAAe,CAC7B,IAAI,EAAE,OAAO,EACb,KAAK,EAAE,OAAO,EACd,OAAO,EAAE,cAAc,EACvB,eAAe,EAAE,OAAO,CAAC,MAAM,EAAE,MAAM,CAAC,EACxC,eAAe,CAAC,EAAE,OAAO,CAAC,MAAM,EAAE,MAAM,CAAC,GACxC,OAAO,CA6RT"}
1
+ {"version":3,"file":"compareEngine.d.ts","sourceRoot":"","sources":["../../../src/utils/compareEngine.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,cAAc;IAC7B,qEAAqE;IACrE,cAAc,EAAE,OAAO,CAAC;IACxB,0EAA0E;IAC1E,sBAAsB,EAAE,OAAO,CAAC;IAChC,yDAAyD;IACzD,mBAAmB,EAAE,OAAO,CAAC;CAC9B;AAID,wBAAgB,eAAe,CAC7B,IAAI,EAAE,OAAO,EACb,KAAK,EAAE,OAAO,EACd,OAAO,EAAE,cAAc,EACvB,eAAe,EAAE,OAAO,CAAC,MAAM,EAAE,MAAM,CAAC,EACxC,eAAe,CAAC,EAAE,OAAO,CAAC,MAAM,EAAE,MAAM,CAAC,GACxC,OAAO,CA4TT"}
@@ -142,6 +142,21 @@ export function compareInternal(left, right, options, leftToRightSeen, rightToLe
142
142
  const rightSet = currentRight;
143
143
  if (leftSet.size !== rightSet.size)
144
144
  return false;
145
+ // Fast path: primitive-only sets can use has() directly - O(n) vs O(n²)
146
+ let allPrimitive = true;
147
+ for (const v of leftSet) {
148
+ if (!isPrimitive(v)) {
149
+ allPrimitive = false;
150
+ break;
151
+ }
152
+ }
153
+ if (allPrimitive) {
154
+ for (const v of leftSet) {
155
+ if (!rightSet.has(v))
156
+ return false;
157
+ }
158
+ continue;
159
+ }
145
160
  const remaining = new Set(rightSet);
146
161
  outer: for (const leftValue of leftSet) {
147
162
  for (const rightValue of remaining) {
@@ -164,6 +179,22 @@ export function compareInternal(left, right, options, leftToRightSeen, rightToLe
164
179
  const rightMap = currentRight;
165
180
  if (leftMap.size !== rightMap.size)
166
181
  return false;
182
+ // Fast path: primitive-only keys can use get() directly - O(n) vs O(n²)
183
+ let allKeysPrimitive = true;
184
+ for (const k of leftMap.keys()) {
185
+ if (!isPrimitive(k)) {
186
+ allKeysPrimitive = false;
187
+ break;
188
+ }
189
+ }
190
+ if (allKeysPrimitive) {
191
+ for (const [leftKey, leftValue] of leftMap.entries()) {
192
+ if (!rightMap.has(leftKey))
193
+ return false;
194
+ worklist.push([leftValue, rightMap.get(leftKey)]);
195
+ }
196
+ continue;
197
+ }
167
198
  const remainingEntries = new Set(rightMap.entries());
168
199
  outer: for (const [leftKey, leftValue] of leftMap.entries()) {
169
200
  for (const candidate of remainingEntries) {
@@ -199,7 +230,7 @@ export function compareInternal(left, right, options, leftToRightSeen, rightToLe
199
230
  }
200
231
  }
201
232
  else {
202
- // Prototype-insensitive (looseEqual) — union-of-keys, missing ≈ undefined
233
+ // Prototype-insensitive (looseEqual) - union-of-keys, missing ≈ undefined
203
234
  const leftKeys = Object.keys(currentLeft);
204
235
  const rightKeys = Object.keys(currentRight);
205
236
  const allKeys = new Set([...leftKeys, ...rightKeys]);
@@ -1,6 +1,5 @@
1
1
  export interface UnriftConfigOptions {
2
2
  testDir?: string;
3
- rootDir?: string;
4
3
  timeoutMs?: number;
5
4
  bail?: boolean;
6
5
  pattern?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"defineConfig.d.ts","sourceRoot":"","sources":["../../../src/utils/defineConfig.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,mBAAmB;IAClC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB;AAED,wBAAgB,YAAY,CAC1B,OAAO,EAAE,mBAAmB,GAC3B,mBAAmB,CAErB"}
1
+ {"version":3,"file":"defineConfig.d.ts","sourceRoot":"","sources":["../../../src/utils/defineConfig.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,mBAAmB;IAClC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB;AAED,wBAAgB,YAAY,CAC1B,OAAO,EAAE,mBAAmB,GAC3B,mBAAmB,CAErB"}
@@ -1 +1 @@
1
- {"version":3,"file":"diff.d.ts","sourceRoot":"","sources":["../../../src/utils/diff.ts"],"names":[],"mappings":"AA2EA,wBAAgB,UAAU,CAAC,QAAQ,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,GAAG,MAAM,CA6BvE"}
1
+ {"version":3,"file":"diff.d.ts","sourceRoot":"","sources":["../../../src/utils/diff.ts"],"names":[],"mappings":"AAuGA,wBAAgB,UAAU,CAAC,QAAQ,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,GAAG,MAAM,CAsCvE"}
@@ -37,6 +37,32 @@ function formatValue(value, indent = 2) {
37
37
  }
38
38
  return String(value);
39
39
  }
40
+ function diffArrays(received, expected) {
41
+ const len = Math.max(received.length, expected.length);
42
+ const lines = [];
43
+ for (let i = 0; i < len; i++) {
44
+ const inReceived = i < received.length;
45
+ const inExpected = i < expected.length;
46
+ if (inReceived && inExpected) {
47
+ const rv = formatValue(received[i]);
48
+ const ev = formatValue(expected[i]);
49
+ if (rv !== ev) {
50
+ lines.push(colors.red(` - [${i}]: ${rv}`));
51
+ lines.push(colors.green(` + [${i}]: ${ev}`));
52
+ }
53
+ else {
54
+ lines.push(` [${i}]: ${rv}`);
55
+ }
56
+ }
57
+ else if (inReceived) {
58
+ lines.push(colors.red(` - [${i}]: ${formatValue(received[i])}`));
59
+ }
60
+ else {
61
+ lines.push(colors.green(` + [${i}]: ${formatValue(expected[i])}`));
62
+ }
63
+ }
64
+ return lines.join("\n");
65
+ }
40
66
  function diffObjects(received, expected) {
41
67
  const allKeys = new Set([...Object.keys(received), ...Object.keys(expected)]);
42
68
  const lines = [];
@@ -64,6 +90,11 @@ function diffObjects(received, expected) {
64
90
  return lines.join("\n");
65
91
  }
66
92
  export function formatDiff(received, expected) {
93
+ // For arrays, provide per-element diffing
94
+ if (Array.isArray(received) && Array.isArray(expected)) {
95
+ const diff = diffArrays(received, expected);
96
+ return (`\n${colors.red("- Received")} / ${colors.green("+ Expected")}\n\n` + diff);
97
+ }
67
98
  // For objects, provide a structural diff
68
99
  if (received !== null &&
69
100
  expected !== null &&
@@ -21,7 +21,7 @@ export function discoverTestFiles(dir) {
21
21
  }
22
22
  }
23
23
  catch {
24
- // Broken symlink — skip silently
24
+ // Broken symlink - skip silently
25
25
  }
26
26
  continue;
27
27
  }
@@ -3,6 +3,27 @@ export type TypedArray = Uint8Array | Int8Array | Uint16Array | Int16Array | Uin
3
3
  export declare function isPrimitive(value: unknown): value is Primitive;
4
4
  export declare function isObjectLike(value: unknown): value is object;
5
5
  export declare function isTypedArray(value: unknown): value is TypedArray;
6
+ /**
7
+ * Returns true if the string looks like a glob pattern (contains * ? [ { }).
8
+ * Patterns without these characters are treated as regex strings for backward compatibility.
9
+ */
10
+ export declare function isGlobPattern(pattern: string): boolean;
11
+ /**
12
+ * Converts a glob pattern to a RegExp.
13
+ * Supports: ** (any path), * (any segment), ? (single char), {a,b} (alternation), [abc] (char class).
14
+ */
15
+ export declare function globToRegex(glob: string): RegExp;
16
+ export declare function safeRegExp(source: string): RegExp;
17
+ /**
18
+ * Stateless RegExp match for user-supplied patterns.
19
+ *
20
+ * `RegExp.prototype.test` advances `lastIndex` for `/g` and `/y` regexes, so
21
+ * reusing the same regex object across calls (a matcher tested against several
22
+ * requests, or the same assertion run twice) can start mid-string and miss.
23
+ * Resetting `lastIndex` first makes every test independent - "does this pattern
24
+ * match the string" regardless of prior calls.
25
+ */
26
+ export declare function regexTest(re: RegExp, value: string): boolean;
6
27
  /**
7
28
  * SameValueZero-ish semantics:
8
29
  * - NaN equals NaN
@@ -1 +1 @@
1
- {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../src/utils/helpers.ts"],"names":[],"mappings":"AAAA,KAAK,SAAS,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC;AAEhF,MAAM,MAAM,UAAU,GAClB,UAAU,GACV,SAAS,GACT,WAAW,GACX,UAAU,GACV,WAAW,GACX,UAAU,GACV,YAAY,GACZ,YAAY,GACZ,aAAa,GACb,cAAc,CAAC;AAEnB,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,SAAS,CAY9D;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAE5D;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,UAAU,CAEhE;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,OAAO,CASzD"}
1
+ {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../src/utils/helpers.ts"],"names":[],"mappings":"AAAA,KAAK,SAAS,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC;AAEhF,MAAM,MAAM,UAAU,GAClB,UAAU,GACV,SAAS,GACT,WAAW,GACX,UAAU,GACV,WAAW,GACX,UAAU,GACV,YAAY,GACZ,YAAY,GACZ,aAAa,GACb,cAAc,CAAC;AAEnB,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,SAAS,CAY9D;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAE5D;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,UAAU,CAEhE;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAEtD;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAyDhD;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAOjD;AAED;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAG5D;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,OAAO,CASzD"}
@@ -14,6 +14,103 @@ export function isObjectLike(value) {
14
14
  export function isTypedArray(value) {
15
15
  return ArrayBuffer.isView(value) && !(value instanceof DataView);
16
16
  }
17
+ /**
18
+ * Returns true if the string looks like a glob pattern (contains * ? [ { }).
19
+ * Patterns without these characters are treated as regex strings for backward compatibility.
20
+ */
21
+ export function isGlobPattern(pattern) {
22
+ return /[*?[\]{]/.test(pattern);
23
+ }
24
+ /**
25
+ * Converts a glob pattern to a RegExp.
26
+ * Supports: ** (any path), * (any segment), ? (single char), {a,b} (alternation), [abc] (char class).
27
+ */
28
+ export function globToRegex(glob) {
29
+ let src = "";
30
+ let i = 0;
31
+ while (i < glob.length) {
32
+ const c = glob[i];
33
+ if (c === "*") {
34
+ if (glob[i + 1] === "*") {
35
+ // ** matches anything including path separators
36
+ src += ".*";
37
+ i += 2;
38
+ // consume optional trailing slash so "fixtures/**" works
39
+ if (glob[i] === "/")
40
+ i++;
41
+ }
42
+ else {
43
+ // * matches anything within a single path segment
44
+ src += "[^/]*";
45
+ i++;
46
+ }
47
+ }
48
+ else if (c === "?") {
49
+ src += "[^/]";
50
+ i++;
51
+ }
52
+ else if (c === "[") {
53
+ // Pass character classes through as-is
54
+ const close = glob.indexOf("]", i);
55
+ if (close !== -1) {
56
+ src += glob.slice(i, close + 1);
57
+ i = close + 1;
58
+ }
59
+ else {
60
+ src += "\\[";
61
+ i++;
62
+ }
63
+ }
64
+ else if (c === "{") {
65
+ // {a,b,c} → (a|b|c)
66
+ const close = glob.indexOf("}", i);
67
+ if (close !== -1) {
68
+ const parts = glob
69
+ .slice(i + 1, close)
70
+ .split(",")
71
+ .map((p) => p.replace(/[.+^${}()|[\]\\]/g, "\\$&"));
72
+ src += `(${parts.join("|")})`;
73
+ i = close + 1;
74
+ }
75
+ else {
76
+ src += "\\{";
77
+ i++;
78
+ }
79
+ }
80
+ else if (/[.+^$|()\\]/.test(c)) {
81
+ // Escape regex special characters
82
+ src += "\\" + c;
83
+ i++;
84
+ }
85
+ else {
86
+ src += c;
87
+ i++;
88
+ }
89
+ }
90
+ return new RegExp(src);
91
+ }
92
+ export function safeRegExp(source) {
93
+ try {
94
+ return new RegExp(source);
95
+ }
96
+ catch (err) {
97
+ const msg = err instanceof Error ? err.message : String(err);
98
+ throw new Error(`Invalid regex "${source}": ${msg}`);
99
+ }
100
+ }
101
+ /**
102
+ * Stateless RegExp match for user-supplied patterns.
103
+ *
104
+ * `RegExp.prototype.test` advances `lastIndex` for `/g` and `/y` regexes, so
105
+ * reusing the same regex object across calls (a matcher tested against several
106
+ * requests, or the same assertion run twice) can start mid-string and miss.
107
+ * Resetting `lastIndex` first makes every test independent - "does this pattern
108
+ * match the string" regardless of prior calls.
109
+ */
110
+ export function regexTest(re, value) {
111
+ re.lastIndex = 0;
112
+ return re.test(value);
113
+ }
17
114
  /**
18
115
  * SameValueZero-ish semantics:
19
116
  * - NaN equals NaN
@@ -1 +1 @@
1
- {"version":3,"file":"loadConfig.d.ts","sourceRoot":"","sources":["../../../src/utils/loadConfig.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAU1D,MAAM,MAAM,YAAY,GAAG;IACzB,MAAM,EAAE,mBAAmB,CAAC;IAE5B,UAAU,EAAE,MAAM,CAAC;IAEnB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAuCF,wBAAsB,kBAAkB,CACtC,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,YAAY,CAAC,CAevB;AAqCD,wBAAsB,UAAU,CAC9B,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAc9B"}
1
+ {"version":3,"file":"loadConfig.d.ts","sourceRoot":"","sources":["../../../src/utils/loadConfig.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAU1D,MAAM,MAAM,YAAY,GAAG;IACzB,MAAM,EAAE,mBAAmB,CAAC;IAE5B,UAAU,EAAE,MAAM,CAAC;IAEnB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAuCF,wBAAsB,kBAAkB,CACtC,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,YAAY,CAAC,CAevB;AAqCD,wBAAsB,UAAU,CAC9B,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAc9B"}