@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 +10 -1
- package/dist/src/Fs.d.ts +63 -0
- package/dist/src/Fs.d.ts.map +1 -0
- package/dist/src/Fs.js +217 -0
- package/dist/src/Task.d.ts +51 -38
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +2 -2
- package/dist/src/TestBundle.d.ts.map +1 -1
- package/dist/src/TestBundle.js +9 -12
- package/dist/src/TestJSDoc.d.ts +4 -1
- package/dist/src/TestJSDoc.d.ts.map +1 -1
- package/dist/src/TestJSDoc.js +1 -0
- package/dist/src/TestOverviewReporter.d.ts +23 -0
- package/dist/src/TestOverviewReporter.d.ts.map +1 -0
- package/dist/src/TestOverviewReporter.js +66 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/local-first/Relay.d.ts +15 -27
- package/dist/src/local-first/Relay.d.ts.map +1 -1
- package/dist/src/local-first/Relay.js +10 -25
- package/package.json +17 -8
- package/src/Cli.test.ts +90 -0
- package/src/Fs.test.ts +1019 -0
- package/src/Fs.ts +300 -0
- package/src/Platform.test.ts +32 -0
- package/src/Task.ts +62 -45
- package/src/TestBundle.ts +11 -15
- package/src/TestJSDoc.ts +7 -1
- package/src/TestOverviewReporter.test.ts +219 -0
- package/src/TestOverviewReporter.ts +86 -0
- package/src/Time.test.ts +67 -0
- package/src/index.ts +1 -0
- package/src/local-first/Relay.ts +15 -26
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.
|
package/dist/src/Fs.d.ts
ADDED
|
@@ -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
|
+
});
|
package/dist/src/Task.d.ts
CHANGED
|
@@ -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
|
|
26
|
-
*
|
|
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
|
|
33
|
+
* Runs the main {@link Task} of a Node.js command or service.
|
|
34
34
|
*
|
|
35
|
-
*
|
|
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
|
-
* - `
|
|
38
|
-
*
|
|
39
|
-
*
|
|
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
|
-
*
|
|
42
|
-
* the main Task
|
|
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
|
-
*
|
|
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
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
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
|
-
* ###
|
|
80
|
+
* ### Example
|
|
62
81
|
*
|
|
63
|
-
*
|
|
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
|
-
*
|
|
74
|
-
*
|
|
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
|
-
*
|
|
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,
|
|
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
|
package/dist/src/Task.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Task.d.ts","sourceRoot":"","sources":["../../src/Task.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,
|
|
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
|
|
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,
|
|
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"}
|
package/dist/src/TestBundle.js
CHANGED
|
@@ -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,
|
|
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 =
|
|
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
|
|
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
|
-
|
|
318
|
-
const viteOutput = outputs
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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(
|
|
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");
|
package/dist/src/TestJSDoc.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
|
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"}
|
package/dist/src/TestJSDoc.js
CHANGED
|
@@ -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;
|
package/dist/src/index.d.ts
CHANGED
package/dist/src/index.d.ts.map
CHANGED
|
@@ -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"}
|