@unrift/core 0.3.0 → 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 (64) hide show
  1. package/bin/unrift.mjs +2 -1
  2. package/dist/esm/cli.js +19 -1
  3. package/dist/esm/expect.d.ts +14 -0
  4. package/dist/esm/expect.d.ts.map +1 -1
  5. package/dist/esm/index.d.ts +3 -1
  6. package/dist/esm/index.d.ts.map +1 -1
  7. package/dist/esm/index.js +2 -0
  8. package/dist/esm/interface.d.ts +7 -3
  9. package/dist/esm/interface.d.ts.map +1 -1
  10. package/dist/esm/interface.js +9 -5
  11. package/dist/esm/matchers.d.ts.map +1 -1
  12. package/dist/esm/matchers.js +195 -1
  13. package/dist/esm/mock/auto.d.ts +27 -0
  14. package/dist/esm/mock/auto.d.ts.map +1 -0
  15. package/dist/esm/mock/auto.js +108 -0
  16. package/dist/esm/mock/fetch.d.ts +47 -0
  17. package/dist/esm/mock/fetch.d.ts.map +1 -0
  18. package/dist/esm/mock/fetch.js +149 -0
  19. package/dist/esm/mock/fs.d.ts +54 -0
  20. package/dist/esm/mock/fs.d.ts.map +1 -0
  21. package/dist/esm/mock/fs.js +125 -0
  22. package/dist/esm/mock/index.d.ts +70 -0
  23. package/dist/esm/mock/index.d.ts.map +1 -0
  24. package/dist/esm/mock/index.js +70 -0
  25. package/dist/esm/mock/module-loader.d.ts +66 -0
  26. package/dist/esm/mock/module-loader.d.ts.map +1 -0
  27. package/dist/esm/mock/module-loader.js +123 -0
  28. package/dist/esm/mock/module.d.ts +99 -0
  29. package/dist/esm/mock/module.d.ts.map +1 -0
  30. package/dist/esm/mock/module.js +198 -0
  31. package/dist/esm/mock/register-loader.d.ts +17 -0
  32. package/dist/esm/mock/register-loader.d.ts.map +1 -0
  33. package/dist/esm/mock/register-loader.js +45 -0
  34. package/dist/esm/mock/spy.d.ts +73 -0
  35. package/dist/esm/mock/spy.d.ts.map +1 -0
  36. package/dist/esm/mock/spy.js +307 -0
  37. package/dist/esm/mock/stub.d.ts +26 -0
  38. package/dist/esm/mock/stub.d.ts.map +1 -0
  39. package/dist/esm/mock/stub.js +82 -0
  40. package/dist/esm/mock/timers.d.ts +51 -0
  41. package/dist/esm/mock/timers.d.ts.map +1 -0
  42. package/dist/esm/mock/timers.js +581 -0
  43. package/dist/esm/run.d.ts +4 -7
  44. package/dist/esm/run.d.ts.map +1 -1
  45. package/dist/esm/run.js +6 -2
  46. package/dist/esm/runner.d.ts +1 -0
  47. package/dist/esm/runner.d.ts.map +1 -1
  48. package/dist/esm/runner.js +21 -10
  49. package/dist/esm/suite.d.ts +24 -6
  50. package/dist/esm/suite.d.ts.map +1 -1
  51. package/dist/esm/suite.js +114 -20
  52. package/dist/esm/test.d.ts +5 -1
  53. package/dist/esm/test.d.ts.map +1 -1
  54. package/dist/esm/test.js +20 -6
  55. package/dist/esm/utils/compareEngine.js +3 -3
  56. package/dist/esm/utils/defineConfig.d.ts +1 -0
  57. package/dist/esm/utils/defineConfig.d.ts.map +1 -1
  58. package/dist/esm/utils/discoverTestFiles.js +1 -1
  59. package/dist/esm/utils/helpers.d.ts +12 -0
  60. package/dist/esm/utils/helpers.d.ts.map +1 -1
  61. package/dist/esm/utils/helpers.js +20 -0
  62. package/dist/esm/utils/toThrow.d.ts.map +1 -1
  63. package/dist/esm/utils/toThrow.js +2 -1
  64. package/package.json +4 -3
