@forinda/kickjs 6.6.1 → 7.0.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.
Files changed (117) hide show
  1. package/dist/{adapter-CbMq-NBf.d.mts → adapter-diFqgxGR.d.mts} +25 -13
  2. package/dist/{adapter-CbMq-NBf.d.mts.map → adapter-diFqgxGR.d.mts.map} +1 -1
  3. package/dist/{app-module-DtoySG6l.d.mts → app-module-DtNJpj0r.d.mts} +35 -36
  4. package/dist/app-module-DtNJpj0r.d.mts.map +1 -0
  5. package/dist/{application-C2SrI5f8.mjs → application-Bflo42Wr.mjs} +3 -3
  6. package/dist/{application-C2SrI5f8.mjs.map → application-Bflo42Wr.mjs.map} +1 -1
  7. package/dist/{application-zlzYGgKD.d.mts → application-D_mReBi_.d.mts} +3 -3
  8. package/dist/{application-zlzYGgKD.d.mts.map → application-D_mReBi_.d.mts.map} +1 -1
  9. package/dist/{bootstrap-Nzgi7K2d.d.mts → bootstrap-CytCvAnr.d.mts} +2 -2
  10. package/dist/bootstrap-CytCvAnr.d.mts.map +1 -0
  11. package/dist/{bootstrap-DiRoG6j9.mjs → bootstrap-DtlMl3-0.mjs} +3 -3
  12. package/dist/{bootstrap-DiRoG6j9.mjs.map → bootstrap-DtlMl3-0.mjs.map} +1 -1
  13. package/dist/config/index.mjs +2 -2
  14. package/dist/{config-BSFg58Cw.mjs → config-Dx_Nbh79.mjs} +3 -3
  15. package/dist/{config-BSFg58Cw.mjs.map → config-Dx_Nbh79.mjs.map} +1 -1
  16. package/dist/{container-CSxotdD4.mjs → container-BmZciE3k.mjs} +3 -3
  17. package/dist/{container-CSxotdD4.mjs.map → container-BmZciE3k.mjs.map} +1 -1
  18. package/dist/{context-errors-BN6Kbb22.mjs → context-errors-DDYA51Nf.mjs} +2 -2
  19. package/dist/{context-errors-BN6Kbb22.mjs.map → context-errors-DDYA51Nf.mjs.map} +1 -1
  20. package/dist/{context-A-vHOCow.mjs → context-vG6HXwHB.mjs} +3 -3
  21. package/dist/context-vG6HXwHB.mjs.map +1 -0
  22. package/dist/core/adapter.d.mts +1 -1
  23. package/dist/core/adapter.mjs +1 -1
  24. package/dist/core/app-module.d.mts +1 -1
  25. package/dist/core/app-module.mjs +1 -1
  26. package/dist/core/cache.mjs +1 -1
  27. package/dist/core/container.mjs +2 -2
  28. package/dist/core/cron.mjs +2 -2
  29. package/dist/core/decorators.mjs +2 -2
  30. package/dist/core/errors.mjs +1 -1
  31. package/dist/core/interfaces.mjs +1 -1
  32. package/dist/core/logger.mjs +1 -1
  33. package/dist/core/path.mjs +1 -1
  34. package/dist/core/plugin.d.mts +1 -1
  35. package/dist/core/plugin.mjs +1 -1
  36. package/dist/core/reactivity.mjs +1 -1
  37. package/dist/{core-OXbl534_.mjs → core-kFuUx8FW.mjs} +3 -3
  38. package/dist/core-kFuUx8FW.mjs.map +1 -0
  39. package/dist/{csrf-CY_WY-VY.mjs → csrf-Dna2I7cx.mjs} +2 -2
  40. package/dist/{csrf-CY_WY-VY.mjs.map → csrf-Dna2I7cx.mjs.map} +1 -1
  41. package/dist/define-adapter-D3dAhxuu.mjs +12 -0
  42. package/dist/define-adapter-D3dAhxuu.mjs.map +1 -0
  43. package/dist/{disposables-DlLzYI40.mjs → disposables-C4BbFv_2.mjs} +2 -2
  44. package/dist/{disposables-DlLzYI40.mjs.map → disposables-C4BbFv_2.mjs.map} +1 -1
  45. package/dist/{env-B_ay2OjK.mjs → env-C5TMXJ7h.mjs} +3 -3
  46. package/dist/{env-B_ay2OjK.mjs.map → env-C5TMXJ7h.mjs.map} +1 -1
  47. package/dist/{handler-BTz5BPXI.mjs → handler-BKqb8K_H.mjs} +3 -3
  48. package/dist/{handler-BTz5BPXI.mjs.map → handler-BKqb8K_H.mjs.map} +1 -1
  49. package/dist/http/application.d.mts +1 -1
  50. package/dist/http/application.mjs +2 -2
  51. package/dist/http/bootstrap.d.mts +1 -1
  52. package/dist/http/bootstrap.mjs +2 -2
  53. package/dist/http/context.d.mts +2 -2
  54. package/dist/http/context.mjs +2 -2
  55. package/dist/http/middleware/cors.mjs +1 -1
  56. package/dist/http/middleware/csrf.mjs +2 -2
  57. package/dist/http/middleware/error-handler.mjs +2 -2
  58. package/dist/http/middleware/helmet.mjs +1 -1
  59. package/dist/http/middleware/rate-limit.mjs +2 -2
  60. package/dist/http/middleware/request-id.mjs +1 -1
  61. package/dist/http/middleware/request-logger.mjs +2 -2
  62. package/dist/http/middleware/session.mjs +2 -2
  63. package/dist/http/middleware/spa.d.mts +2 -2
  64. package/dist/http/middleware/spa.mjs +2 -2
  65. package/dist/http/middleware/spa.mjs.map +1 -1
  66. package/dist/http/middleware/upload.d.mts +1 -1
  67. package/dist/http/middleware/upload.mjs +2 -2
  68. package/dist/http/middleware/validate.mjs +1 -1
  69. package/dist/http/middleware/views.d.mts +1 -1
  70. package/dist/http/middleware/views.mjs +3 -2
  71. package/dist/http/middleware/views.mjs.map +1 -0
  72. package/dist/http/query/index.mjs +2 -2
  73. package/dist/http/router-builder.d.mts +1 -1
  74. package/dist/http/router-builder.mjs +2 -2
  75. package/dist/http/runtimes/fastify.d.mts +1 -1
  76. package/dist/http/runtimes/fastify.mjs +2 -2
  77. package/dist/http/runtimes/h3-web.d.mts +1 -1
  78. package/dist/http/runtimes/h3-web.mjs +2 -2
  79. package/dist/http/runtimes/h3.d.mts +1 -1
  80. package/dist/http/runtimes/h3.mjs +2 -2
  81. package/dist/index.d.mts +9 -10
  82. package/dist/index.mjs +2 -2
  83. package/dist/index.mjs.map +1 -1
  84. package/dist/{kv-stores-CHowq8VP.d.mts → kv-stores-D2kSUAEP.d.mts} +2 -2
  85. package/dist/{kv-stores-CHowq8VP.d.mts.map → kv-stores-D2kSUAEP.d.mts.map} +1 -1
  86. package/dist/{kv-stores-CcD6HvGV.mjs → kv-stores-zApd2iLD.mjs} +2 -2
  87. package/dist/{kv-stores-CcD6HvGV.mjs.map → kv-stores-zApd2iLD.mjs.map} +1 -1
  88. package/dist/{metadata-Ct03gV65.mjs → metadata-Cf6JPQax.mjs} +2 -2
  89. package/dist/{metadata-Ct03gV65.mjs.map → metadata-Cf6JPQax.mjs.map} +1 -1
  90. package/dist/{module-registry-DDdtsQld.mjs → module-registry-Cpf01UdE.mjs} +2 -2
  91. package/dist/{module-registry-DDdtsQld.mjs.map → module-registry-Cpf01UdE.mjs.map} +1 -1
  92. package/dist/{query-D6XQVnUA.mjs → query-tx53xRSZ.mjs} +2 -2
  93. package/dist/{query-D6XQVnUA.mjs.map → query-tx53xRSZ.mjs.map} +1 -1
  94. package/dist/{reply-Dcq46cVC.mjs → reply-dxkvXqNo.mjs} +3 -3
  95. package/dist/{reply-Dcq46cVC.mjs.map → reply-dxkvXqNo.mjs.map} +1 -1
  96. package/dist/{router-builder-C6Qmb3rB.mjs → router-builder-BjLsf6w0.mjs} +3 -3
  97. package/dist/{router-builder-C6Qmb3rB.mjs.map → router-builder-BjLsf6w0.mjs.map} +1 -1
  98. package/dist/{router-builder-CMGhEBU2.d.mts → router-builder-C6E11Grf.d.mts} +2 -2
  99. package/dist/{router-builder-CMGhEBU2.d.mts.map → router-builder-C6E11Grf.d.mts.map} +1 -1
  100. package/dist/{views-DsfBT1fR.d.mts → spa-DJbVV_4F.d.mts} +139 -13
  101. package/dist/spa-DJbVV_4F.d.mts.map +1 -0
  102. package/dist/{upload-XVTyQcNN.d.mts → upload-Wck12jE5.d.mts} +2 -2
  103. package/dist/{upload-XVTyQcNN.d.mts.map → upload-Wck12jE5.d.mts.map} +1 -1
  104. package/dist/{upload-config-DXxieUa0.mjs → upload-config-BlmVJI4n.mjs} +2 -2
  105. package/dist/{upload-config-DXxieUa0.mjs.map → upload-config-BlmVJI4n.mjs.map} +1 -1
  106. package/dist/web.d.mts +3 -3
  107. package/dist/web.mjs +2 -2
  108. package/package.json +2 -2
  109. package/dist/app-module-DtoySG6l.d.mts.map +0 -1
  110. package/dist/bootstrap-Nzgi7K2d.d.mts.map +0 -1
  111. package/dist/context-A-vHOCow.mjs.map +0 -1
  112. package/dist/core-OXbl534_.mjs.map +0 -1
  113. package/dist/spa-DsRUrm69.d.mts +0 -97
  114. package/dist/spa-DsRUrm69.d.mts.map +0 -1
  115. package/dist/views-D_LVXJWs.mjs +0 -12
  116. package/dist/views-D_LVXJWs.mjs.map +0 -1
  117. package/dist/views-DsfBT1fR.d.mts.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"reply-Dcq46cVC.mjs","names":[],"sources":["../src/http/middleware/request-scope.ts","../src/http/reply.ts"],"sourcesContent":["// Web Crypto via globalThis.crypto (randomUUID) — portable across\n// node/bun/deno/workers; node:crypto deliberately NOT imported here so this\n// module stays on the edge-safe request path.\nimport type { Request, Response, NextFunction } from 'express'\nimport { requestStore, type RequestStore } from '../request-store'\nimport { METADATA } from '../../core/interfaces'\nimport { getClassMetaOrUndefined } from '../../core/metadata'\nimport { createLogger } from '../../core/logger'\n\nconst log = createLogger('RequestScope')\n\n// Per-class @PreDestroy method-name cache: the metadata is immutable after\n// class definition, so pay the Reflect read once per class, not once per\n// instance per request. `null` = \"checked, none declared\".\nconst preDestroyCache = new Map<Function, string | symbol | null>()\n\nfunction preDestroyMethod(ctor: Function): string | symbol | null {\n let method = preDestroyCache.get(ctor)\n if (method === undefined) {\n method =\n getClassMetaOrUndefined<string | symbol>(METADATA.PRE_DESTROY, (ctor as any).prototype) ??\n null\n preDestroyCache.set(ctor, method)\n }\n return method\n}\n\n/**\n * Run `@PreDestroy` hooks on every REQUEST-scoped instance the request\n * created, then drop them. Called when the response closes (finished or\n * aborted) — the counterpart to `@PostConstruct`, closing the lifecycle gap\n * where per-request services holding transactions/handles/subscriptions were\n * silently dropped with no cleanup callback.\n *\n * Idempotent (guarded by `store.disposed`); hook errors are logged and\n * swallowed so one failing teardown can't break request completion. Async\n * hooks are fired without being awaited — the response is already gone.\n */\nexport function disposeRequestStore(store: RequestStore): void {\n if (store.disposed) return\n store.disposed = true\n if (store.instances.size === 0) return\n for (const instance of store.instances.values()) {\n if (instance === null || typeof instance !== 'object') continue\n const method = preDestroyMethod(instance.constructor)\n if (!method || typeof instance[method] !== 'function') continue\n try {\n const result = instance[method]()\n if (result && typeof result.then === 'function') {\n ;(result as Promise<void>).catch((err) =>\n log.error(err, `@PreDestroy on ${instance.constructor.name} rejected`),\n )\n }\n } catch (err) {\n log.error(err, `@PreDestroy on ${instance.constructor.name} threw`)\n }\n }\n store.instances.clear()\n}\n\n/**\n * Marker symbol stamped on the function returned by {@link requestScopeMiddleware}.\n *\n * `Application.setup()` uses {@link isRequestScopeMiddleware} to detect whether\n * the user-supplied middleware list already contains the request-scope wrapper,\n * and skips its automatic auto-mount when one is found. Lets adopters control\n * the exact position of the ALS frame in their pipeline (e.g., after a custom\n * tracing wrapper) without ending up with two nested frames.\n */\n// `Symbol.for(...)` returns a plain `symbol` (not `unique symbol`), but the\n// runtime detection marker doesn't need the unique nominal property — it only\n// needs the interned-slot reference equality that `Symbol.for` already provides.\nconst REQUEST_SCOPE_MIDDLEWARE_MARKER = Symbol.for('@kickjs/requestScopeMiddleware')\n\n/**\n * Build a fresh per-request store. Shared by {@link requestScopeMiddleware}\n * (Express) and by runtimes whose handler runs outside the connect-middleware\n * chain (e.g. Fastify), so both establish an identical ALS frame.\n */\nexport function createRequestStore(requestId?: string): RequestStore {\n return {\n requestId: requestId || crypto.randomUUID(),\n instances: new Map(),\n // Explicit generic locks the \"string keys only\" decision.\n values: new Map<string, unknown>(),\n }\n}\n\n/**\n * Wraps each request in an AsyncLocalStorage context.\n * Enables REQUEST-scoped DI and automatic requestId propagation to logs.\n *\n * Should be mounted early in the middleware pipeline (before route handlers).\n * `Application` mounts this automatically unless `contextStore: 'manual'` is set\n * or the user already includes one in their middleware list.\n */\nexport function requestScopeMiddleware() {\n const mw = (req: Request, res: Response, next: NextFunction) => {\n const requestIdHeader = req.headers['x-request-id']\n const requestId =\n (Array.isArray(requestIdHeader) ? requestIdHeader[0] : requestIdHeader) || crypto.randomUUID()\n const store = createRequestStore(requestId)\n // Surface the requestId on req so the standalone requestId() middleware\n // (when also mounted) reuses this value instead of generating a divergent\n // second one. Without this, log lines that read from the ALS store would\n // disagree with the X-Request-Id response header for any request that\n // didn't carry an inbound x-request-id header.\n ;(req as Request & { requestId?: string }).requestId = requestId\n // 'close' fires once the response finished OR the client aborted — run\n // @PreDestroy teardown for the request's scoped instances either way.\n res.once('close', () => disposeRequestStore(store))\n // Pass `next` directly — `run` invokes it with no args, so the arrow\n // wrapper previously allocated per request added nothing.\n requestStore.run(store, next)\n }\n ;(mw as unknown as Record<symbol, unknown>)[REQUEST_SCOPE_MIDDLEWARE_MARKER] = true\n return mw\n}\n\n/**\n * Returns `true` when `value` is the function produced by {@link requestScopeMiddleware}.\n *\n * Detection uses a `Symbol.for(...)` marker so the check survives different\n * copies of `@forinda/kickjs` loaded under different module identities (e.g.,\n * a published package alongside a workspace-linked one during local dev).\n */\nexport function isRequestScopeMiddleware(value: unknown): boolean {\n if (typeof value !== 'function') return false\n return (value as unknown as Record<symbol, unknown>)[REQUEST_SCOPE_MIDDLEWARE_MARKER] === true\n}\n","// Return-value handler support (response-inference-design.md §3.1).\n//\n// Handlers may RETURN their response payload instead of calling `ctx.json`:\n// the runtime auto-sends it when the pipeline finishes with nothing written.\n// `reply(status, body)` wraps a payload with a non-200 status while staying\n// transparent to type inference (`Reply<201, Task>` carries both statically).\n//\n// Edge-safe: no node imports — shared by all four runtimes including the\n// `@forinda/kickjs/web` entry.\n\nimport type { RequestContext } from './context'\n\n// `Symbol.for` so the brand survives duplicate module copies (workspace-linked\n// + published package in one process — same rationale as the request-scope\n// middleware marker).\nconst REPLY_BRAND = Symbol.for('kickjs.reply')\n\n/**\n * A status-carrying response payload for return-value handlers. Create via\n * {@link reply} — the brand is what runtimes detect, and the generics are\n * what the `kick/routes` response typegen unwraps (`Reply<S, T>` → `T`).\n */\nexport interface Reply<S extends number = number, T = unknown> {\n readonly [REPLY_BRAND]: true\n readonly status: S\n readonly body: T\n}\n\n/**\n * Wrap a return-value handler's payload with an explicit status code:\n *\n * ```ts\n * @Post('/')\n * async create(ctx: RequestContext) {\n * return reply(201, await this.tasks.create(ctx.body))\n * }\n * ```\n */\nexport function reply<S extends number, T>(status: S, body: T): Reply<S, T> {\n return { [REPLY_BRAND]: true, status, body }\n}\n\n/** `reply(201, body)` */\nreply.created = <T>(body: T): Reply<201, T> => reply(201, body)\n/** `reply(202, body)` */\nreply.accepted = <T>(body: T): Reply<202, T> => reply(202, body)\n/** Empty 204 — maps to `ctx.noContent()`. */\nreply.noContent = (): Reply<204, undefined> => reply(204, undefined)\n\n/**\n * The statically-inferred response type of a controller handler — what\n * `kick typegen` emits into `KickRoutes[...].response`:\n *\n * - `Awaited<ReturnType>` of the method\n * - `Reply<S, T>` unwraps to `T`\n * - `void`/`undefined` members are dropped (imperative `ctx.json` branches);\n * a handler that never returns a value stays `unknown`, matching the\n * pre-inference emission\n *\n * ```ts\n * type R = InferHandlerResponse<UsersController['get']> // → User\n * ```\n */\nexport type InferHandlerResponse<H> = H extends (...args: never[]) => infer R\n ? [Exclude<Awaited<R>, void | undefined>] extends [never]\n ? unknown\n : UnwrapReply<Exclude<Awaited<R>, void | undefined>>\n : unknown\n\ntype UnwrapReply<A> = A extends Reply<number, infer B> ? B : A\n\nexport function isReply(value: unknown): value is Reply {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as Record<symbol, unknown>)[REPLY_BRAND] === true\n )\n}\n\n/**\n * Send a handler's RETURN value when the pipeline finished without anything\n * having written a response. Callers guard on \"driver/response unsettled\" —\n * this only decides how to send:\n *\n * - `undefined` → not handled (returns false; runtime keeps today's behavior)\n * - `Reply` wrapper → status + body (204 → `ctx.noContent()`)\n * - anything else → `ctx.json(value)` (200)\n *\n * Returns true when a response was produced.\n */\nexport function applyHandlerResult(ctx: RequestContext, result: unknown): boolean {\n if (result === undefined) return false\n if (isReply(result)) {\n // Only 204 maps to noContent — an undefined body must NOT coerce the\n // declared status (reply(304, undefined) keeps its 304; the json body\n // is simply empty).\n if (result.status === 204) {\n ctx.noContent()\n return true\n }\n ctx.json(result.body, result.status)\n return true\n }\n ctx.json(result)\n return true\n}\n"],"mappings":";;;;;;;;;;gNASA,MAAM,IAAM,aAAa,cAAc,EAKjC,gBAAkB,IAAI,IAE5B,SAAS,iBAAiB,KAAwC,CAChE,IAAI,OAAS,gBAAgB,IAAI,IAAI,EAOrC,OANI,SAAW,IAAA,KACb,OACE,wBAAyC,SAAS,YAAc,KAAa,SAAS,GACtF,KACF,gBAAgB,IAAI,KAAM,MAAM,GAE3B,MACT,CAaA,SAAgB,oBAAoB,MAA2B,CACzD,UAAM,WACV,MAAM,SAAW,GACb,MAAM,UAAU,OAAS,GAC7B,KAAK,IAAM,YAAY,MAAM,UAAU,OAAO,EAAG,CAC/C,GAAyB,OAAO,UAAa,WAAzC,SAAmD,SACvD,IAAM,OAAS,iBAAiB,SAAS,WAAW,EAChD,MAAC,QAAU,OAAO,SAAS,SAAY,YAC3C,GAAI,CACF,IAAM,OAAS,SAAS,OAAO,CAAC,EAC5B,QAAU,OAAO,OAAO,MAAS,YAClC,OAA0B,MAAO,KAChC,IAAI,MAAM,IAAK,kBAAkB,SAAS,YAAY,KAAK,UAAU,CACvE,CAEJ,OAAS,IAAK,CACZ,IAAI,MAAM,IAAK,kBAAkB,SAAS,YAAY,KAAK,OAAO,CACpE,CACF,CACA,MAAM,UAAU,MAAM,CADtB,CAEF,CAcA,MAAM,gCAAkC,OAAO,IAAI,gCAAgC,EAOnF,SAAgB,mBAAmB,UAAkC,CACnE,MAAO,CACL,UAAW,WAAa,OAAO,WAAW,EAC1C,UAAW,IAAI,IAEf,OAAQ,IAAI,GACd,CACF,CAUA,SAAgB,wBAAyB,CACvC,IAAM,IAAM,IAAc,IAAe,OAAuB,CAC9D,IAAM,gBAAkB,IAAI,QAAQ,gBAC9B,WACH,MAAM,QAAQ,eAAe,EAAI,gBAAgB,GAAK,kBAAoB,OAAO,WAAW,EACzF,MAAQ,mBAAmB,SAAS,EAMzC,IAA0C,UAAY,UAGvD,IAAI,KAAK,YAAe,oBAAoB,KAAK,CAAC,EAGlD,aAAa,IAAI,MAAO,IAAI,CAC9B,EAEA,MADC,IAA2C,iCAAmC,GACxE,EACT,CASA,SAAgB,yBAAyB,MAAyB,CAEhE,OADI,OAAO,OAAU,YACb,MAA6C,mCAAqC,EAC5F,CClHA,MAAM,YAAc,OAAO,IAAI,cAAc,EAuB7C,SAAgB,MAA2B,OAAW,KAAsB,CAC1E,MAAO,EAAG,aAAc,GAAM,OAAQ,IAAK,CAC7C,CAGA,MAAM,QAAc,MAA2B,MAAM,IAAK,IAAI,EAE9D,MAAM,SAAe,MAA2B,MAAM,IAAK,IAAI,EAE/D,MAAM,cAAyC,MAAM,IAAK,IAAA,EAAS,EAwBnE,SAAgB,QAAQ,MAAgC,CACtD,OACE,OAAO,OAAU,YACjB,OACC,MAAkC,eAAiB,EAExD,CAaA,SAAgB,mBAAmB,IAAqB,OAA0B,CAchF,OAbI,SAAW,IAAA,GAAkB,GAC7B,QAAQ,MAAM,EAIZ,OAAO,SAAW,KACpB,IAAI,UAAU,EACP,KAET,IAAI,KAAK,OAAO,KAAM,OAAO,MAAM,EAC5B,KAET,IAAI,KAAK,MAAM,EACR,GACT"}
