@noverax/courier-build 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -89,8 +89,8 @@ declare module "@noverax/courier/routes" {
89
89
 
90
90
  A page then takes the routes of the API it calls, and its client reaches exactly the endpoints that API opens to a page:
91
91
 
92
- ```ts
93
- import { createProxyCaller } from "@noverax/courier"
92
+ ```ts side="page"
93
+ import { createProxyCaller } from "@noverax/courier/client"
94
94
  import { routes } from "@noverax/courier/routes"
95
95
 
96
96
  export const shop = createProxyCaller(routes.shop)
@@ -103,6 +103,7 @@ export const shop = createProxyCaller(routes.shop)
103
103
  - **The config** is the `courier.config.*` in Vite's root or the nearest folder above it, up to the top of the project: the folder that holds its repository (`.git`, `.hg`, `.jj`) or is the root of its workspace (`pnpm-workspace.yaml`, or a `package.json` with `workspaces`). One found above the root runs only when it belongs to you or to the owner of the root, and no user outside its group may write it; otherwise the routes are not read, and the error says why. Name another config with the `config` option.
104
104
  - **Env files** are those Vite reads for the mode — `.env.[mode].local`, `.env.[mode]`, `.env.local`, `.env`, the first to set a variable winning — with the values Vite gives them, a reference such as `${HOST}` or `${FLAG:-1}` filled in; a variable the process was started with wins over all. An endpoint opened by a variable is opened as the build sees it. The config reads them from `process.env`: `import.meta.env.MODE`, `DEV` and the other values Vite builds into a page are not set for it.
105
105
  - **A build** reads the routes once for every environment, and fails with what the config threw; a build that watches builds again once what failed it is mended. **The dev server** reads them again whenever something they came from changes: the config, a module it imports, a file it reads or a folder it lists, an env file, a variable it looked at, or a config saved where one is looked for, Vite's root or a folder above it. A change saved while the routes are read is read too. A config that breaks leaves the routes it read last in force, says why in the terminal and over the page, and is followed until it is mended.
106
+ - **The framework's own modules** are answered as its server answers them, by the framework the project's package.json lists: Next's `server-only` is an empty module and `client-only` is refused, SvelteKit's `$env/*` and `$app/environment` and Astro's `astro:env/*` come from the environment — see [How Courier reads your project](https://courier.noverax.cloud/docs/reading-your-project).
106
107
  - **The config in a page** fails the build: imported as a value it would ship the server's settings and every API's definition to the browser. Import its type alone, with `import type`.
107
108
  - **Vitest** runs tests through Vite with the plugin: a test gets the routes under Node and under jsdom alike, may import the config itself, and in watch mode an edit reruns the tests that import the routes, with the new ones. Vitest 3.1 and later tell the plugin they run it; under Vitest 3.0 a test that imports the config as a value in a browser environment is refused as a page would be.
108
109
  - **Definitions in a page** are not refused: a module of definitions a page imports as a value goes to the browser whole, with each API's address, its paths and middleware, and its endpoints closed to pages. Keep them out of a page's modules, or import their types alone; the routes are all a page needs to call through the proxy.
@@ -146,7 +147,7 @@ Next.js takes `withCourier()` from `@noverax/courier-next/plugin`, under Turbopa
146
147
 
147
148
  ## esbuild
148
149
 
149
- The same routes, under esbuild: the plugin from `@noverax/courier-build/esbuild` builds them in place of `@noverax/courier/routes`, and a bundle for any `platform` but `node` — a page's — fails where it takes the config itself.
150
+ The same routes, under esbuild: the plugin from `@noverax/courier-build/esbuild` builds them in place of `@noverax/courier/routes`, and a bundle for any `platform` but `node` — a page's — fails where it takes the config itself. A page's bundle takes the core's modules for a page with `platform: "browser"`, esbuild's default; `"neutral"` sets no condition, and a page built so takes the core's server modules.
150
151
 
151
152
  ```ts
152
153
  import { build } from "esbuild"
@@ -163,7 +164,7 @@ await build({ entryPoints: ["src/main.ts"], bundle: true, outdir: "dist", plugin
163
164
 
164
165
  ## Rollup and Rolldown
165
166
 
166
- Rollup and Rolldown take the same plugin, from `@noverax/courier-build/rollup`. Neither tells a page's bundle from a server's, so a bundle is taken for a page's, which fails where it takes the config itself; a server's, which may import it, says so:
167
+ Rollup and Rolldown take the same plugin, from `@noverax/courier-build/rollup`. Neither tells a page's bundle from a server's, so a bundle is taken for a page's, which fails where it takes the config itself; a server's, which may import it, says so. A page's bundle takes the core's modules for a page by the `browser` condition, which Rollup's `@rollup/plugin-node-resolve` sets with `nodeResolve({ browser: true })`:
167
168
 
168
169
  ```js
169
170
  // rollup.config.js
@@ -248,6 +249,11 @@ export default {
248
249
  | `createTransformCache(options?)` | `/config` | Reuse each module's transform while its bytes are unchanged, in memory and in that directory |
249
250
  | `projectTransformCache(projectRoot)` | `/config` | The transforms kept in a project's `node_modules/.cache`, for a process that loads its modules once |
250
251
  | `readPathAliases(projectRoot)` | `/config` | The tsconfig `paths` the loader resolves imports through |
252
+ | `projectFramework(folder)` / `detectFramework(listed)` / `FRAMEWORK_MARKERS` | `/config` | The framework a project is built with, told by the packages the nearest package.json lists |
253
+ | `profileOf(framework)` / `RefusedModuleError` | `/config` | How the framework's server answers the imports it answers itself, and the error for one it refuses where definitions run |
254
+ | `importProjectFile(file, options?)` / `ConfigLoadError` / `configLoadError(error, load)` | `/config` | Run one of the project's files with the project's loader, and why one did not load: its `code`, the `hint`, and `context.importChain` from the entry to the module that failed, no variable's value in any of them |
255
+ | `COURIER_PHASE` / `DEFINITIONS_PHASE` / `enterDefinitionsPhase(env?)` | `/config` | The variable a project's code reads to tell that Courier reads its definitions, its value then, and how a process says so before the project's code runs |
256
+ | `CONFIG_READ_LIMIT_MS` | `/config` | How long one reading of a config may take, 60 s, before `courier-cli routes` and the routes plugin give it up |
251
257
  | `unknownConfigKeys(config)` | `/config` | The keys of a config Courier does not read, which are usually typos |
252
258
 
253
259
  <br />
@@ -1,5 +1,5 @@
1
1
  import { CourierError } from "@noverax/courier";
2
- import { Jiti, TransformOptions, TransformResult } from "jiti";
2
+ import { Jiti, ModuleCache, TransformOptions, TransformResult } from "jiti";
3
3
  import { CourierConfig } from "@noverax/courier/config";
4
4
  /** The file names a Courier config may have, in the order they are looked for */
5
5
  export declare const CONFIG_BASENAMES: ReadonlyArray<string>;
@@ -19,6 +19,107 @@ export declare const CONFIG_BASENAMES: ReadonlyArray<string>;
19
19
  * ```
20
20
  */
21
21
  export declare function configCandidates(startDir: string): string[];
22
+ /** The frameworks Courier tells apart. */
23
+ type FrameworkId = "next" | "nuxt" | "sveltekit" | "astro" | "react-router" | "remix" | "solid-start" | "expo" | "nestjs" | "fastify" | "hono" | "express" | "koa" | "hapi" | "elysia" | "h3" | "nitro" | "react" | "vue";
24
+ /** A framework, and the packages a project that uses it lists. */
25
+ interface FrameworkMarker {
26
+ readonly id: FrameworkId;
27
+ /** The framework as its users name it. */
28
+ readonly label: string;
29
+ /** The package whose presence tells the framework, and whose version is the framework's. */
30
+ readonly marker: string;
31
+ /** Another package the project must list too. */
32
+ readonly also?: string;
33
+ /**
34
+ * Whether the marker tells the framework first as a dependency the app
35
+ * runs with: Courier's setup installs Hono for Studio's mock as a dev
36
+ * dependency, in an app built with another framework. Listed only among
37
+ * the dev dependencies, it still tells the framework where no other is
38
+ * listed, as for a worker a bundler builds.
39
+ */
40
+ readonly runsWith?: boolean;
41
+ /**
42
+ * Whether it runs a server alone, which a package may list beside the
43
+ * framework its page is built with: such a package is told by its
44
+ * server, which comes before the page's in the list, and is not a
45
+ * page's alone. The same servers as the upgrade's, in create-courier.
46
+ */
47
+ readonly server?: boolean;
48
+ }
49
+ /** The frameworks, the ones built on others first: a Next.js app lists React too; a server before a page Vite builds. */
50
+ export declare const FRAMEWORK_MARKERS: readonly FrameworkMarker[];
51
+ /** The packages a package.json lists, by the field it lists them in. */
52
+ interface ListedPackages {
53
+ readonly dependencies: Readonly<Record<string, unknown>>;
54
+ readonly devDependencies: Readonly<Record<string, unknown>>;
55
+ readonly peerDependencies: Readonly<Record<string, unknown>>;
56
+ readonly optionalDependencies: Readonly<Record<string, unknown>>;
57
+ }
58
+ /** The framework a project is built with, and the package.json that says so. */
59
+ interface ProjectFramework {
60
+ /** The framework, or `undefined` when the package.json lists none Courier tells. */
61
+ readonly framework: FrameworkMarker | undefined;
62
+ /** The package.json read, absolute; `undefined` when there is none up to the root of the disk. */
63
+ readonly manifest: string | undefined;
64
+ }
65
+ /**
66
+ * The framework a project is built with, told by the packages it lists: a
67
+ * framework told first by what the app runs with counts among the dev
68
+ * dependencies only where no other framework is listed.
69
+ *
70
+ * @param listed - The packages the project's package.json lists
71
+ * @returns The framework, or `undefined` when it lists none Courier tells
72
+ * @example
73
+ * ```ts
74
+ * detectFramework({ dependencies: { next: "^16.0.0", react: "^19.0.0" }, … })?.label; // "Next.js"
75
+ * ```
76
+ */
77
+ export declare function detectFramework(listed: ListedPackages): FrameworkMarker | undefined;
78
+ /**
79
+ * The framework of the project a folder belongs to, told by the nearest
80
+ * package.json at or above it. A package.json that does not parse lists
81
+ * nothing: the project is then read as plain Node, as it would be with none.
82
+ *
83
+ * @param folder - A folder of the project, absolute: the config's own
84
+ * @returns The framework, and the package.json that told it
85
+ * @example
86
+ * ```ts
87
+ * projectFramework("/app").framework?.id; // "sveltekit", where /app/package.json lists @sveltejs/kit
88
+ * ```
89
+ */
90
+ export declare function projectFramework(folder: string): ProjectFramework;
91
+ /** What a module of the project gets for an import its framework's server answers itself. */
92
+ type ModuleRule = {
93
+ readonly kind: "empty";
94
+ } | {
95
+ readonly kind: "provided";
96
+ readonly exports: () => object;
97
+ } | {
98
+ readonly kind: "refused";
99
+ readonly reason: string;
100
+ readonly remedy: string;
101
+ };
102
+ /** What a framework says of an import or a name of its own that the load could not find. */
103
+ interface Diagnosis {
104
+ /** What the import or the name is, worded to follow it: `is a module of SvelteKit's runtime, …`. */
105
+ readonly reason: string;
106
+ /** What to do about it, worded as an instruction. */
107
+ readonly remedy: string;
108
+ }
109
+ /** What a framework's server answers outside its build, and what it says of what it does not. */
110
+ interface FrameworkRules {
111
+ /** The imports the framework's server answers itself, by specifier. */
112
+ readonly modules: ReadonlyMap<string, ModuleRule>;
113
+ /** What the framework says of an import of its own that does not resolve outside its build. */
114
+ readonly unresolved?: ((specifier: string) => Diagnosis | undefined) | undefined;
115
+ /** What the framework says of a name its build provides where the code imports nothing. */
116
+ readonly undefinedName?: ((name: string) => Diagnosis | undefined) | undefined;
117
+ }
118
+ /** How a framework's server answers imports outside its build. */
119
+ interface ResolutionProfile extends FrameworkRules {
120
+ /** The framework; `undefined` for a project read as plain Node. */
121
+ readonly framework: FrameworkMarker | undefined;
122
+ }
22
123
  /**
23
124
  * A store of jiti transforms: TypeScript and ESM sources compiled to the
24
125
  * CommonJS jiti evaluates, reused while the bytes and options that produced
@@ -132,12 +233,38 @@ interface ConfigJitiOptions {
132
233
  * unnoted: what the transformer reads is its own, not the config's.
133
234
  */
134
235
  runTransform?: (<TResult>(transform: () => TResult) => TResult) | undefined;
236
+ /**
237
+ * How the project's framework answers the imports its server answers
238
+ * itself — Next's `server-only`, SvelteKit's `$env/static/private`; see
239
+ * `ResolutionProfile`. Left out, the framework the nearest package.json to
240
+ * the file lists.
241
+ */
242
+ profile?: ResolutionProfile | undefined;
243
+ /**
244
+ * Told of each module whose transform failed — its syntax — with what the
245
+ * transformer said, so whoever reports the load's failure can say which
246
+ * module would not compile.
247
+ */
248
+ onTransformError?: ((filename: string, error: unknown) => void) | undefined;
135
249
  }
