@forinda/kickjs-http 1.0.0 → 1.1.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.
@@ -76,6 +76,8 @@ declare class Application {
76
76
  * 10. Adapter beforeStart hooks
77
77
  */
78
78
  setup(): void;
79
+ /** Register modules and DI without starting the HTTP server (used by kick tinker) */
80
+ registerOnly(): void;
79
81
  /** Start the HTTP server — fails fast if port is in use */
80
82
  start(): void;
81
83
  /** HMR rebuild: swap Express handler without restarting the server */
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  Application
3
- } from "./chunk-YRCR6Z5C.js";
3
+ } from "./chunk-7ZBIJ6IK.js";
4
4
  import "./chunk-35NUARK7.js";
5
5
  import "./chunk-3NEDJA3J.js";
6
6
  import "./chunk-WCQVDF3K.js";
package/dist/bootstrap.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  bootstrap
3
- } from "./chunk-W35YEQOE.js";
4
- import "./chunk-YRCR6Z5C.js";
3
+ } from "./chunk-KSPABTU5.js";
4
+ import "./chunk-7ZBIJ6IK.js";
5
5
  import "./chunk-35NUARK7.js";
6
6
  import "./chunk-3NEDJA3J.js";
7
7
  import "./chunk-WCQVDF3K.js";
@@ -113,6 +113,10 @@ var Application = class {
113
113
  adapter.beforeStart?.(this.app, this.container);
114
114
  }
115
115
  }
116
+ /** Register modules and DI without starting the HTTP server (used by kick tinker) */
117
+ registerOnly() {
118
+ this.setup();
119
+ }
116
120
  /** Start the HTTP server — fails fast if port is in use */