1
+ {"version":3,"file":"reply-dxkvXqNo.mjs","names":[],"sources":["../src/http/middleware/request-scope.ts","../src/http/reply.ts"],"sourcesContent":["// Web Crypto via globalThis.crypto (randomUUID) — portable across\n// node/bun/deno/workers; node:crypto deliberately NOT imported here so this\n// module stays on the edge-safe request path.\nimport type { Request, Response, NextFunction } from 'express'\nimport { requestStore, type RequestStore } from '../request-store'\nimport { METADATA } from '../../core/interfaces'\nimport { getClassMetaOrUndefined } from '../../core/metadata'\nimport { createLogger } from '../../core/logger'\n\nconst log = createLogger('RequestScope')\n\n// Per-class @PreDestroy method-name cache: the metadata is immutable after\n// class definition, so pay the Reflect read once per class, not once per\n// instance per request. `null` = \"checked, none declared\".\nconst preDestroyCache = new Map<Function, string | symbol | null>()\n\nfunction preDestroyMethod(ctor: Function): string | symbol | null {\n let method = preDestroyCache.get(ctor)\n if (method === undefined) {\n method =\n getClassMetaOrUndefined<string | symbol>(METADATA.PRE_DESTROY, (ctor as any).prototype) ??\n null\n preDestroyCache.set(ctor, method)\n }\n return method\n}\n\n/**\n * Run `@PreDestroy` hooks on every REQUEST-scoped instance the request\n * created, then drop them. Called when the response closes (finished or\n * aborted) — the counterpart to `@PostConstruct`, closing the lifecycle gap\n * where per-request services holding transactions/handles/subscriptions were\n * silently dropped with no cleanup callback.\n *\n * Idempotent (guarded by `store.disposed`); hook errors are logged and\n * swallowed so one failing teardown can't break request completion. Async\n * hooks are fired without being awaited — the response is already gone.\n */\nexport function disposeRequestStore(store: RequestStore): void {\n if (store.disposed) return\n store.disposed = true\n if (store.instances.size === 0) return\n for (const instance of store.instances.values()) {\n if (instance === null || typeof instance !== 'object') continue\n const method = preDestroyMethod(instance.constructor)\n if (!method || typeof instance[method] !== 'function') continue\n try {\n const result = instance[method]()\n if (result && typeof result.then === 'function') {\n ;(result as Promise<void>).catch((err) =>\n log.error(err, `@PreDestroy on ${instance.constructor.name} rejected`),\n )\n }\n } catch (err) {\n log.error(err, `@PreDestroy on ${instance.constructor.name} threw`)\n }\n }\n store.instances.clear()\n}\n\n/**\n * Marker symbol stamped on the function returned by {@link requestScopeMiddleware}.\n *\n * `Application.setup()` uses {@link isRequestScopeMiddleware} to detect whether\n * the user-supplied middleware list already contains the request-scope wrapper,\n * and skips its automatic auto-mount when one is found. Lets adopters control\n * the exact position of the ALS frame in their pipeline (e.g., after a custom\n * tracing wrapper) without ending up with two nested frames.\n */\n// `Symbol.for(...)` returns a plain `symbol` (not `unique symbol`), but the\n// runtime detection marker doesn't need the unique nominal property — it only\n// needs the interned-slot reference equality that `Symbol.for` already provides.\nconst REQUEST_SCOPE_MIDDLEWARE_MARKER = Symbol.for('@kickjs/requestScopeMiddleware')\n\n/**\n * Build a fresh per-request store. Shared by {@link requestScopeMiddleware}\n * (Express) and by runtimes whose handler runs outside the connect-middleware\n * chain (e.g. Fastify), so both establish an identical ALS frame.\n */\nexport function createRequestStore(requestId?: string): RequestStore {\n return {\n requestId: requestId || crypto.randomUUID(),\n instances: new Map(),\n // Explicit generic locks the \"string keys only\" decision.\n values: new Map<string, unknown>(),\n }\n}\n\n/**\n * Wraps each request in an AsyncLocalStorage context.\n * Enables REQUEST-scoped DI and automatic requestId propagation to logs.\n *\n * Should be mounted early in the middleware pipeline (before route handlers).\n * `Application` mounts this automatically unless `contextStore: 'manual'` is set\n * or the user already includes one in their middleware list.\n */\nexport function requestScopeMiddleware() {\n const mw = (req: Request, res: Response, next: NextFunction) => {\n const requestIdHeader = req.headers['x-request-id']\n const requestId =\n (Array.isArray(requestIdHeader) ? requestIdHeader[0] : requestIdHeader) || crypto.randomUUID()\n const store = createRequestStore(requestId)\n // Surface the requestId on req so the standalone requestId() middleware\n // (when also mounted) reuses this value instead of generating a divergent\n // second one. Without this, log lines that read from the ALS store would\n // disagree with the X-Request-Id response header for any request that\n // didn't carry an inbound x-request-id header.\n ;(req as Request & { requestId?: string }).requestId = requestId\n // 'close' fires once the response finished OR the client aborted — run\n // @PreDestroy teardown for the request's scoped instances either way.\n res.once('close', () => disposeRequestStore(store))\n // Pass `next` directly — `run` invokes it with no args, so the arrow\n // wrapper previously allocated per request added nothing.\n requestStore.run(store, next)\n }\n ;(mw as unknown as Record<symbol, unknown>)[REQUEST_SCOPE_MIDDLEWARE_MARKER] = true\n return mw\n}\n\n/**\n * Returns `true` when `value` is the function produced by {@link requestScopeMiddleware}.\n *\n * Detection uses a `Symbol.for(...)` marker so the check survives different\n * copies of `@forinda/kickjs` loaded under different module identities (e.g.,\n * a published package alongside a workspace-linked one during local dev).\n */\nexport function isRequestScopeMiddleware(value: unknown): boolean {\n if (typeof value !== 'function') return false\n return (value as unknown as Record<symbol, unknown>)[REQUEST_SCOPE_MIDDLEWARE_MARKER] === true\n}\n","// Return-value handler support (response-inference-design.md §3.1).\n//\n// Handlers may RETURN their response payload instead of calling `ctx.json`:\n// the runtime auto-sends it when the pipeline finishes with nothing written.\n// `reply(status, body)` wraps a payload with a non-200 status while staying\n// transparent to type inference (`Reply<201, Task>` carries both statically).\n//\n// Edge-safe: no node imports — shared by all four runtimes including the\n// `@forinda/kickjs/web` entry.\n\nimport type { RequestContext } from './context'\n\n// `Symbol.for` so the brand survives duplicate module copies (workspace-linked\n// + published package in one process — same rationale as the request-scope\n// middleware marker).\nconst REPLY_BRAND = Symbol.for('kickjs.reply')\n\n/**\n * A status-carrying response payload for return-value handlers. Create via\n * {@link reply} — the brand is what runtimes detect, and the generics are\n * what the `kick/routes` response typegen unwraps (`Reply<S, T>` → `T`).\n */\nexport interface Reply<S extends number = number, T = unknown> {\n readonly [REPLY_BRAND]: true\n readonly status: S\n readonly body: T\n}\n\n/**\n * Wrap a return-value handler's payload with an explicit status code:\n *\n * ```ts\n * @Post('/')\n * async create(ctx: RequestContext) {\n * return reply(201, await this.tasks.create(ctx.body))\n * }\n * ```\n */\nexport function reply<S extends number, T>(status: S, body: T): Reply<S, T> {\n return { [REPLY_BRAND]: true, status, body }\n}\n\n/** `reply(201, body)` */\nreply.created = <T>(body: T): Reply<201, T> => reply(201, body)\n/** `reply(202, body)` */\nreply.accepted = <T>(body: T): Reply<202, T> => reply(202, body)\n/** Empty 204 — maps to `ctx.noContent()`. */\nreply.noContent = (): Reply<204, undefined> => reply(204, undefined)\n\n/**\n * The statically-inferred response type of a controller handler — what\n * `kick typegen` emits into `KickRoutes[...].response`:\n *\n * - `Awaited<ReturnType>` of the method\n * - `Reply<S, T>` unwraps to `T`\n * - `void`/`undefined` members are dropped (imperative `ctx.json` branches);\n * a handler that never returns a value stays `unknown`, matching the\n * pre-inference emission\n *\n * ```ts\n * type R = InferHandlerResponse<UsersController['get']> // → User\n * ```\n */\nexport type InferHandlerResponse<H> = H extends (...args: never[]) => infer R\n ? [Exclude<Awaited<R>, void | undefined>] extends [never]\n ? unknown\n : UnwrapReply<Exclude<Awaited<R>, void | undefined>>\n : unknown\n\ntype UnwrapReply<A> = A extends Reply<number, infer B> ? B : A\n\nexport function isReply(value: unknown): value is Reply {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as Record<symbol, unknown>)[REPLY_BRAND] === true\n )\n}\n\n/**\n * Send a handler's RETURN value when the pipeline finished without anything\n * having written a response. Callers guard on \"driver/response unsettled\" —\n * this only decides how to send:\n *\n * - `undefined` → not handled (returns false; runtime keeps today's behavior)\n * - `Reply` wrapper → status + body (204 → `ctx.noContent()`)\n * - anything else → `ctx.json(value)` (200)\n *\n * Returns true when a response was produced.\n */\nexport function applyHandlerResult(ctx: RequestContext, result: unknown): boolean {\n if (result === undefined) return false\n if (isReply(result)) {\n // Only 204 maps to noContent — an undefined body must NOT coerce the\n // declared status (reply(304, undefined) keeps its 304; the json body\n // is simply empty).\n if (result.status === 204) {\n ctx.noContent()\n return true\n }\n ctx.json(result.body, result.status)\n return true\n }\n ctx.json(result)\n return true\n}\n"],"mappings":";;;;;;;;;;gNASA,MAAM,IAAM,aAAa,cAAc,EAKjC,gBAAkB,IAAI,IAE5B,SAAS,iBAAiB,KAAwC,CAChE,IAAI,OAAS,gBAAgB,IAAI,IAAI,EAOrC,OANI,SAAW,IAAA,KACb,OACE,wBAAyC,SAAS,YAAc,KAAa,SAAS,GACtF,KACF,gBAAgB,IAAI,KAAM,MAAM,GAE3B,MACT,CAaA,SAAgB,oBAAoB,MAA2B,CACzD,UAAM,WACV,MAAM,SAAW,GACb,MAAM,UAAU,OAAS,GAC7B,KAAK,IAAM,YAAY,MAAM,UAAU,OAAO,EAAG,CAC/C,GAAyB,OAAO,UAAa,WAAzC,SAAmD,SACvD,IAAM,OAAS,iBAAiB,SAAS,WAAW,EAChD,MAAC,QAAU,OAAO,SAAS,SAAY,YAC3C,GAAI,CACF,IAAM,OAAS,SAAS,OAAO,CAAC,EAC5B,QAAU,OAAO,OAAO,MAAS,YAClC,OAA0B,MAAO,KAChC,IAAI,MAAM,IAAK,kBAAkB,SAAS,YAAY,KAAK,UAAU,CACvE,CAEJ,OAAS,IAAK,CACZ,IAAI,MAAM,IAAK,kBAAkB,SAAS,YAAY,KAAK,OAAO,CACpE,CACF,CACA,MAAM,UAAU,MAAM,CADtB,CAEF,CAcA,MAAM,gCAAkC,OAAO,IAAI,gCAAgC,EAOnF,SAAgB,mBAAmB,UAAkC,CACnE,MAAO,CACL,UAAW,WAAa,OAAO,WAAW,EAC1C,UAAW,IAAI,IAEf,OAAQ,IAAI,GACd,CACF,CAUA,SAAgB,wBAAyB,CACvC,IAAM,IAAM,IAAc,IAAe,OAAuB,CAC9D,IAAM,gBAAkB,IAAI,QAAQ,gBAC9B,WACH,MAAM,QAAQ,eAAe,EAAI,gBAAgB,GAAK,kBAAoB,OAAO,WAAW,EACzF,MAAQ,mBAAmB,SAAS,EAMzC,IAA0C,UAAY,UAGvD,IAAI,KAAK,YAAe,oBAAoB,KAAK,CAAC,EAGlD,aAAa,IAAI,MAAO,IAAI,CAC9B,EAEA,MADC,IAA2C,iCAAmC,GACxE,EACT,CASA,SAAgB,yBAAyB,MAAyB,CAEhE,OADI,OAAO,OAAU,YACb,MAA6C,mCAAqC,EAC5F,CClHA,MAAM,YAAc,OAAO,IAAI,cAAc,EAuB7C,SAAgB,MAA2B,OAAW,KAAsB,CAC1E,MAAO,EAAG,aAAc,GAAM,OAAQ,IAAK,CAC7C,CAGA,MAAM,QAAc,MAA2B,MAAM,IAAK,IAAI,EAE9D,MAAM,SAAe,MAA2B,MAAM,IAAK,IAAI,EAE/D,MAAM,cAAyC,MAAM,IAAK,IAAA,EAAS,EAwBnE,SAAgB,QAAQ,MAAgC,CACtD,OACE,OAAO,OAAU,YACjB,OACC,MAAkC,eAAiB,EAExD,CAaA,SAAgB,mBAAmB,IAAqB,OAA0B,CAchF,OAbI,SAAW,IAAA,GAAkB,GAC7B,QAAQ,MAAM,EAIZ,OAAO,SAAW,KACpB,IAAI,UAAU,EACP,KAET,IAAI,KAAK,OAAO,KAAM,OAAO,MAAM,EAC5B,KAET,IAAI,KAAK,MAAM,EACR,GACT"}
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v6.6.1
2
+ * @forinda/kickjs v7.0.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -8,5 +8,5 @@
8
8
  *