136
250
  /**
137
251
  * The module loader for Courier's project files — `courier.config.*` and
138
252
  * `courier.mocks.*` — with the options both need: the project's tsconfig path
139
- * aliases, default-export interop, no module cache, and no native import
140
- * tried before the loader's own.
253
+ * aliases, the imports its framework's server answers itself, default-export
254
+ * interop, no module cache, and no native import tried before the loader's
255
+ * own.
256
+ *
257
+ * A framework resolves some imports where it builds the server, never
258
+ * through Node: Next makes `server-only` an empty module on its server
259
+ * layers, SvelteKit and Astro write their env modules from the environment.
260
+ * The loader answers them as that server does, by the framework the
261
+ * project's package.json lists (see `profileOf`), in every module it
262
+ * transforms; one the project's own path aliases name is theirs. A package
263
+ * the loader hands to Node — an installed library — resolves its own imports
264
+ * as Node does while that works; where it fails — a library that imports
265
+ * `server-only` itself — the loader runs the package itself, and answers
266
+ * them there too. A package Node loads without a failure keeps Node's
267
+ * answers: one that imports an installed `client-only` is not refused.
141
268
  *
142
269
  * The module cache is off because this is also the reload path: the dev
143
270
  * server loads the files again on every edit, and a cached module would hand
@@ -171,7 +298,8 @@ interface ConfigJitiOptions {
171
298
  * @param file - Absolute, symlink-resolved path of the file to load; modules
172
299
  * and aliases resolve from its directory
173
300
  * @param options - Whether to resolve through the project's path aliases
174
- * (default: yes), and where to reuse transforms (default: nowhere)
301
+ * (default: yes), where to reuse transforms (default: nowhere), and how the
302
+ * framework answers its own imports (default: as package.json tells it)
175
303
  * @returns A jiti instance for that file
176
304
  * @example
177
305
  * ```ts
@@ -180,7 +308,182 @@ interface ConfigJitiOptions {
180
308
  * const config = await jiti.import(configFile, { default: true });
181
309
  * ```
182
310
  */