@@ -0,0 +1,149 @@
1
+ /**
2
+ * `mock.fetch` - stub `globalThis.fetch` for HTTP testing.
3
+ *
4
+ * Handlers are matched against requests in registration order; the first
5
+ * matching handler wins. `mock.fetchOnce()` registers a single-use handler
6
+ * that's consumed after its first match.
7
+ *
8
+ * Restoration:
9
+ * - `mock.fetch.restore()` un-patches global fetch and clears handlers
10
+ * - `mock.restoreAll()` does the same as part of bulk cleanup
11
+ */
12
+ import { activeRestorers } from "./spy.js";
13
+ import { regexTest } from "../utils/helpers.js";
14
+ /** Brand applied to `mock.fetch` so matchers can verify their received value. */
15
+ export const FETCH_MOCK_BRAND = Symbol.for("unrift.fetchMock");
16
+ // Process-global singleton state. Correct because unrift runs spec files
17
+ // sequentially in one process and afterEach(restoreAll) resets between tests.
18
+ // Parallel per-file execution (see ISSUES.md) would need per-worker isolation.
19
+ const handlers = [];
20
+ const calls = [];
21
+ let originalFetch;
22
+ // Whether globalThis had a `fetch` at install time. If it didn't (older Node,
23
+ // custom runtimes), restore must DELETE our patch rather than leave it - the
24
+ // original state was "fetch is absent", and `originalFetch` being undefined
25
+ // must not be mistaken for "nothing to restore".
26
+ let fetchWasDefined = false;
27
+ let installed = false;
28
+ function urlOf(req) {
29
+ return req.url;
30
+ }
31
+ async function matches(handler, req) {
32
+ if (handler.consumed)
33
+ return false;
34
+ const m = handler.matcher;
35
+ if (typeof m === "string")
36
+ return urlOf(req) === m;
37
+ if (m instanceof RegExp)
38
+ return regexTest(m, urlOf(req));
39
+ if (typeof m === "function")
40
+ return Boolean(await m(req));
41
+ return false;
42
+ }
43
+ async function buildResponse(spec, req) {
44
+ let resolved;
45
+ if (typeof spec === "function") {
46
+ resolved = await spec(req);
47
+ }
48
+ else {
49
+ resolved = spec;
50
+ }
51
+ if (resolved instanceof Response)
52
+ return resolved;
53
+ const init = resolved;
54
+ let body = init.body;
55
+ const headers = new Headers(init.headers);
56
+ if (init.json !== undefined) {
57
+ body = JSON.stringify(init.json);
58
+ if (!headers.has("Content-Type")) {
59
+ headers.set("Content-Type", "application/json");
60
+ }
61
+ }
62
+ return new Response(body ?? null, {
63
+ status: init.status ?? 200,
64
+ statusText: init.statusText,
65
+ headers,
66
+ });
67
+ }
68
+ function patchedFetch(input, init) {
69
+ /**
70
+ * Normalize all call shapes - `fetch(url)`, `fetch(url, init)`,
71
+ * `fetch(new Request(...))`, `fetch(new Request(...), init)` into a
72
+ * single Request object that handlers can introspect.
73
+ */
74
+ const req = input instanceof Request
75
+ ? new Request(input, init)
76
+ : new Request(input, init);
77
+ calls.push(req);
78
+ return (async () => {
79
+ for (const handler of handlers) {
80
+ if (await matches(handler, req)) {
81
+ if (handler.once)
82
+ handler.consumed = true;
83
+ return buildResponse(handler.response, req);
84
+ }
85
+ }
86
+ throw new Error(`No mock.fetch handler matched: ${req.method} ${req.url}`);
87
+ })();
88
+ }
89
+ function install() {
90
+ if (installed)
91
+ return;
92
+ installed = true;
93
+ // `in` checks presence without triggering Node's lazy fetch getter.
94
+ fetchWasDefined = "fetch" in globalThis;
95
+ originalFetch = globalThis.fetch;
96
+ globalThis.fetch = patchedFetch;
97
+ activeRestorers.add(restoreFetch);
98
+ }
99
+ function restoreFetch() {
100
+ if (!installed)
101
+ return;
102
+ installed = false;
103
+ if (fetchWasDefined) {
104
+ globalThis.fetch = originalFetch;
105
+ }
106
+ else {
107
+ // fetch didn't exist before we patched it - remove our patch so the
108
+ // environment reads `undefined` again, rather than leaking the stale mock.
109
+ delete globalThis.fetch;
110
+ }
111
+ originalFetch = undefined;
112
+ fetchWasDefined = false;
113
+ handlers.length = 0;
114
+ calls.length = 0;
115
+ activeRestorers.delete(restoreFetch);
116
+ }
117
+ function register(matcher, response, once) {
118
+ install();
119
+ handlers.push({ matcher, response, once, consumed: false });
120
+ }
121
+ // Type guard used by toHaveFetched / toHaveFetchedTimes matchers.
122
+ export function isFetchMock(value) {
123
+ return (typeof value === "function" &&
124
+ value[FETCH_MOCK_BRAND] === true);
125
+ }
126
+ const mockFetchImpl = ((matcher, response) => register(matcher, response, false));
127
+ mockFetchImpl.once = (matcher, response) => register(matcher, response, true);
128
+ mockFetchImpl.reset = () => {
129
+ handlers.length = 0;
130
+ calls.length = 0;
131
+ };
132
+ mockFetchImpl.restore = restoreFetch;
133
+ Object.defineProperty(mockFetchImpl, "calls", {
134
+ // Return a snapshot copy, not the internal array, so callers can't mutate
135
+ // recorded state (e.g. push/splice) and corrupt later assertions.
136
+ get: () => calls.slice(),
137
+ enumerable: true,
138
+ configurable: false,
139
+ });
140
+ Object.defineProperty(mockFetchImpl, FETCH_MOCK_BRAND, {
141
+ value: true,
142
+ enumerable: false,
143
+ configurable: false,
144
+ writable: false,
145
+ });
146
+ export const mockFetch = mockFetchImpl;
147
+ export function mockFetchOnce(matcher, response) {
148
+ mockFetch.once(matcher, response);
149
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * File system mocking - `mock.fs()`.
3
+ *
4
+ * Backed by `memfs` today (private implementation detail behind the
5
+ * `FakeFsBackend` interface). The plan is to swap memfs for a hand-rolled
6
+ * package later; consumers of this module should only depend on the public
7
+ * `FakeFsHandle` surface, not on memfs types directly.
8
+ *
9
+ * Interception is layered on Tier A module mocking - `mock.doMock` is
10
+ * called for `node:fs` and `node:fs/promises`, so the fake takes effect
11
+ * when test code does `await import("node:fs")` or `await import("node:fs/promises")`.
12
+ * Static `import { readFileSync } from "node:fs"` is NOT intercepted in
13
+ * Tier A (planned for Tier B). See the docs page for the explicit limit
14
+ * list and workarounds.
15
+ */
16
+ /**
17
+ * `Volume.fromJSON()` accepts a path-to-content map. `null` represents an
18
+ * empty directory; strings are file contents.
19
+ */
20
+ export type FakeFsState = Record<string, string | null>;
21
+ /** Public handle returned by `mock.fs()`. */
22
+ export interface FakeFsHandle {
23
+ /** Dump the current state as a path-to-content map. */
24
+ toJSON(): FakeFsState;
25
+ /** Replace the current state with the given map. */
26
+ fromJSON(state: FakeFsState): void;
27
+ /** Read a file's contents, or null if it doesn't exist. */
28
+ read(path: string): string | null;
29
+ /** Write a file, creating parent directories as needed. */
30
+ write(path: string, content: string): void;
31
+ /** Check whether a path exists in the fake fs. */
32
+ exists(path: string): boolean;
33
+ /** Delete a file. Returns true if something was removed. */
34
+ delete(path: string): boolean;
35
+ /** List entries in a directory. Returns an empty array if missing. */
36
+ list(dir: string): string[];
37
+ /**
38
+ * Unpatch node:fs and node:fs/promises for this handle's interception.
39
+ * Idempotent, and a no-op if a later mock.fs() call has already superseded
40
+ * this handle - so holding an old handle for snapshotting can't tear down
41
+ * the current volume's interception.
42
+ */
43
+ restore(): void;
44
+ }
45
+ /**
46
+ * Install an in-memory file system as `node:fs` and `node:fs/promises`
47
+ * for the duration of the test. Subsequent `await import("node:fs")`
48
+ * calls return the fake.
49
+ *
50
+ * Calling `mock.fs()` while a fake is already installed swaps in a fresh
51
+ * volume - the previous handle's references stop receiving updates.
52
+ */
53
+ export declare function fs(initial?: FakeFsState): FakeFsHandle;
54
+ //# sourceMappingURL=fs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fs.d.ts","sourceRoot":"","sources":["../../../src/mock/fs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAOH;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC;AAqFxD,6CAA6C;AAC7C,MAAM,WAAW,YAAY;IAC3B,uDAAuD;IACvD,MAAM,IAAI,WAAW,CAAC;IACtB,oDAAoD;IACpD,QAAQ,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI,CAAC;IACnC,2DAA2D;IAC3D,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAClC,2DAA2D;IAC3D,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3C,kDAAkD;IAClD,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAC9B,4DAA4D;IAC5D,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAC9B,sEAAsE;IACtE,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC5B;;;;;OAKG;IACH,OAAO,IAAI,IAAI,CAAC;CACjB;AAeD;;;;;;;GAOG;AACH,wBAAgB,EAAE,CAAC,OAAO,CAAC,EAAE,WAAW,GAAG,YAAY,CAkCtD"}
@@ -0,0 +1,125 @@
1
+ /**
2
+ * File system mocking - `mock.fs()`.
3
+ *
4
+ * Backed by `memfs` today (private implementation detail behind the
5
+ * `FakeFsBackend` interface). The plan is to swap memfs for a hand-rolled
6
+ * package later; consumers of this module should only depend on the public
7
+ * `FakeFsHandle` surface, not on memfs types directly.
8
+ *
9
+ * Interception is layered on Tier A module mocking - `mock.doMock` is
10
+ * called for `node:fs` and `node:fs/promises`, so the fake takes effect
11
+ * when test code does `await import("node:fs")` or `await import("node:fs/promises")`.
12
+ * Static `import { readFileSync } from "node:fs"` is NOT intercepted in
13
+ * Tier A (planned for Tier B). See the docs page for the explicit limit
14
+ * list and workarounds.
15
+ */
16
+ import { Volume, createFsFromVolume } from "memfs";
17
+ import { doMock, unmock } from "./module.js";
18
+ import { activeRestorers } from "./spy.js";
19
+ /**
20
+ * The current implementation factory. Swap this single line when replacing
21
+ * memfs with a hand-rolled backend - no other code changes needed.
22
+ */
23
+ function createBackend(initial) {
24
+ const vol = Volume.fromJSON(initial ?? {});
25
+ const fsExports = createFsFromVolume(vol);
26
+ // memfs's createFsFromVolume returns the fs surface; .promises is on it.
27
+ const fsPromisesExports = fsExports
28
+ .promises;
29
+ return {
30
+ fsExports,
31
+ fsPromisesExports,
32
+ toJSON: () => vol.toJSON(),
33
+ fromJSON: (state) => {
34
+ vol.reset();
35
+ vol.fromJSON(state);
36
+ },
37
+ readFile: (path) => {
38
+ try {
39
+ return vol.readFileSync(path, "utf8");
40
+ }
41
+ catch {
42
+ return null;
43
+ }
44
+ },
45
+ writeFile: (path, content) => {
46
+ // memfs throws if the parent directory doesn't exist; create it for
47
+ // ergonomic test code.
48
+ const lastSlash = path.lastIndexOf("/");
49
+ if (lastSlash > 0) {
50
+ vol.mkdirSync(path.slice(0, lastSlash), { recursive: true });
51
+ }
52
+ vol.writeFileSync(path, content);
53
+ },
54
+ exists: (path) => vol.existsSync(path),
55
+ remove: (path) => {
56
+ try {
57
+ vol.unlinkSync(path);
58
+ return true;
59
+ }
60
+ catch {
61
+ return false;
62
+ }
63
+ },
64
+ list: (dir) => {
65
+ try {
66
+ return vol.readdirSync(dir);
67
+ }
68
+ catch {
69
+ return [];
70
+ }
71
+ },
72
+ };
73
+ }
74
+ // Process-global singleton: only one fake fs interception is active at a time.
75
+ // Safe under sequential per-file execution + afterEach(restoreAll); parallel
76
+ // per-file execution (see ISSUES.md) would need per-worker isolation.
77
+ let activeBackend;
78
+ const restoreFakeFs = () => {
79
+ if (!activeBackend)
80
+ return;
81
+ unmock("node:fs");
82
+ unmock("node:fs/promises");
83
+ activeBackend = undefined;
84
+ activeRestorers.delete(restoreFakeFs);
85
+ };
86
+ /**
87
+ * Install an in-memory file system as `node:fs` and `node:fs/promises`
88
+ * for the duration of the test. Subsequent `await import("node:fs")`
89
+ * calls return the fake.
90
+ *
91
+ * Calling `mock.fs()` while a fake is already installed swaps in a fresh
92
+ * volume - the previous handle's references stop receiving updates.
93
+ */
94
+ export function fs(initial) {
95
+ // Tear down any existing fake before installing the new one so the
96
+ // module-mocking registrations don't stack.
97
+ if (activeBackend) {
98
+ unmock("node:fs");
99
+ unmock("node:fs/promises");
100
+ }
101
+ const backend = createBackend(initial);
102
+ activeBackend = backend;
103
+ doMock("node:fs", () => backend.fsExports);
104
+ doMock("node:fs/promises", () => backend.fsPromisesExports);
105
+ activeRestorers.add(restoreFakeFs);
106
+ const handle = {
107
+ toJSON: () => backend.toJSON(),
108
+ fromJSON: (state) => backend.fromJSON(state),
109
+ read: (path) => backend.readFile(path),
110
+ write: (path, content) => backend.writeFile(path, content),
111
+ exists: (path) => backend.exists(path),
112
+ delete: (path) => backend.remove(path),
113
+ list: (dir) => backend.list(dir),
114
+ // Only tear down the interception if THIS handle still owns it. A later
115
+ // mock.fs() call orphans this handle - its volume lives on for snapshot
116
+ // inspection, but restoring it must not rip out the newer volume's
117
+ // interception. On an orphaned handle this is a no-op (also makes restore
118
+ // idempotent: a second call finds activeBackend already cleared).
119
+ restore: () => {
120
+ if (activeBackend === backend)
121
+ restoreFakeFs();
122
+ },
123
+ };
124
+ return handle;
125
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * `mock` namespace - single entry point for all mocking utilities.
3
+ *
4
+ * Both forms are supported:
5
+ * import { mock } from "unrift";
6
+ * mock.fn();
7
+ * mock.spyOn(obj, "method");
8
+ *
9
+ * import { spy, spyOn } from "unrift";
10
+ * spy();
11
+ *
12
+ * They produce identical objects.
13
+ */
14
+ import { spy, spyOn, isSpy, clearAllMocks, resetAllMocks, restoreAllMocks } from "./spy";
15
+ import { mockObject, mockClass } from "./auto";
16
+ import { stub, mockGlobal } from "./stub";
17
+ import { mockFetch, mockFetchOnce } from "./fetch";
18
+ import { useFakeTimers, useRealTimers, advanceTimersByTime, advanceTimersByTimeAsync, runAllTimers, runAllTimersAsync, runOnlyPendingTimers, runOnlyPendingTimersAsync, getTimerCount, setSystemTime, getRealSystemTime } from "./timers";
19
+ import { doMock, unmock, listMockedSpecs, fromModule } from "./module";
20
+ import { fs as mockFs } from "./fs";
21
+ export { spy, spyOn, isSpy, mockObject, mockClass, stub, mockGlobal, mockFetch, mockFetchOnce, useFakeTimers, useRealTimers, advanceTimersByTime, advanceTimersByTimeAsync, runAllTimers, runAllTimersAsync, runOnlyPendingTimers, runOnlyPendingTimersAsync, getTimerCount, setSystemTime, getRealSystemTime, doMock, unmock, listMockedSpecs, fromModule, mockFs, clearAllMocks, resetAllMocks, restoreAllMocks, };
22
+ export type { Spy, MockState, MockResult, AnyFn } from "./spy";
23
+ export type { MockFetch, FetchMatcher, MockResponseInit, MockFetchResponse, } from "./fetch";
24
+ export type { UseFakeTimersOptions, FakeableApi } from "./timers";
25
+ export type { LoaderMessage } from "./module";
26
+ export type { FakeFsHandle, FakeFsState } from "./fs";
27
+ /**
28
+ * Unified mock namespace. Mirrors Jest / Vitest naming so existing test
29
+ * code reads naturally:
30
+ *
31
+ * mock.fn(impl?) // spy()
32
+ * mock.spyOn(obj, key)
33
+ * mock.object(obj) // auto-spy every method
34
+ * mock.class(Ctor) // auto-spy every method on instances
35
+ * mock.global(key, value) // stub on globalThis
36
+ * mock.stub(target, key, value)
37
+ * mock.clearAll()
38
+ * mock.resetAll()
39
+ * mock.restoreAll()
40
+ */
41
+ export declare const mock: {
42
+ readonly fn: typeof spy;
43
+ readonly spyOn: typeof spyOn;
44
+ readonly object: typeof mockObject;
45
+ readonly class: typeof mockClass;
46
+ readonly global: typeof mockGlobal;
47
+ readonly stub: typeof stub;
48
+ readonly fetch: import("./fetch").MockFetch;
49
+ readonly fetchOnce: typeof mockFetchOnce;
50
+ readonly useFakeTimers: typeof useFakeTimers;
51
+ readonly useRealTimers: typeof useRealTimers;
52
+ readonly advanceTimersByTime: typeof advanceTimersByTime;
53
+ readonly advanceTimersByTimeAsync: typeof advanceTimersByTimeAsync;
54
+ readonly runAllTimers: typeof runAllTimers;
55
+ readonly runAllTimersAsync: typeof runAllTimersAsync;
56
+ readonly runOnlyPendingTimers: typeof runOnlyPendingTimers;
57
+ readonly runOnlyPendingTimersAsync: typeof runOnlyPendingTimersAsync;
58
+ readonly getTimerCount: typeof getTimerCount;
59
+ readonly setSystemTime: typeof setSystemTime;
60
+ readonly getRealSystemTime: typeof getRealSystemTime;
61
+ readonly doMock: typeof doMock;
62
+ readonly unmock: typeof unmock;
63
+ readonly fromModule: typeof fromModule;
64
+ readonly listMockedSpecs: typeof listMockedSpecs;
65
+ readonly fs: typeof mockFs;
66
+ readonly clearAll: typeof clearAllMocks;
67
+ readonly resetAll: typeof resetAllMocks;
68
+ readonly restoreAll: typeof restoreAllMocks;
69
+ };
70
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/mock/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EACL,GAAG,EACH,KAAK,EACL,KAAK,EACL,aAAa,EACb,aAAa,EACb,eAAe,EAChB,MAAM,OAAO,CAAC;AACf,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AAC/C,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAC1C,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EACL,aAAa,EACb,aAAa,EACb,mBAAmB,EACnB,wBAAwB,EACxB,YAAY,EACZ,iBAAiB,EACjB,oBAAoB,EACpB,yBAAyB,EACzB,aAAa,EACb,aAAa,EACb,iBAAiB,EAClB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACvE,OAAO,EAAE,EAAE,IAAI,MAAM,EAAE,MAAM,MAAM,CAAC;AAEpC,OAAO,EACL,GAAG,EACH,KAAK,EACL,KAAK,EACL,UAAU,EACV,SAAS,EACT,IAAI,EACJ,UAAU,EACV,SAAS,EACT,aAAa,EACb,aAAa,EACb,aAAa,EACb,mBAAmB,EACnB,wBAAwB,EACxB,YAAY,EACZ,iBAAiB,EACjB,oBAAoB,EACpB,yBAAyB,EACzB,aAAa,EACb,aAAa,EACb,iBAAiB,EACjB,MAAM,EACN,MAAM,EACN,eAAe,EACf,UAAU,EACV,MAAM,EACN,aAAa,EACb,aAAa,EACb,eAAe,GAChB,CAAC;AAEF,YAAY,EAAE,GAAG,EAAE,SAAS,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AAC/D,YAAY,EACV,SAAS,EACT,YAAY,EACZ,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,SAAS,CAAC;AACjB,YAAY,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAClE,YAAY,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAC9C,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,MAAM,CAAC;AAEtD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkCP,CAAC"}
@@ -0,0 +1,70 @@
1
+ /**
2
+ * `mock` namespace - single entry point for all mocking utilities.
3
+ *
4
+ * Both forms are supported:
5
+ * import { mock } from "unrift";
6
+ * mock.fn();
7
+ * mock.spyOn(obj, "method");
8
+ *
9
+ * import { spy, spyOn } from "unrift";
10
+ * spy();
11
+ *
12
+ * They produce identical objects.
13
+ */
14
+ import { spy, spyOn, isSpy, clearAllMocks, resetAllMocks, restoreAllMocks, } from "./spy.js";
15
+ import { mockObject, mockClass } from "./auto.js";
16
+ import { stub, mockGlobal } from "./stub.js";
17
+ import { mockFetch, mockFetchOnce } from "./fetch.js";
18
+ import { useFakeTimers, useRealTimers, advanceTimersByTime, advanceTimersByTimeAsync, runAllTimers, runAllTimersAsync, runOnlyPendingTimers, runOnlyPendingTimersAsync, getTimerCount, setSystemTime, getRealSystemTime, } from "./timers.js";
19
+ import { doMock, unmock, listMockedSpecs, fromModule } from "./module.js";
20
+ import { fs as mockFs } from "./fs.js";
21
+ export { spy, spyOn, isSpy, mockObject, mockClass, stub, mockGlobal, mockFetch, mockFetchOnce, useFakeTimers, useRealTimers, advanceTimersByTime, advanceTimersByTimeAsync, runAllTimers, runAllTimersAsync, runOnlyPendingTimers, runOnlyPendingTimersAsync, getTimerCount, setSystemTime, getRealSystemTime, doMock, unmock, listMockedSpecs, fromModule, mockFs, clearAllMocks, resetAllMocks, restoreAllMocks, };
22
+ /**
23
+ * Unified mock namespace. Mirrors Jest / Vitest naming so existing test
24
+ * code reads naturally:
25
+ *
26
+ * mock.fn(impl?) // spy()
27
+ * mock.spyOn(obj, key)
28
+ * mock.object(obj) // auto-spy every method
29
+ * mock.class(Ctor) // auto-spy every method on instances
30
+ * mock.global(key, value) // stub on globalThis
31
+ * mock.stub(target, key, value)
32
+ * mock.clearAll()
33
+ * mock.resetAll()
34
+ * mock.restoreAll()
35
+ */
36
+ export const mock = {
37
+ fn: spy,
38
+ spyOn,
39
+ object: mockObject,
40
+ class: mockClass,
41
+ global: mockGlobal,
42
+ stub,
43
+ fetch: mockFetch,
44
+ fetchOnce: mockFetchOnce,
45
+ useFakeTimers,
46
+ useRealTimers,
47
+ advanceTimersByTime,
48
+ advanceTimersByTimeAsync,
49
+ runAllTimers,
50
+ runAllTimersAsync,
51
+ runOnlyPendingTimers,
52
+ runOnlyPendingTimersAsync,
53
+ getTimerCount,
54
+ setSystemTime,
55
+ getRealSystemTime,
56
+ // Tier A module mocking - dynamic imports of external specs only.
57
+ // The hoisted `mock.module(spec, factory)` for static imports is
58
+ // planned for Tier B and intentionally not exposed yet.
59
+ doMock,
60
+ unmock,
61
+ fromModule,
62
+ listMockedSpecs,
63
+ // File system mocking - backed by memfs, layered on Tier A module
64
+ // mocking. Works with `await import("node:fs")`; static imports of
65
+ // node:fs are not intercepted yet (planned for Tier B).
66
+ fs: mockFs,
67
+ clearAll: clearAllMocks,
68
+ resetAll: resetAllMocks,
69
+ restoreAll: restoreAllMocks,
70
+ };
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Node module customization hook for unrift module mocking (Tier A).
3
+ *
4
+ * Registered by `bin/unrift.mjs` via `module.register()`. Runs in a
5
+ * worker thread (Node 20.6+ / 18.19+ behavior). Communicates with the
6
+ * main thread via a `MessagePort` to learn which specs have been mocked.
7
+ *
8
+ * Why we use `receiveMessageOnPort()` instead of `port.on("message")`:
9
+ * the resolve hook runs synchronously from the perspective of the
10
+ * import pipeline, but Worker `MessagePort` listeners are async.
11
+ * `receiveMessageOnPort()` (Node 18.7+) drains pending messages
12
+ * synchronously so registrations posted from the main thread are
13
+ * visible by the time the corresponding `await import(spec)` reaches
14
+ * us.
15
+ *
16
+ * The generated synthetic module source runs back in the MAIN thread
17
+ * (Node's loader output is evaluated in the importing thread, not the
18
+ * loader thread). That source reads exports from `globalThis.__unriftMocks`,
19
+ * which the main thread populates in `mock.doMock()`.
20
+ *
21
+ * Source compiled by tsc and shipped at `dist/esm/mock/module-loader.js`.
22
+ */
23
+ import { type MessagePort } from "node:worker_threads";
24
+ interface InitializeData {
25
+ port: MessagePort;
26
+ }
27
+ /**
28
+ * Generate ESM source for a mocked spec. The source executes in the
29
+ * MAIN thread (not the loader thread) - that's where the actual mock
30
+ * object lives, keyed in `globalThis.__unriftMocks`.
31
+ *
32
+ * Pure function so future tiers can reuse this for hoisted (Tier B)
33
+ * and relative (Tier C) mocks without rewriting the emitter.
34
+ */
35
+ export declare function generateMockSource(spec: string, exportKeys: readonly string[]): string;
36
+ /** Node hook: called once when the loader is registered. */
37
+ export declare function initialize(data: InitializeData): void;
38
+ interface ResolveContext {
39
+ conditions: readonly string[];
40
+ importAttributes?: Record<string, string>;
41
+ parentURL?: string;
42
+ }
43
+ interface ResolveResult {
44
+ url: string;
45
+ format?: string;
46
+ shortCircuit?: boolean;
47
+ importAttributes?: Record<string, string>;
48
+ }
49
+ type DefaultResolve = (specifier: string, context: ResolveContext) => ResolveResult | Promise<ResolveResult>;
50
+ /** Node hook: short-circuit mocked specs to a `unrift-mock:` URL. */
51
+ export declare function resolve(specifier: string, context: ResolveContext, nextResolve: DefaultResolve): Promise<ResolveResult>;
52
+ interface LoadContext {
53
+ format?: string;
54
+ importAttributes?: Record<string, string>;
55
+ conditions?: readonly string[];
56
+ }
57
+ interface LoadResult {
58
+ format: string;
59
+ source?: string | ArrayBuffer | Uint8Array;
60
+ shortCircuit?: boolean;
61
+ }
62
+ type DefaultLoad = (url: string, context: LoadContext) => LoadResult | Promise<LoadResult>;
63
+ /** Node hook: emit synthetic ESM source for mocked specs. */
64
+ export declare function load(url: string, context: LoadContext, nextLoad: DefaultLoad): Promise<LoadResult>;
65
+ export {};
66
+ //# sourceMappingURL=module-loader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"module-loader.d.ts","sourceRoot":"","sources":["../../../src/mock/module-loader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAwB,KAAK,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAQ7E,UAAU,cAAc;IACtB,IAAI,EAAE,WAAW,CAAC;CACnB;AAmCD;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,SAAS,MAAM,EAAE,GAC5B,MAAM,CAuBR;AAED,4DAA4D;AAC5D,wBAAgB,UAAU,CAAC,IAAI,EAAE,cAAc,GAAG,IAAI,CAErD;AAED,UAAU,cAAc;IACtB,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9B,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1C,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,UAAU,aAAa;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC3C;AAED,KAAK,cAAc,GAAG,CACpB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,cAAc,KACpB,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;AAE5C,qEAAqE;AACrE,wBAAsB,OAAO,CAC3B,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,cAAc,EACvB,WAAW,EAAE,cAAc,GAC1B,OAAO,CAAC,aAAa,CAAC,CAexB;AAED,UAAU,WAAW;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1C,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAChC;AAED,UAAU,UAAU;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,UAAU,CAAC;IAC3C,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,KAAK,WAAW,GAAG,CACjB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,WAAW,KACjB,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;AAEtC,6DAA6D;AAC7D,wBAAsB,IAAI,CACxB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,WAAW,EACpB,QAAQ,EAAE,WAAW,GACpB,OAAO,CAAC,UAAU,CAAC,CA0BrB"}
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Node module customization hook for unrift module mocking (Tier A).
3
+ *
4
+ * Registered by `bin/unrift.mjs` via `module.register()`. Runs in a
5
+ * worker thread (Node 20.6+ / 18.19+ behavior). Communicates with the
6
+ * main thread via a `MessagePort` to learn which specs have been mocked.
7
+ *
8
+ * Why we use `receiveMessageOnPort()` instead of `port.on("message")`:
9
+ * the resolve hook runs synchronously from the perspective of the
10
+ * import pipeline, but Worker `MessagePort` listeners are async.
11
+ * `receiveMessageOnPort()` (Node 18.7+) drains pending messages
12
+ * synchronously so registrations posted from the main thread are
13
+ * visible by the time the corresponding `await import(spec)` reaches
14
+ * us.
15
+ *
16
+ * The generated synthetic module source runs back in the MAIN thread
17
+ * (Node's loader output is evaluated in the importing thread, not the
18
+ * loader thread). That source reads exports from `globalThis.__unriftMocks`,
19
+ * which the main thread populates in `mock.doMock()`.
20
+ *
21
+ * Source compiled by tsc and shipped at `dist/esm/mock/module-loader.js`.
22
+ */
23
+ import { receiveMessageOnPort } from "node:worker_threads";
24
+ import { MOCKS_GLOBAL_KEY, MOCK_URL_PREFIX, } from "./module.js";
25
+ // Loader-thread state. Populated by `initialize()` and updated by
26
+ // drainMessages() on every resolve() call.
27
+ let loaderPort;
28
+ const registered = new Map();
29
+ /**
30
+ * Pull every pending message off the port synchronously. Called at the
31
+ * top of every resolve() and load() hook so the loader's view of the
32
+ * registry is always current.
33
+ */
34
+ function drainMessages() {
35
+ if (!loaderPort)
36
+ return;
37
+ let received = receiveMessageOnPort(loaderPort);
38
+ while (received !== undefined) {
39
+ const message = received.message;
40
+ if (message.type === "register") {
41
+ registered.set(message.spec, {
42
+ registrationId: message.registrationId,
43
+ exportKeys: message.exportKeys,
44
+ });
45
+ }
46
+ else if (message.type === "unregister") {
47
+ registered.delete(message.spec);
48
+ }
49
+ received = receiveMessageOnPort(loaderPort);
50
+ }
51
+ }
52
+ /**
53
+ * Generate ESM source for a mocked spec. The source executes in the
54
+ * MAIN thread (not the loader thread) - that's where the actual mock
55
+ * object lives, keyed in `globalThis.__unriftMocks`.
56
+ *
57
+ * Pure function so future tiers can reuse this for hoisted (Tier B)
58
+ * and relative (Tier C) mocks without rewriting the emitter.
59
+ */
60
+ export function generateMockSource(spec, exportKeys) {
61
+ const specJson = JSON.stringify(spec);
62
+ const lines = [
63
+ `// Generated by unrift module mocking - do not edit.`,
64
+ `const __mocks = globalThis[${JSON.stringify(MOCKS_GLOBAL_KEY)}];`,
65
+ `if (!__mocks || !__mocks.has(${specJson})) {`,
66
+ ` throw new Error(`,
67
+ ` "Unrift module mock registry is missing the entry for " + ${specJson} + ". " +`,
68
+ ` "This usually means mock.unmock() ran between registration and import."`,
69
+ ` );`,
70
+ `}`,
71
+ `const __exports = __mocks.get(${specJson});`,
72
+ ];
73
+ for (const key of exportKeys) {
74
+ if (key === "default") {
75
+ lines.push(`export default __exports.default;`);
76
+ }
77
+ else {
78
+ lines.push(`export const ${key} = __exports[${JSON.stringify(key)}];`);
79
+ }
80
+ }
81
+ return lines.join("\n");
82
+ }
83
+ /** Node hook: called once when the loader is registered. */
84
+ export function initialize(data) {
85
+ loaderPort = data.port;
86
+ }
87
+ /** Node hook: short-circuit mocked specs to a `unrift-mock:` URL. */
88
+ export async function resolve(specifier, context, nextResolve) {
89
+ drainMessages();
90
+ const entry = registered.get(specifier);
91
+ if (entry) {
92
+ // Embed the registration ID in the URL so Node's URL-keyed module
93
+ // cache treats each re-mock of the same spec as a fresh module.
94
+ return {
95
+ url: `${MOCK_URL_PREFIX}${specifier}?v=${entry.registrationId}`,
96
+ format: "module",
97
+ shortCircuit: true,
98
+ };
99
+ }
100
+ return nextResolve(specifier, context);
101
+ }
102
+ /** Node hook: emit synthetic ESM source for mocked specs. */
103
+ export async function load(url, context, nextLoad) {
104
+ if (url.startsWith(MOCK_URL_PREFIX)) {
105
+ drainMessages();
106
+ // URL shape: `unrift-mock:<spec>?v=<registrationId>`
107
+ const withoutPrefix = url.slice(MOCK_URL_PREFIX.length);
108
+ const queryIdx = withoutPrefix.indexOf("?");
109
+ const spec = queryIdx === -1 ? withoutPrefix : withoutPrefix.slice(0, queryIdx);
110
+ const entry = registered.get(spec);
111
+ if (!entry) {
112
+ // The user called mock.unmock() between resolve() and load().
113
+ // Fall through to the real module so the import still works.
114
+ return nextLoad(spec, context);
115
+ }
116
+ return {
117
+ format: "module",
118
+ source: generateMockSource(spec, entry.exportKeys),
119
+ shortCircuit: true,
120
+ };
121
+ }
122
+ return nextLoad(url, context);
123
+ }