@aponiajs/platform-elysia 0.3.17 → 0.3.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -20,19 +20,19 @@ The first Elysia platform slice for Aponia:
20
20
  - `handle`, `listen`, and `close` application methods.
21
21
 
22
22
  This package intentionally does not yet implement request scopes, lifecycle
23
- enhancers, schema aggregation, or the complete native-plugin compatibility
23
+ enhancers, schema aggregation, or the complete module-level native-plugin
24
24
  contract from the roadmap.
25
25
 
26
- The decorator API is the default application authoring surface. The
27
- `defineElysiaController` API remains available as a low-level escape hatch for
28
- composing existing Elysia plugins without rewriting them.
26
+ The decorator API is the default application authoring surface.
27
+ `defineElysiaController` remains available as a low-level escape hatch for
28
+ controller factories.
29
29
 
30
30
  See `docs/logging.md` for logger configuration, JSON output, level filtering,
31
31
  and custom logger integration.
32
32
 
33
33
  ```ts
34
34
  import { Module } from "@aponiajs/common";
35
- import { AponiaFactory } from "@aponiajs/platform-elysia";
35
+ import { AponiaFactory, ElysiaPluginModule } from "@aponiajs/platform-elysia";
36
36
 
37
37
  @Module({})
38
38
  class AppModule {}
@@ -41,5 +41,58 @@ const application = await AponiaFactory.create(AppModule);
41
41
  await application.listen(3000);
42
42
  ```
43
43
 
44
+ ## Native Elysia plugins
45
+
46
+ Use existing Elysia plugins through Nest-style module imports:
47
+
48
+ ```bash
49
+ bun add @elysiajs/cors @elysiajs/jwt
50
+ ```
51
+
52
+ ```ts
53
+ import { cors } from "@elysiajs/cors";
54
+ import { jwt } from "@elysiajs/jwt";
55
+
56
+ @Module({
57
+ imports: [
58
+ ElysiaPluginModule.register(cors(), {
59
+ key: "cors",
60
+ }),
61
+ ],
62
+ })
63
+ class AppModule {}
64
+ ```
65
+
66
+ The plugin is passed unchanged to Elysia's native `.use()` implementation. For
67
+ plugins that depend on an injectable service, use an async registration:
68
+
69
+ ```ts
70
+ @Module({
71
+ imports: [
72
+ ElysiaPluginModule.registerAsync({
73
+ key: "jwt",
74
+ imports: [ConfigModule],
75
+ inject: [ConfigService],
76
+ useFactory: (config: ConfigService) =>
77
+ jwt({
78
+ name: "jwt",
79
+ secret: config.get("JWT_SECRET"),
80
+ }),
81
+ }),
82
+ ],
83
+ })
84
+ class AuthModule {}
85
+ ```
86
+
87
+ Imported plugins are installed in dependency order before controllers and a
88
+ shared configured module is installed once across diamond imports. A stable
89
+ `key` keeps module diagnostics deterministic and prevents duplicate
90
+ registrations with the same key.
91
+
92
+ `configureNative` remains available as an application-level escape hatch. It
93
+ preserves Elysia's accumulated plugin types on `getNativeApplication()`.
94
+ Module-imported plugin state and decorators are available at runtime, but do
95
+ not yet flow into decorated controller parameter types.
96
+
44
97
  [npm package](https://www.npmjs.com/package/@aponiajs/platform-elysia) ·
45
98
  [complete package catalog](../../docs/packages.md)
package/dist/index.d.mts CHANGED
@@ -1,23 +1,29 @@
1
- import { Constructor, ControllerDefinition, LogLevel, LoggerService, ModuleClass, ModuleDefinition, Token, TokenValues } from "@aponiajs/common";
1
+ import { Constructor, ControllerDefinition, DynamicModule, LogLevel, LoggerService, ModuleDefinition, ModuleImport, Token, TokenValues } from "@aponiajs/common";
2
+ import "@aponiajs/core";
2
3
  import { AnyElysia, Elysia } from "elysia";
3
4
  //#region src/decorated-module.d.ts
4
- type AponiaRootModule = ModuleClass | ModuleDefinition;
5
+ type AponiaRootModule = ModuleImport;
5
6
  declare function compileRootModule(rootModule: AponiaRootModule): ModuleDefinition;
6
7
  //#endregion
7
8
  //#region src/application.d.ts
8
- declare class AponiaElysiaApplication {
9
+ declare class AponiaElysiaApplication<TNativeApplication extends AnyElysia = Elysia> {
9
10
  #private;
10
- constructor(nativeApplication: Elysia, logger: LoggerService | undefined);
11
- getNativeApplication(): Elysia;
11
+ constructor(nativeApplication: TNativeApplication, logger: LoggerService | undefined);
12
+ getNativeApplication(): TNativeApplication;
12
13
  handle(request: Request): Response | Promise<Response>;
13
14
  listen(port: number): Promise<void>;
14
15
  getUrl(): string;
15
16
  close(): Promise<void>;
16
17
  }
18
+ type NativeElysiaConfigurator<TNativeApplication extends AnyElysia> = (application: Elysia) => TNativeApplication;
17
19
  interface AponiaApplicationOptions {
18
20
  readonly logger?: false | LoggerService | readonly LogLevel[];
19
21
  }
22
+ interface ConfiguredAponiaApplicationOptions<TNativeApplication extends AnyElysia> extends AponiaApplicationOptions {
23
+ readonly configureNative: NativeElysiaConfigurator<TNativeApplication>;
24
+ }
20
25
  declare class AponiaFactory {
26
+ static create<const TNativeApplication extends AnyElysia>(rootModule: AponiaRootModule, options: ConfiguredAponiaApplicationOptions<TNativeApplication>): Promise<AponiaElysiaApplication<TNativeApplication>>;
21
27
  static create(rootModule: AponiaRootModule, options?: AponiaApplicationOptions): Promise<AponiaElysiaApplication>;
22
28
  }
23
29
  //#endregion
@@ -34,4 +40,19 @@ declare function defineElysiaController<TController, const TDependencies extends
34
40
  readonly buildPlugin: (controller: TController) => TPlugin;
35
41
  }): ElysiaControllerDefinition<TController, TDependencies, TPlugin>;