183
- export declare function createConfigJiti(file: string, { aliases, transformCache, onModule, runTransform }?: ConfigJitiOptions): Promise<Jiti>;
311
+ export declare function createConfigJiti(file: string, { aliases, transformCache, onModule, runTransform, profile, onTransformError }?: ConfigJitiOptions): Promise<Jiti>;
312
+ /**
313
+ * The signal a project's code reads to tell that Courier is reading its
314
+ * definitions rather than serving requests: `COURIER_PHASE` holds
315
+ * `definitions` in every process Courier starts to read them — the
316
+ * `courier-cli routes` command, `courier-mcp --list`, Studio, the process a
317
+ * bundler plugin reads the routes in — after the fashion of `NEXT_PHASE`.
318
+ * Code that checks the environment as it is imported can skip the check
319
+ * then, as `@t3-oss/env-core` does with `skipValidation`; Courier guesses
320
+ * nothing for it. Where the application runs — `courier-server`,
321
+ * `courier-mcp` serving an agent, `attachRuntime()`, the app itself — the
322
+ * variable is not set.
323
+ */
324
+ /** The variable a project's code reads the phase from. */
325
+ export declare const COURIER_PHASE = "COURIER_PHASE";
326
+ /** Its value while Courier reads the project's definitions. */
327
+ export declare const DEFINITIONS_PHASE = "definitions";
328
+ /**
329
+ * Mark the process as one that reads the project's definitions, before the
330
+ * project's code runs in it.
331
+ *
332
+ * @param env - The environment to mark (default: the process's)
333
+ * @example
334
+ * ```ts
335
+ * enterDefinitionsPhase();
336
+ * process.env.COURIER_PHASE; // "definitions"
337
+ * ```
338
+ */
339
+ export declare function enterDefinitionsPhase(env?: NodeJS.ProcessEnv): void;
340
+ /** Why a project's config — or its mocks file — did not load. */
341
+ type ConfigLoadErrorCode = "MODULE_NOT_FOUND" | "MODULE_REFUSED" | "TRANSFORM_FAILED" | "EVALUATION_FAILED" | "LOAD_TIMED_OUT";
342
+ /** Where a load failed. */
343
+ interface ConfigLoadErrorContext extends Record<string, unknown> {
344
+ /** The file loaded, absolute: `courier.config.*` or `courier.mocks.*`. */
345
+ readonly entry: string;
346
+ /** The import that did not resolve or was refused, as written. */
347
+ readonly specifier: string | undefined;
348
+ /**
349
+ * The modules from the entry to the one that failed, the entry first: each
350
+ * of the project's by its absolute path, as the system names it — with `\`
351
+ * on Windows — and an installed package by its name.
352
+ */
353
+ readonly importChain: readonly string[];
354
+ }
355
+ /**
356
+ * A project's config, or its mocks file, did not load. The message says
357
+ * what failed and in which module, with the chain of imports that led there
358
+ * from the entry — `src/api.ts imports "server-only", which does not resolve
359
+ * (courier.config.ts → src/api.ts).` — in one line; `hint` says what to do,
360
+ * in another. Neither holds a secret: the project's own message is quoted
361
+ * with the environment's values masked and every credential `redact()`
362
+ * finds cut, each as `[redacted]`. What the project threw is the `cause`,
363
+ * for a log at debug level, cut the same way in its message and its stack.
364
+ *
365
+ * @example
366
+ * ```ts
367
+ * if (error instanceof ConfigLoadError) log.error(`${error.message}\n${error.hint}`);
368
+ * ```
369
+ */
370
+ export declare class ConfigLoadError extends CourierError<ConfigLoadErrorCode> {
371
+ readonly context: ConfigLoadErrorContext;
372
+ /** What to do about it, in a sentence or two. */
373
+ readonly hint: string;
374
+ /**
375
+ * @param code - Why the load failed
376
+ * @param message - What failed and where, in one line
377
+ * @param details - Where it failed, what to do, and what the project threw
378
+ */
379
+ constructor(code: ConfigLoadErrorCode, message: string, details: {
380
+ context: ConfigLoadErrorContext;
381
+ hint: string;
382
+ cause?: unknown;
383
+ });
384
+ }
385
+ /** What a loader knows of a load that failed. */
386
+ interface FailedLoad {
387
+ /** The file loaded, absolute: `courier.config.*` or `courier.mocks.*`. */
388
+ readonly entry: string;
389
+ /** The load's module cache, which tells the chain of imports; none, and the chain is the entry. */
390
+ readonly cache?: ModuleCache | undefined;
391
+ /** The entry's key in the cache, when the loader registered it under another than its path. */
392
+ readonly entryKey?: string | undefined;
393
+ /** The framework Courier read the project as; left out, the one the entry's package.json lists. */
394
+ readonly profile?: ResolutionProfile | undefined;
395
+ /** The package.json that told the framework, absolute. */
396
+ readonly manifest?: string | undefined;
397
+ /** Each module whose transform failed in the load, by file, with what the transformer said. */
398
+ readonly transformFailures?: ReadonlyMap<string, unknown> | undefined;
399
+ }
400
+ /**
401
+ * The load error that says why a load failed, what to do, and where — the
402
+ * chain of imports from the entry to the module that failed.
403
+ *
404
+ * @param error - What the load threw
405
+ * @param load - What the loader knows of the load
406
+ * @returns The error, typed: an error that already is one is handed back
407
+ * @example
408
+ * ```ts
409
+ * try { await jiti.evalModule(source, { filename: file, cache }); }
410
+ * catch (error) { throw configLoadError(error, { entry: file, cache }); }
411
+ * ```
412
+ */
413
+ export declare function configLoadError(error: unknown, load: FailedLoad): ConfigLoadError;
414
+ /**
415
+ * The load error for a load given up after its time: the evaluation goes on
416
+ * — nothing stops a module's top-level code — and the process that gave up
417
+ * on it ends rather than wait.
418
+ *
419
+ * @param load - What the loader knows of the load
420
+ * @param limitMs - How long it was given
421
+ * @returns The error
422
+ * @example
423
+ * ```ts
424
+ * reject(loadTimedOut({ entry: configFile }, 60_000));
425
+ * ```
426
+ */
427
+ export declare function loadTimedOut(load: FailedLoad, limitMs: number): ConfigLoadError;
428
+ /** How one of the project's files is run, what runs just before its code does, and how long it may take. */
429
+ interface ImportProjectFileOptions extends ConfigJitiOptions {
430
+ /**
431
+ * Called once the loader is made, just before the file's code runs. A
432
+ * caller noting what that code reads starts here, so the loader's own
433
+ * start-up is not taken for the file's.
434
+ */
435
+ beforeRun?: (() => void) | undefined;
436
+ /**
437
+ * How long the file may take to run before it is given up with
438
+ * `ConfigLoadError` `LOAD_TIMED_OUT`, in milliseconds. Its code runs on —
439
+ * nothing stops a module's top-level code — so a process that gives up
440
+ * should end. Left out, it is waited for however long it takes.
441
+ */
442
+ timeoutMs?: number | undefined;
443
+ /**
444
+ * Hand back the file's default export rather than its namespace, as the
445
+ * runtime's loader does for `import(file, { default: true })`: see
446
+ * {@link settledDefault}. A promise it exports is waited for within the
447
+ * load's time.
448
+ */
449
+ default?: boolean | undefined;
450
+ }
451
+ /**
452
+ * Run one of the project's files — `courier.config.*`, `courier.mocks.*` —
453
+ * with the project's loader, and hand back its module namespace. The
454
+ * modules it runs go into a cache of this load's own, so a failure names the
455
+ * chain of imports that led to it: the file runs as the loader's `import`
456
+ * runs it, the loader choosing by its format whether to transform it or hand
457
+ * it to Node.
458
+ *
459
+ * @param file - The file, absolute and symlink-resolved
460
+ * @param options - How the loader resolves and transforms, what runs before the file, how long it may take, and whether its default is wanted
461
+ * @returns The file's module namespace, or its default export, settled
462
+ * @throws ConfigLoadError saying why the file did not load, where, and what to do
463
+ * @example
464
+ * ```ts
465
+ * const namespace = await importProjectFile("/app/courier.mocks.ts");
466
+ * ```
467
+ */
468
+ export declare function importProjectFile(file: string, { beforeRun, timeoutMs, default: wantsDefault, ...options }?: ImportProjectFileOptions): Promise<unknown>;
469
+ /**
470
+ * A project file's default export, as the runtime's loader hands it back
471
+ * for `import(file, { default: true })`: the namespace's default, or the
472
+ * namespace itself where that is nullish — a promise among them settled.
473
+ * A promise that rejects fails the load as the file's code throwing does,
474
+ * with `ConfigLoadError`, whose text and cause hold no value of the
475
+ * environment. Every loader of a project's file settles its default here.
476
+ *
477
+ * @param namespace - The file's module namespace, as its loader evaluated it
478
+ * @param load - What the loader knows of the load
479
+ * @returns The default export, settled
480
+ * @throws ConfigLoadError when the promise rejects
481
+ * @example
482
+ * ```ts
483
+ * const config = await settledDefault(await loadCourierEntry(file, cache, transformCache), { entry: file });
484
+ * ```
485
+ */
486
+ export declare function settledDefault(namespace: unknown, load: FailedLoad): Promise<unknown>;
184
487
  /**
185
488
  * Result of a successful `loadCourierConfig` call.
186
489
  *
@@ -231,6 +534,13 @@ interface LoadCourierConfigOptions {
231
534
  * of modules, most of a second. Passed over when `transformCache` is given.
232
535
  */
