@evolu/nodejs 3.1.0 → 4.0.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/src/Fs.ts ADDED
@@ -0,0 +1,300 @@
1
+ /**
2
+ * File system operations using Node.js.
3
+ *
4
+ * @module
5
+ */
6
+
7
+ import {
8
+ assert,
9
+ ByteLength,
10
+ disposable,
11
+ err,
12
+ ok,
13
+ tryAsync,
14
+ type Fs,
15
+ type FsEncoding,
16
+ type FsEntryType,
17
+ type FsError,
18
+ type FsErrorReason,
19
+ type FsMetadata,
20
+ type FsPath,
21
+ type Task,
22
+ } from "@evolu/common";
23
+ import { constants, type Stats } from "node:fs";
24
+ import {
25
+ access,
26
+ copyFile,
27
+ cp,
28
+ mkdir,
29
+ mkdtemp,
30
+ readdir,
31
+ readFile,
32
+ realpath,
33
+ rename,
34
+ rm,
35
+ stat,
36
+ writeFile,
37
+ } from "node:fs/promises";
38
+ import { tmpdir } from "node:os";
39
+ import { join, sep } from "node:path";
40
+
41
+ /**
42
+ * Creates a {@link Fs} backed by `node:fs/promises`.
43
+ *
44
+ * `readFile` and `writeFile` pass the Run's abort signal to Node and propagate
45
+ * the Run's abort reason if Node rejects after cancellation. Cancellation can
46
+ * leave a write partially completed. Successful operations return their values
47
+ * even if an abort was requested. Other operations run to completion and return
48
+ * their results once started, so callers also receive created temporary
49
+ * directories and can dispose them.
50
+ *
51
+ * Temporary directory parents are resolved through the file system, preserving
52
+ * the meaning of symbolic links followed by `..`. Returned paths are absolute,
53
+ * so cleanup still targets the created directory after a change to the
54
+ * process's working directory.
55
+ *
56
+ * ### Example
57
+ *
58
+ * ```ts
59
+ * import {
60
+ * assertEqual,
61
+ * assertFalse,
62
+ * ok,
63
+ * type FsDep,
64
+ * type FsError,
65
+ * type Task,
66
+ * } from "@evolu/common";
67
+ * import { createNodeFs, runMain } from "@evolu/nodejs";
68
+ * import { join } from "node:path";
69
+ *
70
+ * const main: Task<void, FsError, FsDep> = async (run) => {
71
+ * const { fs } = run.deps;
72
+ * const temp = await run(fs.createTempDirectory({ prefix: "evolu-" }));
73
+ * if (!temp.ok) return temp;
74
+ *
75
+ * {
76
+ * await using directory = temp.value;
77
+ * const path = join(directory.path, "config.json");
78
+ *
79
+ * const result = await run(fs.writeFile(path, '{ "port": 4000 }'));
80
+ * if (!result.ok) return result;
81
+ *
82
+ * const text = await run(fs.readFile(path, "utf8"));
83
+ * if (!text.ok) return text;
84
+ * assertEqual(text.value, '{ "port": 4000 }');
85
+ * }
86
+ *
87
+ * const exists = await run(fs.exists(temp.value.path));
88
+ * if (!exists.ok) return exists;
89
+ * assertFalse(exists.value);
90
+ * return ok();
91
+ * };
92
+ *
93
+ * await runMain({ fs: createNodeFs() }, { mode: "command" })(main);
94
+ * ```
95
+ */
96
+ export const createNodeFs = (): Fs => {
97
+ function readFileTask(path: FsPath): Task<Uint8Array, FsError>;
98
+ function readFileTask(
99
+ path: FsPath,
100
+ encoding: FsEncoding | { readonly encoding: FsEncoding },
101
+ ): Task<string, FsError>;
102
+ function readFileTask(
103
+ path: FsPath,
104
+ encoding?: FsEncoding | { readonly encoding: FsEncoding },
105
+ ): Task<Uint8Array | string, FsError> {
106
+ return async (run) => {
107
+ const { signal } = run;
108
+ const result = await tryAsync((): Promise<Uint8Array | string> =>
109
+ encoding === undefined
110
+ ? readFile(path, { signal })
111
+ : readFile(path, { ...toEncodingOptions(encoding), signal }),
112
+ );
113
+ if (result.ok) return result;
114
+ signal.throwIfAborted();
115
+ return err(createFsError("readFile", path, result.error));
116
+ };
117
+ }
118
+
119
+ return {
120
+ readFile: readFileTask,
121
+
122
+ writeFile: (path, data, options) => async (run) => {
123
+ const { signal } = run;
124
+ const result = await tryAsync(() =>
125
+ writeFile(path, data, { ...options, signal }),
126
+ );
127
+ if (result.ok) return result;
128
+ signal.throwIfAborted();
129
+ return err(createFsError("writeFile", path, result.error));
130
+ },
131
+
132
+ readDirectory: (path, options) => async () => {
133
+ const result = await tryAsync(() => readdir(path, options));
134
+ return result.ok
135
+ ? result
136
+ : err(createFsError("readDirectory", path, result.error));
137
+ },
138
+
139
+ createDirectory: (path, options) => async () => {
140
+ const result = await tryAsync(async () => {
141
+ await mkdir(path, options);
142
+ });
143
+ return result.ok
144
+ ? ok()
145
+ : err(createFsError("createDirectory", path, result.error));
146
+ },
147
+
148
+ copy: (source, destination, options) => async () => {
149
+ const result = await tryAsync(() =>
150
+ cp(source, destination, { ...options, recursive: true }),
151
+ );
152
+ return result.ok
153
+ ? ok()
154
+ : err(createFsError("copy", source, result.error, destination));
155
+ },
156
+
157
+ copyFile:
158
+ (source, destination, { overwrite = false } = {}) =>
159
+ async () => {
160
+ const result = await tryAsync(() =>
161
+ copyFile(
162
+ source,
163
+ destination,
164
+ overwrite ? 0 : constants.COPYFILE_EXCL,
165
+ ),
166
+ );
167
+ return result.ok
168
+ ? ok()
169
+ : err(createFsError("copyFile", source, result.error, destination));
170
+ },
171
+
172
+ rename: (source, destination) => async () => {
173
+ const result = await tryAsync(() => rename(source, destination));
174
+ return result.ok
175
+ ? ok()
176
+ : err(createFsError("rename", source, result.error, destination));
177
+ },
178
+
179
+ remove: (path, options) => async () => {
180
+ const result = await tryAsync(() => rm(path, options));
181
+ return result.ok
182
+ ? ok()
183
+ : err(createFsError("remove", path, result.error));
184
+ },
185
+
186
+ getMetadata: (path) => async () => {
187
+ const result = await tryAsync(() => stat(path));
188
+ return result.ok
189
+ ? ok(statsToFsMetadata(result.value))
190
+ : err(createFsError("getMetadata", path, result.error));
191
+ },
192
+
193
+ exists: (path) => async () => {
194
+ const result = await tryAsync(() => access(path));
195
+ if (result.ok) return ok(true);
196
+ const error = createFsError("exists", path, result.error);
197
+ return error.reason === "NotFound" ? ok(false) : err(error);
198
+ },
199
+
200
+ createTempDirectory:
201
+ ({ directory, prefix = "" } = {}) =>
202
+ async () => {
203
+ const parent = directory ?? tmpdir();
204
+ const resolvedParent = await tryAsync(
205
+ () => realpath(parent || "."),
206
+ (error) => createFsError("createTempDirectory", parent, error),
207
+ );
208
+ if (!resolvedParent.ok) return resolvedParent;
209
+
210
+ // Keep the parent separator even when the name prefix is empty.
211
+ const pathPrefix = join(resolvedParent.value, sep) + prefix;
212
+ const result = await tryAsync(
213
+ () => mkdtemp(pathPrefix),
214
+ (error) => createFsError("createTempDirectory", pathPrefix, error),
215
+ );
216
+ if (!result.ok) return result;
217
+
218
+ const path = result.value;
219
+ const disposer = new AsyncDisposableStack();
220
+ disposer.defer(() => rm(path, { recursive: true, force: true }));
221
+ return ok(disposable({ path }, disposer));
222
+ },
223
+ };
224
+ };
225
+
226
+ const toEncodingOptions = (
227
+ encoding: FsEncoding | { readonly encoding: FsEncoding },
228
+ ): { readonly encoding: FsEncoding } =>
229
+ typeof encoding === "string" ? { encoding } : encoding;
230
+
231
+ const fsErrorReasonByCode: Readonly<Record<string, FsErrorReason>> = {
232
+ ENOENT: "NotFound",
233
+ EEXIST: "AlreadyExists",
234
+ EACCES: "PermissionDenied",
235
+ EPERM: "PermissionDenied",
236
+ EISDIR: "IsDirectory",
237
+ ERR_FS_EISDIR: "IsDirectory",
238
+ ERR_FS_CP_EEXIST: "AlreadyExists",
239
+ ERR_FS_CP_NON_DIR_TO_DIR: "IsDirectory",
240
+ ERR_FS_CP_DIR_TO_NON_DIR: "NotDirectory",
241
+ ENOTDIR: "NotDirectory",
242
+ ENOTEMPTY: "NotEmpty",
243
+ EBUSY: "Busy",
244
+ };
245
+
246
+ const createFsError = (
247
+ method: string,
248
+ path: FsPath,
249
+ error: unknown,
250
+ destination?: FsPath,
251
+ ): FsError => {
252
+ assert(error instanceof Error, "Node fs rejects with an Error.");
253
+ const { code, syscall } = error as NodeJS.ErrnoException;
254
+
255
+ return {
256
+ type: "FsError",
257
+ reason: fsErrorReasonByCode[String(code)] ?? "Unknown",
258
+ path: typeof path === "string" ? path : path.href,
259
+ ...(destination === undefined
260
+ ? {}
261
+ : {
262
+ destination:
263
+ typeof destination === "string" ? destination : destination.href,
264
+ }),
265
+ syscall: syscall ?? method,
266
+ message: error.message,
267
+ };
268
+ };
269
+
270
+ const fsEntryTypeByMode: Readonly<Record<number, FsEntryType>> = {
271
+ [constants.S_IFREG]: "File",
272
+ [constants.S_IFDIR]: "Directory",
273
+ [constants.S_IFLNK]: "SymbolicLink",
274
+ [constants.S_IFBLK]: "BlockDevice",
275
+ [constants.S_IFCHR]: "CharacterDevice",
276
+ [constants.S_IFIFO]: "FIFO",
277
+ [constants.S_IFSOCK]: "Socket",
278
+ };
279
+
280
+ const statsToFsMetadata = (stats: Stats): FsMetadata => ({
281
+ type: fsEntryTypeByMode[stats.mode & constants.S_IFMT] ?? "Unknown",
282
+ dev: stats.dev,
283
+ ino: stats.ino,
284
+ mode: stats.mode,
285
+ nlink: stats.nlink,
286
+ uid: stats.uid,
287
+ gid: stats.gid,
288
+ rdev: stats.rdev,
289
+ size: ByteLength.orThrow(stats.size),
290
+ blksize: stats.blksize,
291
+ blocks: stats.blocks,
292
+ atimeMs: stats.atimeMs,
293
+ mtimeMs: stats.mtimeMs,
294
+ ctimeMs: stats.ctimeMs,
295
+ birthtimeMs: stats.birthtimeMs,
296
+ atime: stats.atime,
297
+ mtime: stats.mtime,
298
+ ctime: stats.ctime,
299
+ birthtime: stats.birthtime,
300
+ });
@@ -0,0 +1,32 @@
1
+ import {
2
+ assertEqual,
3
+ assertSame,
4
+ assertType,
5
+ PositiveInt,
6
+ } from "@evolu/common";
7
+ import { mock, test } from "node:test";
8
+ import type {
9
+ AvailableParallelism,
10
+ AvailableParallelismDep,
11
+ } from "./Platform.ts";
12
+
13
+ const nodeAvailableParallelism = mock.fn<() => number>();
14
+
15
+ mock.module("node:os", {
16
+ // @ts-expect-error -- Node.js 24.20 replaces the deprecated namedExports option with exports, which @types/node 24.13 does not declare yet.
17
+ exports: { availableParallelism: nodeAvailableParallelism },
18
+ });
19
+
20
+ const { availableParallelism } = await import("./Platform.ts");
21
+
22
+ test("availableParallelism returns the validated Node.js value", () => {
23
+ nodeAvailableParallelism.mock.mockImplementation(() => 128);
24
+
25
+ const parallelism = availableParallelism();
26
+ const deps = { availableParallelism } satisfies AvailableParallelismDep;
27
+
28
+ assertType<typeof availableParallelism, AvailableParallelism>();
29
+ assertType<typeof parallelism, PositiveInt>();
30
+ assertSame(deps.availableParallelism, availableParallelism);
31
+ assertEqual(parallelism, 128);
32
+ });
package/src/Task.ts CHANGED
@@ -7,6 +7,7 @@
7
7
  import {
8
8
  createConsole,
9
9
  createRun,
10
+ getOrThrow,
10
11
  isDisposable,
11
12
  ok,
12
13
  waitForAbort,
@@ -18,6 +19,7 @@ import {
18
19
  type Task,
19
20
  type Typed,
20
21
  } from "@evolu/common";
22
+ import type { createRelay } from "./local-first/Relay.ts";
21
23
 
22
24
  /**
23
25
  * An abort requested by a Node.js termination signal.
@@ -40,8 +42,8 @@ export interface RunMainOptions {
40
42
  * How termination signals affect the process exit status.
41
43
  *
42
44
  * Services treat a gracefully handled signal as a successful shutdown.
43
- * Commands use the conventional `128 + signal number` exit status unless a
44
- * reported defect has already set a failure status.
45
+ * Commands use the conventional `128 + signal number` exit status unless
46
+ * `process.exitCode` is already set.
45
47
  *
46
48
  * @default "service"
47
49
  */
@@ -49,75 +51,90 @@ export interface RunMainOptions {
49
51
  }
50
52
 
51
53
  /**
52
- * Runs the main Task as the Node.js program lifecycle.
54
+ * Runs the main {@link Task} of a Node.js command or service.
53
55
  *
54
- * Creates one root {@link Run} and aborts it on:
56
+ * A command does a finite job, such as importing data. A service handles
57
+ * ongoing work, such as accepting connections. Choose a mode for how the
58
+ * program reports an interruption:
55
59
  *
56
- * - `SIGINT`: Ctrl-C on all platforms.
57
- * - `SIGTERM`: OS, service, Docker, or Kubernetes termination on Unix.
58
- * - `SIGBREAK`: Ctrl-Break on Windows.
60
+ * - `"command"`: A termination signal means the job was interrupted. After
61
+ * cleanup, use the conventional signal exit status, such as 130 for Ctrl-C,
62
+ * unless `process.exitCode` is already set.
63
+ * - `"service"` (default): A termination signal is an expected way to stop the
64
+ * service. Graceful shutdown does not set a failure exit status.
59
65
  *
60
- * The first signal logs shutdown progress, aborts the root Run, and waits for
61
- * the main Task and structured cleanup to finish. A subsequent signal exits
62
- * immediately with its conventional signal status, abandoning cleanup. A signal
63
- * received during final cleanup still applies signal shutdown behavior.
66
+ * Both modes create one root {@link Run} and wait for cleanup. The mode controls
67
+ * signal exit status; the main Task controls how long `runMain` waits.
64
68
  *
65
- * A main Task returning {@link Resource} keeps the program running until a
66
- * termination signal and is disposed during shutdown. A main Task returning
67
- * `void` completes the program immediately. A Resource result transfers
68
- * ownership of a live resource that must remain valid after its creating Task
69
- * settles.
69
+ * ### Lifetime
70
70
  *
71
- * Service mode treats graceful signal shutdown as successful. Command mode
72
- * preserves conventional signal exit statuses. Every defect reported through
73
- * `reportDefect`, including an observer defect that does not abort the Run,
74
- * sets `process.exitCode` to 1. The default reporter logs to the configured
71
+ * When the main Task returns `void`, `runMain` finishes after the Task and root
72
+ * Run cleanup. It does not force the Node.js process to exit.
73
+ *
74
+ * A service can return a live {@link Resource}, such as the relay created by
75
+ * {@link createRelay}. This transfers ownership to `runMain`, which waits for
76
+ * shutdown and then disposes the Resource. It must remain usable after the
77
+ * creating Task finishes; do not dispose it before returning it.
78
+ *
79
+ * Alternatively, the main Task can own its resources with `using` or `await
80
+ * using` while awaiting {@link waitForAbort} with `run(waitForAbort)`. Waiting
81
+ * for abort does not itself keep Node.js running: the service needs active
82
+ * work, such as a listening server.
83
+ *
84
+ * ### Shutdown and errors
85
+ *
86
+ * Handles `SIGINT` (Ctrl-C), `SIGTERM` (termination by the OS or a service
87
+ * host), and `SIGBREAK` (Ctrl-Break on Windows). The first signal logs shutdown
88
+ * progress, aborts the root Run, and waits for cleanup. A second signal exits
89
+ * immediately with its conventional signal status, abandoning cleanup. Signals
90
+ * are still handled during final cleanup.
91
+ *
92
+ * An error returned by the main Task is fatal. {@link getOrThrow} preserves it
93
+ * in `Error.cause`; the Run reports the failure and finishes cleanup. Every
94
+ * reported defect sets `process.exitCode` to 1, including an observer defect
95
+ * that does not abort the Run. The default reporter logs to the configured
75
96
  * Evolu console.
76
97
  *
77
98
  * Escaped uncaught exceptions and unhandled rejections remain under Node.js
78
99
  * native reporting and termination.
79
100
  *
80
- * ### Service Example
101
+ * ### Example
81
102
  *
82
- * ```ts
83
- * const deps = { ...createRelayDeps(), console: createConsole() };
84
- *
85
- * await runMain(deps)(createRelay({ port: 4000 }));
86
- * ```
87
- *
88
- * A Task returning `void` can keep a service alive explicitly when no Resource
89
- * owns its lifetime:
103
+ * A command finishes when its main Task completes:
90
104
  *
91
105
  * ```ts
92
- * await runMain(deps)(async (run) => {
93
- * void run(processMessages);
94
- * return await run(waitForAbort);
95
- * });
96
- * ```
106
+ * import { assertTrue, ok, type Task } from "@evolu/common";
107
+ * import { runMain } from "@evolu/nodejs";
97
108
  *
98
- * ### Command Example
109
+ * let completed = false;
110
+ * const command: Task<void> = () => {
111
+ * completed = true;
112
+ * return ok();
113
+ * };
99
114
  *
100
- * ```ts
101
115
  * await runMain(command, { mode: "command" });
116
+ * assertTrue(completed);
102
117
  * ```
103
118
  *
104
119
  * @group Node.js Task
105
120
  */
106
- export function runMain<T extends void | Resource>(
107
- main: Task<T>,
121
+ export function runMain<T extends void | Resource, E = never>(
122
+ main: Task<T, E>,
108
123
  options?: RunMainOptions,
109
124
  ): Promise<void>;
110
125
  /** With custom dependencies. */
111
126
  export function runMain<D extends object>(
112
127
  deps: RunCustomDeps<D>,
113
128
  options?: RunMainOptions,
114
- ): <T extends void | Resource>(main: Task<T, never, D>) => Promise<void>;
115
- export function runMain<T extends void | Resource, D extends object>(
116
- mainOrDeps: Task<T> | RunCustomDeps<D>,
129
+ ): <T extends void | Resource, E = never>(main: Task<T, E, D>) => Promise<void>;
130
+ export function runMain<T extends void | Resource, E, D extends object>(
131
+ mainOrDeps: Task<T, E> | RunCustomDeps<D>,
117
132
  { mode = "service" }: RunMainOptions = {},
118
133
  ):
119
134
  | Promise<void>
120
- | (<R extends void | Resource>(main: Task<R, never, D>) => Promise<void>) {
135
+ | (<R extends void | Resource, E = never>(
136
+ main: Task<R, E, D>,
137
+ ) => Promise<void>) {
121
138
  return typeof mainOrDeps === "function"
122
139
  ? runMainInternal(mainOrDeps, {}, mode)
123
140
  : (main) => runMainInternal(main, mainOrDeps, mode);
@@ -129,8 +146,8 @@ const commandExitCodeBySignal: Readonly<Record<NodeSignal, number>> = {
129
146
  SIGBREAK: 149,
130
147
  };
131
148
 
132
- const runMainInternal = async <T extends void | Resource, D extends object>(
133
- main: Task<T, never, D>,
149
+ const runMainInternal = async <T extends void | Resource, E, D extends object>(
150
+ main: Task<T, E, D>,
134
151
  deps: RunCustomDeps<D> & Partial<ConsoleDep & ReportDefectDep>,
135
152
  mode: RunMainMode,
136
153
  ): Promise<void> => {
@@ -175,7 +192,7 @@ const runMainInternal = async <T extends void | Resource, D extends object>(
175
192
 
176
193
  try {
177
194
  await run(async (run) => {
178
- const resource = await run.ok(main);
195
+ const resource = getOrThrow(await run(main));
179
196
  if (!isDisposable(resource)) return ok();
180
197
 
181
198
  await using _resource = resource;
package/src/TestBundle.ts CHANGED
@@ -14,18 +14,20 @@
14
14
  import {
15
15
  allSettled,
16
16
  assert,
17
+ assertLength,
18
+ assertSame,
17
19
  assertType,
18
20
  instanceOf,
19
21
  createRun,
20
22
  durationToMillis,
21
23
  escapeRegExp,
22
- filterArray,
23
24
  isErr,
24
25
  mapArray,
25
26
  type NonEmptyReadonlyArray,
26
27
  type PositiveDuration,
27
28
  type ReadonlyRecord,
28
29
  type Result,
30
+ partitionArray,
29
31
  safelyStringifyUnknownValue,
30
32
  String as StringType,
31
33
  type Task,
@@ -301,7 +303,7 @@ export const testBundle = async ({
301
303
  ),
302
304
  );
303
305
 
304
- const failures = filterArray(results, isErr);
306
+ const [failures, successes] = partitionArray(results, isErr);
305
307
  if (failures.length > 0) {
306
308
  const errors = mapArray(
307
309
  failures,
@@ -331,8 +333,7 @@ export const testBundle = async ({
331
333
  >();
332
334
  for (const [caseName] of caseEntries) bundleEntriesByCase.set(caseName, []);
333
335
 
334
- for (const result of results) {
335
- assert(result.ok, "Expected every bundle test to succeed.");
336
+ for (const result of successes) {
336
337
  const { caseName, bundle } = result.value;
337
338
  const entries = bundleEntriesByCase.get(caseName);
338
339
  assert(entries, `Missing bundle test case "${caseName}".`);
@@ -438,16 +439,11 @@ const testViteBundler: TestBundler = {
438
439
  >;
439
440
  assert(Array.isArray(output), "Vite did not return build outputs.");
440
441
  const outputs: ReadonlyArray<ViteOutput> = output;
441
- assert(outputs.length === 1, "Vite did not return one build output.");
442
- const viteOutput = outputs.at(0);
443
- assert(viteOutput, "Vite did not return a build output.");
444
- assert(
445
- viteOutput.output.length === 1,
446
- "Vite did not emit one JavaScript chunk.",
447
- );
448
- const chunk = viteOutput.output.at(0);
449
- assert(chunk, "Vite did not emit a JavaScript chunk.");
450
- assert(chunk.type === "chunk", "Vite did not emit a JavaScript chunk.");
442
+ assertLength(outputs, 1);
443
+ const viteOutput = outputs[0];
444
+ assertLength(viteOutput.output, 1);
445
+ const chunk = viteOutput.output[0];
446
+ assertSame(chunk.type, "chunk");
451
447
  assertType(StringType, chunk.code);
452
448
 
453
449
  return {
@@ -582,7 +578,7 @@ interface TestBundleWorkerError {
582
578
  readonly stack: string;
583
579
  }
584
580
 
585
- const ErrorType = /*#__PURE__*/ instanceOf(globalThis.Error);
581
+ const ErrorType = /*#__PURE__*/ instanceOf(Error);
586
582
 
587
583
  // TODO: Replace the evaluated CommonJS worker with an ESM module worker.
588
584
  const testBundleWorkerSource = String.raw`
package/src/TestJSDoc.ts CHANGED
@@ -31,7 +31,10 @@ import { stripVTControlCharacters } from "node:util";
31
31
  export interface TestJSDocExamplesOptions {
32
32
  /** Source files or glob patterns resolved from `cwd`. */
33
33
  readonly include: string | ReadonlyArray<string>;
34
- /** Package imports redirected to absolute TypeScript entry paths. */
34
+ /**
35
+ * Package imports redirected to absolute TypeScript entry paths. Type
36
+ * checking also applies these aliases to imports within those modules.
37
+ */
35
38
  readonly aliases?: ReadonlyRecord<string, string>;
36
39
  /** Directory used for globbing and package resolution. */
37
40
  readonly cwd?: string;
@@ -219,6 +222,9 @@ export const testJSDocExamples = async ({
219
222
  noEmit: true,
220
223
  noUnusedLocals: false,
221
224
  noUnusedParameters: false,
225
+ paths: Object.fromEntries(
226
+ Object.entries(aliases).map(([name, target]) => [name, [target]]),
227
+ ),
222
228
  target: "es2022",
223
229
  types: ["node"],
224
230
  },