@evolu/nodejs 3.0.1 → 3.2.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.
@@ -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;
@@ -1 +1 @@
1
- {"version":3,"file":"Worker.d.ts","sourceRoot":"","sources":["../../src/Worker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAoB,sBAAsB,EAAE,MAAM,eAAe,CAAC;AAG9E,0EAA0E;AAC1E,eAAO,MAAM,sBAAsB,EAAE,sBAsCpC,CAAC"}
1
+ {"version":3,"file":"Worker.d.ts","sourceRoot":"","sources":["../../src/Worker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAoB,sBAAsB,EAAE,MAAM,eAAe,CAAC;AAG9E,0EAA0E;AAC1E,eAAO,MAAM,sBAAsB,EAAE,sBAwCpC,CAAC"}
@@ -60,6 +60,7 @@ export const createBroadcastChannel = (name) => {
60
60
  let disposed = false;
61
61
  disposer.defer(() => {
62
62
  disposed = true;
63
+ // oxlint-disable-next-line unicorn/prefer-add-event-listener -- This adapter owns and clears one handler.
63
64
  nativeBroadcastChannel.onmessage = null;
64
65
  nativeBroadcastChannel.close();
65
66
  });
@@ -75,6 +76,7 @@ export const createBroadcastChannel = (name) => {
75
76
  if (disposed)
76
77
  return;
77
78
  onMessageHandler = fn;
79
+ // oxlint-disable-next-line unicorn/prefer-add-event-listener -- This adapter owns and clears one handler.
78
80
  nativeBroadcastChannel.onmessage = fn
79
81
  ? (event) => {
80
82
  fn(event.data);
@@ -1 +1 @@
1
- {"version":3,"file":"Relay.d.ts","sourceRoot":"","sources":["../../../src/local-first/Relay.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,qBAAqB,EAK1B,KAAK,SAAS,EACd,KAAK,IAAI,EACT,KAAK,kBAAkB,EAGxB,MAAM,eAAe,CAAC;AACvB,OAAO,EAQL,KAAK,KAAK,EACV,KAAK,WAAW,EACjB,MAAM,2BAA2B,CAAC;AAQnC,MAAM,WAAW,iBAAkB,SAAQ,WAAW;IACpD,2CAA2C;IAC3C,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,MAAM,SAAS,GAAG,qBAAqB,GAAG,SAAS,GAAG,kBAAkB,CAAC;AAE/E,iEAAiE;AACjE,eAAO,MAAM,eAAe,QAAO,SAIjC,CAAC;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,eAAO,MAAM,WAAW,wDAMnB,iBAAiB,KAAG,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,CAgNlD,CAAC"}
1
+ {"version":3,"file":"Relay.d.ts","sourceRoot":"","sources":["../../../src/local-first/Relay.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,qBAAqB,EAK1B,KAAK,SAAS,EACd,KAAK,IAAI,EACT,KAAK,kBAAkB,EAGxB,MAAM,eAAe,CAAC;AACvB,OAAO,EAQL,KAAK,KAAK,EACV,KAAK,WAAW,EACjB,MAAM,2BAA2B,CAAC;AAQnC,MAAM,WAAW,iBAAkB,SAAQ,WAAW;IACpD,2CAA2C;IAC3C,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,MAAM,SAAS,GAAG,qBAAqB,GAAG,SAAS,GAAG,kBAAkB,CAAC;AAE/E,iEAAiE;AACjE,eAAO,MAAM,eAAe,QAAO,SAIjC,CAAC;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,eAAO,MAAM,WAAW,wDAMnB,iBAAiB,KAAG,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,CAkNlD,CAAC"}
@@ -161,7 +161,7 @@ export const createRelay = ({ port = 443, name = Name.orThrow("evolu-relay"), is
161
161
  // ignores abort. The daemon runs in the root Run, so aborting the
162
162
  // current Run does not make its Fiber wait for the service Promise to
163
163
  // settle.
164
- daemon(async (run) => await tryAsync(() => isOwnerAllowed(ownerId, { signal: run.signal }), (error) => ({ type: "OwnerAuthorizationError", error }))));
164
+ daemon(async (run) => tryAsync(() => isOwnerAllowed(ownerId, { signal: run.signal }), (error) => ({ type: "OwnerAuthorizationError", error }))));
165
165
  const abortAuthorization = () => {
166
166
  authorizationFiber.abort({
167
167
  type: "WebSocketUpgradeSocketClosed",
@@ -239,7 +239,7 @@ export const createRelay = ({ port = 443, name = Name.orThrow("evolu-relay"), is
239
239
  server.listen(port);
240
240
  await once(server, "listening");
241
241
  const address = server.address();
242
- assert(address && typeof address !== "string", "Expected TCP address");
242
+ assert(address !== null && typeof address !== "string", "Expected TCP address");
243
243
  const disposables = disposer.move();
244
244
  console.info(`Started on port ${address.port}`);
245
245
  return ok({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evolu/nodejs",
3
- "version": "3.0.1",
3
+ "version": "3.2.0",
4
4
  "description": "Evolu for Node.js",
5
5
  "author": "Daniel Steigerwald <daniel@steigerwald.cz>",
6
6
  "license": "MIT",
@@ -24,40 +24,80 @@
24
24
  "types": "./dist/src/TestBundle.d.ts",
25
25
  "import": "./dist/src/TestBundle.js",
26
26
  "default": "./dist/src/TestBundle.js"
27
+ },
28
+ "./TestJSDoc": {
29
+ "types": "./dist/src/TestJSDoc.d.ts",
30
+ "import": "./dist/src/TestJSDoc.js",
31
+ "default": "./dist/src/TestJSDoc.js"
32
+ },
33
+ "./TestOverviewReporter": {
34
+ "types": "./dist/src/TestOverviewReporter.d.ts",
35
+ "import": "./dist/src/TestOverviewReporter.js",
36
+ "default": "./dist/src/TestOverviewReporter.js"
27
37
  }
28
38
  },
29
39
  "typesVersions": {
30
40
  "*": {
31
41
  "TestBundle": [
32
42
  "./dist/src/TestBundle.d.ts"
43
+ ],
44
+ "TestJSDoc": [
45
+ "./dist/src/TestJSDoc.d.ts"
46
+ ],
47
+ "TestOverviewReporter": [
48
+ "./dist/src/TestOverviewReporter.d.ts"
33
49
  ]
34
50
  }
35
51
  },
36
52
  "files": [
37
53
  "dist/src/**",
54
+ "!dist/**/*.test.*",
38
55
  "src/**",
39
56
  "README.md"
40
57
  ],
41
58
  "dependencies": {
42
59
  "better-sqlite3": "^12.11.1",
43
- "ws": "^8.19.0"
60
+ "ws": "^8.21.3"
44
61
  },
45
62
  "devDependencies": {
63
+ "@evolu/common": "8.8.0",
64
+ "@evolu/oxlint-config": "0.2.0",
65
+ "@evolu/typescript-config": "0.1.1",
46
66
  "@types/better-sqlite3": "^9.6.0",
47
67
  "@types/node": "^24.10.9",
48
68
  "@types/ws": "^8.18.1",
49
69
  "@typescript/native": "npm:typescript@^7.0.2",
50
- "vite": "^8.2.0",
51
- "webpack": "^5.109.2",
52
- "@evolu/common": "8.1.0",
53
- "@evolu/typescript-config": "0.0.2"
70
+ "oxlint": "^1.79.0",
71
+ "oxlint-tsgolint": "7.0.2001",
72
+ "vite": "^8.2.2",
73
+ "webpack": "^5.109.2"
54
74
  },
55
75
  "peerDependencies": {
56
76
  "@evolu/common": "^8.0.0",
77
+ "@evolu/oxlint-config": "^0.2.0",
78
+ "@evolu/typescript-config": "^0.1.0",
79
+ "oxlint": "^1.79.0",
80
+ "oxlint-tsgolint": "7.0.2001",
81
+ "typescript": ">=7.0.0",
57
82
  "vite": ">=8.0.0",
58
83
  "webpack": ">=5.108.4"
59
84
  },
60
85
  "peerDependenciesMeta": {
86
+ "@evolu/oxlint-config": {
87
+ "optional": true
88
+ },
89
+ "@evolu/typescript-config": {
90
+ "optional": true
91
+ },
92
+ "oxlint": {
93
+ "optional": true
94
+ },
95
+ "oxlint-tsgolint": {
96
+ "optional": true
97
+ },
98
+ "typescript": {
99
+ "optional": true
100
+ },
61
101
  "vite": {
62
102
  "optional": true
63
103
  },
@@ -66,7 +106,7 @@
66
106
  }
67
107
  },
68
108
  "engines": {
69
- "node": ">=24.0.0"
109
+ "node": ">=24.20.0"
70
110
  },
71
111
  "publishConfig": {
72
112
  "access": "public"
@@ -0,0 +1,90 @@
1
+ import {
2
+ assertEqual,
3
+ assertErr,
4
+ assertOk,
5
+ assertTrue,
6
+ testAbortError,
7
+ testAbortReason,
8
+ testCreateRun,
9
+ } from "@evolu/common";
10
+ import { test } from "node:test";
11
+ import { spawn } from "./Cli.ts";
12
+
13
+ test("spawn succeeds with inherited stdio and a configured cwd", async () => {
14
+ await using run = testCreateRun();
15
+ const scriptsDirectory = new URL("../../../scripts/", import.meta.url);
16
+ const result = await run(
17
+ spawn(
18
+ process.execPath,
19
+ ["-e", 'process.exit(process.cwd().endsWith("scripts") ? 0 : 1)'],
20
+ { cwd: scriptsDirectory },
21
+ ),
22
+ );
23
+
24
+ assertOk(result);
25
+ });
26
+
27
+ test("spawn returns a start error", async () => {
28
+ await using run = testCreateRun();
29
+ const result = await run(spawn("evolu-command-that-does-not-exist", []));
30
+
31
+ assertErr(result);
32
+ const { message } = result.error;
33
+ assertTrue(
34
+ message.startsWith("Failed to start evolu-command-that-does-not-exist:"),
35
+ );
36
+ assertEqual(result, {
37
+ ok: false,
38
+ error: {
39
+ type: "SpawnError",
40
+ command: "evolu-command-that-does-not-exist",
41
+ exitCode: null,
42
+ signal: null,
43
+ message,
44
+ },
45
+ });
46
+ });
47
+
48
+ test("spawn returns a non-zero exit error", async () => {
49
+ await using run = testCreateRun();
50
+ const result = await run(spawn(process.execPath, ["-e", "process.exit(7)"]));
51
+ const command = `${process.execPath} -e process.exit(7)`;
52
+
53
+ assertErr(result, {
54
+ type: "SpawnError",
55
+ command,
56
+ exitCode: 7,
57
+ signal: null,
58
+ message: `${command} exited with code 7.`,
59
+ });
60
+ });
61
+
62
+ test("spawn returns a signal exit error", async () => {
63
+ await using run = testCreateRun();
64
+ const result = await run(
65
+ spawn(process.execPath, ["-e", 'process.kill(process.pid, "SIGTERM")']),
66
+ );
67
+ const command = `${process.execPath} -e process.kill(process.pid, "SIGTERM")`;
68
+
69
+ assertErr(result, {
70
+ type: "SpawnError",
71
+ command,
72
+ exitCode: null,
73
+ signal: "SIGTERM",
74
+ message: `${command} exited from SIGTERM.`,
75
+ });
76
+ });
77
+
78
+ test("spawn aborts its child process with the Run", async () => {
79
+ await using run = testCreateRun();
80
+ const fiber = run.abortable(
81
+ spawn(process.execPath, ["-e", "setInterval(() => {}, 1000)"]),
82
+ );
83
+
84
+ await new Promise<void>((resolve) => {
85
+ setImmediate(resolve);
86
+ });
87
+ fiber.abort(testAbortReason);
88
+
89
+ assertErr(await fiber, testAbortError);
90
+ });
@@ -0,0 +1,32 @@
1
+ import {
2
+ assertEqual,
3
+ assertSame,
4
+ assertType,
5
+ PositiveInt,
6
+ } from "@evolu/common";
7
+ import { mock, test } from "node:test";
8
+ import type {
9
+ AvailableParallelism,
10
+ AvailableParallelismDep,
11
+ } from "./Platform.ts";
12
+
13
+ const nodeAvailableParallelism = mock.fn<() => number>();
14
+
15
+ mock.module("node:os", {
16
+ // @ts-expect-error -- Node.js 24.20 replaces the deprecated namedExports option with exports, which @types/node 24.13 does not declare yet.
17
+ exports: { availableParallelism: nodeAvailableParallelism },
18
+ });
19
+
20
+ const { availableParallelism } = await import("./Platform.ts");
21
+
22
+ test("availableParallelism returns the validated Node.js value", () => {
23
+ nodeAvailableParallelism.mock.mockImplementation(() => 128);
24
+
25
+ const parallelism = availableParallelism();
26
+ const deps = { availableParallelism } satisfies AvailableParallelismDep;
27
+
28
+ assertType<typeof availableParallelism, AvailableParallelism>();
29
+ assertType<typeof parallelism, PositiveInt>();
30
+ assertSame(deps.availableParallelism, availableParallelism);
31
+ assertEqual(parallelism, 128);
32
+ });
package/src/TestBundle.ts CHANGED
@@ -14,18 +14,20 @@
14
14
  import {
15
15
  allSettled,
16
16
  assert,
17
+ assertLength,
18
+ assertSame,
17
19
  assertType,
18
20
  instanceOf,
19
21
  createRun,
20
22
  durationToMillis,
21
23
  escapeRegExp,
22
- filterArray,
23
24
  isErr,
24
25
  mapArray,
25
26
  type NonEmptyReadonlyArray,
26
27
  type PositiveDuration,
27
28
  type ReadonlyRecord,
28
29
  type Result,
30
+ partitionArray,
29
31
  safelyStringifyUnknownValue,
30
32
  String as StringType,
31
33
  type Task,
@@ -301,7 +303,7 @@ export const testBundle = async ({
301
303
  ),
302
304
  );
303
305
 
304
- const failures = filterArray(results, isErr);
306
+ const [failures, successes] = partitionArray(results, isErr);
305
307
  if (failures.length > 0) {
306
308
  const errors = mapArray(
307
309
  failures,
@@ -331,8 +333,7 @@ export const testBundle = async ({
331
333
  >();
332
334
  for (const [caseName] of caseEntries) bundleEntriesByCase.set(caseName, []);
333
335
 
334
- for (const result of results) {
335
- assert(result.ok, "Expected every bundle test to succeed.");
336
+ for (const result of successes) {
336
337
  const { caseName, bundle } = result.value;
337
338
  const entries = bundleEntriesByCase.get(caseName);
338
339
  assert(entries, `Missing bundle test case "${caseName}".`);
@@ -408,11 +409,11 @@ const testViteBundler: TestBundler = {
408
409
  const output = await vite.build({
409
410
  root: dirname(entryPath),
410
411
  configFile: false,
411
- envFile: false,
412
+ envDir: false,
412
413
  logLevel: "silent",
413
414
  resolve: {
414
415
  alias: Object.entries(aliases).map(([name, replacement]) => ({
415
- find: new RegExp(`^${escapeRegExp(name)}$`),
416
+ find: new RegExp(`^${escapeRegExp(name)}$`, "u"),
416
417
  replacement,
417
418
  })),
418
419
  },
@@ -438,15 +439,11 @@ const testViteBundler: TestBundler = {
438
439
  >;
439
440
  assert(Array.isArray(output), "Vite did not return build outputs.");
440
441
  const outputs: ReadonlyArray<ViteOutput> = output;
441
- assert(outputs.length === 1, "Vite did not return one build output.");
442
+ assertLength(outputs, 1);
442
443
  const viteOutput = outputs[0];
443
- assert(viteOutput, "Vite did not return a build output.");
444
- assert(
445
- viteOutput.output.length === 1,
446
- "Vite did not emit one JavaScript chunk.",
447
- );
444
+ assertLength(viteOutput.output, 1);
448
445
  const chunk = viteOutput.output[0];
449
- assert(chunk, "Vite did not emit a JavaScript chunk.");
446
+ assertSame(chunk.type, "chunk");
450
447
  assertType(StringType, chunk.code);
451
448
 
452
449
  return {
@@ -581,7 +578,7 @@ interface TestBundleWorkerError {
581
578
  readonly stack: string;
582
579
  }
583
580
 
584
- const ErrorType = /*#__PURE__*/ instanceOf(globalThis.Error);
581
+ const ErrorType = /*#__PURE__*/ instanceOf(Error);
585
582
 
586
583
  // TODO: Replace the evaluated CommonJS worker with an ESM module worker.
587
584
  const testBundleWorkerSource = String.raw`