@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.
- package/LICENSE +21 -0
- package/README.md +289 -2
- package/lib/config/index.d.ts +420 -0
- package/lib/config/index.js +66 -0
- package/lib/config-file-of-Zkwyi9zf.js +28 -0
- package/lib/config-in-page-message-DLd3f0Jd.js +20 -0
- package/lib/courier-routes-options-BYF7Sfdf.d.cts +13 -0
- package/lib/courier-routes-options-BYF7Sfdf.d.ts +13 -0
- package/lib/engine/worker.d.ts +1 -0
- package/lib/engine/worker.js +551 -0
- package/lib/errno-code-CNx_E80R.js +32 -0
- package/lib/esbuild/index.d.ts +58 -0
- package/lib/esbuild/index.js +191 -0
- package/lib/index-_VUU5N5v.d.ts +134 -0
- package/lib/index.d.ts +101 -0
- package/lib/index.js +4 -0
- package/lib/jest/routes-thread.d.ts +1 -0
- package/lib/jest/routes-thread.js +46 -0
- package/lib/jest/transformer.cjs +121 -0
- package/lib/jest/transformer.d.cts +39 -0
- package/lib/load-config-BeedbpkN.js +1419 -0
- package/lib/metro/index.cjs +25 -0
- package/lib/metro/index.d.cts +55 -0
- package/lib/metro/index.d.ts +67 -0
- package/lib/metro/index.js +105 -0
- package/lib/package-file-CVAKY_Nu.js +39 -0
- package/lib/rollup/index.d.ts +51 -0
- package/lib/rollup/index.js +95 -0
- package/lib/routes-engine-BfwrCuNL.d.ts +117 -0
- package/lib/routes-file-DcpgxYb1.js +206 -0
- package/lib/routes-loader-context-40OKDrQp.d.ts +26 -0
- package/lib/routes-module-code-DO3aII6v.js +402 -0
- package/lib/routes-module-id-CnUjQjYF.js +12 -0
- package/lib/routes-read-error-DQhKIE2F.js +30 -0
- package/lib/routes-rule-CjzX6NS5.js +108 -0
- package/lib/rsbuild/index.d.ts +41 -0
- package/lib/rsbuild/index.js +37 -0
- package/lib/rspack/index.d.ts +3 -0
- package/lib/rspack/index.js +1 -0
- package/lib/shared-engine-CCu7vP_0.js +28 -0
- package/lib/vite/index.d.ts +33 -0
- package/lib/vite/index.js +346 -0
- package/lib/webpack/index.d.ts +4 -0
- package/lib/webpack/index.js +3 -0
- package/lib/webpack/loader.cjs +31 -0
- package/lib/webpack/loader.d.cts +40 -0
- package/lib/webpack/run-routes-loader.d.ts +22 -0
- package/lib/webpack/run-routes-loader.js +103 -0
- package/lib/webpack-B0v9i8lk.js +42 -0
- package/lib/worker-messages-BT2vvNQJ.js +35 -0
- package/package.json +209 -4
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
import { t as RoutesReadError } from "../routes-read-error-DQhKIE2F.js";
|
|
2
|
+
import { n as isRecord } from "../errno-code-CNx_E80R.js";
|
|
3
|
+
import { t as workerMessages } from "../worker-messages-BT2vvNQJ.js";
|
|
4
|
+
import { a as projectTransformCache, c as UntrustedConfigError, f as configCandidates, i as loadCourierConfigFile, t as findConfigFile } from "../load-config-BeedbpkN.js";
|
|
5
|
+
import * as nodeModule from "node:module";
|
|
6
|
+
import { createRequire, syncBuiltinESMExports } from "node:module";
|
|
7
|
+
import { basename, dirname, isAbsolute, resolve, sep } from "node:path";
|
|
8
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
9
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
10
|
+
import { parseEnv } from "node:util";
|
|
11
|
+
import { MessageChannel } from "node:worker_threads";
|
|
12
|
+
// src/util/define-own.ts
|
|
13
|
+
/**
|
|
14
|
+
* Give a record a key of its own, whatever the key. Assigning `record[key]`
|
|
15
|
+
* with the key `__proto__` sets the record's prototype instead, and the key
|
|
16
|
+
* is lost: a source of that name went missing from the routes built of the
|
|
17
|
+
* config's sources.
|
|
18
|
+
*
|
|
19
|
+
* @param record - The record: an object literal, which no key but `__proto__` sets through a setter
|
|
20
|
+
* @param key - The key, as the config spells it
|
|
21
|
+
* @param value - Its value
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* const routes: Record<string, CourierRoutes> = {};
|
|
25
|
+
* defineOwn(routes, "__proto__", legacyRoutes);
|
|
26
|
+
* Object.keys(routes); // → ["__proto__"]
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
function defineOwn(record, key, value) {
|
|
30
|
+
Object.defineProperty(record, key, {
|
|
31
|
+
value,
|
|
32
|
+
enumerable: true,
|
|
33
|
+
writable: true,
|
|
34
|
+
configurable: true
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
// src/engine/project-config.ts
|
|
38
|
+
/**
|
|
39
|
+
* The config a reading of the routes reads: the one the request names, or
|
|
40
|
+
* the nearest found from its root, up to the top of the project.
|
|
41
|
+
*
|
|
42
|
+
* @param request - Where the project is, and the config it names
|
|
43
|
+
* @returns The config, and where one was looked for first
|
|
44
|
+
* @throws RoutesReadError `CONFIG_NOT_FOUND` with no config up to the top of
|
|
45
|
+
* the project, and `CONFIG_NOT_TRUSTED` for one above the root that someone
|
|
46
|
+
* else could have put there; either names every path looked at
|
|
47
|
+
* @example
|
|
48
|
+
* ```ts
|
|
49
|
+
* projectConfigFile({ root: "/app/web", envFiles: [] });
|
|
50
|
+
* // → { configFile: "/app/courier.config.ts", lookedFor: ["/app/web/courier.config.ts", …] }
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
function projectConfigFile(request) {
|
|
54
|
+
if (request.configFile !== void 0) return {
|
|
55
|
+
configFile: request.configFile,
|
|
56
|
+
lookedFor: []
|
|
57
|
+
};
|
|
58
|
+
const candidates = configCandidates(request.root);
|
|
59
|
+
const unread = (code, message, configFile) => new RoutesReadError(code, message, {
|
|
60
|
+
root: request.root,
|
|
61
|
+
configFile
|
|
62
|
+
}, { inputs: {
|
|
63
|
+
files: [...candidates].sort(),
|
|
64
|
+
env: []
|
|
65
|
+
} });
|
|
66
|
+
let found;
|
|
67
|
+
try {
|
|
68
|
+
found = findConfigFile(request.root);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (!(error instanceof UntrustedConfigError)) throw error;
|
|
71
|
+
throw unread("CONFIG_NOT_TRUSTED", error.message, error.context.configFile);
|
|
72
|
+
}
|
|
73
|
+
if (found === null) throw unread("CONFIG_NOT_FOUND", `No courier.config.* in ${request.root} or a folder above it, up to the top of the project: the routes a page takes are read from it. Name another file with the plugin's \`config\` option.`);
|
|
74
|
+
return {
|
|
75
|
+
configFile: found,
|
|
76
|
+
lookedFor: candidates.slice(0, candidates.indexOf(found))
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Load the project's config with the project's transforms, kept in its
|
|
81
|
+
* `node_modules/.cache` for the next reading.
|
|
82
|
+
*
|
|
83
|
+
* @param configFile - The config, absolute
|
|
84
|
+
* @param request - Where the project is
|
|
85
|
+
* @param options - What runs around the config's code
|
|
86
|
+
* @returns The config, and its real path
|
|
87
|
+
* @throws RoutesReadError `CONFIG_NOT_LOADED` with what the loader or the config threw
|
|
88
|
+
* @example
|
|
89
|
+
* ```ts
|
|
90
|
+
* const loaded = await loadProjectConfig("/app/courier.config.ts", request, { onModule });
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
async function loadProjectConfig(configFile, request, options) {
|
|
94
|
+
try {
|
|
95
|
+
const transformCache = projectTransformCache(dirname(configFile));
|
|
96
|
+
return await loadCourierConfigFile(configFile, {
|
|
97
|
+
...options,
|
|
98
|
+
transformCache
|
|
99
|
+
});
|
|
100
|
+
} catch (error) {
|
|
101
|
+
throw new RoutesReadError("CONFIG_NOT_LOADED", `${basename(configFile)} did not load, so the routes a page takes were not read: ${error instanceof Error ? error.message : String(error)}`, {
|
|
102
|
+
root: request.root,
|
|
103
|
+
configFile
|
|
104
|
+
}, { stack: error instanceof Error ? error.stack : void 0 });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// src/engine/read-env-files.ts
|
|
108
|
+
/**
|
|
109
|
+
* Put the variables a project's env files give into `process.env` before its
|
|
110
|
+
* config is read, as the framework's own server does: the first file to set
|
|
111
|
+
* a variable wins, and one the process was started with keeps its value.
|
|
112
|
+
* The bundler's own reading of them, when given, stands in for this one, so
|
|
113
|
+
* a value built of others with `${VAR}` is the one the app sees.
|
|
114
|
+
*
|
|
115
|
+
* @param files - The env files, absolute, the first winning; one that is not there is passed over
|
|
116
|
+
* @param given - The variables the bundler read from them, in place of reading them here
|
|
117
|
+
* @returns The files that were there
|
|
118
|
+
* @example
|
|
119
|
+
* ```ts
|
|
120
|
+
* readEnvFiles(["/app/.env.production.local", "/app/.env.production", "/app/.env.local", "/app/.env"]);
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
function readEnvFiles(files, given) {
|
|
124
|
+
const fromProcess = new Set(Object.keys(process.env));
|
|
125
|
+
const present = files.filter((file) => existsSync(file));
|
|
126
|
+
const values = given === void 0 ? Object.entries(parsedFrom(present)) : Object.entries(given);
|
|
127
|
+
for (const [key, value] of values) if (!fromProcess.has(key)) process.env[key] = value;
|
|
128
|
+
return present;
|
|
129
|
+
}
|
|
130
|
+
function parsedFrom(files) {
|
|
131
|
+
const parsed = {};
|
|
132
|
+
for (const file of files) for (const [key, value] of Object.entries(parseEnv(readFileSync(file, "utf8")))) if (value !== void 0 && !(key in parsed)) parsed[key] = value;
|
|
133
|
+
return parsed;
|
|
134
|
+
}
|
|
135
|
+
// src/util/local-path.ts
|
|
136
|
+
const NAMESPACE = "\\\\?\\";
|
|
137
|
+
const SHARE_NAMESPACE = "\\\\?\\UNC\\";
|
|
138
|
+
/**
|
|
139
|
+
* A path as this system names it: absolute, written with its own
|
|
140
|
+
* separator, `.` and `..` resolved, and on Windows out of the namespace
|
|
141
|
+
* `path.toNamespacedPath` writes it in (`\\?\D:\app` is `D:\app`). The code
|
|
142
|
+
* a reading runs may name one file in any of these ways; a watcher reports
|
|
143
|
+
* it in this one.
|
|
144
|
+
*
|
|
145
|
+
* @param path - The path, absolute or from the working folder
|
|
146
|
+
* @returns The path
|
|
147
|
+
* @example
|
|
148
|
+
* ```ts
|
|
149
|
+
* localPath("/app/src/../flags.json"); // "/app/flags.json"
|
|
150
|
+
* ```
|
|
151
|
+
*/
|
|
152
|
+
function localPath(path) {
|
|
153
|
+
if (sep === "\\" && path.startsWith(SHARE_NAMESPACE)) return resolve(`\\\\${path.slice(8)}`);
|
|
154
|
+
if (sep === "\\" && path.startsWith(NAMESPACE)) return resolve(path.slice(4));
|
|
155
|
+
return resolve(path);
|
|
156
|
+
}
|
|
157
|
+
// src/engine/track-env-reads.ts
|
|
158
|
+
/**
|
|
159
|
+
* Note every environment variable the code that runs next looks at, by
|
|
160
|
+
* putting a watcher in front of `process.env`: a name read, tested for or
|
|
161
|
+
* described is noted, and listing them all notes them all.
|
|
162
|
+
*
|
|
163
|
+
* @param options - Whether to begin paused
|
|
164
|
+
* @returns What was looked at, asked once the code has run
|
|
165
|
+
* @example
|
|
166
|
+
* ```ts
|
|
167
|
+
* const reads = trackEnvReads();
|
|
168
|
+
* process.env.API_URL;
|
|
169
|
+
* reads.names(); // ["API_URL"]
|
|
170
|
+
* ```
|
|
171
|
+
*/
|
|
172
|
+
function trackEnvReads({ paused: startPaused = false } = {}) {
|
|
173
|
+
const read = /* @__PURE__ */ new Set();
|
|
174
|
+
let all = false;
|
|
175
|
+
let waiting = startPaused;
|
|
176
|
+
let paused = waiting ? 1 : 0;
|
|
177
|
+
process.env = new Proxy(process.env, watcherOf({
|
|
178
|
+
read: (key) => {
|
|
179
|
+
if (paused === 0 && typeof key === "string") read.add(key);
|
|
180
|
+
},
|
|
181
|
+
listed: () => {
|
|
182
|
+
if (paused === 0) all = true;
|
|
183
|
+
}
|
|
184
|
+
}));
|
|
185
|
+
return {
|
|
186
|
+
names: () => all ? "*" : [...read].sort(),
|
|
187
|
+
untracked(run) {
|
|
188
|
+
paused += 1;
|
|
189
|
+
try {
|
|
190
|
+
return run();
|
|
191
|
+
} finally {
|
|
192
|
+
paused -= 1;
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
resume() {
|
|
196
|
+
if (waiting) paused -= 1;
|
|
197
|
+
waiting = false;
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
function watcherOf(looks) {
|
|
202
|
+
return {
|
|
203
|
+
get(target, key) {
|
|
204
|
+
looks.read(key);
|
|
205
|
+
return Reflect.get(target, key);
|
|
206
|
+
},
|
|
207
|
+
has(target, key) {
|
|
208
|
+
looks.read(key);
|
|
209
|
+
return Reflect.has(target, key);
|
|
210
|
+
},
|
|
211
|
+
getOwnPropertyDescriptor(target, key) {
|
|
212
|
+
looks.read(key);
|
|
213
|
+
return Reflect.getOwnPropertyDescriptor(target, key);
|
|
214
|
+
},
|
|
215
|
+
ownKeys(target) {
|
|
216
|
+
looks.listed();
|
|
217
|
+
return Reflect.ownKeys(target);
|
|
218
|
+
},
|
|
219
|
+
set: (target, key, value) => Reflect.set(target, key, value),
|
|
220
|
+
defineProperty: (target, key, descriptor) => Reflect.defineProperty(target, key, descriptor)
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
// src/engine/track-file-reads.ts
|
|
224
|
+
const require$1 = createRequire(import.meta.url);
|
|
225
|
+
const READS = [
|
|
226
|
+
"readFileSync",
|
|
227
|
+
"readFile",
|
|
228
|
+
"createReadStream",
|
|
229
|
+
"existsSync",
|
|
230
|
+
"statSync",
|
|
231
|
+
"lstatSync",
|
|
232
|
+
"stat",
|
|
233
|
+
"lstat",
|
|
234
|
+
"accessSync",
|
|
235
|
+
"access"
|
|
236
|
+
];
|
|
237
|
+
const LISTS = [
|
|
238
|
+
"readdirSync",
|
|
239
|
+
"readdir",
|
|
240
|
+
"opendirSync",
|
|
241
|
+
"opendir"
|
|
242
|
+
];
|
|
243
|
+
const PROMISED_READS = [
|
|
244
|
+
"readFile",
|
|
245
|
+
"stat",
|
|
246
|
+
"lstat",
|
|
247
|
+
"access"
|
|
248
|
+
];
|
|
249
|
+
const PROMISED_LISTS = ["readdir", "opendir"];
|
|
250
|
+
/**
|
|
251
|
+
* Note every path the code that runs next reads or looks at through `fs`,
|
|
252
|
+
* and every folder it lists: a config that reads an OpenAPI document, or
|
|
253
|
+
* builds its sources from a folder's entries, depends on them as on the
|
|
254
|
+
* modules it imports. Each path is noted as the system names it, however
|
|
255
|
+
* the code wrote it. Installed packages are left out, as the install
|
|
256
|
+
* changes them, and so is anything `fs` is handed that names no path.
|
|
257
|
+
*
|
|
258
|
+
* @returns What was read, asked once the code has run
|
|
259
|
+
* @example
|
|
260
|
+
* ```ts
|
|
261
|
+
* const reads = trackFileReads();
|
|
262
|
+
* readFileSync("./openapi.json");
|
|
263
|
+
* reads.paths(); // ["/app/openapi.json"]
|
|
264
|
+
* ```
|
|
265
|
+
*/
|
|
266
|
+
function trackFileReads() {
|
|
267
|
+
const paths = /* @__PURE__ */ new Set();
|
|
268
|
+
const folders = /* @__PURE__ */ new Set();
|
|
269
|
+
const fs = require$1("node:fs");
|
|
270
|
+
const promises = require$1("node:fs/promises");
|
|
271
|
+
if (isRecord(fs)) {
|
|
272
|
+
noteCalls(fs, READS, paths);
|
|
273
|
+
noteCalls(fs, LISTS, folders);
|
|
274
|
+
}
|
|
275
|
+
if (isRecord(promises)) {
|
|
276
|
+
noteCalls(promises, PROMISED_READS, paths);
|
|
277
|
+
noteCalls(promises, PROMISED_LISTS, folders);
|
|
278
|
+
}
|
|
279
|
+
syncBuiltinESMExports();
|
|
280
|
+
return {
|
|
281
|
+
paths: () => [...paths],
|
|
282
|
+
folders: () => [...folders]
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
function noteCalls(target, names, noted) {
|
|
286
|
+
for (const name of names) {
|
|
287
|
+
const original = target[name];
|
|
288
|
+
if (typeof original !== "function") continue;
|
|
289
|
+
target[name] = Object.assign(function(...args) {
|
|
290
|
+
const path = pathOf(args[0]);
|
|
291
|
+
if (path !== void 0 && !path.split(sep).includes("node_modules")) noted.add(path);
|
|
292
|
+
return Reflect.apply(original, this, args);
|
|
293
|
+
}, original);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
function pathOf(value) {
|
|
297
|
+
if (typeof value === "string") return localPath(value);
|
|
298
|
+
if (value instanceof URL) return value.protocol === "file:" ? fileURLToPath(value) : void 0;
|
|
299
|
+
if (Buffer.isBuffer(value)) return pathOf(value.toString());
|
|
300
|
+
}
|
|
301
|
+
// src/engine/note-resolved-files.ts
|
|
302
|
+
const ASYNC_HOOKS = `let port;
|
|
303
|
+
export function initialize(data) {
|
|
304
|
+
port = data.port;
|
|
305
|
+
port.on("message", (message) => { if (message === "flush") port.postMessage("flushed"); });
|
|
306
|
+
port.unref();
|
|
307
|
+
}
|
|
308
|
+
export async function resolve(specifier, context, nextResolve) {
|
|
309
|
+
const resolved = await nextResolve(specifier, context);
|
|
310
|
+
if (resolved.url.startsWith("file:")) port.postMessage(resolved.url);
|
|
311
|
+
return resolved;
|
|
312
|
+
}
|
|
313
|
+
`;
|
|
314
|
+
/**
|
|
315
|
+
* Note the file of every ES module Node resolves from now on, into a set:
|
|
316
|
+
* with the synchronous hooks Node has from 22.15, as it resolves each one;
|
|
317
|
+
* before, with hooks on the loader's own thread, whose notes arrive once
|
|
318
|
+
* `settled()` resolves.
|
|
319
|
+
*
|
|
320
|
+
* @param noted - Where each file goes
|
|
321
|
+
* @param hooks - Which hooks: the synchronous ones wherever Node has them
|
|
322
|
+
* @returns How to wait for every note
|
|
323
|
+
* @example
|
|
324
|
+
* ```ts
|
|
325
|
+
* const resolved = noteResolvedFiles(files);
|
|
326
|
+
* await import(configUrl);
|
|
327
|
+
* await resolved.settled(); // files now holds every module the import resolved
|
|
328
|
+
* ```
|
|
329
|
+
*/
|
|
330
|
+
function noteResolvedFiles(noted, hooks = typeof nodeModule.registerHooks === "function" ? "sync" : "async") {
|
|
331
|
+
if (hooks === "sync" && typeof nodeModule.registerHooks === "function") {
|
|
332
|
+
nodeModule.registerHooks({ resolve(specifier, context, nextResolve) {
|
|
333
|
+
const resolved = nextResolve(specifier, context);
|
|
334
|
+
if (resolved.url.startsWith("file:")) noted.add(fileURLToPath(resolved.url));
|
|
335
|
+
return resolved;
|
|
336
|
+
} });
|
|
337
|
+
return { settled: () => Promise.resolve() };
|
|
338
|
+
}
|
|
339
|
+
return noteOffThread(noted);
|
|
340
|
+
}
|
|
341
|
+
function noteOffThread(noted) {
|
|
342
|
+
const { port1, port2 } = new MessageChannel();
|
|
343
|
+
const flushed = [];
|
|
344
|
+
port1.on("message", (message) => {
|
|
345
|
+
if (message === "flushed") flushed.shift()?.();
|
|
346
|
+
else if (typeof message === "string") noted.add(fileURLToPath(message));
|
|
347
|
+
if (flushed.length === 0) port1.unref();
|
|
348
|
+
});
|
|
349
|
+
port1.unref();
|
|
350
|
+
nodeModule.register(`data:text/javascript,${encodeURIComponent(ASYNC_HOOKS)}`, {
|
|
351
|
+
data: { port: port2 },
|
|
352
|
+
transferList: [port2]
|
|
353
|
+
});
|
|
354
|
+
return { settled: () => new Promise((resolve) => {
|
|
355
|
+
flushed.push(resolve);
|
|
356
|
+
port1.ref();
|
|
357
|
+
port1.postMessage("flush");
|
|
358
|
+
}) };
|
|
359
|
+
}
|
|
360
|
+
// src/engine/track-module-files.ts
|
|
361
|
+
const require = createRequire(import.meta.url);
|
|
362
|
+
/**
|
|
363
|
+
* Note the file of every module the code that runs next loads: the ones
|
|
364
|
+
* the config's loader transforms are told by `noteFile`; the ones Node
|
|
365
|
+
* loads itself are read from its CommonJS cache and from each ES module it
|
|
366
|
+
* resolves.
|
|
367
|
+
*
|
|
368
|
+
* @param hooks - How the ES modules Node resolves are noted: synchronously wherever Node can
|
|
369
|
+
* @returns The files noted, and how to note one
|
|
370
|
+
* @example
|
|
371
|
+
* ```ts
|
|
372
|
+
* const modules = trackModuleFiles();
|
|
373
|
+
* await loadCourierConfigFile(file, { onModule: modules.noteFile });
|
|
374
|
+
* await modules.files(); // ["/app/courier.config.ts", "/app/src/api.ts", …]
|
|
375
|
+
* ```
|
|
376
|
+
*/
|
|
377
|
+
function trackModuleFiles(hooks) {
|
|
378
|
+
const noted = /* @__PURE__ */ new Set();
|
|
379
|
+
const resolved = noteResolvedFiles(noted, hooks);
|
|
380
|
+
return {
|
|
381
|
+
noteFile: (file) => noted.add(file),
|
|
382
|
+
files: async () => {
|
|
383
|
+
await resolved.settled();
|
|
384
|
+
return [...noted, ...Object.keys(require.cache).filter((key) => isAbsolute(key))];
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
// src/engine/track-reading.ts
|
|
389
|
+
/**
|
|
390
|
+
* Note what one reading of the routes reads, from now on: the modules Node
|
|
391
|
+
* and the config's loader load, the files read and folders listed once the
|
|
392
|
+
* config is found, and the environment variables the config's code looks at
|
|
393
|
+
* once it runs, besides the config and the env files.
|
|
394
|
+
*
|
|
395
|
+
* @param request - The reading's request, whose env files are read before it
|
|
396
|
+
* @returns The tracker
|
|
397
|
+
* @example
|
|
398
|
+
* ```ts
|
|
399
|
+
* const reading = trackReading(request);
|
|
400
|
+
* await loadProjectConfig(configFile, request, reading.loadOptions);
|
|
401
|
+
* await reading.inputs(); // { configFile, files: [...], env: ["API_URL"] }
|
|
402
|
+
* ```
|
|
403
|
+
*/
|
|
404
|
+
function trackReading(request) {
|
|
405
|
+
const modules = trackModuleFiles();
|
|
406
|
+
const env = trackEnvReads({ paused: true });
|
|
407
|
+
let configFile;
|
|
408
|
+
let reads;
|
|
409
|
+
const looked = /* @__PURE__ */ new Set();
|
|
410
|
+
return {
|
|
411
|
+
loadOptions: {
|
|
412
|
+
onModule: modules.noteFile,
|
|
413
|
+
beforeRun: env.resume,
|
|
414
|
+
runTransform: env.untracked
|
|
415
|
+
},
|
|
416
|
+
noteConfig(file, lookedFor) {
|
|
417
|
+
configFile = file;
|
|
418
|
+
reads ??= trackFileReads();
|
|
419
|
+
looked.add(file);
|
|
420
|
+
for (const path of lookedFor) looked.add(path);
|
|
421
|
+
},
|
|
422
|
+
inputs: async () => {
|
|
423
|
+
const files = [
|
|
424
|
+
...looked,
|
|
425
|
+
...request.envFiles,
|
|
426
|
+
...await modules.files()
|
|
427
|
+
];
|
|
428
|
+
return {
|
|
429
|
+
configFile,
|
|
430
|
+
files: [...new Set([...files, ...reads?.paths() ?? []].map(localPath))].sort(),
|
|
431
|
+
listed: [...reads?.folders() ?? []].sort(),
|
|
432
|
+
env: env.names()
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
// src/engine/read-project-routes.ts
|
|
438
|
+
/**
|
|
439
|
+
* Read a project's routes from its config: its env files first, then the
|
|
440
|
+
* config, every module it reaches transformed once and kept for the next
|
|
441
|
+
* process, then each source's routes by the rules the proxy applies. Run in
|
|
442
|
+
* a process of its own, which it leaves changed: `process.env` is watched,
|
|
443
|
+
* and Node's resolution hooked.
|
|
444
|
+
*
|
|
445
|
+
* @param request - Where the project is, and which env files it reads
|
|
446
|
+
* @returns The routes, and every file and variable they depended on
|
|
447
|
+
* @throws RoutesReadError `CONFIG_NOT_FOUND` with no config to read,
|
|
448
|
+
* `CONFIG_NOT_TRUSTED` for one someone else could have put above the root,
|
|
449
|
+
* and `CONFIG_NOT_LOADED` for one that would not load or holds no
|
|
450
|
+
* `sources`; each names what the reading had read, which may mend it
|
|
451
|
+
* @example
|
|
452
|
+
* ```ts
|
|
453
|
+
* const { routes } = await readProjectRoutes({ root: "/app", envFiles: ["/app/.env"] });
|
|
454
|
+
* ```
|
|
455
|
+
*/
|
|
456
|
+
async function readProjectRoutes(request) {
|
|
457
|
+
readEnvFiles(request.envFiles, request.env);
|
|
458
|
+
const reading = trackReading(request);
|
|
459
|
+
try {
|
|
460
|
+
const { configFile, lookedFor } = projectConfigFile(request);
|
|
461
|
+
reading.noteConfig(configFile, lookedFor);
|
|
462
|
+
const loaded = await loadProjectConfig(configFile, request, reading.loadOptions);
|
|
463
|
+
reading.noteConfig(loaded.configFile, lookedFor);
|
|
464
|
+
const routes = await routesOfSources(loaded, request.root);
|
|
465
|
+
return {
|
|
466
|
+
...await reading.inputs(),
|
|
467
|
+
configFile: loaded.configFile,
|
|
468
|
+
routes
|
|
469
|
+
};
|
|
470
|
+
} catch (error) {
|
|
471
|
+
throw withInputs(error, await reading.inputs(), request);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
async function routesOfSources(loaded, root) {
|
|
475
|
+
const sources = sourcesOf(loaded, root);
|
|
476
|
+
const read = await projectRoutesOf(loaded.configFile, root);
|
|
477
|
+
const routes = {};
|
|
478
|
+
for (const name of Object.keys(sources).sort(byCodeUnits)) defineOwn(routes, name, read(sources[name], name));
|
|
479
|
+
return routes;
|
|
480
|
+
}
|
|
481
|
+
function sourcesOf(loaded, root) {
|
|
482
|
+
const sources = isRecord(loaded.config) ? loaded.config.sources : void 0;
|
|
483
|
+
if (isRecord(sources)) return sources;
|
|
484
|
+
throw new RoutesReadError("CONFIG_NOT_LOADED", `${basename(loaded.configFile)} exports no \`sources\`: the routes a page takes are read from the APIs it names there.`, {
|
|
485
|
+
root,
|
|
486
|
+
configFile: loaded.configFile
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
async function projectRoutesOf(configFile, root) {
|
|
490
|
+
const path = createRequire(configFile).resolve("@noverax/courier/internal");
|
|
491
|
+
const core = await import(pathToFileURL(path).href);
|
|
492
|
+
if (typeof core.routesOf === "function") return core.routesOf;
|
|
493
|
+
throw new RoutesReadError("CONFIG_NOT_LOADED", `The @noverax/courier at ${path} reads no routes: update it to the version of @noverax/courier-build.`, {
|
|
494
|
+
root,
|
|
495
|
+
configFile
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
function withInputs(error, inputs, request) {
|
|
499
|
+
const stack = error instanceof Error ? error.stack : void 0;
|
|
500
|
+
if (!(error instanceof RoutesReadError)) return new RoutesReadError("CONFIG_NOT_LOADED", `The routes a page takes were not read: ${error instanceof Error ? error.message : String(error)}`, {
|
|
501
|
+
root: request.root,
|
|
502
|
+
configFile: inputs.configFile
|
|
503
|
+
}, {
|
|
504
|
+
stack,
|
|
505
|
+
inputs
|
|
506
|
+
});
|
|
507
|
+
if (error.inputs !== void 0) return error;
|
|
508
|
+
return new RoutesReadError(error.code, error.message, error.context, {
|
|
509
|
+
stack,
|
|
510
|
+
inputs
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
function byCodeUnits(a, b) {
|
|
514
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
515
|
+
}
|
|
516
|
+
// src/engine/worker.ts
|
|
517
|
+
process.on("message", take);
|
|
518
|
+
process.once("disconnect", () => process.exit(1));
|
|
519
|
+
function take(message) {
|
|
520
|
+
if (!workerMessages.isRequest(message)) return;
|
|
521
|
+
process.off("message", take);
|
|
522
|
+
answer(message.request);
|
|
523
|
+
}
|
|
524
|
+
async function answer(request) {
|
|
525
|
+
const reply = await readProjectRoutes(request).then((snapshot) => ({
|
|
526
|
+
ok: true,
|
|
527
|
+
snapshot
|
|
528
|
+
}), (error) => failure(error, request));
|
|
529
|
+
process.send?.(workerMessages.reply(reply), () => process.exit(0));
|
|
530
|
+
}
|
|
531
|
+
function failure(error, request) {
|
|
532
|
+
if (error instanceof RoutesReadError) return {
|
|
533
|
+
ok: false,
|
|
534
|
+
code: error.code,
|
|
535
|
+
message: error.message,
|
|
536
|
+
context: error.context,
|
|
537
|
+
stack: error.stack,
|
|
538
|
+
inputs: error.inputs
|
|
539
|
+
};
|
|
540
|
+
return {
|
|
541
|
+
ok: false,
|
|
542
|
+
code: "CONFIG_NOT_LOADED",
|
|
543
|
+
message: `The routes a page takes were not read: ${error instanceof Error ? error.message : String(error)}`,
|
|
544
|
+
context: {
|
|
545
|
+
root: request.root,
|
|
546
|
+
configFile: request.configFile
|
|
547
|
+
},
|
|
548
|
+
stack: error instanceof Error ? error.stack : void 0
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// src/util/is-record.ts
|
|
2
|
+
/**
|
|
3
|
+
* Whether a value is an object read by its keys: not null, not an array.
|
|
4
|
+
*
|
|
5
|
+
* @param value - The value to check
|
|
6
|
+
* @returns Whether it can be read as a record of fields
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* isRecord({ sources: {} }); // true
|
|
11
|
+
* isRecord(["shop"]); // false
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
function isRecord(value) {
|
|
15
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
16
|
+
}
|
|
17
|
+
// src/config/errno-code.ts
|
|
18
|
+
/**
|
|
19
|
+
* The `code` a Node system error carries, such as `ENOENT` or `EACCES`.
|
|
20
|
+
*
|
|
21
|
+
* @param error - Anything thrown
|
|
22
|
+
* @returns The code, or `undefined` when the value carries none
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* if (errnoCode(err) === "ENOENT") return null;
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
function errnoCode(error) {
|
|
29
|
+
if (typeof error !== "object" || error === null || !("code" in error)) return void 0;
|
|
30
|
+
return typeof error.code === "string" ? error.code : void 0;
|
|
31
|
+
}
|
|
32
|
+
export { isRecord as n, errnoCode as t };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { t as CourierRoutesOptions } from "../courier-routes-options-BYF7Sfdf.js";
|
|
2
|
+
import { Plugin } from "esbuild";
|
|
3
|
+
/**
|
|
4
|
+
* The esbuild plugin that builds the routes a page's proxy client may call
|
|
5
|
+
* from `courier.config.ts`, in place of the module `@noverax/courier/routes`
|
|
6
|
+
* ships. No routes file is written, and the routes carry no address, path or
|
|
7
|
+
* middleware. A page's bundle — any `platform` but `node` — that takes the
|
|
8
|
+
* config itself fails.
|
|
9
|
+
*
|
|
10
|
+
* A build that cannot read the config fails with why. A context that builds
|
|
11
|
+
* again, under `watch()` or `serve()`, follows every file the routes came
|
|
12
|
+
* from; when they cannot be read on a later build, it keeps the routes it
|
|
13
|
+
* read last and warns why.
|
|
14
|
+
*
|
|
15
|
+
* @param options - Where the config is, when it is not found from `absWorkingDir`
|
|
16
|
+
* @returns The plugin
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* import { build } from "esbuild";
|
|
20
|
+
* import { courierRoutes } from "@noverax/courier-build/esbuild";
|
|
21
|
+
*
|
|
22
|
+
* await build({ entryPoints: ["src/main.ts"], bundle: true, plugins: [courierRoutes()] });
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare function courierRoutes(options?: CourierRoutesOptions): Plugin;
|
|
26
|
+
/** Options of the esbuild plugin that takes the routes from a module written to disk. */
|
|
27
|
+
interface WrittenRoutesOptions extends CourierRoutesOptions {
|
|
28
|
+
/** The project's root, absolute: the config is found from it. */
|
|
29
|
+
readonly root: string;
|
|
30
|
+
/** The module the routes are written to, absolute: the host must follow it as a file it reads. */
|
|
31
|
+
readonly file: string;
|
|
32
|
+
/** Whether the host builds again on a change: then the routes read last stay while the config is broken. */
|
|
33
|
+
readonly watch: boolean;
|
|
34
|
+
/** How the app names another place for the module, should that one not take it: said in the error. */
|
|
35
|
+
readonly elsewhere: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The esbuild plugin for a host that follows only the files its build read
|
|
39
|
+
* from disk, as Angular's builders do, not those a plugin names: the
|
|
40
|
+
* routes are written to a module there, which `@noverax/courier/routes`
|
|
41
|
+
* resolves to, and watchers of what they came from rewrite it on a change,
|
|
42
|
+
* so the host builds it again. A page's bundle — any `platform` but `node` —
|
|
43
|
+
* that takes the config itself fails.
|
|
44
|
+
*
|
|
45
|
+
* A build fails while the routes cannot be read, with why; a host that
|
|
46
|
+
* watches keeps the routes read last, and with none read yet the module
|
|
47
|
+
* fails where it is imported, saying why.
|
|
48
|
+
*
|
|
49
|
+
* @param options - Where the project, its config and the module are, and whether the host watches
|
|
50
|
+
* @returns The plugin, once the routes are written
|
|
51
|
+
* @throws RoutesReadError `ROUTES_NOT_WRITTEN` when the module cannot be written
|
|
52
|
+
* @example
|
|
53
|
+
* ```ts
|
|
54
|
+
* const plugin = await writtenRoutesPlugin({ root, file: join(dir, "routes.js"), watch: false, elsewhere: "TMPDIR" });
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export declare function writtenRoutesPlugin(options: WrittenRoutesOptions): Promise<Plugin>;
|
|
58
|
+
export type { CourierRoutesOptions, WrittenRoutesOptions };
|