233
536
  projectTransforms?: boolean | undefined;
537
+ /**
538
+ * How long the load may take before it is given up with `ConfigLoadError`
539
+ * `LOAD_TIMED_OUT`, in milliseconds. The config's code runs on — nothing
540
+ * stops a module's top-level code — so a process that gives up should
541
+ * end. Left out, the load is waited for however long it takes.
542
+ */
543
+ timeoutMs?: number | undefined;
234
544
  }
235
545
  /**
236
546
  * Walk up from `startDir` looking for a `courier.config.*` file, up to the
@@ -266,10 +576,10 @@ export declare function findConfigFileAt(cwd: string): string | null;
266
576
  * Find and load a `courier.config.*` file.
267
577
  *
268
578
  * Returns `null` when no config file exists, so a caller can fall back.
269
- * Throws when a file was found and loading it failed, reporting what the
270
- * loader reported — for a TypeScript config, usually an unresolvable import.
579
+ * Throws `ConfigLoadError` when a file was found and loading it failed,
580
+ * saying why, where — the chain of imports — and what to do.
271
581
  *
272
- * @param options - Where to look, and where to reuse transforms
582
+ * @param options - Where to look, where to reuse transforms, and how long to wait
273
583
  * @returns The config and its canonical path, or `null` when there is no config
274
584
  * @example
275
585
  * ```ts
@@ -290,25 +600,23 @@ export declare function loadCourierConfig(options?: LoadCourierConfigOptions): P
290
600
  * ```
291
601
  */
