@noverax/courier-build 0.4.0 → 0.5.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.
@@ -0,0 +1,2834 @@
1
+ import { n as isRecord, t as errnoCode } from "./errno-code-CNx_E80R.js";
2
+ import { createRequire } from "node:module";
3
+ import { basename, dirname, isAbsolute, join, normalize, posix, relative, resolve, sep, win32 } 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
+ import { REDACTED, cutSecrets, isSecretValue, sliceWhole } from "@noverax/courier/internal";
10
+ import { inspect } from "node:util";
11
+ // src/config/config-candidates.ts
12
+ /** The file names a Courier config may have, in the order they are looked for */
13
+ const CONFIG_BASENAMES = [
14
+ "courier.config.ts",
15
+ "courier.config.mts",
16
+ "courier.config.cts",
17
+ "courier.config.js",
18
+ "courier.config.mjs",
19
+ "courier.config.cjs"
20
+ ];
21
+ const PROJECT_TOP_MARKERS = [
22
+ ".git",
23
+ ".hg",
24
+ ".jj",
25
+ "pnpm-workspace.yaml"
26
+ ];
27
+ /**
28
+ * Every path a config is looked for at from a folder, in the order it is
29
+ * looked for: each of {@link CONFIG_BASENAMES} in the folder, then in each
30
+ * folder above it, up to the top of the project — the first folder that
31
+ * holds its repository (`.git`, `.hg`, `.jj`) or is the root of its
32
+ * workspace (`pnpm-workspace.yaml`, or a `package.json` that declares
33
+ * `workspaces`) — or, with no such folder, the root of the file system.
34
+ *
35
+ * @param startDir - The folder to look in first
36
+ * @returns The paths, absolute, whether there or not
37
+ * @example
38
+ * ```ts
39
+ * configCandidates("/repo/apps/web"); // → ["/repo/apps/web/courier.config.ts", …, "/repo/courier.config.cjs"]
40
+ * ```
41
+ */
42
+ function configCandidates(startDir) {
43
+ const candidates = [];
44
+ let dir = resolve(startDir);
45
+ while (true) {
46
+ for (const basename of CONFIG_BASENAMES) candidates.push(join(dir, basename));
47
+ const parent = dirname(dir);
48
+ if (parent === dir || isProjectTop(dir)) return candidates;
49
+ dir = parent;
50
+ }
51
+ }
52
+ function isProjectTop(dir) {
53
+ return PROJECT_TOP_MARKERS.some((marker) => existsSync(join(dir, marker))) || declaresWorkspaces(join(dir, "package.json"));
54
+ }
55
+ function declaresWorkspaces(manifest) {
56
+ if (!existsSync(manifest)) return false;
57
+ try {
58
+ const parsed = JSON.parse(readFileSync(manifest, "utf8"));
59
+ return isRecord(parsed) && parsed.workspaces !== void 0;
60
+ } catch {
61
+ return false;
62
+ }
63
+ }
64
+ // src/config/frameworks.ts
65
+ /**
66
+ * Which framework a project is built with, told by the packages its
67
+ * package.json lists — the one rule Courier tells a framework by, for
68
+ * reading a project's modules as its server does and for setting Courier
69
+ * up in it.
70
+ */
71
+ /** The frameworks, the ones built on others first: a Next.js app lists React too; a server before a page Vite builds. */
72
+ const FRAMEWORK_MARKERS = [
73
+ {
74
+ id: "next",
75
+ label: "Next.js",
76
+ marker: "next"
77
+ },
78
+ {
79
+ id: "nuxt",
80
+ label: "Nuxt",
81
+ marker: "nuxt"
82
+ },
83
+ {
84
+ id: "sveltekit",
85
+ label: "SvelteKit",
86
+ marker: "@sveltejs/kit"
87
+ },
88
+ {
89
+ id: "astro",
90
+ label: "Astro",
91
+ marker: "astro"
92
+ },
93
+ {
94
+ id: "react-router",
95
+ label: "React Router",
96
+ marker: "@react-router/dev"
97
+ },
98
+ {
99
+ id: "remix",
100
+ label: "Remix",
101
+ marker: "@remix-run/dev"
102
+ },
103
+ {
104
+ id: "solid-start",
105
+ label: "SolidStart",
106
+ marker: "@solidjs/start"
107
+ },
108
+ {
109
+ id: "expo",
110
+ label: "Expo",
111
+ marker: "expo"
112
+ },
113
+ {
114
+ id: "nestjs",
115
+ label: "NestJS",
116
+ marker: "@nestjs/core",
117
+ server: true
118
+ },
119
+ {
120
+ id: "fastify",
121
+ label: "Fastify",
122
+ marker: "fastify",
123
+ server: true
124
+ },
125
+ {
126
+ id: "hono",
127
+ label: "Hono",
128
+ marker: "hono",
129
+ runsWith: true,
130
+ server: true
131
+ },
132
+ {
133
+ id: "express",
134
+ label: "Express",
135
+ marker: "express",
136
+ server: true
137
+ },
138
+ {
139
+ id: "koa",
140
+ label: "Koa",
141
+ marker: "koa",
142
+ server: true
143
+ },
144
+ {
145
+ id: "hapi",
146
+ label: "hapi",
147
+ marker: "@hapi/hapi",
148
+ server: true
149
+ },
150
+ {
151
+ id: "elysia",
152
+ label: "Elysia",
153
+ marker: "elysia",
154
+ server: true
155
+ },
156
+ {
157
+ id: "h3",
158
+ label: "h3",
159
+ marker: "h3",
160
+ server: true
161
+ },
162
+ {
163
+ id: "nitro",
164
+ label: "Nitro",
165
+ marker: "nitro",
166
+ server: true
167
+ },
168
+ {
169
+ id: "react",
170
+ label: "React with Vite",
171
+ marker: "vite",
172
+ also: "react"
173
+ },
174
+ {
175
+ id: "vue",
176
+ label: "Vue with Vite",
177
+ marker: "vite",
178
+ also: "vue"
179
+ }
180
+ ];
181
+ /**
182
+ * The framework a project is built with, told by the packages it lists: a
183
+ * framework told first by what the app runs with counts among the dev
184
+ * dependencies only where no other framework is listed.
185
+ *
186
+ * @param listed - The packages the project's package.json lists
187
+ * @returns The framework, or `undefined` when it lists none Courier tells
188
+ * @example
189
+ * ```ts
190
+ * detectFramework({ dependencies: { next: "^16.0.0", react: "^19.0.0" }, … })?.label; // "Next.js"
191
+ * ```
192
+ */
193
+ function detectFramework(listed) {
194
+ const lists = (name) => [
195
+ listed.dependencies,
196
+ listed.devDependencies,
197
+ listed.peerDependencies,
198
+ listed.optionalDependencies
199
+ ].some((field) => Object.hasOwn(field, name));
200
+ const told = (framework, marked) => marked && (framework.also === void 0 || lists(framework.also));
201
+ return FRAMEWORK_MARKERS.find((framework) => told(framework, framework.runsWith === true ? Object.hasOwn(listed.dependencies, framework.marker) : lists(framework.marker))) ?? FRAMEWORK_MARKERS.find((framework) => told(framework, framework.runsWith === true && lists(framework.marker)));
202
+ }
203
+ /**
204
+ * The framework of the project a folder belongs to, told by the nearest
205
+ * package.json at or above it. A package.json that does not parse lists
206
+ * nothing: the project is then read as plain Node, as it would be with none.
207
+ *
208
+ * @param folder - A folder of the project, absolute: the config's own
209
+ * @returns The framework, and the package.json that told it
210
+ * @example
211
+ * ```ts
212
+ * projectFramework("/app").framework?.id; // "sveltekit", where /app/package.json lists @sveltejs/kit
213
+ * ```
214
+ */
215
+ function projectFramework(folder) {
216
+ const manifest = nearestManifest(folder);
217
+ if (manifest === void 0) return {
218
+ framework: void 0,
219
+ manifest
220
+ };
221
+ return {
222
+ framework: detectFramework(listedIn(manifest)),
223
+ manifest
224
+ };
225
+ }
226
+ function nearestManifest(folder) {
227
+ for (let current = folder;; current = dirname(current)) {
228
+ const candidate = join(current, "package.json");
229
+ if (existsSync(candidate)) return candidate;
230
+ if (dirname(current) === current) return void 0;
231
+ }
232
+ }
233
+ function listedIn(manifest) {
234
+ let parsed;
235
+ try {
236
+ parsed = JSON.parse(readFileSync(manifest, "utf8"));
237
+ } catch {
238
+ parsed = void 0;
239
+ }
240
+ const field = (name) => {
241
+ const value = isRecord(parsed) ? parsed[name] : void 0;
242
+ return isRecord(value) ? value : {};
243
+ };
244
+ return {
245
+ dependencies: field("dependencies"),
246
+ devDependencies: field("devDependencies"),
247
+ peerDependencies: field("peerDependencies"),
248
+ optionalDependencies: field("optionalDependencies")
249
+ };
250
+ }
251
+ // src/config/installed-packages.ts
252
+ /**
253
+ * Which packages a project has installed, for the path aliases to stop at.
254
+ */
255
+ /**
256
+ * The packages installed where `projectRoot` resolves them, each looked up
257
+ * once.
258
+ *
259
+ * @param projectRoot - Directory holding the project's `tsconfig.json`
260
+ * @returns The lookup
261
+ * @example
262
+ * ```ts
263
+ * installedPackages("/app").has("@acme/ui"); // true once it is in node_modules
264
+ * ```
265
+ */
266
+ function installedPackages(projectRoot) {
267
+ const require = createRequire(resolve(projectRoot, "tsconfig.json"));
268
+ const known = /* @__PURE__ */ new Map();
269
+ return { has(name) {
270
+ let installed = known.get(name);
271
+ if (installed === void 0) {
272
+ installed = resolves(require, name);
273
+ known.set(name, installed);
274
+ }
275
+ return installed;
276
+ } };
277
+ }
278
+ /**
279
+ * The package a specifier or an alias prefix names: `@scope/name`, or its
280
+ * first segment; `null` when it names none, as `@scope/` does.
281
+ *
282
+ * @param specifier - A specifier, or an alias's text before its wildcard
283
+ * @returns The package's name, or `null`
284
+ * @example
285
+ * ```ts
286
+ * packageNameOf("@acme/ui/button"); // "@acme/ui"
287
+ * ```
288
+ */
289
+ function packageNameOf(specifier) {
290
+ const segments = specifier.split("/");
291
+ const wanted = specifier.startsWith("@") ? 2 : 1;
292
+ if (segments.length < wanted) return null;
293
+ const name = segments.slice(0, wanted);
294
+ return name.every((segment) => segment.length > 0) ? name.join("/") : null;
295
+ }
296
+ function resolves(require, name) {
297
+ try {
298
+ require.resolve(name);
299
+ return true;
300
+ } catch (error) {
301
+ return errnoCode(error) !== "MODULE_NOT_FOUND";
302
+ }
303
+ }
304
+ // src/config/scope-alias.ts
305
+ /**
306
+ * An alias for a whole scope, `@acme/*`, in the shape the module loader takes.
307
+ */
308
+ const log$7 = createLogger("Config");
309
+ const MODULE_EXTENSION = /\.(d\.ts|[cm]?[jt]sx?|json)$/;
310
+ /**
311
+ * The scope's name, `@acme`, when `prefix` is an alias's text before the
312
+ * wildcard of a pattern for a whole scope, `@acme/*`.
313
+ *
314
+ * @param prefix - An alias's text before its wildcard
315
+ * @returns The scope, or `undefined` for any other prefix
316
+ * @example
317
+ * ```ts
318
+ * scopeOf("@acme/"); // "@acme"
319
+ * ```
320
+ */
321
+ function scopeOf(prefix) {
322
+ return /^(@[^/]+)\/$/.exec(prefix)?.[1];
323
+ }
324
+ /**
325
+ * One loader alias per name a scope alias leads to, leaving out the names of
326
+ * installed packages. The loader follows an alias whatever it finds there,
327
+ * where TypeScript takes an installed package past it: `@acme/*` to
328
+ * `./src/*` keeps `@acme/utils` on `src/utils` while `@acme/ui`, installed,
329
+ * loads the build it ships.
330
+ *
331
+ * @param scope - The scope, `@acme`
332
+ * @param directory - The directory the alias's `*` stands in, absolute
333
+ * @param installed - The packages the project installed
334
+ * @returns Prefix → absolute target, for each name the directory holds
335
+ * @example
336
+ * ```ts
337
+ * scopeAliases("@acme", "/app/src", installed); // { "@acme/utils": "/app/src/utils" }
338
+ * ```
339
+ */
340
+ function scopeAliases(scope, directory, installed) {
341
+ const alias = {};
342
+ for (const entry of entriesOf$1(directory)) {
343
+ const name = entry.replace(MODULE_EXTENSION, "");
344
+ if (!installed.has(`${scope}/${name}`)) alias[`${scope}/${name}`] = join(directory, name);
345
+ }
346
+ return alias;
347
+ }
348
+ function entriesOf$1(directory) {
349
+ try {
350
+ return readdirSync(directory);
351
+ } catch (error) {
352
+ const code = errnoCode(error);
353
+ if (code !== "ENOENT" && code !== "ENOTDIR") log$7.warn(`Could not list ${directory} for a path alias; the alias leads nowhere`, error);
354
+ return [];
355
+ }
356
+ }
357
+ // src/config/read-tsconfig-file.ts
358
+ const log$6 = createLogger("Config");
359
+ function isTextList(value) {
360
+ if (!Array.isArray(value)) return false;
361
+ return value.every((item) => typeof item === "string");
362
+ }
363
+ function readText(path) {
364
+ try {
365
+ if (!statSync(path).isFile()) {
366
+ log$6.warn(`${path} is not a regular file, so its path aliases are ignored`);
367
+ return null;
368
+ }
369
+ return readFileSync(path, "utf-8");
370
+ } catch (err) {
371
+ log$6.warn(`Could not read ${path}, so its path aliases are ignored`, err);
372
+ return null;
373
+ }
374
+ }
375
+ function extendsOf(value, path) {
376
+ if (value === void 0 || value === null) return [];
377
+ if (typeof value === "string") return [value];
378
+ if (!Array.isArray(value)) {
379
+ log$6.warn(`${path}: "extends" is neither a path nor a list of paths, so it is not followed`);
380
+ return [];
381
+ }
382
+ const entries = value;
383
+ const paths = entries.filter((entry) => typeof entry === "string");
384
+ if (paths.length < entries.length) log$6.warn(`${path}: "extends" lists something other than a path, which is skipped`);
385
+ return paths;
386
+ }
387
+ function pathsOf(value, path) {
388
+ if (value === void 0) return void 0;
389
+ if (!isRecord(value)) {
390
+ log$6.warn(`${path}: "paths" is not an object, so its path aliases are ignored`);
391
+ return;
392
+ }
393
+ const kept = [];
394
+ for (const [pattern, targets] of Object.entries(value)) if (isTextList(targets)) kept.push([pattern, targets]);
395
+ else log$6.warn(`${path}: "paths" maps "${pattern}" to something other than a list of paths, so it is ignored`);
396
+ return Object.fromEntries(kept);
397
+ }
398
+ function optionsOf(value, path) {
399
+ if (value === void 0) return {};
400
+ if (!isRecord(value)) {
401
+ log$6.warn(`${path}: "compilerOptions" is not an object, so its path aliases are ignored`);
402
+ return {};
403
+ }
404
+ const { baseUrl } = value;
405
+ const paths = pathsOf(value.paths, path);
406
+ const base = typeof baseUrl === "string" ? baseUrl : void 0;
407
+ if (baseUrl !== void 0 && base === void 0) log$6.warn(`${path}: "baseUrl" is not a path, so its aliases resolve from its own directory`);
408
+ return {
409
+ ...base !== void 0 && { baseUrl: base },
410
+ ...paths && { paths }
411
+ };
412
+ }
413
+ /**
414
+ * Read the fields of a tsconfig that its path mapping depends on. A field
415
+ * of another shape than TypeScript takes is left out with a warning, as
416
+ * TypeScript reports it, rather than failing the load of the config that
417
+ * reads the mapping.
418
+ *
419
+ * @param path - The tsconfig
420
+ * @returns What it declares; `null` when it cannot be read or parsed
421
+ * @example
422
+ * ```ts
423
+ * readTsconfigFile("/app/tsconfig.json");
424
+ * // → { extends: ["./tsconfig.base.json"], paths: { "@/*": ["./src/*"] } }
425
+ * ```
426
+ */
427
+ function readTsconfigFile(path) {
428
+ const text = readText(path);
429
+ if (text === null) return null;
430
+ let parsed;
431
+ try {
432
+ parsed = parseJSON5(text);
433
+ } catch (err) {
434
+ log$6.warn(`Could not parse ${path}, so its path aliases are ignored`, err);
435
+ return null;
436
+ }
437
+ if (!isRecord(parsed)) {
438
+ log$6.warn(`${path} does not hold an object, so its path aliases are ignored`);
439
+ return null;
440
+ }
441
+ return {
442
+ extends: extendsOf(parsed.extends, path),
443
+ ...optionsOf(parsed.compilerOptions, path)
444
+ };
445
+ }
446
+ // src/config/tsconfig-paths.ts
447
+ /**
448
+ * Read a project's `compilerOptions.paths` without loading TypeScript.
449
+ *
450
+ * The compiler would do this for us, but its API is exactly what a config
451
+ * reader must not depend on: it is an optional peer a JavaScript-only project
452
+ * is entitled not to have, and TypeScript 7 publishes no parser under the
453
+ * package root at all. Reading the file directly costs one JSONC parse.
454
+ *
455
+ * `paths` is not merged down an `extends` chain — TypeScript replaces the map
456
+ * wholesale — so the nearest config that declares one wins, and its targets
457
+ * resolve against its own `baseUrl`, or its own directory when it has none.
458
+ *
459
+ * Parsing is JSON5 rather than strict JSONC: a tsconfig is hand-edited and
460
+ * routinely carries comments and trailing commas, both of which TypeScript
461
+ * accepts. Refusing them would drop a real project's aliases without a word.
462
+ */
463
+ const log$5 = createLogger("Config");
464
+ /**
465
+ * Read the path mapping that applies to files in `directory`: the one of the
466
+ * nearest `tsconfig.json` at or above it, as TypeScript finds it, so a config
467
+ * file kept below the project root — `deploy/server.config.ts` — keeps every
468
+ * alias the project declares.
469
+ *
470
+ * @param directory - Directory of the file whose imports are resolved
471
+ * @returns The mapping, or `null` when no tsconfig declares one
472
+ *
473
+ * @example
474
+ * ```ts
475
+ * const mapping = readTsconfigPaths("/app/deploy");
476
+ * // → { baseUrl: "/app", paths: { "@/*": ["./src/*"] } }
477
+ * ```
478
+ */
479
+ function readTsconfigPaths(directory) {
480
+ const tsconfig = nearestTsconfig(resolve(directory));
481
+ return tsconfig ? findPaths(tsconfig, /* @__PURE__ */ new Set()) : null;
482
+ }
483
+ function nearestTsconfig(directory) {
484
+ for (let dir = directory;; dir = dirname(dir)) {
485
+ const candidate = resolve(dir, "tsconfig.json");
486
+ if (existsSync(candidate)) return candidate;
487
+ if (dirname(dir) === dir) return null;
488
+ }
489
+ }
490
+ /** Walk the `extends` chain depth-first, stopping at the first declared map. */
491
+ function findPaths(file, visited) {
492
+ const path = resolve(file);
493
+ if (visited.has(path)) return null;
494
+ visited.add(path);
495
+ const config = readTsconfigFile(path);
496
+ if (!config) return null;
497
+ if (config.paths) return {
498
+ baseUrl: resolve(dirname(path), config.baseUrl ?? "."),
499
+ paths: config.paths
500
+ };
501
+ for (const specifier of [...config.extends].reverse()) {
502
+ const parent = resolveExtends(specifier, dirname(path));
503
+ if (!parent) {
504
+ log$5.warn(`${path} extends "${specifier}", which cannot be found, so its path aliases are not read`);
505
+ continue;
506
+ }
507
+ const found = findPaths(parent, visited);
508
+ if (found) return found;
509
+ }
510
+ return null;
511
+ }
512
+ /**
513
+ * Resolve an `extends` specifier: a path, a directory holding a
514
+ * `tsconfig.json`, or an installed package that ships one.
515
+ */
516
+ function resolveExtends(specifier, fromDir) {
517
+ if (specifier.startsWith(".") || isAbsolute(specifier)) {
518
+ const candidate = resolve(fromDir, specifier);
519
+ if (existsSync(candidate) && statSync(candidate).isDirectory()) {
520
+ const nested = resolve(candidate, "tsconfig.json");
521
+ return existsSync(nested) ? nested : null;
522
+ }
523
+ if (existsSync(candidate)) return candidate;
524
+ const suffixed = `${candidate}.json`;
525
+ return existsSync(suffixed) ? suffixed : null;
526
+ }
527
+ const requireFrom = createRequire(resolve(fromDir, "tsconfig.json"));
528
+ for (const target of [specifier, `${specifier}/tsconfig.json`]) {
529
+ const found = resolveFrom(requireFrom, target);
530
+ if (found) return found;
531
+ }
532
+ return null;
533
+ }
534
+ function resolveFrom(requireFrom, target) {
535
+ try {
536
+ return requireFrom.resolve(target);
537
+ } catch (err) {
538
+ log$5.debug(`"${target}" does not resolve to a tsconfig`, err);
539
+ return null;
540
+ }
541
+ }
542
+ // src/config/path-aliases.ts
543
+ /**
544
+ * Turn a project's tsconfig `paths` into the two shapes that reading a config
545
+ * needs: the alias map the module loader takes, and a lookup the dependency
546
+ * walker uses to follow an aliased import.
547
+ *
548
+ * An alias that shadows an installed package is left out of both. Such an
549
+ * entry redirects a dependency to its sources for the bundler's benefit —
550
+ * `@acme/ui` to `../ui/src`, or a whole scope, `@acme/*` — and following it
551
+ * would load a library's source instead of the build it ships, then treat
552
+ * every file inside it as something the config depends on. A dependency
553
+ * changes when it is reinstalled, not while the developer is editing; the
554
+ * graph stops at the package boundary. A catch-all `*` covers every package,
555
+ * so it is left out too: the loader, which cannot fall back to the package as
556
+ * TypeScript does, would put its target before every path it loads.
557
+ */
558
+ const EMPTY$1 = {
559
+ loader: {},
560
+ candidatesFor: () => []
561
+ };
562
+ /**
563
+ * Read the path aliases that apply to `projectRoot`.
564
+ *
565
+ * @param projectRoot - Directory holding the project's `tsconfig.json`
566
+ * @returns The aliases, empty when the project declares none
567
+ *
568
+ * @example
569
+ * ```ts
570
+ * const aliases = readPathAliases("/app");
571
+ * aliases.candidatesFor("@/lib/sources"); // → ["/app/src/lib/sources"]
572
+ * ```
573
+ */
574
+ function readPathAliases(projectRoot) {
575
+ const mapping = readTsconfigPaths(projectRoot);
576
+ if (!mapping) return EMPTY$1;
577
+ const installed = installedPackages(projectRoot);
578
+ const entries = toEntries(mapping).filter((entry) => !shadowsInstalled(entry, installed));
579
+ if (entries.length === 0) return EMPTY$1;
580
+ return {
581
+ loader: toLoaderAlias(entries, installed),
582
+ candidatesFor: (specifier) => candidatesOf(entries, specifier, installed)
583
+ };
584
+ }
585
+ function shadowsInstalled(entry, installed) {
586
+ if (entry.prefix === "") return true;
587
+ if (entry.wildcard && scopeOf(entry.prefix) !== void 0) return false;
588
+ const name = packageNameOf(entry.prefix);
589
+ return name !== null && installed.has(name);
590
+ }
591
+ function toEntries({ baseUrl, paths }) {
592
+ const entries = [];
593
+ for (const [pattern, targets] of Object.entries(paths)) {
594
+ const star = pattern.indexOf("*");
595
+ entries.push(star === -1 ? {
596
+ prefix: pattern,
597
+ suffix: "",
598
+ wildcard: false,
599
+ base: baseUrl,
600
+ targets
601
+ } : {
602
+ prefix: pattern.slice(0, star),
603
+ suffix: pattern.slice(star + 1),
604
+ wildcard: true,
605
+ base: baseUrl,
606
+ targets
607
+ });
608
+ }
609
+ return entries;
610
+ }
611
+ /** TypeScript picks the single match with the longest prefix before `*`. */
612
+ function candidatesOf(entries, specifier, installed) {
613
+ let best = null;
614
+ for (const entry of entries) {
615
+ if (!matches(entry, specifier)) continue;
616
+ if (!best || entry.prefix.length > best.prefix.length) best = entry;
617
+ }
618
+ if (!best) return [];
619
+ const { base } = best;
620
+ if (!best.wildcard) return best.targets.map((target) => resolve(base, target));
621
+ const name = scopeOf(best.prefix) === void 0 ? null : packageNameOf(specifier);
622
+ if (name !== null && installed.has(name)) return [];
623
+ const matched = specifier.slice(best.prefix.length, specifier.length - best.suffix.length);
624
+ return best.targets.map((target) => resolve(base, withStarFilled(target, matched)));
625
+ }
626
+ function withStarFilled(target, matched) {
627
+ const star = target.indexOf("*");
628
+ return star === -1 ? target : `${target.slice(0, star)}${matched}${target.slice(star + 1)}`;
629
+ }
630
+ function matches(entry, specifier) {
631
+ if (!entry.wildcard) return specifier === entry.prefix;
632
+ return specifier.length >= entry.prefix.length + entry.suffix.length && specifier.startsWith(entry.prefix) && specifier.endsWith(entry.suffix);
633
+ }
634
+ /**
635
+ * Only a mapping that ends in `*` on both sides translates to a prefix alias;
636
+ * one with a suffix after the wildcard has no equivalent and stays behind. A
637
+ * scope's alias becomes one alias per name it leads to that is not installed.
638
+ */
639
+ function toLoaderAlias(entries, installed) {
640
+ const alias = {};
641
+ for (const entry of entries) {
642
+ const written = entry.targets[0];
643
+ if (!written) continue;
644
+ const target = resolve(entry.base, written);
645
+ if (!entry.wildcard) alias[entry.prefix] = target;
646
+ else if (entry.suffix === "" && target.endsWith("*")) {
647
+ const scope = scopeOf(entry.prefix);
648
+ if (scope === void 0) alias[entry.prefix] = target.slice(0, -1);
649
+ else Object.assign(alias, scopeAliases(scope, target.slice(0, -1), installed));
650
+ }
651
+ }
652
+ return alias;
653
+ }
654
+ // src/config/profiles/refused-module-error.ts
655
+ /**
656
+ * A module of the project imported one its framework refuses where
657
+ * definitions run — Next's `client-only` — so the config did not load.
658
+ *
659
+ * @example
660
+ * ```ts
661
+ * throw new RefusedModuleError("client-only", rule, nextMarker);
662
+ * ```
663
+ */
664
+ var RefusedModuleError = class extends CourierError {
665
+ /** Why the framework refuses it, worded to follow the import: `is the marker of …`. */
666
+ reason;
667
+ /** What to do about it, worded as an instruction. */
668
+ remedy;
669
+ /**
670
+ * @param specifier - The import, as written
671
+ * @param rule - Why the framework refuses it, and what to do
672
+ * @param framework - The framework whose rule it is
673
+ */
674
+ constructor(specifier, rule, framework) {
675
+ super(`${specifier} ${rule.reason}. ${rule.remedy}.`, {
676
+ code: "MODULE_REFUSED",
677
+ context: {
678
+ specifier,
679
+ framework: framework?.label
680
+ }
681
+ });
682
+ this.reason = rule.reason;
683
+ this.remedy = rule.remedy;
684
+ }
685
+ };
686
+ // src/config/profiles/profile-modules.ts
687
+ const EMPTY = Object.freeze({});
688
+ /**
689
+ * A profile's imports, in the shape the module loader's `virtualModules`
690
+ * take: each answered before the loader resolves the import, in every module
691
+ * of the load. A `provided` module is made when the load first imports it,
692
+ * and a `refused` one throws there, in the module that imports it. An import
693
+ * the project's own path aliases send elsewhere is left to them: the project
694
+ * says what it is.
695
+ *
696
+ * @param profile - How the framework's server answers its imports
697
+ * @param aliases - The project's path aliases, prefix → target
698
+ * @returns The modules, by specifier
699
+ * @example
700
+ * ```ts
701
+ * createJiti(file, { virtualModules: profileModules(profile, aliases) });
702
+ * ```
703
+ */
704
+ function profileModules(profile, aliases) {
705
+ const modules = {};
706
+ for (const [specifier, rule] of profile.modules) {
707
+ if (isAliased(specifier, aliases)) continue;
708
+ let made;
709
+ Object.defineProperty(modules, specifier, {
710
+ enumerable: true,
711
+ get: () => {
712
+ if (rule.kind === "refused") throw new RefusedModuleError(specifier, rule, profile.framework);
713
+ if (rule.kind === "empty") return EMPTY;
714
+ made ??= rule.exports();
715
+ return made;
716
+ }
717
+ });
718
+ }
719
+ return modules;
720
+ }
721
+ function isAliased(specifier, aliases) {
722
+ return Object.keys(aliases).some((prefix) => specifier === prefix || specifier.startsWith(prefix.endsWith("/") ? prefix : `${prefix}/`));
723
+ }
724
+ // src/config/profiles/environment-view.ts
725
+ /**
726
+ * The environment as a framework's env module shows it, read from
727
+ * `process.env` as code reads it: a module that names one variable depends
728
+ * on that one alone — which a reading of the routes notes, so a change to
729
+ * any other variable does not read them again — and a variable a loader set
730
+ * before the config ran, from the project's env files, is the one seen.
731
+ */
732
+ const NOT_VARIABLES = /* @__PURE__ */ new Set([
733
+ "default",
734
+ "__esModule",
735
+ "then"
736
+ ]);
737
+ /**
738
+ * A read-only view of the variables `shows` lets through, with `members`
739
+ * beside them — Astro's `getSecret`, say.
740
+ *
741
+ * @param shows - Whether the view shows a variable, by its name
742
+ * @param members - What the view holds besides the variables, by name
743
+ * @returns The view, an object whose properties are the variables it shows
744
+ * @example
745
+ * ```ts
746
+ * const publicEnv = environmentView((name) => name.startsWith("PUBLIC_"));
747
+ * publicEnv.PUBLIC_API_URL; // process.env.PUBLIC_API_URL
748
+ * publicEnv.DATABASE_URL; // undefined
749
+ * ```
750
+ */
751
+ function environmentView(shows, members = {}) {
752
+ const isMember = (key) => typeof key === "string" && Object.hasOwn(members, key);
753
+ const isShown = (key) => typeof key === "string" && !NOT_VARIABLES.has(key) && !isMember(key) && shows(key);
754
+ const variable = (name) => {
755
+ const value = process.env[name];
756
+ return typeof value === "string" ? value : void 0;
757
+ };
758
+ const descriptor = (value) => ({
759
+ value,
760
+ enumerable: true,
761
+ configurable: true,
762
+ writable: false
763
+ });
764
+ return new Proxy({}, {
765
+ get: (_, key) => isMember(key) ? members[key] : isShown(key) ? variable(key) : void 0,
766
+ has: (_, key) => isMember(key) || isShown(key) && variable(key) !== void 0,
767
+ ownKeys: () => [...Object.keys(members), ...Object.keys(process.env).filter(isShown)],
768
+ getOwnPropertyDescriptor: (_, key) => {
769
+ if (isMember(key)) return descriptor(members[key]);
770
+ const value = isShown(key) ? variable(key) : void 0;
771
+ return value === void 0 ? void 0 : descriptor(value);
772
+ },
773
+ set: () => false,
774
+ defineProperty: () => false,
775
+ deleteProperty: () => false
776
+ });
777
+ }
778
+ // src/config/profiles/astro-profile.ts
779
+ /**
780
+ * Astro, as its server writes `astro:env/server` and `astro:env/client`:
781
+ * the variables of the environment, and `getSecret` beside them. Astro
782
+ * coerces each variable to the type its `env.schema` declares and fills in
783
+ * its default; the schema is in `astro.config`, which is not run to read
784
+ * the project, so here every variable is the string the environment holds,
785
+ * and one it does not hold is `undefined`.
786
+ */
787
+ const everyVariable = () => true;
788
+ /** What Astro's server answers for its environment modules, and says of its other virtual ones. */
789
+ const ASTRO_RULES = {
790
+ modules: /* @__PURE__ */ new Map([["astro:env/server", {
791
+ kind: "provided",
792
+ exports: () => environmentView(everyVariable, { getSecret: (name) => process.env[name] })
793
+ }], ["astro:env/client", {
794
+ kind: "provided",
795
+ exports: () => environmentView(everyVariable)
796
+ }]]),
797
+ unresolved: (specifier) => specifier.startsWith("astro:") ? {
798
+ reason: "is a module of Astro's runtime, which only Astro's build has",
799
+ remedy: "Import it from a page, an endpoint or a middleware, and keep it out of the modules courier.config imports, which may read astro:env/server and astro:env/client"
800
+ } : void 0
801
+ };
802
+ // src/config/profiles/next-profile.ts
803
+ /** What Next's server layers answer for the markers of server and browser code. */
804
+ const NEXT_RULES = { modules: /* @__PURE__ */ new Map([["server-only", { kind: "empty" }], ["client-only", {
805
+ kind: "refused",
806
+ reason: "is the marker of code for the browser, which Next.js refuses on the server, where courier.config is read",
807
+ remedy: "Import that code from a client component alone, out of the modules courier.config imports"
808
+ }]]) };
809
+ // src/config/profiles/nuxt-profile.ts
810
+ const NUXT_ALIASES = [
811
+ "#imports",
812
+ "#app",
813
+ "#build",
814
+ "#components",
815
+ "#head",
816
+ "#vue-router",
817
+ "#internal"
818
+ ];
819
+ const RUNTIME_REMEDY = "Import what the definitions need from its own package — ofetch, h3, defu — or call the runtime inside a function that runs for a request, such as an endpoint's middleware";
820
+ function unresolved$1(specifier) {
821
+ if (specifier === "~" || specifier.startsWith("~/") || specifier.startsWith("~~/")) return {
822
+ reason: "is Nuxt's alias for the project's source folder, which nuxi prepare writes into .nuxt/tsconfig.json",
823
+ remedy: "Run `npx nuxi prepare` before Courier reads the project — in CI, once the packages are installed"
824
+ };
825
+ if (!NUXT_ALIASES.some((name) => specifier === name || specifier.startsWith(`${name}/`))) return void 0;
826
+ return {
827
+ reason: "is one of Nuxt's aliases, which only Nuxt's build resolves: definitions must not depend on Nuxt's runtime",
828
+ remedy: RUNTIME_REMEDY
829
+ };
830
+ }
831
+ /** What Nuxt says of its aliases and its auto-imports outside its build. */
832
+ const NUXT_RULES = {
833
+ modules: /* @__PURE__ */ new Map(),
834
+ unresolved: unresolved$1,
835
+ undefinedName: () => ({
836
+ reason: "is not defined outside Nuxt: where it is one of the composables or utilities Nuxt imports for you, definitions must not depend on Nuxt's runtime",
837
+ remedy: RUNTIME_REMEDY
838
+ })
839
+ };
840
+ // src/config/profiles/sveltekit-profile.ts
841
+ /**
842
+ * SvelteKit, as its server writes the modules it answers itself: `$env/*`
843
+ * from the environment, split by the public prefix as SvelteKit splits it,
844
+ * and `$app/environment` as it reads while SvelteKit analyses the server
845
+ * during a build — not in a browser, building, in development unless
846
+ * `NODE_ENV` says production. Reading definitions is that case: the server's
847
+ * modules run, and no request is served.
848
+ *
849
+ * The prefix is SvelteKit's default, `PUBLIC_`: one set in `svelte.config.js`
850
+ * is not read, as that would run another of the project's files.
851
+ */
852
+ const PUBLIC_PREFIX = "PUBLIC_";
853
+ const isPublic = (name) => name.startsWith(PUBLIC_PREFIX);
854
+ const isPrivate = (name) => !isPublic(name);
855
+ /** What SvelteKit's server answers for its environment modules. */
856
+ const MODULES = /* @__PURE__ */ new Map([
857
+ ["$env/static/private", {
858
+ kind: "provided",
859
+ exports: () => environmentView(isPrivate)
860
+ }],
861
+ ["$env/static/public", {
862
+ kind: "provided",
863
+ exports: () => environmentView(isPublic)
864
+ }],
865
+ ["$env/dynamic/private", {
866
+ kind: "provided",
867
+ exports: () => Object.freeze({ env: environmentView(isPrivate) })
868
+ }],
869
+ ["$env/dynamic/public", {
870
+ kind: "provided",
871
+ exports: () => Object.freeze({ env: environmentView(isPublic) })
872
+ }],
873
+ ["$app/environment", {
874
+ kind: "provided",
875
+ exports: () => Object.freeze({
876
+ browser: false,
877
+ building: true,
878
+ dev: process.env.NODE_ENV !== "production",
879
+ version: ""
880
+ })
881
+ }]
882
+ ]);
883
+ function unresolved(specifier) {
884
+ if (specifier === "$lib" || specifier.startsWith("$lib/")) return {
885
+ reason: "is SvelteKit's alias for src/lib, which svelte-kit sync writes into .svelte-kit/tsconfig.json",
886
+ remedy: "Run `npx svelte-kit sync` before Courier reads the project — in CI, once the packages are installed"
887
+ };
888
+ if (!specifier.startsWith("$app/") && specifier !== "$service-worker") return void 0;
889
+ return {
890
+ reason: "is a module of SvelteKit's runtime, which only SvelteKit's server has",
891
+ remedy: "Import it where a request is served — a load function, an endpoint, a hook — and keep it out of the modules courier.config imports, which may read $env/* and $app/environment"
892
+ };
893
+ }
894
+ // src/config/profiles/profile-of.ts
895
+ const RULES = {
896
+ next: NEXT_RULES,
897
+ nuxt: NUXT_RULES,
898
+ sveltekit: {
899
+ modules: MODULES,
900
+ unresolved
901
+ },
902
+ astro: ASTRO_RULES
903
+ };
904
+ const NONE = { modules: /* @__PURE__ */ new Map() };
905
+ /**
906
+ * How a framework's server answers the imports it answers itself. A
907
+ * framework that answers none, and a project that lists no framework, are
908
+ * read as plain Node.
909
+ *
910
+ * @param framework - The project's framework, as its package.json tells it
911
+ * @returns The profile every loader of the project's files applies
912
+ * @example
913
+ * ```ts
914
+ * profileOf(projectFramework(projectRoot).framework).modules.has("server-only"); // true for Next.js
915
+ * ```
916
+ */
917
+ function profileOf(framework) {
918
+ return {
919
+ framework,
920
+ ...(framework && RULES[framework.id]) ?? NONE
921
+ };
922
+ }
923
+ /**
924
+ * The framework whose own import a specifier is, among those Courier
925
+ * answers or explains, for a project read as another: `$env/static/private`
926
+ * is SvelteKit's.
927
+ *
928
+ * @param specifier - The import, as written
929
+ * @returns The framework's id, or `undefined` when the import is none of theirs
930
+ * @example
931
+ * ```ts
932
+ * frameworkOfImport("server-only"); // "next"
933
+ * ```
934
+ */
935
+ function frameworkOfImport(specifier) {
936
+ return Object.entries(RULES).find(([, rules]) => rules.modules.has(specifier) || rules.unresolved?.(specifier) !== void 0)?.[0];
937
+ }
938
+ // src/config/config-jiti.ts
939
+ let ownTransform;
940
+ function jitiOwnTransform(createJiti) {
941
+ if (ownTransform) return ownTransform;
942
+ const { transform } = createJiti(fileURLToPath(import.meta.url), {
943
+ fsCache: false,
944
+ moduleCache: false
945
+ }).options;
946
+ if (!transform) throw new Error("jiti created an instance without its own transform");
947
+ ownTransform = transform;
948
+ return transform;
949
+ }
950
+ /**
951
+ * The module loader for Courier's project files — `courier.config.*` and
952
+ * `courier.mocks.*` — with the options both need: the project's tsconfig path
953
+ * aliases, the imports its framework's server answers itself, default-export
954
+ * interop, no module cache, and no native import tried before the loader's
955
+ * own.
956
+ *
957
+ * A framework resolves some imports where it builds the server, never
958
+ * through Node: Next makes `server-only` an empty module on its server
959
+ * layers, SvelteKit and Astro write their env modules from the environment.
960
+ * The loader answers them as that server does, by the framework the
961
+ * project's package.json lists (see `profileOf`), in every module it
962
+ * transforms; one the project's own path aliases name is theirs. A package
963
+ * the loader hands to Node — an installed library — resolves its own imports
964
+ * as Node does while that works; where it fails — a library that imports
965
+ * `server-only` itself — the loader runs the package itself, and answers
966
+ * them there too. A package Node loads without a failure keeps Node's
967
+ * answers: one that imports an installed `client-only` is not refused.
968
+ *
969
+ * The module cache is off because this is also the reload path: the dev
970
+ * server loads the files again on every edit, and a cached module would hand
971
+ * back the file as it was before the user changed it. Every load therefore
972
+ * reads and evaluates every file again; a `transformCache` only spares the
973
+ * transform of a file whose bytes are unchanged.
974
+ *
975
+ * jiti's native attempt, which imports every module natively before falling
976
+ * back to its own loader, stays off too, whatever `JITI_TRY_NATIVE` says and
977
+ * under Bun, where it defaults to on: a module imported natively is the
978
+ * runtime's first evaluation of it for the life of the process, so an edit to
979
+ * it would never be seen, and the attempt comes before the module cache
980
+ * `evalModule` is handed, so two files evaluated over one cache would each
981
+ * evaluate a module they both import.
982
+ *
983
+ * jiti's own file cache stays off, with or without a `transformCache`:
984
+ * - it writes each entry in place, so concurrent writers can leave an entry
985
+ * torn between two transforms that its check still accepts;
986
+ * - an entry it cannot write makes the whole load throw `EACCES`;
987
+ * - unless the file's own directory has a `node_modules`, it falls back to
988
+ * `os.tmpdir()/jiti`, where another user of the machine can place an entry
989
+ * that is then run;
990
+ * - its key ignores `interopDefault` and the babel options, so a transform
991
+ * made under other options can be handed back;
992
+ * - nothing ever prunes it.
993
+ *
994
+ * jiti is loaded here rather than at the top of a module so a bundler that
995
+ * merely reaches this code does not pull a TypeScript transpiler into its
996
+ * graph; only a process that actually reads such a file pays for it.
997
+ *
998
+ * @param file - Absolute, symlink-resolved path of the file to load; modules
999
+ * and aliases resolve from its directory
1000
+ * @param options - Whether to resolve through the project's path aliases
1001
+ * (default: yes), where to reuse transforms (default: nowhere), and how the
1002
+ * framework answers its own imports (default: as package.json tells it)
1003
+ * @returns A jiti instance for that file
1004
+ * @example
1005
+ * ```ts
1006
+ * const transformCache = createTransformCache();
1007
+ * const jiti = await createConfigJiti(configFile, { transformCache });
1008
+ * const config = await jiti.import(configFile, { default: true });
1009
+ * ```
1010
+ */
1011
+ async function createConfigJiti(file, { aliases = true, transformCache, onModule, runTransform, profile, onTransformError } = {}) {
1012
+ const { createJiti } = await import("jiti");
1013
+ const alias = aliases ? readPathAliases(dirname(file)).loader : {};
1014
+ const options = {
1015
+ alias,
1016
+ virtualModules: profileModules(profile ?? profileOf(projectFramework(dirname(file)).framework), alias),
1017
+ interopDefault: true,
1018
+ moduleCache: false,
1019
+ fsCache: false,
1020
+ tryNative: false
1021
+ };
1022
+ if (!transformCache && !onModule && !runTransform && !onTransformError) return createJiti(file, options);
1023
+ const compute = jitiOwnTransform(createJiti);
1024
+ const cached = transformCache ? (transformOptions) => transformCache.transform(transformOptions, compute) : compute;
1025
+ const transform = (given) => {
1026
+ const transformOptions = given.filename === void 0 ? given : {
1027
+ ...given,
1028
+ filename: normalize(given.filename)
1029
+ };
1030
+ const { filename } = transformOptions;
1031
+ if (onModule && filename !== void 0) onModule(filename);
1032
+ const result = runTransform ? runTransform(() => cached(transformOptions)) : cached(transformOptions);
1033
+ if (onTransformError && filename !== void 0 && result.error !== void 0) onTransformError(filename, result.error);
1034
+ return result;
1035
+ };
1036
+ return createJiti(file, {
1037
+ ...options,
1038
+ transform
1039
+ });
1040
+ }
1041
+ // src/config/load-error/config-load-error.ts
1042
+ /**
1043
+ * A project's config, or its mocks file, did not load. The message says
1044
+ * what failed and in which module, with the chain of imports that led there
1045
+ * from the entry — `src/api.ts imports "server-only", which does not resolve
1046
+ * (courier.config.ts → src/api.ts).` — in one line; `hint` says what to do,
1047
+ * in another. Neither holds a secret: the project's own message is quoted
1048
+ * with the environment's values masked and every credential `redact()`
1049
+ * finds cut, each as `[redacted]`. What the project threw is the `cause`,
1050
+ * for a log at debug level, cut the same way in its message and its stack.
1051
+ *
1052
+ * @example
1053
+ * ```ts
1054
+ * if (error instanceof ConfigLoadError) log.error(`${error.message}\n${error.hint}`);
1055
+ * ```
1056
+ */
1057
+ var ConfigLoadError = class extends CourierError {
1058
+ /** What to do about it, in a sentence or two. */
1059
+ hint;
1060
+ /**
1061
+ * @param code - Why the load failed
1062
+ * @param message - What failed and where, in one line
1063
+ * @param details - Where it failed, what to do, and what the project threw
1064
+ */
1065
+ constructor(code, message, details) {
1066
+ super(message, {
1067
+ code,
1068
+ context: details.context,
1069
+ cause: details.cause
1070
+ });
1071
+ this.hint = details.hint;
1072
+ }
1073
+ };
1074
+ // src/util/local-path.ts
1075
+ const NAMESPACE = "\\\\?\\";
1076
+ const SHARE_NAMESPACE = "\\\\?\\UNC\\";
1077
+ /**
1078
+ * A path as this system names it: absolute, written with its own
1079
+ * separator, `.` and `..` resolved, and on Windows out of the namespace
1080
+ * `path.toNamespacedPath` writes it in (`\\?\D:\app` is `D:\app`). The code
1081
+ * a reading runs may name one file in any of these ways, and the loader
1082
+ * writes a path with `/` on Windows too; a watcher reports it in this one.
1083
+ * The system is the one `process.platform` names, as Node's `path` reads it.
1084
+ *
1085
+ * @param path - The path, absolute or from the working folder
1086
+ * @returns The path
1087
+ * @example
1088
+ * ```ts
1089
+ * localPath("/app/src/../flags.json"); // "/app/flags.json"
1090
+ * localPath("D:/a/app/src/api.ts"); // on Windows → "D:\\a\\app\\src\\api.ts"
1091
+ * ```
1092
+ */
1093
+ function localPath(path) {
1094
+ if (process.platform !== "win32") return posix.resolve(path);
1095
+ if (path.startsWith(SHARE_NAMESPACE)) return win32.resolve(`\\\\${path.slice(8)}`);
1096
+ if (path.startsWith(NAMESPACE)) return win32.resolve(path.slice(4));
1097
+ return win32.resolve(path);
1098
+ }
1099
+ // src/config/load-error/import-chain.ts
1100
+ /**
1101
+ * The modules a load ran from its entry to the one that failed, read off the
1102
+ * module cache the loader filled: each module it ran is there, with the
1103
+ * modules it imported as its `children`, and `loaded` set once it ran to its
1104
+ * end. The failure is in the deepest module that has not: every module above
1105
+ * it waits on its import. A module whose failure an importer caught ended
1106
+ * too, so the newest unfinished child is followed.
1107
+ *
1108
+ * Each file is named as the system names it, by `localPath`: the loader
1109
+ * writes the path of a module it imports with `/` on Windows, where the
1110
+ * entry and the error name it with `\`. The failed module is found by its
1111
+ * path so named, and on Windows in any case, as Windows reads a path.
1112
+ *
1113
+ * @param cache - The load's module cache
1114
+ * @param entry - The entry's key in it, as the loader registered it
1115
+ * @param failed - The file of the module that failed, where the error names it
1116
+ * @returns The modules' files, the entry first; only the entry when the cache holds no more
1117
+ * @example
1118
+ * ```ts
1119
+ * importChain(cache, "/app/courier.config.ts");
1120
+ * // → ["/app/courier.config.ts", "/app/src/api.ts", "/app/src/server/db.ts"]
1121
+ * ```
1122
+ */
1123
+ function importChain(cache, entry, failed) {
1124
+ const root = cache[entry];
1125
+ if (root === void 0) return [localPath(entry)];
1126
+ return ((failed === void 0 ? void 0 : pathTo(root, fileKey(failed))) ?? newestUnfinished(root)).map((module) => localPath(module.filename));
1127
+ }
1128
+ function newestUnfinished(root) {
1129
+ const chain = [root];
1130
+ const seen = /* @__PURE__ */ new Set([root]);
1131
+ for (let current = root;;) {
1132
+ const next = current.children.findLast((child) => !child.loaded && !seen.has(child));
1133
+ if (next === void 0) return chain;
1134
+ seen.add(next);
1135
+ chain.push(next);
1136
+ current = next;
1137
+ }
1138
+ }
1139
+ function fileKey(path) {
1140
+ const local = localPath(path);
1141
+ return process.platform === "win32" ? local.toLowerCase() : local;
1142
+ }
1143
+ /**
1144
+ * A chain as a line names it: each module by its path from the project's
1145
+ * folder, a package by its name.
1146
+ *
1147
+ * @param chain - The chain, as `importChain` gives it, a package appended by name
1148
+ * @param folder - The project's folder, absolute
1149
+ * @returns `courier.config.ts → src/api.ts → src/server/db.ts`
1150
+ * @example
1151
+ * ```ts
1152
+ * shownChain(["/app/courier.config.ts", "/app/src/api.ts"], "/app"); // "courier.config.ts → src/api.ts"
1153
+ * ```
1154
+ */
1155
+ function shownChain(chain, folder) {
1156
+ return chain.map((link) => shownModule(link, folder)).join(" → ");
1157
+ }
1158
+ /**
1159
+ * A module as a line names it: a file of the project by its path from the
1160
+ * project's folder, an installed package's by the package's name.
1161
+ *
1162
+ * @param link - A module's file, absolute, or a package's name
1163
+ * @param folder - The project's folder, absolute
1164
+ * @returns The name
1165
+ * @example
1166
+ * ```ts
1167
+ * shownModule("/app/node_modules/@acme/ui/index.js", "/app"); // "@acme/ui"
1168
+ * ```
1169
+ */
1170
+ function shownModule(link, folder) {
1171
+ if (!isAbsolute(link)) return link;
1172
+ return packageOfFile(link, folder) ?? relative(folder, link).split(sep).join("/");
1173
+ }
1174
+ /**
1175
+ * The installed package a file belongs to: the name after its last
1176
+ * `node_modules`, with its scope. Only the part of the path below the
1177
+ * project's folder counts, so a project that is itself kept under a
1178
+ * `node_modules` folder — a cache, a test's — is no package; a file above
1179
+ * the folder, a package installed further up a workspace, counts whole.
1180
+ *
1181
+ * @param file - The file, absolute
1182
+ * @param folder - The project's folder, absolute
1183
+ * @returns The package's name, or `undefined` for a file of no package
1184
+ * @example
1185
+ * ```ts
1186
+ * packageOfFile("/app/node_modules/@clerk/nextjs/dist/esm/server/index.js", "/app"); // "@clerk/nextjs"
1187
+ * ```
1188
+ */
1189
+ function packageOfFile(file, folder) {
1190
+ const below = relative(folder, file);
1191
+ const parts = (below.startsWith("..") || isAbsolute(below) ? file : below).split(/[\\/]/);
1192
+ const at = parts.lastIndexOf("node_modules");
1193
+ if (at === -1) return void 0;
1194
+ const [first, second] = parts.slice(at + 1);
1195
+ if (first === void 0) return void 0;
1196
+ return first.startsWith("@") && second !== void 0 ? `${first}/${second}` : first;
1197
+ }
1198
+ function pathTo(root, failed) {
1199
+ const queue = [{
1200
+ module: root,
1201
+ chain: [root]
1202
+ }];
1203
+ const seen = /* @__PURE__ */ new Set([root]);
1204
+ for (let at = 0; at < queue.length; at += 1) {
1205
+ const step = queue[at];
1206
+ if (step === void 0) break;
1207
+ if (fileKey(step.module.filename) === failed) return step.chain;
1208
+ for (const child of step.module.children) {
1209
+ if (seen.has(child)) continue;
1210
+ seen.add(child);
1211
+ queue.push({
1212
+ module: child,
1213
+ chain: [...step.chain, child]
1214
+ });
1215
+ }
1216
+ }
1217
+ }
1218
+ // src/config/load-error/load-hints.ts
1219
+ /** What to do about a module that could not be compiled. */
1220
+ const TRANSFORM_HINT = "Correct the code where the message points. JSX, and a file a framework compiles itself, such as a .vue or .svelte component, stay out of the modules the config imports.";
1221
+ /** What to do about a load that never ended. */
1222
+ const TIMEOUT_HINT = "Something a module runs as it is imported does not end — a connection, a server, a timer it waits for. Open it in the function that needs it, not as the module loads.";
1223
+ /**
1224
+ * What to do about an import that did not resolve: what the project's
1225
+ * framework says of it, or the framework whose import it is when Courier
1226
+ * reads the project as another, or else how to mend an import.
1227
+ *
1228
+ * @param specifier - The import, as written
1229
+ * @param profile - The framework Courier read the project as
1230
+ * @param project - The package.json that told it, absolute, and the project's folder
1231
+ * @returns The hint
1232
+ * @example
1233
+ * ```ts
1234
+ * notFoundHint("$env/static/private", profileOf(undefined), { manifest: "/app/package.json", folder: "/app" });
1235
+ * // → '"$env/static/private" is SvelteKit's own module. Courier reads this project as plain Node, …'
1236
+ * ```
1237
+ */
1238
+ function notFoundHint(specifier, profile, project) {
1239
+ const { manifest, folder } = project;
1240
+ const own = profile.unresolved?.(specifier);
1241
+ if (own !== void 0) return `"${specifier}" ${own.reason}. ${own.remedy}.`;
1242
+ const owner = FRAMEWORK_MARKERS.find((marker) => marker.id === frameworkOfImport(specifier));
1243
+ if (owner !== void 0 && owner.id !== profile.framework?.id) {
1244
+ const readAs = profile.framework?.label ?? "plain Node";
1245
+ const by = manifest === void 0 ? "as no package.json is in its folder or above it" : `by ${shownModule(manifest, folder)}`;
1246
+ return `"${specifier}" is ${owner.label}'s own module. Courier reads this project as ${readAs}, ${by}: list ${owner.marker} there if the project is built with ${owner.label}.`;
1247
+ }
1248
+ const name = /^[./]|^[A-Za-z]:[\\/]/.test(specifier) ? null : packageNameOf(specifier);
1249
+ return name === null ? "Correct the import, or create the module it names." : `Install ${name} where the project's packages are, or correct the import.`;
1250
+ }
1251
+ /**
1252
+ * What to do about code that threw as it was run: what the framework says of
1253
+ * a name its build provides, or how Courier runs the project.
1254
+ *
1255
+ * @param error - What the code threw
1256
+ * @param profile - The framework Courier read the project as
1257
+ * @param entryName - The entry's file name, `courier.config.ts`
1258
+ * @returns The hint
1259
+ * @example
1260
+ * ```ts
1261
+ * evaluationHint(new ReferenceError("useRuntimeConfig is not defined"), nuxtProfile, "courier.config.ts");
1262
+ * ```
1263
+ */
1264
+ function evaluationHint(error, profile, entryName) {
1265
+ const name = error instanceof ReferenceError ? /^(\S+) is not defined/.exec(error.message)?.[1] : void 0;
1266
+ const said = name === void 0 ? void 0 : profile.undefinedName?.(name);
1267
+ if (name !== void 0 && said !== void 0) return `${name} ${said.reason}. ${said.remedy}.`;
1268
+ return `Courier runs ${entryName} and every module it imports, as your server would: a module that checks the environment as it is imported needs those variables set where Courier reads the project, or can skip the check while COURIER_PHASE is "definitions".`;
1269
+ }
1270
+ // src/config/load-error/shown-variables.ts
1271
+ /**
1272
+ * Which variables of an environment a quoted message shows the values of
1273
+ * rather than masks them. A variable the system sets for every process by
1274
+ * its own name — `PATH`, `HOME`, `PWD`, the user, the shell — is shown.
1275
+ * Any other whose name says it holds a secret, read as the core reads a
1276
+ * record's field of that name, is masked whatever it holds, one of a
1277
+ * family the system names by a prefix too, as npm's `npm_config__auth`
1278
+ * is. The rest are shown where they are of such a family — npm's settings,
1279
+ * the locale — or name places: an absolute path, or a list of them, as
1280
+ * `RUNNER_TEMP`, `APPDATA` and `NODE_PATH` hold. Masking those would take
1281
+ * the path out of every file a message names, and a machine sets more of
1282
+ * them than a list of names could know.
1283
+ *
1284
+ * A path is read as the platform reads one, and on Windows a name in any
1285
+ * case, as Windows reads it. A path holding a run of 16 letters and digits
1286
+ * mixing both — a key in hex or base64, which may start with `/`, or a
1287
+ * token in a path, as a webhook's is — names no place and is masked. The
1288
+ * folders a system variable names are read past: the system names its
1289
+ * own, at random as macOS names a user's temporary folder.
1290
+ */
1291
+ /** Variables a POSIX system, its shells and npm set for every process: paths and settings. */
1292
+ const POSIX_VARIABLES = [
1293
+ "PATH",
1294
+ "HOME",
1295
+ "PWD",
1296
+ "OLDPWD",
1297
+ "INIT_CWD",
1298
+ "TMPDIR",
1299
+ "TMP",
1300
+ "TEMP",
1301
+ "SHELL",
1302
+ "USER",
1303
+ "USERNAME",
1304
+ "LOGNAME",
1305
+ "HOSTNAME",
1306
+ "LANG",
1307
+ "LANGUAGE",
1308
+ "TERM",
1309
+ "TERM_PROGRAM",
1310
+ "TERM_PROGRAM_VERSION",
1311
+ "COLORTERM",
1312
+ "EDITOR",
1313
+ "VISUAL",
1314
+ "PAGER",
1315
+ "NODE",
1316
+ "NODE_ENV",
1317
+ "CI",
1318
+ "COURIER_PHASE"
1319
+ ];
1320
+ /** Variables Windows sets for every process that hold no path: the machine's names and settings. */
1321
+ const WINDOWS_VARIABLES = [
1322
+ "COMPUTERNAME",
1323
+ "USERDOMAIN",
1324
+ "USERDOMAIN_ROAMINGPROFILE",
1325
+ "OS",
1326
+ "PATHEXT",
1327
+ "PROCESSOR_ARCHITECTURE",
1328
+ "PROCESSOR_IDENTIFIER",
1329
+ "PROCESSOR_LEVEL",
1330
+ "PROCESSOR_REVISION",
1331
+ "NUMBER_OF_PROCESSORS",
1332
+ "SESSIONNAME"
1333
+ ];
1334
+ const SYSTEM_PREFIXES = [
1335
+ "LC_",
1336
+ "XDG_",
1337
+ "npm_config_",
1338
+ "npm_package_",
1339
+ "npm_lifecycle_"
1340
+ ];
1341
+ /** The shortest run of a key's characters, mixing letters with digits, taken for a key. */
1342
+ const KEY_RUN = 16;
1343
+ const NO_KEY_CHARACTERS = /[^A-Za-z0-9+=]+/;
1344
+ const LETTER$1 = /[A-Za-z]/;
1345
+ const DIGIT = /\d/;
1346
+ const POSIX = {
1347
+ path: posix,
1348
+ separators: /\/+/,
1349
+ caseless: false,
1350
+ names: new Set(POSIX_VARIABLES),
1351
+ prefixes: SYSTEM_PREFIXES
1352
+ };
1353
+ const WINDOWS = {
1354
+ path: win32,
1355
+ separators: /[\\/]+/,
1356
+ caseless: true,
1357
+ names: /* @__PURE__ */ new Set([...POSIX_VARIABLES, ...WINDOWS_VARIABLES]),
1358
+ prefixes: SYSTEM_PREFIXES.map((prefix) => prefix.toUpperCase())
1359
+ };
1360
+ /**
1361
+ * What tells whether a variable of the environment is shown in a quoted
1362
+ * message, by the rules above, on the platform this process runs on.
1363
+ *
1364
+ * @param env - The environment the variables are of
1365
+ * @returns Whether a variable, by its name and value, is shown
1366
+ * @example
1367
+ * ```ts
1368
+ * const shown = shownVariables(process.env);
1369
+ * shown("RUNNER_TEMP", "/home/runner/work/_temp"); // → true
1370
+ * shown("DATABASE_URL", "postgres://u:p@db/app"); // → false
1371
+ * ```
1372
+ */
1373
+ function shownVariables(env) {
1374
+ const platform = process.platform === "win32" ? WINDOWS : POSIX;
1375
+ const places = systemPlaces(env, platform);
1376
+ return (name, value) => {
1377
+ if (isSystemName(name, platform)) return true;
1378
+ if (isSecretValue(name, value)) return false;
1379
+ return isSystemFamily(name, platform) || namesPlaces(value, places, platform);
1380
+ };
1381
+ }
1382
+ function isSystemName(name, platform) {
1383
+ return platform.names.has(platform.caseless ? name.toUpperCase() : name);
1384
+ }
1385
+ function isSystemFamily(name, platform) {
1386
+ const read = platform.caseless ? name.toUpperCase() : name;
1387
+ return platform.prefixes.some((prefix) => read.startsWith(prefix));
1388
+ }
1389
+ function systemPlaces(env, platform) {
1390
+ return Object.entries(env).flatMap(([name, value]) => value === void 0 || !isSystemName(name, platform) ? [] : entriesOf(value, platform).filter((entry) => platform.path.isAbsolute(entry)).map((entry) => foldersOf(entry, platform)));
1391
+ }
1392
+ function namesPlaces(value, places, platform) {
1393
+ const entries = entriesOf(value, platform);
1394
+ return entries.length > 0 && entries.every((entry) => platform.path.isAbsolute(entry) && !holdsKey(pastPlaces(foldersOf(entry, platform), places, platform)));
1395
+ }
1396
+ function entriesOf(value, platform) {
1397
+ return value.split(platform.path.delimiter).filter((entry) => entry !== "");
1398
+ }
1399
+ function foldersOf(path, platform) {
1400
+ return path.split(platform.separators).filter((folder) => folder !== "");
1401
+ }
1402
+ function pastPlaces(folders, places, platform) {
1403
+ let depth = 0;
1404
+ for (const place of places) if (place.length > depth && place.length <= folders.length && place.every((folder, at) => sameName(folder, folders[at], platform))) depth = place.length;
1405
+ return folders.slice(depth);
1406
+ }
1407
+ function sameName(a, b, platform) {
1408
+ return platform.caseless ? a.toLowerCase() === b?.toLowerCase() : a === b;
1409
+ }
1410
+ function holdsKey(folders) {
1411
+ return folders.some((folder) => folder.split(NO_KEY_CHARACTERS).some((run) => run.length >= KEY_RUN && LETTER$1.test(run) && DIGIT.test(run)));
1412
+ }
1413
+ // src/config/load-error/value-spans.ts
1414
+ /**
1415
+ * Where the values of an environment stand in a text. A value is found as
1416
+ * the variable holds it, and in the forms code writes it in: escaped in
1417
+ * JSON, encoded for an address, trimmed, and each line of a value of
1418
+ * several, such as a key in PEM. A value, or one of its forms, is found
1419
+ * where it is 8 characters long or more; a shorter one — `true`, a port —
1420
+ * would mask words of the message itself and is rarely a secret. A value
1421
+ * the code changed some other way — cut, encoded otherwise — is not found,
1422
+ * nor is one of a variable `shownVariables` shows. Each is found wherever
1423
+ * it stands, the same text found for two values that overlap included, so
1424
+ * no part of one is left where the other was found.
1425
+ */
1426
+ /** The shortest value found. */
1427
+ const MIN_MASKED_LENGTH = 8;
1428
+ /**
1429
+ * The stretches of a text that values of the environment stand in, in
1430
+ * order and apart: two that overlap or touch are one.
1431
+ *
1432
+ * @param text - The text, as the code wrote it
1433
+ * @param env - The environment
1434
+ * @param reach - Where a stretch starts at the latest: the rest of the text is read only as far as a value that starts before it runs
1435
+ * @returns The stretches
1436
+ * @example
1437
+ * ```ts
1438
+ * valueSpans("bad key s3cr3t-v4lue", { KEY: "s3cr3t-v4lue" }); // → [[8, 20]]
1439
+ * ```
1440
+ */
1441
+ function valueSpans(text, env, reach = text.length) {
1442
+ const shown = shownVariables(env);
1443
+ const values = /* @__PURE__ */ new Set();
1444
+ for (const [name, value] of Object.entries(env)) {
1445
+ if (value === void 0 || value.length < MIN_MASKED_LENGTH || shown(name, value)) continue;
1446
+ for (const form of formsOf(value)) if (form.length >= MIN_MASKED_LENGTH) values.add(form);
1447
+ }
1448
+ const longest = Math.max(0, ...[...values].map((value) => value.length));
1449
+ const read = reach + longest < text.length ? text.slice(0, reach + longest) : text;
1450
+ const found = [];
1451
+ for (const value of values) for (let at = read.indexOf(value); at !== -1 && at < reach; at = read.indexOf(value, at + 1)) found.push([at, at + value.length]);
1452
+ return joined(found);
1453
+ }
1454
+ function formsOf(value) {
1455
+ const lines = value.split(/\r?\n/);
1456
+ return [
1457
+ value,
1458
+ JSON.stringify(value).slice(1, -1),
1459
+ encodeURIComponent(value),
1460
+ value.trim(),
1461
+ ...lines.length > 1 ? lines.map((line) => line.trim()) : []
1462
+ ];
1463
+ }
1464
+ /**
1465
+ * Stretches in order, each that overlaps or touches the one before it
1466
+ * joined to it.
1467
+ *
1468
+ * @param spans - The stretches, in any order
1469
+ * @returns The stretches, in order and apart
1470
+ * @example
1471
+ * ```ts
1472
+ * joined([[4, 9], [0, 2], [2, 3]]); // → [[0, 3], [4, 9]]
1473
+ * ```
1474
+ */
1475
+ function joined(spans) {
1476
+ const sorted = [...spans].sort((a, b) => a[0] - b[0]);
1477
+ const apart = [];
1478
+ for (const [start, end] of sorted) {
1479
+ const last = apart.at(-1);
1480
+ if (last !== void 0 && start <= last[1]) last[1] = Math.max(last[1], end);
1481
+ else apart.push([start, end]);
1482
+ }
1483
+ return apart;
1484
+ }
1485
+ // src/config/load-error/without-secrets.ts
1486
+ /**
1487
+ * A message the project's code wrote, as it leaves the process — for a CI
1488
+ * log, which may be public, or a page — with no secret left in it: every
1489
+ * value of the environment masked, where `valueSpans` finds one, and every
1490
+ * credential the core's `redact()` finds cut — a bearer token, a password
1491
+ * in an address, a `token=` pair — each with the one sign, `[redacted]`.
1492
+ *
1493
+ * Each is found in the text as the code wrote it, so neither reads what the
1494
+ * other left. Masking a value first breaks what a credential is found by: a
1495
+ * runner's `PGUSER=postgres` masked in `postgres://app:hunter2@db/shop`
1496
+ * leaves no address to cut the password from. Cutting first leaves a value
1497
+ * whose part a credential's rule cut — the host of `DATABASE_URL`, the last
1498
+ * words of a passphrase — where no mask finds it whole.
1499
+ *
1500
+ * So the text is cut as it is, then cut again for each stretch a value
1501
+ * stands in, with that stretch written over and every character of it
1502
+ * changed: what the two cuts share before and after it does not hang on the
1503
+ * value, and what lies between is masked. A value the cut kept is masked
1504
+ * whole, one it took out leaves nothing to mask, and a credential around
1505
+ * either is cut as the core cuts it. The cut reads the text as a terminal
1506
+ * shows it, without its control codes: a code's letters run into the word
1507
+ * after them, which hides a credential's scheme.
1508
+ *
1509
+ * It costs what `redact()` costs a text, times the cuts, whatever it is
1510
+ * handed: four times 8192 characters read, to a word's end and never into a
1511
+ * value, 8192 kept, and the text cut again 16 times at most.
1512
+ */
1513
+ /** The most of a text kept, in characters, as `redact()` keeps one: past it, the text ends with "…". */
1514
+ const MAX_KEPT = 8192;
1515
+ /** The most of a text read, four times what is kept, as `redact()` reads one: it ends at a word. */
1516
+ const MAX_READ = 4 * MAX_KEPT;
1517
+ /** The most times a text is cut again: more stretches are written over together, in as many groups. */
1518
+ const MAX_RECUTS = 16;
1519
+ const WHITESPACE = /\s/;
1520
+ const ANY = /[\s\S]/g;
1521
+ const ESCAPE = 27;
1522
+ const LETTER = /[A-Za-z]/;
1523
+ /**
1524
+ * The text with no secret left to quote, by the rules above.
1525
+ *
1526
+ * @param text - The message, as the project's code wrote it
1527
+ * @param env - The environment whose values are masked
1528
+ * @returns The message, with the one sign in place of each secret
1529
+ * @example
1530
+ * ```ts
1531
+ * withoutSecrets("cannot reach postgres://app:pw@db/shop", { PGUSER: "postgres" });
1532
+ * // → "cannot reach [redacted]://db/shop"
1533
+ * ```
1534
+ */
1535
+ function withoutSecrets(text, env = process.env) {
1536
+ const reach = Math.min(text.length, MAX_READ);
1537
+ const spans = valueSpans(text, env, reach);
1538
+ const end = readEnd(text, spans, reach);
1539
+ const read = text.slice(0, end);
1540
+ const cut = shownCut(read);
1541
+ const within = spans.filter(([, stop]) => stop <= end);
1542
+ const said = within.length === 0 ? cut : maskedIn(cut, dependentRegions(read, cut, within));
1543
+ if (end === text.length && said.length <= MAX_KEPT) return said;
1544
+ return end === 0 ? `${REDACTED}…` : `${sliceWhole(said, 8191)}…`;
1545
+ }
1546
+ function readEnd(text, spans, reach) {
1547
+ if (reach === text.length) return reach;
1548
+ let end = reach;
1549
+ for (;;) {
1550
+ while (end > 0 && !WHITESPACE.test(text.charAt(end))) end -= 1;
1551
+ const crossed = spans.find(([start, stop]) => start < end && end < stop);
1552
+ if (crossed === void 0) return end;
1553
+ end = crossed[0];
1554
+ }
1555
+ }
1556
+ function dependentRegions(read, cut, spans) {
1557
+ const size = Math.ceil(spans.length / MAX_RECUTS);
1558
+ const regions = [];
1559
+ for (let at = 0; at < spans.length; at += size) {
1560
+ const region = unshared(cut, shownCut(writtenOver(read, spans.slice(at, at + size))));
1561
+ if (region[0] < region[1]) regions.push(region);
1562
+ }
1563
+ return joined(regions);
1564
+ }
1565
+ function writtenOver(text, spans) {
1566
+ let written = "";
1567
+ let at = 0;
1568
+ for (const [start, end] of spans) {
1569
+ const over = text.slice(start, end).replace(ANY, (character) => character === "x" || character === "X" ? "y" : "x");
1570
+ written += `${text.slice(at, start)}${over}`;
1571
+ at = end;
1572
+ }
1573
+ return `${written}${text.slice(at)}`;
1574
+ }
1575
+ function unshared(text, other) {
1576
+ const shorter = Math.min(text.length, other.length);
1577
+ let start = 0;
1578
+ while (start < shorter && text[start] === other[start]) start += 1;
1579
+ let end = 0;
1580
+ while (end < shorter - start && text[text.length - 1 - end] === other[other.length - 1 - end]) end += 1;
1581
+ return [start, text.length - end];
1582
+ }
1583
+ function maskedIn(cut, regions) {
1584
+ let said = "";
1585
+ let at = 0;
1586
+ for (const [start, end] of regions) {
1587
+ said += `${cut.slice(at, start)}${REDACTED}`;
1588
+ at = end;
1589
+ }
1590
+ const parts = `${said}${cut.slice(at)}`.split(REDACTED);
1591
+ return parts.filter((part, index) => part !== "" || index === 0 || index === parts.length - 1).join(REDACTED);
1592
+ }
1593
+ function shownCut(text) {
1594
+ return cutSecrets(withoutTerminalCodes(text));
1595
+ }
1596
+ function withoutTerminalCodes(text) {
1597
+ let shown = "";
1598
+ for (let at = 0; at < text.length; at += 1) {
1599
+ const code = text.charCodeAt(at);
1600
+ if (code === ESCAPE && text[at + 1] === "[") {
1601
+ at += 2;
1602
+ while (at < text.length && !LETTER.test(text.charAt(at))) at += 1;
1603
+ } else shown += code < 32 && code !== 10 && code !== 9 || code === 127 ? " " : text.charAt(at);
1604
+ }
1605
+ return shown;
1606
+ }
1607
+ /**
1608
+ * What the project's code threw, as an error whose message and stack hold
1609
+ * no secret, by the rules above: the error a load error carries as its
1610
+ * `cause`, so no log that prints it — at debug level, by a bundler — holds
1611
+ * one either. It keeps the thrown error's name and `code`; a value thrown
1612
+ * that is no error becomes one, its text cut the same way.
1613
+ *
1614
+ * @param thrown - What the code threw
1615
+ * @param env - The environment whose values are masked
1616
+ * @returns The error, masked
1617
+ * @example
1618
+ * ```ts
1619
+ * maskedError(new TypeError(`bad ${process.env.API_KEY}`)).message; // "bad [redacted]"
1620
+ * ```
1621
+ */
1622
+ function maskedError(thrown, env = process.env) {
1623
+ if (!(thrown instanceof Error)) return new Error(withoutSecrets(describedValue(thrown), env));
1624
+ const masked = new Error(withoutSecrets(thrown.message, env));
1625
+ masked.name = thrown.name;
1626
+ masked.stack = thrown.stack === void 0 ? `${masked.name}: ${masked.message}` : withoutSecrets(thrown.stack, env);
1627
+ const code = Reflect.get(thrown, "code");
1628
+ if (typeof code === "string") Object.defineProperty(masked, "code", {
1629
+ value: code,
1630
+ enumerable: true
1631
+ });
1632
+ return masked;
1633
+ }
1634
+ /**
1635
+ * A value thrown that is no error, as text: a string as it is, anything else
1636
+ * as Node's inspector writes it — `[Object: null prototype] {}` — since a
1637
+ * value with no string form of its own would throw again when made one.
1638
+ *
1639
+ * @param value - What was thrown
1640
+ * @returns Its text
1641
+ * @example
1642
+ * ```ts
1643
+ * describedValue(Object.create(null)); // "[Object: null prototype] {}"
1644
+ * ```
1645
+ */
1646
+ function describedValue(value) {
1647
+ return typeof value === "string" ? value : inspect(value, {
1648
+ depth: 2,
1649
+ breakLength: Number.POSITIVE_INFINITY
1650
+ });
1651
+ }
1652
+ // src/config/load-error/shown-text.ts
1653
+ /** The longest part of the project's own message a load error quotes, in characters. */
1654
+ const MAX_QUOTED_LENGTH = 300;
1655
+ /**
1656
+ * The first line of a message the project's code wrote, as a load error
1657
+ * quotes it: no secret left in it, as `withoutSecrets` leaves none, its
1658
+ * terminal codes and control characters taken out, and cut short past 300
1659
+ * characters. The whole text is read as the code wrote it before a line is
1660
+ * taken, since a value is found only as it stands — a line break, a control
1661
+ * character and all.
1662
+ *
1663
+ * @param text - The message
1664
+ * @returns The line to quote
1665
+ * @example
1666
+ * ```ts
1667
+ * shownText("Invalid environment variables\n DATABASE_URL: Required"); // "Invalid environment variables"
1668
+ * ```
1669
+ */
1670
+ function shownText(text) {
1671
+ const line = (withoutSecrets(text).split("\n", 1)[0] ?? "").replace(/\t/g, " ").trim();
1672
+ return line.length > MAX_QUOTED_LENGTH ? `${sliceWhole(line, MAX_QUOTED_LENGTH)}…` : line;
1673
+ }
1674
+ // src/config/load-error/read-failure.ts
1675
+ /**
1676
+ * What the error a failed load threw says, read as the loader and Node
1677
+ * write it: the module whose transform failed, and where; the import that
1678
+ * did not resolve, and the module that made it; and what was thrown, as a
1679
+ * line to quote.
1680
+ */
1681
+ const PLACE = /^\d+:\d+$/;
1682
+ const IMPORTED_FROM = " imported from ";
1683
+ const LINE_ENDS = [
1684
+ "\r",
1685
+ "\u2028",
1686
+ "\u2029"
1687
+ ];
1688
+ /**
1689
+ * The module whose transform failed, where the error is the one the loader
1690
+ * threw for it.
1691
+ *
1692
+ * @param error - What the load threw
1693
+ * @param failures - Each module whose transform failed in the load, with what the transformer said
1694
+ * @returns The module, where it stopped and what the transformer said; `undefined` for another failure
1695
+ * @example
1696
+ * ```ts
1697
+ * failedTransform(error, new Map([["/app/src/api.ts", babelError]]))?.line; // 3
1698
+ * ```
1699
+ */
1700
+ function failedTransform(error, failures) {
1701
+ const message = error instanceof Error ? error.message : "";
1702
+ for (const [file, said] of failures ?? []) if (message.includes(file)) return {
1703
+ file,
1704
+ ...parseError(said, file)
1705
+ };
1706
+ }
1707
+ function parseError(said, file) {
1708
+ const loc = said instanceof Error ? Reflect.get(said, "loc") : void 0;
1709
+ const line = typeof loc === "object" && loc !== null ? Reflect.get(loc, "line") : void 0;
1710
+ const column = typeof loc === "object" && loc !== null ? Reflect.get(loc, "column") : void 0;
1711
+ const text = said instanceof Error ? said.message : describedValue(said);
1712
+ return {
1713
+ line: typeof line === "number" ? line : void 0,
1714
+ column: typeof column === "number" ? column + 1 : void 0,
1715
+ said: withoutPlace(text.replace(`${file}: `, ""))
1716
+ };
1717
+ }
1718
+ function withoutPlace(text) {
1719
+ const ended = text.trimEnd();
1720
+ if (!ended.endsWith(")")) return text;
1721
+ const open = ended.lastIndexOf("(");
1722
+ if (open === -1 || !PLACE.test(ended.slice(open + 1, -1))) return text;
1723
+ return ended.slice(0, open).trimEnd();
1724
+ }
1725
+ /**
1726
+ * An import Node's resolution or the loader's did not find: the import, and
1727
+ * the module that made it, where the error names one.
1728
+ *
1729
+ * @param error - What the load threw
1730
+ * @returns The import and its importer; `undefined` for another failure
1731
+ * @example
1732
+ * ```ts
1733
+ * missingImport(error)?.specifier; // "server-only"
1734
+ * ```
1735
+ */
1736
+ function missingImport(error) {
1737
+ if (!(error instanceof Error)) return void 0;
1738
+ const code = Reflect.get(error, "code");
1739
+ if (code !== "MODULE_NOT_FOUND" && code !== "ERR_MODULE_NOT_FOUND") return void 0;
1740
+ const line = error.message.split("\n", 1)[0] ?? "";
1741
+ const specifier = /^Cannot find (?:module|package) '([^']+)'/.exec(line)?.[1];
1742
+ if (specifier === void 0) return void 0;
1743
+ const stack = Reflect.get(error, "requireStack");
1744
+ return {
1745
+ specifier,
1746
+ importer: (Array.isArray(stack) && typeof stack[0] === "string" ? stack[0] : void 0) ?? importerIn(line)
1747
+ };
1748
+ }
1749
+ function importerIn(line) {
1750
+ const after = Math.max(...LINE_ENDS.map((end) => line.lastIndexOf(end))) + 1;
1751
+ const at = line.indexOf(IMPORTED_FROM, after);
1752
+ const importer = at === -1 ? "" : line.slice(at + 15);
1753
+ return importer === "" ? void 0 : importer;
1754
+ }
1755
+ /**
1756
+ * What was thrown, in a line to quote: an error's name and the first line of
1757
+ * its message, masked; any other value as text.
1758
+ *
1759
+ * @param error - What was thrown
1760
+ * @returns The line
1761
+ * @example
1762
+ * ```ts
1763
+ * described(new TypeError("Invalid URL")); // "TypeError: Invalid URL"
1764
+ * ```
1765
+ */
1766
+ function described(error) {
1767
+ return error instanceof Error ? `${error.name}: ${shownText(error.message)}` : shownText(describedValue(error));
1768
+ }
1769
+ // src/config/load-error/explain-load-failure.ts
1770
+ /**
1771
+ * Why a project's config — or its mocks file — did not load, told from what
1772
+ * the load left: the error, the module cache that holds every module it
1773
+ * ran, and the transforms that failed. One explanation for every loader, so
1774
+ * the CLI, Studio and a bundler plugin say the same of the same project.
1775
+ */
1776
+ /**
1777
+ * The load error that says why a load failed, what to do, and where — the
1778
+ * chain of imports from the entry to the module that failed.
1779
+ *
1780
+ * @param error - What the load threw
1781
+ * @param load - What the loader knows of the load
1782
+ * @returns The error, typed: an error that already is one is handed back
1783
+ * @example
1784
+ * ```ts
1785
+ * try { await jiti.evalModule(source, { filename: file, cache }); }
1786
+ * catch (error) { throw configLoadError(error, { entry: file, cache }); }
1787
+ * ```
1788
+ */
1789
+ function configLoadError(error, load) {
1790
+ if (error instanceof ConfigLoadError) return error;
1791
+ const known = knownOf(load);
1792
+ const { cache, entryKey } = known;
1793
+ if (error instanceof RefusedModuleError) return failure(known, "MODULE_REFUSED", error, importChain(cache, entryKey), error.context.specifier, {
1794
+ what: `imports "${error.context.specifier}": it ${error.reason}`,
1795
+ hint: `${error.remedy}.`
1796
+ });
1797
+ const compiled = failedTransform(error, load.transformFailures);
1798
+ if (compiled !== void 0) {
1799
+ const chain = importChain(cache, entryKey, compiled.file);
1800
+ const at = compiled.line === void 0 ? "" : `:${compiled.line}:${compiled.column ?? 0}`;
1801
+ return failure(known, "TRANSFORM_FAILED", error, chain, void 0, {
1802
+ who: `${shownModule(compiled.file, known.folder)}${at}`,
1803
+ what: `cannot be compiled: ${shownText(compiled.said)}`,
1804
+ hint: TRANSFORM_HINT
1805
+ });
1806
+ }
1807
+ const missing = missingImport(error);
1808
+ if (missing !== void 0) {
1809
+ const chain = importChain(cache, entryKey, missing.importer);
1810
+ const inPackage = missing.importer === void 0 ? void 0 : packageOfFile(missing.importer, known.folder);
1811
+ if (inPackage !== void 0 && !chain.some((link) => packageOfFile(link, known.folder) === inPackage)) chain.push(inPackage);
1812
+ return failure(known, "MODULE_NOT_FOUND", error, chain, missing.specifier, {
1813
+ what: `imports "${missing.specifier}", which does not resolve`,
1814
+ hint: notFoundHint(missing.specifier, known.profile, {
1815
+ manifest: known.manifest,
1816
+ folder: known.folder
1817
+ })
1818
+ });
1819
+ }
1820
+ return failure(known, "EVALUATION_FAILED", error, importChain(cache, entryKey), void 0, {
1821
+ what: `threw ${described(error)}`,
1822
+ hint: evaluationHint(error, known.profile, basename(load.entry))
1823
+ });
1824
+ }
1825
+ /**
1826
+ * The load error for a load given up after its time: the evaluation goes on
1827
+ * — nothing stops a module's top-level code — and the process that gave up
1828
+ * on it ends rather than wait.
1829
+ *
1830
+ * @param load - What the loader knows of the load
1831
+ * @param limitMs - How long it was given
1832
+ * @returns The error
1833
+ * @example
1834
+ * ```ts
1835
+ * reject(loadTimedOut({ entry: configFile }, 60_000));
1836
+ * ```
1837
+ */
1838
+ function loadTimedOut(load, limitMs) {
1839
+ const known = knownOf(load);
1840
+ const chain = importChain(known.cache, known.entryKey);
1841
+ const where = chain.length > 1 ? ` (${shownChain(chain, known.folder)})` : "";
1842
+ return new ConfigLoadError("LOAD_TIMED_OUT", `${basename(load.entry)} was still loading after ${limitMs / 1e3} s and was given up${where}.`, {
1843
+ context: {
1844
+ entry: load.entry,
1845
+ specifier: void 0,
1846
+ importChain: chain
1847
+ },
1848
+ hint: TIMEOUT_HINT
1849
+ });
1850
+ }
1851
+ function knownOf(load) {
1852
+ const folder = dirname(load.entry);
1853
+ const detected = load.profile === void 0 ? projectFramework(folder) : void 0;
1854
+ return {
1855
+ entry: load.entry,
1856
+ folder,
1857
+ cache: load.cache ?? {},
1858
+ entryKey: load.entryKey ?? load.entry,
1859
+ profile: load.profile ?? profileOf(detected?.framework),
1860
+ manifest: load.manifest ?? detected?.manifest
1861
+ };
1862
+ }
1863
+ function failure(known, code, cause, chain, specifier, said) {
1864
+ const who = said.who ?? shownModule(chain.at(-1) ?? known.entry, known.folder);
1865
+ const where = chain.length > 1 ? ` (${shownChain(chain, known.folder)})` : "";
1866
+ return new ConfigLoadError(code, `${who} ${said.what}${where}.`, {
1867
+ context: {
1868
+ entry: known.entry,
1869
+ specifier,
1870
+ importChain: chain
1871
+ },
1872
+ hint: said.hint,
1873
+ cause: cause instanceof RefusedModuleError ? cause : maskedError(cause)
1874
+ });
1875
+ }
1876
+ // src/config/import-project-file.ts
1877
+ /**
1878
+ * Run one of the project's files — `courier.config.*`, `courier.mocks.*` —
1879
+ * with the project's loader, and hand back its module namespace. The
1880
+ * modules it runs go into a cache of this load's own, so a failure names the
1881
+ * chain of imports that led to it: the file runs as the loader's `import`
1882
+ * runs it, the loader choosing by its format whether to transform it or hand
1883
+ * it to Node.
1884
+ *
1885
+ * @param file - The file, absolute and symlink-resolved
1886
+ * @param options - How the loader resolves and transforms, what runs before the file, how long it may take, and whether its default is wanted
1887
+ * @returns The file's module namespace, or its default export, settled
1888
+ * @throws ConfigLoadError saying why the file did not load, where, and what to do
1889
+ * @example
1890
+ * ```ts
1891
+ * const namespace = await importProjectFile("/app/courier.mocks.ts");
1892
+ * ```
1893
+ */
1894
+ async function importProjectFile(file, { beforeRun, timeoutMs, default: wantsDefault, ...options } = {}) {
1895
+ const { framework, manifest } = projectFramework(dirname(file));
1896
+ const profile = options.profile ?? profileOf(framework);
1897
+ const transformFailures = /* @__PURE__ */ new Map();
1898
+ const jiti = await createConfigJiti(file, {
1899
+ ...options,
1900
+ profile,
1901
+ onTransformError: (filename, error) => {
1902
+ transformFailures.set(filename, error);
1903
+ options.onTransformError?.(filename, error);
1904
+ }
1905
+ });
1906
+ beforeRun?.();
1907
+ const cache = {};
1908
+ const load = {
1909
+ entry: file,
1910
+ cache,
1911
+ profile,
1912
+ manifest,
1913
+ transformFailures
1914
+ };
1915
+ const running = Promise.resolve(jiti.evalModule(readFileSync(file, "utf8"), {
1916
+ filename: file,
1917
+ async: true,
1918
+ cache
1919
+ })).then((namespace) => wantsDefault === true ? settledDefault(namespace, load) : namespace).catch((error) => {
1920
+ throw configLoadError(error, load);
1921
+ });
1922
+ if (timeoutMs === void 0) return running;
1923
+ return within(running, () => loadTimedOut(load, timeoutMs), timeoutMs);
1924
+ }
1925
+ /**
1926
+ * A project file's default export, as the runtime's loader hands it back
1927
+ * for `import(file, { default: true })`: the namespace's default, or the
1928
+ * namespace itself where that is nullish — a promise among them settled.
1929
+ * A promise that rejects fails the load as the file's code throwing does,
1930
+ * with `ConfigLoadError`, whose text and cause hold no value of the
1931
+ * environment. Every loader of a project's file settles its default here.
1932
+ *
1933
+ * @param namespace - The file's module namespace, as its loader evaluated it
1934
+ * @param load - What the loader knows of the load
1935
+ * @returns The default export, settled
1936
+ * @throws ConfigLoadError when the promise rejects
1937
+ * @example
1938
+ * ```ts
1939
+ * const config = await settledDefault(await loadCourierEntry(file, cache, transformCache), { entry: file });
1940
+ * ```
1941
+ */
1942
+ async function settledDefault(namespace, load) {
1943
+ const named = isObjectLike(namespace) ? Reflect.get(namespace, "default") : void 0;
1944
+ try {
1945
+ return await (named ?? namespace);
1946
+ } catch (error) {
1947
+ throw configLoadError(error, load);
1948
+ }
1949
+ }
1950
+ function isObjectLike(value) {
1951
+ return typeof value === "object" && value !== null || typeof value === "function";
1952
+ }
1953
+ function within(running, expired, limitMs) {
1954
+ let timer;
1955
+ const late = new Promise((_, reject) => {
1956
+ timer = setTimeout(() => reject(expired()), limitMs);
1957
+ });
1958
+ return Promise.race([running, late]).finally(() => clearTimeout(timer));
1959
+ }
1960
+ // src/config/untrusted-config-error.ts
1961
+ /**
1962
+ * The config nearest a folder lies in a folder above it, and someone else
1963
+ * could have put it there: it belongs to another user, or any user may
1964
+ * write it. It was not run, as it would run with the rights of whoever
1965
+ * builds or serves the project.
1966
+ *
1967
+ * @example
1968
+ * ```ts
1969
+ * throw new UntrustedConfigError("/tmp/courier.config.ts", "/tmp/app", "it belongs to uid 1001");
1970
+ * ```
1971
+ */
1972
+ var UntrustedConfigError = class extends CourierError {
1973
+ /**
1974
+ * @param configFile - The config that was not run, absolute
1975
+ * @param folder - The folder it was looked for from
1976
+ * @param reason - Why it is not trusted, worded to follow "and"
1977
+ */
1978
+ constructor(configFile, folder, reason) {
1979
+ 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.`, {
1980
+ code: "CONFIG_NOT_TRUSTED",
1981
+ context: {
1982
+ configFile,
1983
+ folder
1984
+ }
1985
+ });
1986
+ }
1987
+ };
1988
+ // src/config/assert-own-config.ts
1989
+ const OTHERS_WRITE = 2;
1990
+ /**
1991
+ * Refuse a config found in a folder above the one it was looked for from,
1992
+ * unless it belongs to this process's user or to the owner of that folder,
1993
+ * and no user outside its group may write it. A folder many users share,
1994
+ * such as `/tmp`, lets anyone put a config there, and it would run with the
1995
+ * rights of whoever builds a project below it. Windows has no owner of that
1996
+ * kind, so nothing is refused there.
1997
+ *
1998
+ * @param configFile - The config found, absolute
1999
+ * @param folder - The folder it was looked for from, absolute
2000
+ * @throws UntrustedConfigError when someone else could have put it there
2001
+ * @example
2002
+ * ```ts
2003
+ * assertOwnConfig("/repo/courier.config.ts", "/repo/apps/web");
2004
+ * ```
2005
+ */
2006
+ function assertOwnConfig(configFile, folder) {
2007
+ const uid = process.getuid?.();
2008
+ if (uid === void 0) return;
2009
+ const stats = statSync(configFile);
2010
+ const folderOwner = statSync(folder, { throwIfNoEntry: false })?.uid ?? uid;
2011
+ 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})`);
2012
+ if ((stats.mode & OTHERS_WRITE) !== 0) throw new UntrustedConfigError(configFile, folder, "any user may write it");
2013
+ }
2014
+ // src/config/transform-entry-format.ts
2015
+ /**
2016
+ * How the first line of a transform cache entry is written.
2017
+ *
2018
+ * - `prefix`: how every entry starts, whatever the version of its format, so
2019
+ * pruning can tell an entry from a file someone else put in the directory.
2020
+ * - `version`: the version this code writes; an entry in any other version
2021
+ * is ignored and replaced.
2022
+ */
2023
+ const TRANSFORM_ENTRY_FORMAT = {
2024
+ prefix: "// courier-transform ",
2025
+ version: "2"
2026
+ };
2027
+ // src/config/encode-transform-entry.ts
2028
+ /**
2029
+ * The bytes of an entry holding `code`, the transform of the input whose
2030
+ * digest is `digest`.
2031
+ *
2032
+ * The first line names the format's version, the input digest, and the byte
2033
+ * length and sha256 of the UTF-8 body that follows it, so a reader can tell
2034
+ * a complete entry from a truncated or altered one.
2035
+ *
2036
+ * @param digest - The input digest, as `transformDigest` computes it
2037
+ * @param code - The transformed code; well-formed UTF-16, so UTF-8 keeps it exactly
2038
+ * @returns The entry's bytes
2039
+ * @example
2040
+ * ```ts
2041
+ * writeFileSync(path, encodeTransformEntry(digest, result.code));
2042
+ * ```
2043
+ */
2044
+ function encodeTransformEntry(digest, code) {
2045
+ const { prefix, version } = TRANSFORM_ENTRY_FORMAT;
2046
+ const body = Buffer.from(code, "utf8");
2047
+ const bodySha256 = createHash("sha256").update(body).digest("hex");
2048
+ const header = `${prefix}${version} ${digest} ${body.length} ${bodySha256}\n`;
2049
+ return Buffer.concat([Buffer.from(header, "latin1"), body]);
2050
+ }
2051
+ // src/config/prune-transform-cache.ts
2052
+ const log$4 = createLogger("Config");
2053
+ const HOUR_MS = 36e5;
2054
+ const ENTRY_MAX_AGE_MS = 720 * HOUR_MS;
2055
+ const TEMPORARY_MAX_AGE_MS = HOUR_MS;
2056
+ const READ_FLAGS$1 = constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0);
2057
+ const PREFIX = Buffer.from(TRANSFORM_ENTRY_FORMAT.prefix, "latin1");
2058
+ function expiryOf(name) {
2059
+ if (name.endsWith(".tmp")) return {
2060
+ maxAgeMs: TEMPORARY_MAX_AGE_MS,
2061
+ prefix: null
2062
+ };
2063
+ if (name.endsWith(".js")) return {
2064
+ maxAgeMs: ENTRY_MAX_AGE_MS,
2065
+ prefix: PREFIX
2066
+ };
2067
+ return null;
2068
+ }
2069
+ function isExpired(path, { maxAgeMs, prefix }, now) {
2070
+ const fd = openSync(path, READ_FLAGS$1);
2071
+ try {
2072
+ const stats = fstatSync(fd);
2073
+ if (!stats.isFile() || now - stats.mtimeMs <= maxAgeMs) return false;
2074
+ if (prefix === null) return true;
2075
+ const start = Buffer.alloc(prefix.length);
2076
+ return readSync(fd, start, 0, prefix.length, 0) === prefix.length && start.equals(prefix);
2077
+ } finally {
2078
+ closeSync(fd);
2079
+ }
2080
+ }
2081
+ /**
2082
+ * Delete what a transform cache directory holds for nothing: entries not
2083
+ * read for 30 days, and temporary files a writer abandoned more than an hour
2084
+ * ago.
2085
+ *
2086
+ * Only regular files directly inside the directory are considered, and no
2087
+ * link is followed: a link, a subdirectory, or a `.js` file that does not
2088
+ * start like an entry is left alone, whatever its age. A file that cannot be
2089
+ * judged or deleted, such as one another process deleted first, is logged at
2090
+ * debug level and skipped.
2091
+ *
2092
+ * @param directory - The transform cache directory
2093
+ * @param now - The current time in milliseconds (default: `Date.now()`)
2094
+ * @example
2095
+ * ```ts
2096
+ * pruneTransformCache("/app/node_modules/.cache/courier/transform");
2097
+ * ```
2098
+ */
2099
+ function pruneTransformCache(directory, now = Date.now()) {
2100
+ let entries;
2101
+ try {
2102
+ entries = readdirSync(directory, { withFileTypes: true });
2103
+ } catch (error) {
2104
+ log$4.debug(`Transform cache: cannot list ${directory} to prune it`, error);
2105
+ return;
2106
+ }
2107
+ for (const entry of entries) {
2108
+ const expiry = entry.isFile() ? expiryOf(entry.name) : null;
2109
+ if (expiry === null) continue;
2110
+ const path = join(directory, entry.name);
2111
+ try {
2112
+ if (isExpired(path, expiry, now)) unlinkSync(path);
2113
+ } catch (error) {
2114
+ log$4.debug(`Transform cache: cannot prune ${path}`, error);
2115
+ }
2116
+ }
2117
+ }
2118
+ // src/config/decode-transform-entry.ts
2119
+ const HEADER = new RegExp(`^${TRANSFORM_ENTRY_FORMAT.prefix}${TRANSFORM_ENTRY_FORMAT.version} ([0-9a-f]{64}) (\\d{1,15}) ([0-9a-f]{64})$`);
2120
+ const NEWLINE = 10;
2121
+ const MAX_HEADER_BYTES = 256;
2122
+ function sha256Hex(bytes) {
2123
+ return createHash("sha256").update(bytes).digest("hex");
2124
+ }
2125
+ /**
2126
+ * Read an entry as the answer for the input whose digest is `digest`.
2127
+ *
2128
+ * @param bytes - The whole entry, as read from disk
2129
+ * @param digest - The digest of the input being transformed
2130
+ * @returns Its code when the header names that input and the body matches
2131
+ * the header's length and sha256; otherwise why it is not a hit
2132
+ * @example
2133
+ * ```ts
2134
+ * const entry = decodeTransformEntry(readFileSync(path), digest);
2135
+ * if (entry.state === "hit") return entry.code;
2136
+ * ```
2137
+ */
2138
+ function decodeTransformEntry(bytes, digest) {
2139
+ const end = bytes.subarray(0, MAX_HEADER_BYTES).indexOf(NEWLINE);
2140
+ if (end === -1) return { state: "stale" };
2141
+ const header = HEADER.exec(bytes.toString("latin1", 0, end));
2142
+ if (header === null || header[1] !== digest) return { state: "stale" };
2143
+ const body = bytes.subarray(end + 1);
2144
+ if (body.length !== Number(header[2]) || sha256Hex(body) !== header[3]) return { state: "damaged" };
2145
+ return {
2146
+ state: "hit",
2147
+ code: body.toString("utf8")
2148
+ };
2149
+ }
2150
+ // src/config/why-untrusted.ts
2151
+ const SHARED_WRITE = 18;
2152
+ /**
2153
+ * Why a path cannot be trusted to hold only what this user put there, or
2154
+ * `null` when it can.
2155
+ *
2156
+ * A symbolic link can lead anywhere, and something of another kind is not
2157
+ * what the caller expects. On POSIX, a path owned by another user, or one the
2158
+ * group or others may write, lets someone else place code that this process
2159
+ * would then run. Windows has no owner or mode bits of that kind, so only
2160
+ * the first two rules apply there.
2161
+ *
2162
+ * @param stats - The `lstat` of the path, or the `fstat` of a descriptor opened on it
2163
+ * @param kind - What the path should be
2164
+ * @param options - Whether write permission for the group and others is accepted (default: no)
2165
+ * @returns The reason, worded to follow "is", or `null`
2166
+ * @example
2167
+ * ```ts
2168
+ * const reason = whyUntrusted(lstatSync(nodeModules), "directory");
2169
+ * if (reason !== null) log.debug(`${nodeModules} is ${reason}`);
2170
+ * ```
2171
+ */
2172
+ function whyUntrusted(stats, kind, { allowSharedWrite = false } = {}) {
2173
+ if (stats.isSymbolicLink()) return "a symbolic link";
2174
+ if (!(kind === "file" ? stats.isFile() : stats.isDirectory())) return `not a ${kind}`;
2175
+ const uid = process.getuid?.();
2176
+ if (uid === void 0) return null;
2177
+ if (stats.uid !== uid) return `owned by uid ${stats.uid}, not ${uid}`;
2178
+ if (!allowSharedWrite && (stats.mode & SHARED_WRITE) !== 0) return "writable by its group or others";
2179
+ return null;
2180
+ }
2181
+ // src/config/read-transform-entry.ts
2182
+ const log$3 = createLogger("Config");
2183
+ const READ_FLAGS = constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0);
2184
+ const ABSENT_CODES = /* @__PURE__ */ new Set(["ENOENT", "ENOTDIR"]);
2185
+ const LINK_CODES = /* @__PURE__ */ new Set(["ELOOP", "EMLINK"]);
2186
+ function openEntry(path) {
2187
+ try {
2188
+ return openSync(path, READ_FLAGS);
2189
+ } catch (error) {
2190
+ const code = errnoCode(error);
2191
+ if (code !== void 0 && ABSENT_CODES.has(code)) return null;
2192
+ if (code === void 0 || !LINK_CODES.has(code)) throw error;
2193
+ log$3.debug(`Transform cache: ignoring ${path}, a symbolic link`);
2194
+ return null;
2195
+ }
2196
+ }
2197
+ function markUsed(fd, path) {
2198
+ try {
2199
+ const now = /* @__PURE__ */ new Date();
2200
+ if (process.platform === "win32") utimesSync(path, now, now);
2201
+ else futimesSync(fd, now, now);
2202
+ } catch (error) {
2203
+ log$3.debug(`Transform cache: cannot mark ${path} as used`, error);
2204
+ }
2205
+ }
2206
+ function readOpenEntry(fd, path, digest, mark) {
2207
+ const reason = whyUntrusted(fstatSync(fd), "file");
2208
+ if (reason !== null) {
2209
+ log$3.debug(`Transform cache: ignoring ${path}, ${reason}`);
2210
+ return null;
2211
+ }
2212
+ const entry = decodeTransformEntry(readFileSync(fd), digest);
2213
+ if (entry.state === "damaged") log$3.debug(`Transform cache: ignoring ${path}, whose body does not match its header`);
2214
+ if (entry.state !== "hit") return null;
2215
+ if (mark) markUsed(fd, path);
2216
+ return entry.code;
2217
+ }
2218
+ /**
2219
+ * Read the transform of one input from its entry.
2220
+ *
2221
+ * The entry is read through one descriptor, opened without blocking and, on
2222
+ * POSIX, without following a link. It is used only when it is a regular
2223
+ * file — on POSIX also owned by this user and writable by no one else — and
2224
+ * when it holds exactly that input's transform. A link, another kind of
2225
+ * file, an untrusted owner or mode, and a damaged body are logged at debug
2226
+ * level; a stale or missing entry is not. On Windows a link is followed, and
2227
+ * what it leads to is judged like the entry itself.
2228
+ *
2229
+ * @param path - The entry's path
2230
+ * @param digest - The digest of the input being transformed
2231
+ * @param mark - Whether a hit updates the entry's modification time, which
2232
+ * keeps an entry in use from being pruned
2233
+ * @returns The kept code, or `null` on a miss
2234
+ * @throws The file system's error for anything else, such as `EACCES`
2235
+ * @example
2236
+ * ```ts
2237
+ * const code = readTransformEntry(join(directory, `${name}.js`), digest, true);
2238
+ * ```
2239
+ */
2240
+ function readTransformEntry(path, digest, mark) {
2241
+ const fd = openEntry(path);
2242
+ if (fd === null) return null;
2243
+ try {
2244
+ return readOpenEntry(fd, path, digest, mark);
2245
+ } finally {
2246
+ closeSync(fd);
2247
+ }
2248
+ }
2249
+ // src/config/write-transform-entry.ts
2250
+ const log$2 = createLogger("Config");
2251
+ const PASSING_RENAME_CODES = /* @__PURE__ */ new Set([
2252
+ "EPERM",
2253
+ "EBUSY",
2254
+ "EACCES"
2255
+ ]);
2256
+ function heldOpen(error, path) {
2257
+ const code = errnoCode(error);
2258
+ if (code === void 0 || !PASSING_RENAME_CODES.has(code)) return false;
2259
+ return lstatSync(path, { throwIfNoEntry: false })?.isDirectory() !== true;
2260
+ }
2261
+ function discard(temporary) {
2262
+ try {
2263
+ unlinkSync(temporary);
2264
+ } catch (error) {
2265
+ log$2.debug(`Transform cache: cannot remove ${temporary}`, error);
2266
+ }
2267
+ }
2268
+ function writeTemporary(temporary, bytes) {
2269
+ const fd = openSync(temporary, "wx", 384);
2270
+ try {
2271
+ try {
2272
+ writeFileSync(fd, bytes);
2273
+ } finally {
2274
+ closeSync(fd);
2275
+ }
2276
+ } catch (error) {
2277
+ discard(temporary);
2278
+ throw error;
2279
+ }
2280
+ }
2281
+ /**
2282
+ * Replace the entry at `path` with `bytes`, so that a reader sees either the
2283
+ * old entry or the whole new one.
2284
+ *
2285
+ * The bytes go to a temporary file beside the entry, named after it, this
2286
+ * process and random bytes, which is then renamed over the entry. On any
2287
+ * failure the temporary file is removed. A rename refused because the entry
2288
+ * is held open (`EPERM`, `EBUSY` or `EACCES`, while no folder stands in its
2289
+ * place) skips this one write, logged at debug level.
2290
+ *
2291
+ * @param path - The entry's path, in a directory that exists
2292
+ * @param bytes - The whole entry
2293
+ * @throws The file system's error for any other failure
2294
+ * @example
2295
+ * ```ts
2296
+ * writeTransformEntry(join(directory, `${name}.js`), encodeTransformEntry(digest, code));
2297
+ * ```
2298
+ */
2299
+ function writeTransformEntry(path, bytes) {
2300
+ const temporary = `${path}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
2301
+ writeTemporary(temporary, bytes);
2302
+ try {
2303
+ renameSync(temporary, path);
2304
+ } catch (error) {
2305
+ discard(temporary);
2306
+ if (!heldOpen(error, path)) throw error;
2307
+ log$2.debug(`Transform cache: skipped writing ${path} (${errnoCode(error)})`);
2308
+ }
2309
+ }
2310
+ // src/config/transform-cache-disk.ts
2311
+ const log$1 = createLogger("Config");
2312
+ const prunedDirectories = /* @__PURE__ */ new Set();
2313
+ const markedEntries = /* @__PURE__ */ new Set();
2314
+ /**
2315
+ * Keep transforms in `directory`, where the next process can read them.
2316
+ *
2317
+ * Each entry is named after the sha256 of its key and replaced atomically,
2318
+ * so a reader sees a whole entry or none. It is read only when it is a
2319
+ * regular file — on POSIX also not a symbolic link, owned by this user and
2320
+ * writable by no one else — and when its header names the same input and its
2321
+ * body the header's length and sha256. Anything else is a miss, and the
2322
+ * write that follows replaces it. The first hit on an entry in a process
2323
+ * marks it as used, so pruning spares it.
2324
+ *
2325
+ * The first write of each cache creates the directory with mode 0700; the
2326
+ * first write in the process prunes it. A failure is never fatal: a missing
2327
+ * entry is a quiet miss; any other read error is logged once and is a miss.
2328
+ * A directory removed while the process runs, as by clearing
2329
+ * `node_modules/.cache`, is created again at the next write. A rename refused
2330
+ * because the entry is held open skips that one write; any other write error
2331
+ * is logged once, naming the directory and the code, and stops this cache's
2332
+ * writes while reads go on.
2333
+ *
2334
+ * A `readOnly` one reads entries by the same rules and changes nothing: its
2335
+ * writes are dropped, and a hit leaves the entry's time as it was, so the
2336
+ * process that keeps the entries alone decides what pruning spares.
2337
+ *
2338
+ * @param directory - Where entries are kept; see `findTransformCacheDirectory`
2339
+ * @param options - Whether the directory is only read (default: no)
2340
+ * @returns The directory's reader and writer
2341
+ * @example
2342
+ * ```ts
2343
+ * const disk = createTransformCacheDisk("/app/node_modules/.cache/courier/transform");
2344
+ * const code = disk.read(key, digest) ?? transformAndKeep();
2345
+ * ```
2346
+ */
2347
+ function createTransformCacheDisk(directory, { readOnly = false } = {}) {
2348
+ const root = resolve(directory);
2349
+ let readFailureLogged = false;
2350
+ let created = false;
2351
+ let writable = true;
2352
+ function entryPath(key) {
2353
+ return join(root, `${createHash("sha256").update(key).digest("hex").slice(0, 40)}.js`);
2354
+ }
2355
+ function warn(failure, outcome, error) {
2356
+ const code = errnoCode(error);
2357
+ const message = `Transform cache: cannot ${failure} ${root} (${code ?? "unexpected error"}); ${outcome}`;
2358
+ log$1.warn(message, code === void 0 ? error : void 0);
2359
+ }
2360
+ function stopWriting(error) {
2361
+ writable = false;
2362
+ warn("write to", "this process keeps new transforms in memory only", error);
2363
+ }
2364
+ function create() {
2365
+ mkdirSync(root, {
2366
+ recursive: true,
2367
+ mode: 448
2368
+ });
2369
+ const reason = whyUntrusted(lstatSync(root), "directory");
2370
+ if (reason !== null) {
2371
+ writable = false;
2372
+ log$1.warn(`Transform cache: ${root} is ${reason}; this process keeps new transforms in memory only`);
2373
+ return false;
2374
+ }
2375
+ created = true;
2376
+ if (prunedDirectories.has(root)) return true;
2377
+ prunedDirectories.add(root);
2378
+ pruneTransformCache(root);
2379
+ return true;
2380
+ }
2381
+ function writeEntry(path, bytes) {
2382
+ if (!created && !create()) return;
2383
+ try {
2384
+ writeTransformEntry(path, bytes);
2385
+ } catch (error) {
2386
+ if (errnoCode(error) !== "ENOENT") throw error;
2387
+ if (create()) writeTransformEntry(path, bytes);
2388
+ }
2389
+ }
2390
+ return {
2391
+ read(key, digest) {
2392
+ const path = entryPath(key);
2393
+ const mark = !readOnly && !markedEntries.has(path);
2394
+ try {
2395
+ const code = readTransformEntry(path, digest, mark);
2396
+ if (code !== null && mark) markedEntries.add(path);
2397
+ return code;
2398
+ } catch (error) {
2399
+ if (readFailureLogged) log$1.debug(`Transform cache: cannot read ${path}`, error);
2400
+ else warn("read entries in", "such modules are transformed instead", error);
2401
+ readFailureLogged = true;
2402
+ return null;
2403
+ }
2404
+ },
2405
+ write(key, digest, code) {
2406
+ if (readOnly || !writable || !code.isWellFormed()) return;
2407
+ try {
2408
+ writeEntry(entryPath(key), encodeTransformEntry(digest, code));
2409
+ } catch (error) {
2410
+ stopWriting(error);
2411
+ }
2412
+ }
2413
+ };
2414
+ }
2415
+ // src/config/transform-digest.ts
2416
+ let jitiVersion;
2417
+ function hasVersion(manifest) {
2418
+ return typeof manifest === "object" && manifest !== null && "version" in manifest && typeof manifest.version === "string";
2419
+ }
2420
+ function readJitiVersion() {
2421
+ if (jitiVersion !== void 0) return jitiVersion;
2422
+ const manifest = createRequire(import.meta.url)("jiti/package.json");
2423
+ if (!hasVersion(manifest)) throw new Error("jiti/package.json names no version");
2424
+ jitiVersion = manifest.version;
2425
+ return jitiVersion;
2426
+ }
2427
+ function isPlainData(value) {
2428
+ const prototype = Object.getPrototypeOf(value);
2429
+ if (Array.isArray(value)) return prototype === Array.prototype;
2430
+ return prototype === Object.prototype || prototype === null;
2431
+ }
2432
+ function describe(value, open) {
2433
+ if (value === void 0) return "u";
2434
+ if (value === null) return "n";
2435
+ if (typeof value === "boolean") return value ? "t" : "f";
2436
+ if (typeof value === "number") return Object.is(value, -0) ? "#-0" : `#${value}`;
2437
+ if (typeof value === "string") return JSON.stringify(value);
2438
+ if (typeof value !== "object" || open.has(value) || !isPlainData(value)) return null;
2439
+ open.add(value);
2440
+ const entries = [];
2441
+ const fields = Object.entries(value);
2442
+ for (const [key, item] of fields) {
2443
+ const described = describe(item, open);
2444
+ if (described === null) return null;
2445
+ entries.push(`${JSON.stringify(key)}:${described}`);
2446
+ }
2447
+ open.delete(value);
2448
+ const [start, end] = Array.isArray(value) ? ["[", "]"] : ["{", "}"];
2449
+ return `${start}${entries.join(",")}${end}`;
2450
+ }
2451
+ /**
2452
+ * The digest of everything that shapes jiti's transform of a module: jiti's
2453
+ * version, every option but the source, and the source.
2454
+ *
2455
+ * Two inputs share a digest only when every option is alike and the source is
2456
+ * the same string, code unit for code unit, so a source that is not
2457
+ * well-formed UTF-16 is not confused with the text it would decode to. An
2458
+ * option whose value cannot be told apart from another — a babel plugin given
2459
+ * as a function, a RegExp, a cycle — yields no digest.
2460
+ *
2461
+ * @param options - What jiti hands its transform
2462
+ * @returns A hex sha256, or `null` when an option holds such a value
2463
+ * @example
2464
+ * ```ts
2465
+ * const digest = transformDigest({ filename: "/app/courier.config.ts", source, ts: true });
2466
+ * ```
2467
+ */
2468
+ function transformDigest({ source, ...rest }) {
2469
+ const described = describe({
2470
+ ...rest,
2471
+ async: rest.async === true
2472
+ }, /* @__PURE__ */ new Set());
2473
+ if (described === null) return null;
2474
+ return createHash("sha256").update(`${readJitiVersion()}\0${described}\0`).update(source, "utf16le").digest("hex");
2475
+ }
2476
+ // src/config/transform-cache.ts
2477
+ const DEFAULT_MAX_MEMORY_CHARS = 128 * 2 ** 20;
2478
+ function keyOf({ filename, async }) {
2479
+ if (filename === void 0 || !isAbsolute(filename)) return null;
2480
+ return `${async === true}\0${filename}`;
2481
+ }
2482
+ /**
2483
+ * Create a {@link TransformCache} that keeps transforms in memory for the
2484
+ * life of the process and, given a `directory`, on disk for the next one.
2485
+ *
2486
+ * Each file holds one entry per module system — required and imported are
2487
+ * transformed differently, and a module reached both ways keeps both. An
2488
+ * entry is reused only when the digest of jiti's version, the options and
2489
+ * the source matches, so an edited file is transformed again the first time
2490
+ * it is loaded.
2491
+ *
2492
+ * A transform is looked for in memory, then in the directory, where a hit
2493
+ * is also kept in memory; only when both miss is it computed, and a success
2494
+ * is then kept in both. The directory is shared with every process that
2495
+ * names it, and a directory that cannot be read or written only costs
2496
+ * transforms; see `createTransformCacheDisk` for what an entry must be to be
2497
+ * used. A mock worker, which loads once the modules Studio has just loaded,
2498
+ * opens Studio's directory `readOnly` with `maxMemoryChars: 0`: it reads
2499
+ * what Studio kept, adds nothing to the directory, and holds nothing in
2500
+ * memory past its one load.
2501
+ *
2502
+ * A failed transform, one whose result carries an `error`, is never kept:
2503
+ * babel can fail on bytes it transforms without complaint a moment later,
2504
+ * such as a deeply nested module that overflows the stack while babel is
2505
+ * still cold. Such a file is transformed again on every load, and every
2506
+ * failure reaches jiti with its error, as it does without a cache. A file
2507
+ * named by a relative path, or not named, is transformed every time.
2508
+ *
2509
+ * @param options - The directory (default: none, memory only), the memory
2510
+ * bound (default: 128 Mi characters), and whether the directory is only
2511
+ * read (default: no)
2512
+ * @returns A cache to pass to `createConfigJiti` or `loadCourierConfig`
2513
+ * @example
2514
+ * ```ts
2515
+ * const transformCache = createTransformCache({
2516
+ * directory: findTransformCacheDirectory(projectRoot),
2517
+ * });
2518
+ * const first = await loadCourierConfig({ cwd: projectRoot, transformCache });
2519
+ * // Unchanged modules are not transformed again, here or in the next process:
2520
+ * const second = await loadCourierConfig({ cwd: projectRoot, transformCache });
2521
+ *
2522
+ * // A process that loads once what another has just kept:
2523
+ * const reader = createTransformCache({ directory, readOnly: true, maxMemoryChars: 0 });
2524
+ * ```
2525
+ */
2526
+ function createTransformCache({ directory, maxMemoryChars = DEFAULT_MAX_MEMORY_CHARS, readOnly = false } = {}) {
2527
+ const disk = directory ? createTransformCacheDisk(directory, { readOnly }) : null;
2528
+ const memo = /* @__PURE__ */ new Map();
2529
+ let memoryChars = 0;
2530
+ function forget(key) {
2531
+ const entry = memo.get(key);
2532
+ if (!entry) return;
2533
+ memo.delete(key);
2534
+ memoryChars -= entry.code.length;
2535
+ }
2536
+ function remember(key, entry) {
2537
+ forget(key);
2538
+ if (maxMemoryChars <= 0 || entry.code.length > maxMemoryChars) return;
2539
+ memo.set(key, entry);
2540
+ memoryChars += entry.code.length;
2541
+ for (const oldest of memo.keys()) {
2542
+ if (memoryChars <= maxMemoryChars) break;
2543
+ forget(oldest);
2544
+ }
2545
+ }
2546
+ return { transform(options, compute) {
2547
+ const key = keyOf(options);
2548
+ const digest = key === null ? null : transformDigest(options);
2549
+ if (key === null || digest === null) return compute(options);
2550
+ const hit = memo.get(key);
2551
+ if (hit?.digest === digest) {
2552
+ memo.delete(key);
2553
+ memo.set(key, hit);
2554
+ return { code: hit.code };
2555
+ }
2556
+ const kept = disk === null ? null : disk.read(key, digest);
2557
+ if (kept !== null) {
2558
+ remember(key, {
2559
+ digest,
2560
+ code: kept
2561
+ });
2562
+ return { code: kept };
2563
+ }
2564
+ const result = compute(options);
2565
+ if (result.error !== void 0) return result;
2566
+ remember(key, {
2567
+ digest,
2568
+ code: result.code
2569
+ });
2570
+ disk?.write(key, digest, result.code);
2571
+ return result;
2572
+ } };
2573
+ }
2574
+ // src/config/transform-cache-directory.ts
2575
+ const log = createLogger("Config");
2576
+ /** Values of `COURIER_TRANSFORM_CACHE` that turn the directory off. */
2577
+ const OFF_VALUES = /* @__PURE__ */ new Set([
2578
+ "0",
2579
+ "false",
2580
+ "off"
2581
+ ]);
2582
+ /** Values of `COURIER_TRANSFORM_CACHE` that keep it, as leaving it unset does. */
2583
+ const ON_VALUES = /* @__PURE__ */ new Set([
2584
+ "",
2585
+ "1",
2586
+ "true",
2587
+ "on"
2588
+ ]);
2589
+ const unknownSettings = /* @__PURE__ */ new Set();
2590
+ function nearestNodeModules(start) {
2591
+ let directory = start;
2592
+ while (true) {
2593
+ const candidate = join(directory, "node_modules");
2594
+ const stats = lstatSync(candidate, { throwIfNoEntry: false });
2595
+ if (stats?.isDirectory() || stats?.isSymbolicLink()) return candidate;
2596
+ const parent = dirname(directory);
2597
+ if (parent === directory) return null;
2598
+ directory = parent;
2599
+ }
2600
+ }
2601
+ function whyRefused(levels) {
2602
+ for (const [path, options] of levels) {
2603
+ const stats = lstatSync(path, { throwIfNoEntry: false });
2604
+ const reason = stats === void 0 ? null : whyUntrusted(stats, "directory", options);
2605
+ if (reason !== null) return `${path} is ${reason}`;
2606
+ }
2607
+ return null;
2608
+ }
2609
+ const TOOL_OWNED = { allowSharedWrite: true };
2610
+ const STUDIO_OWNED = {};
2611
+ const lastRefusals = /* @__PURE__ */ new Map();
2612
+ function refuse(root, reason, error) {
2613
+ const key = error instanceof Error ? `${reason}\0${error.message}` : reason;
2614
+ if (lastRefusals.get(root) === key) return null;
2615
+ lastRefusals.set(root, key);
2616
+ if (error === void 0) log.debug(`Transform cache directory off: ${reason}`);
2617
+ else log.debug(`Transform cache directory off: ${reason}`, error);
2618
+ return null;
2619
+ }
2620
+ function locate(projectRoot) {
2621
+ const nodeModules = nearestNodeModules(projectRoot);
2622
+ if (nodeModules === null) return { refusal: `no node_modules at or above ${projectRoot}` };
2623
+ const cache = join(nodeModules, ".cache");
2624
+ const courier = join(cache, "courier");
2625
+ const directory = join(courier, "transform");
2626
+ const refusal = whyRefused([
2627
+ [nodeModules, TOOL_OWNED],
2628
+ [cache, TOOL_OWNED],
2629
+ [courier, STUDIO_OWNED],
2630
+ [directory, STUDIO_OWNED]
2631
+ ]);
2632
+ return refusal === null ? { directory } : { refusal };
2633
+ }
2634
+ /**
2635
+ * Where a project's transforms are kept across processes:
2636
+ * `.cache/courier/transform` in the nearest `node_modules` directory at or
2637
+ * above the project root.
2638
+ *
2639
+ * The nearest one is taken, not the one module resolution would reach: under
2640
+ * pnpm that lies inside `.pnpm`, and what keeps the directory safe is the
2641
+ * rule below, not where it is. Every file in it holds code that Studio, mock
2642
+ * workers and build plugins run, so on POSIX the directory is refused when
2643
+ * `node_modules`, `.cache`, `courier` or `transform` — those that exist — is
2644
+ * a symbolic link or belongs to another user, and when `courier` or
2645
+ * `transform`, which only Studio creates, is writable by its group or
2646
+ * others; any of these lets someone else replace or remove entries, or put
2647
+ * the directory somewhere they control. `node_modules` and `.cache` may be
2648
+ * group-writable, as package managers leave them under a umask of 002:
2649
+ * whoever can write `node_modules` can replace any package in it already. On
2650
+ * Windows only links and other kinds of file are refused.
2651
+ *
2652
+ * There is none when no `node_modules` exists, as with Yarn Plug'n'Play, or
2653
+ * when `COURIER_TRANSFORM_CACHE` is `0`, `false` or `off` in any case. The
2654
+ * system temporary directory, which every user of the machine may write, is
2655
+ * never used instead. Why there is none is logged at debug level, once for
2656
+ * each root until the reason changes, however often the root is looked up.
2657
+ *
2658
+ * @param projectRoot - The directory holding `courier.config.*`
2659
+ * @param env - The environment to read `COURIER_TRANSFORM_CACHE` from
2660
+ * (default: `process.env`)
2661
+ * @returns The directory, which may not exist yet, or `null` for none
2662
+ * @example
2663
+ * ```ts
2664
+ * const transformCache = createTransformCache({
2665
+ * directory: findTransformCacheDirectory(projectRoot),
2666
+ * });
2667
+ * ```
2668
+ */
2669
+ function findTransformCacheDirectory(projectRoot, env = process.env) {
2670
+ const root = resolve(projectRoot);
2671
+ const setting = env.COURIER_TRANSFORM_CACHE?.trim().toLowerCase();
2672
+ if (setting !== void 0 && OFF_VALUES.has(setting)) return refuse(root, `COURIER_TRANSFORM_CACHE=${setting}`);
2673
+ if (setting !== void 0 && !ON_VALUES.has(setting) && !unknownSettings.has(setting)) {
2674
+ unknownSettings.add(setting);
2675
+ log.warn(`COURIER_TRANSFORM_CACHE=${setting} is not one of 0, false, off, 1, true or on; the transform cache stays on`);
2676
+ }
2677
+ let located;
2678
+ try {
2679
+ located = locate(root);
2680
+ } catch (error) {
2681
+ return refuse(root, `cannot inspect the directories above ${root}`, error);
2682
+ }
2683
+ if (located.refusal !== void 0) return refuse(root, located.refusal);
2684
+ lastRefusals.delete(root);
2685
+ return located.directory;
2686
+ }
2687
+ // src/config/load-config.ts
2688
+ /**
2689
+ * Unified `courier.config.{ts,mts,cts,js,mjs,cjs}` loader — the single
2690
+ * canonical way to resolve and load a user's config file.
2691
+ *
2692
+ * Two-stage design:
2693
+ *
2694
+ * 1. **File discovery** — walk up from `cwd` to the top of the project
2695
+ * looking for a `courier.config.*` file, using nothing but Node builtins.
2696
+ *
2697
+ * 2. **File loading** — `jiti` imports the file, transpiling a TypeScript
2698
+ * config transparently across Node / Next.js dev / tsx / bun / deno and
2699
+ * resolving the project's `tsconfig` path aliases.
2700
+ *
2701
+ * Every consumer resolves the config through this one code path:
2702
+ *
2703
+ * - The Studio dev server (`@noverax/courier-studio`) wraps this loader and
2704
+ * adds dev-only concerns on top (validation, source-key stamping,
2705
+ * dependency extraction for the fs watcher, rich error reporting).
2706
+ *
2707
+ * - The user-app runtime (`attachRuntime` on the server side) calls it
2708
+ * directly to discover and register sources without requiring the user
2709
+ * to import the config file manually.
2710
+ *
2711
+ * - `courier-cli`, the standalone server and the MCP server load the
2712
+ * project's config with it, and the bundler plugins read a page's routes
2713
+ * from the config it loads.
2714
+ *
2715
+ * A missing config is not an error: `null` lets the runtime caller fall back
2716
+ * to adapter auto-discovery. A config that exists and cannot be loaded is a
2717
+ * different thing entirely and throws, because reporting it as "no config"
2718
+ * sends the reader looking for a file that is right where they left it.
2719
+ */
2720
+ /**
2721
+ * Walk up from `startDir` looking for a `courier.config.*` file, up to the
2722
+ * top of the project: the folder that holds its repository or is the root
2723
+ * of its workspace (see {@link configCandidates}). A config found in a folder
2724
+ * above `startDir` is taken only when it belongs to this user or to the
2725
+ * owner of `startDir`, and no user outside its group may write it: a folder
2726
+ * many users share, such as `/tmp`, would otherwise run anyone's config.
2727
+ *
2728
+ * @param startDir - The directory to look in first
2729
+ * @returns The absolute path of the first match, or `null` when no directory
2730
+ * up to the top of the project holds one
2731
+ * @throws UntrustedConfigError when the nearest config lies above `startDir`
2732
+ * and someone else could have put it there
2733
+ * @example
2734
+ * ```ts
2735
+ * findConfigFile("/app/src"); // → "/app/courier.config.ts", or null
2736
+ * ```
2737
+ */
2738
+ function findConfigFile(startDir) {
2739
+ const folder = resolve(startDir);
2740
+ const found = configCandidates(folder).find((candidate) => existsSync(candidate));
2741
+ if (found === void 0) return null;
2742
+ if (dirname(found) !== folder) assertOwnConfig(found, folder);
2743
+ return found;
2744
+ }
2745
+ /**
2746
+ * Find a config file in `cwd` itself, without walking up.
2747
+ *
2748
+ * @param cwd - The directory to look in
2749
+ * @returns The first of {@link CONFIG_BASENAMES} present there, or `null`
2750
+ * @example
2751
+ * ```ts
2752
+ * findConfigFileAt(process.cwd()); // → "/app/courier.config.ts", or null
2753
+ * ```
2754
+ */
2755
+ function findConfigFileAt(cwd) {
2756
+ for (const basename of CONFIG_BASENAMES) {
2757
+ const candidate = resolve(cwd, basename);
2758
+ if (existsSync(candidate)) return candidate;
2759
+ }
2760
+ return null;
2761
+ }
2762
+ /**
2763
+ * Find and load a `courier.config.*` file.
2764
+ *
2765
+ * Returns `null` when no config file exists, so a caller can fall back.
2766
+ * Throws `ConfigLoadError` when a file was found and loading it failed,
2767
+ * saying why, where — the chain of imports — and what to do.
2768
+ *
2769
+ * @param options - Where to look, where to reuse transforms, and how long to wait
2770
+ * @returns The config and its canonical path, or `null` when there is no config
2771
+ * @example
2772
+ * ```ts
2773
+ * const loaded = await loadCourierConfig({ cwd: projectRoot, requireAtCwd: true });
2774
+ * ```
2775
+ */
2776
+ async function loadCourierConfig(options = {}) {
2777
+ const startDir = resolve(options.cwd ?? process.cwd());
2778
+ const found = options.requireAtCwd ? findConfigFileAt(startDir) : findConfigFile(startDir);
2779
+ if (!found) return null;
2780
+ return loadCourierConfigFile(found, {
2781
+ transformCache: options.transformCache ?? (options.projectTransforms === true ? projectTransformCache(dirname(found)) : void 0),
2782
+ onModule: options.onModule,
2783
+ timeoutMs: options.timeoutMs
2784
+ });
2785
+ }
2786
+ /**
2787
+ * The transforms kept in a project's `node_modules/.cache` — the ones Studio
2788
+ * keeps — for a process that loads the project's modules once: it reuses
2789
+ * them, keeps there the ones it makes, and holds none in memory.
2790
+ *
2791
+ * @param projectRoot - The project's root
2792
+ * @returns The cache, or `undefined` when the project has no directory to keep them in
2793
+ * @example
2794
+ * ```ts
2795
+ * await loadCourierConfigFile(file, { transformCache: projectTransformCache(dirname(file)) });
2796
+ * ```
2797
+ */
2798
+ function projectTransformCache(projectRoot) {
2799
+ const directory = findTransformCacheDirectory(projectRoot);
2800
+ return directory === null ? void 0 : createTransformCache({
2801
+ directory,
2802
+ maxMemoryChars: 0
2803
+ });
2804
+ }
2805
+ /**
2806
+ * Load one config file, found by whatever means the caller chose — the
2807
+ * standalone server takes it from `--config`.
2808
+ *
2809
+ * @param file - Absolute path of an existing config file
2810
+ * @param options - How the loader resolves imports and where it reuses
2811
+ * transforms, see {@link ConfigJitiOptions}, what runs before the config,
2812
+ * and how long it may take
2813
+ * @returns The config and the canonical path it was loaded from
2814
+ * @throws ConfigLoadError when the file will not load: an import that does
2815
+ * not resolve or that the framework refuses, a module that does not
2816
+ * compile, or code that throws
2817
+ * @example
2818
+ * ```ts
2819
+ * const { config } = await loadCourierConfigFile("/app/courier.config.ts", {
2820
+ * transformCache: createTransformCache(),
2821
+ * });
2822
+ * ```
2823
+ */
2824
+ async function loadCourierConfigFile(file, options = {}) {
2825
+ const configFile = realpathSync(file);
2826
+ return {
2827
+ config: await importProjectFile(configFile, {
2828
+ ...options,
2829
+ default: true
2830
+ }),
2831
+ configFile
2832
+ };
2833
+ }
2834
+ export { projectFramework as C, detectFramework as S, configCandidates as T, createConfigJiti as _, projectTransformCache as a, readPathAliases as b, UntrustedConfigError as c, configLoadError as d, loadTimedOut as f, ConfigLoadError as g, localPath as h, loadCourierConfigFile as i, importProjectFile as l, withoutSecrets as m, findConfigFileAt as n, findTransformCacheDirectory as o, maskedError as p, loadCourierConfig as r, createTransformCache as s, findConfigFile as t, settledDefault as u, profileOf as v, CONFIG_BASENAMES as w, FRAMEWORK_MARKERS as x, RefusedModuleError as y };