@noverax/courier-build 0.0.0-stage → 0.4.0

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