9
9
  * @license MIT
10
10
  */
11
- import{METADATA}from"./core/interfaces.mjs";import{o as duplicateRouteError,t as Container}from"./container-CSxotdD4.mjs";import{a as getMethodMeta,o as getMethodMetaOrUndefined,t as getClassMeta}from"./metadata-Ct03gV65.mjs";import{i as MissingContributorError,n as DuplicateContributorError,t as ContributorCycleError}from"./context-errors-BN6Kbb22.mjs";const PRECEDENCE_ORDER=[`method`,`class`,`module`,`adapter`,`global`];function buildPipeline(sources,options={}){let route=options.route,bySource=new Map;for(let src of PRECEDENCE_ORDER)bySource.set(src,[]);for(let entry of sources)bySource.get(entry.source).push(entry);let winners=new Map;for(let src of PRECEDENCE_ORDER){let rows=bySource.get(src),keysAtThisLevel=new Map;for(let row of rows){let key=row.registration.key,existing=keysAtThisLevel.get(key)??[];existing.push(row),keysAtThisLevel.set(key,existing)}for(let[key,rowsForKey]of keysAtThisLevel){if(rowsForKey.length>1)throw new DuplicateContributorError(key,rowsForKey.map((r,i)=>r.label??`${src}#${i}`));winners.has(key)||winners.set(key,rowsForKey[0])}}let survivors=[...winners.values()].map(r=>r.registration),allKeys=new Set(survivors.map(r=>r.key));for(let reg of survivors)for(let dep of reg.dependsOn)if(!allKeys.has(dep))throw new MissingContributorError(dep,reg.key,route,reg.definedAt);let sorted=topoSort(survivors,route);return Object.freeze({contributors:Object.freeze(sorted),keys:allKeys})}function topoSort(registrations,route){let indegree=new Map,dependents=new Map,byKey=new Map;for(let reg of registrations)indegree.set(reg.key,reg.dependsOn.length),byKey.set(reg.key,reg);for(let reg of registrations)for(let dep of reg.dependsOn){let list=dependents.get(dep)??[];list.push(reg),dependents.set(dep,list)}let queue=[];for(let reg of registrations)indegree.get(reg.key)===0&&queue.push(reg);let sorted=[];for(;queue.length>0;){let node=queue.shift();sorted.push(node);for(let dependent of dependents.get(node.key)??[]){let next=(indegree.get(dependent.key)??0)-1;indegree.set(dependent.key,next),next===0&&queue.push(dependent)}}if(sorted.length!==registrations.length)throw new ContributorCycleError(reconstructCycle(registrations.filter(r=>(indegree.get(r.key)??0)>0),byKey),route);return sorted}function reconstructCycle(residual,byKey){let residualKeys=new Set(residual.map(r=>r.key)),start=residual[0].key,path=[start],seen=new Set([start]),cursor=start;for(;;){let nextDep=byKey.get(cursor).dependsOn.find(d=>residualKeys.has(d));if(nextDep===void 0)break;if(seen.has(nextDep)){let cycleStart=path.indexOf(nextDep);return[...path.slice(cycleStart),nextDep]}path.push(nextDep),seen.add(nextDep),cursor=nextDep}return path}async function runContributors(options){let{pipeline,ctx,container}=options;for(let reg of pipeline.contributors)await runOne(reg,ctx,container)}async function runOne(reg,ctx,container){let value;try{let deps=resolveDeps(reg,container);value=await reg.resolve(ctx,deps)}catch(err){if(reg.optional)return;if(reg.onError){let replacement=await reg.onError(err,ctx);if(replacement===void 0)return;ctx.set(reg.key,replacement);return}throw err}ctx.set(reg.key,value)}function resolveDeps(reg,container){let out={};for(let[key,token]of Object.entries(reg.deps))out[key]=container.resolve(token);return out}let _externalContributorSources=[];function _setExternalContributorSources(sources){_externalContributorSources=sources}function assertRouteUnique(registry,method,fullPath,owner){let key=`${method.toUpperCase()} ${fullPath.replace(/:[^/]+/g,`:*`)}`,prior=registry.get(key);if(prior!==void 0)throw duplicateRouteError(method.toUpperCase(),fullPath,owner,prior);registry.set(key,owner)}function buildRouteTable(controllerClass,options={}){let container=Container.getInstance(),externalSources=options.externalSources??_externalContributorSources,routes=getClassMeta(METADATA.ROUTES,controllerClass,[]),classMiddlewares=getClassMeta(METADATA.CLASS_MIDDLEWARES,controllerClass,[]),classContributors=getClassMeta(METADATA.CLASS_CONTRIBUTORS,controllerClass,[]),entries=[];for(let route of routes){let method=route.method.toUpperCase(),fullPath=route.path||`/`,methodMiddlewares=getMethodMeta(METADATA.METHOD_MIDDLEWARES,controllerClass,route.handlerName,[]),fileUploadConfig=getMethodMetaOrUndefined(METADATA.FILE_UPLOAD,controllerClass,route.handlerName),methodContributors=getMethodMeta(METADATA.METHOD_CONTRIBUTORS,controllerClass,route.handlerName,[]),contributorRunner=null;if(classContributors.length>0||methodContributors.length>0||externalSources.length>0){let pipeline=buildPipeline([...methodContributors.map((registration,i)=>({source:`method`,registration,label:`${controllerClass.name}.${String(route.handlerName)}#${i}(${registration.key})`})),...classContributors.map((registration,i)=>({source:`class`,registration,label:`${controllerClass.name}.@class#${i}(${registration.key})`})),...externalSources],{route:`${method} ${fullPath}`});contributorRunner=ctx=>runContributors({pipeline,ctx,container})}let handler=ctx=>container.resolve(controllerClass)[route.handlerName](ctx);entries.push({method,path:fullPath,middlewares:[...classMiddlewares,...methodMiddlewares],contributorRunner,handler,meta:{controller:controllerClass,handlerName:route.handlerName,validation:route.validation,upload:fileUploadConfig}})}return entries}export{buildPipeline as a,runContributors as i,assertRouteUnique as n,buildRouteTable as r,_setExternalContributorSources as t};
12
- //# sourceMappingURL=router-builder-C6Qmb3rB.mjs.map
11
+ import{METADATA}from"./core/interfaces.mjs";import{o as duplicateRouteError,t as Container}from"./container-BmZciE3k.mjs";import{a as getMethodMeta,o as getMethodMetaOrUndefined,t as getClassMeta}from"./metadata-Cf6JPQax.mjs";import{i as MissingContributorError,n as DuplicateContributorError,t as ContributorCycleError}from"./context-errors-DDYA51Nf.mjs";const PRECEDENCE_ORDER=[`method`,`class`,`module`,`adapter`,`global`];function buildPipeline(sources,options={}){let route=options.route,bySource=new Map;for(let src of PRECEDENCE_ORDER)bySource.set(src,[]);for(let entry of sources)bySource.get(entry.source).push(entry);let winners=new Map;for(let src of PRECEDENCE_ORDER){let rows=bySource.get(src),keysAtThisLevel=new Map;for(let row of rows){let key=row.registration.key,existing=keysAtThisLevel.get(key)??[];existing.push(row),keysAtThisLevel.set(key,existing)}for(let[key,rowsForKey]of keysAtThisLevel){if(rowsForKey.length>1)throw new DuplicateContributorError(key,rowsForKey.map((r,i)=>r.label??`${src}#${i}`));winners.has(key)||winners.set(key,rowsForKey[0])}}let survivors=[...winners.values()].map(r=>r.registration),allKeys=new Set(survivors.map(r=>r.key));for(let reg of survivors)for(let dep of reg.dependsOn)if(!allKeys.has(dep))throw new MissingContributorError(dep,reg.key,route,reg.definedAt);let sorted=topoSort(survivors,route);return Object.freeze({contributors:Object.freeze(sorted),keys:allKeys})}function topoSort(registrations,route){let indegree=new Map,dependents=new Map,byKey=new Map;for(let reg of registrations)indegree.set(reg.key,reg.dependsOn.length),byKey.set(reg.key,reg);for(let reg of registrations)for(let dep of reg.dependsOn){let list=dependents.get(dep)??[];list.push(reg),dependents.set(dep,list)}let queue=[];for(let reg of registrations)indegree.get(reg.key)===0&&queue.push(reg);let sorted=[];for(;queue.length>0;){let node=queue.shift();sorted.push(node);for(let dependent of dependents.get(node.key)??[]){let next=(indegree.get(dependent.key)??0)-1;indegree.set(dependent.key,next),next===0&&queue.push(dependent)}}if(sorted.length!==registrations.length)throw new ContributorCycleError(reconstructCycle(registrations.filter(r=>(indegree.get(r.key)??0)>0),byKey),route);return sorted}function reconstructCycle(residual,byKey){let residualKeys=new Set(residual.map(r=>r.key)),start=residual[0].key,path=[start],seen=new Set([start]),cursor=start;for(;;){let nextDep=byKey.get(cursor).dependsOn.find(d=>residualKeys.has(d));if(nextDep===void 0)break;if(seen.has(nextDep)){let cycleStart=path.indexOf(nextDep);return[...path.slice(cycleStart),nextDep]}path.push(nextDep),seen.add(nextDep),cursor=nextDep}return path}async function runContributors(options){let{pipeline,ctx,container}=options;for(let reg of pipeline.contributors)await runOne(reg,ctx,container)}async function runOne(reg,ctx,container){let value;try{let deps=resolveDeps(reg,container);value=await reg.resolve(ctx,deps)}catch(err){if(reg.optional)return;if(reg.onError){let replacement=await reg.onError(err,ctx);if(replacement===void 0)return;ctx.set(reg.key,replacement);return}throw err}ctx.set(reg.key,value)}function resolveDeps(reg,container){let out={};for(let[key,token]of Object.entries(reg.deps))out[key]=container.resolve(token);return out}let _externalContributorSources=[];function _setExternalContributorSources(sources){_externalContributorSources=sources}function assertRouteUnique(registry,method,fullPath,owner){let key=`${method.toUpperCase()} ${fullPath.replace(/:[^/]+/g,`:*`)}`,prior=registry.get(key);if(prior!==void 0)throw duplicateRouteError(method.toUpperCase(),fullPath,owner,prior);registry.set(key,owner)}function buildRouteTable(controllerClass,options={}){let container=Container.getInstance(),externalSources=options.externalSources??_externalContributorSources,routes=getClassMeta(METADATA.ROUTES,controllerClass,[]),classMiddlewares=getClassMeta(METADATA.CLASS_MIDDLEWARES,controllerClass,[]),classContributors=getClassMeta(METADATA.CLASS_CONTRIBUTORS,controllerClass,[]),entries=[];for(let route of routes){let method=route.method.toUpperCase(),fullPath=route.path||`/`,methodMiddlewares=getMethodMeta(METADATA.METHOD_MIDDLEWARES,controllerClass,route.handlerName,[]),fileUploadConfig=getMethodMetaOrUndefined(METADATA.FILE_UPLOAD,controllerClass,route.handlerName),methodContributors=getMethodMeta(METADATA.METHOD_CONTRIBUTORS,controllerClass,route.handlerName,[]),contributorRunner=null;if(classContributors.length>0||methodContributors.length>0||externalSources.length>0){let pipeline=buildPipeline([...methodContributors.map((registration,i)=>({source:`method`,registration,label:`${controllerClass.name}.${String(route.handlerName)}#${i}(${registration.key})`})),...classContributors.map((registration,i)=>({source:`class`,registration,label:`${controllerClass.name}.@class#${i}(${registration.key})`})),...externalSources],{route:`${method} ${fullPath}`});contributorRunner=ctx=>runContributors({pipeline,ctx,container})}let handler=ctx=>container.resolve(controllerClass)[route.handlerName](ctx);entries.push({method,path:fullPath,middlewares:[...classMiddlewares,...methodMiddlewares],contributorRunner,handler,meta:{controller:controllerClass,handlerName:route.handlerName,validation:route.validation,upload:fileUploadConfig}})}return entries}export{buildPipeline as a,runContributors as i,assertRouteUnique as n,buildRouteTable as r,_setExternalContributorSources as t};
12
+ //# sourceMappingURL=router-builder-BjLsf6w0.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"router-builder-C6Qmb3rB.mjs","names":[],"sources":["../src/core/contributor-pipeline.ts","../src/core/contributor-runner.ts","../src/http/router-builder.ts"],"sourcesContent":["import type { AnyContributorRegistration } from './context-decorator'\nimport {\n ContributorCycleError,\n DuplicateContributorError,\n MissingContributorError,\n} from './context-errors'\n\n/**\n * Where a contributor was registered, ordered by precedence\n * (lower index wins). Mirrors `architecture.md` §20.4.\n *\n * Levels (highest → lowest precedence):\n * - `'method'` — `@LoadX` on a controller method.\n * - `'class'` — `@LoadX` on a controller class.\n * - `'module'` — returned by `AppModule.contributors?()`. Per-module scope.\n * - `'adapter'` — returned by `AppAdapter.contributors?()`. Cross-cutting, applies\n * to every module's routes.\n * - `'global'` — `ApplicationOptions.contributors`. App-wide default.\n */\nexport type ContributorSource = 'method' | 'class' | 'module' | 'adapter' | 'global'\n\n/** Precedence-ranked source list. Index = precedence; method (0) wins ties. */\nconst PRECEDENCE_ORDER: readonly ContributorSource[] = [\n 'method',\n 'class',\n 'module',\n 'adapter',\n 'global',\n]\n\n/**\n * Input row to {@link buildPipeline}. Pairs a registration with its origin\n * so dedup can apply the precedence rule and diagnostic errors can name\n * the offending site.\n *\n * `registration` is the type-erased {@link AnyContributorRegistration}\n * — the runner doesn't care about each registration's narrow `D` /\n * `Ctx` shape, and adopters frequently mix HTTP / transport-agnostic\n * contributors in the same `externalSources` / `contributors()`\n * array. The narrow generics from `defineContextDecorator` survive\n * at the *definition* site (where `resolve` is typed); they're erased\n * here so collections compose without `as any` casts.\n */\nexport interface SourcedRegistration {\n registration: AnyContributorRegistration\n source: ContributorSource\n /** Human label surfaced in DuplicateContributorError messages. */\n label?: string\n}\n\n/**\n * Resolved, validated, topo-sorted pipeline ready for execution.\n *\n * `contributors` is in execution order — every entry's `dependsOn`\n * keys appear earlier. `keys` is the set of all keys this pipeline\n * produces; the runner uses it for fast existence checks and the\n * builder uses it for missing-dep validation.\n */\nexport interface ContributorPipeline {\n readonly contributors: readonly AnyContributorRegistration[]\n readonly keys: ReadonlySet<string>\n}\n\nexport interface BuildPipelineOptions {\n /** Optional route identifier surfaced in diagnostic errors (e.g., 'GET /projects/:id'). */\n route?: string\n}\n\n/**\n * Build, validate, and topo-sort a contributor pipeline.\n *\n * Pipeline:\n *\n * 1. Group sources by precedence (method > class > module > adapter > global).\n * 2. Within each precedence level, two contributors with the same `key`\n * raise {@link DuplicateContributorError}.\n * 3. Across levels, the highest-precedence contributor wins each key\n * (lower-precedence duplicates are silently dropped — that is the\n * documented override mechanism, not an error).\n * 4. Every key referenced by `dependsOn` must be produced by the\n * surviving set, otherwise {@link MissingContributorError}.\n * 5. Kahn's algorithm topo-sorts the survivors. Any nodes left after\n * the queue drains form a cycle → {@link ContributorCycleError}\n * with a reconstructed path.\n *\n * Pure function — no DI, no I/O. Safe to call once at startup per\n * route and cache the result.\n */\nexport function buildPipeline(\n sources: readonly SourcedRegistration[],\n options: BuildPipelineOptions = {},\n): ContributorPipeline {\n const route = options.route\n\n // Step 1: group by source.\n const bySource = new Map<ContributorSource, SourcedRegistration[]>()\n for (const src of PRECEDENCE_ORDER) bySource.set(src, [])\n for (const entry of sources) bySource.get(entry.source)!.push(entry)\n\n // Step 2 + 3: dedup with precedence. We walk highest precedence first,\n // claim its keys, and drop lower-precedence rows that conflict.\n const winners = new Map<string, SourcedRegistration>()\n for (const src of PRECEDENCE_ORDER) {\n const rows = bySource.get(src)!\n const keysAtThisLevel = new Map<string, SourcedRegistration[]>()\n for (const row of rows) {\n const key = row.registration.key\n const existing = keysAtThisLevel.get(key) ?? []\n existing.push(row)\n keysAtThisLevel.set(key, existing)\n }\n for (const [key, rowsForKey] of keysAtThisLevel) {\n if (rowsForKey.length > 1) {\n const labels = rowsForKey.map((r, i) => r.label ?? `${src}#${i}`)\n throw new DuplicateContributorError(key, labels)\n }\n if (!winners.has(key)) winners.set(key, rowsForKey[0])\n }\n }\n\n const survivors = [...winners.values()].map((r) => r.registration)\n const allKeys = new Set(survivors.map((r) => r.key))\n\n // Step 4: dependsOn validation.\n for (const reg of survivors) {\n for (const dep of reg.dependsOn) {\n if (!allKeys.has(dep)) {\n throw new MissingContributorError(dep, reg.key, route, reg.definedAt)\n }\n }\n }\n\n // Step 5: Kahn topo-sort.\n const sorted = topoSort(survivors, route)\n\n return Object.freeze({\n contributors: Object.freeze(sorted),\n keys: allKeys,\n })\n}\n\n/**\n * Kahn's algorithm with deterministic ordering on ties.\n *\n * Nodes with no remaining incoming edges enter the queue in input\n * order. Cycles surface as nodes still present after the queue\n * drains; we reconstruct one cycle path for diagnostics.\n */\nfunction topoSort(\n registrations: readonly AnyContributorRegistration[],\n route: string | undefined,\n): AnyContributorRegistration[] {\n // indegree[key] = number of unresolved dependsOn entries for key\n const indegree = new Map<string, number>()\n // dependents[depKey] = list of registrations that depend on depKey\n const dependents = new Map<string, AnyContributorRegistration[]>()\n const byKey = new Map<string, AnyContributorRegistration>()\n\n for (const reg of registrations) {\n indegree.set(reg.key, reg.dependsOn.length)\n byKey.set(reg.key, reg)\n }\n for (const reg of registrations) {\n for (const dep of reg.dependsOn) {\n const list = dependents.get(dep) ?? []\n list.push(reg)\n dependents.set(dep, list)\n }\n }\n\n // Initial queue: registrations with zero indegree, preserving input order.\n const queue: AnyContributorRegistration[] = []\n for (const reg of registrations) {\n if (indegree.get(reg.key) === 0) queue.push(reg)\n }\n\n const sorted: AnyContributorRegistration[] = []\n while (queue.length > 0) {\n const node = queue.shift()!\n sorted.push(node)\n for (const dependent of dependents.get(node.key) ?? []) {\n const next = (indegree.get(dependent.key) ?? 0) - 1\n indegree.set(dependent.key, next)\n if (next === 0) queue.push(dependent)\n }\n }\n\n if (sorted.length !== registrations.length) {\n const remaining = registrations.filter((r) => (indegree.get(r.key) ?? 0) > 0)\n throw new ContributorCycleError(reconstructCycle(remaining, byKey), route)\n }\n\n return sorted\n}\n\n/**\n * Reconstruct one cycle path from the residual graph.\n *\n * Walks from any remaining node, following its first dependency that's\n * still in the residual set, until we revisit a node. The path returned\n * starts and ends with the same key so the message reads naturally:\n * `tenant → project → tenant`.\n */\nfunction reconstructCycle(\n residual: readonly AnyContributorRegistration[],\n byKey: ReadonlyMap<string, AnyContributorRegistration>,\n): string[] {\n const residualKeys = new Set(residual.map((r) => r.key))\n const start = residual[0].key\n const path: string[] = [start]\n const seen = new Set<string>([start])\n let cursor: string = start\n\n while (true) {\n const reg = byKey.get(cursor)!\n const nextDep = reg.dependsOn.find((d) => residualKeys.has(d))\n if (nextDep === undefined) break\n if (seen.has(nextDep)) {\n // Trim the prefix that isn't part of the cycle.\n const cycleStart = path.indexOf(nextDep)\n return [...path.slice(cycleStart), nextDep]\n }\n path.push(nextDep)\n seen.add(nextDep)\n cursor = nextDep\n }\n\n // Fallback — couldn't reconstruct a strict cycle, return what we walked.\n return path\n}\n","import type { Container } from './container'\nimport type { ContributorRegistration } from './context-decorator'\nimport type { ExecutionContext } from './execution-context'\nimport type { ContributorPipeline } from './contributor-pipeline'\n\nexport interface RunContributorsOptions {\n /** Pre-built, validated, topo-sorted pipeline. */\n pipeline: ContributorPipeline\n /** The execution context contributors read from and write into. */\n ctx: ExecutionContext\n /** DI container used to resolve declared `deps`. */\n container: Container\n}\n\n/**\n * Execute a built {@link ContributorPipeline} against an {@link ExecutionContext}.\n *\n * Sequential — one contributor at a time, in the topo order chosen at\n * pipeline-build time. Per `architecture.md` §20.10, V1 does not run\n * contributors in parallel even within the same topo level; that is a\n * V2 follow-up gated on profiling.\n *\n * ## Error matrix (architecture.md §20.9)\n *\n * | resolve outcome | optional | onError defined | runner behaviour |\n * |-----------------|----------|-----------------|--------------------------------------------------|\n * | throws | true | — | skip; `ctx.get(key)` remains undefined |\n * | throws | false | yes | call hook; returned value (if any) stored |\n * | throws | false | no | propagate the original error |\n * | hook throws | — | yes | propagate the new error |\n * | hook returns | — | yes | undefined/void → skip; value → `ctx.set(key, v)` |\n * | resolves | — | — | `ctx.set(key, value)` |\n *\n * Container.resolve() throwing while building `deps` is treated as a\n * resolve-side throw and flows through the same matrix — adopters who\n * need different behaviour for missing-DI vs. data-lookup failures can\n * wrap the container access inside their own `resolve()`.\n */\nexport async function runContributors(options: RunContributorsOptions): Promise<void> {\n const { pipeline, ctx, container } = options\n\n for (const reg of pipeline.contributors) {\n await runOne(reg, ctx, container)\n }\n}\n\nasync function runOne(\n reg: ContributorRegistration,\n ctx: ExecutionContext,\n container: Container,\n): Promise<void> {\n let value: unknown\n try {\n const deps = resolveDeps(reg, container)\n value = await reg.resolve(ctx, deps)\n } catch (err) {\n if (reg.optional) return\n if (reg.onError) {\n // Hook throws are unconditionally propagated — there is no\n // second-chance hook. Returning undefined/void from the hook\n // means \"skip\"; returning a value means \"use this instead\".\n const replacement = await reg.onError(err, ctx)\n if (replacement === undefined) return\n ctx.set(reg.key, replacement as never)\n return\n }\n throw err\n }\n ctx.set(reg.key, value as never)\n}\n\n/**\n * Resolve a contributor's declared dependencies through the container,\n * preserving the property names from the spec so `resolve(ctx, deps)`\n * receives exactly the shape the user wrote.\n *\n * Empty `deps` produces an empty object — `resolve()` always sees an\n * object, never `undefined`.\n */\nfunction resolveDeps(reg: ContributorRegistration, container: Container): Record<string, unknown> {\n const out: Record<string, unknown> = {}\n for (const [key, token] of Object.entries(reg.deps)) {\n out[key] = container.resolve(token as never)\n }\n return out\n}\n","// Concrete-module imports (NOT the `../core` barrel): the barrel re-exports\n// the asset manager, whose eager `node:fs` import would poison the edge-safe\n// `@forinda/kickjs/web` entry graph that flows through this file.\nimport { buildPipeline, type SourcedRegistration } from '../core/contributor-pipeline'\nimport { runContributors } from '../core/contributor-runner'\nimport { Container } from '../core/container'\nimport { METADATA } from '../core/interfaces'\nimport type { ContributorRegistration } from '../core/context-decorator'\nimport type { FileUploadConfig, MiddlewareHandler, RouteDefinition } from '../core/decorators'\nimport { getClassMeta, getMethodMeta, getMethodMetaOrUndefined } from '../core/metadata'\nimport { duplicateRouteError } from '../core/kick-errors'\nimport type { CtxHandler, RouteEntry, RouteMethod } from './runtime'\n\n/**\n * Per-module SourcedRegistration[] threaded through the route-mount loop by\n * Application.setup(). Carries module + adapter + global contributors so\n * buildRouteTable() can merge them with class + method ones into a single pipeline.\n *\n * Module setup is sequential, so this slot is race-free. Cleared in a finally\n * block after each module mounts. Outside of Application setup the slot is\n * empty — direct buildRouteTable()/buildRoutes() callers (mostly tests) see only\n * class + method contributors unless they pass `externalSources` explicitly.\n *\n * Same idiom as `Container._requestStoreProvider`: an internal escape hatch for\n * cross-module wiring without inversion-of-control gymnastics.\n *\n * @internal\n */\nlet _externalContributorSources: readonly SourcedRegistration[] = []\n\n/** @internal — set by Application.setup() before each `mod.routes()` call. */\nexport function _setExternalContributorSources(sources: readonly SourcedRegistration[]): void {\n _externalContributorSources = sources\n}\n\n/**\n * Boot-time duplicate-route guard shared by the node and web mount loops.\n *\n * Registers `METHOD /mounted/path` into `registry` and throws\n * {@link duplicateRouteError} (KICK006) if another handler already claimed\n * it. Param *names* are ignored — `/tasks/:id` and `/tasks/:taskId` are the\n * same route at dispatch time, so they collide here too.\n *\n * @internal\n */\nexport function assertRouteUnique(\n registry: Map<string, string>,\n method: string,\n fullPath: string,\n owner: string,\n): void {\n const key = `${method.toUpperCase()} ${fullPath.replace(/:[^/]+/g, ':*')}`\n const prior = registry.get(key)\n if (prior !== undefined) {\n throw duplicateRouteError(method.toUpperCase(), fullPath, owner, prior)\n }\n registry.set(key, owner)\n}\n\nexport interface BuildRoutesOptions {\n /**\n * Extra contributors to merge into the per-route pipeline at their declared\n * precedence levels. Pass explicitly when calling buildRouteTable/buildRoutes\n * outside the Application route-mount loop (typically in tests). When omitted,\n * falls back to the slot set by Application.setup().\n */\n externalSources?: readonly SourcedRegistration[]\n}\n\n/**\n * Turn a controller class decorated with @Get, @Post, etc. into a plain-data\n * {@link RouteEntry}[] — the engine-neutral route table an {@link HttpRuntime}\n * materializes (see `docs/http/spec-http-runtimes.md`, Avenue B).\n *\n * What used to be per-handler Express `(req, res, next)` closures is now\n * captured as data: `middlewares` keep their `(ctx, next)` shape, the contributor\n * pipeline is pre-built into a `contributorRunner` closure, validation / upload\n * stay as metadata, and the terminal `handler` resolves the controller per-request\n * (to respect DI scoping) and invokes it. The runtime owns how these get wrapped\n * onto its engine.\n *\n * Routes use only the method-level decorator paths (e.g. @Get('/me') → '/me').\n * The @Controller path is NOT baked in — it is metadata for Swagger/introspection.\n * The module's routes().path is the single source of truth for the mount prefix,\n * which avoids path doubling when both the module and controller specify the path.\n *\n * The contributor pipeline is built here, so a cycle or missing `dependsOn`\n * throws at table-build time (boot) rather than on first request.\n */\nexport function buildRouteTable(\n controllerClass: any,\n options: BuildRoutesOptions = {},\n): RouteEntry[] {\n const container = Container.getInstance()\n const externalSources = options.externalSources ?? _externalContributorSources\n const routes: RouteDefinition[] = getClassMeta<RouteDefinition[]>(\n METADATA.ROUTES,\n controllerClass,\n [],\n )\n\n // Class-level middleware\n const classMiddlewares: MiddlewareHandler[] = getClassMeta<MiddlewareHandler[]>(\n METADATA.CLASS_MIDDLEWARES,\n controllerClass,\n [],\n )\n\n // Class-level Context Contributors (#107) — applied to every method on the\n // controller. Method-level contributors are added per-route below.\n const classContributors: ContributorRegistration[] = getClassMeta<ContributorRegistration[]>(\n METADATA.CLASS_CONTRIBUTORS,\n controllerClass,\n [],\n )\n\n const entries: RouteEntry[] = []\n\n for (const route of routes) {\n const method = route.method.toUpperCase() as RouteMethod\n const fullPath = route.path || '/'\n\n // Method-level middleware\n const methodMiddlewares: MiddlewareHandler[] = getMethodMeta<MiddlewareHandler[]>(\n METADATA.METHOD_MIDDLEWARES,\n controllerClass,\n route.handlerName,\n [],\n )\n\n // @FileUpload decorator — runtime supplies the upload backend.\n const fileUploadConfig = getMethodMetaOrUndefined<FileUploadConfig>(\n METADATA.FILE_UPLOAD,\n controllerClass,\n route.handlerName,\n )\n\n // Context Contributor pipeline (#107) — class + method contributors,\n // built once at mount time so cycle/missing-dep failures abort boot.\n const methodContributors: ContributorRegistration[] = getMethodMeta<ContributorRegistration[]>(\n METADATA.METHOD_CONTRIBUTORS,\n controllerClass,\n route.handlerName,\n [],\n )\n\n let contributorRunner: CtxHandler | null = null\n if (\n classContributors.length > 0 ||\n methodContributors.length > 0 ||\n externalSources.length > 0\n ) {\n // Labels are surfaced in DuplicateContributorError messages — include\n // the registration's key + the original index so a same-key collision\n // at one precedence level points at the conflicting decorator slot,\n // not just the host method/class.\n const sources: SourcedRegistration[] = [\n ...methodContributors.map(\n (registration, i): SourcedRegistration => ({\n source: 'method',\n registration,\n label: `${controllerClass.name}.${String(route.handlerName)}#${i}(${registration.key})`,\n }),\n ),\n ...classContributors.map(\n (registration, i): SourcedRegistration => ({\n source: 'class',\n registration,\n label: `${controllerClass.name}.@class#${i}(${registration.key})`,\n }),\n ),\n ...externalSources,\n ]\n const pipeline = buildPipeline(sources, {\n route: `${method} ${fullPath}`,\n })\n contributorRunner = (ctx) => runContributors({ pipeline, ctx, container })\n }\n\n // Terminal handler — resolve controller per-request to respect DI scoping.\n const handler: CtxHandler = (ctx) => container.resolve(controllerClass)[route.handlerName](ctx)\n\n entries.push({\n method,\n path: fullPath,\n middlewares: [...classMiddlewares, ...methodMiddlewares],\n contributorRunner,\n handler,\n meta: {\n controller: controllerClass,\n handlerName: route.handlerName,\n validation: route.validation,\n upload: fileUploadConfig,\n },\n })\n }\n\n return entries\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"router-builder-BjLsf6w0.mjs","names":[],"sources":["../src/core/contributor-pipeline.ts","../src/core/contributor-runner.ts","../src/http/router-builder.ts"],"sourcesContent":["import type { AnyContributorRegistration } from './context-decorator'\nimport {\n ContributorCycleError,\n DuplicateContributorError,\n MissingContributorError,\n} from './context-errors'\n\n/**\n * Where a contributor was registered, ordered by precedence\n * (lower index wins). Mirrors `architecture.md` §20.4.\n *\n * Levels (highest → lowest precedence):\n * - `'method'` — `@LoadX` on a controller method.\n * - `'class'` — `@LoadX` on a controller class.\n * - `'module'` — returned by `AppModule.contributors?()`. Per-module scope.\n * - `'adapter'` — returned by `AppAdapter.contributors?()`. Cross-cutting, applies\n * to every module's routes.\n * - `'global'` — `ApplicationOptions.contributors`. App-wide default.\n */\nexport type ContributorSource = 'method' | 'class' | 'module' | 'adapter' | 'global'\n\n/** Precedence-ranked source list. Index = precedence; method (0) wins ties. */\nconst PRECEDENCE_ORDER: readonly ContributorSource[] = [\n 'method',\n 'class',\n 'module',\n 'adapter',\n 'global',\n]\n\n/**\n * Input row to {@link buildPipeline}. Pairs a registration with its origin\n * so dedup can apply the precedence rule and diagnostic errors can name\n * the offending site.\n *\n * `registration` is the type-erased {@link AnyContributorRegistration}\n * — the runner doesn't care about each registration's narrow `D` /\n * `Ctx` shape, and adopters frequently mix HTTP / transport-agnostic\n * contributors in the same `externalSources` / `contributors()`\n * array. The narrow generics from `defineContextDecorator` survive\n * at the *definition* site (where `resolve` is typed); they're erased\n * here so collections compose without `as any` casts.\n */\nexport interface SourcedRegistration {\n registration: AnyContributorRegistration\n source: ContributorSource\n /** Human label surfaced in DuplicateContributorError messages. */\n label?: string\n}\n\n/**\n * Resolved, validated, topo-sorted pipeline ready for execution.\n *\n * `contributors` is in execution order — every entry's `dependsOn`\n * keys appear earlier. `keys` is the set of all keys this pipeline\n * produces; the runner uses it for fast existence checks and the\n * builder uses it for missing-dep validation.\n */\nexport interface ContributorPipeline {\n readonly contributors: readonly AnyContributorRegistration[]\n readonly keys: ReadonlySet<string>\n}\n\nexport interface BuildPipelineOptions {\n /** Optional route identifier surfaced in diagnostic errors (e.g., 'GET /projects/:id'). */\n route?: string\n}\n\n/**\n * Build, validate, and topo-sort a contributor pipeline.\n *\n * Pipeline:\n *\n * 1. Group sources by precedence (method > class > module > adapter > global).\n * 2. Within each precedence level, two contributors with the same `key`\n * raise {@link DuplicateContributorError}.\n * 3. Across levels, the highest-precedence contributor wins each key\n * (lower-precedence duplicates are silently dropped — that is the\n * documented override mechanism, not an error).\n * 4. Every key referenced by `dependsOn` must be produced by the\n * surviving set, otherwise {@link MissingContributorError}.\n * 5. Kahn's algorithm topo-sorts the survivors. Any nodes left after\n * the queue drains form a cycle → {@link ContributorCycleError}\n * with a reconstructed path.\n *\n * Pure function — no DI, no I/O. Safe to call once at startup per\n * route and cache the result.\n */\nexport function buildPipeline(\n sources: readonly SourcedRegistration[],\n options: BuildPipelineOptions = {},\n): ContributorPipeline {\n const route = options.route\n\n // Step 1: group by source.\n const bySource = new Map<ContributorSource, SourcedRegistration[]>()\n for (const src of PRECEDENCE_ORDER) bySource.set(src, [])\n for (const entry of sources) bySource.get(entry.source)!.push(entry)\n\n // Step 2 + 3: dedup with precedence. We walk highest precedence first,\n // claim its keys, and drop lower-precedence rows that conflict.\n const winners = new Map<string, SourcedRegistration>()\n for (const src of PRECEDENCE_ORDER) {\n const rows = bySource.get(src)!\n const keysAtThisLevel = new Map<string, SourcedRegistration[]>()\n for (const row of rows) {\n const key = row.registration.key\n const existing = keysAtThisLevel.get(key) ?? []\n existing.push(row)\n keysAtThisLevel.set(key, existing)\n }\n for (const [key, rowsForKey] of keysAtThisLevel) {\n if (rowsForKey.length > 1) {\n const labels = rowsForKey.map((r, i) => r.label ?? `${src}#${i}`)\n throw new DuplicateContributorError(key, labels)\n }\n if (!winners.has(key)) winners.set(key, rowsForKey[0])\n }\n }\n\n const survivors = [...winners.values()].map((r) => r.registration)\n const allKeys = new Set(survivors.map((r) => r.key))\n\n // Step 4: dependsOn validation.\n for (const reg of survivors) {\n for (const dep of reg.dependsOn) {\n if (!allKeys.has(dep)) {\n throw new MissingContributorError(dep, reg.key, route, reg.definedAt)\n }\n }\n }\n\n // Step 5: Kahn topo-sort.\n const sorted = topoSort(survivors, route)\n\n return Object.freeze({\n contributors: Object.freeze(sorted),\n keys: allKeys,\n })\n}\n\n/**\n * Kahn's algorithm with deterministic ordering on ties.\n *\n * Nodes with no remaining incoming edges enter the queue in input\n * order. Cycles surface as nodes still present after the queue\n * drains; we reconstruct one cycle path for diagnostics.\n */\nfunction topoSort(\n registrations: readonly AnyContributorRegistration[],\n route: string | undefined,\n): AnyContributorRegistration[] {\n // indegree[key] = number of unresolved dependsOn entries for key\n const indegree = new Map<string, number>()\n // dependents[depKey] = list of registrations that depend on depKey\n const dependents = new Map<string, AnyContributorRegistration[]>()\n const byKey = new Map<string, AnyContributorRegistration>()\n\n for (const reg of registrations) {\n indegree.set(reg.key, reg.dependsOn.length)\n byKey.set(reg.key, reg)\n }\n for (const reg of registrations) {\n for (const dep of reg.dependsOn) {\n const list = dependents.get(dep) ?? []\n list.push(reg)\n dependents.set(dep, list)\n }\n }\n\n // Initial queue: registrations with zero indegree, preserving input order.\n const queue: AnyContributorRegistration[] = []\n for (const reg of registrations) {\n if (indegree.get(reg.key) === 0) queue.push(reg)\n }\n\n const sorted: AnyContributorRegistration[] = []\n while (queue.length > 0) {\n const node = queue.shift()!\n sorted.push(node)\n for (const dependent of dependents.get(node.key) ?? []) {\n const next = (indegree.get(dependent.key) ?? 0) - 1\n indegree.set(dependent.key, next)\n if (next === 0) queue.push(dependent)\n }\n }\n\n if (sorted.length !== registrations.length) {\n const remaining = registrations.filter((r) => (indegree.get(r.key) ?? 0) > 0)\n throw new ContributorCycleError(reconstructCycle(remaining, byKey), route)\n }\n\n return sorted\n}\n\n/**\n * Reconstruct one cycle path from the residual graph.\n *\n * Walks from any remaining node, following its first dependency that's\n * still in the residual set, until we revisit a node. The path returned\n * starts and ends with the same key so the message reads naturally:\n * `tenant → project → tenant`.\n */\nfunction reconstructCycle(\n residual: readonly AnyContributorRegistration[],\n byKey: ReadonlyMap<string, AnyContributorRegistration>,\n): string[] {\n const residualKeys = new Set(residual.map((r) => r.key))\n const start = residual[0].key\n const path: string[] = [start]\n const seen = new Set<string>([start])\n let cursor: string = start\n\n while (true) {\n const reg = byKey.get(cursor)!\n const nextDep = reg.dependsOn.find((d) => residualKeys.has(d))\n if (nextDep === undefined) break\n if (seen.has(nextDep)) {\n // Trim the prefix that isn't part of the cycle.\n const cycleStart = path.indexOf(nextDep)\n return [...path.slice(cycleStart), nextDep]\n }\n path.push(nextDep)\n seen.add(nextDep)\n cursor = nextDep\n }\n\n // Fallback — couldn't reconstruct a strict cycle, return what we walked.\n return path\n}\n","import type { Container } from './container'\nimport type { ContributorRegistration } from './context-decorator'\nimport type { ExecutionContext } from './execution-context'\nimport type { ContributorPipeline } from './contributor-pipeline'\n\nexport interface RunContributorsOptions {\n /** Pre-built, validated, topo-sorted pipeline. */\n pipeline: ContributorPipeline\n /** The execution context contributors read from and write into. */\n ctx: ExecutionContext\n /** DI container used to resolve declared `deps`. */\n container: Container\n}\n\n/**\n * Execute a built {@link ContributorPipeline} against an {@link ExecutionContext}.\n *\n * Sequential — one contributor at a time, in the topo order chosen at\n * pipeline-build time. Per `architecture.md` §20.10, V1 does not run\n * contributors in parallel even within the same topo level; that is a\n * V2 follow-up gated on profiling.\n *\n * ## Error matrix (architecture.md §20.9)\n *\n * | resolve outcome | optional | onError defined | runner behaviour |\n * |-----------------|----------|-----------------|--------------------------------------------------|\n * | throws | true | — | skip; `ctx.get(key)` remains undefined |\n * | throws | false | yes | call hook; returned value (if any) stored |\n * | throws | false | no | propagate the original error |\n * | hook throws | — | yes | propagate the new error |\n * | hook returns | — | yes | undefined/void → skip; value → `ctx.set(key, v)` |\n * | resolves | — | — | `ctx.set(key, value)` |\n *\n * Container.resolve() throwing while building `deps` is treated as a\n * resolve-side throw and flows through the same matrix — adopters who\n * need different behaviour for missing-DI vs. data-lookup failures can\n * wrap the container access inside their own `resolve()`.\n */\nexport async function runContributors(options: RunContributorsOptions): Promise<void> {\n const { pipeline, ctx, container } = options\n\n for (const reg of pipeline.contributors) {\n await runOne(reg, ctx, container)\n }\n}\n\nasync function runOne(\n reg: ContributorRegistration,\n ctx: ExecutionContext,\n container: Container,\n): Promise<void> {\n let value: unknown\n try {\n const deps = resolveDeps(reg, container)\n value = await reg.resolve(ctx, deps)\n } catch (err) {\n if (reg.optional) return\n if (reg.onError) {\n // Hook throws are unconditionally propagated — there is no\n // second-chance hook. Returning undefined/void from the hook\n // means \"skip\"; returning a value means \"use this instead\".\n const replacement = await reg.onError(err, ctx)\n if (replacement === undefined) return\n ctx.set(reg.key, replacement as never)\n return\n }\n throw err\n }\n ctx.set(reg.key, value as never)\n}\n\n/**\n * Resolve a contributor's declared dependencies through the container,\n * preserving the property names from the spec so `resolve(ctx, deps)`\n * receives exactly the shape the user wrote.\n *\n * Empty `deps` produces an empty object — `resolve()` always sees an\n * object, never `undefined`.\n */\nfunction resolveDeps(reg: ContributorRegistration, container: Container): Record<string, unknown> {\n const out: Record<string, unknown> = {}\n for (const [key, token] of Object.entries(reg.deps)) {\n out[key] = container.resolve(token as never)\n }\n return out\n}\n","// Concrete-module imports (NOT the `../core` barrel): the barrel re-exports\n// the asset manager, whose eager `node:fs` import would poison the edge-safe\n// `@forinda/kickjs/web` entry graph that flows through this file.\nimport { buildPipeline, type SourcedRegistration } from '../core/contributor-pipeline'\nimport { runContributors } from '../core/contributor-runner'\nimport { Container } from '../core/container'\nimport { METADATA } from '../core/interfaces'\nimport type { ContributorRegistration } from '../core/context-decorator'\nimport type { FileUploadConfig, MiddlewareHandler, RouteDefinition } from '../core/decorators'\nimport { getClassMeta, getMethodMeta, getMethodMetaOrUndefined } from '../core/metadata'\nimport { duplicateRouteError } from '../core/kick-errors'\nimport type { CtxHandler, RouteEntry, RouteMethod } from './runtime'\n\n/**\n * Per-module SourcedRegistration[] threaded through the route-mount loop by\n * Application.setup(). Carries module + adapter + global contributors so\n * buildRouteTable() can merge them with class + method ones into a single pipeline.\n *\n * Module setup is sequential, so this slot is race-free. Cleared in a finally\n * block after each module mounts. Outside of Application setup the slot is\n * empty — direct buildRouteTable()/buildRoutes() callers (mostly tests) see only\n * class + method contributors unless they pass `externalSources` explicitly.\n *\n * Same idiom as `Container._requestStoreProvider`: an internal escape hatch for\n * cross-module wiring without inversion-of-control gymnastics.\n *\n * @internal\n */\nlet _externalContributorSources: readonly SourcedRegistration[] = []\n\n/** @internal — set by Application.setup() before each `mod.routes()` call. */\nexport function _setExternalContributorSources(sources: readonly SourcedRegistration[]): void {\n _externalContributorSources = sources\n}\n\n/**\n * Boot-time duplicate-route guard shared by the node and web mount loops.\n *\n * Registers `METHOD /mounted/path` into `registry` and throws\n * {@link duplicateRouteError} (KICK006) if another handler already claimed\n * it. Param *names* are ignored — `/tasks/:id` and `/tasks/:taskId` are the\n * same route at dispatch time, so they collide here too.\n *\n * @internal\n */\nexport function assertRouteUnique(\n registry: Map<string, string>,\n method: string,\n fullPath: string,\n owner: string,\n): void {\n const key = `${method.toUpperCase()} ${fullPath.replace(/:[^/]+/g, ':*')}`\n const prior = registry.get(key)\n if (prior !== undefined) {\n throw duplicateRouteError(method.toUpperCase(), fullPath, owner, prior)\n }\n registry.set(key, owner)\n}\n\nexport interface BuildRoutesOptions {\n /**\n * Extra contributors to merge into the per-route pipeline at their declared\n * precedence levels. Pass explicitly when calling buildRouteTable/buildRoutes\n * outside the Application route-mount loop (typically in tests). When omitted,\n * falls back to the slot set by Application.setup().\n */\n externalSources?: readonly SourcedRegistration[]\n}\n\n/**\n * Turn a controller class decorated with @Get, @Post, etc. into a plain-data\n * {@link RouteEntry}[] — the engine-neutral route table an {@link HttpRuntime}\n * materializes (see `docs/http/spec-http-runtimes.md`, Avenue B).\n *\n * What used to be per-handler Express `(req, res, next)` closures is now\n * captured as data: `middlewares` keep their `(ctx, next)` shape, the contributor\n * pipeline is pre-built into a `contributorRunner` closure, validation / upload\n * stay as metadata, and the terminal `handler` resolves the controller per-request\n * (to respect DI scoping) and invokes it. The runtime owns how these get wrapped\n * onto its engine.\n *\n * Routes use only the method-level decorator paths (e.g. @Get('/me') → '/me').\n * The @Controller path is NOT baked in — it is metadata for Swagger/introspection.\n * The module's routes().path is the single source of truth for the mount prefix,\n * which avoids path doubling when both the module and controller specify the path.\n *\n * The contributor pipeline is built here, so a cycle or missing `dependsOn`\n * throws at table-build time (boot) rather than on first request.\n */\nexport function buildRouteTable(\n controllerClass: any,\n options: BuildRoutesOptions = {},\n): RouteEntry[] {\n const container = Container.getInstance()\n const externalSources = options.externalSources ?? _externalContributorSources\n const routes: RouteDefinition[] = getClassMeta<RouteDefinition[]>(\n METADATA.ROUTES,\n controllerClass,\n [],\n )\n\n // Class-level middleware\n const classMiddlewares: MiddlewareHandler[] = getClassMeta<MiddlewareHandler[]>(\n METADATA.CLASS_MIDDLEWARES,\n controllerClass,\n [],\n )\n\n // Class-level Context Contributors (#107) — applied to every method on the\n // controller. Method-level contributors are added per-route below.\n const classContributors: ContributorRegistration[] = getClassMeta<ContributorRegistration[]>(\n METADATA.CLASS_CONTRIBUTORS,\n controllerClass,\n [],\n )\n\n const entries: RouteEntry[] = []\n\n for (const route of routes) {\n const method = route.method.toUpperCase() as RouteMethod\n const fullPath = route.path || '/'\n\n // Method-level middleware\n const methodMiddlewares: MiddlewareHandler[] = getMethodMeta<MiddlewareHandler[]>(\n METADATA.METHOD_MIDDLEWARES,\n controllerClass,\n route.handlerName,\n [],\n )\n\n // @FileUpload decorator — runtime supplies the upload backend.\n const fileUploadConfig = getMethodMetaOrUndefined<FileUploadConfig>(\n METADATA.FILE_UPLOAD,\n controllerClass,\n route.handlerName,\n )\n\n // Context Contributor pipeline (#107) — class + method contributors,\n // built once at mount time so cycle/missing-dep failures abort boot.\n const methodContributors: ContributorRegistration[] = getMethodMeta<ContributorRegistration[]>(\n METADATA.METHOD_CONTRIBUTORS,\n controllerClass,\n route.handlerName,\n [],\n )\n\n let contributorRunner: CtxHandler | null = null\n if (\n classContributors.length > 0 ||\n methodContributors.length > 0 ||\n externalSources.length > 0\n ) {\n // Labels are surfaced in DuplicateContributorError messages — include\n // the registration's key + the original index so a same-key collision\n // at one precedence level points at the conflicting decorator slot,\n // not just the host method/class.\n const sources: SourcedRegistration[] = [\n ...methodContributors.map(\n (registration, i): SourcedRegistration => ({\n source: 'method',\n registration,\n label: `${controllerClass.name}.${String(route.handlerName)}#${i}(${registration.key})`,\n }),\n ),\n ...classContributors.map(\n (registration, i): SourcedRegistration => ({\n source: 'class',\n registration,\n label: `${controllerClass.name}.@class#${i}(${registration.key})`,\n }),\n ),\n ...externalSources,\n ]\n const pipeline = buildPipeline(sources, {\n route: `${method} ${fullPath}`,\n })\n contributorRunner = (ctx) => runContributors({ pipeline, ctx, container })\n }\n\n // Terminal handler — resolve controller per-request to respect DI scoping.\n const handler: CtxHandler = (ctx) => container.resolve(controllerClass)[route.handlerName](ctx)\n\n entries.push({\n method,\n path: fullPath,\n middlewares: [...classMiddlewares, ...methodMiddlewares],\n contributorRunner,\n handler,\n meta: {\n controller: controllerClass,\n handlerName: route.handlerName,\n validation: route.validation,\n upload: fileUploadConfig,\n },\n })\n }\n\n return entries\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
 