292
602
  export declare function projectTransformCache(projectRoot: string): TransformCache | undefined;
293
- /** How one config file is loaded: the loader's options, and when its code is about to run. */
294
- interface LoadConfigFileOptions extends ConfigJitiOptions {
295
- /**
296
- * Called once the loader is made, just before the config's code runs. A
297
- * caller noting what that code reads starts here, so the loader's own
298
- * start-up is not taken for the config's.
299
- */
300
- beforeRun?: (() => void) | undefined;
301
- }
603
+ /**
604
+ * How one config file is loaded: the loader's options, what runs just before
605
+ * the config's code does, and how long it may take; see `importProjectFile`.
606
+ */
607
+ interface LoadConfigFileOptions extends Omit<ImportProjectFileOptions, "default"> {}
302
608
  /**
303
609
  * Load one config file, found by whatever means the caller chose — the
304
610
  * standalone server takes it from `--config`.
305
611
  *
306
612
  * @param file - Absolute path of an existing config file
307
613
  * @param options - How the loader resolves imports and where it reuses
308
- * transforms, see {@link ConfigJitiOptions}, and what runs before the config
614
+ * transforms, see {@link ConfigJitiOptions}, what runs before the config,
615
+ * and how long it may take
309
616
  * @returns The config and the canonical path it was loaded from
310
- * @throws What the loader reported when the file will not load — for a
311
- * TypeScript config, usually an unresolvable import
617
+ * @throws ConfigLoadError when the file will not load: an import that does
618
+ * not resolve or that the framework refuses, a module that does not
619
+ * compile, or code that throws
312
620
  * @example
313
621
  * ```ts
314
622
  * const { config } = await loadCourierConfigFile("/app/courier.config.ts", {
@@ -316,7 +624,38 @@ interface LoadConfigFileOptions extends ConfigJitiOptions {
316
624
  * });
317
625
  * ```
318
626
  */