36
42
  //#endregion
37
- export { type AponiaApplicationOptions, AponiaElysiaApplication, AponiaFactory, type AponiaRootModule, ELYSIA_CONTROLLER, type ElysiaControllerDefinition, compileRootModule, defineElysiaController };
43
+ //#region src/plugin-module.d.ts
44
+ type NativeElysiaPlugin = Parameters<Elysia["use"]>[0];
45
+ interface ElysiaPluginModuleOptions {
46
+ readonly key?: string;
47
+ }
48
+ interface AsyncElysiaPluginModuleOptions<TDependencies extends readonly Token<unknown>[], TPlugin extends NativeElysiaPlugin> extends ElysiaPluginModuleOptions {
49
+ readonly imports?: readonly ModuleImport[];
50
+ readonly inject: TDependencies;
51
+ readonly useFactory: (...dependencies: TokenValues<TDependencies>) => TPlugin;
52
+ }
53
+ declare class ElysiaPluginModule {
54
+ static register<const TPlugin extends NativeElysiaPlugin>(plugin: TPlugin, options?: ElysiaPluginModuleOptions): DynamicModule;
55
+ static registerAsync<const TDependencies extends readonly Token<unknown>[], const TPlugin extends NativeElysiaPlugin>(options: AsyncElysiaPluginModuleOptions<TDependencies, TPlugin>): DynamicModule;
56
+ }
57
+ //#endregion
58
+ export { type AponiaApplicationOptions, AponiaElysiaApplication, AponiaFactory, type AponiaRootModule, type AsyncElysiaPluginModuleOptions, type ConfiguredAponiaApplicationOptions, ELYSIA_CONTROLLER, type ElysiaControllerDefinition, ElysiaPluginModule, type ElysiaPluginModuleOptions, type NativeElysiaConfigurator, type NativeElysiaPlugin, compileRootModule, defineElysiaController };
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { AponiaError, Logger, getConstructorDependencies, getControllerMetadata, getModuleMetadata, getRouteMetadata, tokenName } from "@aponiajs/common";
1
+ import { AponiaError, Logger, Module, createToken, getConstructorDependencies, getControllerMetadata, getModuleMetadata, getRouteMetadata, provideFactory, provideValue, tokenName } from "@aponiajs/common";
2
2
  import { createContainer } from "@aponiajs/core";
