@lithia-js/core 1.0.0-canary.2 → 1.0.0-canary.4

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/config.ts DELETED
@@ -1,212 +0,0 @@
1
- import type { DeepPartial } from "@lithia-js/utils";
2
- import {
3
- type C12InputConfig,
4
- loadConfig,
5
- type WatchConfigOptions,
6
- watchConfig,
7
- } from "c12";
8
- import { klona } from "klona";
9
-
10
- /** Result type for lifecycle hooks — may be synchronous or async. */
11
- export type HookResult = void | Promise<void>;
12
-
13
- /** Available lifecycle hooks supported by the runtime. */
14
- export interface LithiaHooks {
15
- /** Called before the HTTP server starts. */
16
- "before:start": () => HookResult;
17
- /** Called after the HTTP server has started. */
18
- "after:start": () => HookResult;
19
- }
20
-
21
- /** Shape of the runtime configuration used by Lithia. */
22
- export interface LithiaOptions {
23
- debug: boolean;
24
- http: {
25
- port: number;
26
- host: string;
27
- maxBodySize?: number;
28
- ssl?: {
29
- key: string;
30
- cert: string;
31
- passphrase?: string;
32
- };
33
- cors: {
34
- origin?: string[];
35
- methods?: string[];
36
- allowedHeaders?: string[];
37
- exposedHeaders?: string[];
38
- credentials?: boolean;
39
- maxAge?: number;
40
- };
41
- mimeTypes?: Record<string, string>;
42
- };
43
- static?: {
44
- root: string;
45
- prefix?: string;
46
- };
47
- studio: {
48
- enabled: boolean;
49
- };
50
- logging: {
51
- /** Enable request logging. Critical errors (5xx) are always logged. */
52
- requests: boolean;
53
- /** Enable event logging. Critical errors are always logged. */
54
- events: boolean;
55
- };
56
- hooks: {
57
- [K in keyof LithiaHooks]: LithiaHooks[K];
58
- };
59
- }
60
-
61
- /** Partial configuration accepted by `defineConfig` and the config loader. */
62
- export interface LithiaConfig
63
- extends DeepPartial<LithiaOptions>,
64
- C12InputConfig<LithiaConfig> {}
65
-
66
- /** Default runtime configuration used when no overrides are provided. */
67
- export const DEFAULT_CONFIG: LithiaConfig = {
68
- debug: false,
69
- http: {
70
- port: 3000,
71
- host: "localhost",
72
- maxBodySize: 1024 * 1024,
73
- cors: {
74
- origin: ["*"],
75
- methods: ["*"],
76
- allowedHeaders: ["*"],
77
- exposedHeaders: ["X-Powered-By"],
78
- credentials: false,
79
- maxAge: 86400,
80
- },
81
- },
82
- logging: {
83
- requests: true,
84
- events: true,
85
- },
86
- hooks: {},
87
- studio: {
88
- enabled: false,
89
- },
90
- };
91
-
92
- type LoadConfigOptions = {
93
- watch?: boolean;
94
- c12?: WatchConfigOptions;
95
- overrides?: LithiaConfig;
96
- };
97
-
98
- /** Error thrown when configuration validation fails. */
99
- export class ConfigValidationError extends Error {
100
- constructor(
101
- message: string,
102
- public readonly field?: string,
103
- ) {
104
- super(message);
105
- this.name = "ConfigValidationError";
106
- }
107
- }
108
-
109
- /** Context passed to `watchConfig` callbacks describing the updated config. */
110
- export interface ConfigUpdateContext {
111
- /** Returns a list of diffs between old and new config. */
112
- getDiff: () => Array<{
113
- key: string;
114
- type: string;
115
- newValue: unknown;
116
- oldValue: unknown;
117
- }>;
118
- /** The new fully materialized config. */
119
- newConfig: LithiaOptions;
120
- /** The previous config prior to the update. */
121
- oldConfig: LithiaOptions;
122
- }
123
-
124
- /** Provider responsible for loading and optionally watching the runtime config. */
125
- export class ConfigProvider {
126
- /**
127
- * Load the configuration, applying optional overrides.
128
- *
129
- * This delegates to `c12` for reading configuration files and defaults
130
- * and validates the resulting `LithiaOptions` before returning them.
131
- */
132
- async loadConfig(overrides: LithiaConfig = {}, opts: LoadConfigOptions = {}) {
133
- overrides = klona(overrides);
134
-
135
- const configOptions = {
136
- name: "lithia",
137
- configFile: "lithia.config",
138
- cwd: process.cwd(),
139
- dotenv: true,
140
- overrides,
141
- defaults: DEFAULT_CONFIG,
142
- ...opts.c12,
143
- };
144
-
145
- const loadedConfig = await loadConfig<LithiaConfig>(configOptions);
146
- const options = klona(loadedConfig.config) as LithiaOptions;
147
-
148
- // overrides are already applied by c12; no need to re-assign here
149
-
150
- this.validateConfig(options);
151
-
152
- return options;
153
- }
154
-
155
- /**
156
- * Watch the configuration for changes and invoke `onChange` when updates occur.
157
- *
158
- * Returns a handle with a `close()` method to stop watching.
159
- */
160
- async watchConfig(
161
- onChange: (ctx: ConfigUpdateContext) => void | Promise<void>,
162
- overrides: LithiaConfig = {},
163
- opts: LoadConfigOptions = {},
164
- ) {
165
- overrides = klona(overrides);
166
-
167
- const configOptions = {
168
- name: "lithia",
169
- configFile: "lithia.config",
170
- cwd: process.cwd(),
171
- dotenv: true,
172
- overrides,
173
- defaults: DEFAULT_CONFIG,
174
- ...opts.c12,
175
- watch: true,
176
- onUpdate: async (context: any) => {
177
- const newOptions = klona(context.newConfig.config) as LithiaOptions;
178
- this.validateConfig(newOptions);
179
-
180
- await onChange({
181
- getDiff: context.getDiff,
182
- newConfig: newOptions,
183
- oldConfig: context.oldConfig.config as LithiaOptions,
184
- });
185
- },
186
- };
187
-
188
- const handle = await watchConfig<LithiaConfig>(configOptions as any);
189
-
190
- return {
191
- close: () => {
192
- if (typeof (handle as any)?.close === "function") {
193
- (handle as any).close();
194
- }
195
- },
196
- };
197
- }
198
-
199
- private validateConfig(config: LithiaOptions): void {
200
- if (config.http.port < 1 || config.http.port > 65535) {
201
- throw new ConfigValidationError(
202
- `HTTP port must be between 1 and 65535, got ${config.http.port}`,
203
- "http.port",
204
- );
205
- }
206
- }
207
- }
208
-
209
- /** Utility helper used by users to define their config with IDE type hints. */
210
- export function defineConfig(config: LithiaConfig): LithiaConfig {
211
- return config;
212
- }
@@ -1,66 +0,0 @@
1
- /**
2
- * Event context module for Socket.IO events.
3
- *
4
- * Provides context specific to Socket.IO event handling, including access
5
- * to event data passed from the client.
6
- *
7
- * @module context/event-context
8
- */
9
-
10
- import { AsyncLocalStorage } from "node:async_hooks";
11
- import type { Socket } from "socket.io";
12
-
13
- /**
14
- * Socket.IO event handler context.
15
- *
16
- * Contains event-specific information available to event handlers
17
- * during Socket.IO event processing.
18
- */
19
- export type EventContext = {
20
- /**
21
- * Data payload sent with the event.
22
- *
23
- * Contains the data passed from the client when emitting the event.
24
- * The structure depends on what the client sends.
25
- */
26
- data: any;
27
-
28
- /**
29
- * The Socket.IO socket instance.
30
- *
31
- * Represents the client connection that triggered the event.
32
- */
33
- socket: Socket;
34
- };
35
-
36
- /**
37
- * AsyncLocalStorage instance for event context.
38
- *
39
- * Uses Node.js AsyncLocalStorage to provide implicit context propagation
40
- * for Socket.IO event handling across async boundaries.
41
- *
42
- * @see https://nodejs.org/api/async_hooks.html#class-asynclocalstorage
43
- */
44
- export const eventContext = new AsyncLocalStorage<EventContext>();
45
-
46
- /**
47
- * Gets the current event context.
48
- *
49
- * @returns The current EventContext
50
- * @throws {Error} If called outside of a Socket.IO event handler
51
- *
52
- * @example
53
- * ```typescript
54
- * const ctx = getEventContext();
55
- * console.log('Event data:', ctx.data);
56
- * ```
57
- */
58
- export function getEventContext(): EventContext {
59
- const ctx = eventContext.getStore();
60
- if (!ctx) {
61
- throw new Error(
62
- "Lithia event context not found. Are you calling a hook outside of an event handler?",
63
- );
64
- }
65
- return ctx;
66
- }
@@ -1,32 +0,0 @@
1
- /**
2
- * Context management for Lithia.
3
- *
4
- * This module provides three separate context types using AsyncLocalStorage:
5
- * - **LithiaContext**: Global application context with DI container
6
- * - **RouteContext**: HTTP request-specific context
7
- * - **EventContext**: Socket.IO event-specific context
8
- *
9
- * Each context is isolated and provides different information based on
10
- * the execution environment (HTTP request vs Socket.IO event).
11
- *
12
- * @module context
13
- */
14
-
15
- // Socket.IO event context
16
- export {
17
- type EventContext,
18
- eventContext,
19
- getEventContext,
20
- } from "./event-context";
21
- // Lithia global context
22
- export {
23
- getLithiaContext,
24
- type LithiaContext,
25
- lithiaContext,
26
- } from "./lithia-context";
27
- // HTTP route context
28
- export {
29
- getRouteContext,
30
- type RouteContext,
31
- routeContext,
32
- } from "./route-context";
@@ -1,59 +0,0 @@
1
- /**
2
- * Lithia global context module.
3
- *
4
- * Provides the main application-level context that holds the global
5
- * dependency injection container. This context is available throughout
6
- * the entire request/event lifecycle.
7
- *
8
- * @module context/lithia-context
9
- */
10
-
11
- import { AsyncLocalStorage } from "node:async_hooks";
12
-
13
- /**
14
- * Global Lithia application context.
15
- *
16
- * Holds the dependency injection container that is shared across
17
- * the entire application and accessible in all request/event handlers.
18
- */
19
- export interface LithiaContext {
20
- /**
21
- * Global dependency injection container.
22
- *
23
- * Stores dependencies registered via `provide()` that can be injected
24
- * into handlers using `inject()` or `injectOptional()`.
25
- */
26
- dependencies: Map<any, any>;
27
- }
28
-
29
- /**
30
- * AsyncLocalStorage instance for Lithia application context.
31
- *
32
- * Uses Node.js AsyncLocalStorage to provide implicit context propagation
33
- * across async boundaries without explicit parameter passing.
34
- *
35
- * @see https://nodejs.org/api/async_hooks.html#class-asynclocalstorage
36
- */
37
- export const lithiaContext = new AsyncLocalStorage<LithiaContext>();
38
-
39
- /**
40
- * Gets the current Lithia context.
41
- *
42
- * @returns The current LithiaContext
43
- * @throws {Error} If called outside of a request or event handler context
44
- *
45
- * @example
46
- * ```typescript
47
- * const ctx = getLithiaContext();
48
- * const db = ctx.dependencies.get(dbKey);
49
- * ```
50
- */
51
- export function getLithiaContext(): LithiaContext {
52
- const ctx = lithiaContext.getStore();
53
- if (!ctx) {
54
- throw new Error(
55
- "Lithia context not found. Are you accessing dependencies outside of a request or event handler?",
56
- );
57
- }
58
- return ctx;
59
- }
@@ -1,89 +0,0 @@
1
- /**
2
- * Route context module for HTTP requests.
3
- *
4
- * Provides context specific to HTTP route handling, including access to
5
- * the current request, response, and matched route information.
6
- *
7
- * @module context/route-context
8
- */
9
-
10
- import { AsyncLocalStorage } from "node:async_hooks";
11
- import type { Route } from "@lithia-js/native";
12
- import type { Server } from "socket.io";
13
- import type { LithiaRequest } from "../server/request";
14
- import type { LithiaResponse } from "../server/response";
15
-
16
- /**
17
- * HTTP route handler context.
18
- *
19
- * Contains request-specific information available to route handlers
20
- * and middlewares during HTTP request processing.
21
- */
22
- export interface RouteContext {
23
- /**
24
- * The current HTTP request object.
25
- *
26
- * Provides access to request data like params, query, headers, and body.
27
- */
28
- req: LithiaRequest;
29
-
30
- /**
31
- * The current HTTP response object.
32
- *
33
- * Used to send responses back to the client.
34
- */
35
- res: LithiaResponse;
36
-
37
- /**
38
- * The matched route definition.
39
- *
40
- * Contains metadata about the current route including path, method,
41
- * and handler information. May be undefined before route resolution
42
- * or in 404 handlers.
43
- */
44
- route?: Route;
45
-
46
- /**
47
- * The Socket.IO server instance.
48
- *
49
- * Provides access to the Socket.IO server, allowing you to emit events
50
- * to all connected clients, manage rooms, or access server-level features
51
- * from within HTTP route handlers.
52
- *
53
- * This is useful for scenarios where an HTTP request needs to trigger
54
- * real-time updates to connected Socket.IO clients.
55
- */
56
- socketServer: Server;
57
- }
58
-
59
- /**
60
- * AsyncLocalStorage instance for route context.
61
- *
62
- * Uses Node.js AsyncLocalStorage to provide implicit context propagation
63
- * for HTTP request handling across async boundaries.
64
- *
65
- * @see https://nodejs.org/api/async_hooks.html#class-asynclocalstorage
66
- */
67
- export const routeContext = new AsyncLocalStorage<RouteContext>();
68
-
69
- /**
70
- * Gets the current route context.
71
- *
72
- * @returns The current RouteContext
73
- * @throws {Error} If called outside of an HTTP request handler
74
- *
75
- * @example
76
- * ```typescript
77
- * const ctx = getRouteContext();
78
- * console.log(ctx.req.method, ctx.req.url);
79
- * ```
80
- */
81
- export function getRouteContext(): RouteContext {
82
- const ctx = routeContext.getStore();
83
- if (!ctx) {
84
- throw new Error(
85
- "Lithia route context not found. Are you calling a hook outside of a request handler?",
86
- );
87
- }
88
- return ctx;
89
- }
package/src/env.ts DELETED
@@ -1,31 +0,0 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
- import { resolve } from "node:path";
3
- import { parse } from "dotenv";
4
-
5
- export function loadEnv(cwd: string) {
6
- const envFiles = [".env", ".env.local"];
7
- const envVars: Record<string, string> = {};
8
-
9
- for (const file of envFiles) {
10
- const filePath = resolve(cwd, file);
11
- if (existsSync(filePath)) {
12
- try {
13
- const parsed = parse(readFileSync(filePath));
14
- Object.assign(envVars, parsed);
15
- } catch {
16
- // Ignore errors parsing env file
17
- }
18
- }
19
- }
20
-
21
- // Apply variables to process.env, but don't overwrite existing ones
22
- for (const key in envVars) {
23
- if (Object.hasOwn(envVars, key)) {
24
- if (process.env[key] === undefined) {
25
- process.env[key] = envVars[key];
26
- }
27
- }
28
- }
29
-
30
- return envVars;
31
- }
package/src/errors.ts DELETED
@@ -1,96 +0,0 @@
1
- /** Possible severity levels for a `LithiaError`. */
2
- export type LithiaErrorLevel = "fatal" | "error" | "warning" | "info";
3
-
4
- /** Base error class for Lithia runtime errors.
5
- *
6
- * All custom runtime errors extend `LithiaError` and provide a machine
7
- * readable `code` and a `level` used for logging and control-flow (for
8
- * instance `fatal` errors may terminate the process).
9
- */
10
- export class LithiaError extends Error {
11
- constructor(
12
- /** Machine-readable error code. */
13
- public code: string,
14
- message: string,
15
- /** Severity level. */
16
- public level: LithiaErrorLevel = "error",
17
- /** Optional underlying cause. */
18
- public cause?: any,
19
- ) {
20
- super(message);
21
- this.name = "LithiaError";
22
- Error.captureStackTrace?.(this, this.constructor as any);
23
- }
24
- }
25
-
26
- /** Error raised when the manifest version does not match the native schema. */
27
- export class SchemaVersionMismatchError extends LithiaError {
28
- constructor(expected: string, received: string) {
29
- super(
30
- "SCHEMA_VERSION_MISMATCH",
31
- `Manifest version ${received} does not match expected version ${expected}`,
32
- "fatal",
33
- undefined,
34
- );
35
- }
36
- }
37
-
38
- /** Error used when reading or parsing the manifest fails. */
39
- export class ManifestLoadError extends LithiaError {
40
- constructor(cause: any) {
41
- super("MANIFEST_LOAD_ERROR", "Failed to load manifest.", "fatal", cause);
42
- }
43
- }
44
-
45
- /** Error raised when serving a static file but no MIME type is configured for its extension. */
46
- export class StaticFileMimeMissingError extends LithiaError {
47
- constructor(extension: string, filePath: string) {
48
- super(
49
- "STATIC_FILE_MIME_MISSING",
50
- `No MIME type configured for extension '${extension}' when serving '${filePath}'. Please configure a MIME type for this extension in your Lithia config.`,
51
- "error",
52
- );
53
- }
54
- }
55
-
56
- /** Error raised when request data validation fails. */
57
- export class ValidationError extends LithiaError {
58
- constructor(
59
- message: string,
60
- public issues?: any[],
61
- ) {
62
- super("VALIDATION_ERROR", message, "error");
63
- }
64
- }
65
-
66
- /** Error raised when a route module does not export a default async function. */
67
- export class InvalidRouteModuleError extends LithiaError {
68
- constructor(filePath: string, reason: string) {
69
- super(
70
- "INVALID_ROUTE_MODULE",
71
- `Invalid route module at '${filePath}': ${reason}. Route modules must export a default async function.`,
72
- "error",
73
- );
74
- }
75
- }
76
-
77
- export class InvalidEventModuleError extends LithiaError {
78
- constructor(filePath: string, reason: string) {
79
- super(
80
- "INVALID_EVENT_MODULE",
81
- `Invalid event module at '${filePath}': ${reason}. Event modules must export a default function that accepts a Lithia instance.`,
82
- "error",
83
- );
84
- }
85
- }
86
-
87
- /** Error raised when the server bootstrap module (_server.ts) is invalid. */
88
- export class InvalidBootstrapModuleError extends LithiaError {
89
- constructor(filePath: string, reason: string) {
90
- super(
91
- "INVALID_BOOTSTRAP_MODULE",
92
- `Invalid server bootstrap module at '${filePath}': ${reason}. The module must export a default async function.`,
93
- "fatal",
94
- );
95
- }
96
- }
@@ -1,122 +0,0 @@
1
- /**
2
- * Dependency injection hooks for Lithia.
3
- *
4
- * Provides a simple but powerful dependency injection system that works
5
- * across both HTTP request handlers and Socket.IO event handlers.
6
- *
7
- * Dependencies can be provided globally (via `lithia.provide()`) or
8
- * scoped to a specific request/event (via `provide()` in middlewares).
9
- *
10
- * @module hooks/dependency-hooks
11
- */
12
-
13
- import { getLithiaContext } from "../context/lithia-context";
14
-
15
- /**
16
- * Unique key for dependency injection.
17
- *
18
- * Can be:
19
- * - A Symbol (recommended for type safety and uniqueness)
20
- * - A string (simple but can conflict)
21
- * - A class constructor (for class-based dependencies)
22
- *
23
- * @template T - Type of the dependency value
24
- *
25
- * @example
26
- * ```typescript
27
- * // Using Symbol (recommended)
28
- * const dbKey = createInjectionKey<Database>('database');
29
- *
30
- * // Using string
31
- * const dbKey = 'database';
32
- *
33
- * // Using class
34
- * class Database {}
35
- * const dbKey = Database;
36
- * ```
37
- */
38
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
39
- export type InjectionKey<T> = symbol | string | { new (...args: any[]): T };
40
-
41
- /**
42
- * Provides a dependency for injection.
43
- *
44
- * Registers a dependency in the current context's DI container.
45
- * Should typically be called in middlewares or during application bootstrap.
46
- *
47
- * Dependencies are available for the entire lifecycle of the current
48
- * request or event.
49
- *
50
- * @param key - Unique key to identify the dependency
51
- * @param value - The dependency value to provide
52
- * @template T - Type of the dependency value
53
- *
54
- * @example
55
- * ```typescript
56
- * // In a middleware
57
- * export default async function authMiddleware(req, res, next) {
58
- * const user = await authenticate(req);
59
- * provide(userKey, user);
60
- * await next();
61
- * }
62
- * ```
63
- */
64
- export function provide<T>(key: InjectionKey<T>, value: T): void {
65
- getLithiaContext().dependencies.set(key, value);
66
- }
67
-
68
- /**
69
- * Injects a dependency from the DI container.
70
- *
71
- * Retrieves a previously provided dependency. Throws an error if the
72
- * dependency was not provided.
73
- *
74
- * @param key - The key of the dependency to inject
75
- * @returns The dependency value
76
- * @throws {Error} If the dependency is not found in the container
77
- * @template T - Type of the dependency value
78
- *
79
- * @example
80
- * ```typescript
81
- * export default async function handler() {
82
- * const db = inject(dbKey);
83
- * const users = await db.query('SELECT * FROM users');
84
- * return users;
85
- * }
86
- * ```
87
- */
88
- export function inject<T>(key: InjectionKey<T>): T {
89
- const context = getLithiaContext();
90
- if (!context.dependencies.has(key)) {
91
- throw new Error(
92
- `Dependency not found: ${String(key)}. Make sure to provide it using 'provide()' in a middleware.`,
93
- );
94
- }
95
- return context.dependencies.get(key) as T;
96
- }
97
-
98
- /**
99
- * Injects a dependency, returning undefined if not found.
100
- *
101
- * Like `inject()`, but returns undefined instead of throwing when the
102
- * dependency is not available. Useful for optional dependencies.
103
- *
104
- * @param key - The key of the dependency to inject
105
- * @returns The dependency value, or undefined if not found
106
- * @template T - Type of the dependency value
107
- *
108
- * @example
109
- * ```typescript
110
- * export default async function handler() {
111
- * const user = injectOptional(userKey);
112
- * if (user) {
113
- * console.log('Authenticated as:', user.name);
114
- * } else {
115
- * console.log('Anonymous user');
116
- * }
117
- * }
118
- * ```
119
- */
120
- export function injectOptional<T>(key: InjectionKey<T>): T | undefined {
121
- return getLithiaContext().dependencies.get(key) as T | undefined;
122
- }