@evolu/vitest 1.0.1 → 2.0.1
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 +8 -3
- package/dist/src/BrowserConfig.d.ts +10 -0
- package/dist/src/BrowserConfig.d.ts.map +1 -0
- package/dist/src/BrowserConfig.js +15 -0
- package/package.json +7 -17
- package/src/BrowserConfig.test.ts +34 -0
- package/src/BrowserConfig.ts +33 -0
- package/src/index.test.ts +74 -0
- package/dist/src/TestJSDoc.d.ts +0 -75
- package/dist/src/TestJSDoc.d.ts.map +0 -1
- package/dist/src/TestJSDoc.js +0 -376
- package/src/TestJSDoc.ts +0 -574
package/README.md
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
# Evolu Vitest
|
|
2
2
|
|
|
3
|
-
Vitest assertions
|
|
3
|
+
Vitest assertions for Evolu.
|
|
4
|
+
|
|
5
|
+
Evolu uses `node:test` with the platform-agnostic assertions from
|
|
6
|
+
`@evolu/common` for Node-only unit and integration tests. This package remains
|
|
7
|
+
available for projects that use Vitest and want Result assertions with Vitest
|
|
8
|
+
semantics.
|
|
4
9
|
|
|
5
10
|
## Documentation
|
|
6
11
|
|
|
7
|
-
|
|
8
|
-
|
|
12
|
+
See Evolu's [testing convention](https://www.evolu.dev/docs/testing) for runner
|
|
13
|
+
and assertion guidance.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
type BrowserName = "chromium" | "firefox" | "webkit";
|
|
2
|
+
interface BrowserInstance {
|
|
3
|
+
readonly browser: BrowserName;
|
|
4
|
+
}
|
|
5
|
+
export declare const createBrowserInstances: ({ coverage, mode, }: {
|
|
6
|
+
readonly coverage: boolean;
|
|
7
|
+
readonly mode: string;
|
|
8
|
+
}) => Array<BrowserInstance>;
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=BrowserConfig.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BrowserConfig.d.ts","sourceRoot":"","sources":["../../src/BrowserConfig.ts"],"names":[],"mappings":"AAAA,KAAK,WAAW,GAAG,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAC;AAErD,UAAU,eAAe;IACvB,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC;CAC/B;AAkBD,eAAO,MAAM,sBAAsB,wBAGhC;IACD,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB,KAAG,KAAK,CAAC,eAAe,CAIU,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// V8 coverage only works with Chromium.
|
|
2
|
+
const chromiumBrowserNames = ["chromium"];
|
|
3
|
+
const compatibilityBrowserNames = [
|
|
4
|
+
"firefox",
|
|
5
|
+
"webkit",
|
|
6
|
+
];
|
|
7
|
+
const browserNamesByMode = {
|
|
8
|
+
chromium: ["chromium"],
|
|
9
|
+
firefox: ["firefox"],
|
|
10
|
+
webkit: ["webkit"],
|
|
11
|
+
"firefox-webkit": ["firefox", "webkit"],
|
|
12
|
+
};
|
|
13
|
+
export const createBrowserInstances = ({ coverage, mode, }) => (coverage
|
|
14
|
+
? chromiumBrowserNames
|
|
15
|
+
: (browserNamesByMode[mode] ?? compatibilityBrowserNames)).map((browser) => ({ browser }));
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evolu/vitest",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Vitest assertions
|
|
3
|
+
"version": "2.0.1",
|
|
4
|
+
"description": "Vitest assertions for Evolu",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"evolu",
|
|
7
7
|
"vitest"
|
|
@@ -22,29 +22,19 @@
|
|
|
22
22
|
".": {
|
|
23
23
|
"types": "./dist/src/index.d.ts",
|
|
24
24
|
"import": "./dist/src/index.js"
|
|
25
|
-
},
|
|
26
|
-
"./TestJSDoc": {
|
|
27
|
-
"types": "./dist/src/TestJSDoc.d.ts",
|
|
28
|
-
"import": "./dist/src/TestJSDoc.js"
|
|
29
|
-
}
|
|
30
|
-
},
|
|
31
|
-
"typesVersions": {
|
|
32
|
-
"*": {
|
|
33
|
-
"TestJSDoc": [
|
|
34
|
-
"./dist/src/TestJSDoc.d.ts"
|
|
35
|
-
]
|
|
36
25
|
}
|
|
37
26
|
},
|
|
38
27
|
"files": [
|
|
39
28
|
"dist/src/**",
|
|
29
|
+
"!dist/**/*.test.*",
|
|
40
30
|
"src/**",
|
|
41
31
|
"README.md"
|
|
42
32
|
],
|
|
43
33
|
"devDependencies": {
|
|
34
|
+
"@evolu/common": "8.8.0",
|
|
35
|
+
"@evolu/typescript-config": "0.1.1",
|
|
44
36
|
"@typescript/native": "npm:typescript@^7.0.2",
|
|
45
|
-
"vitest": "^4.1.
|
|
46
|
-
"@evolu/common": "8.1.0",
|
|
47
|
-
"@evolu/typescript-config": "0.0.2"
|
|
37
|
+
"vitest": "^4.1.11"
|
|
48
38
|
},
|
|
49
39
|
"peerDependencies": {
|
|
50
40
|
"@evolu/common": "^8.0.0",
|
|
@@ -54,7 +44,7 @@
|
|
|
54
44
|
"access": "public"
|
|
55
45
|
},
|
|
56
46
|
"engines": {
|
|
57
|
-
"node": ">=24.
|
|
47
|
+
"node": ">=24.20.0"
|
|
58
48
|
},
|
|
59
49
|
"sideEffects": false,
|
|
60
50
|
"scripts": {
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { assertEqual } from "@evolu/common";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import { createBrowserInstances } from "./BrowserConfig.ts";
|
|
4
|
+
|
|
5
|
+
describe("createBrowserInstances", () => {
|
|
6
|
+
it("uses Chromium for coverage", () => {
|
|
7
|
+
assertEqual(createBrowserInstances({ coverage: true, mode: "" }), [
|
|
8
|
+
{ browser: "chromium" },
|
|
9
|
+
]);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("defaults to Firefox and WebKit without coverage", () => {
|
|
13
|
+
assertEqual(createBrowserInstances({ coverage: false, mode: "" }), [
|
|
14
|
+
{ browser: "firefox" },
|
|
15
|
+
{ browser: "webkit" },
|
|
16
|
+
]);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("selects browsers by mode", () => {
|
|
20
|
+
assertEqual(createBrowserInstances({ coverage: false, mode: "chromium" }), [
|
|
21
|
+
{ browser: "chromium" },
|
|
22
|
+
]);
|
|
23
|
+
assertEqual(createBrowserInstances({ coverage: false, mode: "firefox" }), [
|
|
24
|
+
{ browser: "firefox" },
|
|
25
|
+
]);
|
|
26
|
+
assertEqual(createBrowserInstances({ coverage: false, mode: "webkit" }), [
|
|
27
|
+
{ browser: "webkit" },
|
|
28
|
+
]);
|
|
29
|
+
assertEqual(
|
|
30
|
+
createBrowserInstances({ coverage: false, mode: "firefox-webkit" }),
|
|
31
|
+
[{ browser: "firefox" }, { browser: "webkit" }],
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
type BrowserName = "chromium" | "firefox" | "webkit";
|
|
2
|
+
|
|
3
|
+
interface BrowserInstance {
|
|
4
|
+
readonly browser: BrowserName;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// V8 coverage only works with Chromium.
|
|
8
|
+
const chromiumBrowserNames: ReadonlyArray<BrowserName> = ["chromium"];
|
|
9
|
+
const compatibilityBrowserNames: ReadonlyArray<BrowserName> = [
|
|
10
|
+
"firefox",
|
|
11
|
+
"webkit",
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const browserNamesByMode: Readonly<
|
|
15
|
+
Record<string, ReadonlyArray<BrowserName> | undefined>
|
|
16
|
+
> = {
|
|
17
|
+
chromium: ["chromium"],
|
|
18
|
+
firefox: ["firefox"],
|
|
19
|
+
webkit: ["webkit"],
|
|
20
|
+
"firefox-webkit": ["firefox", "webkit"],
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const createBrowserInstances = ({
|
|
24
|
+
coverage,
|
|
25
|
+
mode,
|
|
26
|
+
}: {
|
|
27
|
+
readonly coverage: boolean;
|
|
28
|
+
readonly mode: string;
|
|
29
|
+
}): Array<BrowserInstance> =>
|
|
30
|
+
(coverage
|
|
31
|
+
? chromiumBrowserNames
|
|
32
|
+
: (browserNamesByMode[mode] ?? compatibilityBrowserNames)
|
|
33
|
+
).map((browser) => ({ browser }));
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assertThrowsInstanceOf,
|
|
3
|
+
assertType,
|
|
4
|
+
err,
|
|
5
|
+
type Err,
|
|
6
|
+
ok,
|
|
7
|
+
type Ok,
|
|
8
|
+
type Result,
|
|
9
|
+
} from "@evolu/common";
|
|
10
|
+
import { describe, it } from "node:test";
|
|
11
|
+
import { expectErr, expectOk } from "./index.ts";
|
|
12
|
+
|
|
13
|
+
const createOkResult = (): Result<number, string> => ok(42);
|
|
14
|
+
const createErrResult = (): Result<number, string> => err("error");
|
|
15
|
+
|
|
16
|
+
describe("expectOk", () => {
|
|
17
|
+
it("accepts an Ok and narrows the Result", () => {
|
|
18
|
+
const result = createOkResult();
|
|
19
|
+
|
|
20
|
+
expectOk(result, 42);
|
|
21
|
+
|
|
22
|
+
assertType<typeof result, Ok<number>>();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("rejects an Err", () => {
|
|
26
|
+
assertThrowsInstanceOf(() => expectOk(createErrResult(), 42), Error);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("rejects an unexpected value", () => {
|
|
30
|
+
assertThrowsInstanceOf(() => expectOk(createOkResult(), 41), Error);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("narrows a heterogeneous Result union", () => {
|
|
34
|
+
const createResult = ():
|
|
35
|
+
Result<number, "NumberError"> | Result<string, "StringError"> => ok(42);
|
|
36
|
+
const result = createResult();
|
|
37
|
+
|
|
38
|
+
expectOk(result, 42);
|
|
39
|
+
|
|
40
|
+
assertType<typeof result, Ok<number> | Ok<string>>();
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("expectErr", () => {
|
|
45
|
+
it("accepts an Err and narrows the Result", () => {
|
|
46
|
+
const result = createErrResult();
|
|
47
|
+
|
|
48
|
+
expectErr(result, "error");
|
|
49
|
+
|
|
50
|
+
assertType<typeof result, Err<string>>();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("rejects an Ok", () => {
|
|
54
|
+
assertThrowsInstanceOf(() => expectErr(createOkResult(), "error"), Error);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("rejects an unexpected error", () => {
|
|
58
|
+
assertThrowsInstanceOf(
|
|
59
|
+
() => expectErr(createErrResult(), "another error"),
|
|
60
|
+
Error,
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("narrows a heterogeneous Result union", () => {
|
|
65
|
+
const createResult = ():
|
|
66
|
+
Result<number, "NumberError"> | Result<string, "StringError"> =>
|
|
67
|
+
err("NumberError");
|
|
68
|
+
const result = createResult();
|
|
69
|
+
|
|
70
|
+
expectErr(result, "NumberError");
|
|
71
|
+
|
|
72
|
+
assertType<typeof result, Err<"NumberError"> | Err<"StringError">>();
|
|
73
|
+
});
|
|
74
|
+
});
|
package/dist/src/TestJSDoc.d.ts
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Vitest utilities for testing TypeScript examples embedded in JSDoc and
|
|
3
|
-
* Markdown.
|
|
4
|
-
*
|
|
5
|
-
* These utilities use the dedicated `@evolu/vitest/TestJSDoc` entry point so
|
|
6
|
-
* normal `@evolu/vitest` imports do not evaluate Node.js test tooling.
|
|
7
|
-
*
|
|
8
|
-
* @module
|
|
9
|
-
*/
|
|
10
|
-
import { type ReadonlyRecord } from "@evolu/common";
|
|
11
|
-
/** Options for compiling and executing TypeScript documentation examples. */
|
|
12
|
-
export interface TestJSDocExamplesOptions {
|
|
13
|
-
/** Source files or glob patterns resolved from `cwd`. */
|
|
14
|
-
readonly include: string | ReadonlyArray<string>;
|
|
15
|
-
/** Package imports redirected to absolute TypeScript entry paths. */
|
|
16
|
-
readonly aliases?: ReadonlyRecord<string, string>;
|
|
17
|
-
/** Directory used for globbing and package resolution. */
|
|
18
|
-
readonly cwd?: string;
|
|
19
|
-
/** TypeScript compiler package. Defaults to `"typescript"`. */
|
|
20
|
-
readonly typescriptPackage?: string;
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Compiles and executes every TypeScript example in the included JSDoc comments
|
|
24
|
-
* and Markdown files.
|
|
25
|
-
*
|
|
26
|
-
* Examples are compiled together as isolated TypeScript modules. Examples
|
|
27
|
-
* without compilation errors are then run concurrently, bounded by the CPU
|
|
28
|
-
* parallelism available to the process. Compilation and execution failures are
|
|
29
|
-
* reported together. Vitest's `assert`, `expect`, and `expectTypeOf`, along
|
|
30
|
-
* with Evolu's `expectOk` and `expectErr`, are injected into each module.
|
|
31
|
-
*
|
|
32
|
-
* Write every TypeScript fence as a standalone, deterministic example and
|
|
33
|
-
* import its dependencies. Use `expectTypeOf` to prove static contracts and an
|
|
34
|
-
* appropriate runtime assertion such as `expect`, `expectOk`, or `expectErr`
|
|
35
|
-
* instead of describing expected behavior only in comments.
|
|
36
|
-
*
|
|
37
|
-
* Package aliases are useful for examples documenting an entry point that is
|
|
38
|
-
* not exported yet. Package subpaths can be aliased independently. Each alias
|
|
39
|
-
* target must be an absolute TypeScript module path exposing the named exports
|
|
40
|
-
* used by the example.
|
|
41
|
-
*
|
|
42
|
-
* ### Example
|
|
43
|
-
*
|
|
44
|
-
* ```ts
|
|
45
|
-
* import { testJSDocExamples } from "@evolu/vitest/TestJSDoc";
|
|
46
|
-
* import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
47
|
-
* import { tmpdir } from "node:os";
|
|
48
|
-
* import { join } from "node:path";
|
|
49
|
-
*
|
|
50
|
-
* const directory = await mkdtemp(join(tmpdir(), "evolu-jsdoc-example-"));
|
|
51
|
-
* try {
|
|
52
|
-
* const sourcePath = join(directory, "Example.ts");
|
|
53
|
-
* await writeFile(
|
|
54
|
-
* sourcePath,
|
|
55
|
-
* [
|
|
56
|
-
* "/**",
|
|
57
|
-
* " * ``" + "`ts",
|
|
58
|
-
* " * expect(1 + 1).toBe(2);",
|
|
59
|
-
* " * ``" + "`",
|
|
60
|
-
* " *" + "/",
|
|
61
|
-
* "export {};",
|
|
62
|
-
* ].join("\n"),
|
|
63
|
-
* );
|
|
64
|
-
* await testJSDocExamples({
|
|
65
|
-
* cwd: process.cwd(),
|
|
66
|
-
* include: sourcePath,
|
|
67
|
-
* typescriptPackage: "@typescript/native",
|
|
68
|
-
* });
|
|
69
|
-
* } finally {
|
|
70
|
-
* await rm(directory, { force: true, recursive: true });
|
|
71
|
-
* }
|
|
72
|
-
* ```
|
|
73
|
-
*/
|
|
74
|
-
export declare const testJSDocExamples: ({ include, aliases, cwd, typescriptPackage, }: TestJSDocExamplesOptions) => Promise<void>;
|
|
75
|
-
//# sourceMappingURL=TestJSDoc.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"TestJSDoc.d.ts","sourceRoot":"","sources":["../../src/TestJSDoc.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAOL,KAAK,cAAc,EAGpB,MAAM,eAAe,CAAC;AAkBvB,6EAA6E;AAC7E,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;AA8BD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AACH,eAAO,MAAM,iBAAiB,kDAK3B,wBAAwB,KAAG,OAAO,CAAC,IAAI,CAsIzC,CAAC"}
|
package/dist/src/TestJSDoc.js
DELETED
|
@@ -1,376 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Vitest utilities for testing TypeScript examples embedded in JSDoc and
|
|
3
|
-
* Markdown.
|
|
4
|
-
*
|
|
5
|
-
* These utilities use the dedicated `@evolu/vitest/TestJSDoc` entry point so
|
|
6
|
-
* normal `@evolu/vitest` imports do not evaluate Node.js test tooling.
|
|
7
|
-
*
|
|
8
|
-
* @module
|
|
9
|
-
*/
|
|
10
|
-
var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
|
|
11
|
-
if (value !== null && value !== void 0) {
|
|
12
|
-
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
|
|
13
|
-
var dispose, inner;
|
|
14
|
-
if (async) {
|
|
15
|
-
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
|
|
16
|
-
dispose = value[Symbol.asyncDispose];
|
|
17
|
-
}
|
|
18
|
-
if (dispose === void 0) {
|
|
19
|
-
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
|
|
20
|
-
dispose = value[Symbol.dispose];
|
|
21
|
-
if (async) inner = dispose;
|
|
22
|
-
}
|
|
23
|
-
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
|
|
24
|
-
if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
|
|
25
|
-
env.stack.push({ value: value, dispose: dispose, async: async });
|
|
26
|
-
}
|
|
27
|
-
else if (async) {
|
|
28
|
-
env.stack.push({ async: true });
|
|
29
|
-
}
|
|
30
|
-
return value;
|
|
31
|
-
};
|
|
32
|
-
var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
|
|
33
|
-
return function (env) {
|
|
34
|
-
function fail(e) {
|
|
35
|
-
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
|
|
36
|
-
env.hasError = true;
|
|
37
|
-
}
|
|
38
|
-
var r, s = 0;
|
|
39
|
-
function next() {
|
|
40
|
-
while (r = env.stack.pop()) {
|
|
41
|
-
try {
|
|
42
|
-
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
|
|
43
|
-
if (r.dispose) {
|
|
44
|
-
var result = r.dispose.call(r.value);
|
|
45
|
-
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
|
|
46
|
-
}
|
|
47
|
-
else s |= 1;
|
|
48
|
-
}
|
|
49
|
-
catch (e) {
|
|
50
|
-
fail(e);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
|
|
54
|
-
if (env.hasError) throw env.error;
|
|
55
|
-
}
|
|
56
|
-
return next();
|
|
57
|
-
};
|
|
58
|
-
})(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
59
|
-
var e = new Error(message);
|
|
60
|
-
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
61
|
-
});
|
|
62
|
-
import { allSettled, createRun, filterArray, isErr, mapArray, PositiveInt, safelyStringifyUnknownValue, tryAsync, } from "@evolu/common";
|
|
63
|
-
import assert from "node:assert/strict";
|
|
64
|
-
import { spawn } from "node:child_process";
|
|
65
|
-
import { globSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync, } from "node:fs";
|
|
66
|
-
import { findPackageJSON } from "node:module";
|
|
67
|
-
import { availableParallelism } from "node:os";
|
|
68
|
-
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
69
|
-
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
70
|
-
import { stripVTControlCharacters } from "node:util";
|
|
71
|
-
const jsdocPattern = /\/\*\*[\s\S]*?\*\//g;
|
|
72
|
-
const fencePattern = /(?:```|~~~)([^\n]*)\n([\s\S]*?)(?:(```|~~~)|$)/g;
|
|
73
|
-
const markdownFilePattern = /\.mdx?$/i;
|
|
74
|
-
const packageNamePattern = /^(?:@[a-z\d][a-z\d._~-]*\/)?[a-z\d][a-z\d._~-]*$/i;
|
|
75
|
-
const packageSubpathSegmentPattern = /^[a-z\d][a-z\d._~-]*$/i;
|
|
76
|
-
const polyfillsPath = fileURLToPath(import.meta.resolve("@evolu/common/polyfills"));
|
|
77
|
-
/**
|
|
78
|
-
* Compiles and executes every TypeScript example in the included JSDoc comments
|
|
79
|
-
* and Markdown files.
|
|
80
|
-
*
|
|
81
|
-
* Examples are compiled together as isolated TypeScript modules. Examples
|
|
82
|
-
* without compilation errors are then run concurrently, bounded by the CPU
|
|
83
|
-
* parallelism available to the process. Compilation and execution failures are
|
|
84
|
-
* reported together. Vitest's `assert`, `expect`, and `expectTypeOf`, along
|
|
85
|
-
* with Evolu's `expectOk` and `expectErr`, are injected into each module.
|
|
86
|
-
*
|
|
87
|
-
* Write every TypeScript fence as a standalone, deterministic example and
|
|
88
|
-
* import its dependencies. Use `expectTypeOf` to prove static contracts and an
|
|
89
|
-
* appropriate runtime assertion such as `expect`, `expectOk`, or `expectErr`
|
|
90
|
-
* instead of describing expected behavior only in comments.
|
|
91
|
-
*
|
|
92
|
-
* Package aliases are useful for examples documenting an entry point that is
|
|
93
|
-
* not exported yet. Package subpaths can be aliased independently. Each alias
|
|
94
|
-
* target must be an absolute TypeScript module path exposing the named exports
|
|
95
|
-
* used by the example.
|
|
96
|
-
*
|
|
97
|
-
* ### Example
|
|
98
|
-
*
|
|
99
|
-
* ```ts
|
|
100
|
-
* import { testJSDocExamples } from "@evolu/vitest/TestJSDoc";
|
|
101
|
-
* import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
102
|
-
* import { tmpdir } from "node:os";
|
|
103
|
-
* import { join } from "node:path";
|
|
104
|
-
*
|
|
105
|
-
* const directory = await mkdtemp(join(tmpdir(), "evolu-jsdoc-example-"));
|
|
106
|
-
* try {
|
|
107
|
-
* const sourcePath = join(directory, "Example.ts");
|
|
108
|
-
* await writeFile(
|
|
109
|
-
* sourcePath,
|
|
110
|
-
* [
|
|
111
|
-
* "/**",
|
|
112
|
-
* " * ``" + "`ts",
|
|
113
|
-
* " * expect(1 + 1).toBe(2);",
|
|
114
|
-
* " * ``" + "`",
|
|
115
|
-
* " *" + "/",
|
|
116
|
-
* "export {};",
|
|
117
|
-
* ].join("\n"),
|
|
118
|
-
* );
|
|
119
|
-
* await testJSDocExamples({
|
|
120
|
-
* cwd: process.cwd(),
|
|
121
|
-
* include: sourcePath,
|
|
122
|
-
* typescriptPackage: "@typescript/native",
|
|
123
|
-
* });
|
|
124
|
-
* } finally {
|
|
125
|
-
* await rm(directory, { force: true, recursive: true });
|
|
126
|
-
* }
|
|
127
|
-
* ```
|
|
128
|
-
*/
|
|
129
|
-
export const testJSDocExamples = async ({ include, aliases = {}, cwd = process.cwd(), typescriptPackage = "typescript", }) => {
|
|
130
|
-
const workingDirectory = realpathSync(resolve(cwd));
|
|
131
|
-
const patterns = typeof include === "string" ? [include] : include;
|
|
132
|
-
assert(patterns.length > 0, "Documentation example tests require included files.");
|
|
133
|
-
const filePaths = Array.from(new Set(globSync(patterns, { cwd: workingDirectory }).map((filePath) => resolve(workingDirectory, filePath)))).sort();
|
|
134
|
-
assert(filePaths.length > 0, "No files matched the documentation example patterns.");
|
|
135
|
-
const examples = filePaths.flatMap((filePath) => extractDocumentationExamples(readFileSync(filePath, "utf8"), filePath));
|
|
136
|
-
assert(examples.length > 0, "No TypeScript documentation examples were found.");
|
|
137
|
-
const compilerPath = resolveTypeScriptCompiler(workingDirectory, typescriptPackage);
|
|
138
|
-
const temporaryRoot = join(workingDirectory, "tmp");
|
|
139
|
-
mkdirSync(temporaryRoot, { recursive: true });
|
|
140
|
-
const temporaryDirectory = mkdtempSync(join(temporaryRoot, "evolu-jsdoc-"));
|
|
141
|
-
try {
|
|
142
|
-
writeFileSync(join(temporaryDirectory, "package.json"), JSON.stringify({ type: "module" }));
|
|
143
|
-
createPackageAliases(temporaryDirectory, aliases);
|
|
144
|
-
const generatedExamples = examples.map((example, index) => {
|
|
145
|
-
const generatedPath = join(temporaryDirectory, `example-${index}.ts`);
|
|
146
|
-
writeFileSync(generatedPath, transformJSDocExample(example, generatedPath));
|
|
147
|
-
return { ...example, generatedPath };
|
|
148
|
-
});
|
|
149
|
-
// TODO: Lint generated examples with Oxc after the repository migration.
|
|
150
|
-
// Treat top-level declarations as example output and ignore `using`
|
|
151
|
-
// bindings whose disposal is their purpose.
|
|
152
|
-
const compilerConfigPath = join(temporaryDirectory, "tsconfig.json");
|
|
153
|
-
writeFileSync(compilerConfigPath, JSON.stringify({
|
|
154
|
-
compilerOptions: {
|
|
155
|
-
allowImportingTsExtensions: true,
|
|
156
|
-
erasableSyntaxOnly: true,
|
|
157
|
-
esModuleInterop: true,
|
|
158
|
-
exactOptionalPropertyTypes: true,
|
|
159
|
-
lib: ["DOM", "ESNext"],
|
|
160
|
-
module: "NodeNext",
|
|
161
|
-
moduleResolution: "NodeNext",
|
|
162
|
-
noEmit: true,
|
|
163
|
-
skipLibCheck: true,
|
|
164
|
-
strict: true,
|
|
165
|
-
target: "ES2022",
|
|
166
|
-
types: ["node"],
|
|
167
|
-
verbatimModuleSyntax: true,
|
|
168
|
-
},
|
|
169
|
-
files: generatedExamples.map(({ generatedPath }) => generatedPath),
|
|
170
|
-
}));
|
|
171
|
-
let compilationError;
|
|
172
|
-
try {
|
|
173
|
-
await runProcess(process.execPath, [compilerPath, "--project", compilerConfigPath], workingDirectory, "Documentation example TypeScript compilation", [
|
|
174
|
-
"Generated example sources:",
|
|
175
|
-
...generatedExamples.map(({ filePath, generatedPath, line }) => `- ${relative(temporaryDirectory, generatedPath)}: ${relative(workingDirectory, filePath)}:${line}`),
|
|
176
|
-
].join("\n"));
|
|
177
|
-
}
|
|
178
|
-
catch (error) {
|
|
179
|
-
compilationError =
|
|
180
|
-
error instanceof Error
|
|
181
|
-
? error
|
|
182
|
-
: new Error(safelyStringifyUnknownValue(error));
|
|
183
|
-
}
|
|
184
|
-
const runnableExamples = compilationError === undefined
|
|
185
|
-
? generatedExamples
|
|
186
|
-
: getExamplesWithoutCompilationErrors(generatedExamples, compilationError, workingDirectory);
|
|
187
|
-
let executionError;
|
|
188
|
-
try {
|
|
189
|
-
await runJSDocExamples(runnableExamples, workingDirectory);
|
|
190
|
-
}
|
|
191
|
-
catch (error) {
|
|
192
|
-
executionError =
|
|
193
|
-
error instanceof Error
|
|
194
|
-
? error
|
|
195
|
-
: new Error(safelyStringifyUnknownValue(error));
|
|
196
|
-
}
|
|
197
|
-
if (compilationError !== undefined && executionError !== undefined) {
|
|
198
|
-
const executionErrors = executionError instanceof AggregateError
|
|
199
|
-
? executionError.errors
|
|
200
|
-
: [executionError];
|
|
201
|
-
throw new AggregateError([compilationError, ...executionErrors], `${compilationError.message}\n${executionError.message}`);
|
|
202
|
-
}
|
|
203
|
-
if (compilationError !== undefined)
|
|
204
|
-
throw compilationError;
|
|
205
|
-
if (executionError !== undefined)
|
|
206
|
-
throw executionError;
|
|
207
|
-
}
|
|
208
|
-
finally {
|
|
209
|
-
rmSync(temporaryDirectory, { force: true, recursive: true });
|
|
210
|
-
}
|
|
211
|
-
};
|
|
212
|
-
const getExamplesWithoutCompilationErrors = (examples, compilationError, workingDirectory) => {
|
|
213
|
-
const compilationErrorMessage = stripVTControlCharacters(compilationError.message);
|
|
214
|
-
const examplesWithErrors = examples.filter(({ generatedPath }) => [generatedPath, relative(workingDirectory, generatedPath)].some((path) => [`${path}(`, `${path}:`].some((segment) => compilationErrorMessage.includes(segment))));
|
|
215
|
-
if (examplesWithErrors.length === 0)
|
|
216
|
-
return [];
|
|
217
|
-
return examples.filter((example) => !examplesWithErrors.includes(example));
|
|
218
|
-
};
|
|
219
|
-
const extractDocumentationExamples = (source, filePath) => {
|
|
220
|
-
if (markdownFilePattern.test(filePath)) {
|
|
221
|
-
return extractFencedExamples(source, source, filePath, 0, false);
|
|
222
|
-
}
|
|
223
|
-
const examples = [];
|
|
224
|
-
for (const jsdoc of source.matchAll(jsdocPattern)) {
|
|
225
|
-
examples.push(...extractFencedExamples(source, jsdoc[0], filePath, jsdoc.index, true));
|
|
226
|
-
}
|
|
227
|
-
return examples;
|
|
228
|
-
};
|
|
229
|
-
const extractFencedExamples = (source, fencedSource, filePath, offset, stripJSDocPrefixes) => {
|
|
230
|
-
const examples = [];
|
|
231
|
-
for (const fence of fencedSource.matchAll(fencePattern)) {
|
|
232
|
-
const metadata = fence[1].trim().toLowerCase().split(/\s+/);
|
|
233
|
-
if (!["ts", "typescript"].includes(metadata[0])) {
|
|
234
|
-
continue;
|
|
235
|
-
}
|
|
236
|
-
const line = getLineNumber(source, offset + fence.index);
|
|
237
|
-
const closingFence = fence[3];
|
|
238
|
-
assert(closingFence !== undefined, `${filePath}:${line} has an unclosed TypeScript example fence.`);
|
|
239
|
-
const exampleSource = (stripJSDocPrefixes ? fence[2].replace(/^[ \t]*\* ?/gm, "") : fence[2]).trim();
|
|
240
|
-
assert(exampleSource.length > 0, `${filePath}:${line} has an empty TypeScript example.`);
|
|
241
|
-
examples.push({ filePath, line, source: exampleSource });
|
|
242
|
-
}
|
|
243
|
-
return examples;
|
|
244
|
-
};
|
|
245
|
-
const getLineNumber = (source, offset) => {
|
|
246
|
-
let line = 1;
|
|
247
|
-
for (let index = 0; index < offset; index++) {
|
|
248
|
-
if (source.charCodeAt(index) === 10)
|
|
249
|
-
line++;
|
|
250
|
-
}
|
|
251
|
-
return line;
|
|
252
|
-
};
|
|
253
|
-
const transformJSDocExample = (example, generatedPath) => [
|
|
254
|
-
`import { installPolyfills } from ${JSON.stringify(pathToImportSpecifier(generatedPath, polyfillsPath))};`,
|
|
255
|
-
'import { expectErr, expectOk } from "@evolu/vitest";',
|
|
256
|
-
'import { assert, expect, expectTypeOf } from "vitest";',
|
|
257
|
-
"installPolyfills();",
|
|
258
|
-
example.source,
|
|
259
|
-
].join("\n\n");
|
|
260
|
-
const resolveTypeScriptCompiler = (workingDirectory, typescriptPackage) => {
|
|
261
|
-
const packagePath = findPackageJSON(typescriptPackage, pathToFileURL(join(workingDirectory, "package.json")));
|
|
262
|
-
assert(packagePath !== undefined, `Cannot resolve ${typescriptPackage} from ${workingDirectory}.`);
|
|
263
|
-
const packageJson = JSON.parse(readFileSync(packagePath, "utf8"));
|
|
264
|
-
const compiler = typeof packageJson.bin === "string" ? packageJson.bin : packageJson.bin.tsc;
|
|
265
|
-
assert(compiler !== undefined, `${typescriptPackage} does not expose a tsc executable.`);
|
|
266
|
-
return resolve(dirname(packagePath), compiler);
|
|
267
|
-
};
|
|
268
|
-
const runJSDocExamples = async (examples, workingDirectory) => {
|
|
269
|
-
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
270
|
-
try {
|
|
271
|
-
const run = __addDisposableResource(env_1, createRun(), true);
|
|
272
|
-
const results = await run.ok(allSettled(examples, (example) => async (run) => tryAsync(() => runProcess(process.execPath, [example.generatedPath], workingDirectory, "Node.js execution"), (error) => {
|
|
273
|
-
run.signal.throwIfAborted();
|
|
274
|
-
return { error, example };
|
|
275
|
-
}), { concurrency: PositiveInt.orThrow(availableParallelism()) }));
|
|
276
|
-
const failures = filterArray(results, isErr);
|
|
277
|
-
if (failures.length === 0)
|
|
278
|
-
return;
|
|
279
|
-
const errors = mapArray(failures, ({ error: { error, example: { filePath, line }, }, }) => {
|
|
280
|
-
const message = error instanceof Error
|
|
281
|
-
? error.message
|
|
282
|
-
: safelyStringifyUnknownValue(error);
|
|
283
|
-
return new Error(`${relative(workingDirectory, filePath)}:${line}: ${message}`, { cause: error });
|
|
284
|
-
});
|
|
285
|
-
throw new AggregateError(errors, [
|
|
286
|
-
"JSDoc example execution failed.",
|
|
287
|
-
...mapArray(errors, (error) => `- ${error.message}`),
|
|
288
|
-
].join("\n"));
|
|
289
|
-
}
|
|
290
|
-
catch (e_1) {
|
|
291
|
-
env_1.error = e_1;
|
|
292
|
-
env_1.hasError = true;
|
|
293
|
-
}
|
|
294
|
-
finally {
|
|
295
|
-
const result_1 = __disposeResources(env_1);
|
|
296
|
-
if (result_1)
|
|
297
|
-
await result_1;
|
|
298
|
-
}
|
|
299
|
-
};
|
|
300
|
-
const createPackageAliases = (temporaryDirectory, aliases) => {
|
|
301
|
-
const aliasesByPackageName = new Map();
|
|
302
|
-
for (const [name, target] of Object.entries(aliases)) {
|
|
303
|
-
assert(isAbsolute(target), `Package alias ${name} must be absolute.`);
|
|
304
|
-
const segments = name.split("/");
|
|
305
|
-
const packageSegmentCount = name.startsWith("@") ? 2 : 1;
|
|
306
|
-
const packageName = segments.slice(0, packageSegmentCount).join("/");
|
|
307
|
-
const subpathSegments = segments.slice(packageSegmentCount);
|
|
308
|
-
assert(packageNamePattern.test(packageName) &&
|
|
309
|
-
subpathSegments.every((segment) => packageSubpathSegmentPattern.test(segment)), `Invalid package alias: ${name}.`);
|
|
310
|
-
const subpath = subpathSegments.join("/");
|
|
311
|
-
const packageAliases = aliasesByPackageName.get(packageName) ?? [];
|
|
312
|
-
packageAliases.push({ subpath, targetPath: realpathSync(target) });
|
|
313
|
-
aliasesByPackageName.set(packageName, packageAliases);
|
|
314
|
-
}
|
|
315
|
-
for (const [packageName, packageAliases] of aliasesByPackageName) {
|
|
316
|
-
const packageSegments = packageName.split("/");
|
|
317
|
-
const packageDirectory = join(temporaryDirectory, "node_modules", ...packageSegments);
|
|
318
|
-
mkdirSync(packageDirectory, { recursive: true });
|
|
319
|
-
const exportsBySubpath = {};
|
|
320
|
-
for (const { subpath, targetPath } of packageAliases) {
|
|
321
|
-
const entryPathWithoutExtension = subpath || "index";
|
|
322
|
-
const exportName = subpath ? `./${subpath}` : ".";
|
|
323
|
-
exportsBySubpath[exportName] = {
|
|
324
|
-
types: `./${entryPathWithoutExtension}.ts`,
|
|
325
|
-
default: `./${entryPathWithoutExtension}.js`,
|
|
326
|
-
};
|
|
327
|
-
for (const extension of ["ts", "js"]) {
|
|
328
|
-
const entryPath = join(packageDirectory, `${entryPathWithoutExtension}.${extension}`);
|
|
329
|
-
mkdirSync(dirname(entryPath), { recursive: true });
|
|
330
|
-
writeFileSync(entryPath, `export * from ${JSON.stringify(pathToImportSpecifier(entryPath, targetPath))};\n`);
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
writeFileSync(join(packageDirectory, "package.json"), JSON.stringify({
|
|
334
|
-
name: packageName,
|
|
335
|
-
type: "module",
|
|
336
|
-
exports: exportsBySubpath,
|
|
337
|
-
}));
|
|
338
|
-
}
|
|
339
|
-
};
|
|
340
|
-
const pathToImportSpecifier = (from, to) => {
|
|
341
|
-
const path = relative(dirname(from), to).split(sep).join("/");
|
|
342
|
-
return path.startsWith(".") ? path : `./${path}`;
|
|
343
|
-
};
|
|
344
|
-
const runProcess = (command, args, cwd, operation, details) => new Promise((resolve, reject) => {
|
|
345
|
-
const child = spawn(command, args, {
|
|
346
|
-
cwd,
|
|
347
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
348
|
-
});
|
|
349
|
-
const stdout = [];
|
|
350
|
-
const stderr = [];
|
|
351
|
-
child.stdout.on("data", (chunk) => {
|
|
352
|
-
stdout.push(chunk);
|
|
353
|
-
});
|
|
354
|
-
child.stderr.on("data", (chunk) => {
|
|
355
|
-
stderr.push(chunk);
|
|
356
|
-
});
|
|
357
|
-
child.once("error", (error) => {
|
|
358
|
-
reject(new Error([`${operation} failed.`, details].filter(Boolean).join("\n"), {
|
|
359
|
-
cause: error,
|
|
360
|
-
}));
|
|
361
|
-
});
|
|
362
|
-
child.once("close", (code, signal) => {
|
|
363
|
-
if (code === 0) {
|
|
364
|
-
resolve();
|
|
365
|
-
return;
|
|
366
|
-
}
|
|
367
|
-
reject(new Error([
|
|
368
|
-
`${operation} failed${signal === null ? ` with exit code ${String(code)}` : ` from signal ${signal}`}.`,
|
|
369
|
-
details,
|
|
370
|
-
Buffer.concat(stdout).toString("utf8"),
|
|
371
|
-
Buffer.concat(stderr).toString("utf8"),
|
|
372
|
-
]
|
|
373
|
-
.filter(Boolean)
|
|
374
|
-
.join("\n")));
|
|
375
|
-
});
|
|
376
|
-
});
|
package/src/TestJSDoc.ts
DELETED
|
@@ -1,574 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Vitest utilities for testing TypeScript examples embedded in JSDoc and
|
|
3
|
-
* Markdown.
|
|
4
|
-
*
|
|
5
|
-
* These utilities use the dedicated `@evolu/vitest/TestJSDoc` entry point so
|
|
6
|
-
* normal `@evolu/vitest` imports do not evaluate Node.js test tooling.
|
|
7
|
-
*
|
|
8
|
-
* @module
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import {
|
|
12
|
-
allSettled,
|
|
13
|
-
createRun,
|
|
14
|
-
filterArray,
|
|
15
|
-
isErr,
|
|
16
|
-
mapArray,
|
|
17
|
-
PositiveInt,
|
|
18
|
-
type ReadonlyRecord,
|
|
19
|
-
safelyStringifyUnknownValue,
|
|
20
|
-
tryAsync,
|
|
21
|
-
} from "@evolu/common";
|
|
22
|
-
import assert from "node:assert/strict";
|
|
23
|
-
import { spawn } from "node:child_process";
|
|
24
|
-
import {
|
|
25
|
-
globSync,
|
|
26
|
-
mkdirSync,
|
|
27
|
-
mkdtempSync,
|
|
28
|
-
readFileSync,
|
|
29
|
-
realpathSync,
|
|
30
|
-
rmSync,
|
|
31
|
-
writeFileSync,
|
|
32
|
-
} from "node:fs";
|
|
33
|
-
import { findPackageJSON } from "node:module";
|
|
34
|
-
import { availableParallelism } from "node:os";
|
|
35
|
-
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
36
|
-
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
37
|
-
import { stripVTControlCharacters } from "node:util";
|
|
38
|
-
|
|
39
|
-
/** Options for compiling and executing TypeScript documentation examples. */
|
|
40
|
-
export interface TestJSDocExamplesOptions {
|
|
41
|
-
/** Source files or glob patterns resolved from `cwd`. */
|
|
42
|
-
readonly include: string | ReadonlyArray<string>;
|
|
43
|
-
/** Package imports redirected to absolute TypeScript entry paths. */
|
|
44
|
-
readonly aliases?: ReadonlyRecord<string, string>;
|
|
45
|
-
/** Directory used for globbing and package resolution. */
|
|
46
|
-
readonly cwd?: string;
|
|
47
|
-
/** TypeScript compiler package. Defaults to `"typescript"`. */
|
|
48
|
-
readonly typescriptPackage?: string;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
interface JSDocExample {
|
|
52
|
-
readonly filePath: string;
|
|
53
|
-
readonly line: number;
|
|
54
|
-
readonly source: string;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
interface GeneratedJSDocExample extends JSDocExample {
|
|
58
|
-
readonly generatedPath: string;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
interface JSDocExampleFailure {
|
|
62
|
-
readonly error: unknown;
|
|
63
|
-
readonly example: GeneratedJSDocExample;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
interface TypeScriptPackage {
|
|
67
|
-
readonly bin: string | { readonly tsc?: string };
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const jsdocPattern = /\/\*\*[\s\S]*?\*\//g;
|
|
71
|
-
const fencePattern = /(?:```|~~~)([^\n]*)\n([\s\S]*?)(?:(```|~~~)|$)/g;
|
|
72
|
-
const markdownFilePattern = /\.mdx?$/i;
|
|
73
|
-
const packageNamePattern = /^(?:@[a-z\d][a-z\d._~-]*\/)?[a-z\d][a-z\d._~-]*$/i;
|
|
74
|
-
const packageSubpathSegmentPattern = /^[a-z\d][a-z\d._~-]*$/i;
|
|
75
|
-
const polyfillsPath = fileURLToPath(
|
|
76
|
-
import.meta.resolve("@evolu/common/polyfills"),
|
|
77
|
-
);
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Compiles and executes every TypeScript example in the included JSDoc comments
|
|
81
|
-
* and Markdown files.
|
|
82
|
-
*
|
|
83
|
-
* Examples are compiled together as isolated TypeScript modules. Examples
|
|
84
|
-
* without compilation errors are then run concurrently, bounded by the CPU
|
|
85
|
-
* parallelism available to the process. Compilation and execution failures are
|
|
86
|
-
* reported together. Vitest's `assert`, `expect`, and `expectTypeOf`, along
|
|
87
|
-
* with Evolu's `expectOk` and `expectErr`, are injected into each module.
|
|
88
|
-
*
|
|
89
|
-
* Write every TypeScript fence as a standalone, deterministic example and
|
|
90
|
-
* import its dependencies. Use `expectTypeOf` to prove static contracts and an
|
|
91
|
-
* appropriate runtime assertion such as `expect`, `expectOk`, or `expectErr`
|
|
92
|
-
* instead of describing expected behavior only in comments.
|
|
93
|
-
*
|
|
94
|
-
* Package aliases are useful for examples documenting an entry point that is
|
|
95
|
-
* not exported yet. Package subpaths can be aliased independently. Each alias
|
|
96
|
-
* target must be an absolute TypeScript module path exposing the named exports
|
|
97
|
-
* used by the example.
|
|
98
|
-
*
|
|
99
|
-
* ### Example
|
|
100
|
-
*
|
|
101
|
-
* ```ts
|
|
102
|
-
* import { testJSDocExamples } from "@evolu/vitest/TestJSDoc";
|
|
103
|
-
* import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
104
|
-
* import { tmpdir } from "node:os";
|
|
105
|
-
* import { join } from "node:path";
|
|
106
|
-
*
|
|
107
|
-
* const directory = await mkdtemp(join(tmpdir(), "evolu-jsdoc-example-"));
|
|
108
|
-
* try {
|
|
109
|
-
* const sourcePath = join(directory, "Example.ts");
|
|
110
|
-
* await writeFile(
|
|
111
|
-
* sourcePath,
|
|
112
|
-
* [
|
|
113
|
-
* "/**",
|
|
114
|
-
* " * ``" + "`ts",
|
|
115
|
-
* " * expect(1 + 1).toBe(2);",
|
|
116
|
-
* " * ``" + "`",
|
|
117
|
-
* " *" + "/",
|
|
118
|
-
* "export {};",
|
|
119
|
-
* ].join("\n"),
|
|
120
|
-
* );
|
|
121
|
-
* await testJSDocExamples({
|
|
122
|
-
* cwd: process.cwd(),
|
|
123
|
-
* include: sourcePath,
|
|
124
|
-
* typescriptPackage: "@typescript/native",
|
|
125
|
-
* });
|
|
126
|
-
* } finally {
|
|
127
|
-
* await rm(directory, { force: true, recursive: true });
|
|
128
|
-
* }
|
|
129
|
-
* ```
|
|
130
|
-
*/
|
|
131
|
-
export const testJSDocExamples = async ({
|
|
132
|
-
include,
|
|
133
|
-
aliases = {},
|
|
134
|
-
cwd = process.cwd(),
|
|
135
|
-
typescriptPackage = "typescript",
|
|
136
|
-
}: TestJSDocExamplesOptions): Promise<void> => {
|
|
137
|
-
const workingDirectory = realpathSync(resolve(cwd));
|
|
138
|
-
const patterns = typeof include === "string" ? [include] : include;
|
|
139
|
-
assert(
|
|
140
|
-
patterns.length > 0,
|
|
141
|
-
"Documentation example tests require included files.",
|
|
142
|
-
);
|
|
143
|
-
|
|
144
|
-
const filePaths = Array.from(
|
|
145
|
-
new Set(
|
|
146
|
-
globSync(patterns, { cwd: workingDirectory }).map((filePath) =>
|
|
147
|
-
resolve(workingDirectory, filePath),
|
|
148
|
-
),
|
|
149
|
-
),
|
|
150
|
-
).sort();
|
|
151
|
-
assert(
|
|
152
|
-
filePaths.length > 0,
|
|
153
|
-
"No files matched the documentation example patterns.",
|
|
154
|
-
);
|
|
155
|
-
|
|
156
|
-
const examples = filePaths.flatMap((filePath) =>
|
|
157
|
-
extractDocumentationExamples(readFileSync(filePath, "utf8"), filePath),
|
|
158
|
-
);
|
|
159
|
-
assert(
|
|
160
|
-
examples.length > 0,
|
|
161
|
-
"No TypeScript documentation examples were found.",
|
|
162
|
-
);
|
|
163
|
-
|
|
164
|
-
const compilerPath = resolveTypeScriptCompiler(
|
|
165
|
-
workingDirectory,
|
|
166
|
-
typescriptPackage,
|
|
167
|
-
);
|
|
168
|
-
const temporaryRoot = join(workingDirectory, "tmp");
|
|
169
|
-
mkdirSync(temporaryRoot, { recursive: true });
|
|
170
|
-
|
|
171
|
-
const temporaryDirectory = mkdtempSync(join(temporaryRoot, "evolu-jsdoc-"));
|
|
172
|
-
|
|
173
|
-
try {
|
|
174
|
-
writeFileSync(
|
|
175
|
-
join(temporaryDirectory, "package.json"),
|
|
176
|
-
JSON.stringify({ type: "module" }),
|
|
177
|
-
);
|
|
178
|
-
createPackageAliases(temporaryDirectory, aliases);
|
|
179
|
-
|
|
180
|
-
const generatedExamples = examples.map((example, index) => {
|
|
181
|
-
const generatedPath = join(temporaryDirectory, `example-${index}.ts`);
|
|
182
|
-
writeFileSync(
|
|
183
|
-
generatedPath,
|
|
184
|
-
transformJSDocExample(example, generatedPath),
|
|
185
|
-
);
|
|
186
|
-
return { ...example, generatedPath } satisfies GeneratedJSDocExample;
|
|
187
|
-
});
|
|
188
|
-
// TODO: Lint generated examples with Oxc after the repository migration.
|
|
189
|
-
// Treat top-level declarations as example output and ignore `using`
|
|
190
|
-
// bindings whose disposal is their purpose.
|
|
191
|
-
const compilerConfigPath = join(temporaryDirectory, "tsconfig.json");
|
|
192
|
-
writeFileSync(
|
|
193
|
-
compilerConfigPath,
|
|
194
|
-
JSON.stringify({
|
|
195
|
-
compilerOptions: {
|
|
196
|
-
allowImportingTsExtensions: true,
|
|
197
|
-
erasableSyntaxOnly: true,
|
|
198
|
-
esModuleInterop: true,
|
|
199
|
-
exactOptionalPropertyTypes: true,
|
|
200
|
-
lib: ["DOM", "ESNext"],
|
|
201
|
-
module: "NodeNext",
|
|
202
|
-
moduleResolution: "NodeNext",
|
|
203
|
-
noEmit: true,
|
|
204
|
-
skipLibCheck: true,
|
|
205
|
-
strict: true,
|
|
206
|
-
target: "ES2022",
|
|
207
|
-
types: ["node"],
|
|
208
|
-
verbatimModuleSyntax: true,
|
|
209
|
-
},
|
|
210
|
-
files: generatedExamples.map(({ generatedPath }) => generatedPath),
|
|
211
|
-
}),
|
|
212
|
-
);
|
|
213
|
-
|
|
214
|
-
let compilationError: Error | undefined;
|
|
215
|
-
try {
|
|
216
|
-
await runProcess(
|
|
217
|
-
process.execPath,
|
|
218
|
-
[compilerPath, "--project", compilerConfigPath],
|
|
219
|
-
workingDirectory,
|
|
220
|
-
"Documentation example TypeScript compilation",
|
|
221
|
-
[
|
|
222
|
-
"Generated example sources:",
|
|
223
|
-
...generatedExamples.map(
|
|
224
|
-
({ filePath, generatedPath, line }) =>
|
|
225
|
-
`- ${relative(temporaryDirectory, generatedPath)}: ${relative(workingDirectory, filePath)}:${line}`,
|
|
226
|
-
),
|
|
227
|
-
].join("\n"),
|
|
228
|
-
);
|
|
229
|
-
} catch (error) {
|
|
230
|
-
compilationError =
|
|
231
|
-
error instanceof Error
|
|
232
|
-
? error
|
|
233
|
-
: new Error(safelyStringifyUnknownValue(error));
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
const runnableExamples =
|
|
237
|
-
compilationError === undefined
|
|
238
|
-
? generatedExamples
|
|
239
|
-
: getExamplesWithoutCompilationErrors(
|
|
240
|
-
generatedExamples,
|
|
241
|
-
compilationError,
|
|
242
|
-
workingDirectory,
|
|
243
|
-
);
|
|
244
|
-
|
|
245
|
-
let executionError: Error | undefined;
|
|
246
|
-
try {
|
|
247
|
-
await runJSDocExamples(runnableExamples, workingDirectory);
|
|
248
|
-
} catch (error) {
|
|
249
|
-
executionError =
|
|
250
|
-
error instanceof Error
|
|
251
|
-
? error
|
|
252
|
-
: new Error(safelyStringifyUnknownValue(error));
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
if (compilationError !== undefined && executionError !== undefined) {
|
|
256
|
-
const executionErrors: ReadonlyArray<unknown> =
|
|
257
|
-
executionError instanceof AggregateError
|
|
258
|
-
? (executionError.errors as ReadonlyArray<unknown>)
|
|
259
|
-
: [executionError];
|
|
260
|
-
throw new AggregateError(
|
|
261
|
-
[compilationError, ...executionErrors],
|
|
262
|
-
`${compilationError.message}\n${executionError.message}`,
|
|
263
|
-
);
|
|
264
|
-
}
|
|
265
|
-
if (compilationError !== undefined) throw compilationError;
|
|
266
|
-
if (executionError !== undefined) throw executionError;
|
|
267
|
-
} finally {
|
|
268
|
-
rmSync(temporaryDirectory, { force: true, recursive: true });
|
|
269
|
-
}
|
|
270
|
-
};
|
|
271
|
-
|
|
272
|
-
const getExamplesWithoutCompilationErrors = (
|
|
273
|
-
examples: ReadonlyArray<GeneratedJSDocExample>,
|
|
274
|
-
compilationError: Error,
|
|
275
|
-
workingDirectory: string,
|
|
276
|
-
): ReadonlyArray<GeneratedJSDocExample> => {
|
|
277
|
-
const compilationErrorMessage = stripVTControlCharacters(
|
|
278
|
-
compilationError.message,
|
|
279
|
-
);
|
|
280
|
-
const examplesWithErrors = examples.filter(({ generatedPath }) =>
|
|
281
|
-
[generatedPath, relative(workingDirectory, generatedPath)].some((path) =>
|
|
282
|
-
[`${path}(`, `${path}:`].some((segment) =>
|
|
283
|
-
compilationErrorMessage.includes(segment),
|
|
284
|
-
),
|
|
285
|
-
),
|
|
286
|
-
);
|
|
287
|
-
|
|
288
|
-
if (examplesWithErrors.length === 0) return [];
|
|
289
|
-
|
|
290
|
-
return examples.filter((example) => !examplesWithErrors.includes(example));
|
|
291
|
-
};
|
|
292
|
-
|
|
293
|
-
const extractDocumentationExamples = (
|
|
294
|
-
source: string,
|
|
295
|
-
filePath: string,
|
|
296
|
-
): ReadonlyArray<JSDocExample> => {
|
|
297
|
-
if (markdownFilePattern.test(filePath)) {
|
|
298
|
-
return extractFencedExamples(source, source, filePath, 0, false);
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
const examples: Array<JSDocExample> = [];
|
|
302
|
-
for (const jsdoc of source.matchAll(jsdocPattern)) {
|
|
303
|
-
examples.push(
|
|
304
|
-
...extractFencedExamples(source, jsdoc[0], filePath, jsdoc.index, true),
|
|
305
|
-
);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
return examples;
|
|
309
|
-
};
|
|
310
|
-
|
|
311
|
-
const extractFencedExamples = (
|
|
312
|
-
source: string,
|
|
313
|
-
fencedSource: string,
|
|
314
|
-
filePath: string,
|
|
315
|
-
offset: number,
|
|
316
|
-
stripJSDocPrefixes: boolean,
|
|
317
|
-
): ReadonlyArray<JSDocExample> => {
|
|
318
|
-
const examples: Array<JSDocExample> = [];
|
|
319
|
-
|
|
320
|
-
for (const fence of fencedSource.matchAll(fencePattern)) {
|
|
321
|
-
const metadata = fence[1].trim().toLowerCase().split(/\s+/);
|
|
322
|
-
if (!["ts", "typescript"].includes(metadata[0])) {
|
|
323
|
-
continue;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
const line = getLineNumber(source, offset + fence.index);
|
|
327
|
-
const closingFence = fence[3] as string | undefined;
|
|
328
|
-
assert(
|
|
329
|
-
closingFence !== undefined,
|
|
330
|
-
`${filePath}:${line} has an unclosed TypeScript example fence.`,
|
|
331
|
-
);
|
|
332
|
-
const exampleSource = (
|
|
333
|
-
stripJSDocPrefixes ? fence[2].replace(/^[ \t]*\* ?/gm, "") : fence[2]
|
|
334
|
-
).trim();
|
|
335
|
-
assert(
|
|
336
|
-
exampleSource.length > 0,
|
|
337
|
-
`${filePath}:${line} has an empty TypeScript example.`,
|
|
338
|
-
);
|
|
339
|
-
examples.push({ filePath, line, source: exampleSource });
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
return examples;
|
|
343
|
-
};
|
|
344
|
-
|
|
345
|
-
const getLineNumber = (source: string, offset: number): number => {
|
|
346
|
-
let line = 1;
|
|
347
|
-
for (let index = 0; index < offset; index++) {
|
|
348
|
-
if (source.charCodeAt(index) === 10) line++;
|
|
349
|
-
}
|
|
350
|
-
return line;
|
|
351
|
-
};
|
|
352
|
-
|
|
353
|
-
const transformJSDocExample = (
|
|
354
|
-
example: JSDocExample,
|
|
355
|
-
generatedPath: string,
|
|
356
|
-
): string =>
|
|
357
|
-
[
|
|
358
|
-
`import { installPolyfills } from ${JSON.stringify(pathToImportSpecifier(generatedPath, polyfillsPath))};`,
|
|
359
|
-
'import { expectErr, expectOk } from "@evolu/vitest";',
|
|
360
|
-
'import { assert, expect, expectTypeOf } from "vitest";',
|
|
361
|
-
"installPolyfills();",
|
|
362
|
-
example.source,
|
|
363
|
-
].join("\n\n");
|
|
364
|
-
|
|
365
|
-
const resolveTypeScriptCompiler = (
|
|
366
|
-
workingDirectory: string,
|
|
367
|
-
typescriptPackage: string,
|
|
368
|
-
): string => {
|
|
369
|
-
const packagePath = findPackageJSON(
|
|
370
|
-
typescriptPackage,
|
|
371
|
-
pathToFileURL(join(workingDirectory, "package.json")),
|
|
372
|
-
);
|
|
373
|
-
assert(
|
|
374
|
-
packagePath !== undefined,
|
|
375
|
-
`Cannot resolve ${typescriptPackage} from ${workingDirectory}.`,
|
|
376
|
-
);
|
|
377
|
-
const packageJson = JSON.parse(
|
|
378
|
-
readFileSync(packagePath, "utf8"),
|
|
379
|
-
) as TypeScriptPackage;
|
|
380
|
-
const compiler =
|
|
381
|
-
typeof packageJson.bin === "string" ? packageJson.bin : packageJson.bin.tsc;
|
|
382
|
-
assert(
|
|
383
|
-
compiler !== undefined,
|
|
384
|
-
`${typescriptPackage} does not expose a tsc executable.`,
|
|
385
|
-
);
|
|
386
|
-
return resolve(dirname(packagePath), compiler);
|
|
387
|
-
};
|
|
388
|
-
|
|
389
|
-
const runJSDocExamples = async (
|
|
390
|
-
examples: ReadonlyArray<GeneratedJSDocExample>,
|
|
391
|
-
workingDirectory: string,
|
|
392
|
-
): Promise<void> => {
|
|
393
|
-
await using run = createRun();
|
|
394
|
-
const results = await run.ok(
|
|
395
|
-
allSettled(
|
|
396
|
-
examples,
|
|
397
|
-
(example) => async (run) =>
|
|
398
|
-
tryAsync(
|
|
399
|
-
() =>
|
|
400
|
-
runProcess(
|
|
401
|
-
process.execPath,
|
|
402
|
-
[example.generatedPath],
|
|
403
|
-
workingDirectory,
|
|
404
|
-
"Node.js execution",
|
|
405
|
-
),
|
|
406
|
-
(error) => {
|
|
407
|
-
run.signal.throwIfAborted();
|
|
408
|
-
return { error, example } satisfies JSDocExampleFailure;
|
|
409
|
-
},
|
|
410
|
-
),
|
|
411
|
-
{ concurrency: PositiveInt.orThrow(availableParallelism()) },
|
|
412
|
-
),
|
|
413
|
-
);
|
|
414
|
-
|
|
415
|
-
const failures = filterArray(results, isErr);
|
|
416
|
-
if (failures.length === 0) return;
|
|
417
|
-
|
|
418
|
-
const errors = mapArray(
|
|
419
|
-
failures,
|
|
420
|
-
({
|
|
421
|
-
error: {
|
|
422
|
-
error,
|
|
423
|
-
example: { filePath, line },
|
|
424
|
-
},
|
|
425
|
-
}) => {
|
|
426
|
-
const message =
|
|
427
|
-
error instanceof Error
|
|
428
|
-
? error.message
|
|
429
|
-
: safelyStringifyUnknownValue(error);
|
|
430
|
-
return new Error(
|
|
431
|
-
`${relative(workingDirectory, filePath)}:${line}: ${message}`,
|
|
432
|
-
{ cause: error },
|
|
433
|
-
);
|
|
434
|
-
},
|
|
435
|
-
);
|
|
436
|
-
throw new AggregateError(
|
|
437
|
-
errors,
|
|
438
|
-
[
|
|
439
|
-
"JSDoc example execution failed.",
|
|
440
|
-
...mapArray(errors, (error) => `- ${error.message}`),
|
|
441
|
-
].join("\n"),
|
|
442
|
-
);
|
|
443
|
-
};
|
|
444
|
-
|
|
445
|
-
const createPackageAliases = (
|
|
446
|
-
temporaryDirectory: string,
|
|
447
|
-
aliases: ReadonlyRecord<string, string>,
|
|
448
|
-
): void => {
|
|
449
|
-
const aliasesByPackageName = new Map<
|
|
450
|
-
string,
|
|
451
|
-
Array<{
|
|
452
|
-
readonly subpath: string;
|
|
453
|
-
readonly targetPath: string;
|
|
454
|
-
}>
|
|
455
|
-
>();
|
|
456
|
-
|
|
457
|
-
for (const [name, target] of Object.entries(aliases)) {
|
|
458
|
-
assert(isAbsolute(target), `Package alias ${name} must be absolute.`);
|
|
459
|
-
const segments = name.split("/");
|
|
460
|
-
const packageSegmentCount = name.startsWith("@") ? 2 : 1;
|
|
461
|
-
const packageName = segments.slice(0, packageSegmentCount).join("/");
|
|
462
|
-
const subpathSegments = segments.slice(packageSegmentCount);
|
|
463
|
-
assert(
|
|
464
|
-
packageNamePattern.test(packageName) &&
|
|
465
|
-
subpathSegments.every((segment) =>
|
|
466
|
-
packageSubpathSegmentPattern.test(segment),
|
|
467
|
-
),
|
|
468
|
-
`Invalid package alias: ${name}.`,
|
|
469
|
-
);
|
|
470
|
-
const subpath = subpathSegments.join("/");
|
|
471
|
-
const packageAliases = aliasesByPackageName.get(packageName) ?? [];
|
|
472
|
-
packageAliases.push({ subpath, targetPath: realpathSync(target) });
|
|
473
|
-
aliasesByPackageName.set(packageName, packageAliases);
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
for (const [packageName, packageAliases] of aliasesByPackageName) {
|
|
477
|
-
const packageSegments = packageName.split("/");
|
|
478
|
-
const packageDirectory = join(
|
|
479
|
-
temporaryDirectory,
|
|
480
|
-
"node_modules",
|
|
481
|
-
...packageSegments,
|
|
482
|
-
);
|
|
483
|
-
mkdirSync(packageDirectory, { recursive: true });
|
|
484
|
-
const exportsBySubpath: Record<
|
|
485
|
-
string,
|
|
486
|
-
{ readonly types: string; readonly default: string }
|
|
487
|
-
> = {};
|
|
488
|
-
|
|
489
|
-
for (const { subpath, targetPath } of packageAliases) {
|
|
490
|
-
const entryPathWithoutExtension = subpath || "index";
|
|
491
|
-
const exportName = subpath ? `./${subpath}` : ".";
|
|
492
|
-
exportsBySubpath[exportName] = {
|
|
493
|
-
types: `./${entryPathWithoutExtension}.ts`,
|
|
494
|
-
default: `./${entryPathWithoutExtension}.js`,
|
|
495
|
-
};
|
|
496
|
-
|
|
497
|
-
for (const extension of ["ts", "js"]) {
|
|
498
|
-
const entryPath = join(
|
|
499
|
-
packageDirectory,
|
|
500
|
-
`${entryPathWithoutExtension}.${extension}`,
|
|
501
|
-
);
|
|
502
|
-
mkdirSync(dirname(entryPath), { recursive: true });
|
|
503
|
-
writeFileSync(
|
|
504
|
-
entryPath,
|
|
505
|
-
`export * from ${JSON.stringify(pathToImportSpecifier(entryPath, targetPath))};\n`,
|
|
506
|
-
);
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
writeFileSync(
|
|
511
|
-
join(packageDirectory, "package.json"),
|
|
512
|
-
JSON.stringify({
|
|
513
|
-
name: packageName,
|
|
514
|
-
type: "module",
|
|
515
|
-
exports: exportsBySubpath,
|
|
516
|
-
}),
|
|
517
|
-
);
|
|
518
|
-
}
|
|
519
|
-
};
|
|
520
|
-
|
|
521
|
-
const pathToImportSpecifier = (from: string, to: string): string => {
|
|
522
|
-
const path = relative(dirname(from), to).split(sep).join("/");
|
|
523
|
-
return path.startsWith(".") ? path : `./${path}`;
|
|
524
|
-
};
|
|
525
|
-
|
|
526
|
-
const runProcess = (
|
|
527
|
-
command: string,
|
|
528
|
-
args: ReadonlyArray<string>,
|
|
529
|
-
cwd: string,
|
|
530
|
-
operation: string,
|
|
531
|
-
details?: string,
|
|
532
|
-
): Promise<void> =>
|
|
533
|
-
new Promise((resolve, reject) => {
|
|
534
|
-
const child = spawn(command, args, {
|
|
535
|
-
cwd,
|
|
536
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
537
|
-
});
|
|
538
|
-
const stdout: Array<Buffer> = [];
|
|
539
|
-
const stderr: Array<Buffer> = [];
|
|
540
|
-
child.stdout.on("data", (chunk: Buffer) => {
|
|
541
|
-
stdout.push(chunk);
|
|
542
|
-
});
|
|
543
|
-
child.stderr.on("data", (chunk: Buffer) => {
|
|
544
|
-
stderr.push(chunk);
|
|
545
|
-
});
|
|
546
|
-
child.once("error", (error) => {
|
|
547
|
-
reject(
|
|
548
|
-
new Error(
|
|
549
|
-
[`${operation} failed.`, details].filter(Boolean).join("\n"),
|
|
550
|
-
{
|
|
551
|
-
cause: error,
|
|
552
|
-
},
|
|
553
|
-
),
|
|
554
|
-
);
|
|
555
|
-
});
|
|
556
|
-
child.once("close", (code, signal) => {
|
|
557
|
-
if (code === 0) {
|
|
558
|
-
resolve();
|
|
559
|
-
return;
|
|
560
|
-
}
|
|
561
|
-
reject(
|
|
562
|
-
new Error(
|
|
563
|
-
[
|
|
564
|
-
`${operation} failed${signal === null ? ` with exit code ${String(code)}` : ` from signal ${signal}`}.`,
|
|
565
|
-
details,
|
|
566
|
-
Buffer.concat(stdout).toString("utf8"),
|
|
567
|
-
Buffer.concat(stderr).toString("utf8"),
|
|
568
|
-
]
|
|
569
|
-
.filter(Boolean)
|
|
570
|
-
.join("\n"),
|
|
571
|
-
),
|
|
572
|
-
);
|
|
573
|
-
});
|
|
574
|
-
});
|