3
3
  import { Elysia } from "elysia";
4
4
  //#region src/controller.ts
@@ -18,29 +18,54 @@ function isElysiaController(controller) {
18
18
  //#endregion
19
19
  //#region src/decorated-module.ts
20
20
  function compileRootModule(rootModule) {
21
- if (typeof rootModule !== "function") return rootModule;
22
- const compiled = /* @__PURE__ */ new Map();
21
+ if (isModuleDefinition(rootModule)) return rootModule;
22
+ const compiledClasses = /* @__PURE__ */ new Map();
23
+ const compiledDynamicModules = /* @__PURE__ */ new Map();
23
24
  const visiting = [];
24
- const compile = (moduleClass) => {
25
- const cached = compiled.get(moduleClass);
25
+ const compile = (moduleImport) => {
26
+ if (typeof moduleImport === "function") return compileClass(moduleImport);
27
+ if (isModuleDefinition(moduleImport)) return moduleImport;
28
+ return compileDynamicModule(moduleImport);
29
+ };
30
+ const compileClass = (moduleClass) => {
31
+ const cached = compiledClasses.get(moduleClass);
26
32
  if (cached) return cached;
27
- const cycleIndex = visiting.indexOf(moduleClass);
28
- if (cycleIndex >= 0) {
29
- const cycle = [...visiting.slice(cycleIndex), moduleClass].map((item) => item.name);
30
- throw new AponiaError("MODULE_CYCLE", `Module import cycle detected: ${cycle.join(" -> ")}.`, { cycle });
31
- }
33
+ assertNoModuleCycle(moduleClass, visiting);
32
34
  const metadata = getModuleMetadata(moduleClass);
33
- if (!metadata) throw new AponiaError("INVALID_MODULE", `Class "${moduleClass.name}" is missing the @Module() decorator.`, { module: moduleClass.name });
35
+ if (!metadata) throw missingModuleDecorator(moduleClass);
34
36
  visiting.push(moduleClass);
35
37
  try {
36
38
  const definition = Object.freeze({
37
39
  id: moduleClass.name,
38
- imports: Object.freeze((metadata.imports ?? []).map((moduleImport) => typeof moduleImport === "function" ? compile(moduleImport) : moduleImport)),
40
+ imports: Object.freeze((metadata.imports ?? []).map(compile)),
39
41
  controllers: Object.freeze((metadata.controllers ?? []).map(compileDecoratedController)),
40
42
  providers: Object.freeze((metadata.providers ?? []).map(compileProvider)),
41
43
  exports: Object.freeze([...metadata.exports ?? []])
42
44
  });
43
- compiled.set(moduleClass, definition);
45
+ compiledClasses.set(moduleClass, definition);
46
+ return definition;
47
+ } finally {
48
+ visiting.pop();
49
+ }
50
+ };
51
+ const compileDynamicModule = (dynamicModule) => {
52
+ const cached = compiledDynamicModules.get(dynamicModule);
53
+ if (cached) return cached;
54
+ assertNoModuleCycle(dynamicModule, visiting);
55
+ const metadata = getModuleMetadata(dynamicModule.module);
56
+ if (!metadata) throw missingModuleDecorator(dynamicModule.module);
57
+ const mergedMetadata = mergeModuleMetadata(metadata, dynamicModule);
58
+ visiting.push(dynamicModule);
59
+ try {
60
+ const definition = Object.freeze({
61
+ id: dynamicModule.id,
62
+ instanceId: dynamicModule.instanceId,
63
+ imports: Object.freeze((mergedMetadata.imports ?? []).map(compile)),
64
+ controllers: Object.freeze((mergedMetadata.controllers ?? []).map(compileDecoratedController)),
65
+ providers: Object.freeze((mergedMetadata.providers ?? []).map(compileProvider)),
66
+ exports: Object.freeze([...mergedMetadata.exports ?? []])
67
+ });
68
+ compiledDynamicModules.set(dynamicModule, definition);
44
69
  return definition;
45
70
  } finally {
46
71
  visiting.pop();
@@ -48,6 +73,30 @@ function compileRootModule(rootModule) {
48
73
  };
49
74
  return compile(rootModule);
50
75
  }
76
+ function isModuleDefinition(moduleImport) {
77
+ return typeof moduleImport !== "function" && "controllers" in moduleImport && !("module" in moduleImport);
78
+ }
79
+ function moduleImportName(moduleImport) {
80
+ if (typeof moduleImport === "function") return moduleImport.name;
81
+ return moduleImport.id;
82
+ }
83
+ function assertNoModuleCycle(moduleImport, visiting) {
84
+ const cycleIndex = visiting.indexOf(moduleImport);
85
+ if (cycleIndex < 0) return;
86
+ const cycle = [...visiting.slice(cycleIndex), moduleImport].map(moduleImportName);
87
+ throw new AponiaError("MODULE_CYCLE", `Module import cycle detected: ${cycle.join(" -> ")}.`, { cycle });
88
+ }
89
+ function missingModuleDecorator(moduleClass) {
90
+ return new AponiaError("INVALID_MODULE", `Class "${moduleClass.name}" is missing the @Module() decorator.`, { module: moduleClass.name });
91
+ }
92
+ function mergeModuleMetadata(metadata, dynamicModule) {
93
+ return Object.freeze({
94
+ imports: Object.freeze([...metadata.imports ?? [], ...dynamicModule.imports ?? []]),
95
+ controllers: Object.freeze([...metadata.controllers ?? [], ...dynamicModule.controllers ?? []]),
96
+ providers: Object.freeze([...metadata.providers ?? [], ...dynamicModule.providers ?? []]),
97
+ exports: Object.freeze([...metadata.exports ?? [], ...dynamicModule.exports ?? []])
98
+ });
99
+ }
51
100
  function compileProvider(provider) {
52
101
  if (typeof provider !== "function") return provider;
53
102
  const inject = getConstructorDependencies(provider);
@@ -87,6 +136,40 @@ function joinPaths(controllerPath, routePath) {
87
136
  return segments.length === 0 ? "/" : `/${segments.join("/")}`;
88
137
  }
89
138
  //#endregion
139
+ //#region src/plugin-module.ts
140
+ const ELYSIA_PLUGIN = createToken("aponia.elysia.native-plugin");
141
+ const keyedModuleIdentityPrefix = "aponia.elysia.plugin-module:";
142
+ var ElysiaPluginModule = @Module({}) class {
143
+ static register(plugin, options = {}) {
144
+ return createPluginModule({ providers: [provideValue(ELYSIA_PLUGIN, plugin)] }, options.key);
145
+ }
146
+ static registerAsync(options) {
147
+ return createPluginModule({
148
+ imports: options.imports,
149
+ providers: [provideFactory(ELYSIA_PLUGIN, options.inject, options.useFactory)]
150
+ }, options.key);
151
+ }
152
+ };
153
+ function isElysiaPluginModule(module) {
154
+ return module.providers.some((provider) => provider.provide === ELYSIA_PLUGIN);
155
+ }
156
+ function getElysiaPlugin(container, module) {
157
+ return container.resolveModuleProvider(module, ELYSIA_PLUGIN);
158
+ }
159
+ function createPluginModule(metadata, key) {
160
+ if (key !== void 0 && key.trim().length === 0) throw new TypeError("Elysia plugin module key must not be empty.");
161
+ const hasStableKey = key !== void 0;
162
+ const id = hasStableKey ? `ElysiaPluginModule[${key}]` : "ElysiaPluginModule";
163
+ const instanceId = hasStableKey ? Symbol.for(`${keyedModuleIdentityPrefix}${key}`) : Symbol(id);
164
+ return Object.freeze({
165
+ module: ElysiaPluginModule,
166
+ id,
167
+ instanceId,
168
+ imports: Object.freeze([...metadata.imports ?? []]),
169
+ providers: Object.freeze([...metadata.providers ?? []])
170
+ });
171
+ }
172
+ //#endregion
90
173
  //#region src/application.ts
91
174
  var AponiaElysiaApplication = class {
92
175
  #nativeApplication;
@@ -127,9 +210,13 @@ var AponiaFactory = class {
127
210
  logger?.log("Starting Aponia application...", "AponiaFactory");
128
211
  const compiledRootModule = compileRootModule(rootModule);
129
212
  const container = createContainer(compiledRootModule);
130
- const nativeApplication = new Elysia({ name: compiledRootModule.id });
213
+ const baseApplication = new Elysia({ name: compiledRootModule.id });
214
+ const configureNative = "configureNative" in options ? options.configureNative : void 0;
215
+ const nativeApplication = configureNative ? configureNative(baseApplication) : baseApplication;
216
+ if (nativeApplication !== baseApplication) throw new AponiaError("INVALID_NATIVE_APPLICATION", "configureNative must return the Elysia application it receives.");
131
217
  for (const module of container.graph.modules) {
132
218
  container.initializeModule(module);
219
+ if (isElysiaPluginModule(module)) nativeApplication.use(getElysiaPlugin(container, module));
133
220
  logger?.log(`${module.id} dependencies initialized`, "InstanceLoader");
134
221
  }
135
222
  for (const module of container.graph.modules) for (const controller of module.controllers) {
@@ -146,10 +233,12 @@ var AponiaFactory = class {
146
233
  module: module.id,
147
234
  controller: tokenName(controller.token)
148
235
  });
149
- const controllerName = tokenName(controller.token);
150
- const controllerPath = controller.path ?? inferControllerPath(plugin);
151
- logger?.log(`${controllerName} {${controllerPath}}:`, "RoutesResolver");
152
- for (const route of plugin.routes) logger?.log(`Mapped {${route.path}, ${String(route.method).toUpperCase()}} route`, "RouterExplorer");
236
+ if (logger) {
237
+ const controllerName = tokenName(controller.token);
238
+ const controllerPath = controller.path ?? inferControllerPath(plugin);
239
+ logger.log(`${controllerName} {${controllerPath}}:`, "RoutesResolver");
240
+ for (const route of plugin.routes) logger.log(`Mapped {${route.path}, ${String(route.method).toUpperCase()}} route`, "RouterExplorer");
241
+ }
153
242
  nativeApplication.use(plugin);
154
243
  }
155
244
  await nativeApplication.modules;
@@ -169,4 +258,4 @@ function inferControllerPath(plugin) {
169
258
  return plugin.routes[0]?.path ?? "/";
170
259
  }
171
260
  //#endregion
172
- export { AponiaElysiaApplication, AponiaFactory, ELYSIA_CONTROLLER, compileRootModule, defineElysiaController };
261
+ export { AponiaElysiaApplication, AponiaFactory, ELYSIA_CONTROLLER, ElysiaPluginModule, compileRootModule, defineElysiaController };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aponiajs/platform-elysia",
3
- "version": "0.3.17",
3
+ "version": "0.3.19",
4
4
  "description": "Minimal Elysia application platform for Aponia modules and controllers.",
5
5
  "homepage": "https://github.com/aponiajs/aponiajs#readme",
6
6
  "bugs": {
@@ -33,8 +33,8 @@
33
33
  "prepublishOnly": "bun run build"
34
34
  },
35
35
  "dependencies": {
36
- "@aponiajs/common": "0.3.16",
37
- "@aponiajs/core": "0.3.16"
36
+ "@aponiajs/common": "0.3.18",
37
+ "@aponiajs/core": "0.3.18"
38
38
  },
39
39
  "devDependencies": {
40
40
  "elysia": "^1.4.29"