117
121
  start() {
118
122
  this.setup();
@@ -212,4 +216,4 @@ var Application = class {
212
216
  export {
213
217
  Application
214
218
  };
215
- //# sourceMappingURL=chunk-YRCR6Z5C.js.map
219
+ //# sourceMappingURL=chunk-7ZBIJ6IK.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/application.ts"],"sourcesContent":["import http from 'node:http'\nimport express, { type Express, type RequestHandler } from 'express'\nimport {\n Container,\n createLogger,\n type AppModuleClass,\n type AppAdapter,\n type AdapterMiddleware,\n type KickPlugin,\n} from '@forinda/kickjs-core'\nimport { buildRoutes } from './router-builder'\nimport { requestId } from './middleware/request-id'\nimport { notFoundHandler, errorHandler } from './middleware/error-handler'\n\nconst log = createLogger('Application')\n\n/**\n * A middleware entry in the declarative pipeline.\n * Can be a bare handler or an object with path scoping.\n */\nexport type MiddlewareEntry = RequestHandler | { path: string; handler: RequestHandler }\n\nexport interface ApplicationOptions {\n /** Feature modules to load */\n modules: AppModuleClass[]\n /** Adapters that hook into the lifecycle (DB, Redis, Swagger, etc.) */\n adapters?: AppAdapter[]\n /** Server port (falls back to PORT env var, then 3000) */\n port?: number\n /** Global API prefix (default: '/api') */\n apiPrefix?: string\n /** Default API version (default: 1) — routes become /{prefix}/v{version}/{path} */\n defaultVersion?: number\n\n /**\n * Global middleware pipeline. Declared in order.\n * Replaces the hardcoded middleware stack — you control exactly what runs.\n *\n * @example\n * ```ts\n * bootstrap({\n * modules,\n * middleware: [\n * helmet(),\n * cors(),\n * compression(),\n * morgan('dev'),\n * express.json({ limit: '1mb' }),\n * ],\n * })\n * ```\n *\n * If omitted, a sensible default is applied:\n * requestId(), express.json({ limit: '100kb' })\n */\n middleware?: MiddlewareEntry[]\n\n /** Plugins that bundle modules, adapters, middleware, and DI bindings */\n plugins?: KickPlugin[]\n\n /** Express `trust proxy` setting */\n trustProxy?: boolean | number | string | ((ip: string, hopIndex: number) => boolean)\n /** Maximum JSON body size (only used when middleware is not provided) */\n jsonLimit?: string | number\n}\n\n/**\n * The main application class. Wires together Express, the DI container,\n * feature modules, adapters, and the middleware pipeline.\n */\nexport class Application {\n private app: Express\n private container: Container\n private httpServer: http.Server | null = null\n private adapters: AppAdapter[]\n\n private plugins: KickPlugin[]\n\n constructor(private readonly options: ApplicationOptions) {\n this.app = express()\n this.container = Container.getInstance()\n this.plugins = options.plugins ?? []\n this.adapters = [\n // Plugin adapters first\n ...this.plugins.flatMap((p) => p.adapters?.() ?? []),\n ...(options.adapters ?? []),\n ]\n }\n\n /**\n * Full setup pipeline:\n * 1. Adapter beforeMount hooks (early routes — docs, health)\n * 2. Adapter middleware (phase: beforeGlobal)\n * 3. Global middleware (user-declared or defaults)\n * 4. Adapter middleware (phase: afterGlobal)\n * 5. Module registration + DI bootstrap\n * 6. Adapter middleware (phase: beforeRoutes)\n * 7. Module route mounting\n * 8. Adapter middleware (phase: afterRoutes)\n * 9. Error handlers (notFound + global)\n * 10. Adapter beforeStart hooks\n */\n setup(): void {\n log.info('Bootstrapping application...')\n\n // Collect adapter middleware by phase\n const adapterMw = this.collectAdapterMiddleware()\n\n // ── 1. Adapter beforeMount hooks ──────────────────────────────────\n for (const adapter of this.adapters) {\n adapter.beforeMount?.(this.app, this.container)\n }\n\n // ── 2. Hardened defaults ──────────────────────────────────────────\n this.app.disable('x-powered-by')\n this.app.set('trust proxy', this.options.trustProxy ?? false)\n\n // ── 3. Adapter middleware: beforeGlobal ───────────────────────────\n this.mountMiddlewareList(adapterMw.beforeGlobal)\n\n // ── 3b. Plugin registration ──────────────────────────────────────\n for (const plugin of this.plugins) {\n plugin.register?.(this.container)\n }\n\n // ── 3c. Plugin middleware ─────────────────────────────────────────\n for (const plugin of this.plugins) {\n const mw = plugin.middleware?.() ?? []\n for (const handler of mw) {\n this.app.use(handler)\n }\n }\n\n // ── 4. Global middleware ─────────────────────────────────────────\n if (this.options.middleware) {\n // User-declared pipeline — full control\n for (const entry of this.options.middleware) {\n this.mountMiddlewareEntry(entry)\n }\n } else {\n // Sensible defaults when no middleware declared\n this.app.use(requestId())\n this.app.use(express.json({ limit: this.options.jsonLimit ?? '100kb' }))\n }\n\n // ── 5. Adapter middleware: afterGlobal ────────────────────────────\n this.mountMiddlewareList(adapterMw.afterGlobal)\n\n // ── 6. Module registration + DI bootstrap ────────────────────────\n // Plugin modules first, then user modules\n const allModuleClasses = [\n ...this.plugins.flatMap((p) => p.modules?.() ?? []),\n ...this.options.modules,\n ]\n const modules = allModuleClasses.map((ModuleClass) => {\n const mod = new ModuleClass()\n mod.register(this.container)\n return mod\n })\n this.container.bootstrap()\n\n // ── 7. Adapter middleware: beforeRoutes ───────────────────────────\n this.mountMiddlewareList(adapterMw.beforeRoutes)\n\n // ── 8. Mount module routes with versioning ───────────────────────\n const apiPrefix = this.options.apiPrefix ?? '/api'\n const defaultVersion = this.options.defaultVersion ?? 1\n\n for (const mod of modules) {\n const result = mod.routes()\n const routeSets = Array.isArray(result) ? result : [result]\n\n for (const route of routeSets) {\n const version = route.version ?? defaultVersion\n const mountPath = `${apiPrefix}/v${version}${route.path}`\n this.app.use(mountPath, route.router)\n\n // Notify adapters (e.g. SwaggerAdapter for OpenAPI spec generation)\n if (route.controller) {\n for (const adapter of this.adapters) {\n adapter.onRouteMount?.(route.controller, mountPath)\n }\n }\n }\n }\n\n // ── 9. Adapter middleware: afterRoutes ────────────────────────────\n this.mountMiddlewareList(adapterMw.afterRoutes)\n\n // ── 10. Error handlers ───────────────────────────────────────────\n this.app.use(notFoundHandler())\n this.app.use(errorHandler())\n\n // ── 11. Adapter beforeStart hooks ────────────────────────────────\n for (const adapter of this.adapters) {\n adapter.beforeStart?.(this.app, this.container)\n }\n }\n\n /** Register modules and DI without starting the HTTP server (used by kick tinker) */\n registerOnly(): void {\n this.setup()\n }\n\n /** Start the HTTP server — fails fast if port is in use */\n start(): void {\n this.setup()\n\n const port = this.options.port ?? parseInt(process.env.PORT || '3000', 10)\n this.httpServer = http.createServer(this.app)\n\n this.httpServer.on('error', (err: NodeJS.ErrnoException) => {\n if (err.code === 'EADDRINUSE') {\n log.error(\n `Port ${port} is already in use. Kill the existing process or use a different port:\\n` +\n ` PORT=${port + 1} kick dev\\n` +\n ` lsof -i :${port} # find what's using it\\n` +\n ` kill <PID> # stop it`,\n )\n process.exit(1)\n }\n throw err\n })\n\n this.httpServer.listen(port, async () => {\n log.info(`Server running on http://localhost:${port}`)\n\n for (const adapter of this.adapters) {\n adapter.afterStart?.(this.httpServer!, this.container)\n }\n\n // Plugin onReady hooks\n for (const plugin of this.plugins) {\n await plugin.onReady?.(this.container)\n }\n })\n }\n\n /** HMR rebuild: swap Express handler without restarting the server */\n rebuild(): void {\n // Reset the DI container so singletons are re-created with fresh code\n Container.reset()\n this.container = Container.getInstance()\n\n this.app = express()\n this.setup()\n\n if (this.httpServer) {\n this.httpServer.removeAllListeners('request')\n this.httpServer.on('request', this.app)\n log.info('HMR: Express app rebuilt and swapped')\n }\n }\n\n /** Graceful shutdown — runs all adapter shutdowns in parallel, resilient to failures */\n async shutdown(): Promise<void> {\n log.info('Shutting down...')\n\n // Run all plugin + adapter shutdowns concurrently\n const results = await Promise.allSettled([\n ...this.plugins.map((plugin) => Promise.resolve(plugin.shutdown?.())),\n ...this.adapters.map((adapter) => Promise.resolve(adapter.shutdown?.())),\n ])\n for (const result of results) {\n if (result.status === 'rejected') {\n log.error({ err: result.reason }, 'Adapter shutdown failed')\n }\n }\n\n if (this.httpServer) {\n await new Promise<void>((resolve) => this.httpServer!.close(() => resolve()))\n }\n }\n\n getExpressApp(): Express {\n return this.app\n }\n\n getHttpServer(): http.Server | null {\n return this.httpServer\n }\n\n // ── Internal helpers ────────────────────────────────────────────────\n\n private collectAdapterMiddleware() {\n const result = {\n beforeGlobal: [] as AdapterMiddleware[],\n afterGlobal: [] as AdapterMiddleware[],\n beforeRoutes: [] as AdapterMiddleware[],\n afterRoutes: [] as AdapterMiddleware[],\n }\n\n for (const adapter of this.adapters) {\n const entries = adapter.middleware?.() ?? []\n for (const entry of entries) {\n const phase = entry.phase ?? 'afterGlobal'\n result[phase].push(entry)\n }\n }\n\n return result\n }\n\n private mountMiddlewareList(entries: AdapterMiddleware[]): void {\n for (const entry of entries) {\n if (entry.path) {\n this.app.use(entry.path, entry.handler)\n } else {\n this.app.use(entry.handler)\n }\n }\n }\n\n private mountMiddlewareEntry(entry: MiddlewareEntry): void {\n if (typeof entry === 'function') {\n this.app.use(entry)\n } else {\n this.app.use(entry.path, entry.handler)\n }\n }\n}\n"],"mappings":"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","names":["http","express","Container","createLogger","log","createLogger","Application","app","container","httpServer","adapters","plugins","options","express","Container","getInstance","flatMap","p","setup","info","adapterMw","collectAdapterMiddleware","adapter","beforeMount","disable","set","trustProxy","mountMiddlewareList","beforeGlobal","plugin","register","mw","middleware","handler","use","entry","mountMiddlewareEntry","requestId","json","limit","jsonLimit","afterGlobal","allModuleClasses","modules","map","ModuleClass","mod","bootstrap","beforeRoutes","apiPrefix","defaultVersion","result","routes","routeSets","Array","isArray","route","version","mountPath","path","router","controller","onRouteMount","afterRoutes","notFoundHandler","errorHandler","beforeStart","registerOnly","start","port","parseInt","process","env","PORT","http","createServer","on","err","code","error","exit","listen","afterStart","onReady","rebuild","reset","removeAllListeners","shutdown","results","Promise","allSettled","resolve","status","reason","close","getExpressApp","getHttpServer","entries","phase","push"]}
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  Application
3
- } from "./chunk-YRCR6Z5C.js";
3
+ } from "./chunk-7ZBIJ6IK.js";
4
4
  import {
5
5
  __name,
6
6
  __require
@@ -46,6 +46,10 @@ function bootstrap(options) {
46
46
  }
47
47
  const app = new Application(options);
48
48
  g.__app = app;
49
+ if (process.env.KICK_TINKER) {
50
+ app.registerOnly();
51
+ return;
52
+ }
49
53
  app.start();
50
54
  const meta = import.meta;
51
55
  if (meta.hot) {
@@ -57,4 +61,4 @@ __name(bootstrap, "bootstrap");
57
61
  export {
58
62
  bootstrap
59
63
  };
60
- //# sourceMappingURL=chunk-W35YEQOE.js.map
64
+ //# sourceMappingURL=chunk-KSPABTU5.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/bootstrap.ts"],"sourcesContent":["import { createLogger } from '@forinda/kickjs-core'\nimport { Application, type ApplicationOptions } from './application'\n\n/** Try to reload env from .env file if config package is available */\nfunction tryReloadEnv(): void {\n try {\n const config = require('@forinda/kickjs-config')\n config.reloadEnv?.()\n } catch {\n // Config package not installed — skip\n }\n}\n\nconst log = createLogger('Process')\n\n/**\n * Bootstrap a KickJS application with zero boilerplate.\n *\n * Handles:\n * - Vite HMR (hot-swaps Express handler without restarting the server)\n * - Graceful shutdown on SIGINT / SIGTERM\n * - Global uncaughtException / unhandledRejection handlers\n * - globalThis app storage for HMR rebuild\n *\n * @example\n * ```ts\n * // src/index.ts — that's it, the whole file\n * import 'reflect-metadata'\n * import { bootstrap } from '@forinda/kickjs-http'\n * import { modules } from './modules'\n *\n * bootstrap({ modules })\n * ```\n */\nexport function bootstrap(options: ApplicationOptions): void {\n const g = globalThis as any\n\n // ── Global error handlers ────────────────────────────────────────────\n if (!g.__kickBootstrapped) {\n process.on('uncaughtException', (err) => {\n log.error(err, 'Uncaught exception')\n })\n\n process.on('unhandledRejection', (reason) => {\n log.error(reason as any, 'Unhandled rejection')\n })\n\n for (const signal of ['SIGINT', 'SIGTERM'] as const) {\n process.on(signal, async () => {\n log.info(`Received ${signal}, shutting down...`)\n if (g.__app) await g.__app.shutdown()\n process.exit(0)\n })\n }\n\n g.__kickBootstrapped = true\n }\n\n // ── HMR rebuild ──────────────────────────────────────────────────────\n if (g.__app) {\n log.info('HMR: Rebuilding application...')\n tryReloadEnv()\n g.__app.rebuild()\n return\n }\n\n // ── First boot ───────────────────────────────────────────────────────\n const app = new Application(options)\n g.__app = app\n app.start()\n\n // ── Vite HMR acceptance ──────────────────────────────────────────────\n const meta = import.meta as any\n if (meta.hot) {\n meta.hot.accept()\n }\n}\n"],"mappings":";;;;;;;;;AAAA,SAASA,oBAAoB;AAI7B,SAASC,eAAAA;AACP,MAAI;AACF,UAAMC,SAASC,UAAQ,wBAAA;AACvBD,WAAOE,YAAS;EAClB,QAAQ;EAER;AACF;AAPSH;AAST,IAAMI,MAAMC,aAAa,SAAA;AAqBlB,SAASC,UAAUC,SAA2B;AACnD,QAAMC,IAAIC;AAGV,MAAI,CAACD,EAAEE,oBAAoB;AACzBC,YAAQC,GAAG,qBAAqB,CAACC,QAAAA;AAC/BT,UAAIU,MAAMD,KAAK,oBAAA;IACjB,CAAA;AAEAF,YAAQC,GAAG,sBAAsB,CAACG,WAAAA;AAChCX,UAAIU,MAAMC,QAAe,qBAAA;IAC3B,CAAA;AAEA,eAAWC,UAAU;MAAC;MAAU;OAAqB;AACnDL,cAAQC,GAAGI,QAAQ,YAAA;AACjBZ,YAAIa,KAAK,YAAYD,MAAAA,oBAA0B;AAC/C,YAAIR,EAAEU,MAAO,OAAMV,EAAEU,MAAMC,SAAQ;AACnCR,gBAAQS,KAAK,CAAA;MACf,CAAA;IACF;AAEAZ,MAAEE,qBAAqB;EACzB;AAGA,MAAIF,EAAEU,OAAO;AACXd,QAAIa,KAAK,gCAAA;AACTjB,iBAAAA;AACAQ,MAAEU,MAAMG,QAAO;AACf;EACF;AAGA,QAAMC,MAAM,IAAIC,YAAYhB,OAAAA;AAC5BC,IAAEU,QAAQI;AACVA,MAAIE,MAAK;AAGT,QAAMC,OAAO;AACb,MAAIA,KAAKC,KAAK;AACZD,SAAKC,IAAIC,OAAM;EACjB;AACF;AA1CgBrB;","names":["createLogger","tryReloadEnv","config","require","reloadEnv","log","createLogger","bootstrap","options","g","globalThis","__kickBootstrapped","process","on","err","error","reason","signal","info","__app","shutdown","exit","rebuild","app","Application","start","meta","hot","accept"]}
1
+ {"version":3,"sources":["../src/bootstrap.ts"],"sourcesContent":["import { createLogger } from '@forinda/kickjs-core'\nimport { Application, type ApplicationOptions } from './application'\n\n/** Try to reload env from .env file if config package is available */\nfunction tryReloadEnv(): void {\n try {\n const config = require('@forinda/kickjs-config')\n config.reloadEnv?.()\n } catch {\n // Config package not installed — skip\n }\n}\n\nconst log = createLogger('Process')\n\n/**\n * Bootstrap a KickJS application with zero boilerplate.\n *\n * Handles:\n * - Vite HMR (hot-swaps Express handler without restarting the server)\n * - Graceful shutdown on SIGINT / SIGTERM\n * - Global uncaughtException / unhandledRejection handlers\n * - globalThis app storage for HMR rebuild\n *\n * @example\n * ```ts\n * // src/index.ts — that's it, the whole file\n * import 'reflect-metadata'\n * import { bootstrap } from '@forinda/kickjs-http'\n * import { modules } from './modules'\n *\n * bootstrap({ modules })\n * ```\n */\nexport function bootstrap(options: ApplicationOptions): void {\n const g = globalThis as any\n\n // ── Global error handlers ────────────────────────────────────────────\n if (!g.__kickBootstrapped) {\n process.on('uncaughtException', (err) => {\n log.error(err, 'Uncaught exception')\n })\n\n process.on('unhandledRejection', (reason) => {\n log.error(reason as any, 'Unhandled rejection')\n })\n\n for (const signal of ['SIGINT', 'SIGTERM'] as const) {\n process.on(signal, async () => {\n log.info(`Received ${signal}, shutting down...`)\n if (g.__app) await g.__app.shutdown()\n process.exit(0)\n })\n }\n\n g.__kickBootstrapped = true\n }\n\n // ── HMR rebuild ──────────────────────────────────────────────────────\n if (g.__app) {\n log.info('HMR: Rebuilding application...')\n tryReloadEnv()\n g.__app.rebuild()\n return\n }\n\n // ── First boot ───────────────────────────────────────────────────────\n const app = new Application(options)\n g.__app = app\n\n // In tinker mode, register modules and DI but skip starting the HTTP server\n if (process.env.KICK_TINKER) {\n app.registerOnly()\n return\n }\n\n app.start()\n\n // ── Vite HMR acceptance ──────────────────────────────────────────────\n const meta = import.meta as any\n if (meta.hot) {\n meta.hot.accept()\n }\n}\n"],"mappings":";;;;;;;;;AAAA,SAASA,oBAAoB;AAI7B,SAASC,eAAAA;AACP,MAAI;AACF,UAAMC,SAASC,UAAQ,wBAAA;AACvBD,WAAOE,YAAS;EAClB,QAAQ;EAER;AACF;AAPSH;AAST,IAAMI,MAAMC,aAAa,SAAA;AAqBlB,SAASC,UAAUC,SAA2B;AACnD,QAAMC,IAAIC;AAGV,MAAI,CAACD,EAAEE,oBAAoB;AACzBC,YAAQC,GAAG,qBAAqB,CAACC,QAAAA;AAC/BT,UAAIU,MAAMD,KAAK,oBAAA;IACjB,CAAA;AAEAF,YAAQC,GAAG,sBAAsB,CAACG,WAAAA;AAChCX,UAAIU,MAAMC,QAAe,qBAAA;IAC3B,CAAA;AAEA,eAAWC,UAAU;MAAC;MAAU;OAAqB;AACnDL,cAAQC,GAAGI,QAAQ,YAAA;AACjBZ,YAAIa,KAAK,YAAYD,MAAAA,oBAA0B;AAC/C,YAAIR,EAAEU,MAAO,OAAMV,EAAEU,MAAMC,SAAQ;AACnCR,gBAAQS,KAAK,CAAA;MACf,CAAA;IACF;AAEAZ,MAAEE,qBAAqB;EACzB;AAGA,MAAIF,EAAEU,OAAO;AACXd,QAAIa,KAAK,gCAAA;AACTjB,iBAAAA;AACAQ,MAAEU,MAAMG,QAAO;AACf;EACF;AAGA,QAAMC,MAAM,IAAIC,YAAYhB,OAAAA;AAC5BC,IAAEU,QAAQI;AAGV,MAAIX,QAAQa,IAAIC,aAAa;AAC3BH,QAAII,aAAY;AAChB;EACF;AAEAJ,MAAIK,MAAK;AAGT,QAAMC,OAAO;AACb,MAAIA,KAAKC,KAAK;AACZD,SAAKC,IAAIC,OAAM;EACjB;AACF;AAjDgBxB;","names":["createLogger","tryReloadEnv","config","require","reloadEnv","log","createLogger","bootstrap","options","g","globalThis","__kickBootstrapped","process","on","err","error","reason","signal","info","__app","shutdown","exit","rebuild","app","Application","env","KICK_TINKER","registerOnly","start","meta","hot","accept"]}
package/dist/index.d.ts CHANGED
@@ -9,7 +9,6 @@ export { CsrfOptions, csrf } from './middleware/csrf.js';
9
9
  export { RateLimitOptions, RateLimitStore, rateLimit } from './middleware/rate-limit.js';
10
10
  export { Session, SessionData, SessionOptions, SessionStore, session } from './middleware/session.js';
11
11
  export { UploadOptions, buildUploadMiddleware, cleanupFiles, resolveMimeTypes, upload } from './middleware/upload.js';
12
- export { DevToolsAdapter, DevToolsOptions } from './devtools.js';
13
12
  export { buildQueryParams, parseFilters, parsePagination, parseQuery, parseSearchQuery, parseSort } from './query/index.js';
14
13
  export { F as FILTER_OPERATORS, a as FilterItem, b as FilterOperator, P as PaginatedResponse, c as PaginationParams, d as ParsedQuery, Q as QueryBuilderAdapter, e as QueryFieldConfig, S as SortItem } from './types-DsbCdE8f.js';
15
14
  import 'node:http';
package/dist/index.js CHANGED
@@ -1,22 +1,16 @@
1
- import {
2
- rateLimit
3
- } from "./chunk-H4S527PH.js";
4
1
  import {
5
2
  session
6
3
  } from "./chunk-NQJNMKW5.js";
7
4
  import {
8
5
  bootstrap
9
- } from "./chunk-W35YEQOE.js";
6
+ } from "./chunk-KSPABTU5.js";
10
7
  import {
11
8
  Application
12
- } from "./chunk-YRCR6Z5C.js";
9
+ } from "./chunk-7ZBIJ6IK.js";
13
10
  import {
14
11
  REQUEST_ID_HEADER,
15
12
  requestId
16
13
  } from "./chunk-35NUARK7.js";