2
- import { L as SourcedRegistration, m as RouteEntry } from "./adapter-CbMq-NBf.mjs";
2
+ import { L as SourcedRegistration, m as RouteEntry } from "./adapter-diFqgxGR.mjs";
3
3
  //#region src/http/router-builder.d.ts
4
4
  /** @internal — set by Application.setup() before each `mod.routes()` call. */
5
5
  declare function _setExternalContributorSources(sources: readonly SourcedRegistration[]): void;
@@ -46,4 +46,4 @@ interface BuildRoutesOptions {
46
46
  declare function buildRouteTable(controllerClass: any, options?: BuildRoutesOptions): RouteEntry[];
47
47
  //#endregion
48
48
  export { buildRouteTable as i, _setExternalContributorSources as n, assertRouteUnique as r, BuildRoutesOptions as t };
49
- //# sourceMappingURL=router-builder-CMGhEBU2.d.mts.map
49
+ //# sourceMappingURL=router-builder-C6E11Grf.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"router-builder-CMGhEBU2.d.mts","names":[],"sources":["../src/http/router-builder.ts"],"mappings":";;;;iBA+BgB,+BAA+B,kBAAkB;;;;;;;;;;;iBAcjD,kBACd,UAAU,qBACV,gBACA,kBACA;UAUe;;;;;;;EAOf,2BAA2B;;;;;;;;;;;;;;;;;;;;;;iBAuBb,gBACd,sBACA,UAAS,qBACR"}
1
+ {"version":3,"file":"router-builder-C6E11Grf.d.mts","names":[],"sources":["../src/http/router-builder.ts"],"mappings":";;;;iBA+BgB,+BAA+B,kBAAkB;;;;;;;;;;;iBAcjD,kBACd,UAAU,qBACV,gBACA,kBACA;UAUe;;;;;;;EAOf,2BAA2B;;;;;;;;;;;;;;;;;;;;;;iBAuBb,gBACd,sBACA,UAAS,qBACR"}
@@ -1,9 +1,9 @@
1
1
 
2
2
  import "./index-M6TF75MS.mjs";
3
- import { f as DepValue, o as ContextDecorator, s as ContextDecoratorSpec, v as MetaValue } from "./app-module-DtoySG6l.mjs";
4
- import { T as RequestContext, f as HttpRuntime, k as AdapterFactory, m as RouteEntry } from "./adapter-CbMq-NBf.mjs";
5
- import "./application-zlzYGgKD.mjs";
6
- import "./bootstrap-Nzgi7K2d.mjs";
3
+ import { S as MetaValue, b as ContextMetaKey, l as ContextDecoratorSpec, m as DepValue, s as ContextDecorator } from "./app-module-DtNJpj0r.mjs";
4
+ import { T as RequestContext, f as HttpRuntime, k as AdapterFactory, m as RouteEntry } from "./adapter-diFqgxGR.mjs";
5
+ import "./application-D_mReBi_.mjs";
6
+ import "./bootstrap-CytCvAnr.mjs";
7
7
  import "./cors-BuDU9CpA.mjs";
8
8
  import "./csrf-vR23CJas.mjs";
9
9
  import "./error-handler-Cr6Yt9as.mjs";
@@ -12,11 +12,10 @@ import "./rate-limit-Dme747JJ.mjs";
12
12
  import "./request-id-CVzJ1uHD.mjs";
13
13
  import "./request-logger-BU4ulKLp.mjs";
14
14
  import "./session-z9iewOXA.mjs";
15
- import "./spa-DsRUrm69.mjs";
16
- import "./upload-XVTyQcNN.mjs";
15
+ import "./kv-stores-D2kSUAEP.mjs";
16
+ import { t as BuildRoutesOptions } from "./router-builder-C6E11Grf.mjs";
17
17
  import "./validate-DMYpUnrf.mjs";
18
- import "./kv-stores-CHowq8VP.mjs";
19
- import { t as BuildRoutesOptions } from "./router-builder-CMGhEBU2.mjs";
18
+ import "./upload-Wck12jE5.mjs";
20
19
  import { Express, NextFunction, Request, Response, Router } from "express";
21
20
  import { AsyncLocalStorage } from "node:async_hooks";
22
21
  //#region src/core/disposables.d.ts
@@ -412,7 +411,7 @@ declare function isClusterPrimary(): boolean;
412
411
  * from the spec, and `Ctx` is locked to `RequestContext`.
413
412
  */
414
413
  interface DefineHttpContextDecoratorWithParams<P extends Record<string, unknown>> {
415
- <K extends string, D extends Record<string, DepValue> = Record<string, never>>(spec: ContextDecoratorSpec<K, D, P, RequestContext>): ContextDecorator<K, D, P, RequestContext>;
414
+ <K extends ContextMetaKey, D extends Record<string, DepValue> = Record<string, never>>(spec: ContextDecoratorSpec<K, D, P, RequestContext>): ContextDecorator<K, D, P, RequestContext>;
416
415
  }
417
416
  /**
418
417
  * Public shape of {@link defineHttpContextDecorator} — the positional
@@ -420,7 +419,7 @@ interface DefineHttpContextDecoratorWithParams<P extends Record<string, unknown>
420
419
  * partial-inference for the parameterised case.
421
420
  */
422
421
  interface DefineHttpContextDecoratorFn {
423
- <K extends string, D extends Record<string, DepValue> = Record<string, never>, P extends Record<string, unknown> = Record<string, never>>(spec: ContextDecoratorSpec<K, D, P, RequestContext>): ContextDecorator<K, D, P, RequestContext>;
422
+ <K extends ContextMetaKey, D extends Record<string, DepValue> = Record<string, never>, P extends Record<string, unknown> = Record<string, never>>(spec: ContextDecoratorSpec<K, D, P, RequestContext>): ContextDecorator<K, D, P, RequestContext>;
424
423
  /**
425
424
  * Curried entry point. Spell only `P`; `K` and `D` infer from the
426
425
  * spec. `Ctx` is locked to `RequestContext`.
@@ -533,7 +532,7 @@ declare function getRequestStore(): RequestStore;
533
532
  * }
534
533
  * ```
535
534
  */
536
- declare function getRequestValue<K extends string>(key: K): MetaValue<K> | undefined;
535
+ declare function getRequestValue<K extends ContextMetaKey>(key: K): MetaValue<K> | undefined;
537
536
  //#endregion
538
537
  //#region src/http/middleware/request-scope.d.ts
539
538
  /**
@@ -655,5 +654,132 @@ interface ViewAdapterOptions {
655
654
  */
656
655
  declare const ViewAdapter: AdapterFactory<ViewAdapterOptions, unknown>;
657
656
  //#endregion
658
- export { mountSort as A, FormatKickErrorOptions as B, groupAssetKeys as C, MountCycleError as D, MissingMountDepError as E, CircuitBreaker as F, disposeAll as G, KickErrorInit as H, CircuitBreakerOptions as I, registerDisposable as K, CircuitBreakerState as L, DuplicateContributorError as M, MissingContextValueError as N, MountKind as O, MissingContributorError as P, CircuitBreakerStats as R, GroupAssetKeysResult as S, DuplicateMountNameError as T, formatKickError as U, KickError as V, Disposable as W, ClusterOptions as _, TraceContextOptions as a, AssetKeyStrategy as b, requestScopeMiddleware as c, getRequestValue as d, requestStore as f, defineHttpContextDecorator as g, materializeRouter as h, TraceContext as i, ContributorCycleError as j, MountSortItem as k, RequestStore as l, expressRuntime as m, ViewAdapterOptions as n, parseTraceparent as o, buildRoutes as p, TRACEPARENT_HEADER as r, traceContext as s, ViewAdapter as t, getRequestStore as u, isClusterPrimary as v, stripExt as w, GroupAssetKeysOptions as x, AssetKeyPair as y, CircuitOpenError as z };
659
- //# sourceMappingURL=views-DsfBT1fR.d.mts.map
657
+ //#region src/http/middleware/spa.d.ts
658
+ interface SpaAdapterOptions {
659
+ /**
660
+ * Directory containing the built SPA files (index.html, assets, etc.)
661
+ * Default: 'dist/client'
662
+ *
663
+ * @example
664
+ * ```ts
665
+ * // Vue: 'dist'
666
+ * // React (Vite): 'dist'
667
+ * // React (CRA): 'build'
668
+ * // Svelte: 'build'
669
+ * // Angular: 'dist/my-app/browser'
670
+ * ```
671
+ */
672
+ clientDir?: string;
673
+ /**
674
+ * URL prefix for API routes. SPA fallback only applies to
675
+ * non-API routes (routes NOT starting with this prefix).
676
+ * Default: '/api'
677
+ *
678
+ * Matching is segment-aware: `/api` excludes `/api` and `/api/users`, but
679
+ * NOT `/apidocs` — a plain `startsWith` swallowed any path that merely
680
+ * began with the same letters.
681
+ *
682
+ * Set to an array for multiple prefixes:
683
+ * ```ts
684
+ * apiPrefix: ['/api', '/graphql', '/_debug']
685
+ * ```
686
+ */
687
+ apiPrefix?: string | string[];
688
+ /**
689
+ * Additional paths to exclude from SPA fallback.
690
+ * These paths will NOT serve index.html. Segment-aware, like `apiPrefix`.
691
+ *
692
+ * @example
693
+ * ```ts
694
+ * exclude: ['/health', '/metrics', '/ws']
695
+ * ```
696
+ */
697
+ exclude?: string[];
698
+ /**
699
+ * Cache-Control header for static assets (default: 'public, max-age=31536000, immutable')
700
+ * Set to false to disable caching headers.
701
+ */
702
+ cacheControl?: string | false;
703
+ /**
704
+ * Cache-Control for index.html (default: 'no-cache')
705
+ * index.html should not be cached to ensure clients get fresh builds.
706
+ */
707
+ indexCacheControl?: string;
708
+ /**
709
+ * Serve index.html even when the client did not ask for HTML.
710
+ * Default: false.
711
+ *
712
+ * By default the fallback only fires for requests whose `Accept` header
713
+ * includes `text/html` — the rule `connect-history-api-fallback` uses. That
714
+ * keeps a missing `/assets/app.js` a 404 instead of handing back an HTML
715
+ * document that the browser then fails to parse as JavaScript, which is a
716
+ * confusing way to discover a broken build.
717
+ *
718
+ * Turn this on if you have non-browser clients that deep-link into SPA
719
+ * routes without an `Accept` header.
720
+ */
721
+ alwaysFallback?: boolean;
722
+ }
723
+ /**
724
+ * Where to look for the built SPA.
725
+ *
726
+ * `process.cwd()` is the framework's convention (assets and env files resolve
727
+ * the same way) and stays the primary base. But a relative `clientDir` then
728
+ * only works when the process happens to be started from the app directory —
729
+ * `node server/dist/index.js` run from a monorepo root resolved `../web/dist`
730
+ * against the root and found nothing, serving no SPA with only a warning.
731
+ *
732
+ * So: try cwd first, and only if that misses, retry against the package root
733
+ * of the entry script. Both are checked for existence before being chosen, so
734
+ * this can never silently pick a directory that is not there — a genuinely
735
+ * missing build still lands on the cwd path, which is what the warning names.
736
+ *
737
+ * Deliberately not entry-relative FIRST: under `kick dev` the entry is the CLI
738
+ * binary, whose package root is the CLI itself.
739
+ *
740
+ * Exported for tests only — not re-exported from the package index.
741
+ */
742
+ declare function resolveClientDir(clientDir: string, cwd?: string, entry?: string | undefined): string;
743
+ /**
744
+ * SPA adapter — serve a Vue, React, Svelte, or Angular build alongside
745
+ * your KickJS API. API routes are handled by controllers; everything else
746
+ * falls back to index.html for client-side routing.
747
+ *
748
+ * Written against the engine-agnostic `http` surface (`serveStatic` / `use`),
749
+ * so it runs under Express, Fastify, and h3 alike. The previous version
750
+ * reached for `express.static`, `req.path`, and `res.send`, all of which are
751
+ * Express-only — it silently served nothing on the other runtimes.
752
+ *
753
+ * Migrated to the `defineAdapter()` factory — call without `new`, as
754
+ * `ViewAdapter` has since v4.
755
+ *
756
+ * @example
757
+ * ```ts
758
+ * import { SpaAdapter } from '@forinda/kickjs/spa'
759
+ *
760
+ * bootstrap({
761
+ * modules,
762
+ * adapters: [
763
+ * SpaAdapter({
764
+ * clientDir: 'dist/client', // or 'build', 'dist', etc.
765
+ * apiPrefix: '/api',
766
+ * }),
767
+ * ],
768
+ * })
769
+ * ```
770
+ *
771
+ * File structure:
772
+ * ```
773
+ * dist/
774
+ * client/ ← SPA build output
775
+ * index.html
776
+ * assets/
777
+ * app.js
778
+ * style.css
779
+ * server/ ← KickJS server
780
+ * ```
781
+ */
782
+ declare const SpaAdapter: AdapterFactory<SpaAdapterOptions, unknown>;
783
+ //#endregion
784
+ export { MountCycleError as A, CircuitBreakerState as B, AssetKeyStrategy as C, stripExt as D, groupAssetKeys as E, DuplicateContributorError as F, KickErrorInit as G, CircuitOpenError as H, MissingContextValueError as I, disposeAll as J, formatKickError as K, MissingContributorError as L, MountSortItem as M, mountSort as N, DuplicateMountNameError as O, ContributorCycleError as P, CircuitBreaker as R, AssetKeyPair as S, GroupAssetKeysResult as T, FormatKickErrorOptions as U, CircuitBreakerStats as V, KickError as W, registerDisposable as Y, expressRuntime as _, ViewAdapterOptions as a, ClusterOptions as b, TraceContextOptions as c, requestScopeMiddleware as d, RequestStore as f, buildRoutes as g, requestStore as h, ViewAdapter as i, MountKind as j, MissingMountDepError as k, parseTraceparent as l, getRequestValue as m, SpaAdapterOptions as n, TRACEPARENT_HEADER as o, getRequestStore as p, Disposable as q, resolveClientDir as r, TraceContext as s, SpaAdapter as t, traceContext as u, materializeRouter as v, GroupAssetKeysOptions as w, isClusterPrimary as x, defineHttpContextDecorator as y, CircuitBreakerOptions as z };
785
+ //# sourceMappingURL=spa-DJbVV_4F.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spa-DJbVV_4F.d.mts","names":[],"sources":["../src/core/disposables.ts","../src/core/kick-error.ts","../src/core/circuit-breaker.ts","../src/core/context-errors.ts","../src/core/mount-sort.ts","../src/core/asset-keys.ts","../src/http/cluster.ts","../src/http/define-http-context-decorator.ts","../src/http/runtimes/express.ts","../src/http/request-store.ts","../src/http/middleware/request-scope.ts","../src/http/middleware/trace-context.ts","../src/http/middleware/views.ts","../src/http/middleware/spa.ts"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
 
2
2
  import { g as FileUploadConfig, o as BaseUploadOptions } from "./decorators-Bks7cgBJ.mjs";
3
- import "./adapter-CbMq-NBf.mjs";
3
+ import "./adapter-diFqgxGR.mjs";
4
4
  import { NextFunction, Request, RequestHandler, Response } from "express";
5
5
  import { Options } from "multer";
6
6
  //#region src/http/middleware/upload-config.d.ts
@@ -113,4 +113,4 @@ declare const upload: {
113
113
  };
114
114
  //#endregion
115
115
  export { RawUploadPart as a, buildFileTypeFilter as c, upload as i, resolveMimeTypes as l, buildUploadMiddleware as n, UploadedFileLike as o, cleanupFiles as r, applyUploadConfig as s, UploadOptions as t };
116
- //# sourceMappingURL=upload-XVTyQcNN.d.mts.map
116
+ //# sourceMappingURL=upload-Wck12jE5.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"upload-XVTyQcNN.d.mts","names":[],"sources":["../src/http/middleware/upload-config.ts","../src/http/middleware/upload.ts"],"mappings":";;;;;;;;;;;;;;;;iBAuEgB,iBAAiB;;;;;;;;iBAcjB,oBACd,cAAc,mCACd,gBAAgB,0BACd,kBAAkB;;UAYL;EACf;EACA;EACA;EACA;EACA;EACA,QAAQ;;;UAIO;EACf;EACA;EACA;EACA,QAAQ;;;;;;;;;iBAYM,kBACd,OAAO,iBACP,QAAQ;EACL,OAAO;EAAkB,QAAQ;;;;;;;;;UChFrB,sBAAsB;;EAErC,UAAU;;EAEV;;;;;;;;;;;;;;iBA0CO,OAAO,mBAAmB,UAAS,gBAAqB;;;;iBAQxD,MAAM,mBAAmB,mBAAe,UAAS,gBAAqB;;;;iBAQtE,KAAK,UAAS,gBAAqB;;;;;;;;;;;;;;;iBAmB5B,iBAAY,KACb,SAAO,KAAO,UAAQ,MAAQ;;;;;;iBA+B7B,sBAAsB,QAAQ,mBAAmB;;cAmBpD"}
1
+ {"version":3,"file":"upload-Wck12jE5.d.mts","names":[],"sources":["../src/http/middleware/upload-config.ts","../src/http/middleware/upload.ts"],"mappings":";;;;;;;;;;;;;;;;iBAuEgB,iBAAiB;;;;;;;;iBAcjB,oBACd,cAAc,mCACd,gBAAgB,0BACd,kBAAkB;;UAYL;EACf;EACA;EACA;EACA;EACA;EACA,QAAQ;;;UAIO;EACf;EACA;EACA;EACA,QAAQ;;;;;;;;;iBAYM,kBACd,OAAO,iBACP,QAAQ;EACL,OAAO;EAAkB,QAAQ;;;;;;;;;UChFrB,sBAAsB;;EAErC,UAAU;;EAEV;;;;;;;;;;;;;;iBA0CO,OAAO,mBAAmB,UAAS,gBAAqB;;;;iBAQxD,MAAM,mBAAmB,mBAAe,UAAS,gBAAqB;;;;iBAQtE,KAAK,UAAS,gBAAqB;;;;;;;;;;;;;;;iBAmB5B,iBAAY,KACb,SAAO,KAAO,UAAQ,MAAQ;;;;;;iBA+B7B,sBAAsB,QAAQ,mBAAmB;;cAmBpD"}
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v6.6.1
2
+ * @forinda/kickjs v7.0.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -9,4 +9,4 @@
9
9
  * @license MIT
10
10
  */
11
11
  import{HttpException,HttpStatus}from"./core/errors.mjs";const MIME_MAP={jpg:`image/jpeg`,jpeg:`image/jpeg`,png:`image/png`,gif:`image/gif`,webp:`image/webp`,svg:`image/svg+xml`,bmp:`image/bmp`,ico:`image/x-icon`,tiff:`image/tiff`,tif:`image/tiff`,avif:`image/avif`,pdf:`application/pdf`,doc:`application/msword`,docx:`application/vnd.openxmlformats-officedocument.wordprocessingml.document`,xls:`application/vnd.ms-excel`,xlsx:`application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`,ppt:`application/vnd.ms-powerpoint`,pptx:`application/vnd.openxmlformats-officedocument.presentationml.presentation`,odt:`application/vnd.oasis.opendocument.text`,ods:`application/vnd.oasis.opendocument.spreadsheet`,odp:`application/vnd.oasis.opendocument.presentation`,rtf:`application/rtf`,txt:`text/plain`,csv:`text/csv`,zip:`application/zip`,gz:`application/gzip`,tar:`application/x-tar`,rar:`application/vnd.rar`,"7z":`application/x-7z-compressed`,mp3:`audio/mpeg`,wav:`audio/wav`,ogg:`audio/ogg`,flac:`audio/flac`,aac:`audio/aac`,mp4:`video/mp4`,webm:`video/webm`,avi:`video/x-msvideo`,mov:`video/quicktime`,mkv:`video/x-matroska`,json:`application/json`,xml:`application/xml`,html:`text/html`};function resolveMimeTypes(types){return types.map(t=>{let lower=t.toLowerCase().replace(/^\./,``);return MIME_MAP[lower]??t})}function buildFileTypeFilter(allowedTypes,customMimeMap){if(!allowedTypes)return()=>!0;if(typeof allowedTypes==`function`)return allowedTypes;let mimeMap=customMimeMap?{...MIME_MAP,...customMimeMap}:MIME_MAP,resolved=allowedTypes.map(t=>mimeMap[t.toLowerCase().replace(/^\./,``)]??t);return mimetype=>resolved.some(type=>type.endsWith(`/*`)?mimetype.startsWith(type.replace(`/*`,`/`)):mimetype===type)}function applyUploadConfig(parts,config){if(config.mode===`none`)return{};let field=config.fieldName??`file`,maxSize=config.maxSize??5242880,typeAllowed=buildFileTypeFilter(config.allowedTypes,config.customMimeMap),files=parts.filter(p=>p.fieldname===field).map(p=>{if(!typeAllowed(p.mimetype,p.filename))throw new HttpException(HttpStatus.UNSUPPORTED_MEDIA_TYPE,`File type ${p.mimetype} is not allowed`);if(p.buffer.length>maxSize)throw new HttpException(HttpStatus.PAYLOAD_TOO_LARGE,`File ${p.filename} exceeds the ${maxSize}-byte limit`);return{fieldname:p.fieldname,originalname:p.filename,encoding:`7bit`,mimetype:p.mimetype,size:p.buffer.length,buffer:p.buffer}});if(config.mode===`single`)return{file:files[0]};let maxCount=config.maxCount??10;return{files:files.slice(0,maxCount)}}export{buildFileTypeFilter as n,resolveMimeTypes as r,applyUploadConfig as t};
12
- //# sourceMappingURL=upload-config-DXxieUa0.mjs.map
12
+ //# sourceMappingURL=upload-config-BlmVJI4n.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"upload-config-DXxieUa0.mjs","names":[],"sources":["../src/http/middleware/upload-config.ts"],"sourcesContent":["// Pure (edge-safe) upload core — the engine-neutral half of the upload\n// middleware. Extracted from `upload.ts` so the `@forinda/kickjs/web` entry\n// graph can validate/shape buffered multipart parts WITHOUT dragging in\n// upload.ts's node-only surface (multer peer loading via `node:module`,\n// disk cleanup via `node:fs/promises`). `upload.ts` re-exports everything\n// here, so existing import sites are unchanged.\n\nimport { HttpException, HttpStatus } from '../../core/errors'\nimport type { BaseUploadOptions, FileUploadConfig } from '../../core/decorators'\n\nexport const MIME_MAP: Record<string, string> = {\n // Images\n jpg: 'image/jpeg',\n jpeg: 'image/jpeg',\n png: 'image/png',\n gif: 'image/gif',\n webp: 'image/webp',\n svg: 'image/svg+xml',\n bmp: 'image/bmp',\n ico: 'image/x-icon',\n tiff: 'image/tiff',\n tif: 'image/tiff',\n avif: 'image/avif',\n // Documents\n pdf: 'application/pdf',\n doc: 'application/msword',\n docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',\n xls: 'application/vnd.ms-excel',\n xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',\n ppt: 'application/vnd.ms-powerpoint',\n pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',\n odt: 'application/vnd.oasis.opendocument.text',\n ods: 'application/vnd.oasis.opendocument.spreadsheet',\n odp: 'application/vnd.oasis.opendocument.presentation',\n rtf: 'application/rtf',\n txt: 'text/plain',\n csv: 'text/csv',\n // Archives\n zip: 'application/zip',\n gz: 'application/gzip',\n tar: 'application/x-tar',\n rar: 'application/vnd.rar',\n '7z': 'application/x-7z-compressed',\n // Audio\n mp3: 'audio/mpeg',\n wav: 'audio/wav',\n ogg: 'audio/ogg',\n flac: 'audio/flac',\n aac: 'audio/aac',\n // Video\n mp4: 'video/mp4',\n webm: 'video/webm',\n avi: 'video/x-msvideo',\n mov: 'video/quicktime',\n mkv: 'video/x-matroska',\n // Other\n json: 'application/json',\n xml: 'application/xml',\n html: 'text/html',\n}\n\n/**\n * Resolves a list of file type identifiers to MIME type strings.\n * Accepts short extensions (`'jpg'`, `'pdf'`) or full MIME types (`'image/jpeg'`).\n *\n * @example\n * ```ts\n * resolveMimeTypes(['jpg', 'png', 'application/pdf'])\n * // → ['image/jpeg', 'image/png', 'application/pdf']\n * ```\n */\nexport function resolveMimeTypes(types: string[]): string[] {\n return types.map((t) => {\n const lower = t.toLowerCase().replace(/^\\./, '')\n return MIME_MAP[lower] ?? t\n })\n}\n\n/**\n * Build a runtime-agnostic `(mimetype, filename) => boolean` file-type filter\n * from `allowedTypes` (a predicate, or a list of extensions / MIME types /\n * wildcards). Returned by both the multer middleware (Express) and the\n * Fastify / h3 / web multipart paths so the accept/reject rule stays\n * identical across engines. No `allowedTypes` → accept everything.\n */\nexport function buildFileTypeFilter(\n allowedTypes: BaseUploadOptions['allowedTypes'],\n customMimeMap?: Record<string, string>,\n): (mimetype: string, filename: string) => boolean {\n if (!allowedTypes) return () => true\n if (typeof allowedTypes === 'function') return allowedTypes\n const mimeMap = customMimeMap ? { ...MIME_MAP, ...customMimeMap } : MIME_MAP\n const resolved = allowedTypes.map((t) => mimeMap[t.toLowerCase().replace(/^\\./, '')] ?? t)\n return (mimetype: string) =>\n resolved.some((type) =>\n type.endsWith('/*') ? mimetype.startsWith(type.replace('/*', '/')) : mimetype === type,\n )\n}\n\n/** A multer-shaped uploaded file — what `ctx.file` / `ctx.files` expose. */\nexport interface UploadedFileLike {\n fieldname: string\n originalname: string\n encoding: string\n mimetype: string\n size: number\n buffer: Buffer\n}\n\n/** A buffered multipart file part, as the runtime hands it to {@link applyUploadConfig}. */\nexport interface RawUploadPart {\n fieldname: string\n filename: string\n mimetype: string\n buffer: Buffer\n}\n\nconst DEFAULT_MAX_SIZE = 5 * 1024 * 1024\n\n/**\n * Apply a `@FileUpload` config to already-buffered multipart parts and return\n * the multer-shaped `file` / `files` for `ctx`. Enforces the field name, type\n * filter, per-file size limit, and (array mode) max count — throwing\n * `HttpException` on a violation, mirroring the Express/multer error path.\n * Used by the Fastify, h3 and web runtimes; Express keeps using multer directly.\n */\nexport function applyUploadConfig(\n parts: RawUploadPart[],\n config: FileUploadConfig,\n): { file?: UploadedFileLike; files?: UploadedFileLike[] } {\n if (config.mode === 'none') return {}\n\n const field = config.fieldName ?? 'file'\n const maxSize = config.maxSize ?? DEFAULT_MAX_SIZE\n const typeAllowed = buildFileTypeFilter(config.allowedTypes, config.customMimeMap)\n\n const matched = parts.filter((p) => p.fieldname === field)\n const files: UploadedFileLike[] = matched.map((p) => {\n if (!typeAllowed(p.mimetype, p.filename)) {\n throw new HttpException(\n HttpStatus.UNSUPPORTED_MEDIA_TYPE,\n `File type ${p.mimetype} is not allowed`,\n )\n }\n if (p.buffer.length > maxSize) {\n throw new HttpException(\n HttpStatus.PAYLOAD_TOO_LARGE,\n `File ${p.filename} exceeds the ${maxSize}-byte limit`,\n )\n }\n return {\n fieldname: p.fieldname,\n originalname: p.filename,\n encoding: '7bit',\n mimetype: p.mimetype,\n size: p.buffer.length,\n buffer: p.buffer,\n }\n })\n\n if (config.mode === 'single') return { file: files[0] }\n const maxCount = config.maxCount ?? 10\n return { files: files.slice(0, maxCount) }\n}\n"],"mappings":";;;;;;;;;;wDAUA,MAAa,SAAmC,CAE9C,IAAK,aACL,KAAM,aACN,IAAK,YACL,IAAK,YACL,KAAM,aACN,IAAK,gBACL,IAAK,YACL,IAAK,eACL,KAAM,aACN,IAAK,aACL,KAAM,aAEN,IAAK,kBACL,IAAK,qBACL,KAAM,0EACN,IAAK,2BACL,KAAM,oEACN,IAAK,gCACL,KAAM,4EACN,IAAK,0CACL,IAAK,iDACL,IAAK,kDACL,IAAK,kBACL,IAAK,aACL,IAAK,WAEL,IAAK,kBACL,GAAI,mBACJ,IAAK,oBACL,IAAK,sBACL,KAAM,8BAEN,IAAK,aACL,IAAK,YACL,IAAK,YACL,KAAM,aACN,IAAK,YAEL,IAAK,YACL,KAAM,aACN,IAAK,kBACL,IAAK,kBACL,IAAK,mBAEL,KAAM,mBACN,IAAK,kBACL,KAAM,WACR,EAYA,SAAgB,iBAAiB,MAA2B,CAC1D,OAAO,MAAM,IAAK,GAAM,CACtB,IAAM,MAAQ,EAAE,YAAY,CAAC,CAAC,QAAQ,MAAO,EAAE,EAC/C,OAAO,SAAS,QAAU,CAC5B,CAAC,CACH,CASA,SAAgB,oBACd,aACA,cACiD,CACjD,GAAI,CAAC,aAAc,UAAa,GAChC,GAAI,OAAO,cAAiB,WAAY,OAAO,aAC/C,IAAM,QAAU,cAAgB,CAAE,GAAG,SAAU,GAAG,aAAc,EAAI,SAC9D,SAAW,aAAa,IAAK,GAAM,QAAQ,EAAE,YAAY,CAAC,CAAC,QAAQ,MAAO,EAAE,IAAM,CAAC,EACzF,MAAQ,WACN,SAAS,KAAM,MACb,KAAK,SAAS,IAAI,EAAI,SAAS,WAAW,KAAK,QAAQ,KAAM,GAAG,CAAC,EAAI,WAAa,IACpF,CACJ,CA6BA,SAAgB,kBACd,MACA,OACyD,CACzD,GAAI,OAAO,OAAS,OAAQ,MAAO,CAAC,EAEpC,IAAM,MAAQ,OAAO,WAAa,OAC5B,QAAU,OAAO,SAAW,QAC5B,YAAc,oBAAoB,OAAO,aAAc,OAAO,aAAa,EAG3E,MADU,MAAM,OAAQ,GAAM,EAAE,YAAc,KACZ,CAAC,CAAC,IAAK,GAAM,CACnD,GAAI,CAAC,YAAY,EAAE,SAAU,EAAE,QAAQ,EACrC,MAAM,IAAI,cACR,WAAW,uBACX,aAAa,EAAE,SAAS,gBAC1B,EAEF,GAAI,EAAE,OAAO,OAAS,QACpB,MAAM,IAAI,cACR,WAAW,kBACX,QAAQ,EAAE,SAAS,eAAe,QAAQ,YAC5C,EAEF,MAAO,CACL,UAAW,EAAE,UACb,aAAc,EAAE,SAChB,SAAU,OACV,SAAU,EAAE,SACZ,KAAM,EAAE,OAAO,OACf,OAAQ,EAAE,MACZ,CACF,CAAC,EAED,GAAI,OAAO,OAAS,SAAU,MAAO,CAAE,KAAM,MAAM,EAAG,EACtD,IAAM,SAAW,OAAO,UAAY,GACpC,MAAO,CAAE,MAAO,MAAM,MAAM,EAAG,QAAQ,CAAE,CAC3C"}
1
+ {"version":3,"file":"upload-config-BlmVJI4n.mjs","names":[],"sources":["../src/http/middleware/upload-config.ts"],"sourcesContent":["// Pure (edge-safe) upload core — the engine-neutral half of the upload\n// middleware. Extracted from `upload.ts` so the `@forinda/kickjs/web` entry\n// graph can validate/shape buffered multipart parts WITHOUT dragging in\n// upload.ts's node-only surface (multer peer loading via `node:module`,\n// disk cleanup via `node:fs/promises`). `upload.ts` re-exports everything\n// here, so existing import sites are unchanged.\n\nimport { HttpException, HttpStatus } from '../../core/errors'\nimport type { BaseUploadOptions, FileUploadConfig } from '../../core/decorators'\n\nexport const MIME_MAP: Record<string, string> = {\n // Images\n jpg: 'image/jpeg',\n jpeg: 'image/jpeg',\n png: 'image/png',\n gif: 'image/gif',\n webp: 'image/webp',\n svg: 'image/svg+xml',\n bmp: 'image/bmp',\n ico: 'image/x-icon',\n tiff: 'image/tiff',\n tif: 'image/tiff',\n avif: 'image/avif',\n // Documents\n pdf: 'application/pdf',\n doc: 'application/msword',\n docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',\n xls: 'application/vnd.ms-excel',\n xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',\n ppt: 'application/vnd.ms-powerpoint',\n pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',\n odt: 'application/vnd.oasis.opendocument.text',\n ods: 'application/vnd.oasis.opendocument.spreadsheet',\n odp: 'application/vnd.oasis.opendocument.presentation',\n rtf: 'application/rtf',\n txt: 'text/plain',\n csv: 'text/csv',\n // Archives\n zip: 'application/zip',\n gz: 'application/gzip',\n tar: 'application/x-tar',\n rar: 'application/vnd.rar',\n '7z': 'application/x-7z-compressed',\n // Audio\n mp3: 'audio/mpeg',\n wav: 'audio/wav',\n ogg: 'audio/ogg',\n flac: 'audio/flac',\n aac: 'audio/aac',\n // Video\n mp4: 'video/mp4',\n webm: 'video/webm',\n avi: 'video/x-msvideo',\n mov: 'video/quicktime',\n mkv: 'video/x-matroska',\n // Other\n json: 'application/json',\n xml: 'application/xml',\n html: 'text/html',\n}\n\n/**\n * Resolves a list of file type identifiers to MIME type strings.\n * Accepts short extensions (`'jpg'`, `'pdf'`) or full MIME types (`'image/jpeg'`).\n *\n * @example\n * ```ts\n * resolveMimeTypes(['jpg', 'png', 'application/pdf'])\n * // → ['image/jpeg', 'image/png', 'application/pdf']\n * ```\n */\nexport function resolveMimeTypes(types: string[]): string[] {\n return types.map((t) => {\n const lower = t.toLowerCase().replace(/^\\./, '')\n return MIME_MAP[lower] ?? t\n })\n}\n\n/**\n * Build a runtime-agnostic `(mimetype, filename) => boolean` file-type filter\n * from `allowedTypes` (a predicate, or a list of extensions / MIME types /\n * wildcards). Returned by both the multer middleware (Express) and the\n * Fastify / h3 / web multipart paths so the accept/reject rule stays\n * identical across engines. No `allowedTypes` → accept everything.\n */\nexport function buildFileTypeFilter(\n allowedTypes: BaseUploadOptions['allowedTypes'],\n customMimeMap?: Record<string, string>,\n): (mimetype: string, filename: string) => boolean {\n if (!allowedTypes) return () => true\n if (typeof allowedTypes === 'function') return allowedTypes\n const mimeMap = customMimeMap ? { ...MIME_MAP, ...customMimeMap } : MIME_MAP\n const resolved = allowedTypes.map((t) => mimeMap[t.toLowerCase().replace(/^\\./, '')] ?? t)\n return (mimetype: string) =>\n resolved.some((type) =>\n type.endsWith('/*') ? mimetype.startsWith(type.replace('/*', '/')) : mimetype === type,\n )\n}\n\n/** A multer-shaped uploaded file — what `ctx.file` / `ctx.files` expose. */\nexport interface UploadedFileLike {\n fieldname: string\n originalname: string\n encoding: string\n mimetype: string\n size: number\n buffer: Buffer\n}\n\n/** A buffered multipart file part, as the runtime hands it to {@link applyUploadConfig}. */\nexport interface RawUploadPart {\n fieldname: string\n filename: string\n mimetype: string\n buffer: Buffer\n}\n\nconst DEFAULT_MAX_SIZE = 5 * 1024 * 1024\n\n/**\n * Apply a `@FileUpload` config to already-buffered multipart parts and return\n * the multer-shaped `file` / `files` for `ctx`. Enforces the field name, type\n * filter, per-file size limit, and (array mode) max count — throwing\n * `HttpException` on a violation, mirroring the Express/multer error path.\n * Used by the Fastify, h3 and web runtimes; Express keeps using multer directly.\n */\nexport function applyUploadConfig(\n parts: RawUploadPart[],\n config: FileUploadConfig,\n): { file?: UploadedFileLike; files?: UploadedFileLike[] } {\n if (config.mode === 'none') return {}\n\n const field = config.fieldName ?? 'file'\n const maxSize = config.maxSize ?? DEFAULT_MAX_SIZE\n const typeAllowed = buildFileTypeFilter(config.allowedTypes, config.customMimeMap)\n\n const matched = parts.filter((p) => p.fieldname === field)\n const files: UploadedFileLike[] = matched.map((p) => {\n if (!typeAllowed(p.mimetype, p.filename)) {\n throw new HttpException(\n HttpStatus.UNSUPPORTED_MEDIA_TYPE,\n `File type ${p.mimetype} is not allowed`,\n )\n }\n if (p.buffer.length > maxSize) {\n throw new HttpException(\n HttpStatus.PAYLOAD_TOO_LARGE,\n `File ${p.filename} exceeds the ${maxSize}-byte limit`,\n )\n }\n return {\n fieldname: p.fieldname,\n originalname: p.filename,\n encoding: '7bit',\n mimetype: p.mimetype,\n size: p.buffer.length,\n buffer: p.buffer,\n }\n })\n\n if (config.mode === 'single') return { file: files[0] }\n const maxCount = config.maxCount ?? 10\n return { files: files.slice(0, maxCount) }\n}\n"],"mappings":";;;;;;;;;;wDAUA,MAAa,SAAmC,CAE9C,IAAK,aACL,KAAM,aACN,IAAK,YACL,IAAK,YACL,KAAM,aACN,IAAK,gBACL,IAAK,YACL,IAAK,eACL,KAAM,aACN,IAAK,aACL,KAAM,aAEN,IAAK,kBACL,IAAK,qBACL,KAAM,0EACN,IAAK,2BACL,KAAM,oEACN,IAAK,gCACL,KAAM,4EACN,IAAK,0CACL,IAAK,iDACL,IAAK,kDACL,IAAK,kBACL,IAAK,aACL,IAAK,WAEL,IAAK,kBACL,GAAI,mBACJ,IAAK,oBACL,IAAK,sBACL,KAAM,8BAEN,IAAK,aACL,IAAK,YACL,IAAK,YACL,KAAM,aACN,IAAK,YAEL,IAAK,YACL,KAAM,aACN,IAAK,kBACL,IAAK,kBACL,IAAK,mBAEL,KAAM,mBACN,IAAK,kBACL,KAAM,WACR,EAYA,SAAgB,iBAAiB,MAA2B,CAC1D,OAAO,MAAM,IAAK,GAAM,CACtB,IAAM,MAAQ,EAAE,YAAY,CAAC,CAAC,QAAQ,MAAO,EAAE,EAC/C,OAAO,SAAS,QAAU,CAC5B,CAAC,CACH,CASA,SAAgB,oBACd,aACA,cACiD,CACjD,GAAI,CAAC,aAAc,UAAa,GAChC,GAAI,OAAO,cAAiB,WAAY,OAAO,aAC/C,IAAM,QAAU,cAAgB,CAAE,GAAG,SAAU,GAAG,aAAc,EAAI,SAC9D,SAAW,aAAa,IAAK,GAAM,QAAQ,EAAE,YAAY,CAAC,CAAC,QAAQ,MAAO,EAAE,IAAM,CAAC,EACzF,MAAQ,WACN,SAAS,KAAM,MACb,KAAK,SAAS,IAAI,EAAI,SAAS,WAAW,KAAK,QAAQ,KAAM,GAAG,CAAC,EAAI,WAAa,IACpF,CACJ,CA6BA,SAAgB,kBACd,MACA,OACyD,CACzD,GAAI,OAAO,OAAS,OAAQ,MAAO,CAAC,EAEpC,IAAM,MAAQ,OAAO,WAAa,OAC5B,QAAU,OAAO,SAAW,QAC5B,YAAc,oBAAoB,OAAO,aAAc,OAAO,aAAa,EAG3E,MADU,MAAM,OAAQ,GAAM,EAAE,YAAc,KACZ,CAAC,CAAC,IAAK,GAAM,CACnD,GAAI,CAAC,YAAY,EAAE,SAAU,EAAE,QAAQ,EACrC,MAAM,IAAI,cACR,WAAW,uBACX,aAAa,EAAE,SAAS,gBAC1B,EAEF,GAAI,EAAE,OAAO,OAAS,QACpB,MAAM,IAAI,cACR,WAAW,kBACX,QAAQ,EAAE,SAAS,eAAe,QAAQ,YAC5C,EAEF,MAAO,CACL,UAAW,EAAE,UACb,aAAc,EAAE,SAChB,SAAU,OACV,SAAU,EAAE,SACZ,KAAM,EAAE,OAAO,OACf,OAAQ,EAAE,MACZ,CACF,CAAC,EAED,GAAI,OAAO,OAAS,SAAU,MAAO,CAAE,KAAM,MAAM,EAAG,EACtD,IAAM,SAAW,OAAO,UAAY,GACpC,MAAO,CAAE,MAAO,MAAM,MAAM,EAAG,QAAQ,CAAE,CAC3C"}
package/dist/web.d.mts CHANGED
@@ -1,8 +1,8 @@
1
1
 
2
2
  import { x as MiddlewareHandler } from "./decorators-Bks7cgBJ.mjs";
3
- import { d as ContributorRegistrations, r as AppModuleEntry } from "./app-module-DtoySG6l.mjs";
4
- import { D as SseHandler, b as RuntimeResponse, m as RouteEntry } from "./adapter-CbMq-NBf.mjs";
5
- import { a as KvSessionStoreOptions, c as InferHandlerResponse, d as isReply, f as reply, i as KvSessionStore, l as Reply, n as KvRateLimitStore, o as RateLimitGuardOptions, r as KvRateLimitStoreOptions, s as rateLimitGuard, t as KvLike } from "./kv-stores-CHowq8VP.mjs";
3
+ import { p as ContributorRegistrations, r as AppModuleEntry } from "./app-module-DtNJpj0r.mjs";
4
+ import { D as SseHandler, b as RuntimeResponse, m as RouteEntry } from "./adapter-diFqgxGR.mjs";
5
+ import { a as KvSessionStoreOptions, c as InferHandlerResponse, d as isReply, f as reply, i as KvSessionStore, l as Reply, n as KvRateLimitStore, o as RateLimitGuardOptions, r as KvRateLimitStoreOptions, s as rateLimitGuard, t as KvLike } from "./kv-stores-D2kSUAEP.mjs";
6
6
  //#region src/http/web/driver.d.ts
7
7
  /**
8
8
  * Presents a web `Request` as the express-shaped `req` object that
package/dist/web.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs v6.6.1
2
+ * @forinda/kickjs v7.0.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -8,5 +8,5 @@
8
8
  *
9
9
  * @license MIT
10
10
  */
11
- import{t as Container}from"./container-CSxotdD4.mjs";import{t as MutableModuleRegistry}from"./module-registry-DDdtsQld.mjs";import{normalizePath}from"./core/path.mjs";import{n as assertRouteUnique,r as buildRouteTable,t as _setExternalContributorSources}from"./router-builder-C6Qmb3rB.mjs";import{i as requestStore}from"./context-A-vHOCow.mjs";import{n as isReply,r as reply}from"./reply-Dcq46cVC.mjs";import{n as KvSessionStore,r as rateLimitGuard,t as KvRateLimitStore}from"./kv-stores-CcD6HvGV.mjs";import{n as WebRequestShim,r as WebResponseDriver,t as compileWebRoute}from"./handler-BTz5BPXI.mjs";function createWebApp(options){let h3mod=options.h3;if(typeof h3mod?.H3!=`function`)throw Error(`@forinda/kickjs/web: pass the h3 v2 module — \`createWebApp({ h3: await import("h3"), ... })\`. The installed h3 must be v2 (npm dist-tag 'latest'); v1 has no H3 class.`);let container=Container.getInstance();if(options.env){let env=options.env;Container._envResolver=key=>env[key]}Container._requestStoreProvider=()=>requestStore.getStore()??null;let registry=new MutableModuleRegistry;for(let m of options.modules)registry.mount(m);let modules=registry.entries.map(entry=>{let mod=typeof entry==`function`?new entry:entry;return mod.register?.(container),mod});container.bootstrap();let app=new h3mod.H3,apiPrefix=options.apiPrefix??`/api`,defaultVersion=options.defaultVersion??1,globalMiddleware=options.middleware??[],globalSources=(options.contributors??[]).map(registration=>({source:`global`,registration,label:`createWebApp`})),seenRoutes=new Map;for(let mod of modules){let declaredName=mod.name,moduleLabel=typeof declaredName==`string`&&declaredName.length>0?declaredName:mod.constructor?.name??`module`;_setExternalContributorSources([...(mod.contributors?.()??[]).map(registration=>({source:`module`,registration,label:moduleLabel})),...globalSources]);try{let result=mod.routes();if(!result)continue;let routeSets=Array.isArray(result)?result:[result];for(let route of routeSets){if(!route.controller)throw Error(`@forinda/kickjs/web: module route at '${route.path}' has no controller. Express-style \`router\` mounts are not supported on the web entry.`);let mountPath=`${apiPrefix}/v${route.version??defaultVersion}${normalizePath(route.path)}`;for(let entry of buildRouteTable(route.controller)){let url=joinPath(mountPath,entry.path),owner=`${route.controller.name??`controller`}.${String(entry.meta.handlerName??`?`)}`;assertRouteUnique(seenRoutes,entry.method,url,owner),globalMiddleware.length>0&&(entry.middlewares=[...globalMiddleware,...entry.middlewares]);let run=compileWebRoute(entry);app.on(entry.method,url,event=>run({request:event.req,url:event.url,params:event.context.params??{}}))}}}finally{_setExternalContributorSources([])}}return{fetch:request=>app.fetch(request),h3:app}}function createFetchHandler(build){let app;return{fetch:(request,env={})=>(app??=createWebApp(build(env)),app.fetch(request))}}function joinPath(mountPath,path){let a=mountPath.endsWith(`/`)?mountPath.slice(0,-1):mountPath;return path===`/`||path===``?a===``?`/`:a:`${a}${path.startsWith(`/`)?path:`/${path}`}`}export{KvRateLimitStore,KvSessionStore,WebRequestShim,WebResponseDriver,compileWebRoute,createFetchHandler,createWebApp,isReply,rateLimitGuard,reply};
11
+ import{t as Container}from"./container-BmZciE3k.mjs";import{t as MutableModuleRegistry}from"./module-registry-Cpf01UdE.mjs";import{normalizePath}from"./core/path.mjs";import{n as assertRouteUnique,r as buildRouteTable,t as _setExternalContributorSources}from"./router-builder-BjLsf6w0.mjs";import{i as requestStore}from"./context-vG6HXwHB.mjs";import{n as isReply,r as reply}from"./reply-dxkvXqNo.mjs";import{n as KvSessionStore,r as rateLimitGuard,t as KvRateLimitStore}from"./kv-stores-zApd2iLD.mjs";import{n as WebRequestShim,r as WebResponseDriver,t as compileWebRoute}from"./handler-BKqb8K_H.mjs";function createWebApp(options){let h3mod=options.h3;if(typeof h3mod?.H3!=`function`)throw Error(`@forinda/kickjs/web: pass the h3 v2 module — \`createWebApp({ h3: await import("h3"), ... })\`. The installed h3 must be v2 (npm dist-tag 'latest'); v1 has no H3 class.`);let container=Container.getInstance();if(options.env){let env=options.env;Container._envResolver=key=>env[key]}Container._requestStoreProvider=()=>requestStore.getStore()??null;let registry=new MutableModuleRegistry;for(let m of options.modules)registry.mount(m);let modules=registry.entries.map(entry=>{let mod=typeof entry==`function`?new entry:entry;return mod.register?.(container),mod});container.bootstrap();let app=new h3mod.H3,apiPrefix=options.apiPrefix??`/api`,defaultVersion=options.defaultVersion??1,globalMiddleware=options.middleware??[],globalSources=(options.contributors??[]).map(registration=>({source:`global`,registration,label:`createWebApp`})),seenRoutes=new Map;for(let mod of modules){let declaredName=mod.name,moduleLabel=typeof declaredName==`string`&&declaredName.length>0?declaredName:mod.constructor?.name??`module`;_setExternalContributorSources([...(mod.contributors?.()??[]).map(registration=>({source:`module`,registration,label:moduleLabel})),...globalSources]);try{let result=mod.routes();if(!result)continue;let routeSets=Array.isArray(result)?result:[result];for(let route of routeSets){if(!route.controller)throw Error(`@forinda/kickjs/web: module route at '${route.path}' has no controller. Express-style \`router\` mounts are not supported on the web entry.`);let mountPath=`${apiPrefix}/v${route.version??defaultVersion}${normalizePath(route.path)}`;for(let entry of buildRouteTable(route.controller)){let url=joinPath(mountPath,entry.path),owner=`${route.controller.name??`controller`}.${String(entry.meta.handlerName??`?`)}`;assertRouteUnique(seenRoutes,entry.method,url,owner),globalMiddleware.length>0&&(entry.middlewares=[...globalMiddleware,...entry.middlewares]);let run=compileWebRoute(entry);app.on(entry.method,url,event=>run({request:event.req,url:event.url,params:event.context.params??{}}))}}}finally{_setExternalContributorSources([])}}return{fetch:request=>app.fetch(request),h3:app}}function createFetchHandler(build){let app;return{fetch:(request,env={})=>(app??=createWebApp(build(env)),app.fetch(request))}}function joinPath(mountPath,path){let a=mountPath.endsWith(`/`)?mountPath.slice(0,-1):mountPath;return path===`/`||path===``?a===``?`/`:a:`${a}${path.startsWith(`/`)?path:`/${path}`}`}export{KvRateLimitStore,KvSessionStore,WebRequestShim,WebResponseDriver,compileWebRoute,createFetchHandler,createWebApp,isReply,rateLimitGuard,reply};
12
12
  //# sourceMappingURL=web.mjs.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forinda/kickjs",
3
- "version": "6.6.1",
3
+ "version": "7.0.0",
4
4
  "description": "A production-grade, decorator-driven Node.js framework for TypeScript — runs on Express, Fastify, or h3 (swap the engine in one line).",
5
5
  "keywords": [
6
6
  "kickjs",
@@ -190,7 +190,7 @@
190
190
  "build": "tsdown",
191
191
  "dev": "tsdown --watch",
192
192
  "test": "vitest run --passWithNoTests",
193
- "typecheck": "tsc --noEmit",
193
+ "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.typetests.json",
194
194
  "clean": "rm -rf dist .wireit"
195
195
  }
196
196
  }
@@ -1 +0,0 @@
1
- {"version":3,"file":"app-module-DtoySG6l.d.mts","names":[],"sources":["../src/core/execution-context.ts","../src/core/context-decorator.ts","../src/core/app-module.ts"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"bootstrap-Nzgi7K2d.d.mts","names":[],"sources":["../src/http/bootstrap.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;iBAqCsB,UAAU,SAAS,qBAAqB,QAAQ"}