319
- export declare function loadCourierConfigFile(file: string, { beforeRun, ...options }?: LoadConfigFileOptions): Promise<LoadedCourierConfig>;
627
+ export declare function loadCourierConfigFile(file: string, options?: LoadConfigFileOptions): Promise<LoadedCourierConfig>;
628
+ /** An environment, as a process holds it. */
629
+ type Environment = Readonly<Record<string, string | undefined>>;
630
+ /**
631
+ * The text with no secret left to quote, by the rules above.
632
+ *
633
+ * @param text - The message, as the project's code wrote it
634
+ * @param env - The environment whose values are masked
635
+ * @returns The message, with the one sign in place of each secret
636
+ * @example
637
+ * ```ts
638
+ * withoutSecrets("cannot reach postgres://app:pw@db/shop", { PGUSER: "postgres" });
639
+ * // → "cannot reach [redacted]://db/shop"
640
+ * ```
641
+ */
642
+ export declare function withoutSecrets(text: string, env?: Environment): string;
643
+ /**
644
+ * What the project's code threw, as an error whose message and stack hold
645
+ * no secret, by the rules above: the error a load error carries as its
646
+ * `cause`, so no log that prints it — at debug level, by a bundler — holds
647
+ * one either. It keeps the thrown error's name and `code`; a value thrown
648
+ * that is no error becomes one, its text cut the same way.
649
+ *
650
+ * @param thrown - What the code threw
651
+ * @param env - The environment whose values are masked
652
+ * @returns The error, masked
653
+ * @example
654
+ * ```ts
655
+ * maskedError(new TypeError(`bad ${process.env.API_KEY}`)).message; // "bad [redacted]"
656
+ * ```
657
+ */
658
+ export declare function maskedError(thrown: unknown, env?: Environment): Error;
320
659
  /** Path aliases a project declares, ready for both consumers. */
