@evolu/nodejs 3.1.0 → 4.0.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.
package/README.md CHANGED
@@ -1,11 +1,20 @@
1
1
  # Evolu for Node.js
2
2
 
3
- This package provides Evolu for [Node.js](https://nodejs.org) 24+.
3
+ This package provides Evolu for [Node.js](https://nodejs.org) 24.20+.
4
4
 
5
5
  ## Documentation
6
6
 
7
7
  For detailed information and usage examples, please visit [evolu.dev](https://www.evolu.dev).
8
8
 
9
+ ## Test overview reporter
10
+
11
+ `@evolu/nodejs/TestOverviewReporter` lists Node.js test files slowest-first while
12
+ preserving native failure diagnostics, run totals, and coverage output:
13
+
14
+ ```sh
15
+ node --test --test-reporter=@evolu/nodejs/TestOverviewReporter
16
+ ```
17
+
9
18
  ## Community
10
19
 
11
20
  The Evolu community is on [GitHub Discussions](https://github.com/evoluhq/evolu/discussions), where you can ask questions and voice ideas.
@@ -0,0 +1,63 @@
1
+ /**
2
+ * File system operations using Node.js.
3
+ *
4
+ * @module
5
+ */
6
+ import { type Fs } from "@evolu/common";
7
+ /**
8
+ * Creates a {@link Fs} backed by `node:fs/promises`.
9
+ *
10
+ * `readFile` and `writeFile` pass the Run's abort signal to Node and propagate
11
+ * the Run's abort reason if Node rejects after cancellation. Cancellation can
12
+ * leave a write partially completed. Successful operations return their values
13
+ * even if an abort was requested. Other operations run to completion and return
14
+ * their results once started, so callers also receive created temporary
15
+ * directories and can dispose them.
16
+ *
17
+ * Temporary directory parents are resolved through the file system, preserving
18
+ * the meaning of symbolic links followed by `..`. Returned paths are absolute,
19
+ * so cleanup still targets the created directory after a change to the
20
+ * process's working directory.
21
+ *
22
+ * ### Example
23
+ *
24
+ * ```ts
25
+ * import {
26
+ * assertEqual,
27
+ * assertFalse,
28
+ * ok,
29
+ * type FsDep,
30
+ * type FsError,
31
+ * type Task,
32
+ * } from "@evolu/common";
33
+ * import { createNodeFs, runMain } from "@evolu/nodejs";
34
+ * import { join } from "node:path";
35
+ *
36
+ * const main: Task<void, FsError, FsDep> = async (run) => {
37
+ * const { fs } = run.deps;
38
+ * const temp = await run(fs.createTempDirectory({ prefix: "evolu-" }));
39
+ * if (!temp.ok) return temp;
40
+ *
41
+ * {
42
+ * await using directory = temp.value;
43
+ * const path = join(directory.path, "config.json");
44
+ *
45
+ * const result = await run(fs.writeFile(path, '{ "port": 4000 }'));
46
+ * if (!result.ok) return result;
47
+ *
48
+ * const text = await run(fs.readFile(path, "utf8"));
49
+ * if (!text.ok) return text;
50
+ * assertEqual(text.value, '{ "port": 4000 }');
51
+ * }
52
+ *
53
+ * const exists = await run(fs.exists(temp.value.path));
54
+ * if (!exists.ok) return exists;
55
+ * assertFalse(exists.value);
56
+ * return ok();
57
+ * };
58
+ *
59
+ * await runMain({ fs: createNodeFs() }, { mode: "command" })(main);
60
+ * ```
61
+ */
62
+ export declare const createNodeFs: () => Fs;
63
+ //# sourceMappingURL=Fs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Fs.d.ts","sourceRoot":"","sources":["../../src/Fs.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAOL,KAAK,EAAE,EAQR,MAAM,eAAe,CAAC;AAmBvB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AACH,eAAO,MAAM,YAAY,QAAO,EAgI/B,CAAC"}
package/dist/src/Fs.js ADDED
@@ -0,0 +1,217 @@
1
+ /**
2
+ * File system operations using Node.js.
3
+ *
4
+ * @module
5
+ */
6
+ import { assert, ByteLength, disposable, err, ok, tryAsync, } from "@evolu/common";
7
+ import { constants } from "node:fs";
8
+ import { access, copyFile, cp, mkdir, mkdtemp, readdir, readFile, realpath, rename, rm, stat, writeFile, } from "node:fs/promises";
9
+ import { tmpdir } from "node:os";
10
+ import { join, sep } from "node:path";
11
+ /**
12
+ * Creates a {@link Fs} backed by `node:fs/promises`.
13
+ *
14
+ * `readFile` and `writeFile` pass the Run's abort signal to Node and propagate
15
+ * the Run's abort reason if Node rejects after cancellation. Cancellation can
16
+ * leave a write partially completed. Successful operations return their values
17
+ * even if an abort was requested. Other operations run to completion and return
18
+ * their results once started, so callers also receive created temporary
19
+ * directories and can dispose them.
20
+ *
21
+ * Temporary directory parents are resolved through the file system, preserving
22
+ * the meaning of symbolic links followed by `..`. Returned paths are absolute,
23
+ * so cleanup still targets the created directory after a change to the
24
+ * process's working directory.
25
+ *
26
+ * ### Example
27
+ *
28
+ * ```ts
29
+ * import {
30
+ * assertEqual,
31
+ * assertFalse,
32
+ * ok,
33
+ * type FsDep,
34
+ * type FsError,
35
+ * type Task,
36
+ * } from "@evolu/common";
37
+ * import { createNodeFs, runMain } from "@evolu/nodejs";
38
+ * import { join } from "node:path";
39
+ *
40
+ * const main: Task<void, FsError, FsDep> = async (run) => {
41
+ * const { fs } = run.deps;
42
+ * const temp = await run(fs.createTempDirectory({ prefix: "evolu-" }));
43
+ * if (!temp.ok) return temp;
44
+ *
45
+ * {
46
+ * await using directory = temp.value;
47
+ * const path = join(directory.path, "config.json");
48
+ *
49
+ * const result = await run(fs.writeFile(path, '{ "port": 4000 }'));
50
+ * if (!result.ok) return result;
51
+ *
52
+ * const text = await run(fs.readFile(path, "utf8"));
53
+ * if (!text.ok) return text;
54
+ * assertEqual(text.value, '{ "port": 4000 }');
55
+ * }
56
+ *
57
+ * const exists = await run(fs.exists(temp.value.path));
58
+ * if (!exists.ok) return exists;
59
+ * assertFalse(exists.value);
60
+ * return ok();
61
+ * };
62
+ *
63
+ * await runMain({ fs: createNodeFs() }, { mode: "command" })(main);
64
+ * ```
65
+ */
66
+ export const createNodeFs = () => {
67
+ function readFileTask(path, encoding) {
68
+ return async (run) => {
69
+ const { signal } = run;
70
+ const result = await tryAsync(() => encoding === undefined
71
+ ? readFile(path, { signal })
72
+ : readFile(path, { ...toEncodingOptions(encoding), signal }));
73
+ if (result.ok)
74
+ return result;
75
+ signal.throwIfAborted();
76
+ return err(createFsError("readFile", path, result.error));
77
+ };
78
+ }
79
+ return {
80
+ readFile: readFileTask,
81
+ writeFile: (path, data, options) => async (run) => {
82
+ const { signal } = run;
83
+ const result = await tryAsync(() => writeFile(path, data, { ...options, signal }));
84
+ if (result.ok)
85
+ return result;
86
+ signal.throwIfAborted();
87
+ return err(createFsError("writeFile", path, result.error));
88
+ },
89
+ readDirectory: (path, options) => async () => {
90
+ const result = await tryAsync(() => readdir(path, options));
91
+ return result.ok
92
+ ? result
93
+ : err(createFsError("readDirectory", path, result.error));
94
+ },
95
+ createDirectory: (path, options) => async () => {
96
+ const result = await tryAsync(async () => {
97
+ await mkdir(path, options);
98
+ });
99
+ return result.ok
100
+ ? ok()
101
+ : err(createFsError("createDirectory", path, result.error));
102
+ },
103
+ copy: (source, destination, options) => async () => {
104
+ const result = await tryAsync(() => cp(source, destination, { ...options, recursive: true }));
105
+ return result.ok
106
+ ? ok()
107
+ : err(createFsError("copy", source, result.error, destination));
108
+ },
109
+ copyFile: (source, destination, { overwrite = false } = {}) => async () => {
110
+ const result = await tryAsync(() => copyFile(source, destination, overwrite ? 0 : constants.COPYFILE_EXCL));
111
+ return result.ok
112
+ ? ok()
113
+ : err(createFsError("copyFile", source, result.error, destination));
114
+ },
115
+ rename: (source, destination) => async () => {
116
+ const result = await tryAsync(() => rename(source, destination));
117
+ return result.ok
118
+ ? ok()
119
+ : err(createFsError("rename", source, result.error, destination));
120
+ },
121
+ remove: (path, options) => async () => {
122
+ const result = await tryAsync(() => rm(path, options));
123
+ return result.ok
124
+ ? ok()
125
+ : err(createFsError("remove", path, result.error));
126
+ },
127
+ getMetadata: (path) => async () => {
128
+ const result = await tryAsync(() => stat(path));
129
+ return result.ok
130
+ ? ok(statsToFsMetadata(result.value))
131
+ : err(createFsError("getMetadata", path, result.error));
132
+ },
133
+ exists: (path) => async () => {
134
+ const result = await tryAsync(() => access(path));
135
+ if (result.ok)
136
+ return ok(true);
137
+ const error = createFsError("exists", path, result.error);
138
+ return error.reason === "NotFound" ? ok(false) : err(error);
139
+ },
140
+ createTempDirectory: ({ directory, prefix = "" } = {}) => async () => {
141
+ const parent = directory ?? tmpdir();
142
+ const resolvedParent = await tryAsync(() => realpath(parent || "."), (error) => createFsError("createTempDirectory", parent, error));
143
+ if (!resolvedParent.ok)
144
+ return resolvedParent;
145
+ // Keep the parent separator even when the name prefix is empty.
146
+ const pathPrefix = join(resolvedParent.value, sep) + prefix;
147
+ const result = await tryAsync(() => mkdtemp(pathPrefix), (error) => createFsError("createTempDirectory", pathPrefix, error));
148
+ if (!result.ok)
149
+ return result;
150
+ const path = result.value;
151
+ const disposer = new AsyncDisposableStack();
152
+ disposer.defer(() => rm(path, { recursive: true, force: true }));
153
+ return ok(disposable({ path }, disposer));
154
+ },
155
+ };
156
+ };
157
+ const toEncodingOptions = (encoding) => typeof encoding === "string" ? { encoding } : encoding;
158
+ const fsErrorReasonByCode = {
159
+ ENOENT: "NotFound",
160
+ EEXIST: "AlreadyExists",
161
+ EACCES: "PermissionDenied",
162
+ EPERM: "PermissionDenied",
163
+ EISDIR: "IsDirectory",
164
+ ERR_FS_EISDIR: "IsDirectory",
165
+ ERR_FS_CP_EEXIST: "AlreadyExists",
166
+ ERR_FS_CP_NON_DIR_TO_DIR: "IsDirectory",
167
+ ERR_FS_CP_DIR_TO_NON_DIR: "NotDirectory",
168
+ ENOTDIR: "NotDirectory",
169
+ ENOTEMPTY: "NotEmpty",
170
+ EBUSY: "Busy",
171
+ };
172
+ const createFsError = (method, path, error, destination) => {
173
+ assert(error instanceof Error, "Node fs rejects with an Error.");
174
+ const { code, syscall } = error;
175
+ return {
176
+ type: "FsError",
177
+ reason: fsErrorReasonByCode[String(code)] ?? "Unknown",
178
+ path: typeof path === "string" ? path : path.href,
179
+ ...(destination === undefined
180
+ ? {}
181
+ : {
182
+ destination: typeof destination === "string" ? destination : destination.href,
183
+ }),
184
+ syscall: syscall ?? method,
185
+ message: error.message,
186
+ };
187
+ };
188
+ const fsEntryTypeByMode = {
189
+ [constants.S_IFREG]: "File",
190
+ [constants.S_IFDIR]: "Directory",
191
+ [constants.S_IFLNK]: "SymbolicLink",
192
+ [constants.S_IFBLK]: "BlockDevice",
193
+ [constants.S_IFCHR]: "CharacterDevice",
194
+ [constants.S_IFIFO]: "FIFO",
195
+ [constants.S_IFSOCK]: "Socket",
196
+ };
197
+ const statsToFsMetadata = (stats) => ({
198
+ type: fsEntryTypeByMode[stats.mode & constants.S_IFMT] ?? "Unknown",
199
+ dev: stats.dev,
200
+ ino: stats.ino,
201
+ mode: stats.mode,
202
+ nlink: stats.nlink,
203
+ uid: stats.uid,
204
+ gid: stats.gid,
205
+ rdev: stats.rdev,
206
+ size: ByteLength.orThrow(stats.size),
207
+ blksize: stats.blksize,
208
+ blocks: stats.blocks,
209
+ atimeMs: stats.atimeMs,
210
+ mtimeMs: stats.mtimeMs,
211
+ ctimeMs: stats.ctimeMs,
212
+ birthtimeMs: stats.birthtimeMs,
213
+ atime: stats.atime,
214
+ mtime: stats.mtime,
215
+ ctime: stats.ctime,
216
+ birthtime: stats.birthtime,
217
+ });
@@ -22,69 +22,82 @@ export interface RunMainOptions {
22
22
  * How termination signals affect the process exit status.
23
23
  *
24
24
  * Services treat a gracefully handled signal as a successful shutdown.
25
- * Commands use the conventional `128 + signal number` exit status unless a
26
- * reported defect has already set a failure status.
25
+ * Commands use the conventional `128 + signal number` exit status unless
26
+ * `process.exitCode` is already set.
27
27
  *
28
28
  * @default "service"
29
29
  */
30
30
  readonly mode?: RunMainMode;
31
31
  }
32
32
  /**
33
- * Runs the main Task as the Node.js program lifecycle.
33
+ * Runs the main {@link Task} of a Node.js command or service.
34
34
  *
35
- * Creates one root {@link Run} and aborts it on:
35
+ * A command does a finite job, such as importing data. A service handles
36
+ * ongoing work, such as accepting connections. Choose a mode for how the
37
+ * program reports an interruption:
36
38
  *
37
- * - `SIGINT`: Ctrl-C on all platforms.
38
- * - `SIGTERM`: OS, service, Docker, or Kubernetes termination on Unix.
39
- * - `SIGBREAK`: Ctrl-Break on Windows.
39
+ * - `"command"`: A termination signal means the job was interrupted. After
40
+ * cleanup, use the conventional signal exit status, such as 130 for Ctrl-C,
41
+ * unless `process.exitCode` is already set.
42
+ * - `"service"` (default): A termination signal is an expected way to stop the
43
+ * service. Graceful shutdown does not set a failure exit status.
40
44
  *
41
- * The first signal logs shutdown progress, aborts the root Run, and waits for
42
- * the main Task and structured cleanup to finish. A subsequent signal exits
43
- * immediately with its conventional signal status, abandoning cleanup. A signal
44
- * received during final cleanup still applies signal shutdown behavior.
45
+ * Both modes create one root {@link Run} and wait for cleanup. The mode controls
46
+ * signal exit status; the main Task controls how long `runMain` waits.
45
47
  *
46
- * A main Task returning {@link Resource} keeps the program running until a
47
- * termination signal and is disposed during shutdown. A main Task returning
48
- * `void` completes the program immediately. A Resource result transfers
49
- * ownership of a live resource that must remain valid after its creating Task
50
- * settles.
48
+ * ### Lifetime
51
49
  *
52
- * Service mode treats graceful signal shutdown as successful. Command mode
53
- * preserves conventional signal exit statuses. Every defect reported through
54
- * `reportDefect`, including an observer defect that does not abort the Run,
55
- * sets `process.exitCode` to 1. The default reporter logs to the configured
50
+ * When the main Task returns `void`, `runMain` finishes after the Task and root
51
+ * Run cleanup. It does not force the Node.js process to exit.
52
+ *
53
+ * A service can return a live {@link Resource}, such as the relay created by
54
+ * {@link createRelay}. This transfers ownership to `runMain`, which waits for
55
+ * shutdown and then disposes the Resource. It must remain usable after the
56
+ * creating Task finishes; do not dispose it before returning it.
57
+ *
58
+ * Alternatively, the main Task can own its resources with `using` or `await
59
+ * using` while awaiting {@link waitForAbort} with `run(waitForAbort)`. Waiting
60
+ * for abort does not itself keep Node.js running: the service needs active
61
+ * work, such as a listening server.
62
+ *
63
+ * ### Shutdown and errors
64
+ *
65
+ * Handles `SIGINT` (Ctrl-C), `SIGTERM` (termination by the OS or a service
66
+ * host), and `SIGBREAK` (Ctrl-Break on Windows). The first signal logs shutdown
67
+ * progress, aborts the root Run, and waits for cleanup. A second signal exits
68
+ * immediately with its conventional signal status, abandoning cleanup. Signals
69
+ * are still handled during final cleanup.
70
+ *
71
+ * An error returned by the main Task is fatal. {@link getOrThrow} preserves it
72
+ * in `Error.cause`; the Run reports the failure and finishes cleanup. Every
73
+ * reported defect sets `process.exitCode` to 1, including an observer defect
74
+ * that does not abort the Run. The default reporter logs to the configured
56
75
  * Evolu console.
57
76
  *
58
77
  * Escaped uncaught exceptions and unhandled rejections remain under Node.js
59
78
  * native reporting and termination.
60
79
  *
61
- * ### Service Example
80
+ * ### Example
62
81
  *
63
- * ```ts
64
- * const deps = { ...createRelayDeps(), console: createConsole() };
65
- *
66
- * await runMain(deps)(createRelay({ port: 4000 }));
67
- * ```
68
- *
69
- * A Task returning `void` can keep a service alive explicitly when no Resource
70
- * owns its lifetime:
82
+ * A command finishes when its main Task completes:
71
83
  *
72
84
  * ```ts
73
- * await runMain(deps)(async (run) => {
74
- * void run(processMessages);
75
- * return await run(waitForAbort);
76
- * });
77
- * ```
85
+ * import { assertTrue, ok, type Task } from "@evolu/common";
86
+ * import { runMain } from "@evolu/nodejs";
78
87
  *
79
- * ### Command Example
88
+ * let completed = false;
89
+ * const command: Task<void> = () => {
90
+ * completed = true;
91
+ * return ok();
92
+ * };
80
93
  *
81
- * ```ts
82
94
  * await runMain(command, { mode: "command" });
95
+ * assertTrue(completed);
83
96
  * ```
84
97
  *
85
98
  * @group Node.js Task
86
99
  */
87
- export declare function runMain<T extends void | Resource>(main: Task<T>, options?: RunMainOptions): Promise<void>;
100
+ export declare function runMain<T extends void | Resource, E = never>(main: Task<T, E>, options?: RunMainOptions): Promise<void>;
88
101
  /** With custom dependencies. */
89
- export declare function runMain<D extends object>(deps: RunCustomDeps<D>, options?: RunMainOptions): <T extends void | Resource>(main: Task<T, never, D>) => Promise<void>;
102
+ export declare function runMain<D extends object>(deps: RunCustomDeps<D>, options?: RunMainOptions): <T extends void | Resource, E = never>(main: Task<T, E, D>) => Promise<void>;
90
103
  //# sourceMappingURL=Task.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Task.d.ts","sourceRoot":"","sources":["../../src/Task.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAQL,KAAK,QAAQ,EAEb,KAAK,aAAa,EAClB,KAAK,IAAI,EACT,KAAK,KAAK,EACX,MAAM,eAAe,CAAC;AAEvB;;;;GAIG;AACH,MAAM,WAAW,qBAAsB,SAAQ,KAAK,CAAC,uBAAuB,CAAC;IAC3E,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;CAC7B;AAED,+DAA+D;AAC/D,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,SAAS,GAAG,UAAU,CAAC;AAE3D,sDAAsD;AACtD,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,SAAS,CAAC;AAEhD,mCAAmC;AACnC,MAAM,WAAW,cAAc;IAC7B;;;;;;;;OAQG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,WAAW,CAAC;CAC7B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AACH,wBAAgB,OAAO,CAAC,CAAC,SAAS,IAAI,GAAG,QAAQ,EAC/C,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,EACb,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,IAAI,CAAC,CAAC;AACjB,gCAAgC;AAChC,wBAAgB,OAAO,CAAC,CAAC,SAAS,MAAM,EACtC,IAAI,EAAE,aAAa,CAAC,CAAC,CAAC,EACtB,OAAO,CAAC,EAAE,cAAc,GACvB,CAAC,CAAC,SAAS,IAAI,GAAG,QAAQ,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC"}
1
+ {"version":3,"file":"Task.d.ts","sourceRoot":"","sources":["../../src/Task.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EASL,KAAK,QAAQ,EAEb,KAAK,aAAa,EAClB,KAAK,IAAI,EACT,KAAK,KAAK,EACX,MAAM,eAAe,CAAC;AAGvB;;;;GAIG;AACH,MAAM,WAAW,qBAAsB,SAAQ,KAAK,CAAC,uBAAuB,CAAC;IAC3E,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;CAC7B;AAED,+DAA+D;AAC/D,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,SAAS,GAAG,UAAU,CAAC;AAE3D,sDAAsD;AACtD,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,SAAS,CAAC;AAEhD,mCAAmC;AACnC,MAAM,WAAW,cAAc;IAC7B;;;;;;;;OAQG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,WAAW,CAAC;CAC7B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmEG;AACH,wBAAgB,OAAO,CAAC,CAAC,SAAS,IAAI,GAAG,QAAQ,EAAE,CAAC,GAAG,KAAK,EAC1D,IAAI,EAAE,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,EAChB,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,IAAI,CAAC,CAAC;AACjB,gCAAgC;AAChC,wBAAgB,OAAO,CAAC,CAAC,SAAS,MAAM,EACtC,IAAI,EAAE,aAAa,CAAC,CAAC,CAAC,EACtB,OAAO,CAAC,EAAE,cAAc,GACvB,CAAC,CAAC,SAAS,IAAI,GAAG,QAAQ,EAAE,CAAC,GAAG,KAAK,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC"}
package/dist/src/Task.js CHANGED
@@ -55,7 +55,7 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
55
55
  var e = new Error(message);
56
56
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
57
57
  });
58
- import { createConsole, createRun, isDisposable, ok, waitForAbort, } from "@evolu/common";
58
+ import { createConsole, createRun, getOrThrow, isDisposable, ok, waitForAbort, } from "@evolu/common";
59
59
  export function runMain(mainOrDeps, { mode = "service" } = {}) {
60
60
  return typeof mainOrDeps === "function"
61
61
  ? runMainInternal(mainOrDeps, {}, mode)
@@ -106,7 +106,7 @@ const runMainInternal = async (main, deps, mode) => {
106
106
  await run(async (run) => {
107
107
  const env_2 = { stack: [], error: void 0, hasError: false };
108
108
  try {
109
- const resource = await run.ok(main);
109
+ const resource = getOrThrow(await run(main));
110
110
  if (!isDisposable(resource))
111
111
  return ok();
112
112
  const _resource = __addDisposableResource(env_2, resource, true);
@@ -1 +1 @@
1
- {"version":3,"file":"TestBundle.d.ts","sourceRoot":"","sources":["../../src/TestBundle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAYL,KAAK,gBAAgB,EACrB,KAAK,cAAc,EAQpB,MAAM,eAAe,CAAC;AA4CvB,wCAAwC;AACxC,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,mDAAmD;IACnD,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;CACpC;AAED,sDAAsD;AACtD,MAAM,MAAM,oBAAoB,GAAG,cAAc,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;AAE1E,0DAA0D;AAC1D,MAAM,MAAM,gBAAgB,GAAG,cAAc,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAC;AAE5E,qEAAqE;AACrE,MAAM,WAAW,UAAW,SAAQ,cAAc;IAChD,gEAAgE;IAChE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,6EAA6E;IAC7E,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CACpC;AAED,kCAAkC;AAClC,MAAM,WAAW,cAAc;IAC7B;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,mEAAmE;IACnE,QAAQ,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/E;AAED,8DAA8D;AAC9D,MAAM,WAAW,iBAAiB;IAChC,wDAAwD;IACxD,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IACvD;;;OAGG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,cAAc,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClD;;;OAGG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC,uEAAuE;IACvE,QAAQ,CAAC,eAAe,CAAC,EAAE,gBAAgB,CAAC;IAC5C,8EAA8E;IAC9E,QAAQ,CAAC,OAAO,CAAC,EAAE,gBAAgB,CAAC;CACrC;AAOD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,eAAO,MAAM,UAAU,qFAMpB,iBAAiB,KAAG,OAAO,CAAC,gBAAgB,CAgK9C,CAAC"}
1
+ {"version":3,"file":"TestBundle.d.ts","sourceRoot":"","sources":["../../src/TestBundle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAaL,KAAK,gBAAgB,EACrB,KAAK,cAAc,EASpB,MAAM,eAAe,CAAC;AA4CvB,wCAAwC;AACxC,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,mDAAmD;IACnD,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;CACpC;AAED,sDAAsD;AACtD,MAAM,MAAM,oBAAoB,GAAG,cAAc,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;AAE1E,0DAA0D;AAC1D,MAAM,MAAM,gBAAgB,GAAG,cAAc,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAC;AAE5E,qEAAqE;AACrE,MAAM,WAAW,UAAW,SAAQ,cAAc;IAChD,gEAAgE;IAChE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,6EAA6E;IAC7E,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CACpC;AAED,kCAAkC;AAClC,MAAM,WAAW,cAAc;IAC7B;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,mEAAmE;IACnE,QAAQ,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/E;AAED,8DAA8D;AAC9D,MAAM,WAAW,iBAAiB;IAChC,wDAAwD;IACxD,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IACvD;;;OAGG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,cAAc,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClD;;;OAGG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC,uEAAuE;IACvE,QAAQ,CAAC,eAAe,CAAC,EAAE,gBAAgB,CAAC;IAC5C,8EAA8E;IAC9E,QAAQ,CAAC,OAAO,CAAC,EAAE,gBAAgB,CAAC;CACrC;AAOD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,eAAO,MAAM,UAAU,qFAMpB,iBAAiB,KAAG,OAAO,CAAC,gBAAgB,CA+J9C,CAAC"}
@@ -62,7 +62,7 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
62
62
  var e = new Error(message);
63
63
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
64
64
  });
65
- import { allSettled, assert, assertType, instanceOf, createRun, durationToMillis, escapeRegExp, filterArray, isErr, mapArray, safelyStringifyUnknownValue, String as StringType, timeout, TimeoutError, tryAsync, } from "@evolu/common";
65
+ import { allSettled, assert, assertLength, assertSame, assertType, instanceOf, createRun, durationToMillis, escapeRegExp, isErr, mapArray, partitionArray, safelyStringifyUnknownValue, String as StringType, timeout, TimeoutError, tryAsync, } from "@evolu/common";
66
66
  import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
67
67
  import { tmpdir } from "node:os";
68
68
  import { dirname, isAbsolute, join, resolve } from "node:path";
@@ -186,7 +186,7 @@ export const testBundle = async ({ cases, aliases = {}, outputDirectory, bundlin
186
186
  error,
187
187
  };
188
188
  }), { concurrency: availableParallelism() }));
189
- const failures = filterArray(results, isErr);
189
+ const [failures, successes] = partitionArray(results, isErr);
190
190
  if (failures.length > 0) {
191
191
  const errors = mapArray(failures, ({ error: { caseName, bundler, error } }) => {
192
192
  const message = error instanceof Error
@@ -202,8 +202,7 @@ export const testBundle = async ({ cases, aliases = {}, outputDirectory, bundlin
202
202
  const bundleEntriesByCase = new Map();
203
203
  for (const [caseName] of caseEntries)
204
204
  bundleEntriesByCase.set(caseName, []);
205
- for (const result of results) {
206
- assert(result.ok, "Expected every bundle test to succeed.");
205
+ for (const result of successes) {
207
206
  const { caseName, bundle } = result.value;
208
207
  const entries = bundleEntriesByCase.get(caseName);
209
208
  assert(entries, `Missing bundle test case "${caseName}".`);
@@ -314,13 +313,11 @@ const testViteBundler = {
314
313
  });
315
314
  assert(Array.isArray(output), "Vite did not return build outputs.");
316
315
  const outputs = output;
317
- assert(outputs.length === 1, "Vite did not return one build output.");
318
- const viteOutput = outputs.at(0);
319
- assert(viteOutput, "Vite did not return a build output.");
320
- assert(viteOutput.output.length === 1, "Vite did not emit one JavaScript chunk.");
321
- const chunk = viteOutput.output.at(0);
322
- assert(chunk, "Vite did not emit a JavaScript chunk.");
323
- assert(chunk.type === "chunk", "Vite did not emit a JavaScript chunk.");
316
+ assertLength(outputs, 1);
317
+ const viteOutput = outputs[0];
318
+ assertLength(viteOutput.output, 1);
319
+ const chunk = viteOutput.output[0];
320
+ assertSame(chunk.type, "chunk");
324
321
  assertType(StringType, chunk.code);
325
322
  return {
326
323
  code: chunk.code,
@@ -415,7 +412,7 @@ const runTestBundler = (bundlerName, options) => async (run) => tryAsync(async (
415
412
  assertType(ErrorType, error);
416
413
  return error;
417
414
  });
418
- const ErrorType = /*#__PURE__*/ instanceOf(globalThis.Error);
415
+ const ErrorType = /*#__PURE__*/ instanceOf(Error);
419
416
  // TODO: Replace the evaluated CommonJS worker with an ESM module worker.
420
417
  const testBundleWorkerSource = String.raw `
421
418
  const { parentPort, workerData } = require("node:worker_threads");
@@ -14,7 +14,10 @@ import { type ReadonlyRecord } from "@evolu/common";
14
14
  export interface TestJSDocExamplesOptions {
15
15
  /** Source files or glob patterns resolved from `cwd`. */
16
16
  readonly include: string | ReadonlyArray<string>;
17
- /** Package imports redirected to absolute TypeScript entry paths. */
17
+ /**
18
+ * Package imports redirected to absolute TypeScript entry paths. Type
19
+ * checking also applies these aliases to imports within those modules.
20
+ */
18
21
  readonly aliases?: ReadonlyRecord<string, string>;
19
22
  /** Directory used for globbing and package resolution. */
20
23
  readonly cwd?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"TestJSDoc.d.ts","sourceRoot":"","sources":["../../src/TestJSDoc.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAY,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AAiB9D;;;GAGG;AACH,MAAM,WAAW,wBAAwB;IACvC,yDAAyD;IACzD,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC;IACjD,qEAAqE;IACrE,QAAQ,CAAC,OAAO,CAAC,EAAE,cAAc,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClD,0DAA0D;IAC1D,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,+DAA+D;IAC/D,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;CACrC;AA+CD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyDG;AACH,eAAO,MAAM,iBAAiB,kDAK3B,wBAAwB,KAAG,OAAO,CAAC,IAAI,CAgKzC,CAAC"}
1
+ {"version":3,"file":"TestJSDoc.d.ts","sourceRoot":"","sources":["../../src/TestJSDoc.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAY,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AAiB9D;;;GAGG;AACH,MAAM,WAAW,wBAAwB;IACvC,yDAAyD;IACzD,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC;IACjD;;;OAGG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,cAAc,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClD,0DAA0D;IAC1D,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,+DAA+D;IAC/D,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;CACrC;AA+CD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyDG;AACH,eAAO,MAAM,iBAAiB,kDAK3B,wBAAwB,KAAG,OAAO,CAAC,IAAI,CAmKzC,CAAC"}
@@ -113,6 +113,7 @@ export const testJSDocExamples = async ({ include, aliases = {}, cwd = process.c
113
113
  noEmit: true,
114
114
  noUnusedLocals: false,
115
115
  noUnusedParameters: false,
116
+ paths: Object.fromEntries(Object.entries(aliases).map(([name, target]) => [name, [target]])),
116
117
  target: "es2022",
117
118
  types: ["node"],
118
119
  },
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Overview reporting for Node.js tests.
3
+ *
4
+ * Use the dedicated `@evolu/nodejs/TestOverviewReporter` entry point directly
5
+ * with Node.js:
6
+ *
7
+ * ```sh
8
+ * node --test --test-reporter=@evolu/nodejs/TestOverviewReporter
9
+ * ```
10
+ *
11
+ * @module
12
+ */
13
+ import { type TestEvent } from "node:test/reporters";
14
+ /**
15
+ * Lists test files slowest-first and preserves Node.js failure diagnostics, run
16
+ * totals, and coverage output.
17
+ *
18
+ * Durations longer than 300 ms are highlighted when terminal colors are
19
+ * supported.
20
+ */
21
+ declare const testOverviewReporter: (source: AsyncIterable<TestEvent> | Iterable<TestEvent>) => AsyncGenerator<string | Uint8Array, void>;
22
+ export default testOverviewReporter;
23
+ //# sourceMappingURL=TestOverviewReporter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TestOverviewReporter.d.ts","sourceRoot":"","sources":["../../src/TestOverviewReporter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,EAAa,KAAK,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAKhE;;;;;;GAMG;AACH,QAAA,MAAM,oBAAoB,WAChB,aAAa,CAAC,SAAS,CAAC,GAAG,QAAQ,CAAC,SAAS,CAAC,KACrD,cAAc,CAAC,MAAM,GAAG,UAAU,EAAE,IAAI,CAuD1C,CAAC;eAEa,oBAAoB"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Overview reporting for Node.js tests.
3
+ *
4
+ * Use the dedicated `@evolu/nodejs/TestOverviewReporter` entry point directly
5
+ * with Node.js:
6
+ *
7
+ * ```sh
8
+ * node --test --test-reporter=@evolu/nodejs/TestOverviewReporter
9
+ * ```
10
+ *
11
+ * @module
12
+ */
13
+ import { relative } from "node:path";
14
+ import { dot, spec } from "node:test/reporters";
15
+ import { styleText } from "node:util";
16
+ const slowTestThresholdMs = 300;
17
+ /**
18
+ * Lists test files slowest-first and preserves Node.js failure diagnostics, run
19
+ * totals, and coverage output.
20
+ *
21
+ * Durations longer than 300 ms are highlighted when terminal colors are
22
+ * supported.
23
+ */
24
+ const testOverviewReporter = async function* (source) {
25
+ const fileSummaries = [];
26
+ const dotOutput = [];
27
+ const specEvents = [];
28
+ let hasFailures = false;
29
+ const captureEvents = async function* () {
30
+ for await (const event of source) {
31
+ if (event.type === "test:fail")
32
+ hasFailures = true;
33
+ if (event.type === "test:summary" && event.data.file !== undefined) {
34
+ fileSummaries.push({ ...event.data, file: event.data.file });
35
+ }
36
+ if (event.type === "test:coverage" || event.type === "test:diagnostic") {
37
+ specEvents.push(event);
38
+ }
39
+ yield event;
40
+ }
41
+ };
42
+ for await (const output of dot(captureEvents()))
43
+ dotOutput.push(output);
44
+ fileSummaries.sort((a, b) => b.duration_ms - a.duration_ms);
45
+ if (hasFailures) {
46
+ for (const output of dotOutput)
47
+ yield output;
48
+ }
49
+ if (fileSummaries.length > 0) {
50
+ yield `${styleText("bold", "Test files:")}\n\n`;
51
+ for (const { counts, duration_ms, file, success } of fileSummaries) {
52
+ const testLabel = counts.tests === 1 ? "test" : "tests";
53
+ const status = styleText(success ? "green" : "red", success ? "✔" : "✖");
54
+ const tests = styleText("dim", `(${counts.tests} ${testLabel})`);
55
+ const duration = styleText(duration_ms > slowTestThresholdMs ? "yellow" : "green", `${Math.round(duration_ms)}ms`);
56
+ yield `${status} ${relative(process.cwd(), file)} ${tests} ${duration}\n`;
57
+ }
58
+ yield "\n";
59
+ }
60
+ const summaryReporter = spec();
61
+ for (const event of specEvents)
62
+ summaryReporter.write(event);
63
+ summaryReporter.end();
64
+ yield* summaryReporter;
65
+ };
66
+ export default testOverviewReporter;
@@ -4,6 +4,7 @@
4
4
  */
5
5
  export * from "./Cli.ts";
6
6
  export * from "./Crypto.ts";
7
+ export * from "./Fs.ts";
7
8
  export * from "./local-first/Relay.ts";
8
9
  export * from "./Platform.ts";
9
10
  export * from "./Sqlite.ts";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,cAAc,UAAU,CAAC;AACzB,cAAc,aAAa,CAAC;AAC5B,cAAc,wBAAwB,CAAC;AACvC,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,cAAc,UAAU,CAAC;AACzB,cAAc,aAAa,CAAC;AAC5B,cAAc,SAAS,CAAC;AACxB,cAAc,wBAAwB,CAAC;AACvC,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,aAAa,CAAC"}