@evolu/nodejs 3.0.0 → 3.1.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/dist/src/TestBundle.js +5 -4
- 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/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 +38 -7
- package/src/TestBundle.ts +5 -4
- package/src/TestJSDoc.ts +820 -0
- package/src/Worker.ts +2 -0
- package/src/local-first/Relay.ts +9 -7
package/dist/src/TestBundle.js
CHANGED
|
@@ -289,11 +289,11 @@ const testViteBundler = {
|
|
|
289
289
|
const output = await vite.build({
|
|
290
290
|
root: dirname(entryPath),
|
|
291
291
|
configFile: false,
|
|
292
|
-
|
|
292
|
+
envDir: false,
|
|
293
293
|
logLevel: "silent",
|
|
294
294
|
resolve: {
|
|
295
295
|
alias: Object.entries(aliases).map(([name, replacement]) => ({
|
|
296
|
-
find: new RegExp(`^${escapeRegExp(name)}
|
|
296
|
+
find: new RegExp(`^${escapeRegExp(name)}$`, "u"),
|
|
297
297
|
replacement,
|
|
298
298
|
})),
|
|
299
299
|
},
|
|
@@ -315,11 +315,12 @@ const testViteBundler = {
|
|
|
315
315
|
assert(Array.isArray(output), "Vite did not return build outputs.");
|
|
316
316
|
const outputs = output;
|
|
317
317
|
assert(outputs.length === 1, "Vite did not return one build output.");
|
|
318
|
-
const viteOutput = outputs
|
|
318
|
+
const viteOutput = outputs.at(0);
|
|
319
319
|
assert(viteOutput, "Vite did not return a build output.");
|
|
320
320
|
assert(viteOutput.output.length === 1, "Vite did not emit one JavaScript chunk.");
|
|
321
|
-
const chunk = viteOutput.output
|
|
321
|
+
const chunk = viteOutput.output.at(0);
|
|
322
322
|
assert(chunk, "Vite did not emit a JavaScript chunk.");
|
|
323
|
+
assert(chunk.type === "chunk", "Vite did not emit a JavaScript chunk.");
|
|
323
324
|
assertType(StringType, chunk.code);
|
|
324
325
|
return {
|
|
325
326
|
code: chunk.code,
|
|
@@ -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
|
+
});
|
package/dist/src/Worker.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Worker.d.ts","sourceRoot":"","sources":["../../src/Worker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAoB,sBAAsB,EAAE,MAAM,eAAe,CAAC;AAG9E,0EAA0E;AAC1E,eAAO,MAAM,sBAAsB,EAAE,
|
|
1
|
+
{"version":3,"file":"Worker.d.ts","sourceRoot":"","sources":["../../src/Worker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAoB,sBAAsB,EAAE,MAAM,eAAe,CAAC;AAG9E,0EAA0E;AAC1E,eAAO,MAAM,sBAAsB,EAAE,sBAwCpC,CAAC"}
|
package/dist/src/Worker.js
CHANGED
|
@@ -60,6 +60,7 @@ export const createBroadcastChannel = (name) => {
|
|
|
60
60
|
let disposed = false;
|
|
61
61
|
disposer.defer(() => {
|
|
62
62
|
disposed = true;
|
|
63
|
+
// oxlint-disable-next-line unicorn/prefer-add-event-listener -- This adapter owns and clears one handler.
|
|
63
64
|
nativeBroadcastChannel.onmessage = null;
|
|
64
65
|
nativeBroadcastChannel.close();
|
|
65
66
|
});
|
|
@@ -75,6 +76,7 @@ export const createBroadcastChannel = (name) => {
|
|
|
75
76
|
if (disposed)
|
|
76
77
|
return;
|
|
77
78
|
onMessageHandler = fn;
|
|
79
|
+
// oxlint-disable-next-line unicorn/prefer-add-event-listener -- This adapter owns and clears one handler.
|
|
78
80
|
nativeBroadcastChannel.onmessage = fn
|
|
79
81
|
? (event) => {
|
|
80
82
|
fn(event.data);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Relay.d.ts","sourceRoot":"","sources":["../../../src/local-first/Relay.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,qBAAqB,EAK1B,KAAK,SAAS,EACd,KAAK,IAAI,EACT,KAAK,kBAAkB,EAGxB,MAAM,eAAe,CAAC;AACvB,OAAO,EAQL,KAAK,KAAK,EACV,KAAK,WAAW,EACjB,MAAM,2BAA2B,CAAC;AAQnC,MAAM,WAAW,iBAAkB,SAAQ,WAAW;IACpD,2CAA2C;IAC3C,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,MAAM,SAAS,GAAG,qBAAqB,GAAG,SAAS,GAAG,kBAAkB,CAAC;AAE/E,iEAAiE;AACjE,eAAO,MAAM,eAAe,QAAO,SAIjC,CAAC;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,eAAO,MAAM,WAAW,wDAMnB,iBAAiB,KAAG,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,
|
|
1
|
+
{"version":3,"file":"Relay.d.ts","sourceRoot":"","sources":["../../../src/local-first/Relay.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,qBAAqB,EAK1B,KAAK,SAAS,EACd,KAAK,IAAI,EACT,KAAK,kBAAkB,EAGxB,MAAM,eAAe,CAAC;AACvB,OAAO,EAQL,KAAK,KAAK,EACV,KAAK,WAAW,EACjB,MAAM,2BAA2B,CAAC;AAQnC,MAAM,WAAW,iBAAkB,SAAQ,WAAW;IACpD,2CAA2C;IAC3C,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,MAAM,SAAS,GAAG,qBAAqB,GAAG,SAAS,GAAG,kBAAkB,CAAC;AAE/E,iEAAiE;AACjE,eAAO,MAAM,eAAe,QAAO,SAIjC,CAAC;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,eAAO,MAAM,WAAW,wDAMnB,iBAAiB,KAAG,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,CAkNlD,CAAC"}
|
|
@@ -161,7 +161,7 @@ export const createRelay = ({ port = 443, name = Name.orThrow("evolu-relay"), is
|
|
|
161
161
|
// ignores abort. The daemon runs in the root Run, so aborting the
|
|
162
162
|
// current Run does not make its Fiber wait for the service Promise to
|
|
163
163
|
// settle.
|
|
164
|
-
daemon(async (run) =>
|
|
164
|
+
daemon(async (run) => tryAsync(() => isOwnerAllowed(ownerId, { signal: run.signal }), (error) => ({ type: "OwnerAuthorizationError", error }))));
|
|
165
165
|
const abortAuthorization = () => {
|
|
166
166
|
authorizationFiber.abort({
|
|
167
167
|
type: "WebSocketUpgradeSocketClosed",
|
|
@@ -239,7 +239,7 @@ export const createRelay = ({ port = 443, name = Name.orThrow("evolu-relay"), is
|
|
|
239
239
|
server.listen(port);
|
|
240
240
|
await once(server, "listening");
|
|
241
241
|
const address = server.address();
|
|
242
|
-
assert(address && typeof address !== "string", "Expected TCP address");
|
|
242
|
+
assert(address !== null && typeof address !== "string", "Expected TCP address");
|
|
243
243
|
const disposables = disposer.move();
|
|
244
244
|
console.info(`Started on port ${address.port}`);
|
|
245
245
|
return ok({
|