@noverax/courier-build 0.0.0-stage → 0.4.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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +289 -2
  3. package/lib/config/index.d.ts +420 -0
  4. package/lib/config/index.js +66 -0
  5. package/lib/config-file-of-Zkwyi9zf.js +28 -0
  6. package/lib/config-in-page-message-DLd3f0Jd.js +20 -0
  7. package/lib/courier-routes-options-BYF7Sfdf.d.cts +13 -0
  8. package/lib/courier-routes-options-BYF7Sfdf.d.ts +13 -0
  9. package/lib/engine/worker.d.ts +1 -0
  10. package/lib/engine/worker.js +551 -0
  11. package/lib/errno-code-CNx_E80R.js +32 -0
  12. package/lib/esbuild/index.d.ts +58 -0
  13. package/lib/esbuild/index.js +191 -0
  14. package/lib/index-_VUU5N5v.d.ts +134 -0
  15. package/lib/index.d.ts +101 -0
  16. package/lib/index.js +4 -0
  17. package/lib/jest/routes-thread.d.ts +1 -0
  18. package/lib/jest/routes-thread.js +46 -0
  19. package/lib/jest/transformer.cjs +121 -0
  20. package/lib/jest/transformer.d.cts +39 -0
  21. package/lib/load-config-BeedbpkN.js +1419 -0
  22. package/lib/metro/index.cjs +25 -0
  23. package/lib/metro/index.d.cts +55 -0
  24. package/lib/metro/index.d.ts +67 -0
  25. package/lib/metro/index.js +105 -0
  26. package/lib/package-file-CVAKY_Nu.js +39 -0
  27. package/lib/rollup/index.d.ts +51 -0
  28. package/lib/rollup/index.js +95 -0
  29. package/lib/routes-engine-BfwrCuNL.d.ts +117 -0
  30. package/lib/routes-file-DcpgxYb1.js +206 -0
  31. package/lib/routes-loader-context-40OKDrQp.d.ts +26 -0
  32. package/lib/routes-module-code-DO3aII6v.js +402 -0
  33. package/lib/routes-module-id-CnUjQjYF.js +12 -0
  34. package/lib/routes-read-error-DQhKIE2F.js +30 -0
  35. package/lib/routes-rule-CjzX6NS5.js +108 -0
  36. package/lib/rsbuild/index.d.ts +41 -0
  37. package/lib/rsbuild/index.js +37 -0
  38. package/lib/rspack/index.d.ts +3 -0
  39. package/lib/rspack/index.js +1 -0
  40. package/lib/shared-engine-CCu7vP_0.js +28 -0
  41. package/lib/vite/index.d.ts +33 -0
  42. package/lib/vite/index.js +346 -0
  43. package/lib/webpack/index.d.ts +4 -0
  44. package/lib/webpack/index.js +3 -0
  45. package/lib/webpack/loader.cjs +31 -0
  46. package/lib/webpack/loader.d.cts +40 -0
  47. package/lib/webpack/run-routes-loader.d.ts +22 -0
  48. package/lib/webpack/run-routes-loader.js +103 -0
  49. package/lib/webpack-B0v9i8lk.js +42 -0
  50. package/lib/worker-messages-BT2vvNQJ.js +35 -0
  51. package/package.json +209 -4