321
660
  interface PathAliases {
322
661
  /**
@@ -341,6 +680,58 @@ interface PathAliases {
341
680
  * ```
342
681
  */
343
682
  export declare function readPathAliases(projectRoot: string): PathAliases;
683
+ /**
684
+ * How a framework's server answers the imports it answers itself. A
685
+ * framework that answers none, and a project that lists no framework, are
686
+ * read as plain Node.
687
+ *
688
+ * @param framework - The project's framework, as its package.json tells it
689
+ * @returns The profile every loader of the project's files applies
690
+ * @example
691
+ * ```ts
692
+ * profileOf(projectFramework(projectRoot).framework).modules.has("server-only"); // true for Next.js
693
+ * ```
694
+ */
695
+ export declare function profileOf(framework: FrameworkMarker | undefined): ResolutionProfile;
696
+ /** The import a profile refused, and the framework whose rule refused it. */
697
+ interface RefusedModuleContext extends Record<string, unknown> {
698
+ readonly specifier: string;
699
+ /** The framework, as its users name it. */
700
+ readonly framework: string | undefined;
701
+ }
702
+ /**
703
+ * A module of the project imported one its framework refuses where
704
+ * definitions run — Next's `client-only` — so the config did not load.
705
+ *
706
+ * @example
707
+ * ```ts
708
+ * throw new RefusedModuleError("client-only", rule, nextMarker);
709
+ * ```
710
+ */
711
+ export declare class RefusedModuleError extends CourierError<"MODULE_REFUSED"> {
712
+ readonly context: RefusedModuleContext;
713
+ /** Why the framework refuses it, worded to follow the import: `is the marker of …`. */
714
+ readonly reason: string;
715
+ /** What to do about it, worded as an instruction. */
716
+ readonly remedy: string;
717
+ /**
718
+ * @param specifier - The import, as written
719
+ * @param rule - Why the framework refuses it, and what to do
720
+ * @param framework - The framework whose rule it is
721
+ */
722
+ constructor(specifier: string, rule: {
723
+ readonly reason: string;
724
+ readonly remedy: string;
725
+ }, framework: FrameworkMarker | undefined);
726
+ }
727
+ /**
728
+ * How long one reading of a project's config may take before it is given
729
+ * up, in milliseconds: a config that waits on the network, or on a timer,
730
+ * never ends. The routes plugin's process and `courier-cli routes` hold to
731
+ * it; Studio, which keeps running what it loaded last, has its own,
732
+ * `COURIER_CONFIG_LOAD_TIMEOUT_MS`.
733
+ */
734
+ export declare const CONFIG_READ_LIMIT_MS = 6e4;
344
735
  /**
345
736
  * Where a project's transforms are kept across processes:
346
737
  * `.cache/courier/transform` in the nearest `node_modules` directory at or
@@ -417,4 +808,4 @@ export declare class UntrustedConfigError extends CourierError<"CONFIG_NOT_TRUST
417
808
  */
418
809
  constructor(configFile: string, folder: string, reason: string);
419
810
  }
