@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.
Files changed (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +295 -2
  3. package/lib/config/index.d.ts +811 -0
  4. package/lib/config/index.js +75 -0
  5. package/lib/config-file-of-Clf_dWUQ.js +28 -0
  6. package/lib/config-in-page-message-DLd3f0Jd.js +20 -0
  7. package/lib/courier-routes-options-BYF7Sfdf.d.cts +13 -0
  8. package/lib/courier-routes-options-BYF7Sfdf.d.ts +13 -0
  9. package/lib/engine/worker.d.ts +1 -0
  10. package/lib/engine/worker.js +548 -0
  11. package/lib/errno-code-CNx_E80R.js +32 -0
  12. package/lib/esbuild/index.d.ts +58 -0
  13. package/lib/esbuild/index.js +191 -0
  14. package/lib/index-_VUU5N5v.d.ts +134 -0
  15. package/lib/index.d.ts +101 -0
  16. package/lib/index.js +4 -0
  17. package/lib/jest/routes-thread.d.ts +1 -0
  18. package/lib/jest/routes-thread.js +46 -0
  19. package/lib/jest/transformer.cjs +121 -0
  20. package/lib/jest/transformer.d.cts +39 -0
  21. package/lib/load-config-barNPfXN.js +2834 -0
  22. package/lib/metro/index.cjs +25 -0
  23. package/lib/metro/index.d.cts +55 -0
  24. package/lib/metro/index.d.ts +67 -0
  25. package/lib/metro/index.js +105 -0
  26. package/lib/package-file-CVAKY_Nu.js +39 -0
  27. package/lib/read-limits-DUuUaEQM.js +41 -0
  28. package/lib/rollup/index.d.ts +51 -0
  29. package/lib/rollup/index.js +95 -0
  30. package/lib/routes-engine-BfwrCuNL.d.ts +117 -0
  31. package/lib/routes-file-DRoqUZ0S.js +206 -0
  32. package/lib/routes-loader-context-40OKDrQp.d.ts +26 -0
  33. package/lib/routes-module-code-DYtP7uj1.js +405 -0
  34. package/lib/routes-module-id-CnUjQjYF.js +12 -0
  35. package/lib/routes-read-error-DQhKIE2F.js +30 -0
  36. package/lib/routes-rule-Pu1Tz8Xm.js +108 -0
  37. package/lib/rsbuild/index.d.ts +41 -0
  38. package/lib/rsbuild/index.js +37 -0
  39. package/lib/rspack/index.d.ts +3 -0
  40. package/lib/rspack/index.js +1 -0
  41. package/lib/shared-engine-D0j7uXg8.js +28 -0
  42. package/lib/vite/index.d.ts +33 -0
  43. package/lib/vite/index.js +357 -0
  44. package/lib/webpack/index.d.ts +4 -0
  45. package/lib/webpack/index.js +3 -0
  46. package/lib/webpack/loader.cjs +31 -0
  47. package/lib/webpack/loader.d.cts +40 -0
  48. package/lib/webpack/run-routes-loader.d.ts +22 -0
  49. package/lib/webpack/run-routes-loader.js +103 -0
  50. package/lib/webpack-BPxhBYGV.js +42 -0
  51. package/lib/worker-messages-BT2vvNQJ.js +35 -0
  52. package/package.json +209 -4
@@ -0,0 +1,206 @@
1
+ import { t as RoutesReadError } from "./routes-read-error-DQhKIE2F.js";
2
+ import { n as routesModuleCode, r as unreadRoutesModuleCode } from "./routes-module-code-DYtP7uj1.js";
3
+ import { t as errnoCode } from "./errno-code-CNx_E80R.js";
4
+ import { t as sharedEngine } from "./shared-engine-D0j7uXg8.js";
5
+ import { t as configFileOf } from "./config-file-of-Clf_dWUQ.js";
6
+ import { dirname, join, sep } from "node:path";
7
+ import { mkdirSync, readFileSync, renameSync, rmSync, watch, writeFileSync } from "node:fs";
8
+ // src/routes-file/follow-routes-inputs.ts
9
+ /**
10
+ * Watch what a reading of the routes read, so a change to any of it reads
11
+ * them again and rewrites the module, which the bundler then builds anew:
12
+ * the folder of each file — an edit saved by replacing the file, as editors
13
+ * save, is seen there, where a watcher on the file itself loses it — and
14
+ * each folder whose entries the reading listed. Packages under
15
+ * `node_modules` are not watched: the bundler is restarted when they
16
+ * change. The watchers hold no process open, and a folder watched already
17
+ * gets no second one.
18
+ *
19
+ * @param routes - The project's routes
20
+ * @param inputs - What the reading read, whether it gave routes or failed
21
+ * @param onChange - Called on each change to any of it
22
+ * @returns Whether a folder is watched now that was not before
23
+ * @example
24
+ * ```ts
25
+ * followRoutesInputs(routes, snapshot, () => refreshSoon(routes));
26
+ * ```
27
+ */
28
+ function followRoutesInputs(routes, inputs, onChange) {
29
+ const listed = new Set(inputs.listed ?? []);
30
+ let added = false;
31
+ for (const path of [...inputs.files, ...listed]) {
32
+ if (path === routes.file || isPackaged(path)) continue;
33
+ routes.inputs.add(path);
34
+ added = watchFolder(routes, listed.has(path) ? path : dirname(path), onChange) || added;
35
+ }
36
+ return added;
37
+ }
38
+ function watchFolder(routes, folder, onChange) {
39
+ if (routes.watchers.has(folder)) return false;
40
+ let watcher;
41
+ try {
42
+ watcher = watch(folder, { persistent: false }, (_event, name) => {
43
+ const path = name === null ? folder : join(folder, name);
44
+ if (routes.inputs.has(path) || routes.inputs.has(folder)) onChange();
45
+ });
46
+ } catch {
47
+ return false;
48
+ }
49
+ watcher.on("error", () => {
50
+ watcher.close();
51
+ routes.watchers.delete(folder);
52
+ });
53
+ routes.watchers.set(folder, watcher);
54
+ return true;
55
+ }
56
+ function isPackaged(path) {
57
+ return path.includes(`${sep}node_modules${sep}`);
58
+ }
59
+ // src/routes-file/write-routes-file.ts
60
+ const NOT_WRITABLE = /* @__PURE__ */ new Set([
61
+ "EACCES",
62
+ "EPERM",
63
+ "EROFS",
64
+ "ENOTDIR"
65
+ ]);
66
+ /**
67
+ * Write the routes module a bundler takes from disk, when it would change:
68
+ * whole, under a name of its own first, so the bundler never reads it half
69
+ * written, and not at all when it holds the code already, so an unchanged
70
+ * reading builds nothing again.
71
+ *
72
+ * @param file - The module, absolute
73
+ * @param code - Its code
74
+ * @param root - The project's root, for the error
75
+ * @param elsewhere - How the plugin is told to write it elsewhere, for the error
76
+ * @returns Whether the file changed
77
+ * @throws RoutesReadError `ROUTES_NOT_WRITTEN` when its folder cannot hold it
78
+ * @example
79
+ * ```ts
80
+ * writeRoutesFile(file, routesModuleCode(snapshot), "/app", "withCourier(config, { outDir })");
81
+ * ```
82
+ */
83
+ function writeRoutesFile(file, code, root, elsewhere) {
84
+ if (contentOf(file) === code) return false;
85
+ const temporary = `${file}.${process.pid}.tmp`;
86
+ try {
87
+ mkdirSync(dirname(file), { recursive: true });
88
+ writeFileSync(temporary, code);
89
+ renameSync(temporary, file);
90
+ return true;
91
+ } catch (error) {
92
+ rmSync(temporary, { force: true });
93
+ const reason = errnoCode(error);
94
+ if (reason === void 0 || !NOT_WRITABLE.has(reason)) throw error;
95
+ throw new RoutesReadError("ROUTES_NOT_WRITTEN", `The routes module cannot be written to ${dirname(file)} (${reason}). Name a folder it can be written to with ${elsewhere}.`, { root });
96
+ }
97
+ }
98
+ function contentOf(file) {
99
+ try {
100
+ return readFileSync(file, "utf8");
101
+ } catch {
102
+ return;
103
+ }
104
+ }
105
+ // src/routes-file/routes-file.ts
106
+ const REGISTRY = Symbol.for("@noverax/courier-build/routes-file");
107
+ const holder = globalThis;
108
+ const SETTLE_MS = 30;
109
+ const STARTING_MS = [250, 2e3];
110
+ const settling = /* @__PURE__ */ new WeakMap();
111
+ /**
112
+ * The routes of a project written to a module, made once per process for
113
+ * the same module and read again on every call: a config the bundler loads
114
+ * again reads them anew, and adds no watcher a folder has already.
115
+ *
116
+ * @param root - The project's root
117
+ * @param config - The config, from the root or absolute, when it is not the one found
118
+ * @param file - The module the bundler takes, absolute
119
+ * @param elsewhere - How the plugin is told to write it elsewhere, for the error
120
+ * @returns The routes, read and written
121
+ * @throws RoutesReadError `ROUTES_NOT_WRITTEN` when the module cannot be written
122
+ * @example
123
+ * ```ts
124
+ * const routes = await routesFile("/app", undefined, "/app/node_modules/.cache/@noverax/courier/routes.js", "withCourier(config, { outDir })");
125
+ * ```
126
+ */
127
+ async function routesFile(root, config, file, elsewhere) {
128
+ const registry = registryOf();
129
+ let routes = registry.get(file);
130
+ if (routes === void 0) {
131
+ routes = {
132
+ root,
133
+ config,
134
+ file,
135
+ elsewhere,
136
+ engine: sharedEngine({
137
+ root,
138
+ config,
139
+ envFiles: []
140
+ }),
141
+ configFile: void 0,
142
+ failure: void 0,
143
+ inputs: /* @__PURE__ */ new Set(),
144
+ watchers: /* @__PURE__ */ new Map()
145
+ };
146
+ registry.set(file, routes);
147
+ }
148
+ await refreshRoutes(routes);
149
+ return routes;
150
+ }
151
+ /**
152
+ * Read the routes again and write the module when they changed. The
153
+ * engine's readings end in the order they began, so the module is left as
154
+ * the last one read it. When the routes cannot be read, those read last
155
+ * stay, and why is kept, for a build to fail with, and told in the
156
+ * bundler's terminal; with none read yet, the module fails where it is
157
+ * imported, saying why.
158
+ *
159
+ * @param routes - The project's routes
160
+ * @throws RoutesReadError `ROUTES_NOT_WRITTEN` when the module cannot be written
161
+ * @example
162
+ * ```ts
163
+ * await refreshRoutes(routes);
164
+ * ```
165
+ */
166
+ async function refreshRoutes(routes) {
167
+ const follow = (inputs) => {
168
+ if (!followRoutesInputs(routes, inputs, () => refreshSoon(routes))) return;
169
+ for (const ms of STARTING_MS) setTimeout(() => refreshSoon(routes), ms).unref();
170
+ };
171
+ try {
172
+ const snapshot = await routes.engine.read();
173
+ routes.failure = void 0;
174
+ writeRoutesFile(routes.file, routesModuleCode(snapshot), routes.root, routes.elsewhere);
175
+ follow(snapshot);
176
+ } catch (error) {
177
+ if (error instanceof RoutesReadError && error.code === "ROUTES_NOT_WRITTEN") throw error;
178
+ const failure = error instanceof Error ? error : new Error(String(error));
179
+ routes.failure = failure;
180
+ const followed = (failure instanceof RoutesReadError ? failure.inputs : void 0) ?? routes.engine.last;
181
+ if (followed !== void 0) follow(followed);
182
+ if (routes.engine.last === void 0) {
183
+ const code = unreadRoutesModuleCode(failure.message);
184
+ writeRoutesFile(routes.file, code, routes.root, routes.elsewhere);
185
+ }
186
+ console.error(`[courier] ${failure.message}`);
187
+ }
188
+ routes.configFile = configFileOf(routes.root, routes.config);
189
+ }
190
+ function refreshSoon(routes) {
191
+ clearTimeout(settling.get(routes));
192
+ const timer = setTimeout(() => {
193
+ settling.delete(routes);
194
+ if (registryOf().get(routes.file) !== routes) return;
195
+ refreshRoutes(routes).catch((error) => {
196
+ console.error(`[courier] ${error instanceof Error ? error.message : String(error)}`);
197
+ });
198
+ }, SETTLE_MS);
199
+ timer.unref();
200
+ settling.set(routes, timer);
201
+ }
202
+ function registryOf() {
203
+ holder[REGISTRY] ??= /* @__PURE__ */ new Map();
204
+ return holder[REGISTRY];
205
+ }
206
+ export { routesFile as t };
@@ -0,0 +1,26 @@
1
+ /**
2
+ * What of a loader's context the routes loader uses. webpack and Rspack give
3
+ * all of it; Turbopack runs webpack loaders with a part of it, so what it may
4
+ * lack is optional and used only where it is there.
5
+ */
6
+ interface RoutesLoaderContext {
7
+ /** The module the loader runs on, absolute. */
8
+ readonly resourcePath: string;
9
+ /** `"production"` in a build, `"development"` under a dev server; Turbopack names none. */
10
+ readonly mode?: string | undefined;
11
+ /** The options the rule hands the loader. */
12
+ getOptions(): unknown;
13
+ /** Whether the loader's result may be kept for the same inputs: never for the routes. */
14
+ cacheable(flag?: boolean): void;
15
+ /** Mark the loader asynchronous; the callback ends it with an error or the module's code. */
16
+ async(): (error: Error | null, content?: string) => void;
17
+ /** Build the module again when this file changes. */
18
+ addDependency(file: string): void;
19
+ /** Build the module again when an entry of this folder is added or removed. */
20
+ addContextDependency?(folder: string): void;
21
+ /** Build the module again once this path, absent now, appears. */
22
+ addMissingDependency?(path: string): void;
23
+ /** Report an error while the module still builds: what a dev server shows over the page. */
24
+ emitError?(error: Error): void;
25
+ }
26
+ export { RoutesLoaderContext as t };
@@ -0,0 +1,405 @@
1
+ import { n as COURIER_PHASE, r as DEFINITIONS_PHASE, t as CONFIG_READ_LIMIT_MS } from "./read-limits-DUuUaEQM.js";
2
+ import { t as packageFile } from "./package-file-CVAKY_Nu.js";
3
+ import { t as RoutesReadError } from "./routes-read-error-DQhKIE2F.js";
4
+ import { n as isRecord, t as errnoCode } from "./errno-code-CNx_E80R.js";
5
+ import { t as workerMessages } from "./worker-messages-BT2vvNQJ.js";
6
+ import { isAbsolute, resolve } from "node:path";
7
+ import { fork } from "node:child_process";
8
+ import { readdirSync, statSync } from "node:fs";
9
+ import { createHash } from "node:crypto";
10
+ // src/engine/supervise-child.ts
11
+ /** How long a process is given to end once let go, before it is killed. */
12
+ const GRACE_MS = 2e3;
13
+ const running = /* @__PURE__ */ new Set();
14
+ let exitWatched = false;
15
+ /**
16
+ * Watch over a reading's process for as long as it runs. Let go, it no
17
+ * longer holds this process open: the channel to it closes, which ends it,
18
+ * one that never answered is asked to end, and one still running after a
19
+ * grace period is killed — a config may ignore SIGTERM, or block the
20
+ * process's loop. One still running when this process exits is killed then.
21
+ *
22
+ * @param child - The process, just started
23
+ * @returns How to let it go, saying whether it answered
24
+ * @example
25
+ * ```ts
26
+ * const release = superviseChild(fork(worker));
27
+ * release(false); // gave up waiting for it
28
+ * ```
29
+ */
30
+ function superviseChild(child) {
31
+ watchExit();
32
+ running.add(child);
33
+ child.once("exit", () => running.delete(child));
34
+ return (answered) => {
35
+ if (!isRunning(child)) return;
36
+ if (child.connected) child.disconnect();
37
+ child.unref();
38
+ if (!answered) child.kill("SIGTERM");
39
+ setTimeout(() => {
40
+ if (isRunning(child)) child.kill("SIGKILL");
41
+ }, GRACE_MS).unref();
42
+ };
43
+ }
44
+ function watchExit() {
45
+ if (exitWatched) return;
46
+ exitWatched = true;
47
+ process.on("exit", () => {
48
+ for (const child of running) child.kill("SIGKILL");
49
+ });
50
+ }
51
+ function isRunning(child) {
52
+ return child.exitCode === null && child.signalCode === null;
53
+ }
54
+ // src/engine/evaluate-in-child.ts
55
+ /** The package this module ships in, by the name its manifest gives. */
56
+ const PACKAGE_NAME = "@noverax/courier-build";
57
+ /**
58
+ * Read a project's routes in a process of its own, started for this reading
59
+ * alone: the config runs there, never in the bundler's process, so a module
60
+ * it reaches is read as it is now, and nothing it leaves behind lingers.
61
+ *
62
+ * @param request - Where the project is, and which env files it reads
63
+ * @param limitMs - How long the reading may take
64
+ * @returns The routes, and what they depended on
65
+ * @throws RoutesReadError for a config that would not load, and
66
+ * `ROUTES_NOT_READ` when the process ran out of time or ended unanswered
67
+ * @example
68
+ * ```ts
69
+ * const snapshot = await evaluateInChild({ root: "/app", envFiles: [] });
70
+ * ```
71
+ */
72
+ function evaluateInChild(request, limitMs = CONFIG_READ_LIMIT_MS) {
73
+ return new Promise((resolve, reject) => {
74
+ const unread = (message) => new RoutesReadError("ROUTES_NOT_READ", message, {
75
+ root: request.root,
76
+ configFile: request.configFile
77
+ });
78
+ const worker = packageFile("lib/engine/worker.js");
79
+ if (worker === void 0) {
80
+ reject(unread(`${PACKAGE_NAME} cannot find the folder it is installed in: install it again`));
81
+ return;
82
+ }
83
+ const child = fork(worker, [], {
84
+ execArgv: [],
85
+ env: {
86
+ ...process.env,
87
+ [COURIER_PHASE]: DEFINITIONS_PHASE
88
+ },
89
+ stdio: [
90
+ "ignore",
91
+ "inherit",
92
+ "inherit",
93
+ "ipc"
94
+ ]
95
+ });
96
+ const settle = settleOnce(superviseChild(child), resolve, reject);
97
+ const timer = setTimeout(() => {
98
+ settle(unread(`The config was still being read after ${limitMs / 1e3} s, and was given up`));
99
+ }, limitMs);
100
+ settle.onSettled(() => clearTimeout(timer));
101
+ ask(child, request, settle, unread);
102
+ });
103
+ }
104
+ function ask(child, request, settle, unread) {
105
+ child.on("message", (message) => {
106
+ if (workerMessages.isReply(message)) settle(outcomeOf$1(message.reply), true);
107
+ });
108
+ child.once("error", (error) => settle(unread(`The process that reads the config did not start: ${error.message}`)));
109
+ child.once("close", (code, signal) => settle(unread(`The process that reads the config ended (${signal ?? code}) before it answered`)));
110
+ child.once("spawn", () => child.send(workerMessages.request(request)));
111
+ }
112
+ function outcomeOf$1(reply) {
113
+ if (reply.ok) return reply.snapshot;
114
+ return new RoutesReadError(reply.code, reply.message, reply.context, {
115
+ stack: reply.stack,
116
+ inputs: reply.inputs
117
+ });
118
+ }
119
+ function settleOnce(release, resolve, reject) {
120
+ let settled = false;
121
+ const after = [];
122
+ const settle = (outcome, answered = false) => {
123
+ if (settled) return;
124
+ settled = true;
125
+ for (const then of after) then();
126
+ release(answered);
127
+ if (outcome instanceof RoutesReadError) reject(outcome);
128
+ else resolve(outcome);
129
+ };
130
+ return Object.assign(settle, { onSettled: (then) => after.push(then) });
131
+ }
132
+ // src/engine/fingerprint.ts
133
+ /** What no file and no variable can digest to: a fingerprint nothing matches. */
134
+ const UNSETTLED = "unsettled";
135
+ /**
136
+ * How far before a reading began a file system that keeps whole seconds or
137
+ * coarser (HFS+, FAT, some network ones) may date a change made after it:
138
+ * FAT counts in two-second steps.
139
+ */
140
+ const COARSE_CLOCK_MS = 2e3;
141
+ /**
142
+ * How far before a reading began a file system that keeps finer times may
143
+ * date a change made after it: Linux and Windows date a change by a clock
144
+ * that moves once a tick, every 1 to 10 milliseconds on Linux and about 16
145
+ * on Windows, while the reading's start is taken from a precise one.
146
+ */
147
+ const TICK_MS = 100;
148
+ /**
149
+ * A digest of what a reading of the routes depended on, as it is now: the
150
+ * size and the time of the last change of every file it read, whether a
151
+ * path it looked at is there and is a folder, the entries of every folder
152
+ * it listed, and the value of every variable it looked at. Equal digests
153
+ * mean the same routes.
154
+ *
155
+ * A file changed after the reading began may have been read before or after
156
+ * the change, so the digest is then {@link UNSETTLED}, which nothing equals,
157
+ * and the next reading reads the routes again. The system may date such a
158
+ * change before the reading began: by up to a tick of the clock it dates
159
+ * changes by, and by up to two seconds where it keeps whole milliseconds or
160
+ * coarser. A change that close to the start counts as made after it.
161
+ *
162
+ * @param inputs - What the reading depended on
163
+ * @param since - When the reading began, in milliseconds since the epoch; omitted, every change counts as before it
164
+ * @returns The digest
165
+ * @example
166
+ * ```ts
167
+ * fingerprintOf(snapshot) === fingerprintOf(snapshot); // true while nothing it read changed
168
+ * ```
169
+ */
170
+ function fingerprintOf(inputs, since) {
171
+ const hash = createHash("sha256").update(inputs.configFile ?? "\0none");
172
+ const parts = [...inputs.files.map((file) => filePart(file, since)), ...(inputs.listed ?? []).map((folder) => folderPart(folder, since))];
173
+ for (const part of parts) {
174
+ if (part === "unsettled") return UNSETTLED;
175
+ hash.update(part);
176
+ }
177
+ for (const [name, value] of variablesOf(inputs.env)) hash.update(`\n${name}=${value ?? "\0unset"}`);
178
+ return hash.digest("hex");
179
+ }
180
+ function filePart(file, since) {
181
+ const stats = statOf(file);
182
+ if (typeof stats === "string") return `\n${file}\0${stats}`;
183
+ if (stats.isDirectory()) return `\n${file}\0folder`;
184
+ if (since !== void 0 && changedSince(stats.mtimeMs, since)) return UNSETTLED;
185
+ return `\n${file}\0${stats.size}\0${stats.mtimeMs}`;
186
+ }
187
+ function folderPart(folder, since) {
188
+ const stats = statOf(folder);
189
+ if (since !== void 0 && typeof stats !== "string" && changedSince(stats.mtimeMs, since)) return UNSETTLED;
190
+ return `\n${folder}/\0${entriesOf(folder)}`;
191
+ }
192
+ function changedSince(mtimeMs, since) {
193
+ return mtimeMs >= since - (mtimeMs % 1 === 0 ? COARSE_CLOCK_MS : TICK_MS);
194
+ }
195
+ function statOf(path) {
196
+ try {
197
+ return statSync(path, { throwIfNoEntry: false }) ?? "absent";
198
+ } catch (error) {
199
+ return errnoCode(error) ?? "unreadable";
200
+ }
201
+ }
202
+ function entriesOf(folder) {
203
+ try {
204
+ return readdirSync(folder).sort().join("\0");
205
+ } catch (error) {
206
+ return `\0${errnoCode(error) ?? "unreadable"}`;
207
+ }
208
+ }
209
+ function variablesOf(names) {
210
+ return (names === "*" ? Object.keys(process.env).sort() : names).map((name) => [name, process.env[name]]);
211
+ }
212
+ // src/engine/routes-readings.ts
213
+ /**
214
+ * Make the readings of a project's routes one at a time. Callers that ask
215
+ * while one runs share it, unless something it read changed while it ran:
216
+ * a caller that asked after it began then gets the next reading, which
217
+ * begins after the change, so an edit saved mid-reading is never served
218
+ * stale. A failure is not kept: the next caller reads again.
219
+ *
220
+ * @param read - One reading
221
+ * @returns The readings
222
+ * @example
223
+ * ```ts
224
+ * const readings = createReadings(() => evaluateInChild(request));
225
+ * const outcome = await readings.next();
226
+ * ```
227
+ */
228
+ function createReadings(read) {
229
+ const state = {
230
+ begun: 0,
231
+ running: false,
232
+ waiting: []
233
+ };
234
+ return {
235
+ next() {
236
+ const kept = state.kept;
237
+ if (kept !== void 0 && isCurrent(kept)) return Promise.resolve({
238
+ ok: true,
239
+ snapshot: kept.snapshot
240
+ });
241
+ return new Promise((settle) => {
242
+ state.waiting.push({
243
+ seen: state.begun,
244
+ settle
245
+ });
246
+ if (!state.running) run(state, read);
247
+ });
248
+ },
249
+ get kept() {
250
+ return state.kept;
251
+ }
252
+ };
253
+ }
254
+ async function run(state, read) {
255
+ state.running = true;
256
+ try {
257
+ while (state.waiting.length > 0) await readOnce(state, read);
258
+ } catch (error) {
259
+ const stranded = state.waiting;
260
+ state.waiting = [];
261
+ for (const waiter of stranded) waiter.settle({
262
+ ok: false,
263
+ error
264
+ });
265
+ } finally {
266
+ state.running = false;
267
+ }
268
+ }
269
+ async function readOnce(state, read) {
270
+ state.begun += 1;
271
+ const reading = state.begun;
272
+ const since = Date.now();
273
+ const outcome = await outcomeOf(read);
274
+ const inputs = inputsOf(outcome);
275
+ const fingerprint = inputs === void 0 ? UNSETTLED : fingerprintOf(inputs, since);
276
+ if (outcome.ok) state.kept = {
277
+ snapshot: outcome.snapshot,
278
+ fingerprint
279
+ };
280
+ const settled = fingerprint !== "unsettled" || inputs === void 0;
281
+ const served = state.waiting.filter((waiter) => settled || waiter.seen < reading);
282
+ state.waiting = state.waiting.filter((waiter) => !served.includes(waiter));
283
+ for (const waiter of served) waiter.settle(outcome);
284
+ }
285
+ function isCurrent(kept) {
286
+ return kept.fingerprint !== "unsettled" && fingerprintOf(kept.snapshot) === kept.fingerprint;
287
+ }
288
+ async function outcomeOf(read) {
289
+ try {
290
+ return {
291
+ ok: true,
292
+ snapshot: await read()
293
+ };
294
+ } catch (error) {
295
+ return {
296
+ ok: false,
297
+ error
298
+ };
299
+ }
300
+ }
301
+ function inputsOf(outcome) {
302
+ if (outcome.ok) return outcome.snapshot;
303
+ return outcome.error instanceof RoutesReadError ? outcome.error.inputs : void 0;
304
+ }
305
+ // src/engine/routes-engine.ts
306
+ /**
307
+ * Create the engine that reads a project's routes for a bundler plugin.
308
+ *
309
+ * @param options - Where the project and its config are
310
+ * @param evaluate - How one reading runs: in a process of its own
311
+ * @returns The engine
312
+ * @example
313
+ * ```ts
314
+ * const engine = createRoutesEngine({ root: process.cwd(), envFiles: [".env.local", ".env"] });
315
+ * const { routes } = await engine.read();
316
+ * ```
317
+ */
318
+ function createRoutesEngine(options, evaluate = evaluateInChild) {
319
+ const request = requestOf(options);
320
+ const readings = createReadings(() => evaluate({
321
+ ...request,
322
+ env: options.envValues?.()
323
+ }));
324
+ return {
325
+ async read() {
326
+ const outcome = await readings.next();
327
+ if (outcome.ok) return outcome.snapshot;
328
+ throw outcome.error;
329
+ },
330
+ get last() {
331
+ return readings.kept?.snapshot;
332
+ },
333
+ fingerprint() {
334
+ const kept = readings.kept;
335
+ return kept === void 0 || kept.fingerprint === "unsettled" ? void 0 : fingerprintOf(kept.snapshot);
336
+ }
337
+ };
338
+ }
339
+ function requestOf(options) {
340
+ const root = resolve(options.root);
341
+ const fromRoot = (path) => isAbsolute(path) ? path : resolve(root, path);
342
+ return {
343
+ root,
344
+ configFile: options.config === void 0 ? void 0 : fromRoot(options.config),
345
+ envFiles: (options.envFiles ?? []).map(fromRoot)
346
+ };
347
+ }
348
+ // src/engine/routes-module-code.ts
349
+ /**
350
+ * The module a page's bundle takes in place of `@noverax/courier/routes`:
351
+ * the routes of every source of the config, as data alone, written as an
352
+ * object literal that keeps every name its own — `__proto__` too, which a
353
+ * literal would otherwise take for the object's prototype.
354
+ *
355
+ * @param snapshot - The routes as the config gave them
356
+ * @returns The module's code
357
+ * @example
358
+ * ```ts
359
+ * routesModuleCode(snapshot); // 'export const routes = {"shop":{"source":"shop","endpoints":{…}}};\n'
360
+ * ```
361
+ */
362
+ function routesModuleCode(snapshot) {
363
+ return `export const routes = ${literalOf(snapshot.routes)};\n`;
364
+ }
365
+ /**
366
+ * The same module, as CommonJS, for a host that requires it and transforms
367
+ * nothing it takes: Jest, outside its ES module mode. It exports `routes`
368
+ * as an ES module would, so an import compiled to a `require` takes it by
369
+ * name.
370
+ *
371
+ * @param snapshot - The routes as the config gave them
372
+ * @returns The module's code
373
+ * @example
374
+ * ```ts
375
+ * routesCommonJsCode(snapshot); // '"use strict";\nObject.defineProperty(exports, "__esModule", …);\nexports.routes = {…};\n'
376
+ * ```
377
+ */
378
+ function routesCommonJsCode(snapshot) {
379
+ return `"use strict";\nObject.defineProperty(exports, "__esModule", { value: true });\nexports.routes = ${literalOf(snapshot.routes)};\n`;
380
+ }
381
+ function literalOf(value) {
382
+ if (Array.isArray(value)) return `[${value.map(literalOf).join(",")}]`;
383
+ if (!isRecord(value)) return JSON.stringify(value) ?? "null";
384
+ return `{${Object.entries(value).filter(([, member]) => member !== void 0).map(([key, member]) => `${keyOf(key)}:${literalOf(member)}`).join(",")}}`;
385
+ }
386
+ function keyOf(key) {
387
+ return key === "__proto__" ? "[\"__proto__\"]" : JSON.stringify(key);
388
+ }
389
+ /**
390
+ * The module a page's bundle takes when the routes were never read: one
391
+ * that fails where it is imported, saying why, as a build fails with it.
392
+ * It still exports `routes`, so a module that imports it links, and the
393
+ * reason is the error the page shows.
394
+ *
395
+ * @param message - Why the routes were not read
396
+ * @returns The module's code
397
+ * @example
398
+ * ```ts
399
+ * unreadRoutesModuleCode("courier.config.ts did not load: …");
400
+ * ```
401
+ */
402
+ function unreadRoutesModuleCode(message) {
403
+ return `throw new Error(${JSON.stringify(`[courier] ${message}`)});\nexport const routes = {};\n`;
404
+ }
405
+ export { createRoutesEngine as i, routesModuleCode as n, unreadRoutesModuleCode as r, routesCommonJsCode as t };
@@ -0,0 +1,12 @@
1
+ // src/util/routes-module-id.ts
2
+ /** What a page imports its routes by: the module the package ships, which the plugin answers. */
3
+ const ROUTES_SPECIFIER = "@noverax/courier/routes";
4
+ /**
5
+ * The module the plugin answers it with. A virtual id, so no environment
6
+ * takes it for an installed file to load as it is, and a server's bundle
7
+ * builds it in as the page's does.
8
+ */
9
+ const ROUTES_MODULE_ID = "\0@noverax/courier/routes";
10
+ /** Exactly the specifier: a path that merely begins with it is another module. */
11
+ const ROUTES_PATTERN = /^@noverax\/courier\/routes$/;
12
+ export { ROUTES_PATTERN as n, ROUTES_SPECIFIER as r, ROUTES_MODULE_ID as t };
@@ -0,0 +1,30 @@
1
+ import { CourierError } from "@noverax/courier";
2
+ // src/engine/routes-read-error.ts
3
+ /**
4
+ * A project's routes could not be read from its config. A build fails with
5
+ * it; a dev server keeps the routes it read last and reports it.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * throw new RoutesReadError("CONFIG_NOT_FOUND", "No courier.config.* in /app or above it", { root: "/app" });
10
+ * ```
11
+ */
12
+ var RoutesReadError = class extends CourierError {
13
+ /** What the reading had read when it failed, when it answered at all. */
14
+ inputs;
15
+ /**
16
+ * @param code - Why the routes were not read
17
+ * @param message - What went wrong, and what to do about it
18
+ * @param context - The project's root, and the config file once found
19
+ * @param details - The stack of what the config threw, and what the reading had read
20
+ */
21
+ constructor(code, message, context, { stack, inputs } = {}) {
22
+ super(message, {
23
+ code,
24
+ context
25
+ });
26
+ if (stack !== void 0) this.stack = stack;
27
+ this.inputs = inputs;
28
+ }
29
+ };
30
+ export { RoutesReadError as t };