@forinda/kickjs-http 0.7.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";
@@ -6,7 +6,7 @@ import {
6
6
  } from "./chunk-RPN7UFUO.js";
7
7
  import {
8
8
  RequestContext
9
- } from "./chunk-Y5ZSC2FB.js";
9
+ } from "./chunk-IUT42U72.js";
10
10
  import {
11
11
  __name
12
12
  } from "./chunk-WCQVDF3K.js";
@@ -66,4 +66,4 @@ export {
66
66
  getControllerPath,
67
67
  buildRoutes
68
68
  };
69
- //# sourceMappingURL=chunk-E552HYEB.js.map
69
+ //# sourceMappingURL=chunk-2OSVROBK.js.map
@@ -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"]}
@@ -107,6 +107,22 @@ var RequestContext = class {
107
107
  return this.res.send(buffer);
108
108
  }
109
109
  /**
110
+ * Render a template using the registered view engine (EJS, Pug, Handlebars, etc.).
111
+ * Requires a ViewAdapter to be configured in bootstrap().
112
+ *
113
+ * @param template - Template name (without extension, relative to viewsDir)
114
+ * @param data - Data to pass to the template
115
+ *
116
+ * @example
117
+ * ```ts
118
+ * ctx.render('dashboard', { user, title: 'Dashboard' })
119
+ * ctx.render('emails/welcome', { name: 'Alice' })
120
+ * ```
121
+ */
122
+ render(template, data = {}) {
123
+ return this.res.render(template, data);
124
+ }
125
+ /**
110
126
  * Parse query params and return a standardized paginated response.
111
127
  * Calls `ctx.qs()` internally, then wraps your data with pagination meta.
112
128
  *
@@ -211,4 +227,4 @@ var RequestContext = class {
211
227
  export {
212
228
  RequestContext
213
229
  };
214
- //# sourceMappingURL=chunk-Y5ZSC2FB.js.map
230
+ //# sourceMappingURL=chunk-IUT42U72.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/context.ts"],"sourcesContent":["import type { Request, Response, NextFunction } from 'express'\nimport {\n parseQuery,\n type ParsedQuery,\n type QueryFieldConfig,\n type PaginatedResponse,\n} from './query'\n\n/**\n * Unified request/response abstraction passed to every controller method.\n * Shields handlers from raw Express objects and provides convenience methods.\n */\nexport class RequestContext<TBody = any, TParams = any, TQuery = any> {\n private metadata = new Map<string, any>()\n\n constructor(\n public readonly req: Request,\n public readonly res: Response,\n public readonly next: NextFunction,\n ) {}\n\n // ── Request Data ────────────────────────────────────────────────────\n\n get body(): TBody {\n return this.req.body as TBody\n }\n\n get params(): TParams {\n return this.req.params as TParams\n }\n\n get query(): TQuery {\n return this.req.query as TQuery\n }\n\n get headers() {\n return this.req.headers\n }\n\n get requestId(): string | undefined {\n return (this.req as any).requestId ?? (this.req.headers['x-request-id'] as string | undefined)\n }\n\n /** Session data (requires session middleware) */\n get session(): any {\n return (this.req as any).session\n }\n\n // ── Query String Parsing ───────────────────────────────────────────\n\n /**\n * Parse the request query string into structured filters, sort, pagination, and search.\n * Pass the result to an ORM query builder adapter (Drizzle, Prisma, Sequelize, etc.).\n *\n * @param fieldConfig - Optional whitelist for filterable, sortable, and searchable fields\n *\n * @example\n * ```ts\n * @Get('/')\n * async list(ctx: RequestContext) {\n * const parsed = ctx.qs({\n * filterable: ['status', 'priority'],\n * sortable: ['createdAt', 'title'],\n * })\n * const q = drizzleAdapter.build(parsed, { columns })\n * // ... use q.where, q.orderBy, q.limit, q.offset\n * }\n * ```\n */\n qs(fieldConfig?: QueryFieldConfig): ParsedQuery {\n return parseQuery(this.req.query as Record<string, any>, fieldConfig)\n }\n\n // ── File Uploads ────────────────────────────────────────────────────\n\n /** Single uploaded file (requires @FileUpload({ mode: 'single' })) */\n get file(): any {\n return (this.req as any).file\n }\n\n /** Array of uploaded files (requires @FileUpload({ mode: 'array' })) */\n get files(): any[] | undefined {\n return (this.req as any).files\n }\n\n // ── Metadata Store ──────────────────────────────────────────────────\n\n get<T = any>(key: string): T | undefined {\n return this.metadata.get(key) as T | undefined\n }\n\n set(key: string, value: any): void {\n this.metadata.set(key, value)\n }\n\n // ── Response Helpers ────────────────────────────────────────────────\n\n json(data: any, status = 200) {\n return this.res.status(status).json(data)\n }\n\n created(data: any) {\n return this.res.status(201).json(data)\n }\n\n noContent() {\n return this.res.status(204).end()\n }\n\n notFound(message = 'Not Found') {\n return this.res.status(404).json({ message })\n }\n\n badRequest(message: string) {\n return this.res.status(400).json({ message })\n }\n\n html(content: string, status = 200) {\n return this.res.status(status).type('html').send(content)\n }\n\n download(buffer: Buffer, filename: string, contentType = 'application/octet-stream') {\n this.res.setHeader('Content-Disposition', `attachment; filename=\"${filename}\"`)\n this.res.setHeader('Content-Type', contentType)\n return this.res.send(buffer)\n }\n\n /**\n * Render a template using the registered view engine (EJS, Pug, Handlebars, etc.).\n * Requires a ViewAdapter to be configured in bootstrap().\n *\n * @param template - Template name (without extension, relative to viewsDir)\n * @param data - Data to pass to the template\n *\n * @example\n * ```ts\n * ctx.render('dashboard', { user, title: 'Dashboard' })\n * ctx.render('emails/welcome', { name: 'Alice' })\n * ```\n */\n render(template: string, data: Record<string, any> = {}) {\n return this.res.render(template, data)\n }\n\n /**\n * Parse query params and return a standardized paginated response.\n * Calls `ctx.qs()` internally, then wraps your data with pagination meta.\n *\n * @param fetcher - Async function that receives ParsedQuery and returns `{ data, total }`\n * @param fieldConfig - Optional whitelist for filterable, sortable, searchable fields\n *\n * @example\n * ```ts\n * @Get('/')\n * async list(ctx: RequestContext) {\n * return ctx.paginate(\n * async (parsed) => {\n * const data = await db.select().from(users)\n * .where(query.where).limit(parsed.pagination.limit)\n * .offset(parsed.pagination.offset).all()\n * const total = await db.select({ count: count() }).from(users).get()\n * return { data, total: total?.count ?? 0 }\n * },\n * { filterable: ['name', 'role'], sortable: ['createdAt'] },\n * )\n * }\n * ```\n */\n async paginate<T>(\n fetcher: (parsed: ParsedQuery) => Promise<{ data: T[]; total: number }>,\n fieldConfig?: QueryFieldConfig,\n ) {\n const parsed = this.qs(fieldConfig)\n const { data, total } = await fetcher(parsed)\n const { page, limit } = parsed.pagination\n const totalPages = Math.ceil(total / limit) || 1\n\n const response: PaginatedResponse<T> = {\n data,\n meta: {\n page,\n limit,\n total,\n totalPages,\n hasNext: page < totalPages,\n hasPrev: page > 1,\n },\n }\n\n return this.json(response)\n }\n\n // ── Server-Sent Events ──────────────────────────────────────────────\n\n /**\n * Start an SSE (Server-Sent Events) stream.\n * Sets the correct headers and returns helpers to send events.\n *\n * @example\n * ```ts\n * @Get('/events')\n * async stream(ctx: RequestContext) {\n * const sse = ctx.sse()\n *\n * const interval = setInterval(() => {\n * sse.send({ time: new Date().toISOString() }, 'tick')\n * }, 1000)\n *\n * sse.onClose(() => clearInterval(interval))\n * }\n * ```\n */\n sse() {\n this.res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n })\n this.res.flushHeaders()\n\n const closeCallbacks: Array<() => void> = []\n\n this.req.on('close', () => {\n for (const cb of closeCallbacks) cb()\n })\n\n return {\n /** Send an SSE event with optional event name and id */\n send: (data: any, event?: string, id?: string) => {\n if (id) this.res.write(`id: ${id}\\n`)\n if (event) this.res.write(`event: ${event}\\n`)\n this.res.write(`data: ${JSON.stringify(data)}\\n\\n`)\n },\n /** Send a comment (keeps connection alive) */\n comment: (text: string) => {\n this.res.write(`: ${text}\\n\\n`)\n },\n /** Register a callback when the client disconnects */\n onClose: (fn: () => void) => {\n closeCallbacks.push(fn)\n },\n /** End the SSE stream */\n close: () => {\n this.res.end()\n },\n }\n }\n}\n"],"mappings":"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","names":["RequestContext","metadata","Map","req","res","next","body","params","query","headers","requestId","session","qs","fieldConfig","parseQuery","file","files","get","key","set","value","json","data","status","created","noContent","end","notFound","message","badRequest","html","content","type","send","download","buffer","filename","contentType","setHeader","render","template","paginate","fetcher","parsed","total","page","limit","pagination","totalPages","Math","ceil","response","meta","hasNext","hasPrev","sse","writeHead","Connection","flushHeaders","closeCallbacks","on","cb","event","id","write","JSON","stringify","comment","text","onClose","fn","push","close"]}
@@ -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/context.d.ts CHANGED
@@ -52,6 +52,20 @@ declare class RequestContext<TBody = any, TParams = any, TQuery = any> {
52
52
  badRequest(message: string): Response<any, Record<string, any>>;
53
53
  html(content: string, status?: number): Response<any, Record<string, any>>;
54
54
  download(buffer: Buffer, filename: string, contentType?: string): Response<any, Record<string, any>>;
55
+ /**
56
+ * Render a template using the registered view engine (EJS, Pug, Handlebars, etc.).
57
+ * Requires a ViewAdapter to be configured in bootstrap().
58
+ *
59
+ * @param template - Template name (without extension, relative to viewsDir)
60
+ * @param data - Data to pass to the template
61
+ *
62
+ * @example
63
+ * ```ts
64
+ * ctx.render('dashboard', { user, title: 'Dashboard' })
65
+ * ctx.render('emails/welcome', { name: 'Alice' })
66
+ * ```
67
+ */
68
+ render(template: string, data?: Record<string, any>): void;
55
69
  /**
56
70
  * Parse query params and return a standardized paginated response.
57
71
  * Calls `ctx.qs()` internally, then wraps your data with pagination meta.
package/dist/context.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  RequestContext
3
- } from "./chunk-Y5ZSC2FB.js";
3
+ } from "./chunk-IUT42U72.js";
4
4
  import "./chunk-WYY34UWG.js";
5
5
  import "./chunk-WCQVDF3K.js";
6
6
  export {
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,26 +1,20 @@
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
23
- } from "./chunk-E552HYEB.js";
17
+ } from "./chunk-2OSVROBK.js";
24
18
  import {
25
19
  buildUploadMiddleware,
26
20
  cleanupFiles,
@@ -32,7 +26,7 @@ import {
32
26
  } from "./chunk-RPN7UFUO.js";
33
27
  import {
34
28
  RequestContext
35
- } from "./chunk-Y5ZSC2FB.js";
29
+ } from "./chunk-IUT42U72.js";
36
30
  import {
37
31
  FILTER_OPERATORS,
38
32
  buildQueryParams,
@@ -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"]}
@@ -0,0 +1,58 @@
1
+ import { AppAdapter, Container } from '@forinda/kickjs-core';
2
+
3
+ interface ViewAdapterOptions {
4
+ /**
5
+ * Template engine — pass the engine module or a render function.
6
+ * Supported engines: ejs, pug, handlebars, nunjucks, or any
7
+ * Express-compatible engine with a __express property.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * import ejs from 'ejs'
12
+ * new ViewAdapter({ engine: ejs, ext: 'ejs' })
13
+ *
14
+ * import pug from 'pug'
15
+ * new ViewAdapter({ engine: pug, ext: 'pug' })
16
+ * ```
17
+ */
18
+ engine: any;
19
+ /** File extension for templates (e.g., 'ejs', 'pug', 'hbs') */
20
+ ext: string;
21
+ /** Directory containing template files (default: 'src/views') */
22
+ viewsDir?: string;
23
+ /** Default layout template (optional — depends on engine) */
24
+ layout?: string;
25
+ }
26
+ /**
27
+ * View/template adapter — pluggable template engine support for KickJS.
28
+ *
29
+ * Registers an Express view engine and sets the views directory.
30
+ * Use `ctx.render()` in controllers to render templates.
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * import ejs from 'ejs'
35
+ * import { ViewAdapter } from '@forinda/kickjs-http/views'
36
+ *
37
+ * bootstrap({
38
+ * modules,
39
+ * adapters: [
40
+ * new ViewAdapter({ engine: ejs, ext: 'ejs', viewsDir: 'src/views' }),
41
+ * ],
42
+ * })
43
+ *
44
+ * // In a controller:
45
+ * @Get('/dashboard')
46
+ * async dashboard(ctx: RequestContext) {
47
+ * ctx.render('dashboard', { user: currentUser, title: 'Dashboard' })
48
+ * }
49
+ * ```
50
+ */
51
+ declare class ViewAdapter implements AppAdapter {
52
+ private options;
53
+ name: string;
54
+ constructor(options: ViewAdapterOptions);
55
+ beforeMount(app: any, _container: Container): void;
56
+ }
57
+
58
+ export { ViewAdapter, type ViewAdapterOptions };
@@ -0,0 +1,40 @@
1
+ import {
2
+ __name
3
+ } from "../chunk-WCQVDF3K.js";
4
+
5
+ // src/middleware/views.ts
6
+ import { resolve } from "path";
7
+ import { Logger } from "@forinda/kickjs-core";
8
+ var log = Logger.for("ViewEngine");
9
+ var ViewAdapter = class {
10
+ static {
11
+ __name(this, "ViewAdapter");
12
+ }
13
+ options;
14
+ name = "ViewAdapter";
15
+ constructor(options) {
16
+ this.options = options;
17
+ }
18
+ beforeMount(app, _container) {
19
+ const { engine, ext, viewsDir = "src/views" } = this.options;
20
+ if (engine.__express) {
21
+ app.engine(ext, engine.__express);
22
+ } else if (typeof engine.renderFile === "function") {
23
+ app.engine(ext, (path, options, callback) => {
24
+ engine.renderFile(path, options, callback);
25
+ });
26
+ } else if (typeof engine === "function") {
27
+ app.engine(ext, engine);
28
+ } else {
29
+ log.warn(`Engine for .${ext} does not have __express or renderFile. Trying as-is.`);
30
+ app.engine(ext, engine);
31
+ }
32
+ app.set("view engine", ext);
33
+ app.set("views", resolve(viewsDir));
34
+ log.info(`View engine: ${ext} (${resolve(viewsDir)})`);
35
+ }
36
+ };
37
+ export {
38
+ ViewAdapter
39
+ };
40
+ //# sourceMappingURL=views.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/middleware/views.ts"],"sourcesContent":["import { resolve } from 'node:path'\nimport { Logger, type AppAdapter, type Container } from '@forinda/kickjs-core'\n\nconst log = Logger.for('ViewEngine')\n\nexport interface ViewAdapterOptions {\n /**\n * Template engine — pass the engine module or a render function.\n * Supported engines: ejs, pug, handlebars, nunjucks, or any\n * Express-compatible engine with a __express property.\n *\n * @example\n * ```ts\n * import ejs from 'ejs'\n * new ViewAdapter({ engine: ejs, ext: 'ejs' })\n *\n * import pug from 'pug'\n * new ViewAdapter({ engine: pug, ext: 'pug' })\n * ```\n */\n engine: any\n\n /** File extension for templates (e.g., 'ejs', 'pug', 'hbs') */\n ext: string\n\n /** Directory containing template files (default: 'src/views') */\n viewsDir?: string\n\n /** Default layout template (optional — depends on engine) */\n layout?: string\n}\n\n/**\n * View/template adapter — pluggable template engine support for KickJS.\n *\n * Registers an Express view engine and sets the views directory.\n * Use `ctx.render()` in controllers to render templates.\n *\n * @example\n * ```ts\n * import ejs from 'ejs'\n * import { ViewAdapter } from '@forinda/kickjs-http/views'\n *\n * bootstrap({\n * modules,\n * adapters: [\n * new ViewAdapter({ engine: ejs, ext: 'ejs', viewsDir: 'src/views' }),\n * ],\n * })\n *\n * // In a controller:\n * @Get('/dashboard')\n * async dashboard(ctx: RequestContext) {\n * ctx.render('dashboard', { user: currentUser, title: 'Dashboard' })\n * }\n * ```\n */\nexport class ViewAdapter implements AppAdapter {\n name = 'ViewAdapter'\n\n constructor(private options: ViewAdapterOptions) {}\n\n beforeMount(app: any, _container: Container): void {\n const { engine, ext, viewsDir = 'src/views' } = this.options\n\n // Register the engine\n if (engine.__express) {\n // EJS, Pug — have __express method\n app.engine(ext, engine.__express)\n } else if (typeof engine.renderFile === 'function') {\n // Engines with renderFile (nunjucks-style)\n app.engine(ext, (path: string, options: any, callback: Function) => {\n engine.renderFile(path, options, callback)\n })\n } else if (typeof engine === 'function') {\n // Custom render function: (path, options, callback) => void\n app.engine(ext, engine)\n } else {\n log.warn(`Engine for .${ext} does not have __express or renderFile. Trying as-is.`)\n app.engine(ext, engine)\n }\n\n app.set('view engine', ext)\n app.set('views', resolve(viewsDir))\n\n log.info(`View engine: ${ext} (${resolve(viewsDir)})`)\n }\n}\n"],"mappings":";;;;;AAAA,SAASA,eAAe;AACxB,SAASC,cAA+C;AAExD,IAAMC,MAAMC,OAAOC,IAAI,YAAA;AAsDhB,IAAMC,cAAN,MAAMA;EAzDb,OAyDaA;;;;EACXC,OAAO;EAEP,YAAoBC,SAA6B;SAA7BA,UAAAA;EAA8B;EAElDC,YAAYC,KAAUC,YAA6B;AACjD,UAAM,EAAEC,QAAQC,KAAKC,WAAW,YAAW,IAAK,KAAKN;AAGrD,QAAII,OAAOG,WAAW;AAEpBL,UAAIE,OAAOC,KAAKD,OAAOG,SAAS;IAClC,WAAW,OAAOH,OAAOI,eAAe,YAAY;AAElDN,UAAIE,OAAOC,KAAK,CAACI,MAAcT,SAAcU,aAAAA;AAC3CN,eAAOI,WAAWC,MAAMT,SAASU,QAAAA;MACnC,CAAA;IACF,WAAW,OAAON,WAAW,YAAY;AAEvCF,UAAIE,OAAOC,KAAKD,MAAAA;IAClB,OAAO;AACLT,UAAIgB,KAAK,eAAeN,GAAAA,uDAA0D;AAClFH,UAAIE,OAAOC,KAAKD,MAAAA;IAClB;AAEAF,QAAIU,IAAI,eAAeP,GAAAA;AACvBH,QAAIU,IAAI,SAASC,QAAQP,QAAAA,CAAAA;AAEzBX,QAAImB,KAAK,gBAAgBT,GAAAA,KAAQQ,QAAQP,QAAAA,CAAAA,GAAY;EACvD;AACF;","names":["resolve","Logger","log","Logger","for","ViewAdapter","name","options","beforeMount","app","_container","engine","ext","viewsDir","__express","renderFile","path","callback","warn","set","resolve","info"]}
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  buildRoutes,
3
3
  getControllerPath
4
- } from "./chunk-E552HYEB.js";
4
+ } from "./chunk-2OSVROBK.js";
5
5
  import "./chunk-LEILPDMW.js";
6
6
  import "./chunk-RPN7UFUO.js";
7
- import "./chunk-Y5ZSC2FB.js";
7
+ import "./chunk-IUT42U72.js";
8
8
  import "./chunk-WYY34UWG.js";
9
9
  import "./chunk-WCQVDF3K.js";
10
10
  export {