@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 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,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}".`);
@@ -289,11 +288,11 @@ const testViteBundler = {
289
288
  const output = await vite.build({
290
289
  root: dirname(entryPath),
291
290
  configFile: false,
292
- envFile: false,
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
- assert(outputs.length === 1, "Vite did not return one build output.");
316
+ assertLength(outputs, 1);
318
317
  const viteOutput = outputs[0];
319
- assert(viteOutput, "Vite did not return a build output.");
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
- assert(chunk, "Vite did not emit a JavaScript chunk.");
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(globalThis.Error);
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"}