@lithia-js/core 1.0.0-canary.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +4 -0
- package/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +60 -0
- package/dist/config.d.ts +101 -0
- package/dist/config.js +113 -0
- package/dist/config.js.map +1 -0
- package/dist/context/event-context.d.ts +53 -0
- package/dist/context/event-context.js +42 -0
- package/dist/context/event-context.js.map +1 -0
- package/dist/context/index.d.ts +16 -0
- package/dist/context/index.js +29 -0
- package/dist/context/index.js.map +1 -0
- package/dist/context/lithia-context.d.ts +47 -0
- package/dist/context/lithia-context.js +43 -0
- package/dist/context/lithia-context.js.map +1 -0
- package/dist/context/route-context.d.ts +74 -0
- package/dist/context/route-context.js +42 -0
- package/dist/context/route-context.js.map +1 -0
- package/dist/env.d.ts +1 -0
- package/dist/env.js +32 -0
- package/dist/env.js.map +1 -0
- package/dist/errors.d.ts +51 -0
- package/dist/errors.js +80 -0
- package/dist/errors.js.map +1 -0
- package/dist/hooks/dependency-hooks.d.ts +105 -0
- package/dist/hooks/dependency-hooks.js +96 -0
- package/dist/hooks/dependency-hooks.js.map +1 -0
- package/dist/hooks/event-hooks.d.ts +61 -0
- package/dist/hooks/event-hooks.js +70 -0
- package/dist/hooks/event-hooks.js.map +1 -0
- package/dist/hooks/index.d.ts +41 -0
- package/dist/hooks/index.js +59 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/route-hooks.d.ts +154 -0
- package/dist/hooks/route-hooks.js +174 -0
- package/dist/hooks/route-hooks.js.map +1 -0
- package/dist/lib.d.ts +10 -0
- package/dist/lib.js +30 -0
- package/dist/lib.js.map +1 -0
- package/dist/lithia.d.ts +447 -0
- package/dist/lithia.js +649 -0
- package/dist/lithia.js.map +1 -0
- package/dist/logger.d.ts +11 -0
- package/dist/logger.js +55 -0
- package/dist/logger.js.map +1 -0
- package/dist/module-loader.d.ts +12 -0
- package/dist/module-loader.js +78 -0
- package/dist/module-loader.js.map +1 -0
- package/dist/server/event-processor.d.ts +195 -0
- package/dist/server/event-processor.js +253 -0
- package/dist/server/event-processor.js.map +1 -0
- package/dist/server/http-server.d.ts +196 -0
- package/dist/server/http-server.js +295 -0
- package/dist/server/http-server.js.map +1 -0
- package/dist/server/middlewares/validation.d.ts +12 -0
- package/dist/server/middlewares/validation.js +34 -0
- package/dist/server/middlewares/validation.js.map +1 -0
- package/dist/server/request-processor.d.ts +400 -0
- package/dist/server/request-processor.js +652 -0
- package/dist/server/request-processor.js.map +1 -0
- package/dist/server/request.d.ts +73 -0
- package/dist/server/request.js +207 -0
- package/dist/server/request.js.map +1 -0
- package/dist/server/response.d.ts +69 -0
- package/dist/server/response.js +173 -0
- package/dist/server/response.js.map +1 -0
- package/package.json +46 -0
- package/src/config.ts +212 -0
- package/src/context/event-context.ts +66 -0
- package/src/context/index.ts +32 -0
- package/src/context/lithia-context.ts +59 -0
- package/src/context/route-context.ts +89 -0
- package/src/env.ts +31 -0
- package/src/errors.ts +96 -0
- package/src/hooks/dependency-hooks.ts +122 -0
- package/src/hooks/event-hooks.ts +69 -0
- package/src/hooks/index.ts +58 -0
- package/src/hooks/route-hooks.ts +177 -0
- package/src/lib.ts +27 -0
- package/src/lithia.ts +777 -0
- package/src/logger.ts +66 -0
- package/src/module-loader.ts +45 -0
- package/src/server/event-processor.ts +344 -0
- package/src/server/http-server.ts +371 -0
- package/src/server/middlewares/validation.ts +46 -0
- package/src/server/request-processor.ts +860 -0
- package/src/server/request.ts +247 -0
- package/src/server/response.ts +204 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.json +8 -0
package/src/config.ts
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
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";
|
|
@@ -0,0 +1,59 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
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
|
+
}
|