@@ -0,0 +1,1419 @@
1
+ import { n as isRecord, t as errnoCode } from "./errno-code-CNx_E80R.js";
2
+ import { createRequire } from "node:module";
3
+ import { dirname, isAbsolute, join, normalize, resolve } from "node:path";
4
+ import { closeSync, constants, existsSync, fstatSync, futimesSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, realpathSync, renameSync, statSync, unlinkSync, utimesSync, writeFileSync } from "node:fs";
5
+ import { fileURLToPath } from "node:url";
6
+ import { CourierError, createLogger } from "@noverax/courier";
7
+ import { createHash, randomBytes } from "node:crypto";
8
+ import { parseJSON5 } from "confbox";
9
+ // src/config/config-candidates.ts
10
+ /** The file names a Courier config may have, in the order they are looked for */
11
+ const CONFIG_BASENAMES = [
12
+ "courier.config.ts",
13
+ "courier.config.mts",
14
+ "courier.config.cts",
15
+ "courier.config.js",
16
+ "courier.config.mjs",
17
+ "courier.config.cjs"
18
+ ];
19
+ const PROJECT_TOP_MARKERS = [
20
+ ".git",
21
+ ".hg",
22
+ ".jj",
23
+ "pnpm-workspace.yaml"
24
+ ];
25
+ /**
26
+ * Every path a config is looked for at from a folder, in the order it is
27
+ * looked for: each of {@link CONFIG_BASENAMES} in the folder, then in each
28
+ * folder above it, up to the top of the project — the first folder that
29
+ * holds its repository (`.git`, `.hg`, `.jj`) or is the root of its
30
+ * workspace (`pnpm-workspace.yaml`, or a `package.json` that declares
31
+ * `workspaces`) — or, with no such folder, the root of the file system.
32
+ *
33
+ * @param startDir - The folder to look in first
34
+ * @returns The paths, absolute, whether there or not
35
+ * @example
36
+ * ```ts
37
+ * configCandidates("/repo/apps/web"); // → ["/repo/apps/web/courier.config.ts", …, "/repo/courier.config.cjs"]
38
+ * ```
39
+ */
40
+ function configCandidates(startDir) {
41
+ const candidates = [];
42
+ let dir = resolve(startDir);
43
+ while (true) {
44
+ for (const basename of CONFIG_BASENAMES) candidates.push(join(dir, basename));
45
+ const parent = dirname(dir);
46
+ if (parent === dir || isProjectTop(dir)) return candidates;
47
+ dir = parent;
48
+ }
49
+ }
50
+ function isProjectTop(dir) {
51
+ return PROJECT_TOP_MARKERS.some((marker) => existsSync(join(dir, marker))) || declaresWorkspaces(join(dir, "package.json"));
52
+ }
53
+ function declaresWorkspaces(manifest) {
54
+ if (!existsSync(manifest)) return false;
55
+ try {
56
+ const parsed = JSON.parse(readFileSync(manifest, "utf8"));
57
+ return isRecord(parsed) && parsed.workspaces !== void 0;
58
+ } catch {
59
+ return false;
60
+ }
61
+ }
62
+ // src/config/installed-packages.ts
63
+ /**
64
+ * Which packages a project has installed, for the path aliases to stop at.
65
+ */
66
+ /**
67
+ * The packages installed where `projectRoot` resolves them, each looked up
68
+ * once.
69
+ *
70
+ * @param projectRoot - Directory holding the project's `tsconfig.json`
71
+ * @returns The lookup
72
+ * @example
73
+ * ```ts
74
+ * installedPackages("/app").has("@acme/ui"); // true once it is in node_modules
75
+ * ```
76
+ */
77
+ function installedPackages(projectRoot) {
78
+ const require = createRequire(resolve(projectRoot, "tsconfig.json"));
79
+ const known = /* @__PURE__ */ new Map();
80
+ return { has(name) {
81
+ let installed = known.get(name);
82
+ if (installed === void 0) {
83
+ installed = resolves(require, name);
84
+ known.set(name, installed);
85
+ }
86
+ return installed;
87
+ } };
88
+ }
89
+ /**
90
+ * The package a specifier or an alias prefix names: `@scope/name`, or its
91
+ * first segment; `null` when it names none, as `@scope/` does.
92
+ *
93
+ * @param specifier - A specifier, or an alias's text before its wildcard
94
+ * @returns The package's name, or `null`
95
+ * @example
96
+ * ```ts
97
+ * packageNameOf("@acme/ui/button"); // "@acme/ui"
98
+ * ```
99
+ */
100
+ function packageNameOf(specifier) {
101
+ const segments = specifier.split("/");
102
+ const wanted = specifier.startsWith("@") ? 2 : 1;
103
+ if (segments.length < wanted) return null;
104
+ const name = segments.slice(0, wanted);
105
+ return name.every((segment) => segment.length > 0) ? name.join("/") : null;
106
+ }
107
+ function resolves(require, name) {
108
+ try {
109
+ require.resolve(name);
110
+ return true;
111
+ } catch (error) {
112
+ return errnoCode(error) !== "MODULE_NOT_FOUND";
113
+ }
114
+ }
115
+ // src/config/scope-alias.ts
116
+ /**
117
+ * An alias for a whole scope, `@acme/*`, in the shape the module loader takes.
118
+ */
119
+ const log$7 = createLogger("Config");
120
+ const MODULE_EXTENSION = /\.(d\.ts|[cm]?[jt]sx?|json)$/;
121
+ /**
122
+ * The scope's name, `@acme`, when `prefix` is an alias's text before the
123
+ * wildcard of a pattern for a whole scope, `@acme/*`.
124
+ *
125
+ * @param prefix - An alias's text before its wildcard
126
+ * @returns The scope, or `undefined` for any other prefix
127
+ * @example
128
+ * ```ts
129
+ * scopeOf("@acme/"); // "@acme"
130
+ * ```
131
+ */
132
+ function scopeOf(prefix) {
133
+ return /^(@[^/]+)\/$/.exec(prefix)?.[1];
134
+ }
135
+ /**
136
+ * One loader alias per name a scope alias leads to, leaving out the names of
137
+ * installed packages. The loader follows an alias whatever it finds there,
138
+ * where TypeScript takes an installed package past it: `@acme/*` to
139
+ * `./src/*` keeps `@acme/utils` on `src/utils` while `@acme/ui`, installed,
140
+ * loads the build it ships.
141
+ *
142
+ * @param scope - The scope, `@acme`
143
+ * @param directory - The directory the alias's `*` stands in, absolute
144
+ * @param installed - The packages the project installed
145
+ * @returns Prefix → absolute target, for each name the directory holds
146
+ * @example
147
+ * ```ts
148
+ * scopeAliases("@acme", "/app/src", installed); // { "@acme/utils": "/app/src/utils" }
149
+ * ```
150
+ */
151
+ function scopeAliases(scope, directory, installed) {
152
+ const alias = {};
153
+ for (const entry of entriesOf(directory)) {
154
+ const name = entry.replace(MODULE_EXTENSION, "");
155
+ if (!installed.has(`${scope}/${name}`)) alias[`${scope}/${name}`] = join(directory, name);
156
+ }
157
+ return alias;
158
+ }
159
+ function entriesOf(directory) {
160
+ try {
161
+ return readdirSync(directory);
162
+ } catch (error) {
163
+ const code = errnoCode(error);
164
+ if (code !== "ENOENT" && code !== "ENOTDIR") log$7.warn(`Could not list ${directory} for a path alias; the alias leads nowhere`, error);
165
+ return [];
166
+ }
167
+ }
168
+ // src/config/read-tsconfig-file.ts
169
+ const log$6 = createLogger("Config");
170
+ function isTextList(value) {
171
+ if (!Array.isArray(value)) return false;
172
+ return value.every((item) => typeof item === "string");
173
+ }
174
+ function readText(path) {
175
+ try {
176
+ if (!statSync(path).isFile()) {
177
+ log$6.warn(`${path} is not a regular file, so its path aliases are ignored`);
178
+ return null;
179
+ }
180
+ return readFileSync(path, "utf-8");
181
+ } catch (err) {
182
+ log$6.warn(`Could not read ${path}, so its path aliases are ignored`, err);
183
+ return null;
184
+ }
185
+ }
186
+ function extendsOf(value, path) {
187
+ if (value === void 0 || value === null) return [];
188
+ if (typeof value === "string") return [value];
189
+ if (!Array.isArray(value)) {
190
+ log$6.warn(`${path}: "extends" is neither a path nor a list of paths, so it is not followed`);
191
+ return [];
192
+ }
193
+ const entries = value;
194
+ const paths = entries.filter((entry) => typeof entry === "string");
195
+ if (paths.length < entries.length) log$6.warn(`${path}: "extends" lists something other than a path, which is skipped`);
196
+ return paths;
197
+ }
198
+ function pathsOf(value, path) {
199
+ if (value === void 0) return void 0;
200
+ if (!isRecord(value)) {
201
+ log$6.warn(`${path}: "paths" is not an object, so its path aliases are ignored`);
202
+ return;
203
+ }
204
+ const kept = [];
205
+ for (const [pattern, targets] of Object.entries(value)) if (isTextList(targets)) kept.push([pattern, targets]);
206
+ else log$6.warn(`${path}: "paths" maps "${pattern}" to something other than a list of paths, so it is ignored`);
207
+ return Object.fromEntries(kept);
208
+ }
209
+ function optionsOf(value, path) {
210
+ if (value === void 0) return {};
211
+ if (!isRecord(value)) {
212
+ log$6.warn(`${path}: "compilerOptions" is not an object, so its path aliases are ignored`);
213
+ return {};
214
+ }
215
+ const { baseUrl } = value;
216
+ const paths = pathsOf(value.paths, path);
217
+ const base = typeof baseUrl === "string" ? baseUrl : void 0;
218
+ if (baseUrl !== void 0 && base === void 0) log$6.warn(`${path}: "baseUrl" is not a path, so its aliases resolve from its own directory`);
219
+ return {
220
+ ...base !== void 0 && { baseUrl: base },
221
+ ...paths && { paths }
222
+ };
223
+ }
224
+ /**
225
+ * Read the fields of a tsconfig that its path mapping depends on. A field
226
+ * of another shape than TypeScript takes is left out with a warning, as
227
+ * TypeScript reports it, rather than failing the load of the config that
228
+ * reads the mapping.
229
+ *
230
+ * @param path - The tsconfig
231
+ * @returns What it declares; `null` when it cannot be read or parsed
232
+ * @example
233
+ * ```ts
234
+ * readTsconfigFile("/app/tsconfig.json");
235
+ * // → { extends: ["./tsconfig.base.json"], paths: { "@/*": ["./src/*"] } }
236
+ * ```
237
+ */
238
+ function readTsconfigFile(path) {
239
+ const text = readText(path);
240
+ if (text === null) return null;
241
+ let parsed;
242
+ try {
243
+ parsed = parseJSON5(text);
244
+ } catch (err) {
245
+ log$6.warn(`Could not parse ${path}, so its path aliases are ignored`, err);
246
+ return null;
247
+ }
248
+ if (!isRecord(parsed)) {
249
+ log$6.warn(`${path} does not hold an object, so its path aliases are ignored`);
250
+ return null;
251
+ }
252
+ return {
253
+ extends: extendsOf(parsed.extends, path),
254
+ ...optionsOf(parsed.compilerOptions, path)
255
+ };
256
+ }
257
+ // src/config/tsconfig-paths.ts
258
+ /**
259
+ * Read a project's `compilerOptions.paths` without loading TypeScript.
260
+ *
261
+ * The compiler would do this for us, but its API is exactly what a config
262
+ * reader must not depend on: it is an optional peer a JavaScript-only project
263
+ * is entitled not to have, and TypeScript 7 publishes no parser under the
264
+ * package root at all. Reading the file directly costs one JSONC parse.
265
+ *
266
+ * `paths` is not merged down an `extends` chain — TypeScript replaces the map
267
+ * wholesale — so the nearest config that declares one wins, and its targets
268
+ * resolve against its own `baseUrl`, or its own directory when it has none.
269
+ *
270
+ * Parsing is JSON5 rather than strict JSONC: a tsconfig is hand-edited and
271
+ * routinely carries comments and trailing commas, both of which TypeScript
272
+ * accepts. Refusing them would drop a real project's aliases without a word.
273
+ */
274
+ const log$5 = createLogger("Config");
275
+ /**
276
+ * Read the path mapping that applies to files in `directory`: the one of the
277
+ * nearest `tsconfig.json` at or above it, as TypeScript finds it, so a config
278
+ * file kept below the project root — `deploy/server.config.ts` — keeps every
279
+ * alias the project declares.
280
+ *
281
+ * @param directory - Directory of the file whose imports are resolved
282
+ * @returns The mapping, or `null` when no tsconfig declares one
283
+ *
284
+ * @example
285
+ * ```ts
286
+ * const mapping = readTsconfigPaths("/app/deploy");
287
+ * // → { baseUrl: "/app", paths: { "@/*": ["./src/*"] } }
288
+ * ```
289
+ */
290
+ function readTsconfigPaths(directory) {
291
+ const tsconfig = nearestTsconfig(resolve(directory));
292
+ return tsconfig ? findPaths(tsconfig, /* @__PURE__ */ new Set()) : null;
293
+ }
294
+ function nearestTsconfig(directory) {
295
+ for (let dir = directory;; dir = dirname(dir)) {
296
+ const candidate = resolve(dir, "tsconfig.json");
297
+ if (existsSync(candidate)) return candidate;
298
+ if (dirname(dir) === dir) return null;
299
+ }
300
+ }
301
+ /** Walk the `extends` chain depth-first, stopping at the first declared map. */
302
+ function findPaths(file, visited) {
303
+ const path = resolve(file);
304
+ if (visited.has(path)) return null;
305
+ visited.add(path);
306
+ const config = readTsconfigFile(path);
307
+ if (!config) return null;
308
+ if (config.paths) return {
309
+ baseUrl: resolve(dirname(path), config.baseUrl ?? "."),
310
+ paths: config.paths
311
+ };
312
+ for (const specifier of [...config.extends].reverse()) {
313
+ const parent = resolveExtends(specifier, dirname(path));
314
+ if (!parent) {
315
+ log$5.warn(`${path} extends "${specifier}", which cannot be found, so its path aliases are not read`);
316
+ continue;
317
+ }
318
+ const found = findPaths(parent, visited);
319
+ if (found) return found;
320
+ }
321
+ return null;
322
+ }
323
+ /**
324
+ * Resolve an `extends` specifier: a path, a directory holding a
325
+ * `tsconfig.json`, or an installed package that ships one.
326
+ */
327
+ function resolveExtends(specifier, fromDir) {
328
+ if (specifier.startsWith(".") || isAbsolute(specifier)) {
329
+ const candidate = resolve(fromDir, specifier);
330
+ if (existsSync(candidate) && statSync(candidate).isDirectory()) {
331
+ const nested = resolve(candidate, "tsconfig.json");
332
+ return existsSync(nested) ? nested : null;
333
+ }
334
+ if (existsSync(candidate)) return candidate;
335
+ const suffixed = `${candidate}.json`;
336
+ return existsSync(suffixed) ? suffixed : null;
337
+ }
338
+ const requireFrom = createRequire(resolve(fromDir, "tsconfig.json"));
339
+ for (const target of [specifier, `${specifier}/tsconfig.json`]) {
340
+ const found = resolveFrom(requireFrom, target);
341
+ if (found) return found;
342
+ }
343
+ return null;
344
+ }
345
+ function resolveFrom(requireFrom, target) {
346
+ try {
347
+ return requireFrom.resolve(target);
348
+ } catch (err) {
349
+ log$5.debug(`"${target}" does not resolve to a tsconfig`, err);
350
+ return null;
351
+ }
352
+ }
353
+ // src/config/path-aliases.ts
354
+ /**
355
+ * Turn a project's tsconfig `paths` into the two shapes that reading a config
356
+ * needs: the alias map the module loader takes, and a lookup the dependency
357
+ * walker uses to follow an aliased import.
358
+ *
359
+ * An alias that shadows an installed package is left out of both. Such an
360
+ * entry redirects a dependency to its sources for the bundler's benefit —
361
+ * `@acme/ui` to `../ui/src`, or a whole scope, `@acme/*` — and following it
362
+ * would load a library's source instead of the build it ships, then treat
363
+ * every file inside it as something the config depends on. A dependency
364
+ * changes when it is reinstalled, not while the developer is editing; the
365
+ * graph stops at the package boundary. A catch-all `*` covers every package,
366
+ * so it is left out too: the loader, which cannot fall back to the package as
367
+ * TypeScript does, would put its target before every path it loads.
368
+ */
369
+ const EMPTY = {
370
+ loader: {},
371
+ candidatesFor: () => []
372
+ };
373
+ /**
374
+ * Read the path aliases that apply to `projectRoot`.
375
+ *
376
+ * @param projectRoot - Directory holding the project's `tsconfig.json`
377
+ * @returns The aliases, empty when the project declares none
378
+ *
379
+ * @example
380
+ * ```ts
381
+ * const aliases = readPathAliases("/app");
382
+ * aliases.candidatesFor("@/lib/sources"); // → ["/app/src/lib/sources"]
383
+ * ```
384
+ */
385
+ function readPathAliases(projectRoot) {
386
+ const mapping = readTsconfigPaths(projectRoot);
387
+ if (!mapping) return EMPTY;
388
+ const installed = installedPackages(projectRoot);
389
+ const entries = toEntries(mapping).filter((entry) => !shadowsInstalled(entry, installed));
390
+ if (entries.length === 0) return EMPTY;
391
+ return {
392
+ loader: toLoaderAlias(entries, installed),
393
+ candidatesFor: (specifier) => candidatesOf(entries, specifier, installed)
394
+ };
395
+ }
396
+ function shadowsInstalled(entry, installed) {
397
+ if (entry.prefix === "") return true;
398
+ if (entry.wildcard && scopeOf(entry.prefix) !== void 0) return false;
399
+ const name = packageNameOf(entry.prefix);
400
+ return name !== null && installed.has(name);
401
+ }
402
+ function toEntries({ baseUrl, paths }) {
403
+ const entries = [];
404
+ for (const [pattern, targets] of Object.entries(paths)) {
405
+ const star = pattern.indexOf("*");
406
+ entries.push(star === -1 ? {
407
+ prefix: pattern,
408
+ suffix: "",
409
+ wildcard: false,
410
+ base: baseUrl,
411
+ targets
412
+ } : {
413
+ prefix: pattern.slice(0, star),
414
+ suffix: pattern.slice(star + 1),
415
+ wildcard: true,
416
+ base: baseUrl,
417
+ targets
418
+ });
419
+ }
420
+ return entries;
421
+ }
422
+ /** TypeScript picks the single match with the longest prefix before `*`. */
423
+ function candidatesOf(entries, specifier, installed) {
424
+ let best = null;
425
+ for (const entry of entries) {
426
+ if (!matches(entry, specifier)) continue;
427
+ if (!best || entry.prefix.length > best.prefix.length) best = entry;
428
+ }
429
+ if (!best) return [];
430
+ const { base } = best;
431
+ if (!best.wildcard) return best.targets.map((target) => resolve(base, target));
432
+ const name = scopeOf(best.prefix) === void 0 ? null : packageNameOf(specifier);
433
+ if (name !== null && installed.has(name)) return [];
434
+ const matched = specifier.slice(best.prefix.length, specifier.length - best.suffix.length);
435
+ return best.targets.map((target) => resolve(base, withStarFilled(target, matched)));
436
+ }
437
+ function withStarFilled(target, matched) {
438
+ const star = target.indexOf("*");
439
+ return star === -1 ? target : `${target.slice(0, star)}${matched}${target.slice(star + 1)}`;
440
+ }
441
+ function matches(entry, specifier) {
442
+ if (!entry.wildcard) return specifier === entry.prefix;
443
+ return specifier.length >= entry.prefix.length + entry.suffix.length && specifier.startsWith(entry.prefix) && specifier.endsWith(entry.suffix);
444
+ }
445
+ /**
446
+ * Only a mapping that ends in `*` on both sides translates to a prefix alias;
447
+ * one with a suffix after the wildcard has no equivalent and stays behind. A
448
+ * scope's alias becomes one alias per name it leads to that is not installed.
449
+ */
450
+ function toLoaderAlias(entries, installed) {
451
+ const alias = {};
452
+ for (const entry of entries) {
453
+ const written = entry.targets[0];
454
+ if (!written) continue;
455
+ const target = resolve(entry.base, written);
456
+ if (!entry.wildcard) alias[entry.prefix] = target;
457
+ else if (entry.suffix === "" && target.endsWith("*")) {
458
+ const scope = scopeOf(entry.prefix);
459
+ if (scope === void 0) alias[entry.prefix] = target.slice(0, -1);
460
+ else Object.assign(alias, scopeAliases(scope, target.slice(0, -1), installed));
461
+ }
462
+ }
463
+ return alias;
464
+ }
465
+ // src/config/config-jiti.ts
466
+ let ownTransform;
467
+ function jitiOwnTransform(createJiti) {
468
+ if (ownTransform) return ownTransform;
469
+ const { transform } = createJiti(fileURLToPath(import.meta.url), {
470
+ fsCache: false,
471
+ moduleCache: false
472
+ }).options;
473
+ if (!transform) throw new Error("jiti created an instance without its own transform");
474
+ ownTransform = transform;
475
+ return transform;
476
+ }
477
+ /**
478
+ * The module loader for Courier's project files — `courier.config.*` and
479
+ * `courier.mocks.*` — with the options both need: the project's tsconfig path
480
+ * aliases, default-export interop, no module cache, and no native import
481
+ * tried before the loader's own.
482
+ *
483
+ * The module cache is off because this is also the reload path: the dev
484
+ * server loads the files again on every edit, and a cached module would hand
485
+ * back the file as it was before the user changed it. Every load therefore
486
+ * reads and evaluates every file again; a `transformCache` only spares the
487
+ * transform of a file whose bytes are unchanged.
488
+ *
489
+ * jiti's native attempt, which imports every module natively before falling
490
+ * back to its own loader, stays off too, whatever `JITI_TRY_NATIVE` says and
491
+ * under Bun, where it defaults to on: a module imported natively is the
492
+ * runtime's first evaluation of it for the life of the process, so an edit to
493
+ * it would never be seen, and the attempt comes before the module cache
494
+ * `evalModule` is handed, so two files evaluated over one cache would each
495
+ * evaluate a module they both import.
496
+ *
497
+ * jiti's own file cache stays off, with or without a `transformCache`:
498
+ * - it writes each entry in place, so concurrent writers can leave an entry
499
+ * torn between two transforms that its check still accepts;
500
+ * - an entry it cannot write makes the whole load throw `EACCES`;
501
+ * - unless the file's own directory has a `node_modules`, it falls back to
502
+ * `os.tmpdir()/jiti`, where another user of the machine can place an entry
503
+ * that is then run;
504
+ * - its key ignores `interopDefault` and the babel options, so a transform
505
+ * made under other options can be handed back;
506
+ * - nothing ever prunes it.
507
+ *
508
+ * jiti is loaded here rather than at the top of a module so a bundler that
509
+ * merely reaches this code does not pull a TypeScript transpiler into its
510
+ * graph; only a process that actually reads such a file pays for it.
511
+ *
512
+ * @param file - Absolute, symlink-resolved path of the file to load; modules
513
+ * and aliases resolve from its directory
514
+ * @param options - Whether to resolve through the project's path aliases
515
+ * (default: yes), and where to reuse transforms (default: nowhere)
516
+ * @returns A jiti instance for that file
517
+ * @example
518
+ * ```ts
519
+ * const transformCache = createTransformCache();
520
+ * const jiti = await createConfigJiti(configFile, { transformCache });
521
+ * const config = await jiti.import(configFile, { default: true });
522
+ * ```
523
+ */
524
+ async function createConfigJiti(file, { aliases = true, transformCache, onModule, runTransform } = {}) {
525
+ const { createJiti } = await import("jiti");
526
+ const options = {
527
+ alias: aliases ? readPathAliases(dirname(file)).loader : {},
528
+ interopDefault: true,
529
+ moduleCache: false,
530
+ fsCache: false,
531
+ tryNative: false
532
+ };
533
+ if (!transformCache && !onModule && !runTransform) return createJiti(file, options);
534
+ const compute = jitiOwnTransform(createJiti);
535
+ const cached = transformCache ? (transformOptions) => transformCache.transform(transformOptions, compute) : compute;
536
+ const transform = (given) => {
537
+ const transformOptions = given.filename === void 0 ? given : {
538
+ ...given,
539
+ filename: normalize(given.filename)
540
+ };
541
+ if (onModule && transformOptions.filename !== void 0) onModule(transformOptions.filename);
542
+ return runTransform ? runTransform(() => cached(transformOptions)) : cached(transformOptions);
543
+ };
544
+ return createJiti(file, {
545
+ ...options,
546
+ transform
547
+ });
548
+ }
549
+ // src/config/untrusted-config-error.ts
550
+ /**
551
+ * The config nearest a folder lies in a folder above it, and someone else
552
+ * could have put it there: it belongs to another user, or any user may
553
+ * write it. It was not run, as it would run with the rights of whoever
554
+ * builds or serves the project.
555
+ *
556
+ * @example
557
+ * ```ts
558
+ * throw new UntrustedConfigError("/tmp/courier.config.ts", "/tmp/app", "it belongs to uid 1001");
559
+ * ```
560
+ */
561
+ var UntrustedConfigError = class extends CourierError {
562
+ /**
563
+ * @param configFile - The config that was not run, absolute
564
+ * @param folder - The folder it was looked for from
565
+ * @param reason - Why it is not trusted, worded to follow "and"
566
+ */
567
+ constructor(configFile, folder, reason) {
568
+ super(`${configFile} was not run: it lies above ${folder}, and ${reason}. A config in a folder above the project runs only when it is yours and no one else may change it; move it into the project, or name the project's own config.`, {
569
+ code: "CONFIG_NOT_TRUSTED",
570
+ context: {
571
+ configFile,
572
+ folder
573
+ }
574
+ });
575
+ }
576
+ };
577
+ // src/config/assert-own-config.ts
578
+ const OTHERS_WRITE = 2;
579
+ /**
580
+ * Refuse a config found in a folder above the one it was looked for from,
581
+ * unless it belongs to this process's user or to the owner of that folder,
582
+ * and no user outside its group may write it. A folder many users share,
583
+ * such as `/tmp`, lets anyone put a config there, and it would run with the
584
+ * rights of whoever builds a project below it. Windows has no owner of that
585
+ * kind, so nothing is refused there.
586
+ *
587
+ * @param configFile - The config found, absolute
588
+ * @param folder - The folder it was looked for from, absolute
589
+ * @throws UntrustedConfigError when someone else could have put it there
590
+ * @example
591
+ * ```ts
592
+ * assertOwnConfig("/repo/courier.config.ts", "/repo/apps/web");
593
+ * ```
594
+ */
595
+ function assertOwnConfig(configFile, folder) {
596
+ const uid = process.getuid?.();
597
+ if (uid === void 0) return;
598
+ const stats = statSync(configFile);
599
+ const folderOwner = statSync(folder, { throwIfNoEntry: false })?.uid ?? uid;
600
+ if (stats.uid !== uid && stats.uid !== folderOwner) throw new UntrustedConfigError(configFile, folder, `it belongs to uid ${stats.uid}, neither this user (${uid}) nor the owner of the folder (${folderOwner})`);
601
+ if ((stats.mode & OTHERS_WRITE) !== 0) throw new UntrustedConfigError(configFile, folder, "any user may write it");
602
+ }
603
+ // src/config/transform-entry-format.ts
604
+ /**
605
+ * How the first line of a transform cache entry is written.
606
+ *
607
+ * - `prefix`: how every entry starts, whatever the version of its format, so
608
+ * pruning can tell an entry from a file someone else put in the directory.
609
+ * - `version`: the version this code writes; an entry in any other version
610
+ * is ignored and replaced.
611
+ */
612
+ const TRANSFORM_ENTRY_FORMAT = {
613
+ prefix: "// courier-transform ",
614
+ version: "2"
615
+ };
616
+ // src/config/encode-transform-entry.ts
617
+ /**
618
+ * The bytes of an entry holding `code`, the transform of the input whose
619
+ * digest is `digest`.
620
+ *
621
+ * The first line names the format's version, the input digest, and the byte
622
+ * length and sha256 of the UTF-8 body that follows it, so a reader can tell
623
+ * a complete entry from a truncated or altered one.
624
+ *
625
+ * @param digest - The input digest, as `transformDigest` computes it
626
+ * @param code - The transformed code; well-formed UTF-16, so UTF-8 keeps it exactly
627
+ * @returns The entry's bytes
628
+ * @example
629
+ * ```ts
630
+ * writeFileSync(path, encodeTransformEntry(digest, result.code));
631
+ * ```
632
+ */
633
+ function encodeTransformEntry(digest, code) {
634
+ const { prefix, version } = TRANSFORM_ENTRY_FORMAT;
635
+ const body = Buffer.from(code, "utf8");
636
+ const bodySha256 = createHash("sha256").update(body).digest("hex");
637
+ const header = `${prefix}${version} ${digest} ${body.length} ${bodySha256}\n`;
638
+ return Buffer.concat([Buffer.from(header, "latin1"), body]);
639
+ }
640
+ // src/config/prune-transform-cache.ts
641
+ const log$4 = createLogger("Config");
642
+ const HOUR_MS = 36e5;
643
+ const ENTRY_MAX_AGE_MS = 720 * HOUR_MS;
644
+ const TEMPORARY_MAX_AGE_MS = HOUR_MS;
645
+ const READ_FLAGS$1 = constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0);
646
+ const PREFIX = Buffer.from(TRANSFORM_ENTRY_FORMAT.prefix, "latin1");
647
+ function expiryOf(name) {
648
+ if (name.endsWith(".tmp")) return {
649
+ maxAgeMs: TEMPORARY_MAX_AGE_MS,
650
+ prefix: null
651
+ };
652
+ if (name.endsWith(".js")) return {
653
+ maxAgeMs: ENTRY_MAX_AGE_MS,
654
+ prefix: PREFIX
655
+ };
656
+ return null;
657
+ }
658
+ function isExpired(path, { maxAgeMs, prefix }, now) {
659
+ const fd = openSync(path, READ_FLAGS$1);
660
+ try {
661
+ const stats = fstatSync(fd);
662
+ if (!stats.isFile() || now - stats.mtimeMs <= maxAgeMs) return false;
663
+ if (prefix === null) return true;
664
+ const start = Buffer.alloc(prefix.length);
665
+ return readSync(fd, start, 0, prefix.length, 0) === prefix.length && start.equals(prefix);
666
+ } finally {
667
+ closeSync(fd);
668
+ }
669
+ }
670
+ /**
671
+ * Delete what a transform cache directory holds for nothing: entries not
672
+ * read for 30 days, and temporary files a writer abandoned more than an hour
673
+ * ago.
674
+ *
675
+ * Only regular files directly inside the directory are considered, and no
676
+ * link is followed: a link, a subdirectory, or a `.js` file that does not
677
+ * start like an entry is left alone, whatever its age. A file that cannot be
678
+ * judged or deleted, such as one another process deleted first, is logged at
679
+ * debug level and skipped.
680
+ *
681
+ * @param directory - The transform cache directory
682
+ * @param now - The current time in milliseconds (default: `Date.now()`)
683
+ * @example
684
+ * ```ts
685
+ * pruneTransformCache("/app/node_modules/.cache/courier/transform");
686
+ * ```
687
+ */
688
+ function pruneTransformCache(directory, now = Date.now()) {
689
+ let entries;
690
+ try {
691
+ entries = readdirSync(directory, { withFileTypes: true });
692
+ } catch (error) {
693
+ log$4.debug(`Transform cache: cannot list ${directory} to prune it`, error);
694
+ return;
695
+ }
696
+ for (const entry of entries) {
697
+ const expiry = entry.isFile() ? expiryOf(entry.name) : null;
698
+ if (expiry === null) continue;
699
+ const path = join(directory, entry.name);
700
+ try {
701
+ if (isExpired(path, expiry, now)) unlinkSync(path);
702
+ } catch (error) {
703
+ log$4.debug(`Transform cache: cannot prune ${path}`, error);
704
+ }
705
+ }
706
+ }
707
+ // src/config/decode-transform-entry.ts
708
+ const HEADER = new RegExp(`^${TRANSFORM_ENTRY_FORMAT.prefix}${TRANSFORM_ENTRY_FORMAT.version} ([0-9a-f]{64}) (\\d{1,15}) ([0-9a-f]{64})$`);
709
+ const NEWLINE = 10;
710
+ const MAX_HEADER_BYTES = 256;
711
+ function sha256Hex(bytes) {
712
+ return createHash("sha256").update(bytes).digest("hex");
713
+ }
714
+ /**
715
+ * Read an entry as the answer for the input whose digest is `digest`.
716
+ *
717
+ * @param bytes - The whole entry, as read from disk
718
+ * @param digest - The digest of the input being transformed
719
+ * @returns Its code when the header names that input and the body matches
720
+ * the header's length and sha256; otherwise why it is not a hit
721
+ * @example
722
+ * ```ts
723
+ * const entry = decodeTransformEntry(readFileSync(path), digest);
724
+ * if (entry.state === "hit") return entry.code;
725
+ * ```
726
+ */
727
+ function decodeTransformEntry(bytes, digest) {
728
+ const end = bytes.subarray(0, MAX_HEADER_BYTES).indexOf(NEWLINE);
729
+ if (end === -1) return { state: "stale" };
730
+ const header = HEADER.exec(bytes.toString("latin1", 0, end));
731
+ if (header === null || header[1] !== digest) return { state: "stale" };
732
+ const body = bytes.subarray(end + 1);
733
+ if (body.length !== Number(header[2]) || sha256Hex(body) !== header[3]) return { state: "damaged" };
734
+ return {
735
+ state: "hit",
736
+ code: body.toString("utf8")
737
+ };
738
+ }
739
+ // src/config/why-untrusted.ts
740
+ const SHARED_WRITE = 18;
741
+ /**
742
+ * Why a path cannot be trusted to hold only what this user put there, or
743
+ * `null` when it can.
744
+ *
745
+ * A symbolic link can lead anywhere, and something of another kind is not
746
+ * what the caller expects. On POSIX, a path owned by another user, or one the
747
+ * group or others may write, lets someone else place code that this process
748
+ * would then run. Windows has no owner or mode bits of that kind, so only
749
+ * the first two rules apply there.
750
+ *
751
+ * @param stats - The `lstat` of the path, or the `fstat` of a descriptor opened on it
752
+ * @param kind - What the path should be
753
+ * @param options - Whether write permission for the group and others is accepted (default: no)
754
+ * @returns The reason, worded to follow "is", or `null`
755
+ * @example
756
+ * ```ts
757
+ * const reason = whyUntrusted(lstatSync(nodeModules), "directory");
758
+ * if (reason !== null) log.debug(`${nodeModules} is ${reason}`);
759
+ * ```
760
+ */
761
+ function whyUntrusted(stats, kind, { allowSharedWrite = false } = {}) {
762
+ if (stats.isSymbolicLink()) return "a symbolic link";
763
+ if (!(kind === "file" ? stats.isFile() : stats.isDirectory())) return `not a ${kind}`;
764
+ const uid = process.getuid?.();
765
+ if (uid === void 0) return null;
766
+ if (stats.uid !== uid) return `owned by uid ${stats.uid}, not ${uid}`;
767
+ if (!allowSharedWrite && (stats.mode & SHARED_WRITE) !== 0) return "writable by its group or others";
768
+ return null;
769
+ }
770
+ // src/config/read-transform-entry.ts
771
+ const log$3 = createLogger("Config");
772
+ const READ_FLAGS = constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0);
773
+ const ABSENT_CODES = /* @__PURE__ */ new Set(["ENOENT", "ENOTDIR"]);
774
+ const LINK_CODES = /* @__PURE__ */ new Set(["ELOOP", "EMLINK"]);
775
+ function openEntry(path) {
776
+ try {
777
+ return openSync(path, READ_FLAGS);
778
+ } catch (error) {
779
+ const code = errnoCode(error);
780
+ if (code !== void 0 && ABSENT_CODES.has(code)) return null;
781
+ if (code === void 0 || !LINK_CODES.has(code)) throw error;
782
+ log$3.debug(`Transform cache: ignoring ${path}, a symbolic link`);
783
+ return null;
784
+ }
785
+ }
786
+ function markUsed(fd, path) {
787
+ try {
788
+ const now = /* @__PURE__ */ new Date();
789
+ if (process.platform === "win32") utimesSync(path, now, now);
790
+ else futimesSync(fd, now, now);
791
+ } catch (error) {
792
+ log$3.debug(`Transform cache: cannot mark ${path} as used`, error);
793
+ }
794
+ }
795
+ function readOpenEntry(fd, path, digest, mark) {
796
+ const reason = whyUntrusted(fstatSync(fd), "file");
797
+ if (reason !== null) {
798
+ log$3.debug(`Transform cache: ignoring ${path}, ${reason}`);
799
+ return null;
800
+ }
801
+ const entry = decodeTransformEntry(readFileSync(fd), digest);
802
+ if (entry.state === "damaged") log$3.debug(`Transform cache: ignoring ${path}, whose body does not match its header`);
803
+ if (entry.state !== "hit") return null;
804
+ if (mark) markUsed(fd, path);
805
+ return entry.code;
806
+ }
807
+ /**
808
+ * Read the transform of one input from its entry.
809
+ *
810
+ * The entry is read through one descriptor, opened without blocking and, on
811
+ * POSIX, without following a link. It is used only when it is a regular
812
+ * file — on POSIX also owned by this user and writable by no one else — and
813
+ * when it holds exactly that input's transform. A link, another kind of
814
+ * file, an untrusted owner or mode, and a damaged body are logged at debug
815
+ * level; a stale or missing entry is not. On Windows a link is followed, and
816
+ * what it leads to is judged like the entry itself.
817
+ *
818
+ * @param path - The entry's path
819
+ * @param digest - The digest of the input being transformed
820
+ * @param mark - Whether a hit updates the entry's modification time, which
821
+ * keeps an entry in use from being pruned
822
+ * @returns The kept code, or `null` on a miss
823
+ * @throws The file system's error for anything else, such as `EACCES`
824
+ * @example
825
+ * ```ts
826
+ * const code = readTransformEntry(join(directory, `${name}.js`), digest, true);
827
+ * ```
828
+ */
829
+ function readTransformEntry(path, digest, mark) {
830
+ const fd = openEntry(path);
831
+ if (fd === null) return null;
832
+ try {
833
+ return readOpenEntry(fd, path, digest, mark);
834
+ } finally {
835
+ closeSync(fd);
836
+ }
837
+ }
838
+ // src/config/write-transform-entry.ts
839
+ const log$2 = createLogger("Config");
840
+ const PASSING_RENAME_CODES = /* @__PURE__ */ new Set([
841
+ "EPERM",
842
+ "EBUSY",
843
+ "EACCES"
844
+ ]);
845
+ function heldOpen(error, path) {
846
+ const code = errnoCode(error);
847
+ if (code === void 0 || !PASSING_RENAME_CODES.has(code)) return false;
848
+ return lstatSync(path, { throwIfNoEntry: false })?.isDirectory() !== true;
849
+ }
850
+ function discard(temporary) {
851
+ try {
852
+ unlinkSync(temporary);
853
+ } catch (error) {
854
+ log$2.debug(`Transform cache: cannot remove ${temporary}`, error);
855
+ }
856
+ }
857
+ function writeTemporary(temporary, bytes) {
858
+ const fd = openSync(temporary, "wx", 384);
859
+ try {
860
+ try {
861
+ writeFileSync(fd, bytes);
862
+ } finally {
863
+ closeSync(fd);
864
+ }
865
+ } catch (error) {
866
+ discard(temporary);
867
+ throw error;
868
+ }
869
+ }
870
+ /**
871
+ * Replace the entry at `path` with `bytes`, so that a reader sees either the
872
+ * old entry or the whole new one.
873
+ *
874
+ * The bytes go to a temporary file beside the entry, named after it, this
875
+ * process and random bytes, which is then renamed over the entry. On any
876
+ * failure the temporary file is removed. A rename refused because the entry
877
+ * is held open (`EPERM`, `EBUSY` or `EACCES`, while no folder stands in its
878
+ * place) skips this one write, logged at debug level.
879
+ *
880
+ * @param path - The entry's path, in a directory that exists
881
+ * @param bytes - The whole entry
882
+ * @throws The file system's error for any other failure
883
+ * @example
884
+ * ```ts
885
+ * writeTransformEntry(join(directory, `${name}.js`), encodeTransformEntry(digest, code));
886
+ * ```
887
+ */
888
+ function writeTransformEntry(path, bytes) {
889
+ const temporary = `${path}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
890
+ writeTemporary(temporary, bytes);
891
+ try {
892
+ renameSync(temporary, path);
893
+ } catch (error) {
894
+ discard(temporary);
895
+ if (!heldOpen(error, path)) throw error;
896
+ log$2.debug(`Transform cache: skipped writing ${path} (${errnoCode(error)})`);
897
+ }
898
+ }
899
+ // src/config/transform-cache-disk.ts
900
+ const log$1 = createLogger("Config");
901
+ const prunedDirectories = /* @__PURE__ */ new Set();
902
+ const markedEntries = /* @__PURE__ */ new Set();
903
+ /**
904
+ * Keep transforms in `directory`, where the next process can read them.
905
+ *
906
+ * Each entry is named after the sha256 of its key and replaced atomically,
907
+ * so a reader sees a whole entry or none. It is read only when it is a
908
+ * regular file — on POSIX also not a symbolic link, owned by this user and
909
+ * writable by no one else — and when its header names the same input and its
910
+ * body the header's length and sha256. Anything else is a miss, and the
911
+ * write that follows replaces it. The first hit on an entry in a process
912
+ * marks it as used, so pruning spares it.
913
+ *
914
+ * The first write of each cache creates the directory with mode 0700; the
915
+ * first write in the process prunes it. A failure is never fatal: a missing
916
+ * entry is a quiet miss; any other read error is logged once and is a miss.
917
+ * A directory removed while the process runs, as by clearing
918
+ * `node_modules/.cache`, is created again at the next write. A rename refused
919
+ * because the entry is held open skips that one write; any other write error
920
+ * is logged once, naming the directory and the code, and stops this cache's
921
+ * writes while reads go on.
922
+ *
923
+ * A `readOnly` one reads entries by the same rules and changes nothing: its
924
+ * writes are dropped, and a hit leaves the entry's time as it was, so the
925
+ * process that keeps the entries alone decides what pruning spares.
926
+ *
927
+ * @param directory - Where entries are kept; see `findTransformCacheDirectory`
928
+ * @param options - Whether the directory is only read (default: no)
929
+ * @returns The directory's reader and writer
930
+ * @example
931
+ * ```ts
932
+ * const disk = createTransformCacheDisk("/app/node_modules/.cache/courier/transform");
933
+ * const code = disk.read(key, digest) ?? transformAndKeep();
934
+ * ```
935
+ */
936
+ function createTransformCacheDisk(directory, { readOnly = false } = {}) {
937
+ const root = resolve(directory);
938
+ let readFailureLogged = false;
939
+ let created = false;
940
+ let writable = true;
941
+ function entryPath(key) {
942
+ return join(root, `${createHash("sha256").update(key).digest("hex").slice(0, 40)}.js`);
943
+ }
944
+ function warn(failure, outcome, error) {
945
+ const code = errnoCode(error);
946
+ const message = `Transform cache: cannot ${failure} ${root} (${code ?? "unexpected error"}); ${outcome}`;
947
+ log$1.warn(message, code === void 0 ? error : void 0);
948
+ }
949
+ function stopWriting(error) {
950
+ writable = false;
951
+ warn("write to", "this process keeps new transforms in memory only", error);
952
+ }
953
+ function create() {
954
+ mkdirSync(root, {
955
+ recursive: true,
956
+ mode: 448
957
+ });
958
+ const reason = whyUntrusted(lstatSync(root), "directory");
959
+ if (reason !== null) {
960
+ writable = false;
961
+ log$1.warn(`Transform cache: ${root} is ${reason}; this process keeps new transforms in memory only`);
962
+ return false;
963
+ }
964
+ created = true;
965
+ if (prunedDirectories.has(root)) return true;
966
+ prunedDirectories.add(root);
967
+ pruneTransformCache(root);
968
+ return true;
969
+ }
970
+ function writeEntry(path, bytes) {
971
+ if (!created && !create()) return;
972
+ try {
973
+ writeTransformEntry(path, bytes);
974
+ } catch (error) {
975
+ if (errnoCode(error) !== "ENOENT") throw error;
976
+ if (create()) writeTransformEntry(path, bytes);
977
+ }
978
+ }
979
+ return {
980
+ read(key, digest) {
981
+ const path = entryPath(key);
982
+ const mark = !readOnly && !markedEntries.has(path);
983
+ try {
984
+ const code = readTransformEntry(path, digest, mark);
985
+ if (code !== null && mark) markedEntries.add(path);
986
+ return code;
987
+ } catch (error) {
988
+ if (readFailureLogged) log$1.debug(`Transform cache: cannot read ${path}`, error);
989
+ else warn("read entries in", "such modules are transformed instead", error);
990
+ readFailureLogged = true;
991
+ return null;
992
+ }
993
+ },
994
+ write(key, digest, code) {
995
+ if (readOnly || !writable || !code.isWellFormed()) return;
996
+ try {
997
+ writeEntry(entryPath(key), encodeTransformEntry(digest, code));
998
+ } catch (error) {
999
+ stopWriting(error);
1000
+ }
1001
+ }
1002
+ };
1003
+ }
1004
+ // src/config/transform-digest.ts
1005
+ let jitiVersion;
1006
+ function hasVersion(manifest) {
1007
+ return typeof manifest === "object" && manifest !== null && "version" in manifest && typeof manifest.version === "string";
1008
+ }
1009
+ function readJitiVersion() {
1010
+ if (jitiVersion !== void 0) return jitiVersion;
1011
+ const manifest = createRequire(import.meta.url)("jiti/package.json");
1012
+ if (!hasVersion(manifest)) throw new Error("jiti/package.json names no version");
1013
+ jitiVersion = manifest.version;
1014
+ return jitiVersion;
1015
+ }
1016
+ function isPlainData(value) {
1017
+ const prototype = Object.getPrototypeOf(value);
1018
+ if (Array.isArray(value)) return prototype === Array.prototype;
1019
+ return prototype === Object.prototype || prototype === null;
1020
+ }
1021
+ function describe(value, open) {
1022
+ if (value === void 0) return "u";
1023
+ if (value === null) return "n";
1024
+ if (typeof value === "boolean") return value ? "t" : "f";
1025
+ if (typeof value === "number") return Object.is(value, -0) ? "#-0" : `#${value}`;
1026
+ if (typeof value === "string") return JSON.stringify(value);
1027
+ if (typeof value !== "object" || open.has(value) || !isPlainData(value)) return null;
1028
+ open.add(value);
1029
+ const entries = [];
1030
+ const fields = Object.entries(value);
1031
+ for (const [key, item] of fields) {
1032
+ const described = describe(item, open);
1033
+ if (described === null) return null;
1034
+ entries.push(`${JSON.stringify(key)}:${described}`);
1035
+ }
1036
+ open.delete(value);
1037
+ const [start, end] = Array.isArray(value) ? ["[", "]"] : ["{", "}"];
1038
+ return `${start}${entries.join(",")}${end}`;
1039
+ }
1040
+ /**
1041
+ * The digest of everything that shapes jiti's transform of a module: jiti's
1042
+ * version, every option but the source, and the source.
1043
+ *
1044
+ * Two inputs share a digest only when every option is alike and the source is
1045
+ * the same string, code unit for code unit, so a source that is not
1046
+ * well-formed UTF-16 is not confused with the text it would decode to. An
1047
+ * option whose value cannot be told apart from another — a babel plugin given
1048
+ * as a function, a RegExp, a cycle — yields no digest.
1049
+ *
1050
+ * @param options - What jiti hands its transform
1051
+ * @returns A hex sha256, or `null` when an option holds such a value
1052
+ * @example
1053
+ * ```ts
1054
+ * const digest = transformDigest({ filename: "/app/courier.config.ts", source, ts: true });
1055
+ * ```
1056
+ */
1057
+ function transformDigest({ source, ...rest }) {
1058
+ const described = describe({
1059
+ ...rest,
1060
+ async: rest.async === true
1061
+ }, /* @__PURE__ */ new Set());
1062
+ if (described === null) return null;
1063
+ return createHash("sha256").update(`${readJitiVersion()}\0${described}\0`).update(source, "utf16le").digest("hex");
1064
+ }
1065
+ // src/config/transform-cache.ts
1066
+ const DEFAULT_MAX_MEMORY_CHARS = 128 * 2 ** 20;
1067
+ function keyOf({ filename, async }) {
1068
+ if (filename === void 0 || !isAbsolute(filename)) return null;
1069
+ return `${async === true}\0${filename}`;
1070
+ }
1071
+ /**
1072
+ * Create a {@link TransformCache} that keeps transforms in memory for the
1073
+ * life of the process and, given a `directory`, on disk for the next one.
1074
+ *
1075
+ * Each file holds one entry per module system — required and imported are
1076
+ * transformed differently, and a module reached both ways keeps both. An
1077
+ * entry is reused only when the digest of jiti's version, the options and
1078
+ * the source matches, so an edited file is transformed again the first time
1079
+ * it is loaded.
1080
+ *
1081
+ * A transform is looked for in memory, then in the directory, where a hit
1082
+ * is also kept in memory; only when both miss is it computed, and a success
1083
+ * is then kept in both. The directory is shared with every process that
1084
+ * names it, and a directory that cannot be read or written only costs
1085
+ * transforms; see `createTransformCacheDisk` for what an entry must be to be
1086
+ * used. A mock worker, which loads once the modules Studio has just loaded,
1087
+ * opens Studio's directory `readOnly` with `maxMemoryChars: 0`: it reads
1088
+ * what Studio kept, adds nothing to the directory, and holds nothing in
1089
+ * memory past its one load.
1090
+ *
1091
+ * A failed transform, one whose result carries an `error`, is never kept:
1092
+ * babel can fail on bytes it transforms without complaint a moment later,
1093
+ * such as a deeply nested module that overflows the stack while babel is
1094
+ * still cold. Such a file is transformed again on every load, and every
1095
+ * failure reaches jiti with its error, as it does without a cache. A file
1096
+ * named by a relative path, or not named, is transformed every time.
1097
+ *
1098
+ * @param options - The directory (default: none, memory only), the memory
1099
+ * bound (default: 128 Mi characters), and whether the directory is only
1100
+ * read (default: no)
1101
+ * @returns A cache to pass to `createConfigJiti` or `loadCourierConfig`
1102
+ * @example
1103
+ * ```ts
1104
+ * const transformCache = createTransformCache({
1105
+ * directory: findTransformCacheDirectory(projectRoot),
1106
+ * });
1107
+ * const first = await loadCourierConfig({ cwd: projectRoot, transformCache });
1108
+ * // Unchanged modules are not transformed again, here or in the next process:
1109
+ * const second = await loadCourierConfig({ cwd: projectRoot, transformCache });
1110
+ *
1111
+ * // A process that loads once what another has just kept:
1112
+ * const reader = createTransformCache({ directory, readOnly: true, maxMemoryChars: 0 });
1113
+ * ```
1114
+ */
1115
+ function createTransformCache({ directory, maxMemoryChars = DEFAULT_MAX_MEMORY_CHARS, readOnly = false } = {}) {
1116
+ const disk = directory ? createTransformCacheDisk(directory, { readOnly }) : null;
1117
+ const memo = /* @__PURE__ */ new Map();
1118
+ let memoryChars = 0;
1119
+ function forget(key) {
1120
+ const entry = memo.get(key);
1121
+ if (!entry) return;
1122
+ memo.delete(key);
1123
+ memoryChars -= entry.code.length;
1124
+ }
1125
+ function remember(key, entry) {
1126
+ forget(key);
1127
+ if (maxMemoryChars <= 0 || entry.code.length > maxMemoryChars) return;
1128
+ memo.set(key, entry);
1129
+ memoryChars += entry.code.length;
1130
+ for (const oldest of memo.keys()) {
1131
+ if (memoryChars <= maxMemoryChars) break;
1132
+ forget(oldest);
1133
+ }
1134
+ }
1135
+ return { transform(options, compute) {
1136
+ const key = keyOf(options);
1137
+ const digest = key === null ? null : transformDigest(options);
1138
+ if (key === null || digest === null) return compute(options);
1139
+ const hit = memo.get(key);
1140
+ if (hit?.digest === digest) {
1141
+ memo.delete(key);
1142
+ memo.set(key, hit);
1143
+ return { code: hit.code };
1144
+ }
1145
+ const kept = disk === null ? null : disk.read(key, digest);
1146
+ if (kept !== null) {
1147
+ remember(key, {
1148
+ digest,
1149
+ code: kept
1150
+ });
1151
+ return { code: kept };
1152
+ }
1153
+ const result = compute(options);
1154
+ if (result.error !== void 0) return result;
1155
+ remember(key, {
1156
+ digest,
1157
+ code: result.code
1158
+ });
1159
+ disk?.write(key, digest, result.code);
1160
+ return result;
1161
+ } };
1162
+ }
1163
+ // src/config/transform-cache-directory.ts
1164
+ const log = createLogger("Config");
1165
+ /** Values of `COURIER_TRANSFORM_CACHE` that turn the directory off. */
1166
+ const OFF_VALUES = /* @__PURE__ */ new Set([
1167
+ "0",
1168
+ "false",
1169
+ "off"
1170
+ ]);
1171
+ /** Values of `COURIER_TRANSFORM_CACHE` that keep it, as leaving it unset does. */
1172
+ const ON_VALUES = /* @__PURE__ */ new Set([
1173
+ "",
1174
+ "1",
1175
+ "true",
1176
+ "on"
1177
+ ]);
1178
+ const unknownSettings = /* @__PURE__ */ new Set();
1179
+ function nearestNodeModules(start) {
1180
+ let directory = start;
1181
+ while (true) {
1182
+ const candidate = join(directory, "node_modules");
1183
+ const stats = lstatSync(candidate, { throwIfNoEntry: false });
1184
+ if (stats?.isDirectory() || stats?.isSymbolicLink()) return candidate;
1185
+ const parent = dirname(directory);
1186
+ if (parent === directory) return null;
1187
+ directory = parent;
1188
+ }
1189
+ }
1190
+ function whyRefused(levels) {
1191
+ for (const [path, options] of levels) {
1192
+ const stats = lstatSync(path, { throwIfNoEntry: false });
1193
+ const reason = stats === void 0 ? null : whyUntrusted(stats, "directory", options);
1194
+ if (reason !== null) return `${path} is ${reason}`;
1195
+ }
1196
+ return null;
1197
+ }
1198
+ const TOOL_OWNED = { allowSharedWrite: true };
1199
+ const STUDIO_OWNED = {};
1200
+ const lastRefusals = /* @__PURE__ */ new Map();
1201
+ function refuse(root, reason, error) {
1202
+ const key = error instanceof Error ? `${reason}\0${error.message}` : reason;
1203
+ if (lastRefusals.get(root) === key) return null;
1204
+ lastRefusals.set(root, key);
1205
+ if (error === void 0) log.debug(`Transform cache directory off: ${reason}`);
1206
+ else log.debug(`Transform cache directory off: ${reason}`, error);
1207
+ return null;
1208
+ }
1209
+ function locate(projectRoot) {
1210
+ const nodeModules = nearestNodeModules(projectRoot);
1211
+ if (nodeModules === null) return { refusal: `no node_modules at or above ${projectRoot}` };
1212
+ const cache = join(nodeModules, ".cache");
1213
+ const courier = join(cache, "courier");
1214
+ const directory = join(courier, "transform");
1215
+ const refusal = whyRefused([
1216
+ [nodeModules, TOOL_OWNED],
1217
+ [cache, TOOL_OWNED],
1218
+ [courier, STUDIO_OWNED],
1219
+ [directory, STUDIO_OWNED]
1220
+ ]);
1221
+ return refusal === null ? { directory } : { refusal };
1222
+ }
1223
+ /**
1224
+ * Where a project's transforms are kept across processes:
1225
+ * `.cache/courier/transform` in the nearest `node_modules` directory at or
1226
+ * above the project root.
1227
+ *
1228
+ * The nearest one is taken, not the one module resolution would reach: under
1229
+ * pnpm that lies inside `.pnpm`, and what keeps the directory safe is the
1230
+ * rule below, not where it is. Every file in it holds code that Studio, mock
1231
+ * workers and build plugins run, so on POSIX the directory is refused when
1232
+ * `node_modules`, `.cache`, `courier` or `transform` — those that exist — is
1233
+ * a symbolic link or belongs to another user, and when `courier` or
1234
+ * `transform`, which only Studio creates, is writable by its group or
1235
+ * others; any of these lets someone else replace or remove entries, or put
1236
+ * the directory somewhere they control. `node_modules` and `.cache` may be
1237
+ * group-writable, as package managers leave them under a umask of 002:
1238
+ * whoever can write `node_modules` can replace any package in it already. On
1239
+ * Windows only links and other kinds of file are refused.
1240
+ *
1241
+ * There is none when no `node_modules` exists, as with Yarn Plug'n'Play, or
1242
+ * when `COURIER_TRANSFORM_CACHE` is `0`, `false` or `off` in any case. The
1243
+ * system temporary directory, which every user of the machine may write, is
1244
+ * never used instead. Why there is none is logged at debug level, once for
1245
+ * each root until the reason changes, however often the root is looked up.
1246
+ *
1247
+ * @param projectRoot - The directory holding `courier.config.*`
1248
+ * @param env - The environment to read `COURIER_TRANSFORM_CACHE` from
1249
+ * (default: `process.env`)
1250
+ * @returns The directory, which may not exist yet, or `null` for none
1251
+ * @example
1252
+ * ```ts
1253
+ * const transformCache = createTransformCache({
1254
+ * directory: findTransformCacheDirectory(projectRoot),
1255
+ * });
1256
+ * ```
1257
+ */
1258
+ function findTransformCacheDirectory(projectRoot, env = process.env) {
1259
+ const root = resolve(projectRoot);
1260
+ const setting = env.COURIER_TRANSFORM_CACHE?.trim().toLowerCase();
1261
+ if (setting !== void 0 && OFF_VALUES.has(setting)) return refuse(root, `COURIER_TRANSFORM_CACHE=${setting}`);
1262
+ if (setting !== void 0 && !ON_VALUES.has(setting) && !unknownSettings.has(setting)) {
1263
+ unknownSettings.add(setting);
1264
+ log.warn(`COURIER_TRANSFORM_CACHE=${setting} is not one of 0, false, off, 1, true or on; the transform cache stays on`);
1265
+ }
1266
+ let located;
1267
+ try {
1268
+ located = locate(root);
1269
+ } catch (error) {
1270
+ return refuse(root, `cannot inspect the directories above ${root}`, error);
1271
+ }
1272
+ if (located.refusal !== void 0) return refuse(root, located.refusal);
1273
+ lastRefusals.delete(root);
1274
+ return located.directory;
1275
+ }
1276
+ // src/config/load-config.ts
1277
+ /**
1278
+ * Unified `courier.config.{ts,mts,cts,js,mjs,cjs}` loader — the single
1279
+ * canonical way to resolve and load a user's config file.
1280
+ *
1281
+ * Two-stage design:
1282
+ *
1283
+ * 1. **File discovery** — walk up from `cwd` to the top of the project
1284
+ * looking for a `courier.config.*` file, using nothing but Node builtins.
1285
+ *
1286
+ * 2. **File loading** — `jiti` imports the file, transpiling a TypeScript
1287
+ * config transparently across Node / Next.js dev / tsx / bun / deno and
1288
+ * resolving the project's `tsconfig` path aliases.
1289
+ *
1290
+ * Every consumer resolves the config through this one code path:
1291
+ *
1292
+ * - The Studio dev server (`@noverax/courier-studio`) wraps this loader and
1293
+ * adds dev-only concerns on top (validation, source-key stamping,
1294
+ * dependency extraction for the fs watcher, rich error reporting).
1295
+ *
1296
+ * - The user-app runtime (`attachRuntime` on the server side) calls it
1297
+ * directly to discover and register sources without requiring the user
1298
+ * to import the config file manually.
1299
+ *
1300
+ * - `courier-cli`, the standalone server and the MCP server load the
1301
+ * project's config with it, and the bundler plugins read a page's routes
1302
+ * from the config it loads.
1303
+ *
1304
+ * A missing config is not an error: `null` lets the runtime caller fall back
1305
+ * to adapter auto-discovery. A config that exists and cannot be loaded is a
1306
+ * different thing entirely and throws, because reporting it as "no config"
1307
+ * sends the reader looking for a file that is right where they left it.
1308
+ */
1309
+ /**
1310
+ * Walk up from `startDir` looking for a `courier.config.*` file, up to the
1311
+ * top of the project: the folder that holds its repository or is the root
1312
+ * of its workspace (see {@link configCandidates}). A config found in a folder
1313
+ * above `startDir` is taken only when it belongs to this user or to the
1314
+ * owner of `startDir`, and no user outside its group may write it: a folder
1315
+ * many users share, such as `/tmp`, would otherwise run anyone's config.
1316
+ *
1317
+ * @param startDir - The directory to look in first
1318
+ * @returns The absolute path of the first match, or `null` when no directory
1319
+ * up to the top of the project holds one
1320
+ * @throws UntrustedConfigError when the nearest config lies above `startDir`
1321
+ * and someone else could have put it there
1322
+ * @example
1323
+ * ```ts
1324
+ * findConfigFile("/app/src"); // → "/app/courier.config.ts", or null
1325
+ * ```
1326
+ */
1327
+ function findConfigFile(startDir) {
1328
+ const folder = resolve(startDir);
1329
+ const found = configCandidates(folder).find((candidate) => existsSync(candidate));
1330
+ if (found === void 0) return null;
1331
+ if (dirname(found) !== folder) assertOwnConfig(found, folder);
1332
+ return found;
1333
+ }
1334
+ /**
1335
+ * Find a config file in `cwd` itself, without walking up.
1336
+ *
1337
+ * @param cwd - The directory to look in
1338
+ * @returns The first of {@link CONFIG_BASENAMES} present there, or `null`
1339
+ * @example
1340
+ * ```ts
1341
+ * findConfigFileAt(process.cwd()); // → "/app/courier.config.ts", or null
1342
+ * ```
1343
+ */
1344
+ function findConfigFileAt(cwd) {
1345
+ for (const basename of CONFIG_BASENAMES) {
1346
+ const candidate = resolve(cwd, basename);
1347
+ if (existsSync(candidate)) return candidate;
1348
+ }
1349
+ return null;
1350
+ }
1351
+ /**
1352
+ * Find and load a `courier.config.*` file.
1353
+ *
1354
+ * Returns `null` when no config file exists, so a caller can fall back.
1355
+ * Throws when a file was found and loading it failed, reporting what the
1356
+ * loader reported — for a TypeScript config, usually an unresolvable import.
1357
+ *
1358
+ * @param options - Where to look, and where to reuse transforms
1359
+ * @returns The config and its canonical path, or `null` when there is no config
1360
+ * @example
1361
+ * ```ts
1362
+ * const loaded = await loadCourierConfig({ cwd: projectRoot, requireAtCwd: true });
1363
+ * ```
1364
+ */
1365
+ async function loadCourierConfig(options = {}) {
1366
+ const startDir = resolve(options.cwd ?? process.cwd());
1367
+ const found = options.requireAtCwd ? findConfigFileAt(startDir) : findConfigFile(startDir);
1368
+ if (!found) return null;
1369
+ return loadCourierConfigFile(found, {
1370
+ transformCache: options.transformCache ?? (options.projectTransforms === true ? projectTransformCache(dirname(found)) : void 0),
1371
+ onModule: options.onModule
1372
+ });
1373
+ }
1374
+ /**
1375
+ * The transforms kept in a project's `node_modules/.cache` — the ones Studio
1376
+ * keeps — for a process that loads the project's modules once: it reuses
1377
+ * them, keeps there the ones it makes, and holds none in memory.
1378
+ *
1379
+ * @param projectRoot - The project's root
1380
+ * @returns The cache, or `undefined` when the project has no directory to keep them in
1381
+ * @example
1382
+ * ```ts
1383
+ * await loadCourierConfigFile(file, { transformCache: projectTransformCache(dirname(file)) });
1384
+ * ```
1385
+ */
1386
+ function projectTransformCache(projectRoot) {
1387
+ const directory = findTransformCacheDirectory(projectRoot);
1388
+ return directory === null ? void 0 : createTransformCache({
1389
+ directory,
1390
+ maxMemoryChars: 0
1391
+ });
1392
+ }
1393
+ /**
1394
+ * Load one config file, found by whatever means the caller chose — the
1395
+ * standalone server takes it from `--config`.
1396
+ *
1397
+ * @param file - Absolute path of an existing config file
1398
+ * @param options - How the loader resolves imports and where it reuses
1399
+ * transforms, see {@link ConfigJitiOptions}, and what runs before the config
1400
+ * @returns The config and the canonical path it was loaded from
1401
+ * @throws What the loader reported when the file will not load — for a
1402
+ * TypeScript config, usually an unresolvable import
1403
+ * @example
1404
+ * ```ts
1405
+ * const { config } = await loadCourierConfigFile("/app/courier.config.ts", {
1406
+ * transformCache: createTransformCache(),
1407
+ * });
1408
+ * ```
1409
+ */
1410
+ async function loadCourierConfigFile(file, { beforeRun, ...options } = {}) {
1411
+ const configFile = realpathSync(file);
1412
+ const jiti = await createConfigJiti(configFile, options);
1413
+ beforeRun?.();
1414
+ return {
1415
+ config: await jiti.import(configFile, { default: true }),
1416
+ configFile
1417
+ };
1418
+ }
1419
+ export { projectTransformCache as a, UntrustedConfigError as c, CONFIG_BASENAMES as d, configCandidates as f, loadCourierConfigFile as i, createConfigJiti as l, findConfigFileAt as n, findTransformCacheDirectory as o, loadCourierConfig as r, createTransformCache as s, findConfigFile as t, readPathAliases as u };