@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.
- package/README.md +10 -1
- package/dist/src/TestBundle.d.ts.map +1 -1
- package/dist/src/TestBundle.js +9 -11
- package/dist/src/TestJSDoc.d.ts +83 -0
- package/dist/src/TestJSDoc.d.ts.map +1 -0
- package/dist/src/TestJSDoc.js +452 -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/Worker.d.ts.map +1 -1
- package/dist/src/Worker.js +2 -0
- package/dist/src/local-first/Relay.d.ts.map +1 -1
- package/dist/src/local-first/Relay.js +2 -2
- package/package.json +47 -7
- package/src/Cli.test.ts +90 -0
- package/src/Platform.test.ts +32 -0
- package/src/TestBundle.ts +11 -14
- package/src/TestJSDoc.ts +820 -0
- package/src/TestOverviewReporter.test.ts +219 -0
- package/src/TestOverviewReporter.ts +86 -0
- package/src/Time.test.ts +67 -0
- package/src/Worker.ts +2 -0
- package/src/local-first/Relay.ts +9 -7
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.
|
|
@@ -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}".`);
|
|
@@ -289,11 +288,11 @@ const testViteBundler = {
|
|
|
289
288
|
const output = await vite.build({
|
|
290
289
|
root: dirname(entryPath),
|
|
291
290
|
configFile: false,
|
|
292
|
-
|
|
291
|
+
envDir: false,
|
|
293
292
|
logLevel: "silent",
|
|
294
293
|
resolve: {
|
|
295
294
|
alias: Object.entries(aliases).map(([name, replacement]) => ({
|
|
296
|
-
find: new RegExp(`^${escapeRegExp(name)}
|
|
295
|
+
find: new RegExp(`^${escapeRegExp(name)}$`, "u"),
|
|
297
296
|
replacement,
|
|
298
297
|
})),
|
|
299
298
|
},
|
|
@@ -314,12 +313,11 @@ const testViteBundler = {
|
|
|
314
313
|
});
|
|
315
314
|
assert(Array.isArray(output), "Vite did not return build outputs.");
|
|
316
315
|
const outputs = output;
|
|
317
|
-
|
|
316
|
+
assertLength(outputs, 1);
|
|
318
317
|
const viteOutput = outputs[0];
|
|
319
|
-
|
|
320
|
-
assert(viteOutput.output.length === 1, "Vite did not emit one JavaScript chunk.");
|
|
318
|
+
assertLength(viteOutput.output, 1);
|
|
321
319
|
const chunk = viteOutput.output[0];
|
|
322
|
-
|
|
320
|
+
assertSame(chunk.type, "chunk");
|
|
323
321
|
assertType(StringType, chunk.code);
|
|
324
322
|
return {
|
|
325
323
|
code: chunk.code,
|
|
@@ -414,7 +412,7 @@ const runTestBundler = (bundlerName, options) => async (run) => tryAsync(async (
|
|
|
414
412
|
assertType(ErrorType, error);
|
|
415
413
|
return error;
|
|
416
414
|
});
|
|
417
|
-
const ErrorType = /*#__PURE__*/ instanceOf(
|
|
415
|
+
const ErrorType = /*#__PURE__*/ instanceOf(Error);
|
|
418
416
|
// TODO: Replace the evaluated CommonJS worker with an ESM module worker.
|
|
419
417
|
const testBundleWorkerSource = String.raw `
|
|
420
418
|
const { parentPort, workerData } = require("node:worker_threads");
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for testing TypeScript examples embedded in JSDoc and Markdown.
|
|
3
|
+
*
|
|
4
|
+
* These utilities use the dedicated `@evolu/nodejs/TestJSDoc` entry point so
|
|
5
|
+
* normal `@evolu/nodejs` imports do not evaluate TypeScript and JSDoc tooling.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
import { type ReadonlyRecord } from "@evolu/common";
|
|
10
|
+
/**
|
|
11
|
+
* Options for linting, compiling, and executing TypeScript documentation
|
|
12
|
+
* examples.
|
|
13
|
+
*/
|
|
14
|
+
export interface TestJSDocExamplesOptions {
|
|
15
|
+
/** Source files or glob patterns resolved from `cwd`. */
|
|
16
|
+
readonly include: string | ReadonlyArray<string>;
|
|
17
|
+
/** Package imports redirected to absolute TypeScript entry paths. */
|
|
18
|
+
readonly aliases?: ReadonlyRecord<string, string>;
|
|
19
|
+
/** Directory used for globbing and package resolution. */
|
|
20
|
+
readonly cwd?: string;
|
|
21
|
+
/** TypeScript compiler package. Defaults to `"typescript"`. */
|
|
22
|
+
readonly typescriptPackage?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Lints, compiles, and executes every TypeScript example in the included JSDoc
|
|
26
|
+
* comments and Markdown files.
|
|
27
|
+
*
|
|
28
|
+
* Examples are compiled together as isolated TypeScript modules. Examples
|
|
29
|
+
* without compilation errors are then imported in source order by one Node.js
|
|
30
|
+
* process. Lint, compilation, and execution failures are reported together.
|
|
31
|
+
* Examples are linted with `@evolu/oxlint-config`. Evolu's required polyfills
|
|
32
|
+
* are installed before each example runs.
|
|
33
|
+
*
|
|
34
|
+
* Install `@evolu/oxlint-config`, `@evolu/typescript-config`, `oxlint`,
|
|
35
|
+
* `oxlint-tsgolint`, and TypeScript as development dependencies in the project
|
|
36
|
+
* that calls this helper.
|
|
37
|
+
*
|
|
38
|
+
* Write every TypeScript fence as a standalone, deterministic example and
|
|
39
|
+
* explicitly import its dependencies and assertions. Use `assertType` to prove
|
|
40
|
+
* static contracts, `assertEqual` for Data comparisons, `assert` with a
|
|
41
|
+
* descriptive message for invariants and narrowing, and `assertOk` or
|
|
42
|
+
* `assertErr` for Results. Prefix intentionally unused declarations with `_`;
|
|
43
|
+
* an underscore-prefixed declaration must remain unused. Package aliases are
|
|
44
|
+
* useful for examples documenting an entry point that is not exported yet.
|
|
45
|
+
* Package subpaths can be aliased independently. Each alias target must be an
|
|
46
|
+
* absolute TypeScript module path exposing the named exports used by the
|
|
47
|
+
* example.
|
|
48
|
+
*
|
|
49
|
+
* ### Example
|
|
50
|
+
*
|
|
51
|
+
* ```ts
|
|
52
|
+
* import { testJSDocExamples } from "@evolu/nodejs/TestJSDoc";
|
|
53
|
+
* import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
54
|
+
* import { tmpdir } from "node:os";
|
|
55
|
+
* import { join } from "node:path";
|
|
56
|
+
*
|
|
57
|
+
* const directory = await mkdtemp(join(tmpdir(), "evolu-jsdoc-example-"));
|
|
58
|
+
* try {
|
|
59
|
+
* const sourcePath = join(directory, "Example.ts");
|
|
60
|
+
* await writeFile(
|
|
61
|
+
* sourcePath,
|
|
62
|
+
* [
|
|
63
|
+
* "/**",
|
|
64
|
+
* " * ``" + "`ts",
|
|
65
|
+
* ' * import { assertEqual } from "@evolu/common";',
|
|
66
|
+
* " *",
|
|
67
|
+
* " * assertEqual(1 + 1, 2);",
|
|
68
|
+
* " * ``" + "`",
|
|
69
|
+
* " *" + "/",
|
|
70
|
+
* "export {};",
|
|
71
|
+
* ].join("\n"),
|
|
72
|
+
* );
|
|
73
|
+
* await testJSDocExamples({
|
|
74
|
+
* cwd: process.cwd(),
|
|
75
|
+
* include: sourcePath,
|
|
76
|
+
* });
|
|
77
|
+
* } finally {
|
|
78
|
+
* await rm(directory, { force: true, recursive: true });
|
|
79
|
+
* }
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
export declare const testJSDocExamples: ({ include, aliases, cwd, typescriptPackage, }: TestJSDocExamplesOptions) => Promise<void>;
|
|
83
|
+
//# sourceMappingURL=TestJSDoc.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for testing TypeScript examples embedded in JSDoc and Markdown.
|
|
3
|
+
*
|
|
4
|
+
* These utilities use the dedicated `@evolu/nodejs/TestJSDoc` entry point so
|
|
5
|
+
* normal `@evolu/nodejs` imports do not evaluate TypeScript and JSDoc tooling.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
import { mapArray } from "@evolu/common";
|
|
10
|
+
import assert from "node:assert/strict";
|
|
11
|
+
import { spawn } from "node:child_process";
|
|
12
|
+
import { globSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync, } from "node:fs";
|
|
13
|
+
import { findPackageJSON } from "node:module";
|
|
14
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
15
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
16
|
+
import { stripVTControlCharacters } from "node:util";
|
|
17
|
+
const jsdocPattern = /\/\*\*[\s\S]*?\*\//gu;
|
|
18
|
+
const fencePattern = /(?:```|~~~)([^\n]*)\n([\s\S]*?)(?:(```|~~~)|$)/gu;
|
|
19
|
+
const markdownFilePattern = /\.mdx?$/iu;
|
|
20
|
+
const packageNamePattern = /^(?:@[a-z\d][a-z\d._~-]*\/)?[a-z\d][a-z\d._~-]*$/iu;
|
|
21
|
+
const packageSubpathSegmentPattern = /^[a-z\d][a-z\d._~-]*$/iu;
|
|
22
|
+
const polyfillsPath = fileURLToPath(import.meta.resolve("@evolu/common/polyfills"));
|
|
23
|
+
/**
|
|
24
|
+
* Lints, compiles, and executes every TypeScript example in the included JSDoc
|
|
25
|
+
* comments and Markdown files.
|
|
26
|
+
*
|
|
27
|
+
* Examples are compiled together as isolated TypeScript modules. Examples
|
|
28
|
+
* without compilation errors are then imported in source order by one Node.js
|
|
29
|
+
* process. Lint, compilation, and execution failures are reported together.
|
|
30
|
+
* Examples are linted with `@evolu/oxlint-config`. Evolu's required polyfills
|
|
31
|
+
* are installed before each example runs.
|
|
32
|
+
*
|
|
33
|
+
* Install `@evolu/oxlint-config`, `@evolu/typescript-config`, `oxlint`,
|
|
34
|
+
* `oxlint-tsgolint`, and TypeScript as development dependencies in the project
|
|
35
|
+
* that calls this helper.
|
|
36
|
+
*
|
|
37
|
+
* Write every TypeScript fence as a standalone, deterministic example and
|
|
38
|
+
* explicitly import its dependencies and assertions. Use `assertType` to prove
|
|
39
|
+
* static contracts, `assertEqual` for Data comparisons, `assert` with a
|
|
40
|
+
* descriptive message for invariants and narrowing, and `assertOk` or
|
|
41
|
+
* `assertErr` for Results. Prefix intentionally unused declarations with `_`;
|
|
42
|
+
* an underscore-prefixed declaration must remain unused. Package aliases are
|
|
43
|
+
* useful for examples documenting an entry point that is not exported yet.
|
|
44
|
+
* Package subpaths can be aliased independently. Each alias target must be an
|
|
45
|
+
* absolute TypeScript module path exposing the named exports used by the
|
|
46
|
+
* example.
|
|
47
|
+
*
|
|
48
|
+
* ### Example
|
|
49
|
+
*
|
|
50
|
+
* ```ts
|
|
51
|
+
* import { testJSDocExamples } from "@evolu/nodejs/TestJSDoc";
|
|
52
|
+
* import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
53
|
+
* import { tmpdir } from "node:os";
|
|
54
|
+
* import { join } from "node:path";
|
|
55
|
+
*
|
|
56
|
+
* const directory = await mkdtemp(join(tmpdir(), "evolu-jsdoc-example-"));
|
|
57
|
+
* try {
|
|
58
|
+
* const sourcePath = join(directory, "Example.ts");
|
|
59
|
+
* await writeFile(
|
|
60
|
+
* sourcePath,
|
|
61
|
+
* [
|
|
62
|
+
* "/**",
|
|
63
|
+
* " * ``" + "`ts",
|
|
64
|
+
* ' * import { assertEqual } from "@evolu/common";',
|
|
65
|
+
* " *",
|
|
66
|
+
* " * assertEqual(1 + 1, 2);",
|
|
67
|
+
* " * ``" + "`",
|
|
68
|
+
* " *" + "/",
|
|
69
|
+
* "export {};",
|
|
70
|
+
* ].join("\n"),
|
|
71
|
+
* );
|
|
72
|
+
* await testJSDocExamples({
|
|
73
|
+
* cwd: process.cwd(),
|
|
74
|
+
* include: sourcePath,
|
|
75
|
+
* });
|
|
76
|
+
* } finally {
|
|
77
|
+
* await rm(directory, { force: true, recursive: true });
|
|
78
|
+
* }
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
export const testJSDocExamples = async ({ include, aliases = {}, cwd = process.cwd(), typescriptPackage = "typescript", }) => {
|
|
82
|
+
const workingDirectory = realpathSync(resolve(cwd));
|
|
83
|
+
const patterns = typeof include === "string" ? [include] : include;
|
|
84
|
+
assert(patterns.length > 0, "Documentation example tests require included files.");
|
|
85
|
+
const filePaths = Array.from(new Set(globSync(patterns, { cwd: workingDirectory }).map((filePath) => resolve(workingDirectory, filePath)))).toSorted();
|
|
86
|
+
assert(filePaths.length > 0, "No files matched the documentation example patterns.");
|
|
87
|
+
const examples = filePaths.flatMap((filePath) => extractDocumentationExamples(readFileSync(filePath, "utf8"), filePath));
|
|
88
|
+
assert(examples.length > 0, "No TypeScript documentation examples were found.");
|
|
89
|
+
const compilerPath = resolveTypeScriptCompiler(workingDirectory, typescriptPackage);
|
|
90
|
+
const typescriptConfigPath = resolve(dirname(resolvePackageJSON(workingDirectory, "@evolu/typescript-config")), "base.json");
|
|
91
|
+
const temporaryRoot = join(workingDirectory, "tmp");
|
|
92
|
+
mkdirSync(temporaryRoot, { recursive: true });
|
|
93
|
+
const temporaryDirectory = mkdtempSync(join(temporaryRoot, "evolu-jsdoc-"));
|
|
94
|
+
try {
|
|
95
|
+
writeFileSync(join(temporaryDirectory, "package.json"), JSON.stringify({ type: "module" }));
|
|
96
|
+
createPackageAliases(temporaryDirectory, aliases);
|
|
97
|
+
const generatedExamples = examples.map((example, index) => {
|
|
98
|
+
const generatedPath = join(temporaryDirectory, `example-${index}.ts`);
|
|
99
|
+
writeFileSync(generatedPath, transformJSDocExample(example, generatedPath));
|
|
100
|
+
return { ...example, generatedPath };
|
|
101
|
+
});
|
|
102
|
+
const compilerConfigPath = join(temporaryDirectory, "tsconfig.json");
|
|
103
|
+
writeFileSync(compilerConfigPath, JSON.stringify({
|
|
104
|
+
extends: typescriptConfigPath,
|
|
105
|
+
compilerOptions: {
|
|
106
|
+
allowImportingTsExtensions: true,
|
|
107
|
+
composite: false,
|
|
108
|
+
declaration: false,
|
|
109
|
+
declarationMap: false,
|
|
110
|
+
incremental: false,
|
|
111
|
+
lib: ["dom", "esnext"],
|
|
112
|
+
module: "NodeNext",
|
|
113
|
+
noEmit: true,
|
|
114
|
+
noUnusedLocals: false,
|
|
115
|
+
noUnusedParameters: false,
|
|
116
|
+
target: "es2022",
|
|
117
|
+
types: ["node"],
|
|
118
|
+
},
|
|
119
|
+
files: generatedExamples.map(({ generatedPath }) => generatedPath),
|
|
120
|
+
}));
|
|
121
|
+
let lintError;
|
|
122
|
+
try {
|
|
123
|
+
await lintJSDocExamples(generatedExamples, compilerConfigPath, workingDirectory);
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
assert(error instanceof Error, "Expected Oxlint to fail with an Error.");
|
|
127
|
+
lintError = error;
|
|
128
|
+
}
|
|
129
|
+
let compilationError;
|
|
130
|
+
try {
|
|
131
|
+
await runProcess(process.execPath, [compilerPath, "--project", compilerConfigPath], workingDirectory, "Documentation example TypeScript compilation", {
|
|
132
|
+
details: generatedExamplesToDetails(generatedExamples, temporaryDirectory, workingDirectory),
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
assert(error instanceof Error, "Expected TypeScript compilation to fail with an Error.");
|
|
137
|
+
compilationError = error;
|
|
138
|
+
}
|
|
139
|
+
const runnableExamples = compilationError === undefined
|
|
140
|
+
? generatedExamples
|
|
141
|
+
: getExamplesWithoutCompilationErrors(generatedExamples, compilationError, workingDirectory);
|
|
142
|
+
let executionError;
|
|
143
|
+
try {
|
|
144
|
+
await runJSDocExamples(runnableExamples, temporaryDirectory, workingDirectory);
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
assert(error instanceof Error, "Expected example execution to fail with an Error.");
|
|
148
|
+
executionError = error;
|
|
149
|
+
}
|
|
150
|
+
const failures = [];
|
|
151
|
+
if (lintError !== undefined)
|
|
152
|
+
failures.push(lintError);
|
|
153
|
+
if (compilationError !== undefined)
|
|
154
|
+
failures.push(compilationError);
|
|
155
|
+
if (executionError !== undefined)
|
|
156
|
+
failures.push(executionError);
|
|
157
|
+
const firstFailure = failures[0];
|
|
158
|
+
if (failures.length === 1 && firstFailure !== undefined) {
|
|
159
|
+
throw firstFailure;
|
|
160
|
+
}
|
|
161
|
+
if (failures.length > 1) {
|
|
162
|
+
throw new AggregateError(failures.flatMap((failure) => failure instanceof AggregateError
|
|
163
|
+
? failure.errors
|
|
164
|
+
: [failure]), mapArray(failures, ({ message }) => message).join("\n"));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
rmSync(temporaryDirectory, { force: true, recursive: true });
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
const lintJSDocExamples = async (examples, compilerConfigPath, workingDirectory) => {
|
|
172
|
+
const oxlintPath = resolvePackageBinary(workingDirectory, "oxlint", "oxlint");
|
|
173
|
+
const tsgolintPath = resolvePackageBinary(workingDirectory, "oxlint-tsgolint", "tsgolint");
|
|
174
|
+
const sharedOxlintConfigPath = resolve(dirname(resolvePackageJSON(workingDirectory, "@evolu/oxlint-config")), "config.jsonc");
|
|
175
|
+
const oxlintConfigPath = join(dirname(compilerConfigPath), "oxlint.json");
|
|
176
|
+
writeFileSync(oxlintConfigPath, JSON.stringify({
|
|
177
|
+
extends: [sharedOxlintConfigPath],
|
|
178
|
+
overrides: [
|
|
179
|
+
{
|
|
180
|
+
files: ["**/*.{ts,tsx,mts}"],
|
|
181
|
+
rules: {
|
|
182
|
+
"eslint/no-unused-vars": [
|
|
183
|
+
"error",
|
|
184
|
+
{
|
|
185
|
+
args: "all",
|
|
186
|
+
argsIgnorePattern: "^_",
|
|
187
|
+
caughtErrors: "all",
|
|
188
|
+
caughtErrorsIgnorePattern: "^_",
|
|
189
|
+
destructuredArrayIgnorePattern: "^_",
|
|
190
|
+
reportUsedIgnorePattern: true,
|
|
191
|
+
varsIgnorePattern: "^_",
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
],
|
|
197
|
+
}));
|
|
198
|
+
await runProcess(process.execPath, [
|
|
199
|
+
oxlintPath,
|
|
200
|
+
"--config",
|
|
201
|
+
oxlintConfigPath,
|
|
202
|
+
"--deny-warnings",
|
|
203
|
+
"--format",
|
|
204
|
+
"json",
|
|
205
|
+
"--tsconfig",
|
|
206
|
+
compilerConfigPath,
|
|
207
|
+
"--report-unused-disable-directives-severity=error",
|
|
208
|
+
...mapArray(examples, ({ generatedPath }) => generatedPath),
|
|
209
|
+
], workingDirectory, "Documentation example Oxlint", {
|
|
210
|
+
environment: { OXLINT_TSGOLINT_PATH: tsgolintPath },
|
|
211
|
+
exitErrorFromOutput: (stdout, stderr) => oxlintOutputToError(stdout, stderr, examples, workingDirectory),
|
|
212
|
+
});
|
|
213
|
+
};
|
|
214
|
+
const oxlintOutputToError = (stdout, stderr, examples, workingDirectory) => {
|
|
215
|
+
const { diagnostics } = JSON.parse(stdout);
|
|
216
|
+
if (diagnostics.length === 0) {
|
|
217
|
+
return new Error(["Documentation example Oxlint failed.", stdout, stderr]
|
|
218
|
+
.filter(Boolean)
|
|
219
|
+
.join("\n"));
|
|
220
|
+
}
|
|
221
|
+
const examplesByGeneratedPath = new Map(mapArray(examples, (example) => [example.generatedPath, example]));
|
|
222
|
+
const errors = diagnostics
|
|
223
|
+
.map((diagnostic) => {
|
|
224
|
+
const example = examplesByGeneratedPath.get(resolve(workingDirectory, diagnostic.filename));
|
|
225
|
+
assert(example !== undefined, `Oxlint reported an unknown generated example: ${diagnostic.filename}.`);
|
|
226
|
+
const label = diagnostic.labels[0];
|
|
227
|
+
assert(label !== undefined, `Oxlint did not report a source location for ${diagnostic.filename}.`);
|
|
228
|
+
const line = example.sourceLine + label.span.line - 5;
|
|
229
|
+
const filePath = relative(workingDirectory, example.filePath);
|
|
230
|
+
const code = diagnostic.code ?? "";
|
|
231
|
+
return {
|
|
232
|
+
code,
|
|
233
|
+
filePath,
|
|
234
|
+
line,
|
|
235
|
+
message: `${filePath}:${line}: ${code === "" ? "" : `${code}: `}${diagnostic.message}${diagnostic.help === undefined ? "" : ` ${diagnostic.help}`}`,
|
|
236
|
+
};
|
|
237
|
+
})
|
|
238
|
+
.toSorted((first, second) => first.filePath.localeCompare(second.filePath) ||
|
|
239
|
+
first.line - second.line ||
|
|
240
|
+
first.code.localeCompare(second.code))
|
|
241
|
+
.map(({ message }) => new Error(message));
|
|
242
|
+
return new AggregateError(errors, [
|
|
243
|
+
"Documentation example Oxlint failed.",
|
|
244
|
+
...mapArray(errors, ({ message }) => `- ${message}`),
|
|
245
|
+
].join("\n"));
|
|
246
|
+
};
|
|
247
|
+
const generatedExamplesToDetails = (examples, temporaryDirectory, workingDirectory) => [
|
|
248
|
+
"Generated example sources:",
|
|
249
|
+
...mapArray(examples, ({ filePath, generatedPath, line }) => `- ${relative(temporaryDirectory, generatedPath)}: ${relative(workingDirectory, filePath)}:${line}`),
|
|
250
|
+
].join("\n");
|
|
251
|
+
const getExamplesWithoutCompilationErrors = (examples, compilationError, workingDirectory) => {
|
|
252
|
+
const compilationErrorMessage = stripVTControlCharacters(compilationError.message);
|
|
253
|
+
const examplesWithErrors = examples.filter(({ generatedPath }) => [generatedPath, relative(workingDirectory, generatedPath)].some((path) => [`${path}(`, `${path}:`].some((segment) => compilationErrorMessage.includes(segment))));
|
|
254
|
+
if (examplesWithErrors.length === 0)
|
|
255
|
+
return [];
|
|
256
|
+
return examples.filter((example) => !examplesWithErrors.includes(example));
|
|
257
|
+
};
|
|
258
|
+
const extractDocumentationExamples = (source, filePath) => {
|
|
259
|
+
if (markdownFilePattern.test(filePath)) {
|
|
260
|
+
return extractFencedExamples(source, source, filePath, 0, false);
|
|
261
|
+
}
|
|
262
|
+
const examples = [];
|
|
263
|
+
for (const jsdoc of source.matchAll(jsdocPattern)) {
|
|
264
|
+
examples.push(...extractFencedExamples(source, jsdoc[0], filePath, jsdoc.index, true));
|
|
265
|
+
}
|
|
266
|
+
return examples;
|
|
267
|
+
};
|
|
268
|
+
const extractFencedExamples = (source, fencedSource, filePath, offset, stripJSDocPrefixes) => {
|
|
269
|
+
const examples = [];
|
|
270
|
+
for (const fence of fencedSource.matchAll(fencePattern)) {
|
|
271
|
+
const metadata = fence[1].trim().toLowerCase().split(/\s+/u);
|
|
272
|
+
if (!["ts", "typescript"].includes(metadata[0])) {
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
const line = getLineNumber(source, offset + fence.index);
|
|
276
|
+
const closingFence = fence[3];
|
|
277
|
+
assert(closingFence !== undefined, `${filePath}:${line} has an unclosed TypeScript example fence.`);
|
|
278
|
+
const untrimmedExampleSource = stripJSDocPrefixes
|
|
279
|
+
? fence[2].replaceAll(/^[ \t]*\* ?/gmu, "")
|
|
280
|
+
: fence[2];
|
|
281
|
+
const exampleSource = untrimmedExampleSource.trim();
|
|
282
|
+
assert(exampleSource.length > 0, `${filePath}:${line} has an empty TypeScript example.`);
|
|
283
|
+
const sourceLine = line +
|
|
284
|
+
getLineNumber(untrimmedExampleSource, untrimmedExampleSource.indexOf(exampleSource));
|
|
285
|
+
examples.push({ filePath, line, source: exampleSource, sourceLine });
|
|
286
|
+
}
|
|
287
|
+
return examples;
|
|
288
|
+
};
|
|
289
|
+
const getLineNumber = (source, offset) => {
|
|
290
|
+
let line = 1;
|
|
291
|
+
for (let index = 0; index < offset; index++) {
|
|
292
|
+
if (source.charCodeAt(index) === 10)
|
|
293
|
+
line++;
|
|
294
|
+
}
|
|
295
|
+
return line;
|
|
296
|
+
};
|
|
297
|
+
const transformJSDocExample = (example, generatedPath) => [
|
|
298
|
+
`import { installPolyfills } from ${JSON.stringify(pathToImportSpecifier(generatedPath, polyfillsPath))};`,
|
|
299
|
+
"installPolyfills();",
|
|
300
|
+
example.source,
|
|
301
|
+
].join("\n\n");
|
|
302
|
+
const resolveTypeScriptCompiler = (workingDirectory, typescriptPackage) => resolvePackageBinary(workingDirectory, typescriptPackage, "tsc");
|
|
303
|
+
const resolvePackageBinary = (workingDirectory, packageName, binaryName) => {
|
|
304
|
+
const packagePath = resolvePackageJSON(workingDirectory, packageName);
|
|
305
|
+
const packageJson = JSON.parse(readFileSync(packagePath, "utf8"));
|
|
306
|
+
const namedBinaries = typeof packageJson.bin === "object" ? Object.values(packageJson.bin) : [];
|
|
307
|
+
const binary = typeof packageJson.bin === "string"
|
|
308
|
+
? packageJson.bin
|
|
309
|
+
: (packageJson.bin?.[binaryName] ??
|
|
310
|
+
(namedBinaries.length === 1 ? namedBinaries[0] : undefined));
|
|
311
|
+
assert(binary !== undefined, `${packageName} does not expose a ${binaryName} executable.`);
|
|
312
|
+
return resolve(dirname(packagePath), binary);
|
|
313
|
+
};
|
|
314
|
+
const resolvePackageJSON = (workingDirectory, packageName) => {
|
|
315
|
+
const packagePath = findPackageJSON(packageName, pathToFileURL(join(workingDirectory, "package.json")));
|
|
316
|
+
assert(packagePath !== undefined, `Cannot resolve ${packageName} from ${workingDirectory}.`);
|
|
317
|
+
return packagePath;
|
|
318
|
+
};
|
|
319
|
+
const runJSDocExamples = async (examples, temporaryDirectory, workingDirectory) => {
|
|
320
|
+
if (examples.length === 0)
|
|
321
|
+
return;
|
|
322
|
+
const runnerPath = join(temporaryDirectory, "run-examples.mjs");
|
|
323
|
+
const resultPath = join(temporaryDirectory, "run-examples-result.json");
|
|
324
|
+
writeFileSync(runnerPath, [
|
|
325
|
+
'import { writeFileSync } from "node:fs";',
|
|
326
|
+
"",
|
|
327
|
+
`const examples = ${JSON.stringify(mapArray(examples, ({ generatedPath }) => pathToFileURL(generatedPath).href))};`,
|
|
328
|
+
"const failures = [];",
|
|
329
|
+
"for (const [index, example] of examples.entries()) {",
|
|
330
|
+
" try {",
|
|
331
|
+
" await import(example);",
|
|
332
|
+
" } catch (error) {",
|
|
333
|
+
" failures.push({",
|
|
334
|
+
" index,",
|
|
335
|
+
" message: error instanceof Error ? error.message : String(error),",
|
|
336
|
+
" });",
|
|
337
|
+
" }",
|
|
338
|
+
"}",
|
|
339
|
+
`writeFileSync(${JSON.stringify(resultPath)}, JSON.stringify({ failures }));`,
|
|
340
|
+
"",
|
|
341
|
+
].join("\n"));
|
|
342
|
+
await runProcess(process.execPath, [runnerPath], workingDirectory, "Node.js execution");
|
|
343
|
+
const { failures } = JSON.parse(readFileSync(resultPath, "utf8"));
|
|
344
|
+
if (failures.length === 0)
|
|
345
|
+
return;
|
|
346
|
+
const errors = mapArray(failures, ({ index, message }) => {
|
|
347
|
+
const example = examples[index];
|
|
348
|
+
assert(example !== undefined, `Expected generated example at index ${index}.`);
|
|
349
|
+
return new Error(`${relative(workingDirectory, example.filePath)}:${example.line}: ${message}`);
|
|
350
|
+
});
|
|
351
|
+
throw new AggregateError(errors, [
|
|
352
|
+
"JSDoc example execution failed.",
|
|
353
|
+
...mapArray(errors, (error) => `- ${error.message}`),
|
|
354
|
+
].join("\n"));
|
|
355
|
+
};
|
|
356
|
+
const createPackageAliases = (temporaryDirectory, aliases) => {
|
|
357
|
+
const aliasesByPackageName = new Map();
|
|
358
|
+
for (const [name, target] of Object.entries(aliases)) {
|
|
359
|
+
assert(isAbsolute(target), `Package alias ${name} must be absolute.`);
|
|
360
|
+
const segments = name.split("/");
|
|
361
|
+
const packageSegmentCount = name.startsWith("@") ? 2 : 1;
|
|
362
|
+
const packageName = segments.slice(0, packageSegmentCount).join("/");
|
|
363
|
+
const subpathSegments = segments.slice(packageSegmentCount);
|
|
364
|
+
assert(packageNamePattern.test(packageName) &&
|
|
365
|
+
subpathSegments.every((segment) => packageSubpathSegmentPattern.test(segment)), `Invalid package alias: ${name}.`);
|
|
366
|
+
const subpath = subpathSegments.join("/");
|
|
367
|
+
const packageAliases = aliasesByPackageName.get(packageName) ?? [];
|
|
368
|
+
packageAliases.push({ subpath, targetPath: realpathSync(target) });
|
|
369
|
+
aliasesByPackageName.set(packageName, packageAliases);
|
|
370
|
+
}
|
|
371
|
+
for (const [packageName, packageAliases] of aliasesByPackageName) {
|
|
372
|
+
const packageSegments = packageName.split("/");
|
|
373
|
+
const packageDirectory = join(temporaryDirectory, "node_modules", ...packageSegments);
|
|
374
|
+
mkdirSync(packageDirectory, { recursive: true });
|
|
375
|
+
const exportsBySubpath = {};
|
|
376
|
+
for (const { subpath, targetPath } of packageAliases) {
|
|
377
|
+
const entryPathWithoutExtension = subpath || "index";
|
|
378
|
+
const exportName = subpath ? `./${subpath}` : ".";
|
|
379
|
+
exportsBySubpath[exportName] = {
|
|
380
|
+
types: `./${entryPathWithoutExtension}.ts`,
|
|
381
|
+
default: `./${entryPathWithoutExtension}.js`,
|
|
382
|
+
};
|
|
383
|
+
for (const extension of ["ts", "js"]) {
|
|
384
|
+
const entryPath = join(packageDirectory, `${entryPathWithoutExtension}.${extension}`);
|
|
385
|
+
mkdirSync(dirname(entryPath), { recursive: true });
|
|
386
|
+
writeFileSync(entryPath, `export * from ${JSON.stringify(pathToImportSpecifier(entryPath, targetPath))};\n`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
writeFileSync(join(packageDirectory, "package.json"), JSON.stringify({
|
|
390
|
+
name: packageName,
|
|
391
|
+
type: "module",
|
|
392
|
+
exports: exportsBySubpath,
|
|
393
|
+
}));
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
const pathToImportSpecifier = (from, to) => {
|
|
397
|
+
const path = relative(dirname(from), to).split(sep).join("/");
|
|
398
|
+
return `./${path}`;
|
|
399
|
+
};
|
|
400
|
+
const runProcess = (command, args, cwd, operation, { details, environment, exitErrorFromOutput, } = {}) => new Promise((resolve, reject) => {
|
|
401
|
+
const child = spawn(command, args, {
|
|
402
|
+
cwd,
|
|
403
|
+
env: environment === undefined
|
|
404
|
+
? process.env
|
|
405
|
+
: { ...process.env, ...environment },
|
|
406
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
407
|
+
});
|
|
408
|
+
const stdout = [];
|
|
409
|
+
const stderr = [];
|
|
410
|
+
child.stdout.on("data", (chunk) => {
|
|
411
|
+
stdout.push(chunk);
|
|
412
|
+
});
|
|
413
|
+
child.stderr.on("data", (chunk) => {
|
|
414
|
+
stderr.push(chunk);
|
|
415
|
+
});
|
|
416
|
+
child.once("error", (error) => {
|
|
417
|
+
reject(new Error([`${operation} failed.`, details].filter(Boolean).join("\n"), {
|
|
418
|
+
cause: error,
|
|
419
|
+
}));
|
|
420
|
+
});
|
|
421
|
+
child.once("close", (code, signal) => {
|
|
422
|
+
if (code === 0) {
|
|
423
|
+
resolve();
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
const stdoutText = Buffer.concat(stdout).toString("utf8");
|
|
427
|
+
const stderrText = Buffer.concat(stderr).toString("utf8");
|
|
428
|
+
if (exitErrorFromOutput !== undefined) {
|
|
429
|
+
try {
|
|
430
|
+
reject(exitErrorFromOutput(stdoutText, stderrText));
|
|
431
|
+
}
|
|
432
|
+
catch (error) {
|
|
433
|
+
reject(new Error([
|
|
434
|
+
`${operation} failed while reading its output.`,
|
|
435
|
+
stdoutText,
|
|
436
|
+
stderrText,
|
|
437
|
+
]
|
|
438
|
+
.filter(Boolean)
|
|
439
|
+
.join("\n"), { cause: error }));
|
|
440
|
+
}
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
reject(new Error([
|
|
444
|
+
`${operation} failed${signal === null ? ` with exit code ${String(code)}` : ` from signal ${signal}`}.`,
|
|
445
|
+
details,
|
|
446
|
+
stdoutText,
|
|
447
|
+
stderrText,
|
|
448
|
+
]
|
|
449
|
+
.filter(Boolean)
|
|
450
|
+
.join("\n")));
|
|
451
|
+
});
|
|
452
|
+
});
|
|
@@ -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"}
|