@c9up/ream 0.2.4 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -9
- package/dist/Application.d.ts +85 -5
- package/dist/Application.d.ts.map +1 -1
- package/dist/Application.js +171 -21
- package/dist/Application.js.map +1 -1
- package/dist/ErrorBoundary.d.ts +1 -7
- package/dist/ErrorBoundary.d.ts.map +1 -1
- package/dist/ErrorBoundary.js +31 -31
- package/dist/ErrorBoundary.js.map +1 -1
- package/dist/HttpKernel.js +2 -2
- package/dist/HttpKernel.js.map +1 -1
- package/dist/Ignitor.d.ts +18 -49
- package/dist/Ignitor.d.ts.map +1 -1
- package/dist/Ignitor.js +211 -173
- package/dist/Ignitor.js.map +1 -1
- package/dist/env/Env.d.ts +6 -0
- package/dist/env/Env.d.ts.map +1 -1
- package/dist/env/Env.js +6 -0
- package/dist/env/Env.js.map +1 -1
- package/dist/events/Emitter.d.ts +0 -6
- package/dist/events/Emitter.d.ts.map +1 -1
- package/dist/events/Emitter.js +42 -42
- package/dist/events/Emitter.js.map +1 -1
- package/dist/events/native.d.ts +13 -11
- package/dist/events/native.d.ts.map +1 -1
- package/dist/events/native.js.map +1 -1
- package/dist/helpers/compose.d.ts +30 -0
- package/dist/helpers/compose.d.ts.map +1 -0
- package/dist/helpers/compose.js +27 -0
- package/dist/helpers/compose.js.map +1 -0
- package/dist/helpers/index.d.ts +1 -0
- package/dist/helpers/index.d.ts.map +1 -1
- package/dist/helpers/index.js +1 -0
- package/dist/helpers/index.js.map +1 -1
- package/dist/helpers/is.d.ts +69 -16
- package/dist/helpers/is.d.ts.map +1 -1
- package/dist/helpers/is.js +79 -16
- package/dist/helpers/is.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -3
- package/dist/index.js.map +1 -1
- package/dist/middleware/Pipeline.d.ts +9 -3
- package/dist/middleware/Pipeline.d.ts.map +1 -1
- package/dist/middleware/Pipeline.js +10 -4
- package/dist/middleware/Pipeline.js.map +1 -1
- package/dist/native/generated.d.ts +243 -0
- package/dist/native/generated.d.ts.map +1 -0
- package/dist/native/generated.js +7 -0
- package/dist/native/generated.js.map +1 -0
- package/dist/rpc/RpcRouter.js +2 -2
- package/dist/rpc/RpcRouter.js.map +1 -1
- package/dist/services/app.d.ts +6 -0
- package/dist/services/app.d.ts.map +1 -1
- package/dist/services/app.js +8 -0
- package/dist/services/app.js.map +1 -1
- package/dist/services/config.d.ts +15 -0
- package/dist/services/config.d.ts.map +1 -0
- package/dist/services/config.js +18 -0
- package/dist/services/config.js.map +1 -0
- package/dist/services/encryption.d.ts +23 -0
- package/dist/services/encryption.d.ts.map +1 -0
- package/dist/services/encryption.js +33 -0
- package/dist/services/encryption.js.map +1 -0
- package/dist/services/urlBuilder.d.ts +21 -0
- package/dist/services/urlBuilder.d.ts.map +1 -0
- package/dist/services/urlBuilder.js +34 -0
- package/dist/services/urlBuilder.js.map +1 -0
- package/dist/session/drivers/CookieDriver.d.ts +1 -1
- package/dist/session/drivers/CookieDriver.d.ts.map +1 -1
- package/dist/session/drivers/CookieDriver.js +4 -4
- package/dist/session/drivers/CookieDriver.js.map +1 -1
- package/dist/testing/ApiResponse.d.ts +44 -44
- package/dist/testing/ApiResponse.d.ts.map +1 -1
- package/dist/testing/ApiResponse.js +48 -48
- package/dist/testing/ApiResponse.js.map +1 -1
- package/dist/testing/RequestBuilder.d.ts +39 -39
- package/dist/testing/RequestBuilder.d.ts.map +1 -1
- package/dist/testing/RequestBuilder.js +49 -49
- package/dist/testing/RequestBuilder.js.map +1 -1
- package/dist/testing/TestClient.d.ts +5 -5
- package/dist/testing/TestClient.d.ts.map +1 -1
- package/dist/testing/TestClient.js +7 -7
- package/dist/testing/TestClient.js.map +1 -1
- package/events.win32-x64-msvc.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +15 -2
- package/scheduler.win32-x64-msvc.node +0 -0
- package/scripts/build-napi-types.mjs +68 -0
- package/scripts/generate-napi-types.mjs +171 -0
- package/src/Application.ts +221 -21
- package/src/ErrorBoundary.ts +31 -31
- package/src/HttpKernel.ts +2 -2
- package/src/Ignitor.ts +228 -176
- package/src/env/Env.ts +6 -0
- package/src/events/Emitter.ts +42 -42
- package/src/events/native.ts +21 -14
- package/src/helpers/compose.ts +84 -0
- package/src/helpers/index.ts +1 -0
- package/src/helpers/is.ts +87 -16
- package/src/index.ts +3 -3
- package/src/middleware/Pipeline.ts +10 -4
- package/src/native/generated.ts +291 -0
- package/src/rpc/RpcRouter.ts +2 -2
- package/src/services/app.ts +9 -0
- package/src/services/config.ts +24 -0
- package/src/services/encryption.ts +42 -0
- package/src/services/urlBuilder.ts +41 -0
- package/src/session/drivers/CookieDriver.ts +4 -4
- package/src/testing/ApiResponse.ts +52 -52
- package/src/testing/RequestBuilder.ts +55 -55
- package/src/testing/TestClient.ts +8 -8
package/dist/Ignitor.js
CHANGED
|
@@ -31,11 +31,31 @@ import { CookieSigner } from './security/CookieSigner.js';
|
|
|
31
31
|
import { SignedUrl } from './security/SignedUrl.js';
|
|
32
32
|
import { Server } from './server/Server.js';
|
|
33
33
|
import { clearApp, setApp } from './services/app.js';
|
|
34
|
+
import { clearEncryption, setEncryption } from './services/encryption.js';
|
|
34
35
|
import { clearRouter, setRouter } from './services/router.js';
|
|
35
36
|
import { clearServer, setServer } from './services/server.js';
|
|
36
37
|
import { registerSessionTemplateTags } from './session/templateTags.js';
|
|
38
|
+
/**
|
|
39
|
+
* Every option the `tests` block accepts.
|
|
40
|
+
*
|
|
41
|
+
* Kept beside the interface because it is the same list: TypeScript's
|
|
42
|
+
* excess-property check only fires on an object literal, so a key that reached
|
|
43
|
+
* `defineConfig` through a variable or a spread is invisible to it.
|
|
44
|
+
*/
|
|
45
|
+
const TESTS_OPTIONS = ['bootstrap', 'forceExit', 'suites', 'timeout'];
|
|
37
46
|
/** defineConfig helper — like AdonisJS defineConfig(). */
|
|
38
47
|
export function defineConfig(config) {
|
|
48
|
+
// A key nobody reads is worse than a rejected one: an option that was
|
|
49
|
+
// renamed, removed between versions, or simply mistyped sits in the rc file
|
|
50
|
+
// doing nothing, and the run behaves as if it had never been written. Say so
|
|
51
|
+
// instead, and list what the block does accept — which is also the answer to
|
|
52
|
+
// "then where did my option go".
|
|
53
|
+
const unknown = Object.keys(config.tests ?? {}).filter((key) => !TESTS_OPTIONS.includes(key));
|
|
54
|
+
if (unknown.length > 0) {
|
|
55
|
+
throw new ReamError('E_REAMRC_UNKNOWN_OPTION', `\`tests\` has no option named ${unknown.map((k) => `\`${k}\``).join(', ')}.`, {
|
|
56
|
+
hint: `The \`tests\` block accepts: ${TESTS_OPTIONS.join(', ')}. An option removed between versions is gone from the rc file too — the runner is configured in the bootstrap module now.`,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
39
59
|
return config;
|
|
40
60
|
}
|
|
41
61
|
/**
|
|
@@ -44,30 +64,35 @@ export function defineConfig(config) {
|
|
|
44
64
|
* Lifecycle: register → boot → start → ready → shutdown
|
|
45
65
|
*/
|
|
46
66
|
export class Ignitor {
|
|
47
|
-
app;
|
|
48
|
-
router;
|
|
49
|
-
server;
|
|
50
|
-
middleware;
|
|
67
|
+
#app;
|
|
68
|
+
#router;
|
|
69
|
+
#server;
|
|
70
|
+
#middleware;
|
|
51
71
|
/** Where each data package registers its migration runner. See `MigrationRegistry`. */
|
|
52
|
-
migrations;
|
|
53
|
-
errorBoundary;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
72
|
+
#migrations;
|
|
73
|
+
#errorBoundary;
|
|
74
|
+
/**
|
|
75
|
+
* The APP_KEY-backed signer, when there is one. Held so `stop()` can release
|
|
76
|
+
* the `services/encryption` locator against the exact instance it installed
|
|
77
|
+
* — an unguarded clear would tear down a newer Ignitor's binding.
|
|
78
|
+
*/
|
|
79
|
+
#signer;
|
|
80
|
+
#_httpServer;
|
|
81
|
+
#config;
|
|
82
|
+
#appRoot;
|
|
83
|
+
#reamrc;
|
|
84
|
+
#providers = [];
|
|
85
|
+
#errorListeners = [];
|
|
86
|
+
#phase = 'created';
|
|
87
|
+
#hotReloadCleanup;
|
|
63
88
|
#shutdownHandle;
|
|
64
89
|
#host;
|
|
65
90
|
#console;
|
|
66
91
|
// Inline configuration (for simple use or testing)
|
|
67
|
-
inlineRoutes;
|
|
68
|
-
inlineMiddleware = [];
|
|
69
|
-
inlineNamedMiddleware = [];
|
|
70
|
-
inlineProviderFactories = [];
|
|
92
|
+
#inlineRoutes;
|
|
93
|
+
#inlineMiddleware = [];
|
|
94
|
+
#inlineNamedMiddleware = [];
|
|
95
|
+
#inlineProviderFactories = [];
|
|
71
96
|
/**
|
|
72
97
|
* Create the Ignitor.
|
|
73
98
|
*
|
|
@@ -79,46 +104,46 @@ export class Ignitor {
|
|
|
79
104
|
*/
|
|
80
105
|
constructor(appRootOrConfig, config) {
|
|
81
106
|
if (appRootOrConfig instanceof URL) {
|
|
82
|
-
this
|
|
83
|
-
this
|
|
107
|
+
this.#appRoot = appRootOrConfig;
|
|
108
|
+
this.#config = config ?? {};
|
|
84
109
|
}
|
|
85
110
|
else {
|
|
86
|
-
this
|
|
111
|
+
this.#config = appRootOrConfig ?? {};
|
|
87
112
|
}
|
|
88
|
-
this
|
|
89
|
-
this
|
|
90
|
-
this
|
|
91
|
-
this
|
|
92
|
-
this
|
|
93
|
-
this
|
|
113
|
+
this.#app = new Application();
|
|
114
|
+
this.#router = new Router();
|
|
115
|
+
this.#server = new Server(this.#router);
|
|
116
|
+
this.#middleware = new MiddlewareRegistry();
|
|
117
|
+
this.#migrations = new MigrationRegistry();
|
|
118
|
+
this.#errorBoundary = new ErrorBoundary((event) => this.#handleError(event), this.isDevMode());
|
|
94
119
|
// Register framework services in container
|
|
95
|
-
this
|
|
96
|
-
this
|
|
97
|
-
this
|
|
120
|
+
this.#app.container.singleton('router', () => this.#router);
|
|
121
|
+
this.#app.container.singleton('server', () => this.#server);
|
|
122
|
+
this.#app.container.singleton('middleware', () => this.#middleware);
|
|
98
123
|
// Always bound, even with no data package: an EMPTY registry is what lets
|
|
99
124
|
// the CLI tell "this app has no migrations" from "this app predates the
|
|
100
125
|
// registry" and say something useful in each case.
|
|
101
|
-
this
|
|
102
|
-
this
|
|
126
|
+
this.#app.container.singleton('migrations', () => this.#migrations);
|
|
127
|
+
this.#app.container.singleton('app', () => this.#app);
|
|
103
128
|
// AdonisJS binds the config store too, so code that only needs settings can
|
|
104
129
|
// resolve `config` instead of taking the whole application.
|
|
105
|
-
this
|
|
130
|
+
this.#app.container.singleton('config', () => this.#app.config);
|
|
106
131
|
// The HttpContext CLASS, so an integration provider can extend it the way
|
|
107
132
|
// AdonisJS does — `HttpContext.getter('view', …)` is how the view layer
|
|
108
133
|
// attaches `ctx.view`. Exported from the barrel too, but a provider that
|
|
109
134
|
// must stay agnostic reaches it through the container instead of importing
|
|
110
135
|
// ream at runtime.
|
|
111
|
-
this
|
|
136
|
+
this.#app.container.singleton('HttpContext', () => HttpContext);
|
|
112
137
|
// `appRoot` is the URL passed to `new Ignitor(new URL('../', import.meta.url))`.
|
|
113
138
|
// Providers resolve it through the container so they can interpret
|
|
114
139
|
// relative paths in config files (e.g. `pages.root: './resources/pages'`)
|
|
115
140
|
// against the project root — same convention `modules.path` uses.
|
|
116
|
-
if (this
|
|
117
|
-
const root = this
|
|
118
|
-
this
|
|
141
|
+
if (this.#appRoot) {
|
|
142
|
+
const root = this.#appRoot;
|
|
143
|
+
this.#app.container.singleton('appRoot', () => root);
|
|
119
144
|
// Also expose it on the Application for the AdonisJS-style path helpers
|
|
120
145
|
// (`app.makePath`, `app.configPath`, `app.migrationsPath`, …).
|
|
121
|
-
this
|
|
146
|
+
this.#app.setAppRoot(root);
|
|
122
147
|
}
|
|
123
148
|
// Encryption / cookie-signing service (AdonisJS `APP_KEY` idiom). Registered
|
|
124
149
|
// only when APP_KEY is set; HttpContext hands it to Response/Request so
|
|
@@ -130,17 +155,19 @@ export class Ignitor {
|
|
|
130
155
|
throw new Error('APP_KEY is too short — use at least a 16-character (ideally 32-byte) random key for cookie signing/encryption.');
|
|
131
156
|
}
|
|
132
157
|
const signer = new CookieSigner(appKey);
|
|
133
|
-
this
|
|
158
|
+
this.#app.container.singleton('encryption', () => signer);
|
|
159
|
+
this.#signer = signer;
|
|
160
|
+
setEncryption(signer);
|
|
134
161
|
// Signed-URL helper (same APP_KEY): the router signs via makeSignedUrl,
|
|
135
162
|
// HttpContext hands it to the request so hasValidSignature() can verify.
|
|
136
163
|
const signedUrl = new SignedUrl({ secret: appKey });
|
|
137
|
-
this
|
|
138
|
-
this
|
|
164
|
+
this.#app.container.singleton('signedUrl', () => signedUrl);
|
|
165
|
+
this.#router.setSignedUrl(signedUrl);
|
|
139
166
|
}
|
|
140
167
|
// Set service singletons so route/kernel files can import them
|
|
141
|
-
setApp(this
|
|
142
|
-
setRouter(this
|
|
143
|
-
setServer(this
|
|
168
|
+
setApp(this.#app);
|
|
169
|
+
setRouter(this.#router);
|
|
170
|
+
setServer(this.#server);
|
|
144
171
|
}
|
|
145
172
|
// ─── Configuration ────────────────────────────────────────
|
|
146
173
|
/**
|
|
@@ -148,93 +175,99 @@ export class Ignitor {
|
|
|
148
175
|
* Like AdonisJS: .tap((app) => { app.booting(...) })
|
|
149
176
|
*/
|
|
150
177
|
tap(callback) {
|
|
151
|
-
callback(this
|
|
178
|
+
callback(this.#app);
|
|
152
179
|
return this;
|
|
153
180
|
}
|
|
154
|
-
/**
|
|
181
|
+
/**
|
|
182
|
+
* Set the application environment.
|
|
183
|
+
*
|
|
184
|
+
* Stored on the Application, which is where a provider reads it
|
|
185
|
+
* (`app.getEnvironment()`, the AdonisJS shape). The Ignitor keeps the setter
|
|
186
|
+
* because that is where the entry point declares it.
|
|
187
|
+
*/
|
|
155
188
|
setEnvironment(env) {
|
|
156
|
-
this.
|
|
189
|
+
this.#app.setEnvironment(env);
|
|
157
190
|
return this;
|
|
158
191
|
}
|
|
159
192
|
getEnvironment() {
|
|
160
|
-
return this.
|
|
193
|
+
return this.#app.getEnvironment();
|
|
161
194
|
}
|
|
162
195
|
/**
|
|
163
196
|
* Load the reamrc config (equivalent to adonisrc.ts).
|
|
164
197
|
*/
|
|
165
198
|
useRcFile(reamrc) {
|
|
166
|
-
this
|
|
199
|
+
this.#reamrc = reamrc;
|
|
167
200
|
// Hand the app its directory overrides now rather than at boot: a path
|
|
168
201
|
// helper called from a provider's register() must already see them.
|
|
169
|
-
this
|
|
202
|
+
this.#app.rcContents(reamrc);
|
|
170
203
|
return this;
|
|
171
204
|
}
|
|
172
205
|
// === Inline configuration (simple mode / testing) ===
|
|
173
206
|
/** Define routes inline (simple mode). */
|
|
174
207
|
routes(callback) {
|
|
175
|
-
this
|
|
208
|
+
this.#inlineRoutes = callback;
|
|
176
209
|
return this;
|
|
177
210
|
}
|
|
178
211
|
/** Add global middleware inline. */
|
|
179
212
|
use(mw) {
|
|
180
|
-
this
|
|
213
|
+
this.#inlineMiddleware.push(mw);
|
|
181
214
|
return this;
|
|
182
215
|
}
|
|
183
216
|
/** Register a named middleware inline. */
|
|
184
217
|
named(name, mw) {
|
|
185
|
-
this
|
|
218
|
+
this.#inlineNamedMiddleware.push([name, mw]);
|
|
186
219
|
return this;
|
|
187
220
|
}
|
|
188
221
|
/** Register a provider inline (for testing or simple apps). */
|
|
189
222
|
provider(factory) {
|
|
190
|
-
this
|
|
223
|
+
this.#inlineProviderFactories.push(factory);
|
|
191
224
|
return this;
|
|
192
225
|
}
|
|
193
226
|
/** Listen for error events. */
|
|
194
227
|
onError(listener) {
|
|
195
|
-
this
|
|
228
|
+
this.#errorListeners.push(listener);
|
|
196
229
|
return this;
|
|
197
230
|
}
|
|
198
231
|
/** Set a config value. */
|
|
199
232
|
configure(key, value) {
|
|
200
|
-
this
|
|
233
|
+
this.#app.config.set(key, value);
|
|
201
234
|
return this;
|
|
202
235
|
}
|
|
203
236
|
// ─── Mode selection ───────────────────────────────────────
|
|
204
237
|
/** Configure for HTTP server mode. */
|
|
205
238
|
httpServer() {
|
|
206
|
-
this.
|
|
239
|
+
this.#app.setEnvironment('web');
|
|
207
240
|
return this;
|
|
208
241
|
}
|
|
209
242
|
/** Configure for CLI/console mode. Returns a ConsoleKernel for dispatching commands. */
|
|
210
243
|
console() {
|
|
211
|
-
this.
|
|
244
|
+
this.#app.setEnvironment('console');
|
|
212
245
|
return new ConsoleKernel(this);
|
|
213
246
|
}
|
|
214
247
|
/** Configure for test mode. */
|
|
215
248
|
testMode() {
|
|
216
|
-
this.
|
|
249
|
+
this.#app.setEnvironment('test');
|
|
217
250
|
return this;
|
|
218
251
|
}
|
|
219
252
|
// ─── Lifecycle ────────────────────────────────────────────
|
|
220
253
|
async start() {
|
|
221
254
|
// Double-start guard: a second start() would re-run phaseRegister and
|
|
222
255
|
// instantiate + register every reamrc provider a second time.
|
|
223
|
-
if (this
|
|
224
|
-
throw new ReamError('IGNITOR_ALREADY_STARTED', `start() called while in phase '${this
|
|
256
|
+
if (this.#phase !== 'created') {
|
|
257
|
+
throw new ReamError('IGNITOR_ALREADY_STARTED', `start() called while in phase '${this.#phase}' — an Ignitor boots once`, { hint: 'Create a new Ignitor instance instead of restarting this one.' });
|
|
225
258
|
}
|
|
226
259
|
this.#loadEnvironmentFiles();
|
|
227
260
|
try {
|
|
228
|
-
await this
|
|
229
|
-
await this
|
|
230
|
-
await this
|
|
261
|
+
await this.#phaseRegister();
|
|
262
|
+
await this.#phaseBoot();
|
|
263
|
+
await this.#phaseStart();
|
|
231
264
|
// Console documents its `consoleApp` service as available once the application has
|
|
232
265
|
// booted, so the locator is installed here rather than on first use —
|
|
233
266
|
// `import consoleApp from '@c9up/ream/services/console'` must not throw in a running
|
|
234
267
|
// app. Only the façade is built (three small modules); loading the
|
|
235
268
|
// commands themselves stays lazy, inside `consoleApp.boot()`.
|
|
236
269
|
await this.consoleApp();
|
|
237
|
-
await this
|
|
270
|
+
await this.#phaseReady();
|
|
238
271
|
}
|
|
239
272
|
catch (err) {
|
|
240
273
|
// A throw mid-boot (e.g. a provider ready() failing AFTER the HTTP port
|
|
@@ -261,9 +294,9 @@ export class Ignitor {
|
|
|
261
294
|
* `process.env` win, so the shell / CI always overrides the files.
|
|
262
295
|
*/
|
|
263
296
|
#loadEnvironmentFiles() {
|
|
264
|
-
if (!this
|
|
297
|
+
if (!this.#appRoot)
|
|
265
298
|
return;
|
|
266
|
-
loadEnvFiles(this
|
|
299
|
+
loadEnvFiles(this.#appRoot, { skipEnvLocal: this.#app.getEnvironment() === 'test' });
|
|
267
300
|
}
|
|
268
301
|
/**
|
|
269
302
|
* Give the template engine `@error` / `@errors` / `@inputError` /
|
|
@@ -272,11 +305,11 @@ export class Ignitor {
|
|
|
272
305
|
* The engine is duck-typed out of the container so ream carries no dependency
|
|
273
306
|
* on it, and an app with no template layer simply skips this.
|
|
274
307
|
*/
|
|
275
|
-
async registerSessionTemplateTags() {
|
|
308
|
+
async #registerSessionTemplateTags() {
|
|
276
309
|
for (const token of ['inker', 'view']) {
|
|
277
|
-
if (!this
|
|
310
|
+
if (!this.#app.container.has(token))
|
|
278
311
|
continue;
|
|
279
|
-
const binding = await this
|
|
312
|
+
const binding = await this.#app.container.resolve(token);
|
|
280
313
|
for (const candidate of [binding, Reflect.get(Object(binding), '_templates')]) {
|
|
281
314
|
const register = Reflect.get(Object(candidate), 'registerTag');
|
|
282
315
|
if (typeof register !== 'function')
|
|
@@ -290,91 +323,91 @@ export class Ignitor {
|
|
|
290
323
|
}
|
|
291
324
|
}
|
|
292
325
|
}
|
|
293
|
-
async phaseRegister() {
|
|
326
|
+
async #phaseRegister() {
|
|
294
327
|
// Auto-load config/*.ts files into app.config
|
|
295
|
-
await this
|
|
328
|
+
await this.#autoloadConfig();
|
|
296
329
|
// Load providers from reamrc
|
|
297
|
-
if (this
|
|
298
|
-
for (const providerEntry of this
|
|
330
|
+
if (this.#reamrc?.providers) {
|
|
331
|
+
for (const providerEntry of this.#reamrc.providers) {
|
|
299
332
|
const providerImport = typeof providerEntry === 'function' ? providerEntry : providerEntry.file;
|
|
300
333
|
const env = typeof providerEntry === 'function' ? undefined : providerEntry.environment;
|
|
301
334
|
// Skip providers not matching current environment
|
|
302
|
-
if (env && !env.includes(this.
|
|
335
|
+
if (env && !env.includes(this.#app.getEnvironment()))
|
|
303
336
|
continue;
|
|
304
337
|
const mod = await providerImport();
|
|
305
338
|
const ProviderClass = mod.default;
|
|
306
|
-
const instance = new ProviderClass(this
|
|
307
|
-
this
|
|
308
|
-
this
|
|
339
|
+
const instance = new ProviderClass(this.#app);
|
|
340
|
+
this.#providers.push(instance);
|
|
341
|
+
this.#app.register(instance);
|
|
309
342
|
}
|
|
310
343
|
}
|
|
311
344
|
// Register inline providers
|
|
312
|
-
for (const factory of this
|
|
313
|
-
const instance = factory(this
|
|
314
|
-
this
|
|
315
|
-
this
|
|
345
|
+
for (const factory of this.#inlineProviderFactories) {
|
|
346
|
+
const instance = factory(this.#app);
|
|
347
|
+
this.#providers.push(instance);
|
|
348
|
+
this.#app.register(instance);
|
|
316
349
|
}
|
|
317
|
-
this
|
|
350
|
+
this.#phase = 'registered';
|
|
318
351
|
}
|
|
319
|
-
async phaseBoot() {
|
|
320
|
-
await this
|
|
321
|
-
this
|
|
352
|
+
async #phaseBoot() {
|
|
353
|
+
await this.#app.boot();
|
|
354
|
+
this.#phase = 'booted';
|
|
322
355
|
}
|
|
323
|
-
async phaseStart() {
|
|
356
|
+
async #phaseStart() {
|
|
324
357
|
// Import preload files (routes.ts, kernel.ts, etc.)
|
|
325
|
-
if (this
|
|
326
|
-
for (const preloadEntry of this
|
|
358
|
+
if (this.#reamrc?.preloads) {
|
|
359
|
+
for (const preloadEntry of this.#reamrc.preloads) {
|
|
327
360
|
const preloadImport = typeof preloadEntry === 'function' ? preloadEntry : preloadEntry.file;
|
|
328
361
|
const env = typeof preloadEntry === 'function'
|
|
329
362
|
? undefined
|
|
330
363
|
: preloadEntry.environment;
|
|
331
|
-
if (env && !env.includes(this.
|
|
364
|
+
if (env && !env.includes(this.#app.getEnvironment()))
|
|
332
365
|
continue;
|
|
333
366
|
await preloadImport();
|
|
334
367
|
}
|
|
335
368
|
}
|
|
336
369
|
// Auto-load module files (routes.ts, etc.) from modules directory
|
|
337
|
-
await this
|
|
370
|
+
await this.#autoloadModules();
|
|
338
371
|
// Apply inline configuration
|
|
339
|
-
for (const mw of this
|
|
340
|
-
this
|
|
372
|
+
for (const mw of this.#inlineMiddleware) {
|
|
373
|
+
this.#middleware.use(mw);
|
|
341
374
|
}
|
|
342
|
-
for (const [name, mw] of this
|
|
343
|
-
this
|
|
375
|
+
for (const [name, mw] of this.#inlineNamedMiddleware) {
|
|
376
|
+
this.#middleware.register(name, mw);
|
|
344
377
|
}
|
|
345
|
-
if (this
|
|
346
|
-
this
|
|
378
|
+
if (this.#inlineRoutes) {
|
|
379
|
+
this.#inlineRoutes(this.#router);
|
|
347
380
|
}
|
|
348
381
|
// Call start() on providers
|
|
349
|
-
for (const provider of this
|
|
382
|
+
for (const provider of this.#providers) {
|
|
350
383
|
await callProviderPhase(provider, 'start');
|
|
351
384
|
}
|
|
352
|
-
this
|
|
385
|
+
this.#phase = 'started';
|
|
353
386
|
}
|
|
354
|
-
async phaseReady() {
|
|
387
|
+
async #phaseReady() {
|
|
355
388
|
// Boot the Server (resolves lazy error handler etc.)
|
|
356
|
-
await this
|
|
389
|
+
await this.#server.boot();
|
|
357
390
|
// Start HTTP server if in web mode
|
|
358
|
-
if (this.
|
|
391
|
+
if (this.#app.getEnvironment() === 'web' && this.#config.serverFactory) {
|
|
359
392
|
// Build the HttpKernel with server middleware + router middleware.
|
|
360
393
|
// The streaming backend is resolved lazily — the HyperServer is
|
|
361
394
|
// created a few lines down, after the kernel — so the kernel
|
|
362
|
-
// closes over the `this
|
|
395
|
+
// closes over the `this.#_httpServer` slot and reads it on every
|
|
363
396
|
// request through the factory.
|
|
364
397
|
const kernel = createHttpKernel({
|
|
365
|
-
router: this
|
|
366
|
-
middleware: this
|
|
367
|
-
container: this
|
|
368
|
-
exceptionHandler: this
|
|
369
|
-
serverMiddleware: this
|
|
370
|
-
routerMiddleware: this
|
|
398
|
+
router: this.#router,
|
|
399
|
+
middleware: this.#middleware,
|
|
400
|
+
container: this.#app.container,
|
|
401
|
+
exceptionHandler: this.#server.getErrorHandler() ?? new ExceptionHandler(!this.#app.inProduction),
|
|
402
|
+
serverMiddleware: this.#server.getServerMiddleware(),
|
|
403
|
+
routerMiddleware: this.#router.getRouterMiddleware(),
|
|
371
404
|
onError: (error, ctx) => {
|
|
372
|
-
this
|
|
405
|
+
this.#errorBoundary.serviceError('HttpKernel', error, ctx.id);
|
|
373
406
|
},
|
|
374
407
|
// AdonisJS `config/app.ts` → `http: { allowMethodSpoofing: true }`.
|
|
375
|
-
allowMethodSpoofing: this
|
|
408
|
+
allowMethodSpoofing: this.#app.config.get('app.http.allowMethodSpoofing') === true,
|
|
376
409
|
streamBackend: () => {
|
|
377
|
-
const server = this
|
|
410
|
+
const server = this.#_httpServer;
|
|
378
411
|
if (server &&
|
|
379
412
|
typeof server.registerStream === 'function' &&
|
|
380
413
|
typeof server.writeStream === 'function' &&
|
|
@@ -393,33 +426,33 @@ export class Ignitor {
|
|
|
393
426
|
return undefined;
|
|
394
427
|
},
|
|
395
428
|
});
|
|
396
|
-
const desiredPort = this
|
|
429
|
+
const desiredPort = this.#config.port ?? 3000;
|
|
397
430
|
// Port-scan fallback (+1..+19) is a DEV convenience only. In production
|
|
398
431
|
// a silent drift to :3001 while the LB targets :3000 is an outage —
|
|
399
432
|
// bind the configured port and let EADDRINUSE fail loudly. The probe
|
|
400
433
|
// also has an inherent TOCTOU; acceptable in dev, not in prod.
|
|
401
|
-
const availablePort = this
|
|
434
|
+
const availablePort = this.#app.inProduction
|
|
402
435
|
? desiredPort
|
|
403
436
|
: await findAvailablePort(desiredPort);
|
|
404
437
|
this.#host =
|
|
405
|
-
this
|
|
406
|
-
this
|
|
407
|
-
this
|
|
438
|
+
this.#config.host ?? process.env.HOST ?? (this.#app.inProduction ? '0.0.0.0' : 'localhost');
|
|
439
|
+
this.#_httpServer = this.#config.serverFactory(availablePort, this.#host);
|
|
440
|
+
this.#_httpServer.onRequest(kernel);
|
|
408
441
|
// Pre-resolve client IPs in Rust from the trusted-proxy CIDRs before
|
|
409
442
|
// listen. Security filtering itself lives in @c9up/blackhole.
|
|
410
|
-
const trustedProxies = this
|
|
443
|
+
const trustedProxies = this.#server.getTrustedProxies();
|
|
411
444
|
if (trustedProxies.length > 0 &&
|
|
412
|
-
typeof this
|
|
413
|
-
this
|
|
445
|
+
typeof this.#_httpServer.configureTrustedProxies === 'function') {
|
|
446
|
+
this.#_httpServer.configureTrustedProxies([...trustedProxies]);
|
|
414
447
|
}
|
|
415
|
-
await this
|
|
448
|
+
await this.#_httpServer.listen();
|
|
416
449
|
// Wire OS-signal graceful shutdown. Without it the process never closes
|
|
417
450
|
// the HTTP server on SIGTERM/SIGINT, so live keep-alive / SSE sockets keep
|
|
418
451
|
// the event loop alive — a `ream dev` restart or Ctrl+C ends in a watcher
|
|
419
452
|
// force-kill, and an orchestrator's rolling deploy drops in-flight work.
|
|
420
453
|
// onShutdown = this.stop(), which closes the port (aborting connections),
|
|
421
454
|
// shuts providers down (DB pools), and releases the locators.
|
|
422
|
-
if (this
|
|
455
|
+
if (this.#config.gracefulShutdown !== false) {
|
|
423
456
|
this.#shutdownHandle = installGracefulShutdown({
|
|
424
457
|
onShutdown: () => this.stop(),
|
|
425
458
|
logger: {
|
|
@@ -429,26 +462,26 @@ export class Ignitor {
|
|
|
429
462
|
});
|
|
430
463
|
}
|
|
431
464
|
}
|
|
432
|
-
else if (this.
|
|
465
|
+
else if (this.#app.getEnvironment() === 'web' && !this.#config.serverFactory) {
|
|
433
466
|
throw new ReamError('IGNITOR_NO_SERVER_FACTORY', 'httpServer() requires a serverFactory in config', {
|
|
434
467
|
hint: 'Example: new Ignitor({ serverFactory: (port, host) => new HyperServer(port, host) })',
|
|
435
468
|
});
|
|
436
469
|
}
|
|
437
470
|
// Install error boundary
|
|
438
|
-
this
|
|
471
|
+
this.#errorBoundary.install();
|
|
439
472
|
// Publish the flash-message tags to the template engine, if one is
|
|
440
473
|
// installed. After boot(), so the engine's own provider has bound it;
|
|
441
474
|
// before ready(), so a provider that pre-renders finds the tags in place.
|
|
442
|
-
await this
|
|
475
|
+
await this.#registerSessionTemplateTags();
|
|
443
476
|
// Call ready() on providers
|
|
444
|
-
for (const provider of this
|
|
477
|
+
for (const provider of this.#providers) {
|
|
445
478
|
await callProviderPhase(provider, 'ready');
|
|
446
479
|
}
|
|
447
480
|
// Core domain event: the app finished booting (all providers ready).
|
|
448
481
|
// Emitted once through the bus when events are wired — zero hot-path cost.
|
|
449
|
-
if (this
|
|
450
|
-
const bus = await this
|
|
451
|
-
bus.emit('app:ready', { environment: this.
|
|
482
|
+
if (this.#app.container.has('events')) {
|
|
483
|
+
const bus = await this.#app.container.resolve('events');
|
|
484
|
+
bus.emit('app:ready', { environment: this.#app.getEnvironment() });
|
|
452
485
|
}
|
|
453
486
|
// Dev-mode change watcher. IMPORTANT: this does NOT attempt an in-process
|
|
454
487
|
// reload. The previous implementation cleared the router + service registry
|
|
@@ -462,11 +495,11 @@ export class Ignitor {
|
|
|
462
495
|
// the Ignitor without a supervisor. A future true HMR needs a loader-hook
|
|
463
496
|
// (hot-hook style) that can invalidate the ESM cache — plug it in here.
|
|
464
497
|
if (this.isDevMode()) {
|
|
465
|
-
const watchDirs = this
|
|
466
|
-
this
|
|
498
|
+
const watchDirs = this.#config.watchDirs ?? ['app', 'start'];
|
|
499
|
+
this.#hotReloadCleanup = startHotReload({
|
|
467
500
|
watchDirs,
|
|
468
501
|
onReload: () => {
|
|
469
|
-
this
|
|
502
|
+
this.#handleError({
|
|
470
503
|
type: 'system.info',
|
|
471
504
|
source: 'HotReload',
|
|
472
505
|
message: 'File change detected — a process restart is required to apply it (`ream dev` restarts automatically).',
|
|
@@ -475,7 +508,7 @@ export class Ignitor {
|
|
|
475
508
|
});
|
|
476
509
|
},
|
|
477
510
|
logger: {
|
|
478
|
-
info: (msg) => this
|
|
511
|
+
info: (msg) => this.#handleError({
|
|
479
512
|
type: 'system.info',
|
|
480
513
|
source: 'HotReload',
|
|
481
514
|
message: msg,
|
|
@@ -485,19 +518,19 @@ export class Ignitor {
|
|
|
485
518
|
},
|
|
486
519
|
});
|
|
487
520
|
}
|
|
488
|
-
this
|
|
521
|
+
this.#phase = 'ready';
|
|
489
522
|
}
|
|
490
523
|
/**
|
|
491
524
|
* Auto-load config/*.ts files into app.config store.
|
|
492
525
|
* Each file's default export is stored under its filename (without extension).
|
|
493
526
|
* e.g. config/database.ts → app.config.get('database')
|
|
494
527
|
*/
|
|
495
|
-
async autoloadConfig() {
|
|
528
|
+
async #autoloadConfig() {
|
|
496
529
|
const { readdirSync, existsSync } = await import('node:fs');
|
|
497
530
|
const { join, basename } = await import('node:path');
|
|
498
531
|
const { fileURLToPath, pathToFileURL } = await import('node:url');
|
|
499
|
-
const configDir = this
|
|
500
|
-
? join(fileURLToPath(this
|
|
532
|
+
const configDir = this.#appRoot
|
|
533
|
+
? join(fileURLToPath(this.#appRoot), 'config')
|
|
501
534
|
: join(process.cwd(), 'config');
|
|
502
535
|
if (!existsSync(configDir))
|
|
503
536
|
return;
|
|
@@ -518,15 +551,15 @@ export class Ignitor {
|
|
|
518
551
|
// is what AdonisJS does (`fsImportAll`). Spread rather than stored as-is:
|
|
519
552
|
// an ESM namespace is sealed and null-prototype, so a later merge into
|
|
520
553
|
// this entry would fail on it.
|
|
521
|
-
this
|
|
554
|
+
this.#app.config.set(key, 'default' in mod ? mod.default : { ...mod });
|
|
522
555
|
}
|
|
523
556
|
}
|
|
524
557
|
/**
|
|
525
558
|
* Auto-load module files (routes.ts, etc.) from the modules directory.
|
|
526
559
|
* Scans modules.path for subdirectories and imports matching files.
|
|
527
560
|
*/
|
|
528
|
-
async autoloadModules() {
|
|
529
|
-
const modulesConfig = this
|
|
561
|
+
async #autoloadModules() {
|
|
562
|
+
const modulesConfig = this.#reamrc?.modules;
|
|
530
563
|
if (!modulesConfig?.path)
|
|
531
564
|
return;
|
|
532
565
|
const { readdirSync, existsSync } = await import('node:fs');
|
|
@@ -534,8 +567,8 @@ export class Ignitor {
|
|
|
534
567
|
const { fileURLToPath } = await import('node:url');
|
|
535
568
|
const { pathToFileURL } = await import('node:url');
|
|
536
569
|
// Resolve modules path relative to app root or cwd
|
|
537
|
-
const basePath = this
|
|
538
|
-
? join(fileURLToPath(this
|
|
570
|
+
const basePath = this.#appRoot
|
|
571
|
+
? join(fileURLToPath(this.#appRoot), modulesConfig.path)
|
|
539
572
|
: resolve(modulesConfig.path);
|
|
540
573
|
if (!existsSync(basePath))
|
|
541
574
|
return;
|
|
@@ -592,22 +625,27 @@ export class Ignitor {
|
|
|
592
625
|
this.#shutdownHandle = undefined;
|
|
593
626
|
});
|
|
594
627
|
await attempt(() => {
|
|
595
|
-
if (this
|
|
596
|
-
this
|
|
628
|
+
if (this.#hotReloadCleanup)
|
|
629
|
+
this.#hotReloadCleanup();
|
|
597
630
|
});
|
|
598
631
|
await attempt(async () => {
|
|
599
|
-
if (this
|
|
600
|
-
await this
|
|
632
|
+
if (this.#_httpServer)
|
|
633
|
+
await this.#_httpServer.close();
|
|
601
634
|
});
|
|
602
|
-
await attempt(() => this
|
|
603
|
-
await attempt(() => this
|
|
635
|
+
await attempt(() => this.#errorBoundary.uninstall());
|
|
636
|
+
await attempt(() => this.#app.shutdown());
|
|
604
637
|
// Release the process-wide service locators (ownership-guarded): a stopped
|
|
605
638
|
// app must not keep its container/router reachable through the module
|
|
606
639
|
// singletons, and must not clobber a newer Ignitor's bindings.
|
|
607
|
-
clearApp(this
|
|
608
|
-
clearRouter(this
|
|
609
|
-
clearServer(this
|
|
610
|
-
|
|
640
|
+
clearApp(this.#app);
|
|
641
|
+
clearRouter(this.#router);
|
|
642
|
+
clearServer(this.#server);
|
|
643
|
+
// `signedUrl` is what the router was handed; the signer behind
|
|
644
|
+
// `services/encryption` is the same object, so releasing it here keeps the
|
|
645
|
+
// locator from outliving the app that built it.
|
|
646
|
+
if (this.#signer)
|
|
647
|
+
clearEncryption(this.#signer);
|
|
648
|
+
this.#phase = 'shutdown';
|
|
611
649
|
if (errors.length === 1)
|
|
612
650
|
throw errors[0];
|
|
613
651
|
if (errors.length > 1) {
|
|
@@ -616,7 +654,7 @@ export class Ignitor {
|
|
|
616
654
|
}
|
|
617
655
|
// ─── Accessors ────────────────────────────────────────────
|
|
618
656
|
async port() {
|
|
619
|
-
return this
|
|
657
|
+
return this.#_httpServer ? this.#_httpServer.port() : 0;
|
|
620
658
|
}
|
|
621
659
|
/**
|
|
622
660
|
* The address the HTTP server was bound to. Resolved at start; undefined
|
|
@@ -628,18 +666,18 @@ export class Ignitor {
|
|
|
628
666
|
return this.#host;
|
|
629
667
|
}
|
|
630
668
|
getApp() {
|
|
631
|
-
return this
|
|
669
|
+
return this.#app;
|
|
632
670
|
}
|
|
633
671
|
/**
|
|
634
672
|
* The application root passed to `new Ignitor(new URL('../', import.meta.url))`.
|
|
635
673
|
* Undefined for inline/test ignitors created without one.
|
|
636
674
|
*/
|
|
637
675
|
getAppRoot() {
|
|
638
|
-
return this
|
|
676
|
+
return this.#appRoot;
|
|
639
677
|
}
|
|
640
678
|
/** Has the application already been started? */
|
|
641
679
|
isStarted() {
|
|
642
|
-
return this
|
|
680
|
+
return this.#phase === 'started' || this.#phase === 'ready';
|
|
643
681
|
}
|
|
644
682
|
/**
|
|
645
683
|
* The `consoleApp` façade — the programmatic console.
|
|
@@ -658,22 +696,22 @@ export class Ignitor {
|
|
|
658
696
|
const consoleApp = new Console({ kernel, load });
|
|
659
697
|
this.#console = consoleApp;
|
|
660
698
|
setConsole(consoleApp);
|
|
661
|
-
this
|
|
699
|
+
this.#app.container.singleton('console', () => consoleApp);
|
|
662
700
|
return consoleApp;
|
|
663
701
|
}
|
|
664
702
|
/** The loaded rc file, if `useRcFile()` was called. */
|
|
665
703
|
getRcFile() {
|
|
666
|
-
return this
|
|
704
|
+
return this.#reamrc;
|
|
667
705
|
}
|
|
668
706
|
/** The custom module importer, when the app provided one. */
|
|
669
707
|
getImporter() {
|
|
670
|
-
return this
|
|
708
|
+
return this.#config.importer;
|
|
671
709
|
}
|
|
672
710
|
getRouter() {
|
|
673
|
-
return this
|
|
711
|
+
return this.#router;
|
|
674
712
|
}
|
|
675
713
|
getServer() {
|
|
676
|
-
return this
|
|
714
|
+
return this.#server;
|
|
677
715
|
}
|
|
678
716
|
/**
|
|
679
717
|
* Get the kernel callback (for serverless / testing). Memoized — building a
|
|
@@ -681,24 +719,24 @@ export class Ignitor {
|
|
|
681
719
|
*/
|
|
682
720
|
getKernel() {
|
|
683
721
|
this.#kernel ??= createHttpKernel({
|
|
684
|
-
router: this
|
|
685
|
-
middleware: this
|
|
686
|
-
container: this
|
|
687
|
-
exceptionHandler: this
|
|
688
|
-
serverMiddleware: this
|
|
689
|
-
routerMiddleware: this
|
|
722
|
+
router: this.#router,
|
|
723
|
+
middleware: this.#middleware,
|
|
724
|
+
container: this.#app.container,
|
|
725
|
+
exceptionHandler: this.#server.getErrorHandler() ?? new ExceptionHandler(!this.#app.inProduction),
|
|
726
|
+
serverMiddleware: this.#server.getServerMiddleware(),
|
|
727
|
+
routerMiddleware: this.#router.getRouterMiddleware(),
|
|
690
728
|
});
|
|
691
729
|
return this.#kernel;
|
|
692
730
|
}
|
|
693
731
|
#kernel;
|
|
694
732
|
isDevMode() {
|
|
695
|
-
return this
|
|
733
|
+
return this.#app.inDev;
|
|
696
734
|
}
|
|
697
735
|
getPhase() {
|
|
698
|
-
return this
|
|
736
|
+
return this.#phase;
|
|
699
737
|
}
|
|
700
|
-
handleError(event) {
|
|
701
|
-
for (const listener of this
|
|
738
|
+
#handleError(event) {
|
|
739
|
+
for (const listener of this.#errorListeners) {
|
|
702
740
|
try {
|
|
703
741
|
listener(event);
|
|
704
742
|
}
|
|
@@ -713,7 +751,7 @@ export class Ignitor {
|
|
|
713
751
|
* shutdown logger so drain progress / timeouts are observable.
|
|
714
752
|
*/
|
|
715
753
|
#emitSystemInfo(source, message, kind) {
|
|
716
|
-
this
|
|
754
|
+
this.#handleError({
|
|
717
755
|
type: kind === 'error' ? 'system.error' : 'system.info',
|
|
718
756
|
source,
|
|
719
757
|
message,
|