@noverax/courier-build 0.0.0-stage → 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.
- package/LICENSE +21 -0
- package/README.md +295 -2
- package/lib/config/index.d.ts +811 -0
- package/lib/config/index.js +75 -0
- package/lib/config-file-of-Clf_dWUQ.js +28 -0
- package/lib/config-in-page-message-DLd3f0Jd.js +20 -0
- package/lib/courier-routes-options-BYF7Sfdf.d.cts +13 -0
- package/lib/courier-routes-options-BYF7Sfdf.d.ts +13 -0
- package/lib/engine/worker.d.ts +1 -0
- package/lib/engine/worker.js +548 -0
- package/lib/errno-code-CNx_E80R.js +32 -0
- package/lib/esbuild/index.d.ts +58 -0
- package/lib/esbuild/index.js +191 -0
- package/lib/index-_VUU5N5v.d.ts +134 -0
- package/lib/index.d.ts +101 -0
- package/lib/index.js +4 -0
- package/lib/jest/routes-thread.d.ts +1 -0
- package/lib/jest/routes-thread.js +46 -0
- package/lib/jest/transformer.cjs +121 -0
- package/lib/jest/transformer.d.cts +39 -0
- package/lib/load-config-barNPfXN.js +2834 -0
- package/lib/metro/index.cjs +25 -0
- package/lib/metro/index.d.cts +55 -0
- package/lib/metro/index.d.ts +67 -0
- package/lib/metro/index.js +105 -0
- package/lib/package-file-CVAKY_Nu.js +39 -0
- package/lib/read-limits-DUuUaEQM.js +41 -0
- package/lib/rollup/index.d.ts +51 -0
- package/lib/rollup/index.js +95 -0
- package/lib/routes-engine-BfwrCuNL.d.ts +117 -0
- package/lib/routes-file-DRoqUZ0S.js +206 -0
- package/lib/routes-loader-context-40OKDrQp.d.ts +26 -0
- package/lib/routes-module-code-DYtP7uj1.js +405 -0
- package/lib/routes-module-id-CnUjQjYF.js +12 -0
- package/lib/routes-read-error-DQhKIE2F.js +30 -0
- package/lib/routes-rule-Pu1Tz8Xm.js +108 -0
- package/lib/rsbuild/index.d.ts +41 -0
- package/lib/rsbuild/index.js +37 -0
- package/lib/rspack/index.d.ts +3 -0
- package/lib/rspack/index.js +1 -0
- package/lib/shared-engine-D0j7uXg8.js +28 -0
- package/lib/vite/index.d.ts +33 -0
- package/lib/vite/index.js +357 -0
- package/lib/webpack/index.d.ts +4 -0
- package/lib/webpack/index.js +3 -0
- package/lib/webpack/loader.cjs +31 -0
- package/lib/webpack/loader.d.cts +40 -0
- package/lib/webpack/run-routes-loader.d.ts +22 -0
- package/lib/webpack/run-routes-loader.js +103 -0
- package/lib/webpack-BPxhBYGV.js +42 -0
- package/lib/worker-messages-BT2vvNQJ.js +35 -0
- package/package.json +209 -4
|
@@ -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 };
|