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