@lithia-js/core 1.0.0-canary.1 → 1.0.0-canary.3

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/src/lithia.ts DELETED
@@ -1,777 +0,0 @@
1
- /**
2
- * Core runtime entry for Lithia.
3
- *
4
- * This module provides the main `Lithia` class, which orchestrates the entire
5
- * framework lifecycle including:
6
- * - Building the project with the native Rust compiler
7
- * - Loading route and event manifests
8
- * - Managing the HTTP server lifecycle
9
- * - Configuration management and hot-reloading
10
- * - Dependency injection and global middleware
11
- * - Lifecycle event emission (built, error, config:changed)
12
- *
13
- * @module lithia
14
- */
15
-
16
- import { EventEmitter } from "node:events";
17
- import { existsSync, readFileSync } from "node:fs";
18
- import path from "node:path";
19
- import {
20
- buildProject,
21
- type Event,
22
- type EventsManifest,
23
- type Route,
24
- type RoutesManifest,
25
- schemaVersion,
26
- } from "@lithia-js/native";
27
- import { red } from "@lithia-js/utils";
28
- import sourceMapSupport from "source-map-support";
29
- import { ConfigProvider, type LithiaOptions } from "./config";
30
- import { type LithiaContext, lithiaContext } from "./context/lithia-context";
31
- import {
32
- InvalidBootstrapModuleError,
33
- LithiaError,
34
- ManifestLoadError,
35
- SchemaVersionMismatchError,
36
- } from "./errors";
37
- import type { InjectionKey } from "./hooks/dependency-hooks";
38
- import { logger } from "./logger";
39
- import { coldImport, isAsyncFunction } from "./module-loader";
40
- import {
41
- createHttpServerFromConfig,
42
- type HttpServer,
43
- } from "./server/http-server";
44
- import type { LithiaMiddleware } from "./server/request-processor";
45
-
46
- /**
47
- * Configuration keys that require a full server restart when changed.
48
- *
49
- * These settings cannot be hot-reloaded and require stopping/starting
50
- * the HTTP server to take effect.
51
- */
52
- const RESTART_CONFIG_PREFIXES = ["http.port", "http.host", "http.ssl"];
53
-
54
- // Install source map support for better stack traces in development
55
- sourceMapSupport.install({
56
- environment: "node",
57
- handleUncaughtExceptions: false,
58
- });
59
-
60
- /**
61
- * The runtime environment mode.
62
- *
63
- * Influences logging verbosity, error output formatting, and features like
64
- * configuration hot-reloading (enabled only in development).
65
- */
66
- export type Environment = "production" | "development";
67
-
68
- /**
69
- * Options required to create a Lithia instance.
70
- *
71
- * These options configure the fundamental paths and environment settings
72
- * that Lithia needs to build and run your application.
73
- */
74
- export interface LithiaCreateOptions {
75
- /**
76
- * Runtime environment mode.
77
- *
78
- * - `development`: Enables config watching, verbose logging, source maps
79
- * - `production`: Optimized for performance with minimal logging
80
- */
81
- environment: Environment;
82
-
83
- /**
84
- * Absolute path to the source directory containing application code.
85
- *
86
- * @example "./src"
87
- */
88
- sourceRoot: string;
89
-
90
- /**
91
- * Absolute path to the output directory for compiled JavaScript.
92
- *
93
- * @example "./dist"
94
- */
95
- outRoot: string;
96
- }
97
-
98
- /**
99
- * Lithia runtime controller and main framework orchestrator.
100
- *
101
- * This is the core class that manages the entire Lithia application lifecycle.
102
- * It acts as a singleton and coordinates:
103
- * - Project compilation via the native Rust builder
104
- * - Route and event manifest loading
105
- * - HTTP server lifecycle management
106
- * - Configuration management with hot-reloading in development
107
- * - Global middleware and dependency injection
108
- * - Lifecycle event emission and handling
109
- *
110
- * @remarks
111
- * Always use `Lithia.create()` to obtain the singleton instance.
112
- * Direct instantiation is not supported.
113
- *
114
- * @example
115
- * ```typescript
116
- * const lithia = await Lithia.create({
117
- * environment: 'development',
118
- * sourceRoot: './src',
119
- * outRoot: './dist'
120
- * });
121
- *
122
- * lithia.build();
123
- * await lithia.start();
124
- * ```
125
- */
126
- export class Lithia {
127
- /** Singleton instance of Lithia. */
128
- private static instance: Lithia;
129
-
130
- /** Current runtime environment (production or development). */
131
- private environment: Environment;
132
-
133
- /** Absolute path to the source directory. */
134
- private sourceRoot: string;
135
-
136
- /** Absolute path to the compiled output directory. */
137
- private outRoot: string;
138
-
139
- /** Array of loaded application routes from the manifest. */
140
- private routes: Route[];
141
-
142
- /** Array of loaded Socket.IO events from the manifest. */
143
- private events: Event[];
144
-
145
- /** Current runtime configuration. */
146
- private config: LithiaOptions;
147
-
148
- /** Event emitter for lifecycle events (built, error, config:changed). */
149
- private emitter: EventEmitter;
150
-
151
- /** Configuration provider that handles loading and watching. */
152
- private configProvider: ConfigProvider;
153
-
154
- /** HTTP server instance (created when start() is called). */
155
- private httpServer?: HttpServer;
156
-
157
- /** Flag indicating whether the HTTP server is currently running. */
158
- private serverRunning = false;
159
-
160
- /** Handle for the configuration file watcher (development only). */
161
- private configWatchHandle?: { close?: () => void };
162
-
163
- /**
164
- * Global middlewares executed for every HTTP request.
165
- *
166
- * These run before route-specific handlers and can modify requests,
167
- * responses, or perform authentication/logging.
168
- */
169
- public globalMiddlewares: LithiaMiddleware[] = [];
170
-
171
- /**
172
- * Global dependency injection container.
173
- *
174
- * Stores dependencies registered via `provide()` that can be injected
175
- * into route handlers and middlewares.
176
- */
177
- public globalDependencies = new Map<any, any>();
178
-
179
- /**
180
- * Gets the current runtime configuration.
181
- *
182
- * @returns The current LithiaOptions configuration object
183
- */
184
- public get options(): LithiaOptions {
185
- return this.config;
186
- }
187
-
188
- /**
189
- * Private constructor to enforce singleton pattern.
190
- *
191
- * Use `Lithia.create()` instead of instantiating directly.
192
- *
193
- * @private
194
- */
195
- private constructor() {
196
- this.routes = [];
197
- this.events = [];
198
- this.emitter = new EventEmitter();
199
- this.configProvider = new ConfigProvider();
200
- }
201
-
202
- /**
203
- * Creates or returns the global Lithia singleton instance.
204
- *
205
- * This is the primary entry point for creating a Lithia application.
206
- * On first call, it initializes the framework with the provided options,
207
- * loads configuration, and (in development mode) sets up configuration
208
- * hot-reloading.
209
- *
210
- * Subsequent calls return the same singleton instance.
211
- *
212
- * @param options - Configuration for paths and environment
213
- * @returns The initialized Lithia singleton instance
214
- *
215
- * @example
216
- * ```typescript
217
- * const lithia = await Lithia.create({
218
- * environment: 'development',
219
- * sourceRoot: path.resolve('./src'),
220
- * outRoot: path.resolve('./dist')
221
- * });
222
- * ```
223
- */
224
- static async create(options: LithiaCreateOptions) {
225
- if (!Lithia.instance) {
226
- const lithia = new Lithia();
227
- await lithia.initialize(options);
228
- Lithia.instance = lithia;
229
- }
230
-
231
- return Lithia.instance;
232
- }
233
-
234
- /**
235
- * Registers a global middleware to run on every HTTP request.
236
- *
237
- * Middlewares are executed in the order they are registered, before
238
- * route-specific handlers. They can modify the request/response or
239
- * perform cross-cutting concerns like logging and authentication.
240
- *
241
- * @param middleware - The middleware function to register
242
- * @returns The Lithia instance for method chaining
243
- *
244
- * @example
245
- * ```typescript
246
- * lithia.use(async (req, res, next) => {
247
- * console.log(`${req.method} ${req.url}`);
248
- * await next();
249
- * });
250
- * ```
251
- */
252
- use(middleware: LithiaMiddleware) {
253
- this.globalMiddlewares.push(middleware);
254
- return this;
255
- }
256
-
257
- /**
258
- * Registers a global dependency for dependency injection.
259
- *
260
- * Registered dependencies can be injected into route handlers and
261
- * middlewares using the `inject()` hook.
262
- *
263
- * @param key - The injection key (use `createInjectionKey<T>()` to create)
264
- * @param value - The dependency value to provide
265
- * @returns The Lithia instance for method chaining
266
- *
267
- * @example
268
- * ```typescript
269
- * const dbKey = createInjectionKey<Database>('database');
270
- * lithia.provide(dbKey, new Database());
271
- * ```
272
- */
273
- provide<T>(key: InjectionKey<T>, value: T) {
274
- this.globalDependencies.set(key, value);
275
- return this;
276
- }
277
-
278
- /**
279
- * Initializes the Lithia instance with configuration and environment settings.
280
- *
281
- * This method:
282
- * - Sets up the environment, source root, and output root
283
- * - Loads the initial configuration from lithia.config.ts
284
- * - Configures the event emitter for lifecycle events
285
- * - Sets up configuration hot-reloading (development mode only)
286
- *
287
- * @param options - Initialization options
288
- * @private
289
- */
290
- private async initialize(options: LithiaCreateOptions) {
291
- this.environment = options.environment;
292
- this.sourceRoot = options.sourceRoot;
293
- this.outRoot = options.outRoot;
294
- this.config = await this.configProvider.loadConfig();
295
-
296
- this.configureEventEmitter();
297
-
298
- if (options.environment === "development") {
299
- await this.setupConfigWatcher();
300
- }
301
- }
302
-
303
- /**
304
- * Sets up configuration file watching in development mode.
305
- *
306
- * Monitors lithia.config.ts for changes and emits `config:changed` events.
307
- * Logs configuration diffs and warns when changes require a server restart.
308
- *
309
- * @private
310
- */
311
- private async setupConfigWatcher() {
312
- try {
313
- this.configWatchHandle = await this.configProvider.watchConfig((ctx) => {
314
- this.config = ctx.newConfig;
315
- this.emit("config:changed", ctx.newConfig);
316
-
317
- try {
318
- const diffs = typeof ctx.getDiff === "function" ? ctx.getDiff() : [];
319
-
320
- if (diffs && diffs.length > 0) {
321
- logger.event(`Config updated — ${diffs.length} change(s)`);
322
-
323
- // Log first 20 changes to avoid spam
324
- for (const d of diffs.slice(0, 20)) {
325
- const requiresRestart = this.configChangeRequiresRestart(d.key);
326
-
327
- if (requiresRestart) {
328
- logger.warn(
329
- ` • ${d.key}: ${d.oldValue} → ${d.newValue} (requires server restart)`,
330
- );
331
- } else {
332
- logger.info(` • ${d.key}: ${d.oldValue} → ${d.newValue}`);
333
- }
334
- }
335
- }
336
- } catch (logErr) {
337
- logger.debug("Failed to summarize config diff:", logErr);
338
- }
339
- }, undefined);
340
- } catch (err) {
341
- this.emitter.emit("error", err);
342
- }
343
- }
344
-
345
- /**
346
- * Checks if a configuration key change requires a server restart.
347
- *
348
- * @param key - The configuration key that changed
349
- * @returns True if the change requires restarting the server
350
- * @private
351
- */
352
- private configChangeRequiresRestart(key: string): boolean {
353
- return RESTART_CONFIG_PREFIXES.some(
354
- (prefix) => key === prefix || key.startsWith(`${prefix}.`),
355
- );
356
- }
357
-
358
- /**
359
- * Loads and executes the optional user bootstrap module.
360
- *
361
- * Looks for `src/app/_server.ts` (compiled to `dist/app/_server.js`).
362
- * The bootstrap module should export an async default function that
363
- * receives the Lithia instance and can register middlewares, providers, etc.
364
- *
365
- * @throws {InvalidBootstrapModuleError} If the module has invalid structure
366
- * @private
367
- */
368
- private async loadBootstrapModule(): Promise<void> {
369
- const bootstrapFile = path.join(this.outRoot, "app", "_server.js");
370
-
371
- if (!existsSync(bootstrapFile)) {
372
- return; // Bootstrap module is optional
373
- }
374
-
375
- try {
376
- const mod = await coldImport<any>(
377
- bootstrapFile,
378
- this.environment === "development",
379
- );
380
-
381
- this.validateBootstrapModule(mod, bootstrapFile);
382
-
383
- // Execute the bootstrap function
384
- await mod.default(this);
385
- } catch (err) {
386
- // Don't swallow fatal validation errors
387
- if (err instanceof InvalidBootstrapModuleError) {
388
- this.emitter.emit("error", err);
389
- throw err; // Prevent server from starting with invalid bootstrap
390
- }
391
- logger.error(`Failed to load _server.ts: ${err}`);
392
- }
393
- }
394
-
395
- /**
396
- * Validates the structure of the bootstrap module.
397
- *
398
- * Ensures the module:
399
- * - Has a default export
400
- * - Default export is a function
401
- * - Default export is async
402
- *
403
- * @param mod - The loaded module
404
- * @param filePath - Path to the module file (for error messages)
405
- * @throws {InvalidBootstrapModuleError} If validation fails
406
- * @private
407
- */
408
- private validateBootstrapModule(mod: any, filePath: string): void {
409
- if (!mod.default) {
410
- throw new InvalidBootstrapModuleError(filePath, "missing default export");
411
- }
412
-
413
- if (typeof mod.default !== "function") {
414
- throw new InvalidBootstrapModuleError(
415
- filePath,
416
- "default export is not a function",
417
- );
418
- }
419
-
420
- if (!isAsyncFunction(mod.default)) {
421
- throw new InvalidBootstrapModuleError(
422
- filePath,
423
- "default export is not an async function",
424
- );
425
- }
426
- }
427
-
428
- /**
429
- * Starts the HTTP server.
430
- *
431
- * This method:
432
- * 1. Loads and executes the optional bootstrap module (_server.ts)
433
- * 2. Creates the HTTP server with current configuration
434
- * 3. Starts listening on the configured host and port
435
- *
436
- * Safe to call multiple times; subsequent calls are no-ops while the
437
- * server is running.
438
- *
439
- * @throws {InvalidBootstrapModuleError} If bootstrap module is invalid
440
- * @throws {Error} If server fails to start
441
- *
442
- * @example
443
- * ```typescript
444
- * await lithia.start();
445
- * // Server is now listening on configured port
446
- * ```
447
- */
448
- async start() {
449
- if (this.serverRunning) return;
450
-
451
- // Load optional user bootstrap logic
452
- await this.loadBootstrapModule();
453
-
454
- // Create and start the HTTP server
455
- this.httpServer = createHttpServerFromConfig({
456
- options: this.config,
457
- lithia: this,
458
- });
459
-
460
- try {
461
- await this.httpServer.listen();
462
- this.serverRunning = true;
463
- } catch (err) {
464
- this.emitter.emit("error", err);
465
- }
466
- }
467
-
468
- /**
469
- * Stops the HTTP server.
470
- *
471
- * Gracefully shuts down the server and closes all active connections.
472
- * No-op if the server isn't currently running.
473
- *
474
- * @example
475
- * ```typescript
476
- * await lithia.stop();
477
- * // Server is now stopped
478
- * ```
479
- */
480
- async stop() {
481
- if (!this.serverRunning) return;
482
- try {
483
- await this.httpServer?.close();
484
- this.serverRunning = false;
485
- } catch (err) {
486
- this.emitter.emit("error", err);
487
- }
488
- }
489
-
490
- /**
491
- * Executes a function within the Lithia context.
492
- *
493
- * Provides access to the global dependency container during execution.
494
- * Used internally by the request processor to make dependencies available
495
- * to route handlers and middlewares.
496
- *
497
- * @param fn - Async function to execute within the context
498
- * @returns The result of the function execution
499
- * @template T - The return type of the function
500
- *
501
- * @example
502
- * ```typescript
503
- * const result = await lithia.runWithContext(async () => {
504
- * const db = inject(dbKey);
505
- * return db.query('SELECT * FROM users');
506
- * });
507
- * ```
508
- */
509
- async runWithContext<T>(fn: () => Promise<T>): Promise<T> {
510
- const ctx: LithiaContext = {
511
- dependencies: this.globalDependencies,
512
- };
513
-
514
- return lithiaContext.run(ctx, fn);
515
- }
516
-
517
- /**
518
- * Gets the current runtime environment.
519
- *
520
- * @returns The environment mode ('production' or 'development')
521
- */
522
- getEnvironment() {
523
- return this.environment;
524
- }
525
-
526
- /**
527
- * Gets the currently loaded routes from the manifest.
528
- *
529
- * Routes are loaded after a successful build via the `loadRoutes()` method.
530
- *
531
- * @returns Array of route definitions
532
- */
533
- getRoutes() {
534
- return this.routes;
535
- }
536
-
537
- /**
538
- * Gets the currently loaded Socket.IO events from the manifest.
539
- *
540
- * Events are loaded after a successful build via the `loadEvents()` method.
541
- *
542
- * @returns Array of event definitions
543
- */
544
- getEvents() {
545
- return this.events;
546
- }
547
-
548
- /**
549
- * Gets the current runtime configuration.
550
- *
551
- * @returns The current LithiaOptions configuration object
552
- */
553
- getConfig() {
554
- return this.config;
555
- }
556
-
557
- /**
558
- * Configures internal event handlers for the lifecycle emitter.
559
- *
560
- * Sets up handlers for:
561
- * - `built` event: Triggered after successful compilation, loads routes and events
562
- * - `error` event: Logs errors with appropriate severity and exits on fatal errors
563
- *
564
- * @private
565
- */
566
- private configureEventEmitter() {
567
- // Auto-load routes and events after successful build
568
- this.emitter.on("built", (durationMs: number) => {
569
- logger.success(`Build completed in ${durationMs.toFixed(2)}ms`);
570
-
571
- this.loadRoutes();
572
- this.loadEvents();
573
- });
574
-
575
- // Handle errors with appropriate logging and exit behavior
576
- this.emitter.on("error", (err: any) => {
577
- const level = err instanceof LithiaError ? err.level : "error";
578
-
579
- logger.error(
580
- `[${red(level.toUpperCase())}] ${err?.code ?? "UNKNOWN"} - ${err?.message ?? String(err)}`,
581
- );
582
-
583
- if (err?.cause) {
584
- logger.debug(`cause:`, err.cause);
585
- }
586
-
587
- // Fatal errors should terminate the process
588
- if (level === "fatal") {
589
- process.exit(1);
590
- }
591
- });
592
- }
593
-
594
- /**
595
- * Compiles the project using the native Rust compiler.
596
- *
597
- * This performs a synchronous build that:
598
- * - Scans the source directory for routes and events
599
- * - Compiles TypeScript to JavaScript
600
- * - Generates route and event manifests (routes.json, events.json)
601
- *
602
- * On success, emits the `built` event with build duration in milliseconds.
603
- * On failure, emits the `error` event.
604
- *
605
- * @example
606
- * ```typescript
607
- * lithia.on('built', () => {
608
- * console.log('Build complete!');
609
- * });
610
- * lithia.build();
611
- * ```
612
- */
613
- build() {
614
- const start = process.hrtime.bigint();
615
- try {
616
- buildProject(this.sourceRoot, this.outRoot);
617
- const durationMs = Number(process.hrtime.bigint() - start) / 1_000_000;
618
- this.emitter.emit("built", durationMs);
619
- } catch (err) {
620
- this.emitter.emit("error", err);
621
- }
622
- }
623
-
624
- /**
625
- * Loads and validates a JSON manifest file.
626
- *
627
- * @param fileName - Name of the manifest file (e.g., 'routes.json')
628
- * @returns The parsed manifest object
629
- * @throws {ManifestLoadError} If the file cannot be read or parsed
630
- * @throws {SchemaVersionMismatchError} If the manifest version doesn't match
631
- * @private
632
- */
633
- private loadManifest<T extends { version: string }>(
634
- fileName: string,
635
- ): T | null {
636
- const manifestPath = path.join(this.outRoot, fileName);
637
-
638
- if (!existsSync(manifestPath)) {
639
- return null;
640
- }
641
-
642
- const raw = readFileSync(manifestPath, "utf-8");
643
- const manifest = JSON.parse(raw) as T;
644
- const expectedVersion = schemaVersion();
645
-
646
- if (manifest.version !== expectedVersion) {
647
- throw new SchemaVersionMismatchError(expectedVersion, manifest.version);
648
- }
649
-
650
- return manifest;
651
- }
652
-
653
- /**
654
- * Loads and validates the routes manifest generated by the compiler.
655
- *
656
- * Reads `routes.json` from the output directory and populates the
657
- * internal routes array. This is automatically called after a successful
658
- * build (via the `built` event handler).
659
- *
660
- * On error, emits an `error` event and leaves routes unchanged.
661
- */
662
- loadRoutes() {
663
- try {
664
- const manifest = this.loadManifest<RoutesManifest>("routes.json");
665
-
666
- if (!manifest) {
667
- return;
668
- }
669
-
670
- this.routes = manifest.routes;
671
- } catch (err) {
672
- if (err instanceof SchemaVersionMismatchError) {
673
- this.emitter.emit("error", err);
674
- return;
675
- }
676
-
677
- this.emitter.emit("error", new ManifestLoadError(err));
678
- }
679
- }
680
-
681
- /**
682
- * Loads and validates the events manifest generated by the compiler.
683
- *
684
- * Reads `events.json` from the output directory and populates the
685
- * internal events array. This is automatically called after a successful
686
- * build (via the `built` event handler).
687
- *
688
- * On error, emits an `error` event and leaves events unchanged.
689
- */
690
- loadEvents() {
691
- try {
692
- const manifest = this.loadManifest<EventsManifest>("events.json");
693
-
694
- if (!manifest) {
695
- return;
696
- }
697
-
698
- this.events = manifest.events;
699
- } catch (err) {
700
- if (err instanceof SchemaVersionMismatchError) {
701
- this.emitter.emit("error", err);
702
- return;
703
- }
704
-
705
- this.emitter.emit("error", new ManifestLoadError(err));
706
- }
707
- }
708
-
709
- /**
710
- * Gets the internal EventEmitter instance.
711
- *
712
- * The event emitter is used for lifecycle events like:
713
- * - `built`: Emitted after successful compilation
714
- * - `error`: Emitted when errors occur
715
- * - `config:changed`: Emitted when configuration is updated (dev mode)
716
- *
717
- * @returns The internal EventEmitter instance
718
- */
719
- getEventEmitter() {
720
- return this.emitter;
721
- }
722
-
723
- /**
724
- * Emits a lifecycle event.
725
- *
726
- * @param event - The event name to emit
727
- * @param payload - Optional payload data for the event
728
- * @returns True if the event had listeners, false otherwise
729
- *
730
- * @example
731
- * ```typescript
732
- * lithia.emit('custom:event', { data: 'value' });
733
- * ```
734
- */
735
- emit(event: string, payload?: any) {
736
- return this.emitter.emit(event, payload);
737
- }
738
-
739
- /**
740
- * Registers a listener for a lifecycle event.
741
- *
742
- * @param event - The event name to listen for
743
- * @param listener - Callback function to execute when the event is emitted
744
- *
745
- * @example
746
- * ```typescript
747
- * lithia.on('built', (durationMs) => {
748
- * console.log(`Build took ${durationMs}ms`);
749
- * });
750
- *
751
- * lithia.on('error', (error) => {
752
- * console.error('An error occurred:', error);
753
- * });
754
- * ```
755
- */
756
- on(event: string, listener: (...args: any[]) => void) {
757
- this.emitter.on(event, listener);
758
- }
759
-
760
- /**
761
- * Cleans up resources and removes all event listeners.
762
- *
763
- * This method should be called when shutting down the application to:
764
- * - Close the configuration file watcher
765
- * - Remove all event listeners to prevent memory leaks
766
- *
767
- * @example
768
- * ```typescript
769
- * await lithia.stop();
770
- * lithia.close();
771
- * ```
772
- */
773
- close() {
774
- this.configWatchHandle?.close?.();
775
- this.emitter.removeAllListeners();
776
- }
777
- }