420
- export type { ConfigJitiOptions, LoadConfigFileOptions, LoadCourierConfigOptions, LoadedCourierConfig, PathAliases, TransformCache, TransformCacheOptions, UntrustedConfigContext };
811
+ export type { ConfigJitiOptions, ConfigLoadErrorCode, ConfigLoadErrorContext, FailedLoad, FrameworkId, FrameworkMarker, ImportProjectFileOptions, ListedPackages, LoadConfigFileOptions, LoadCourierConfigOptions, LoadedCourierConfig, ModuleRule, PathAliases, ProjectFramework, RefusedModuleContext, ResolutionProfile, TransformCache, TransformCacheOptions, UntrustedConfigContext };
@@ -1,5 +1,6 @@
1
+ import { i as enterDefinitionsPhase, n as COURIER_PHASE, r as DEFINITIONS_PHASE, t as CONFIG_READ_LIMIT_MS } from "../read-limits-DUuUaEQM.js";
1
2
  import { n as isRecord } from "../errno-code-CNx_E80R.js";
2
- import { a as projectTransformCache, c as UntrustedConfigError, d as CONFIG_BASENAMES, f as configCandidates, i as loadCourierConfigFile, l as createConfigJiti, n as findConfigFileAt, o as findTransformCacheDirectory, r as loadCourierConfig, s as createTransformCache, t as findConfigFile, u as readPathAliases } from "../load-config-BeedbpkN.js";
3
+ import { C as projectFramework, S as detectFramework, T as configCandidates, _ as createConfigJiti, a as projectTransformCache, b as readPathAliases, c as UntrustedConfigError, d as configLoadError, f as loadTimedOut, g as ConfigLoadError, i as loadCourierConfigFile, l as importProjectFile, m as withoutSecrets, n as findConfigFileAt, o as findTransformCacheDirectory, p as maskedError, r as loadCourierConfig, s as createTransformCache, t as findConfigFile, u as settledDefault, v as profileOf, w as CONFIG_BASENAMES, x as FRAMEWORK_MARKERS, y as RefusedModuleError } from "../load-config-barNPfXN.js";
3
4
  // src/config/unknown-config-keys.ts
4
5
  const TOP_LEVEL = {
5
6
  sources: true,
@@ -25,6 +26,14 @@ const BLOCKS = /* @__PURE__ */ new Map([
25
26
  allowedHosts: true,
26
27
  maxBodyBytes: true,
27
28
  maxUploadBytes: true,
29
+ uploadMode: true,
30
+ uploadMemoryBytes: true,
31
+ totalUploadMemoryBytes: true,
32
+ totalUploadDiskBytes: true,
33
+ uploadDirectory: true,
34
+ maxUploadParts: true,
35
+ bodyIdleTimeoutMs: true,
36
+ bodyTimeoutMs: true,
28
37
  rateLimit: true,
29
38
  browsersOnly: true
30
39
  }],
@@ -63,4 +72,4 @@ function unknownConfigKeys(config) {
63
72
  }
64
73
  return unknown;
65
74
  }
66
- export { CONFIG_BASENAMES, UntrustedConfigError, configCandidates, createConfigJiti, createTransformCache, findConfigFile, findConfigFileAt, findTransformCacheDirectory, loadCourierConfig, loadCourierConfigFile, projectTransformCache, readPathAliases, unknownConfigKeys };
75
+ export { CONFIG_BASENAMES, CONFIG_READ_LIMIT_MS, COURIER_PHASE, ConfigLoadError, DEFINITIONS_PHASE, FRAMEWORK_MARKERS, RefusedModuleError, UntrustedConfigError, configCandidates, configLoadError, createConfigJiti, createTransformCache, detectFramework, enterDefinitionsPhase, findConfigFile, findConfigFileAt, findTransformCacheDirectory, importProjectFile, loadCourierConfig, loadCourierConfigFile, loadTimedOut, maskedError, profileOf, projectFramework, projectTransformCache, readPathAliases, settledDefault, unknownConfigKeys, withoutSecrets };
@@ -1,5 +1,5 @@
1
1
  import { t as errnoCode } from "./errno-code-CNx_E80R.js";
2
- import { c as UntrustedConfigError, t as findConfigFile } from "./load-config-BeedbpkN.js";
2
+ import { c as UntrustedConfigError, t as findConfigFile } from "./load-config-barNPfXN.js";
3
3
  import { resolve } from "node:path";
4
4
  import { realpathSync } from "node:fs";
5
5
  // src/util/config-file-of.ts