@evolu/nodejs 3.0.1 → 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.
@@ -289,11 +289,11 @@ const testViteBundler = {
289
289
  const output = await vite.build({
290
290
  root: dirname(entryPath),
291
291
  configFile: false,
292
- envFile: false,
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[0];
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[0];
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
+ });
@@ -1 +1 @@
1
- {"version":3,"file":"Worker.d.ts","sourceRoot":"","sources":["../../src/Worker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAoB,sBAAsB,EAAE,MAAM,eAAe,CAAC;AAG9E,0EAA0E;AAC1E,eAAO,MAAM,sBAAsB,EAAE,sBAsCpC,CAAC"}
1
+ {"version":3,"file":"Worker.d.ts","sourceRoot":"","sources":["../../src/Worker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAoB,sBAAsB,EAAE,MAAM,eAAe,CAAC;AAG9E,0EAA0E;AAC1E,eAAO,MAAM,sBAAsB,EAAE,sBAwCpC,CAAC"}
@@ -60,6 +60,7 @@ export const createBroadcastChannel = (name) => {
60
60
  let disposed = false;
61
61
  disposer.defer(() => {
62
62
  disposed = true;
63
+ // oxlint-disable-next-line unicorn/prefer-add-event-listener -- This adapter owns and clears one handler.
63
64
  nativeBroadcastChannel.onmessage = null;
64
65
  nativeBroadcastChannel.close();
65
66
  });
@@ -75,6 +76,7 @@ export const createBroadcastChannel = (name) => {
75
76
  if (disposed)
76
77
  return;
77
78
  onMessageHandler = fn;
79
+ // oxlint-disable-next-line unicorn/prefer-add-event-listener -- This adapter owns and clears one handler.
78
80
  nativeBroadcastChannel.onmessage = fn
79
81
  ? (event) => {
80
82
  fn(event.data);
@@ -1 +1 @@
1
- {"version":3,"file":"Relay.d.ts","sourceRoot":"","sources":["../../../src/local-first/Relay.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,qBAAqB,EAK1B,KAAK,SAAS,EACd,KAAK,IAAI,EACT,KAAK,kBAAkB,EAGxB,MAAM,eAAe,CAAC;AACvB,OAAO,EAQL,KAAK,KAAK,EACV,KAAK,WAAW,EACjB,MAAM,2BAA2B,CAAC;AAQnC,MAAM,WAAW,iBAAkB,SAAQ,WAAW;IACpD,2CAA2C;IAC3C,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,MAAM,SAAS,GAAG,qBAAqB,GAAG,SAAS,GAAG,kBAAkB,CAAC;AAE/E,iEAAiE;AACjE,eAAO,MAAM,eAAe,QAAO,SAIjC,CAAC;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,eAAO,MAAM,WAAW,wDAMnB,iBAAiB,KAAG,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,CAgNlD,CAAC"}
1
+ {"version":3,"file":"Relay.d.ts","sourceRoot":"","sources":["../../../src/local-first/Relay.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,qBAAqB,EAK1B,KAAK,SAAS,EACd,KAAK,IAAI,EACT,KAAK,kBAAkB,EAGxB,MAAM,eAAe,CAAC;AACvB,OAAO,EAQL,KAAK,KAAK,EACV,KAAK,WAAW,EACjB,MAAM,2BAA2B,CAAC;AAQnC,MAAM,WAAW,iBAAkB,SAAQ,WAAW;IACpD,2CAA2C;IAC3C,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,MAAM,SAAS,GAAG,qBAAqB,GAAG,SAAS,GAAG,kBAAkB,CAAC;AAE/E,iEAAiE;AACjE,eAAO,MAAM,eAAe,QAAO,SAIjC,CAAC;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,eAAO,MAAM,WAAW,wDAMnB,iBAAiB,KAAG,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,CAkNlD,CAAC"}
@@ -161,7 +161,7 @@ export const createRelay = ({ port = 443, name = Name.orThrow("evolu-relay"), is
161
161
  // ignores abort. The daemon runs in the root Run, so aborting the
162
162
  // current Run does not make its Fiber wait for the service Promise to
163
163
  // settle.
164
- daemon(async (run) => await tryAsync(() => isOwnerAllowed(ownerId, { signal: run.signal }), (error) => ({ type: "OwnerAuthorizationError", error }))));
164
+ daemon(async (run) => tryAsync(() => isOwnerAllowed(ownerId, { signal: run.signal }), (error) => ({ type: "OwnerAuthorizationError", error }))));
165
165
  const abortAuthorization = () => {
166
166
  authorizationFiber.abort({
167
167
  type: "WebSocketUpgradeSocketClosed",
@@ -239,7 +239,7 @@ export const createRelay = ({ port = 443, name = Name.orThrow("evolu-relay"), is
239
239
  server.listen(port);
240
240
  await once(server, "listening");
241
241
  const address = server.address();
242
- assert(address && typeof address !== "string", "Expected TCP address");
242
+ assert(address !== null && typeof address !== "string", "Expected TCP address");
243
243
  const disposables = disposer.move();
244
244
  console.info(`Started on port ${address.port}`);
245
245
  return ok({