17
- import {
18
- DevToolsAdapter
19
- } from "./chunk-PLKCXCBN.js";
20
14
  import {
21
15
  buildRoutes,
22
16
  getControllerPath
@@ -49,10 +43,12 @@ import {
49
43
  errorHandler,
50
44
  notFoundHandler
51
45
  } from "./chunk-3NEDJA3J.js";
46
+ import {
47
+ rateLimit
48
+ } from "./chunk-H4S527PH.js";
52
49
  import "./chunk-WCQVDF3K.js";
53
50
  export {
54
51
  Application,
55
- DevToolsAdapter,
56
52
  FILTER_OPERATORS,
57
53
  REQUEST_ID_HEADER,
58
54
  RequestContext,
@@ -0,0 +1,95 @@
1
+ import { AppAdapter } from '@forinda/kickjs-core';
2
+
3
+ interface SpaAdapterOptions {
4
+ /**
5
+ * Directory containing the built SPA files (index.html, assets, etc.)
6
+ * Default: 'dist/client'
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * // Vue: 'dist'
11
+ * // React (Vite): 'dist'
12
+ * // React (CRA): 'build'
13
+ * // Svelte: 'build'
14
+ * // Angular: 'dist/my-app/browser'
15
+ * ```
16
+ */
17
+ clientDir?: string;
18
+ /**
19
+ * URL prefix for API routes. SPA fallback only applies to
20
+ * non-API routes (routes NOT starting with this prefix).
21
+ * Default: '/api'
22
+ *
23
+ * Set to an array for multiple prefixes:
24
+ * ```ts
25
+ * apiPrefix: ['/api', '/graphql', '/_debug']
26
+ * ```
27
+ */
28
+ apiPrefix?: string | string[];
29
+ /**
30
+ * Additional paths to exclude from SPA fallback.
31
+ * These paths will NOT serve index.html.
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * exclude: ['/health', '/metrics', '/ws']
36
+ * ```
37
+ */
38
+ exclude?: string[];
39
+ /**
40
+ * Cache-Control header for static assets (default: 'public, max-age=31536000, immutable')
41
+ * Set to false to disable caching headers.
42
+ */
43
+ cacheControl?: string | false;
44
+ /**
45
+ * Cache-Control for index.html (default: 'no-cache')
46
+ * index.html should not be cached to ensure clients get fresh builds.
47
+ */
48
+ indexCacheControl?: string;
49
+ }
50
+ /**
51
+ * SPA adapter — serve a Vue, React, Svelte, or Angular build alongside
52
+ * your KickJS API. API routes are handled by controllers; everything else
53
+ * falls back to index.html for client-side routing.
54
+ *
55
+ * @example
56
+ * ```ts
57
+ * import { SpaAdapter } from '@forinda/kickjs-http/spa'
58
+ *
59
+ * bootstrap({
60
+ * modules,
61
+ * adapters: [
62
+ * new SpaAdapter({
63
+ * clientDir: 'dist/client', // or 'build', 'dist', etc.
64
+ * apiPrefix: '/api',
65
+ * }),
66
+ * ],
67
+ * })
68
+ * ```
69
+ *
70
+ * File structure:
71
+ * ```
72
+ * dist/
73
+ * client/ ← SPA build output
74
+ * index.html
75
+ * assets/
76
+ * app.js
77
+ * style.css
78
+ * server/ ← KickJS server
79
+ * ```
80
+ */
81
+ declare class SpaAdapter implements AppAdapter {
82
+ private options;
83
+ name: string;
84
+ private clientDir;
85
+ private apiPrefixes;
86
+ private excludePaths;
87
+ private cacheControl;
88
+ private indexCacheControl;
89
+ private indexHtml;
90
+ constructor(options?: SpaAdapterOptions);
91
+ beforeMount(app: any): void;
92
+ beforeStart(app: any): void;
93
+ }
94
+
95
+ export { SpaAdapter, type SpaAdapterOptions };
@@ -0,0 +1,86 @@
1
+ import {
2
+ __name,
3
+ __require
4
+ } from "../chunk-WCQVDF3K.js";
5
+
6
+ // src/middleware/spa.ts
7
+ import { existsSync, readFileSync } from "fs";
8
+ import { resolve, join } from "path";
9
+ import { Logger } from "@forinda/kickjs-core";
10
+ var log = Logger.for("SpaAdapter");
11
+ var SpaAdapter = class {
12
+ static {
13
+ __name(this, "SpaAdapter");
14
+ }
15
+ options;
16
+ name = "SpaAdapter";
17
+ clientDir;
18
+ apiPrefixes;
19
+ excludePaths;
20
+ cacheControl;
21
+ indexCacheControl;
22
+ indexHtml = null;
23
+ constructor(options = {}) {
24
+ this.options = options;
25
+ this.clientDir = resolve(options.clientDir ?? "dist/client");
26
+ this.excludePaths = options.exclude ?? [];
27
+ this.cacheControl = options.cacheControl ?? "public, max-age=31536000, immutable";
28
+ this.indexCacheControl = options.indexCacheControl ?? "no-cache";
29
+ const prefix = options.apiPrefix ?? "/api";
30
+ this.apiPrefixes = Array.isArray(prefix) ? prefix : [
31
+ prefix
32
+ ];
33
+ }
34
+ beforeMount(app) {
35
+ if (!existsSync(this.clientDir)) {
36
+ log.warn(`SPA client directory not found: ${this.clientDir}`);
37
+ log.warn("Build your frontend first, or set clientDir to the correct path.");
38
+ return;
39
+ }
40
+ const indexPath = join(this.clientDir, "index.html");
41
+ if (existsSync(indexPath)) {
42
+ this.indexHtml = readFileSync(indexPath, "utf-8");
43
+ } else {
44
+ log.warn(`index.html not found in ${this.clientDir}`);
45
+ return;
46
+ }
47
+ try {
48
+ const express = __require("express");
49
+ const staticOpts = {};
50
+ if (this.cacheControl) {
51
+ staticOpts.setHeaders = (res, filePath) => {
52
+ if (filePath.endsWith(".html")) {
53
+ res.setHeader("Cache-Control", this.indexCacheControl);
54
+ } else {
55
+ res.setHeader("Cache-Control", this.cacheControl);
56
+ }
57
+ };
58
+ }
59
+ app.use(express.static(this.clientDir, staticOpts));
60
+ } catch {
61
+ log.error("express.static not available \u2014 SPA static file serving disabled");
62
+ return;
63
+ }
64
+ log.info(`Serving SPA from ${this.clientDir}`);
65
+ }
66
+ beforeStart(app) {
67
+ if (!this.indexHtml) return;
68
+ app.use((req, res, next) => {
69
+ for (const prefix of this.apiPrefixes) {
70
+ if (req.path.startsWith(prefix)) return next();
71
+ }
72
+ for (const path of this.excludePaths) {
73
+ if (req.path.startsWith(path)) return next();
74
+ }
75
+ if (req.path.includes(".")) return next();
76
+ if (req.method !== "GET") return next();
77
+ res.setHeader("Content-Type", "text/html");
78
+ res.setHeader("Cache-Control", this.indexCacheControl);
79
+ res.send(this.indexHtml);
80
+ });
81
+ }
82
+ };
83
+ export {
84
+ SpaAdapter
85
+ };
86
+ //# sourceMappingURL=spa.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/middleware/spa.ts"],"sourcesContent":["import { existsSync, readFileSync } from 'node:fs'\nimport { resolve, join } from 'node:path'\nimport { Logger, type AppAdapter } from '@forinda/kickjs-core'\n\nconst log = Logger.for('SpaAdapter')\n\nexport interface SpaAdapterOptions {\n /**\n * Directory containing the built SPA files (index.html, assets, etc.)\n * Default: 'dist/client'\n *\n * @example\n * ```ts\n * // Vue: 'dist'\n * // React (Vite): 'dist'\n * // React (CRA): 'build'\n * // Svelte: 'build'\n * // Angular: 'dist/my-app/browser'\n * ```\n */\n clientDir?: string\n\n /**\n * URL prefix for API routes. SPA fallback only applies to\n * non-API routes (routes NOT starting with this prefix).\n * Default: '/api'\n *\n * Set to an array for multiple prefixes:\n * ```ts\n * apiPrefix: ['/api', '/graphql', '/_debug']\n * ```\n */\n apiPrefix?: string | string[]\n\n /**\n * Additional paths to exclude from SPA fallback.\n * These paths will NOT serve index.html.\n *\n * @example\n * ```ts\n * exclude: ['/health', '/metrics', '/ws']\n * ```\n */\n exclude?: string[]\n\n /**\n * Cache-Control header for static assets (default: 'public, max-age=31536000, immutable')\n * Set to false to disable caching headers.\n */\n cacheControl?: string | false\n\n /**\n * Cache-Control for index.html (default: 'no-cache')\n * index.html should not be cached to ensure clients get fresh builds.\n */\n indexCacheControl?: string\n}\n\n/**\n * SPA adapter — serve a Vue, React, Svelte, or Angular build alongside\n * your KickJS API. API routes are handled by controllers; everything else\n * falls back to index.html for client-side routing.\n *\n * @example\n * ```ts\n * import { SpaAdapter } from '@forinda/kickjs-http/spa'\n *\n * bootstrap({\n * modules,\n * adapters: [\n * new SpaAdapter({\n * clientDir: 'dist/client', // or 'build', 'dist', etc.\n * apiPrefix: '/api',\n * }),\n * ],\n * })\n * ```\n *\n * File structure:\n * ```\n * dist/\n * client/ ← SPA build output\n * index.html\n * assets/\n * app.js\n * style.css\n * server/ ← KickJS server\n * ```\n */\nexport class SpaAdapter implements AppAdapter {\n name = 'SpaAdapter'\n private clientDir: string\n private apiPrefixes: string[]\n private excludePaths: string[]\n private cacheControl: string | false\n private indexCacheControl: string\n private indexHtml: string | null = null\n\n constructor(private options: SpaAdapterOptions = {}) {\n this.clientDir = resolve(options.clientDir ?? 'dist/client')\n this.excludePaths = options.exclude ?? []\n this.cacheControl = options.cacheControl ?? 'public, max-age=31536000, immutable'\n this.indexCacheControl = options.indexCacheControl ?? 'no-cache'\n\n const prefix = options.apiPrefix ?? '/api'\n this.apiPrefixes = Array.isArray(prefix) ? prefix : [prefix]\n }\n\n beforeMount(app: any): void {\n if (!existsSync(this.clientDir)) {\n log.warn(`SPA client directory not found: ${this.clientDir}`)\n log.warn('Build your frontend first, or set clientDir to the correct path.')\n return\n }\n\n // Read index.html into memory\n const indexPath = join(this.clientDir, 'index.html')\n if (existsSync(indexPath)) {\n this.indexHtml = readFileSync(indexPath, 'utf-8')\n } else {\n log.warn(`index.html not found in ${this.clientDir}`)\n return\n }\n\n // Serve static files with cache headers\n try {\n // Dynamic import to avoid hard dependency on express.static\n const express = require('express')\n const staticOpts: any = {}\n\n if (this.cacheControl) {\n staticOpts.setHeaders = (res: any, filePath: string) => {\n if (filePath.endsWith('.html')) {\n res.setHeader('Cache-Control', this.indexCacheControl)\n } else {\n res.setHeader('Cache-Control', this.cacheControl as string)\n }\n }\n }\n\n app.use(express.static(this.clientDir, staticOpts))\n } catch {\n log.error('express.static not available — SPA static file serving disabled')\n return\n }\n\n log.info(`Serving SPA from ${this.clientDir}`)\n }\n\n beforeStart(app: any): void {\n if (!this.indexHtml) return\n\n // SPA fallback: serve index.html for all non-API, non-file routes\n app.use((req: any, res: any, next: any) => {\n // Skip API routes\n for (const prefix of this.apiPrefixes) {\n if (req.path.startsWith(prefix)) return next()\n }\n\n // Skip excluded paths\n for (const path of this.excludePaths) {\n if (req.path.startsWith(path)) return next()\n }\n\n // Skip requests for files (have an extension)\n if (req.path.includes('.')) return next()\n\n // Skip non-GET requests\n if (req.method !== 'GET') return next()\n\n // Serve index.html\n res.setHeader('Content-Type', 'text/html')\n res.setHeader('Cache-Control', this.indexCacheControl)\n res.send(this.indexHtml)\n })\n }\n}\n"],"mappings":";;;;;;AAAA,SAASA,YAAYC,oBAAoB;AACzC,SAASC,SAASC,YAAY;AAC9B,SAASC,cAA+B;AAExC,IAAMC,MAAMC,OAAOC,IAAI,YAAA;AAqFhB,IAAMC,aAAN,MAAMA;EAzFb,OAyFaA;;;;EACXC,OAAO;EACCC;EACAC;EACAC;EACAC;EACAC;EACAC,YAA2B;EAEnC,YAAoBC,UAA6B,CAAC,GAAG;SAAjCA,UAAAA;AAClB,SAAKN,YAAYO,QAAQD,QAAQN,aAAa,aAAA;AAC9C,SAAKE,eAAeI,QAAQE,WAAW,CAAA;AACvC,SAAKL,eAAeG,QAAQH,gBAAgB;AAC5C,SAAKC,oBAAoBE,QAAQF,qBAAqB;AAEtD,UAAMK,SAASH,QAAQI,aAAa;AACpC,SAAKT,cAAcU,MAAMC,QAAQH,MAAAA,IAAUA,SAAS;MAACA;;EACvD;EAEAI,YAAYC,KAAgB;AAC1B,QAAI,CAACC,WAAW,KAAKf,SAAS,GAAG;AAC/BL,UAAIqB,KAAK,mCAAmC,KAAKhB,SAAS,EAAE;AAC5DL,UAAIqB,KAAK,kEAAA;AACT;IACF;AAGA,UAAMC,YAAYC,KAAK,KAAKlB,WAAW,YAAA;AACvC,QAAIe,WAAWE,SAAAA,GAAY;AACzB,WAAKZ,YAAYc,aAAaF,WAAW,OAAA;IAC3C,OAAO;AACLtB,UAAIqB,KAAK,2BAA2B,KAAKhB,SAAS,EAAE;AACpD;IACF;AAGA,QAAI;AAEF,YAAMoB,UAAUC,UAAQ,SAAA;AACxB,YAAMC,aAAkB,CAAC;AAEzB,UAAI,KAAKnB,cAAc;AACrBmB,mBAAWC,aAAa,CAACC,KAAUC,aAAAA;AACjC,cAAIA,SAASC,SAAS,OAAA,GAAU;AAC9BF,gBAAIG,UAAU,iBAAiB,KAAKvB,iBAAiB;UACvD,OAAO;AACLoB,gBAAIG,UAAU,iBAAiB,KAAKxB,YAAY;UAClD;QACF;MACF;AAEAW,UAAIc,IAAIR,QAAQS,OAAO,KAAK7B,WAAWsB,UAAAA,CAAAA;IACzC,QAAQ;AACN3B,UAAImC,MAAM,sEAAA;AACV;IACF;AAEAnC,QAAIoC,KAAK,oBAAoB,KAAK/B,SAAS,EAAE;EAC/C;EAEAgC,YAAYlB,KAAgB;AAC1B,QAAI,CAAC,KAAKT,UAAW;AAGrBS,QAAIc,IAAI,CAACK,KAAUT,KAAUU,SAAAA;AAE3B,iBAAWzB,UAAU,KAAKR,aAAa;AACrC,YAAIgC,IAAIE,KAAKC,WAAW3B,MAAAA,EAAS,QAAOyB,KAAAA;MAC1C;AAGA,iBAAWC,QAAQ,KAAKjC,cAAc;AACpC,YAAI+B,IAAIE,KAAKC,WAAWD,IAAAA,EAAO,QAAOD,KAAAA;MACxC;AAGA,UAAID,IAAIE,KAAKE,SAAS,GAAA,EAAM,QAAOH,KAAAA;AAGnC,UAAID,IAAIK,WAAW,MAAO,QAAOJ,KAAAA;AAGjCV,UAAIG,UAAU,gBAAgB,WAAA;AAC9BH,UAAIG,UAAU,iBAAiB,KAAKvB,iBAAiB;AACrDoB,UAAIe,KAAK,KAAKlC,SAAS;IACzB,CAAA;EACF;AACF;","names":["existsSync","readFileSync","resolve","join","Logger","log","Logger","for","SpaAdapter","name","clientDir","apiPrefixes","excludePaths","cacheControl","indexCacheControl","indexHtml","options","resolve","exclude","prefix","apiPrefix","Array","isArray","beforeMount","app","existsSync","warn","indexPath","join","readFileSync","express","require","staticOpts","setHeaders","res","filePath","endsWith","setHeader","use","static","error","info","beforeStart","req","next","path","startsWith","includes","method","send"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forinda/kickjs-http",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Express 5 integration, router builder, RequestContext, and middleware for KickJS",
5
5
  "keywords": [
6
6
  "kickjs",
@@ -82,10 +82,6 @@
82
82
  "import": "./dist/middleware/error-handler.js",
83
83
  "types": "./dist/middleware/error-handler.d.ts"
84
84
  },
85
- "./devtools": {
86
- "import": "./dist/devtools.js",
87
- "types": "./dist/devtools.d.ts"
88
- },
89
85
  "./query": {
90
86
  "import": "./dist/query/index.js",
91
87
  "types": "./dist/query/index.d.ts"
@@ -93,6 +89,10 @@
93
89
  "./views": {
94
90
  "import": "./dist/middleware/views.js",
95
91
  "types": "./dist/middleware/views.d.ts"
92
+ },
93
+ "./spa": {
94
+ "import": "./dist/middleware/spa.js",
95
+ "types": "./dist/middleware/spa.d.ts"
96
96
  }
97
97
  },
98
98
  "files": [
@@ -102,7 +102,7 @@
102
102
  "cookie-parser": "^1.4.7",
103
103
  "multer": "^2.1.1",
104
104
  "reflect-metadata": "^0.2.2",
105
- "@forinda/kickjs-core": "1.0.0"
105
+ "@forinda/kickjs-core": "1.1.0"
106
106
  },
107
107
  "peerDependencies": {
108
108
  "express": "^5.1.0"
@@ -1,429 +0,0 @@
1
- import {
2
- __name
3
- } from "./chunk-WCQVDF3K.js";
4
-
5
- // src/devtools.ts
6
- import { Router } from "express";
7
-
8
- // src/devtools/dashboard.ts
9
- function renderDashboard(basePath) {
10
- return `<!DOCTYPE html>
11
- <html lang="en">
12
- <head>
13
- <meta charset="utf-8">
14
- <meta name="viewport" content="width=device-width, initial-scale=1">
15
- <title>KickJS DevTools</title>
16
- <style>
17
- ${CSS}
18
- </style>
19
- </head>
20
- <body>
21
- ${BODY}
22
- <script>
23
- const BASE = '${basePath}';
24
- const POLL_MS = 30000;
25
- ${SCRIPT}
26
- </script>
27
- </body>
28
- </html>`;
29
- }
30
- __name(renderDashboard, "renderDashboard");
31
- var CSS = `
32
- * { margin: 0; padding: 0; box-sizing: border-box; }
33
- body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; padding: 24px; }
34
- h1 { font-size: 24px; margin-bottom: 8px; color: #38bdf8; }
35
- .subtitle { color: #64748b; font-size: 14px; margin-bottom: 24px; }
36
- .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 16px; margin-bottom: 24px; }
37
- .card { background: #1e293b; border-radius: 12px; padding: 20px; border: 1px solid #334155; }
38
- .card h2 { font-size: 14px; text-transform: uppercase; letter-spacing: 1px; color: #94a3b8; margin-bottom: 12px; }
39
- .stat { display: flex; justify-content: space-between; padding: 6px 0; border-bottom: 1px solid #334155; }
40
- .stat:last-child { border-bottom: none; }
41
- .stat-label { color: #94a3b8; }
42
- .stat-value { font-weight: 600; font-variant-numeric: tabular-nums; }
43
- .badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 12px; font-weight: 600; }
44
- .badge-green { background: #065f46; color: #6ee7b7; }
45
- .badge-red { background: #7f1d1d; color: #fca5a5; }
46
- .badge-blue { background: #1e3a5f; color: #93c5fd; }
47
- .badge-yellow { background: #713f12; color: #fcd34d; }
48
- table { width: 100%; border-collapse: collapse; font-size: 13px; }
49
- th { text-align: left; padding: 8px; color: #94a3b8; border-bottom: 2px solid #334155; font-weight: 600; }
50
- td { padding: 8px; border-bottom: 1px solid #1e293b; }
51
- .method { font-weight: 700; font-size: 11px; }
52
- .method-get { color: #34d399; }
53
- .method-post { color: #60a5fa; }
54
- .method-put { color: #fbbf24; }
55
- .method-delete { color: #f87171; }
56
- .method-patch { color: #a78bfa; }
57
- .refresh-bar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
58
- .refresh-info { font-size: 12px; color: #64748b; }
59
- .pulse { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: #34d399; margin-right: 6px; animation: pulse 2s infinite; }
60
- @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
61
- .empty { color: #64748b; font-style: italic; padding: 12px 0; }
62
- `;
63
- var BODY = `
64
- <h1>\u26A1 KickJS DevTools</h1>
65
- <div class="refresh-bar">
66
- <div class="subtitle">Development introspection dashboard</div>
67
- <div class="refresh-info"><span class="pulse"></span>Auto-refresh every 30s \xB7 <span id="lastUpdate">loading...</span></div>
68
- </div>
69
-
70
- <div class="grid">
71
- <div class="card">
72
- <h2>Health</h2>
73
- <div id="health"><div class="empty">Loading...</div></div>
74
- </div>
75
- <div class="card">
76
- <h2>Metrics</h2>
77
- <div id="metrics"><div class="empty">Loading...</div></div>
78
- </div>
79
- <div class="card">
80
- <h2>WebSocket</h2>
81
- <div id="ws"><div class="empty">Loading...</div></div>
82
- </div>
83
- </div>
84
-
85
- <div class="card" style="margin-bottom: 16px;">
86
- <h2>Routes (<span id="routeCount">0</span>)</h2>
87
- <div id="routes" style="overflow-x: auto;"><div class="empty">Loading...</div></div>
88
- </div>
89
-
90
- <div class="card">
91
- <h2>DI Container (<span id="diCount">0</span>)</h2>
92
- <div id="container" style="overflow-x: auto;"><div class="empty">Loading...</div></div>
93
- </div>
94
- `;
95
- var SCRIPT = `
96
- async function fetchJSON(path) {
97
- try { const r = await fetch(BASE + path); return r.ok ? r.json() : null; } catch { return null; }
98
- }
99
-
100
- function stat(label, value) {
101
- return '<div class="stat"><span class="stat-label">' + label + '</span><span class="stat-value">' + value + '</span></div>';
102
- }
103
-
104
- function badge(text, type) {
105
- return '<span class="badge badge-' + type + '">' + text + '</span>';
106
- }
107
-
108
- function methodClass(m) { return 'method method-' + m.toLowerCase(); }
109
-
110
- async function refresh() {
111
- const [health, metrics, routes, container, ws] = await Promise.all([
112
- fetchJSON('/health'), fetchJSON('/metrics'), fetchJSON('/routes'),
113
- fetchJSON('/container'), fetchJSON('/ws'),
114
- ]);
115
-
116
- if (health) {
117
- const statusBadge = health.status === 'healthy' ? badge('healthy', 'green') : badge('degraded', 'red');
118
- let html = stat('Status', statusBadge);
119
- html += stat('Uptime', formatDuration(health.uptime));
120
- html += stat('Error Rate', (health.errorRate * 100).toFixed(2) + '%');
121
- if (health.adapters) {
122
- Object.entries(health.adapters).forEach(function(e) {
123
- html += stat(e[0], badge(e[1], e[1] === 'running' ? 'green' : 'yellow'));
124
- });
125
- }
126
- document.getElementById('health').innerHTML = html;
127
- }
128
-
129
- if (metrics) {
130
- let html = stat('Total Requests', metrics.requests.toLocaleString());
131
- html += stat('Server Errors (5xx)', metrics.serverErrors);
132
- html += stat('Client Errors (4xx)', metrics.clientErrors);
133
- html += stat('Error Rate', (metrics.errorRate * 100).toFixed(2) + '%');
134
- html += stat('Uptime', formatDuration(metrics.uptimeSeconds));
135
- html += stat('Started', new Date(metrics.startedAt).toLocaleTimeString());
136
- document.getElementById('metrics').innerHTML = html;
137
- }
138
-
139
- if (ws) {
140
- if (!ws.enabled) {
141
- document.getElementById('ws').innerHTML = '<div class="empty">No WsAdapter</div>';
142
- } else {
143
- let html = stat('Active Connections', ws.activeConnections);
144
- html += stat('Total Connections', ws.totalConnections);
145
- html += stat('Messages In', ws.messagesReceived);
146
- html += stat('Messages Out', ws.messagesSent);
147
- html += stat('Errors', ws.errors);
148
- if (ws.namespaces) {
149
- Object.entries(ws.namespaces).forEach(function(e) {
150
- html += stat(e[0], e[1].connections + ' conn / ' + e[1].handlers + ' handlers');
151
- });
152
- }
153
- document.getElementById('ws').innerHTML = html;
154
- }
155
- }
156
-
157
- if (routes) {
158
- document.getElementById('routeCount').textContent = routes.routes.length;
159
- if (routes.routes.length === 0) {
160
- document.getElementById('routes').innerHTML = '<div class="empty">No routes registered</div>';
161
- } else {
162
- let html = '<table><tr><th>Method</th><th>Path</th><th>Controller</th><th>Handler</th><th>Middleware</th></tr>';
163
- routes.routes.forEach(function(r) {
164
- html += '<tr><td class="' + methodClass(r.method) + '">' + r.method + '</td>';
165
- html += '<td><code>' + r.path + '</code></td>';
166
- html += '<td>' + r.controller + '</td>';
167
- html += '<td>' + r.handler + '</td>';
168
- html += '<td>' + (r.middleware.length ? r.middleware.join(', ') : '\u2014') + '</td></tr>';
169
- });
170
- html += '</table>';
171
- document.getElementById('routes').innerHTML = html;
172
- }
173
- }
174
-
175
- if (container) {
176
- document.getElementById('diCount').textContent = container.count;
177
- if (container.count === 0) {
178
- document.getElementById('container').innerHTML = '<div class="empty">No DI registrations</div>';
179
- } else {
180
- let html = '<table><tr><th>Token</th><th>Scope</th><th>Instantiated</th></tr>';
181
- container.registrations.forEach(function(r) {
182
- html += '<tr><td><code>' + r.token + '</code></td>';
183
- html += '<td>' + badge(r.scope, 'blue') + '</td>';
184
- html += '<td>' + (r.instantiated ? badge('yes', 'green') : badge('no', 'yellow')) + '</td></tr>';
185
- });
186
- html += '</table>';
187
- document.getElementById('container').innerHTML = html;
188
- }
189
- }
190
-
191
- document.getElementById('lastUpdate').textContent = new Date().toLocaleTimeString();
192
- }
193
-
194
- function formatDuration(seconds) {
195
- if (seconds < 60) return seconds + 's';
196
- if (seconds < 3600) return Math.floor(seconds / 60) + 'm ' + (seconds % 60) + 's';
197
- var h = Math.floor(seconds / 3600);
198
- var m = Math.floor((seconds % 3600) / 60);
199
- return h + 'h ' + m + 'm';
200
- }
201
-
202
- refresh();
203
- setInterval(refresh, POLL_MS);
204
- `;
205
-
206
- // src/devtools.ts
207
- import { METADATA, ref, computed, reactive, watch, createLogger } from "@forinda/kickjs-core";
208
- var log = createLogger("DevTools");
209
- var DevToolsAdapter = class {
210
- static {
211
- __name(this, "DevToolsAdapter");
212
- }
213
- name = "DevToolsAdapter";
214
- basePath;
215
- enabled;
216
- exposeConfig;
217
- configPrefixes;
218
- errorRateThreshold;
219
- // ── Reactive State ───────────────────────────────────────────────────
220
- /** Total requests received */
221
- requestCount;
222
- /** Total responses with status >= 500 */
223
- errorCount;
224
- /** Total responses with status >= 400 and < 500 */
225
- clientErrorCount;
226
- /** Server start time */
227
- startedAt;
228
- /** Computed error rate (server errors / total requests) */
229
- errorRate;
230
- /** Computed uptime in seconds */
231
- uptimeSeconds;
232
- /** Per-route latency tracking */
233
- routeLatency;
234
- // ── Internal State ───────────────────────────────────────────────────
235
- routes = [];
236
- container = null;
237
- adapterStatuses = {};
238
- stopErrorWatch = null;
239
- peerAdapters = [];
240
- constructor(options = {}) {
241
- this.basePath = options.basePath ?? "/_debug";
242
- this.enabled = options.enabled ?? process.env.NODE_ENV !== "production";
243
- this.exposeConfig = options.exposeConfig ?? false;
244
- this.configPrefixes = options.configPrefixes ?? [
245
- "APP_",
246
- "NODE_ENV"
247
- ];
248
- this.errorRateThreshold = options.errorRateThreshold ?? 0.5;
249
- this.peerAdapters = options.adapters ?? [];
250
- this.requestCount = ref(0);
251
- this.errorCount = ref(0);
252
- this.clientErrorCount = ref(0);
253
- this.startedAt = ref(Date.now());
254
- this.routeLatency = reactive({});
255
- this.errorRate = computed(() => this.requestCount.value > 0 ? this.errorCount.value / this.requestCount.value : 0);
256
- this.uptimeSeconds = computed(() => Math.floor((Date.now() - this.startedAt.value) / 1e3));
257
- if (options.onErrorRateExceeded) {
258
- const callback = options.onErrorRateExceeded;
259
- const threshold = this.errorRateThreshold;
260
- this.stopErrorWatch = watch(this.errorRate, (rate) => {
261
- if (rate > threshold) {
262
- callback(rate);
263
- }
264
- });
265
- } else {
266
- this.stopErrorWatch = watch(this.errorRate, (rate) => {
267
- if (rate > this.errorRateThreshold) {
268
- log.warn(`Error rate elevated: ${(rate * 100).toFixed(1)}%`);
269
- }
270
- });
271
- }
272
- }
273
- // ── Adapter Lifecycle ────────────────────────────────────────────────
274
- beforeMount(app, container) {
275
- if (!this.enabled) return;
276
- this.container = container;
277
- this.startedAt.value = Date.now();
278
- this.routes = [];
279
- this.adapterStatuses[this.name] = "running";
280
- const router = Router();
281
- router.get("/routes", (_req, res) => {
282
- res.json({
283
- routes: this.routes
284
- });
285
- });
286
- router.get("/container", (_req, res) => {
287
- const registrations = this.container?.getRegistrations() ?? [];
288
- res.json({
289
- registrations,
290
- count: registrations.length
291
- });
292
- });
293
- router.get("/metrics", (_req, res) => {
294
- res.json({
295
- requests: this.requestCount.value,
296
- serverErrors: this.errorCount.value,
297
- clientErrors: this.clientErrorCount.value,
298
- errorRate: this.errorRate.value,
299
- uptimeSeconds: this.uptimeSeconds.value,
300
- startedAt: new Date(this.startedAt.value).toISOString(),
301
- routeLatency: this.routeLatency
302
- });
303
- });
304
- router.get("/health", (_req, res) => {
305
- const healthy = this.errorRate.value < this.errorRateThreshold;
306
- const status = healthy ? "healthy" : "degraded";
307
- res.status(healthy ? 200 : 503).json({
308
- status,
309
- errorRate: this.errorRate.value,
310
- uptime: this.uptimeSeconds.value,
311
- adapters: this.adapterStatuses
312
- });
313
- });
314
- router.get("/state", (_req, res) => {
315
- const wsAdapter = this.peerAdapters.find((a) => a.name === "WsAdapter" && typeof a.getStats === "function");
316
- res.json({
317
- reactive: {
318
- requestCount: this.requestCount.value,
319
- errorCount: this.errorCount.value,
320
- clientErrorCount: this.clientErrorCount.value,
321
- errorRate: this.errorRate.value,
322
- uptimeSeconds: this.uptimeSeconds.value,
323
- startedAt: new Date(this.startedAt.value).toISOString()
324
- },
325
- routes: this.routes.length,
326
- container: this.container?.getRegistrations().length ?? 0,
327
- routeLatency: this.routeLatency,
328
- ...wsAdapter ? {
329
- ws: wsAdapter.getStats()
330
- } : {}
331
- });
332
- });
333
- router.get("/ws", (_req, res) => {
334
- const wsAdapter = this.peerAdapters.find((a) => a.name === "WsAdapter" && typeof a.getStats === "function");
335
- if (!wsAdapter) {
336
- res.json({
337
- enabled: false,
338
- message: "WsAdapter not found"
339
- });
340
- return;
341
- }
342
- res.json({
343
- enabled: true,
344
- ...wsAdapter.getStats()
345
- });
346
- });
347
- if (this.exposeConfig) {
348
- router.get("/config", (_req, res) => {
349
- const config = {};
350
- for (const [key, value] of Object.entries(process.env)) {
351
- if (value === void 0) continue;
352
- const allowed = this.configPrefixes.some((prefix) => key.startsWith(prefix));
353
- config[key] = allowed ? value : "[REDACTED]";
354
- }
355
- res.json({
356
- config
357
- });
358
- });
359
- }
360
- router.get("/", (_req, res) => {
361
- res.type("html").send(renderDashboard(this.basePath));
362
- });
363
- app.use(this.basePath, router);
364
- log.info(`DevTools mounted at ${this.basePath}`);
365
- }
366
- middleware() {
367
- if (!this.enabled) return [];
368
- return [
369
- {
370
- handler: /* @__PURE__ */ __name((req, res, next) => {
371
- const start = Date.now();
372
- this.requestCount.value++;
373
- res.on("finish", () => {
374
- if (res.statusCode >= 500) this.errorCount.value++;
375
- else if (res.statusCode >= 400) this.clientErrorCount.value++;
376
- const routeKey = `${req.method} ${req.route?.path ?? req.path}`;
377
- const elapsed = Date.now() - start;
378
- if (!this.routeLatency[routeKey]) {
379
- this.routeLatency[routeKey] = {
380
- count: 0,
381
- totalMs: 0,
382
- minMs: Infinity,
383
- maxMs: 0
384
- };
385
- }
386
- const stats = this.routeLatency[routeKey];
387
- stats.count++;
388
- stats.totalMs += elapsed;
389
- stats.minMs = Math.min(stats.minMs, elapsed);
390
- stats.maxMs = Math.max(stats.maxMs, elapsed);
391
- });
392
- next();
393
- }, "handler"),
394
- phase: "beforeGlobal"
395
- }
396
- ];
397
- }
398
- onRouteMount(controllerClass, mountPath) {
399
- if (!this.enabled) return;
400
- const routes = Reflect.getMetadata(METADATA.ROUTES, controllerClass) ?? [];
401
- const classMiddleware = Reflect.getMetadata(METADATA.CLASS_MIDDLEWARES, controllerClass) ?? [];
402
- for (const route of routes) {
403
- const methodMiddleware = Reflect.getMetadata(METADATA.METHOD_MIDDLEWARES, controllerClass.prototype, route.handlerName) ?? [];
404
- this.routes.push({
405
- method: route.method.toUpperCase(),
406
- path: `${mountPath}${route.path === "/" ? "" : route.path}`,
407
- controller: controllerClass.name,
408
- handler: route.handlerName,
409
- middleware: [
410
- ...classMiddleware.map((m) => m.name || "anonymous"),
411
- ...methodMiddleware.map((m) => m.name || "anonymous")
412
- ]
413
- });
414
- }
415
- }
416
- afterStart(_server, _container) {
417
- if (!this.enabled) return;
418
- log.info(`DevTools ready \u2014 ${this.routes.length} routes tracked, ${this.container?.getRegistrations().length ?? 0} DI bindings`);
419
- }
420
- shutdown() {
421
- this.stopErrorWatch?.();
422
- this.adapterStatuses[this.name] = "stopped";
423
- }
424
- };
425
-
426
- export {
427
- DevToolsAdapter
428
- };
429
- //# sourceMappingURL=chunk-PLKCXCBN.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/devtools.ts","../src/devtools/dashboard.ts"],"sourcesContent":["import type { Request, Response, NextFunction } from 'express'\nimport { Router } from 'express'\nimport { renderDashboard } from './devtools/dashboard'\nimport {\n type AppAdapter,\n type AdapterMiddleware,\n type Container,\n METADATA,\n ref,\n computed,\n reactive,\n watch,\n createLogger,\n type Ref,\n type ComputedRef,\n} from '@forinda/kickjs-core'\n\nconst log = createLogger('DevTools')\n\n/** Route metadata collected during mount */\ninterface RouteInfo {\n method: string\n path: string\n controller: string\n handler: string\n middleware: string[]\n}\n\n/** Per-route latency stats */\ninterface RouteStats {\n count: number\n totalMs: number\n minMs: number\n maxMs: number\n}\n\nexport interface DevToolsOptions {\n /** Base path for debug endpoints (default: '/_debug') */\n basePath?: string\n /** Only enable when this is true (default: process.env.NODE_ENV !== 'production') */\n enabled?: boolean\n /** Include environment variables (sanitized) at /_debug/config (default: false) */\n exposeConfig?: boolean\n /** Env var prefixes to expose (default: ['APP_', 'NODE_ENV']). Others are redacted. */\n configPrefixes?: string[]\n /** Callback when error rate exceeds threshold */\n onErrorRateExceeded?: (rate: number) => void\n /** Error rate threshold (default: 0.5 = 50%) */\n errorRateThreshold?: number\n /** Other adapters to discover stats from (e.g., WsAdapter) */\n adapters?: any[]\n}\n\n/**\n * DevToolsAdapter — Vue-style reactive introspection for KickJS applications.\n *\n * Exposes debug endpoints powered by reactive state (ref, computed, watch):\n * - `GET /_debug/routes` — all registered routes with middleware\n * - `GET /_debug/container` — DI registry with scopes and instantiation status\n * - `GET /_debug/metrics` — live request/error counts, error rate, uptime\n * - `GET /_debug/health` — deep health check with adapter status\n * - `GET /_debug/config` — sanitized environment variables (opt-in)\n * - `GET /_debug/state` — full reactive state snapshot\n *\n * @example\n * ```ts\n * import { DevToolsAdapter } from '@forinda/kickjs-http/devtools'\n *\n * bootstrap({\n * modules: [UserModule],\n * adapters: [\n * new DevToolsAdapter({\n * enabled: process.env.NODE_ENV !== 'production',\n * exposeConfig: true,\n * configPrefixes: ['APP_', 'DATABASE_'],\n * }),\n * ],\n * })\n * ```\n */\nexport class DevToolsAdapter implements AppAdapter {\n readonly name = 'DevToolsAdapter'\n\n private basePath: string\n private enabled: boolean\n private exposeConfig: boolean\n private configPrefixes: string[]\n private errorRateThreshold: number\n\n // ── Reactive State ───────────────────────────────────────────────────\n /** Total requests received */\n readonly requestCount: Ref<number>\n /** Total responses with status >= 500 */\n readonly errorCount: Ref<number>\n /** Total responses with status >= 400 and < 500 */\n readonly clientErrorCount: Ref<number>\n /** Server start time */\n readonly startedAt: Ref<number>\n /** Computed error rate (server errors / total requests) */\n readonly errorRate: ComputedRef<number>\n /** Computed uptime in seconds */\n readonly uptimeSeconds: ComputedRef<number>\n /** Per-route latency tracking */\n readonly routeLatency: Record<string, RouteStats>\n\n // ── Internal State ───────────────────────────────────────────────────\n private routes: RouteInfo[] = []\n private container: Container | null = null\n private adapterStatuses: Record<string, string> = {}\n private stopErrorWatch: (() => void) | null = null\n private peerAdapters: any[] = []\n\n constructor(options: DevToolsOptions = {}) {\n this.basePath = options.basePath ?? '/_debug'\n this.enabled = options.enabled ?? process.env.NODE_ENV !== 'production'\n this.exposeConfig = options.exposeConfig ?? false\n this.configPrefixes = options.configPrefixes ?? ['APP_', 'NODE_ENV']\n this.errorRateThreshold = options.errorRateThreshold ?? 0.5\n this.peerAdapters = options.adapters ?? []\n\n // Initialize reactive state\n this.requestCount = ref(0)\n this.errorCount = ref(0)\n this.clientErrorCount = ref(0)\n this.startedAt = ref(Date.now())\n this.routeLatency = reactive({})\n\n this.errorRate = computed(() =>\n this.requestCount.value > 0 ? this.errorCount.value / this.requestCount.value : 0,\n )\n\n this.uptimeSeconds = computed(() => Math.floor((Date.now() - this.startedAt.value) / 1000))\n\n // Watch error rate — log warnings when elevated\n if (options.onErrorRateExceeded) {\n const callback = options.onErrorRateExceeded\n const threshold = this.errorRateThreshold\n this.stopErrorWatch = watch(this.errorRate, (rate) => {\n if (rate > threshold) {\n callback(rate)\n }\n })\n } else {\n this.stopErrorWatch = watch(this.errorRate, (rate) => {\n if (rate > this.errorRateThreshold) {\n log.warn(`Error rate elevated: ${(rate * 100).toFixed(1)}%`)\n }\n })\n }\n }\n\n // ── Adapter Lifecycle ────────────────────────────────────────────────\n\n beforeMount(app: any, container: Container): void {\n if (!this.enabled) return\n\n this.container = container\n this.startedAt.value = Date.now()\n // Clear routes on rebuild/restart to prevent HMR duplication\n this.routes = []\n this.adapterStatuses[this.name] = 'running'\n\n const router = Router()\n\n router.get('/routes', (_req: Request, res: Response) => {\n res.json({ routes: this.routes })\n })\n\n router.get('/container', (_req: Request, res: Response) => {\n const registrations = this.container?.getRegistrations() ?? []\n res.json({ registrations, count: registrations.length })\n })\n\n router.get('/metrics', (_req: Request, res: Response) => {\n res.json({\n requests: this.requestCount.value,\n serverErrors: this.errorCount.value,\n clientErrors: this.clientErrorCount.value,\n errorRate: this.errorRate.value,\n uptimeSeconds: this.uptimeSeconds.value,\n startedAt: new Date(this.startedAt.value).toISOString(),\n routeLatency: this.routeLatency,\n })\n })\n\n router.get('/health', (_req: Request, res: Response) => {\n const healthy = this.errorRate.value < this.errorRateThreshold\n const status = healthy ? 'healthy' : 'degraded'\n\n res.status(healthy ? 200 : 503).json({\n status,\n errorRate: this.errorRate.value,\n uptime: this.uptimeSeconds.value,\n adapters: this.adapterStatuses,\n })\n })\n\n router.get('/state', (_req: Request, res: Response) => {\n const wsAdapter = this.peerAdapters.find(\n (a) => a.name === 'WsAdapter' && typeof a.getStats === 'function',\n )\n res.json({\n reactive: {\n requestCount: this.requestCount.value,\n errorCount: this.errorCount.value,\n clientErrorCount: this.clientErrorCount.value,\n errorRate: this.errorRate.value,\n uptimeSeconds: this.uptimeSeconds.value,\n startedAt: new Date(this.startedAt.value).toISOString(),\n },\n routes: this.routes.length,\n container: this.container?.getRegistrations().length ?? 0,\n routeLatency: this.routeLatency,\n ...(wsAdapter ? { ws: wsAdapter.getStats() } : {}),\n })\n })\n\n router.get('/ws', (_req: Request, res: Response) => {\n const wsAdapter = this.peerAdapters.find(\n (a) => a.name === 'WsAdapter' && typeof a.getStats === 'function',\n )\n if (!wsAdapter) {\n res.json({ enabled: false, message: 'WsAdapter not found' })\n return\n }\n res.json({ enabled: true, ...wsAdapter.getStats() })\n })\n\n if (this.exposeConfig) {\n router.get('/config', (_req: Request, res: Response) => {\n const config: Record<string, string> = {}\n for (const [key, value] of Object.entries(process.env)) {\n if (value === undefined) continue\n const allowed = this.configPrefixes.some((prefix) => key.startsWith(prefix))\n config[key] = allowed ? value : '[REDACTED]'\n }\n res.json({ config })\n })\n }\n\n // Dashboard UI — self-contained HTML that polls JSON endpoints\n router.get('/', (_req: Request, res: Response) => {\n res.type('html').send(renderDashboard(this.basePath))\n })\n\n app.use(this.basePath, router)\n log.info(`DevTools mounted at ${this.basePath}`)\n }\n\n middleware(): AdapterMiddleware[] {\n if (!this.enabled) return []\n\n return [\n {\n handler: (req: Request, res: Response, next: NextFunction) => {\n const start = Date.now()\n this.requestCount.value++\n\n res.on('finish', () => {\n if (res.statusCode >= 500) this.errorCount.value++\n else if (res.statusCode >= 400) this.clientErrorCount.value++\n\n // Track per-route latency\n const routeKey = `${req.method} ${req.route?.path ?? req.path}`\n const elapsed = Date.now() - start\n\n if (!this.routeLatency[routeKey]) {\n this.routeLatency[routeKey] = {\n count: 0,\n totalMs: 0,\n minMs: Infinity,\n maxMs: 0,\n }\n }\n const stats = this.routeLatency[routeKey]\n stats.count++\n stats.totalMs += elapsed\n stats.minMs = Math.min(stats.minMs, elapsed)\n stats.maxMs = Math.max(stats.maxMs, elapsed)\n })\n\n next()\n },\n phase: 'beforeGlobal',\n },\n ]\n }\n\n onRouteMount(controllerClass: any, mountPath: string): void {\n if (!this.enabled) return\n\n const routes: Array<{ method: string; path: string; handlerName: string }> =\n Reflect.getMetadata(METADATA.ROUTES, controllerClass) ?? []\n\n const classMiddleware: any[] =\n Reflect.getMetadata(METADATA.CLASS_MIDDLEWARES, controllerClass) ?? []\n\n for (const route of routes) {\n const methodMiddleware: any[] =\n Reflect.getMetadata(\n METADATA.METHOD_MIDDLEWARES,\n controllerClass.prototype,\n route.handlerName,\n ) ?? []\n\n this.routes.push({\n method: route.method.toUpperCase(),\n path: `${mountPath}${route.path === '/' ? '' : route.path}`,\n controller: controllerClass.name,\n handler: route.handlerName,\n middleware: [\n ...classMiddleware.map((m: any) => m.name || 'anonymous'),\n ...methodMiddleware.map((m: any) => m.name || 'anonymous'),\n ],\n })\n }\n }\n\n afterStart(_server: any, _container: Container): void {\n if (!this.enabled) return\n log.info(\n `DevTools ready — ${this.routes.length} routes tracked, ` +\n `${this.container?.getRegistrations().length ?? 0} DI bindings`,\n )\n }\n\n shutdown(): void {\n this.stopErrorWatch?.()\n this.adapterStatuses[this.name] = 'stopped'\n }\n}\n","/**\n * Generates the self-contained HTML dashboard for DevTools.\n * Served at GET /_debug — dark-themed, auto-refreshes every 30s.\n */\nexport function renderDashboard(basePath: string): string {\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>KickJS DevTools</title>\n<style>\n${CSS}\n</style>\n</head>\n<body>\n${BODY}\n<script>\nconst BASE = '${basePath}';\nconst POLL_MS = 30000;\n${SCRIPT}\n</script>\n</body>\n</html>`\n}\n\n// ── Styles ──────────────────────────────────────────────────────────────\n\nconst CSS = `\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; padding: 24px; }\n h1 { font-size: 24px; margin-bottom: 8px; color: #38bdf8; }\n .subtitle { color: #64748b; font-size: 14px; margin-bottom: 24px; }\n .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 16px; margin-bottom: 24px; }\n .card { background: #1e293b; border-radius: 12px; padding: 20px; border: 1px solid #334155; }\n .card h2 { font-size: 14px; text-transform: uppercase; letter-spacing: 1px; color: #94a3b8; margin-bottom: 12px; }\n .stat { display: flex; justify-content: space-between; padding: 6px 0; border-bottom: 1px solid #334155; }\n .stat:last-child { border-bottom: none; }\n .stat-label { color: #94a3b8; }\n .stat-value { font-weight: 600; font-variant-numeric: tabular-nums; }\n .badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 12px; font-weight: 600; }\n .badge-green { background: #065f46; color: #6ee7b7; }\n .badge-red { background: #7f1d1d; color: #fca5a5; }\n .badge-blue { background: #1e3a5f; color: #93c5fd; }\n .badge-yellow { background: #713f12; color: #fcd34d; }\n table { width: 100%; border-collapse: collapse; font-size: 13px; }\n th { text-align: left; padding: 8px; color: #94a3b8; border-bottom: 2px solid #334155; font-weight: 600; }\n td { padding: 8px; border-bottom: 1px solid #1e293b; }\n .method { font-weight: 700; font-size: 11px; }\n .method-get { color: #34d399; }\n .method-post { color: #60a5fa; }\n .method-put { color: #fbbf24; }\n .method-delete { color: #f87171; }\n .method-patch { color: #a78bfa; }\n .refresh-bar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }\n .refresh-info { font-size: 12px; color: #64748b; }\n .pulse { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: #34d399; margin-right: 6px; animation: pulse 2s infinite; }\n @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }\n .empty { color: #64748b; font-style: italic; padding: 12px 0; }\n`\n\n// ── HTML Body ───────────────────────────────────────────────────────────\n\nconst BODY = `\n<h1>⚡ KickJS DevTools</h1>\n<div class=\"refresh-bar\">\n <div class=\"subtitle\">Development introspection dashboard</div>\n <div class=\"refresh-info\"><span class=\"pulse\"></span>Auto-refresh every 30s · <span id=\"lastUpdate\">loading...</span></div>\n</div>\n\n<div class=\"grid\">\n <div class=\"card\">\n <h2>Health</h2>\n <div id=\"health\"><div class=\"empty\">Loading...</div></div>\n </div>\n <div class=\"card\">\n <h2>Metrics</h2>\n <div id=\"metrics\"><div class=\"empty\">Loading...</div></div>\n </div>\n <div class=\"card\">\n <h2>WebSocket</h2>\n <div id=\"ws\"><div class=\"empty\">Loading...</div></div>\n </div>\n</div>\n\n<div class=\"card\" style=\"margin-bottom: 16px;\">\n <h2>Routes (<span id=\"routeCount\">0</span>)</h2>\n <div id=\"routes\" style=\"overflow-x: auto;\"><div class=\"empty\">Loading...</div></div>\n</div>\n\n<div class=\"card\">\n <h2>DI Container (<span id=\"diCount\">0</span>)</h2>\n <div id=\"container\" style=\"overflow-x: auto;\"><div class=\"empty\">Loading...</div></div>\n</div>\n`\n\n// ── Client-side Script ──────────────────────────────────────────────────\n\nconst SCRIPT = `\nasync function fetchJSON(path) {\n try { const r = await fetch(BASE + path); return r.ok ? r.json() : null; } catch { return null; }\n}\n\nfunction stat(label, value) {\n return '<div class=\"stat\"><span class=\"stat-label\">' + label + '</span><span class=\"stat-value\">' + value + '</span></div>';\n}\n\nfunction badge(text, type) {\n return '<span class=\"badge badge-' + type + '\">' + text + '</span>';\n}\n\nfunction methodClass(m) { return 'method method-' + m.toLowerCase(); }\n\nasync function refresh() {\n const [health, metrics, routes, container, ws] = await Promise.all([\n fetchJSON('/health'), fetchJSON('/metrics'), fetchJSON('/routes'),\n fetchJSON('/container'), fetchJSON('/ws'),\n ]);\n\n if (health) {\n const statusBadge = health.status === 'healthy' ? badge('healthy', 'green') : badge('degraded', 'red');\n let html = stat('Status', statusBadge);\n html += stat('Uptime', formatDuration(health.uptime));\n html += stat('Error Rate', (health.errorRate * 100).toFixed(2) + '%');\n if (health.adapters) {\n Object.entries(health.adapters).forEach(function(e) {\n html += stat(e[0], badge(e[1], e[1] === 'running' ? 'green' : 'yellow'));\n });\n }\n document.getElementById('health').innerHTML = html;\n }\n\n if (metrics) {\n let html = stat('Total Requests', metrics.requests.toLocaleString());\n html += stat('Server Errors (5xx)', metrics.serverErrors);\n html += stat('Client Errors (4xx)', metrics.clientErrors);\n html += stat('Error Rate', (metrics.errorRate * 100).toFixed(2) + '%');\n html += stat('Uptime', formatDuration(metrics.uptimeSeconds));\n html += stat('Started', new Date(metrics.startedAt).toLocaleTimeString());\n document.getElementById('metrics').innerHTML = html;\n }\n\n if (ws) {\n if (!ws.enabled) {\n document.getElementById('ws').innerHTML = '<div class=\"empty\">No WsAdapter</div>';\n } else {\n let html = stat('Active Connections', ws.activeConnections);\n html += stat('Total Connections', ws.totalConnections);\n html += stat('Messages In', ws.messagesReceived);\n html += stat('Messages Out', ws.messagesSent);\n html += stat('Errors', ws.errors);\n if (ws.namespaces) {\n Object.entries(ws.namespaces).forEach(function(e) {\n html += stat(e[0], e[1].connections + ' conn / ' + e[1].handlers + ' handlers');\n });\n }\n document.getElementById('ws').innerHTML = html;\n }\n }\n\n if (routes) {\n document.getElementById('routeCount').textContent = routes.routes.length;\n if (routes.routes.length === 0) {\n document.getElementById('routes').innerHTML = '<div class=\"empty\">No routes registered</div>';\n } else {\n let html = '<table><tr><th>Method</th><th>Path</th><th>Controller</th><th>Handler</th><th>Middleware</th></tr>';\n routes.routes.forEach(function(r) {\n html += '<tr><td class=\"' + methodClass(r.method) + '\">' + r.method + '</td>';\n html += '<td><code>' + r.path + '</code></td>';\n html += '<td>' + r.controller + '</td>';\n html += '<td>' + r.handler + '</td>';\n html += '<td>' + (r.middleware.length ? r.middleware.join(', ') : '—') + '</td></tr>';\n });\n html += '</table>';\n document.getElementById('routes').innerHTML = html;\n }\n }\n\n if (container) {\n document.getElementById('diCount').textContent = container.count;\n if (container.count === 0) {\n document.getElementById('container').innerHTML = '<div class=\"empty\">No DI registrations</div>';\n } else {\n let html = '<table><tr><th>Token</th><th>Scope</th><th>Instantiated</th></tr>';\n container.registrations.forEach(function(r) {\n html += '<tr><td><code>' + r.token + '</code></td>';\n html += '<td>' + badge(r.scope, 'blue') + '</td>';\n html += '<td>' + (r.instantiated ? badge('yes', 'green') : badge('no', 'yellow')) + '</td></tr>';\n });\n html += '</table>';\n document.getElementById('container').innerHTML = html;\n }\n }\n\n document.getElementById('lastUpdate').textContent = new Date().toLocaleTimeString();\n}\n\nfunction formatDuration(seconds) {\n if (seconds < 60) return seconds + 's';\n if (seconds < 3600) return Math.floor(seconds / 60) + 'm ' + (seconds % 60) + 's';\n var h = Math.floor(seconds / 3600);\n var m = Math.floor((seconds % 3600) / 60);\n return h + 'h ' + m + 'm';\n}\n\nrefresh();\nsetInterval(refresh, POLL_MS);\n`\n"],"mappings":"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","names":["Router","renderDashboard","basePath","CSS","BODY","SCRIPT","METADATA","ref","computed","reactive","watch","createLogger","log","createLogger","DevToolsAdapter","name","basePath","enabled","exposeConfig","configPrefixes","errorRateThreshold","requestCount","errorCount","clientErrorCount","startedAt","errorRate","uptimeSeconds","routeLatency","routes","container","adapterStatuses","stopErrorWatch","peerAdapters","options","process","env","NODE_ENV","adapters","ref","Date","now","reactive","computed","value","Math","floor","onErrorRateExceeded","callback","threshold","watch","rate","warn","toFixed","beforeMount","app","router","Router","get","_req","res","json","registrations","getRegistrations","count","length","requests","serverErrors","clientErrors","toISOString","healthy","status","uptime","wsAdapter","find","a","getStats","ws","message","config","key","Object","entries","undefined","allowed","some","prefix","startsWith","type","send","renderDashboard","use","info","middleware","handler","req","next","start","on","statusCode","routeKey","method","route","path","elapsed","totalMs","minMs","Infinity","maxMs","stats","min","max","phase","onRouteMount","controllerClass","mountPath","Reflect","getMetadata","METADATA","ROUTES","classMiddleware","CLASS_MIDDLEWARES","methodMiddleware","METHOD_MIDDLEWARES","prototype","handlerName","push","toUpperCase","controller","map","m","afterStart","_server","_container","shutdown"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/application.ts"],"sourcesContent":["import http from 'node:http'\nimport express, { type Express, type RequestHandler } from 'express'\nimport {\n Container,\n createLogger,\n type AppModuleClass,\n type AppAdapter,\n type AdapterMiddleware,\n type KickPlugin,\n} from '@forinda/kickjs-core'\nimport { buildRoutes } from './router-builder'\nimport { requestId } from './middleware/request-id'\nimport { notFoundHandler, errorHandler } from './middleware/error-handler'\n\nconst log = createLogger('Application')\n\n/**\n * A middleware entry in the declarative pipeline.\n * Can be a bare handler or an object with path scoping.\n */\nexport type MiddlewareEntry = RequestHandler | { path: string; handler: RequestHandler }\n\nexport interface ApplicationOptions {\n /** Feature modules to load */\n modules: AppModuleClass[]\n /** Adapters that hook into the lifecycle (DB, Redis, Swagger, etc.) */\n adapters?: AppAdapter[]\n /** Server port (falls back to PORT env var, then 3000) */\n port?: number\n /** Global API prefix (default: '/api') */\n apiPrefix?: string\n /** Default API version (default: 1) — routes become /{prefix}/v{version}/{path} */\n defaultVersion?: number\n\n /**\n * Global middleware pipeline. Declared in order.\n * Replaces the hardcoded middleware stack — you control exactly what runs.\n *\n * @example\n * ```ts\n * bootstrap({\n * modules,\n * middleware: [\n * helmet(),\n * cors(),\n * compression(),\n * morgan('dev'),\n * express.json({ limit: '1mb' }),\n * ],\n * })\n * ```\n *\n * If omitted, a sensible default is applied:\n * requestId(), express.json({ limit: '100kb' })\n */\n middleware?: MiddlewareEntry[]\n\n /** Plugins that bundle modules, adapters, middleware, and DI bindings */\n plugins?: KickPlugin[]\n\n /** Express `trust proxy` setting */\n trustProxy?: boolean | number | string | ((ip: string, hopIndex: number) => boolean)\n /** Maximum JSON body size (only used when middleware is not provided) */\n jsonLimit?: string | number\n}\n\n/**\n * The main application class. Wires together Express, the DI container,\n * feature modules, adapters, and the middleware pipeline.\n */\nexport class Application {\n private app: Express\n private container: Container\n private httpServer: http.Server | null = null\n private adapters: AppAdapter[]\n\n private plugins: KickPlugin[]\n\n constructor(private readonly options: ApplicationOptions) {\n this.app = express()\n this.container = Container.getInstance()\n this.plugins = options.plugins ?? []\n this.adapters = [\n // Plugin adapters first\n ...this.plugins.flatMap((p) => p.adapters?.() ?? []),\n ...(options.adapters ?? []),\n ]\n }\n\n /**\n * Full setup pipeline:\n * 1. Adapter beforeMount hooks (early routes — docs, health)\n * 2. Adapter middleware (phase: beforeGlobal)\n * 3. Global middleware (user-declared or defaults)\n * 4. Adapter middleware (phase: afterGlobal)\n * 5. Module registration + DI bootstrap\n * 6. Adapter middleware (phase: beforeRoutes)\n * 7. Module route mounting\n * 8. Adapter middleware (phase: afterRoutes)\n * 9. Error handlers (notFound + global)\n * 10. Adapter beforeStart hooks\n */\n setup(): void {\n log.info('Bootstrapping application...')\n\n // Collect adapter middleware by phase\n const adapterMw = this.collectAdapterMiddleware()\n\n // ── 1. Adapter beforeMount hooks ──────────────────────────────────\n for (const adapter of this.adapters) {\n adapter.beforeMount?.(this.app, this.container)\n }\n\n // ── 2. Hardened defaults ──────────────────────────────────────────\n this.app.disable('x-powered-by')\n this.app.set('trust proxy', this.options.trustProxy ?? false)\n\n // ── 3. Adapter middleware: beforeGlobal ───────────────────────────\n this.mountMiddlewareList(adapterMw.beforeGlobal)\n\n // ── 3b. Plugin registration ──────────────────────────────────────\n for (const plugin of this.plugins) {\n plugin.register?.(this.container)\n }\n\n // ── 3c. Plugin middleware ─────────────────────────────────────────\n for (const plugin of this.plugins) {\n const mw = plugin.middleware?.() ?? []\n for (const handler of mw) {\n this.app.use(handler)\n }\n }\n\n // ── 4. Global middleware ─────────────────────────────────────────\n if (this.options.middleware) {\n // User-declared pipeline — full control\n for (const entry of this.options.middleware) {\n this.mountMiddlewareEntry(entry)\n }\n } else {\n // Sensible defaults when no middleware declared\n this.app.use(requestId())\n this.app.use(express.json({ limit: this.options.jsonLimit ?? '100kb' }))\n }\n\n // ── 5. Adapter middleware: afterGlobal ────────────────────────────\n this.mountMiddlewareList(adapterMw.afterGlobal)\n\n // ── 6. Module registration + DI bootstrap ────────────────────────\n // Plugin modules first, then user modules\n const allModuleClasses = [\n ...this.plugins.flatMap((p) => p.modules?.() ?? []),\n ...this.options.modules,\n ]\n const modules = allModuleClasses.map((ModuleClass) => {\n const mod = new ModuleClass()\n mod.register(this.container)\n return mod\n })\n this.container.bootstrap()\n\n // ── 7. Adapter middleware: beforeRoutes ───────────────────────────\n this.mountMiddlewareList(adapterMw.beforeRoutes)\n\n // ── 8. Mount module routes with versioning ───────────────────────\n const apiPrefix = this.options.apiPrefix ?? '/api'\n const defaultVersion = this.options.defaultVersion ?? 1\n\n for (const mod of modules) {\n const result = mod.routes()\n const routeSets = Array.isArray(result) ? result : [result]\n\n for (const route of routeSets) {\n const version = route.version ?? defaultVersion\n const mountPath = `${apiPrefix}/v${version}${route.path}`\n this.app.use(mountPath, route.router)\n\n // Notify adapters (e.g. SwaggerAdapter for OpenAPI spec generation)\n if (route.controller) {\n for (const adapter of this.adapters) {\n adapter.onRouteMount?.(route.controller, mountPath)\n }\n }\n }\n }\n\n // ── 9. Adapter middleware: afterRoutes ────────────────────────────\n this.mountMiddlewareList(adapterMw.afterRoutes)\n\n // ── 10. Error handlers ───────────────────────────────────────────\n this.app.use(notFoundHandler())\n this.app.use(errorHandler())\n\n // ── 11. Adapter beforeStart hooks ────────────────────────────────\n for (const adapter of this.adapters) {\n adapter.beforeStart?.(this.app, this.container)\n }\n }\n\n /** Start the HTTP server — fails fast if port is in use */\n start(): void {\n this.setup()\n\n const port = this.options.port ?? parseInt(process.env.PORT || '3000', 10)\n this.httpServer = http.createServer(this.app)\n\n this.httpServer.on('error', (err: NodeJS.ErrnoException) => {\n if (err.code === 'EADDRINUSE') {\n log.error(\n `Port ${port} is already in use. Kill the existing process or use a different port:\\n` +\n ` PORT=${port + 1} kick dev\\n` +\n ` lsof -i :${port} # find what's using it\\n` +\n ` kill <PID> # stop it`,\n )\n process.exit(1)\n }\n throw err\n })\n\n this.httpServer.listen(port, async () => {\n log.info(`Server running on http://localhost:${port}`)\n\n for (const adapter of this.adapters) {\n adapter.afterStart?.(this.httpServer!, this.container)\n }\n\n // Plugin onReady hooks\n for (const plugin of this.plugins) {\n await plugin.onReady?.(this.container)\n }\n })\n }\n\n /** HMR rebuild: swap Express handler without restarting the server */\n rebuild(): void {\n // Reset the DI container so singletons are re-created with fresh code\n Container.reset()\n this.container = Container.getInstance()\n\n this.app = express()\n this.setup()\n\n if (this.httpServer) {\n this.httpServer.removeAllListeners('request')\n this.httpServer.on('request', this.app)\n log.info('HMR: Express app rebuilt and swapped')\n }\n }\n\n /** Graceful shutdown — runs all adapter shutdowns in parallel, resilient to failures */\n async shutdown(): Promise<void> {\n log.info('Shutting down...')\n\n // Run all plugin + adapter shutdowns concurrently\n const results = await Promise.allSettled([\n ...this.plugins.map((plugin) => Promise.resolve(plugin.shutdown?.())),\n ...this.adapters.map((adapter) => Promise.resolve(adapter.shutdown?.())),\n ])\n for (const result of results) {\n if (result.status === 'rejected') {\n log.error({ err: result.reason }, 'Adapter shutdown failed')\n }\n }\n\n if (this.httpServer) {\n await new Promise<void>((resolve) => this.httpServer!.close(() => resolve()))\n }\n }\n\n getExpressApp(): Express {\n return this.app\n }\n\n getHttpServer(): http.Server | null {\n return this.httpServer\n }\n\n // ── Internal helpers ────────────────────────────────────────────────\n\n private collectAdapterMiddleware() {\n const result = {\n beforeGlobal: [] as AdapterMiddleware[],\n afterGlobal: [] as AdapterMiddleware[],\n beforeRoutes: [] as AdapterMiddleware[],\n afterRoutes: [] as AdapterMiddleware[],\n }\n\n for (const adapter of this.adapters) {\n const entries = adapter.middleware?.() ?? []\n for (const entry of entries) {\n const phase = entry.phase ?? 'afterGlobal'\n result[phase].push(entry)\n }\n }\n\n return result\n }\n\n private mountMiddlewareList(entries: AdapterMiddleware[]): void {\n for (const entry of entries) {\n if (entry.path) {\n this.app.use(entry.path, entry.handler)\n } else {\n this.app.use(entry.handler)\n }\n }\n }\n\n private mountMiddlewareEntry(entry: MiddlewareEntry): void {\n if (typeof entry === 'function') {\n this.app.use(entry)\n } else {\n this.app.use(entry.path, entry.handler)\n }\n }\n}\n"],"mappings":"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","names":["http","express","Container","createLogger","log","createLogger","Application","app","container","httpServer","adapters","plugins","options","express","Container","getInstance","flatMap","p","setup","info","adapterMw","collectAdapterMiddleware","adapter","beforeMount","disable","set","trustProxy","mountMiddlewareList","beforeGlobal","plugin","register","mw","middleware","handler","use","entry","mountMiddlewareEntry","requestId","json","limit","jsonLimit","afterGlobal","allModuleClasses","modules","map","ModuleClass","mod","bootstrap","beforeRoutes","apiPrefix","defaultVersion","result","routes","routeSets","Array","isArray","route","version","mountPath","path","router","controller","onRouteMount","afterRoutes","notFoundHandler","errorHandler","beforeStart","start","port","parseInt","process","env","PORT","http","createServer","on","err","code","error","exit","listen","afterStart","onReady","rebuild","reset","removeAllListeners","shutdown","results","Promise","allSettled","resolve","status","reason","close","getExpressApp","getHttpServer","entries","phase","push"]}
@@ -1,87 +0,0 @@
1
- import { AppAdapter, Ref, ComputedRef, Container, AdapterMiddleware } from '@forinda/kickjs-core';
2
-
3
- /** Per-route latency stats */
4
- interface RouteStats {
5
- count: number;
6
- totalMs: number;
7
- minMs: number;
8
- maxMs: number;
9
- }
10
- interface DevToolsOptions {
11
- /** Base path for debug endpoints (default: '/_debug') */
12
- basePath?: string;
13
- /** Only enable when this is true (default: process.env.NODE_ENV !== 'production') */
14
- enabled?: boolean;
15
- /** Include environment variables (sanitized) at /_debug/config (default: false) */
16
- exposeConfig?: boolean;
17
- /** Env var prefixes to expose (default: ['APP_', 'NODE_ENV']). Others are redacted. */
18
- configPrefixes?: string[];
19
- /** Callback when error rate exceeds threshold */
20
- onErrorRateExceeded?: (rate: number) => void;
21
- /** Error rate threshold (default: 0.5 = 50%) */
22
- errorRateThreshold?: number;
23
- /** Other adapters to discover stats from (e.g., WsAdapter) */
24
- adapters?: any[];
25
- }
26
- /**
27
- * DevToolsAdapter — Vue-style reactive introspection for KickJS applications.
28
- *
29
- * Exposes debug endpoints powered by reactive state (ref, computed, watch):
30
- * - `GET /_debug/routes` — all registered routes with middleware
31
- * - `GET /_debug/container` — DI registry with scopes and instantiation status
32
- * - `GET /_debug/metrics` — live request/error counts, error rate, uptime
33
- * - `GET /_debug/health` — deep health check with adapter status
34
- * - `GET /_debug/config` — sanitized environment variables (opt-in)
35
- * - `GET /_debug/state` — full reactive state snapshot
36
- *
37
- * @example
38
- * ```ts
39
- * import { DevToolsAdapter } from '@forinda/kickjs-http/devtools'
40
- *
41
- * bootstrap({
42
- * modules: [UserModule],
43
- * adapters: [
44
- * new DevToolsAdapter({
45
- * enabled: process.env.NODE_ENV !== 'production',
46
- * exposeConfig: true,
47
- * configPrefixes: ['APP_', 'DATABASE_'],
48
- * }),
49
- * ],
50
- * })
51
- * ```
52
- */
53
- declare class DevToolsAdapter implements AppAdapter {
54
- readonly name = "DevToolsAdapter";
55
- private basePath;
56
- private enabled;
57
- private exposeConfig;
58
- private configPrefixes;
59
- private errorRateThreshold;
60
- /** Total requests received */
61
- readonly requestCount: Ref<number>;
62
- /** Total responses with status >= 500 */
63
- readonly errorCount: Ref<number>;
64
- /** Total responses with status >= 400 and < 500 */
65
- readonly clientErrorCount: Ref<number>;
66
- /** Server start time */
67
- readonly startedAt: Ref<number>;
68
- /** Computed error rate (server errors / total requests) */
69
- readonly errorRate: ComputedRef<number>;
70
- /** Computed uptime in seconds */
71
- readonly uptimeSeconds: ComputedRef<number>;
72
- /** Per-route latency tracking */
73
- readonly routeLatency: Record<string, RouteStats>;
74
- private routes;
75
- private container;
76
- private adapterStatuses;
77
- private stopErrorWatch;
78
- private peerAdapters;
79
- constructor(options?: DevToolsOptions);
80
- beforeMount(app: any, container: Container): void;
81
- middleware(): AdapterMiddleware[];
82
- onRouteMount(controllerClass: any, mountPath: string): void;
83
- afterStart(_server: any, _container: Container): void;
84
- shutdown(): void;
85
- }
86
-
87
- export { DevToolsAdapter, type DevToolsOptions };
package/dist/devtools.js DELETED
@@ -1,8 +0,0 @@
1
- import {
2
- DevToolsAdapter
3
- } from "./chunk-PLKCXCBN.js";
4
- import "./chunk-WCQVDF3K.js";
5
- export {
6
- DevToolsAdapter
7
- };
8
- //# sourceMappingURL=devtools.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}