@forinda/kickjs 8.6.0 → 9.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.
- package/README.md +27 -164
- package/dist/{adapter-2bnoYEPX.d.mts → adapter-DhjoIYVI.d.mts} +122 -106
- package/dist/adapter-DhjoIYVI.d.mts.map +1 -0
- package/dist/application-BnFA6J97.mjs +12 -0
- package/dist/application-BnFA6J97.mjs.map +1 -0
- package/dist/{application-_QOiAVx2.d.mts → application-DqD58rhF.d.mts} +104 -15
- package/dist/application-DqD58rhF.d.mts.map +1 -0
- package/dist/bootstrap-K1PcxSTg.mjs +12 -0
- package/dist/bootstrap-K1PcxSTg.mjs.map +1 -0
- package/dist/cache-C0qL0NSD.d.mts.map +1 -1
- package/dist/{client-ip-DF_smNX7.mjs → client-ip-DAnYuS54.mjs} +2 -2
- package/dist/{client-ip-DF_smNX7.mjs.map → client-ip-DAnYuS54.mjs.map} +1 -1
- package/dist/config/index.d.mts +2 -2
- package/dist/config/index.mjs +2 -2
- package/dist/config-16yw-Mxk.mjs +12 -0
- package/dist/config-16yw-Mxk.mjs.map +1 -0
- package/dist/container-Dw45sa0Y.mjs +132 -0
- package/dist/container-Dw45sa0Y.mjs.map +1 -0
- package/dist/context-DlIdXA6I.mjs +12 -0
- package/dist/context-DlIdXA6I.mjs.map +1 -0
- package/dist/{context-errors-Ch0DuUQo.mjs → context-errors-BqOFD7UE.mjs} +2 -2
- package/dist/{context-errors-Ch0DuUQo.mjs.map → context-errors-BqOFD7UE.mjs.map} +1 -1
- package/dist/{contributor-runner-DujKZy4N.mjs → contributor-runner-DLvBA96K.mjs} +3 -3
- package/dist/{contributor-runner-DujKZy4N.mjs.map → contributor-runner-DLvBA96K.mjs.map} +1 -1
- package/dist/core/adapter.d.mts +2 -2
- package/dist/core/adapter.mjs +1 -1
- package/dist/core/app-module.mjs +1 -1
- package/dist/core/cache.mjs +2 -2
- package/dist/core/cache.mjs.map +1 -1
- package/dist/core/container.mjs +2 -2
- package/dist/core/cron.d.mts +2 -2
- package/dist/core/cron.mjs +2 -2
- package/dist/core/cron.mjs.map +1 -1
- package/dist/core/decorators.d.mts +1 -1
- package/dist/core/decorators.mjs +2 -2
- package/dist/core/decorators.mjs.map +1 -1
- package/dist/core/errors.mjs +1 -1
- package/dist/core/interfaces.mjs +1 -1
- package/dist/core/logger.mjs +1 -1
- package/dist/core/path.mjs +1 -1
- package/dist/core/plugin.d.mts +1 -1
- package/dist/core/plugin.mjs +1 -1
- package/dist/core/reactivity.mjs +1 -1
- package/dist/core-CCwlN0Gn.mjs +12 -0
- package/dist/{core-y8QbzHM5.mjs.map → core-CCwlN0Gn.mjs.map} +1 -1
- package/dist/cron-BTGytNg-.d.mts +122 -0
- package/dist/cron-BTGytNg-.d.mts.map +1 -0
- package/dist/{csrf-RP5hgc0c.mjs → csrf-B8CGeOlP.mjs} +3 -3
- package/dist/csrf-B8CGeOlP.mjs.map +1 -0
- package/dist/{csrf-C4MwaMp4.d.mts → csrf-COFjRrUz.d.mts} +2 -2
- package/dist/{csrf-C4MwaMp4.d.mts.map → csrf-COFjRrUz.d.mts.map} +1 -1
- package/dist/{decorate-D85rEnU-.mjs → decorate-CahyjuWW.mjs} +1 -1
- package/dist/{decorators-mg0qJ6Qc.d.mts → decorators-DOcWsXci.d.mts} +2 -2
- package/dist/{decorators-mg0qJ6Qc.d.mts.map → decorators-DOcWsXci.d.mts.map} +1 -1
- package/dist/{define-adapter-C1Db6xfO.mjs → define-adapter-Ckc2P-wH.mjs} +2 -2
- package/dist/{define-adapter-C1Db6xfO.mjs.map → define-adapter-Ckc2P-wH.mjs.map} +1 -1
- package/dist/{disposables-CEAxve3A.mjs → disposables-DnvT8DXr.mjs} +2 -2
- package/dist/{disposables-CEAxve3A.mjs.map → disposables-DnvT8DXr.mjs.map} +1 -1
- package/dist/{env-BelmDj19.mjs → env-DMAdjj8Z.mjs} +4 -4
- package/dist/env-DMAdjj8Z.mjs.map +1 -0
- package/dist/{error-handler-C1uWVGVY.d.mts → error-handler-DiueQHes.d.mts} +2 -2
- package/dist/{error-handler-C1uWVGVY.d.mts.map → error-handler-DiueQHes.d.mts.map} +1 -1
- package/dist/{handler-DHlakr0v.mjs → handler-2CbWK63p.mjs} +3 -3
- package/dist/handler-2CbWK63p.mjs.map +1 -0
- package/dist/http/application.d.mts +2 -2
- package/dist/http/application.mjs +2 -2
- package/dist/http/bootstrap.d.mts +1 -1
- package/dist/http/bootstrap.mjs +2 -2
- package/dist/http/context.d.mts +2 -2
- package/dist/http/context.mjs +2 -2
- package/dist/http/middleware/cors.mjs +1 -1
- package/dist/http/middleware/csrf.d.mts +1 -1
- package/dist/http/middleware/csrf.mjs +2 -2
- package/dist/http/middleware/error-handler.d.mts +1 -1
- package/dist/http/middleware/error-handler.mjs +2 -2
- package/dist/http/middleware/error-handler.mjs.map +1 -1
- package/dist/http/middleware/helmet.mjs +1 -1
- package/dist/http/middleware/rate-limit.mjs +2 -2
- package/dist/http/middleware/request-id.mjs +1 -1
- package/dist/http/middleware/request-logger.mjs +2 -2
- package/dist/http/middleware/session.d.mts +1 -1
- package/dist/http/middleware/session.mjs +2 -2
- package/dist/http/middleware/session.mjs.map +1 -1
- package/dist/http/middleware/spa.d.mts +1 -1
- package/dist/http/middleware/spa.mjs +2 -2
- package/dist/http/middleware/upload.d.mts +1 -1
- package/dist/http/middleware/upload.mjs +3 -3
- package/dist/http/middleware/upload.mjs.map +1 -1
- package/dist/http/middleware/validate.mjs +1 -1
- package/dist/http/middleware/views.d.mts +1 -1
- package/dist/http/middleware/views.mjs +2 -2
- package/dist/http/query/index.mjs +2 -2
- package/dist/http/router-builder.d.mts +1 -1
- package/dist/http/router-builder.mjs +2 -2
- package/dist/http/runtimes/fastify.d.mts +1 -1
- package/dist/http/runtimes/fastify.d.mts.map +1 -1
- package/dist/http/runtimes/fastify.mjs +3 -3
- package/dist/http/runtimes/fastify.mjs.map +1 -1
- package/dist/http/runtimes/h3-web.d.mts +1 -1
- package/dist/http/runtimes/h3-web.d.mts.map +1 -1
- package/dist/http/runtimes/h3-web.mjs +2 -2
- package/dist/http/runtimes/h3-web.mjs.map +1 -1
- package/dist/http/runtimes/h3.d.mts +1 -6
- package/dist/http/runtimes/h3.d.mts.map +1 -1
- package/dist/http/runtimes/h3.mjs +2 -2
- package/dist/http/runtimes/h3.mjs.map +1 -1
- package/dist/{index-Btz1W0X5.d.mts → index-CDn-p3tq.d.mts} +27 -18
- package/dist/index-CDn-p3tq.d.mts.map +1 -0
- package/dist/index.d.mts +12 -12
- package/dist/index.mjs +2 -2
- package/dist/index.mjs.map +1 -1
- package/dist/jobs-C8_ESbx0.mjs +12 -0
- package/dist/jobs-C8_ESbx0.mjs.map +1 -0
- package/dist/{kv-stores-DUFcps01.mjs → kv-stores-BhUmirED.mjs} +3 -3
- package/dist/{kv-stores-DUFcps01.mjs.map → kv-stores-BhUmirED.mjs.map} +1 -1
- package/dist/{kv-stores-Dx5RvIaP.d.mts → kv-stores-CetFF9N_.d.mts} +135 -5
- package/dist/kv-stores-CetFF9N_.d.mts.map +1 -0
- package/dist/{metadata-B8_KXybT.mjs → metadata-BQG1MmqL.mjs} +2 -2
- package/dist/{metadata-B8_KXybT.mjs.map → metadata-BQG1MmqL.mjs.map} +1 -1
- package/dist/{module-registry-BFrJ1PQ4.mjs → module-registry-BXnvRtH9.mjs} +2 -2
- package/dist/{module-registry-BFrJ1PQ4.mjs.map → module-registry-BXnvRtH9.mjs.map} +1 -1
- package/dist/observers-BI8H_g45.mjs +12 -0
- package/dist/observers-BI8H_g45.mjs.map +1 -0
- package/dist/{query-CC7o6I5K.mjs → query-CRTeiKUv.mjs} +2 -2
- package/dist/{query-CC7o6I5K.mjs.map → query-CRTeiKUv.mjs.map} +1 -1
- package/dist/{respond-B-EDymxF.mjs → respond-BAxhPjRj.mjs} +2 -2
- package/dist/{respond-B-EDymxF.mjs.map → respond-BAxhPjRj.mjs.map} +1 -1
- package/dist/route-flag-B1lQht6v.d.mts.map +1 -1
- package/dist/{route-flag-B4sNP7zH.mjs → route-flag-GipNvQ4O.mjs} +3 -3
- package/dist/route-flag-GipNvQ4O.mjs.map +1 -0
- package/dist/{route-policy-CDlMbl1b.mjs → route-policy-DtGR4sar.mjs} +2 -2
- package/dist/route-policy-DtGR4sar.mjs.map +1 -0
- package/dist/{router-builder-C6gmK0nZ.d.mts → router-builder-BSBSYgYW.d.mts} +3 -3
- package/dist/{router-builder-C6gmK0nZ.d.mts.map → router-builder-BSBSYgYW.d.mts.map} +1 -1
- package/dist/{router-builder-cTsA2XtO.mjs → router-builder-C2nnS0VJ.mjs} +3 -3
- package/dist/{router-builder-cTsA2XtO.mjs.map → router-builder-C2nnS0VJ.mjs.map} +1 -1
- package/dist/{runtime-C-7Yhs7a.mjs → runtime-DGzigyO2.mjs} +3 -3
- package/dist/runtime-DGzigyO2.mjs.map +1 -0
- package/dist/{session-BHgRyfqZ.d.mts → session-4Tb6q0Ny.d.mts} +11 -1
- package/dist/session-4Tb6q0Ny.d.mts.map +1 -0
- package/dist/{upload-TW2ZuECF.d.mts → upload-DjhnyHHm.d.mts} +3 -15
- package/dist/{upload-TW2ZuECF.d.mts.map → upload-DjhnyHHm.d.mts.map} +1 -1
- package/dist/{upload-config-CoE6BzM4.mjs → upload-config-CcoZp7xA.mjs} +2 -2
- package/dist/{upload-config-CoE6BzM4.mjs.map → upload-config-CcoZp7xA.mjs.map} +1 -1
- package/dist/web.d.mts +39 -7
- package/dist/web.d.mts.map +1 -1
- package/dist/web.mjs +2 -2
- package/dist/web.mjs.map +1 -1
- package/package.json +15 -10
- package/dist/adapter-2bnoYEPX.d.mts.map +0 -1
- package/dist/application-DZiG1QCw.mjs +0 -12
- package/dist/application-DZiG1QCw.mjs.map +0 -1
- package/dist/application-_QOiAVx2.d.mts.map +0 -1
- package/dist/bootstrap-BEbka3UV.mjs +0 -12
- package/dist/bootstrap-BEbka3UV.mjs.map +0 -1
- package/dist/config-CIOXZmLS.mjs +0 -12
- package/dist/config-CIOXZmLS.mjs.map +0 -1
- package/dist/container-DF8FRoY8.mjs +0 -117
- package/dist/container-DF8FRoY8.mjs.map +0 -1
- package/dist/context-BTqeZH1A.mjs +0 -12
- package/dist/context-BTqeZH1A.mjs.map +0 -1
- package/dist/core-y8QbzHM5.mjs +0 -12
- package/dist/cron-DOOtTY4q.d.mts +0 -30
- package/dist/cron-DOOtTY4q.d.mts.map +0 -1
- package/dist/csrf-RP5hgc0c.mjs.map +0 -1
- package/dist/env-BelmDj19.mjs.map +0 -1
- package/dist/handler-DHlakr0v.mjs.map +0 -1
- package/dist/index-Btz1W0X5.d.mts.map +0 -1
- package/dist/kv-stores-Dx5RvIaP.d.mts.map +0 -1
- package/dist/route-flag-B4sNP7zH.mjs.map +0 -1
- package/dist/route-policy-CDlMbl1b.mjs.map +0 -1
- package/dist/runtime-C-7Yhs7a.mjs.map +0 -1
- package/dist/session-BHgRyfqZ.d.mts.map +0 -1
package/dist/core/cache.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cache.mjs","names":[],"sources":["../../src/core/cache.ts"],"sourcesContent":["import 'reflect-metadata'\n\n// String metadata key (post-Symbol migration). Slash-delimited\n// `kick/cacheable` so it can't collide with adopter metadata keys.\nconst CACHEABLE_META = 'kick/cacheable'\n\n// ── CacheProvider Interface ─────────────────────────────────────────────\n\n/**\n * Abstract cache backend. Implement this interface to use Redis, Memcached,\n * or any other cache store with @Cacheable and @CacheEvict.\n *\n * @example\n * ```ts\n * class RedisCacheProvider implements CacheProvider {\n * private client: RedisClient\n * constructor(client: RedisClient) { this.client = client }\n * async get(key: string) {\n * const val = await this.client.get(key)\n * return val ? JSON.parse(val) : null\n * }\n * async set(key: string, value: any, ttlMs: number) {\n * await this.client.set(key, JSON.stringify(value), 'PX', ttlMs)\n * }\n * async del(key: string) { await this.client.del(key) }\n * async delByPrefix(prefix: string) {\n * const keys = await this.client.keys(prefix + '*')\n * if (keys.length) await this.client.del(...keys)\n * }\n * }\n * ```\n */\nexport interface CacheProvider {\n /** Retrieve a cached value. Return null if not found or expired. */\n get(key: string): Promise<any | null>\n /** Store a value with a TTL in milliseconds. */\n set(key: string, value: any, ttlMs: number): Promise<void>\n /** Delete a specific cache key. */\n del(key: string): Promise<void>\n /** Delete all keys matching a prefix. Used by @CacheEvict. */\n delByPrefix?(prefix: string): Promise<void>\n /** Optional cleanup on shutdown. */\n shutdown?(): Promise<void>\n}\n\n// ── Built-in Memory Provider ────────────────────────────────────────────\n\n/**\n * Default in-memory cache provider using a Map with LRU eviction.\n * Suitable for development and single-instance deployments.\n * For multi-instance or production, use a Redis-backed provider.\n *\n * Bounded: at most `maxEntries` (default 10 000) live entries. Expiry is\n * lazy (checked on read); the size cap is what bounds memory — without it,\n * a `@Cacheable` method over high-cardinality args grew the Map without\n * limit, since write-once-never-read keys were never revisited.\n */\nexport class MemoryCacheProvider implements CacheProvider {\n private store = new Map<string, { data: any; expiresAt: number }>()\n\n constructor(private readonly maxEntries = 10_000) {}\n\n async get(key: string): Promise<any | null> {\n const entry = this.store.get(key)\n if (!entry) return null\n if (entry.expiresAt <= Date.now()) {\n this.store.delete(key)\n return null\n }\n // LRU touch: re-insert so Map iteration order tracks recency and\n // eviction removes the least-recently-used key, not the oldest write.\n this.store.delete(key)\n this.store.set(key, entry)\n return entry.data\n }\n\n async set(key: string, value: any, ttlMs: number): Promise<void> {\n if (!this.store.has(key) && this.store.size >= this.maxEntries) {\n // Evict the least-recently-used entry (first in iteration order).\n const oldest = this.store.keys().next().value\n if (oldest !== undefined) this.store.delete(oldest)\n }\n this.store.delete(key)\n this.store.set(key, { data: value, expiresAt: Date.now() + ttlMs })\n }\n\n async del(key: string): Promise<void> {\n this.store.delete(key)\n }\n\n async delByPrefix(prefix: string): Promise<void> {\n for (const key of this.store.keys()) {\n if (key.startsWith(prefix)) {\n this.store.delete(key)\n }\n }\n }\n\n async shutdown(): Promise<void> {\n this.store.clear()\n }\n}\n\n// ── Global Cache Registry ───────────────────────────────────────────────\n\nlet _cacheProvider: CacheProvider = new MemoryCacheProvider()\n\n/**\n * Set the global cache provider used by @Cacheable and @CacheEvict.\n * Call this before bootstrapping your app.\n *\n * @example\n * ```ts\n * import { setCacheProvider } from '@forinda/kickjs'\n * import { RedisCacheProvider } from './redis-cache'\n *\n * setCacheProvider(new RedisCacheProvider(redisClient))\n * ```\n */\nexport function setCacheProvider(provider: CacheProvider): void {\n _cacheProvider = provider\n}\n\n/** Get the current cache provider */\nexport function getCacheProvider(): CacheProvider {\n return _cacheProvider\n}\n\n// ── Cache Options ───────────────────────────────────────────────────────\n\nexport interface CacheOptions {\n /** Time-to-live in seconds (default: 60) */\n ttl?: number\n /** Cache key prefix (default: ClassName.methodName) */\n key?: string\n}\n\n// ── @Cacheable Decorator ────────────────────────────────────────────────\n\n/**\n * Cache the return value of a method for the specified TTL.\n * Uses the globally registered CacheProvider (default: MemoryCacheProvider).\n *\n * Call `setCacheProvider()` to use Redis or any custom backend.\n *\n * @param ttl - Time-to-live in seconds (default: 60)\n * @param options.key - Custom cache key prefix\n */\n// Sentinel stored in place of a legitimate `null` result. Providers signal\n// \"miss\" with null, so caching null directly was indistinguishable from a\n// miss — the method re-executed on every call (cache stampede on the exact\n// path caching should protect). An object envelope (structurally checked,\n// so it survives any JSON round-trip through Redis etc.) rather than a bare\n// string: a method legitimately RETURNING that string (echoing user input,\n// etc.) must not be replayed as null. A user value could still forge this\n// exact single-key shape, but that requires deliberately constructing it —\n// not a value class that occurs by accident.\nconst CACHED_NULL = { __kickjs_cached_null__: true }\n\nfunction isCachedNull(value: unknown): boolean {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as Record<string, unknown>).__kickjs_cached_null__ === true &&\n Object.keys(value).length === 1\n )\n}\n\nexport function Cacheable(ttl?: number, options?: { key?: string }): MethodDecorator {\n return (_target, propertyKey, descriptor: PropertyDescriptor) => {\n const original = descriptor.value\n const cacheTtl = (ttl ?? 60) * 1000\n const keyPrefix = options?.key ?? String(propertyKey)\n\n descriptor.value = async function (...args: any[]) {\n const cacheKey = `${keyPrefix}:${JSON.stringify(args)}`\n const provider = getCacheProvider()\n\n const cached = await provider.get(cacheKey)\n if (cached !== null) {\n return isCachedNull(cached) ? null : cached\n }\n\n const result = await original.apply(this, args)\n // Cache legit-null results via the sentinel; `undefined` stays\n // uncached (callers returning undefined get pre-existing behavior).\n if (result === null) await provider.set(cacheKey, CACHED_NULL, cacheTtl)\n else if (result !== undefined) await provider.set(cacheKey, result, cacheTtl)\n return result\n }\n\n return descriptor\n }\n}\n\n// ── @CacheEvict Decorator ───────────────────────────────────────────────\n\n/**\n * Evict cached values matching a key prefix after the method executes.\n * Works with any CacheProvider that implements `delByPrefix`.\n *\n * @param key - Cache key prefix to evict (matches the @Cacheable key)\n */\nexport function CacheEvict(key: string): MethodDecorator {\n return (_target, _propertyKey, descriptor: PropertyDescriptor) => {\n const original = descriptor.value\n\n descriptor.value = async function (...args: any[]) {\n const result = await original.apply(this, args)\n const provider = getCacheProvider()\n if (provider.delByPrefix) {\n await provider.delByPrefix(key)\n }\n return result\n }\n\n return descriptor\n }\n}\n\nexport { CACHEABLE_META }\n"],"mappings":";;;;;;;;;;yBAIA,MAAM,eAAiB,iBAqDvB,IAAa,oBAAb,KAA0D,CAG3B,WAF7B,MAAgB,IAAI,IAEpB,YAAY,WAA8B,IAAQ,CAArB,KAAA,WAAA,UAAsB,CAEnD,MAAM,IAAI,IAAkC,CAC1C,IAAM,MAAQ,KAAK,MAAM,IAAI,GAAG,EAUhC,OATK,MACD,MAAM,WAAa,KAAK,IAAI,GAC9B,KAAK,MAAM,OAAO,GAAG,EACd,OAIT,KAAK,MAAM,OAAO,GAAG,EACrB,KAAK,MAAM,IAAI,IAAK,KAAK,EAClB,MAAM,MATM,IAUrB,CAEA,MAAM,IAAI,IAAa,MAAY,MAA8B,CAC/D,GAAI,CAAC,KAAK,MAAM,IAAI,GAAG,GAAK,KAAK,MAAM,MAAQ,KAAK,WAAY,CAE9D,IAAM,OAAS,KAAK,MAAM,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,MACpC,SAAW,IAAA,IAAW,KAAK,MAAM,OAAO,MAAM,CACpD,CACA,KAAK,MAAM,OAAO,GAAG,EACrB,KAAK,MAAM,IAAI,IAAK,CAAE,KAAM,MAAO,UAAW,KAAK,IAAI,EAAI,KAAM,CAAC,CACpE,CAEA,MAAM,IAAI,IAA4B,CACpC,KAAK,MAAM,OAAO,GAAG,CACvB,CAEA,MAAM,YAAY,OAA+B,CAC/C,IAAK,IAAM,OAAO,KAAK,MAAM,KAAK,EAC5B,IAAI,WAAW,MAAM,GACvB,KAAK,MAAM,OAAO,GAAG,CAG3B,CAEA,MAAM,UAA0B,CAC9B,KAAK,MAAM,MAAM,CACnB,CACF,EAIA,IAAI,eAAgC,IAAI,oBAcxC,SAAgB,iBAAiB,SAA+B,CAC9D,eAAiB,QACnB,CAGA,SAAgB,kBAAkC,CAChD,OAAO,cACT,CA+BA,MAAM,YAAc,CAAE,uBAAwB,EAAK,EAEnD,SAAS,aAAa,MAAyB,CAC7C,OACE,OAAO,OAAU,YACjB,OACC,MAAkC,yBAA2B,IAC9D,OAAO,KAAK,KAAK,CAAC,CAAC,SAAW,CAElC,CAEA,SAAgB,UAAU,IAAc,QAA6C,CACnF,OAAQ,QAAS,YAAa,aAAmC,CAC/D,IAAM,SAAW,WAAW,MACtB,UAAY,KAAO,IAAM,IACzB,UAAY,SAAS,KAAO,OAAO,WAAW,EAmBpD,MAjBA,YAAW,MAAQ,eAAgB,GAAG,KAAa,CACjD,IAAM,SAAW,GAAG,UAAU,GAAG,KAAK,UAAU,IAAI,IAC9C,SAAW,iBAAiB,EAE5B,OAAS,MAAM,SAAS,IAAI,QAAQ,EAC1C,GAAI,SAAW,KACb,OAAO,aAAa,MAAM,EAAI,KAAO,OAGvC,IAAM,OAAS,MAAM,SAAS,MAAM,KAAM,IAAI,EAK9C,OAFI,SAAW,KAAM,MAAM,SAAS,IAAI,SAAU,YAAa,QAAQ,EAC9D,SAAW,IAAA,IAAW,MAAM,SAAS,IAAI,SAAU,OAAQ,QAAQ,EACrE,MACT,EAEO,UACT,CACF,CAUA,SAAgB,WAAW,IAA8B,CACvD,OAAQ,QAAS,aAAc,aAAmC,CAChE,IAAM,SAAW,WAAW,MAW5B,MATA,YAAW,MAAQ,eAAgB,GAAG,KAAa,CACjD,IAAM,OAAS,MAAM,SAAS,MAAM,KAAM,IAAI,EACxC,SAAW,iBAAiB,EAIlC,OAHI,SAAS,aACX,MAAM,SAAS,YAAY,GAAG,EAEzB,MACT,EAEO,UACT,CACF"}
|
|
1
|
+
{"version":3,"file":"cache.mjs","names":[],"sources":["../../src/core/cache.ts"],"sourcesContent":["import 'reflect-metadata'\n\n// String metadata key (post-Symbol migration). Slash-delimited\n// `kick/cacheable` so it can't collide with adopter metadata keys.\nconst CACHEABLE_META = 'kick/cacheable'\n\n// ── CacheProvider Interface ─────────────────────────────────────────────\n\n/**\n * Abstract cache backend. Implement this interface to use Redis, Memcached,\n * or any other cache store with @Cacheable and @CacheEvict.\n *\n * @example\n * ```ts\n * class RedisCacheProvider implements CacheProvider {\n * private client: RedisClient\n * constructor(client: RedisClient) { this.client = client }\n * async get(key: string) {\n * const val = await this.client.get(key)\n * return val ? JSON.parse(val) : null\n * }\n * async set(key: string, value: any, ttlMs: number) {\n * await this.client.set(key, JSON.stringify(value), 'PX', ttlMs)\n * }\n * async del(key: string) { await this.client.del(key) }\n * async delByPrefix(prefix: string) {\n * const keys = await this.client.keys(prefix + '*')\n * if (keys.length) await this.client.del(...keys)\n * }\n * }\n * ```\n */\nexport interface CacheProvider {\n /** Retrieve a cached value. Return null if not found or expired. */\n get(key: string): Promise<any | null>\n /** Store a value with a TTL in milliseconds. */\n set(key: string, value: any, ttlMs: number): Promise<void>\n /** Delete a specific cache key. */\n del(key: string): Promise<void>\n /** Delete all keys matching a prefix. Used by @CacheEvict. */\n delByPrefix?(prefix: string): Promise<void>\n /** Optional cleanup on shutdown. */\n shutdown?(): Promise<void>\n}\n\n// ── Built-in Memory Provider ────────────────────────────────────────────\n\n/**\n * Default in-memory cache provider using a Map with LRU eviction.\n * Suitable for development and single-instance deployments.\n * For multi-instance or production, use a Redis-backed provider.\n *\n * Bounded: at most `maxEntries` (default 10 000) live entries. Expiry is\n * lazy (checked on read); the size cap is what bounds memory — without it,\n * a `@Cacheable` method over high-cardinality args grew the Map without\n * limit, since write-once-never-read keys were never revisited.\n */\nexport class MemoryCacheProvider implements CacheProvider {\n private store = new Map<string, { data: any; expiresAt: number }>()\n\n constructor(private readonly maxEntries = 10_000) {}\n\n async get(key: string): Promise<any | null> {\n const entry = this.store.get(key)\n if (!entry) return null\n if (entry.expiresAt <= Date.now()) {\n this.store.delete(key)\n return null\n }\n // LRU touch: re-insert so Map iteration order tracks recency and\n // eviction removes the least-recently-used key, not the oldest write.\n this.store.delete(key)\n this.store.set(key, entry)\n return entry.data\n }\n\n async set(key: string, value: any, ttlMs: number): Promise<void> {\n if (!this.store.has(key) && this.store.size >= this.maxEntries) {\n // Evict the least-recently-used entry (first in iteration order).\n const oldest = this.store.keys().next().value\n if (oldest !== undefined) this.store.delete(oldest)\n }\n this.store.delete(key)\n this.store.set(key, { data: value, expiresAt: Date.now() + ttlMs })\n }\n\n async del(key: string): Promise<void> {\n this.store.delete(key)\n }\n\n async delByPrefix(prefix: string): Promise<void> {\n for (const key of this.store.keys()) {\n if (key.startsWith(prefix)) {\n this.store.delete(key)\n }\n }\n }\n\n async shutdown(): Promise<void> {\n this.store.clear()\n }\n}\n\n// ── Global Cache Registry ───────────────────────────────────────────────\n\nlet _cacheProvider: CacheProvider = new MemoryCacheProvider()\n\n/**\n * Set the global cache provider used by @Cacheable and @CacheEvict.\n * Call this before bootstrapping your app.\n *\n * @example\n * ```ts\n * import { setCacheProvider } from '@forinda/kickjs'\n * import { RedisCacheProvider } from './redis-cache'\n *\n * setCacheProvider(new RedisCacheProvider(redisClient))\n * ```\n */\nexport function setCacheProvider(provider: CacheProvider): void {\n _cacheProvider = provider\n}\n\n/** Get the current cache provider */\nexport function getCacheProvider(): CacheProvider {\n return _cacheProvider\n}\n\n// ── Cache Options ───────────────────────────────────────────────────────\n\nexport interface CacheOptions {\n /** Time-to-live in seconds (default: 60) */\n ttl?: number\n /** Cache key prefix (default: ClassName.methodName) */\n key?: string\n}\n\n// ── @Cacheable Decorator ────────────────────────────────────────────────\n\n/**\n * Cache the return value of a method for the specified TTL.\n * Uses the globally registered CacheProvider (default: MemoryCacheProvider).\n *\n * Call `setCacheProvider()` to use Redis or any custom backend.\n *\n * @param ttl - Time-to-live in seconds (default: 60)\n * @param options.key - Custom cache key prefix\n */\n// Sentinel stored in place of a legitimate `null` result. Providers signal\n// \"miss\" with null, so caching null directly was indistinguishable from a\n// miss — the method re-executed on every call (cache stampede on the exact\n// path caching should protect). An object envelope (structurally checked,\n// so it survives any JSON round-trip through Redis etc.) rather than a bare\n// string: a method legitimately RETURNING that string (echoing user input,\n// etc.) must not be replayed as null. A user value could still forge this\n// exact single-key shape, but that requires deliberately constructing it —\n// not a value class that occurs by accident.\nconst CACHED_NULL = { __kickjs_cached_null__: true }\n\nfunction isCachedNull(value: unknown): boolean {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as Record<string, unknown>).__kickjs_cached_null__ === true &&\n Object.keys(value).length === 1\n )\n}\n\n/**\n * Loads in flight, by cache key — simultaneous misses on one key share a\n * single call instead of each running the method (a cache stampede).\n * Per process: instances sharing a remote cache still each load once.\n */\nconst inFlight = new Map<string, Promise<unknown>>()\n\nexport function Cacheable(ttl?: number, options?: { key?: string }): MethodDecorator {\n return (target, propertyKey, descriptor: PropertyDescriptor) => {\n const original = descriptor.value\n const cacheTtl = (ttl ?? 60) * 1000\n // The default key keeps the method name FIRST — `@CacheEvict('findAll')`\n // is a prefix match and must keep reaching it — and adds the class after\n // it, so `UserService.findAll` and `PostService.findAll` no longer share\n // entries. The class NAME, not an identity, so every instance behind a\n // shared cache computes the same key. An explicit `key` is used as given:\n // the caller chose it, possibly to share it.\n const owner = typeof target === 'function' ? target.name : target.constructor?.name\n const keyPrefix = options?.key ?? `${String(propertyKey)}:${owner ?? 'anonymous'}`\n\n descriptor.value = async function (...args: any[]) {\n const cacheKey = `${keyPrefix}:${JSON.stringify(args)}`\n const provider = getCacheProvider()\n\n const cached = await provider.get(cacheKey)\n if (cached !== null) {\n return isCachedNull(cached) ? null : cached\n }\n\n const pending = inFlight.get(cacheKey)\n if (pending) return pending\n\n const load = (async () => {\n const result = await original.apply(this, args)\n // Cache legit-null results via the sentinel; `undefined` stays\n // uncached (callers returning undefined get pre-existing behavior).\n if (result === null) await provider.set(cacheKey, CACHED_NULL, cacheTtl)\n else if (result !== undefined) await provider.set(cacheKey, result, cacheTtl)\n return result\n })()\n inFlight.set(cacheKey, load)\n try {\n return await load\n } finally {\n // Settled either way: a failed load is not cached, so the next call retries.\n inFlight.delete(cacheKey)\n }\n }\n\n return descriptor\n }\n}\n\n// ── @CacheEvict Decorator ───────────────────────────────────────────────\n\n/**\n * Evict cached values matching a key prefix after the method executes.\n * Works with any CacheProvider that implements `delByPrefix`.\n *\n * @param key - Cache key prefix to evict (matches the @Cacheable key)\n */\nexport function CacheEvict(key: string): MethodDecorator {\n return (_target, _propertyKey, descriptor: PropertyDescriptor) => {\n const original = descriptor.value\n\n descriptor.value = async function (...args: any[]) {\n const result = await original.apply(this, args)\n const provider = getCacheProvider()\n if (provider.delByPrefix) {\n await provider.delByPrefix(key)\n }\n return result\n }\n\n return descriptor\n }\n}\n\nexport { CACHEABLE_META }\n"],"mappings":"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"}
|
package/dist/core/container.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs
|
|
2
|
+
* @forinda/kickjs v9.0.0
|
|
3
3
|
*
|
|
4
4
|
* Copyright (c) Felix Orinda
|
|
5
5
|
*
|
|
@@ -8,4 +8,4 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @license MIT
|
|
10
10
|
*/
|
|
11
|
-
import{n as tokenName,t as Container}from"../container-
|
|
11
|
+
import{n as tokenName,t as Container}from"../container-Dw45sa0Y.mjs";export{Container,tokenName};
|
package/dist/core/cron.d.mts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
|
|
2
|
-
import {
|
|
3
|
-
export { CRON_META, Cron, CronJobMeta, getCronJobs };
|
|
2
|
+
import { a as CronOptions, c as getCronJobs, d as runCronJob, f as runCronJobs, i as CronJobMeta, l as isCronJobEnabled, n as Cron, o as CronRun, r as CronJob, s as cronScheduleId, t as CRON_META, u as listCronJobs } from "../cron-BTGytNg-.mjs";
|
|
3
|
+
export { CRON_META, Cron, CronJob, CronJobMeta, CronOptions, CronRun, cronScheduleId, getCronJobs, isCronJobEnabled, listCronJobs, runCronJob, runCronJobs };
|
package/dist/core/cron.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs
|
|
2
|
+
* @forinda/kickjs v9.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{c as pushClassMeta,t as getClassMeta}from"../metadata-
|
|
11
|
+
import{createLogger}from"./logger.mjs";import{c as pushClassMeta,t as getClassMeta}from"../metadata-BQG1MmqL.mjs";import{r as reportError}from"../observers-BI8H_g45.mjs";const CRON_META=`kick/cron`,log=createLogger(`KickCron`),cronClasses=new Map;function Cron(expression,options){return(target,propertyKey)=>{pushClassMeta(CRON_META,target.constructor,{...options,expression,handlerName:propertyKey}),cronClasses.set(target.constructor.name,target.constructor)}}function getCronJobs(target){return getClassMeta(CRON_META,target,[])}function listCronJobs(container){let jobs=[];for(let target of cronClasses.values())if(container.has(target))for(let meta of getCronJobs(target))jobs.push({...meta,name:meta.name??`${target.name}.${meta.handlerName}`,scheduleId:cronScheduleId(meta.expression),target});return jobs}function isCronJobEnabled(job){let{enabled}=job;return typeof enabled==`function`?enabled():enabled!==!1}const running=new Set;async function runCronJob(job,container,trigger){if(!isCronJobEnabled(job))return!1;let key=`${job.target.name}.${job.handlerName}`;if(!job.overlap&&running.has(key))return log.warn(`${job.name} is still running — skipped this tick`),!1;running.add(key);try{return await container.resolve(job.target)[job.handlerName]({name:job.name,expression:job.expression,meta:job.meta??{},firedAt:new Date,trigger}),!0}catch(err){throw log.error({err},`Cron job ${job.name} failed`),reportError(err,{source:`cron`,context:{job:job.name,expression:job.expression,trigger}}),err}finally{running.delete(key)}}async function runCronJobs(container,filter,trigger){let jobs=listCronJobs(container).filter(job=>(filter.name===void 0||job.name===filter.name)&&(filter.scheduleId===void 0||job.scheduleId===filter.scheduleId)&&(filter.expression===void 0||job.expression===filter.expression)),results=await Promise.allSettled(jobs.map(job=>runCronJob(job,container,trigger)));return{ran:results.filter(r=>r.status===`fulfilled`&&r.value).length,failed:results.filter(r=>r.status===`rejected`).length}}function cronScheduleId(expression){let hash=2166136261;for(let char of expression.trim().replace(/\s+/g,` `))hash^=char.codePointAt(0),hash=Math.imul(hash,16777619)>>>0;return hash.toString(36)}export{CRON_META,Cron,cronScheduleId,getCronJobs,isCronJobEnabled,listCronJobs,runCronJob,runCronJobs};
|
|
12
12
|
//# sourceMappingURL=cron.mjs.map
|
package/dist/core/cron.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cron.mjs","names":[],"sources":["../../src/core/cron.ts"],"sourcesContent":["import { pushClassMeta, getClassMeta } from './metadata'\n\n// String metadata key (post-Symbol migration). Slash-delimited\n// `kick/cron` so it can't collide with adopter metadata keys.\nconst CRON_META = 'kick/cron'\n\nexport interface CronJobMeta {\n expression: string\n handlerName: string\n description?: string\n timezone?: string\n runOnInit?: boolean\n}\n\n// Cron format: minute hour day month weekday\n// Examples:\n// '* * * * *' - every minute\n// '0 * * * *' - every hour\n// '0 0 * * *' - daily at midnight\n// '0 9 * * MON-FRI' - weekdays at 9am\n// '0 0 1 * *' - first of each month\n\n/**\n * Schedule a method to run on a cron expression.\n * Requires a CronAdapter to be registered in bootstrap().\n *\n * @param expression - Standard 5-part cron: minute hour day month weekday.\n * Use forward-slash for intervals (e.g. every 5 min, every 2 hours).\n * @param options.description - Human-readable label for logging\n * @param options.timezone - IANA timezone (e.g. 'America/New_York', 'UTC')\n * @param options.runOnInit - Run immediately on startup before first scheduled tick\n */\nexport function Cron(\n expression: string,\n options?: { description?: string; timezone?: string; runOnInit?: boolean },\n): MethodDecorator {\n return (target, propertyKey) => {\n pushClassMeta<CronJobMeta>(CRON_META, target.constructor, {\n expression,\n handlerName: propertyKey as string,\n description: options?.description,\n timezone: options?.timezone,\n runOnInit: options?.runOnInit,\n })\n }\n}\n\n/** Read cron jobs registered on a class */\nexport function getCronJobs(target: any): CronJobMeta[] {\n return getClassMeta<CronJobMeta[]>(CRON_META, target, [])\n}\n\nexport { CRON_META }\n"],"mappings":";;;;;;;;;;2EAIA,MAAM,UAAY,YA4BlB,SAAgB,KACd,WACA,QACiB,CACjB,OAAQ,OAAQ,cAAgB,CAC9B,cAA2B,UAAW,OAAO,YAAa,CACxD,WACA,YAAa,YACb,YAAa,SAAS,YACtB,SAAU,SAAS,SACnB,UAAW,SAAS,SACtB,CAAC,CACH,CACF,CAGA,SAAgB,YAAY,OAA4B,CACtD,OAAO,aAA4B,UAAW,OAAQ,CAAC,CAAC,CAC1D"}
|
|
1
|
+
{"version":3,"file":"cron.mjs","names":[],"sources":["../../src/core/cron.ts"],"sourcesContent":["import { pushClassMeta, getClassMeta } from './metadata'\nimport { reportError } from './observers'\nimport { createLogger } from './logger'\n\n// String metadata key (post-Symbol migration). Slash-delimited\n// `kick/cron` so it can't collide with adopter metadata keys.\nconst CRON_META = 'kick/cron'\n\nconst log = createLogger('KickCron')\n\nexport interface CronOptions<Meta extends Record<string, unknown> = Record<string, unknown>> {\n /**\n * Stable job name for logs, error reports and `runCronJobs({ name })`.\n * Defaults to `ClassName.method`.\n */\n name?: string\n /** Human-readable label for logging */\n description?: string\n /** IANA timezone (e.g. 'Africa/Nairobi'). Node runner and Workers only — Vercel crons are UTC. */\n timezone?: string\n /** Run once at startup (Node server), before the first scheduled tick */\n runOnInit?: boolean\n /**\n * Allow a run to start while the previous one is still going. Default\n * `false`: a tick that finds the job busy is skipped, so a job slower than\n * its interval never stacks up.\n */\n overlap?: boolean\n /**\n * Turn the job off without removing the decorator — a boolean, or a function\n * checked on every tick (e.g. `() => process.env.JOBS === 'on'`).\n */\n enabled?: boolean | (() => boolean)\n /**\n * Free-form data for the job: arguments the handler reads from its run\n * context (`run.meta`), and anything a custom runner or the DevTools want\n * to know. Stored as-is with the job metadata.\n */\n meta?: Meta\n}\n\nexport interface CronJobMeta<\n Meta extends Record<string, unknown> = Record<string, unknown>,\n> extends CronOptions<Meta> {\n expression: string\n handlerName: string\n}\n\n/** What a `@Cron` handler receives when it runs. */\nexport interface CronRun<Meta extends Record<string, unknown> = Record<string, unknown>> {\n name: string\n expression: string\n meta: Meta\n firedAt: Date\n /** What started this run. */\n trigger: 'schedule' | 'init' | 'http' | 'workers' | 'manual'\n}\n\n// Cron format: minute hour day month weekday\n// Examples:\n// '* * * * *' - every minute\n// '0 * * * *' - every hour\n// '0 0 * * *' - daily at midnight\n// '0 9 * * MON-FRI' - weekdays at 9am\n// '0 0 1 * *' - first of each month\n\n/**\n * Every class that declares at least one `@Cron` job, keyed by class name like\n * the DI registry: an HMR re-evaluation replaces the old class instead of\n * leaving it to run beside the new one.\n */\nconst cronClasses = new Map<string, Function>()\n\n/**\n * Schedule a method to run on a cron expression.\n *\n * - On the Node server, `KickCronAdapter()` schedules it (opt-in; install the\n * optional peer `croner`). Without it — or with your own adapter — `@Cron`\n * only records metadata.\n * - On Vercel, `kick build:vercel` emits a cron entry that triggers it.\n * - On Cloudflare Workers, the web entry's `scheduled()` runs it.\n *\n * The handler receives a {@link CronRun} (name, expression, `meta`, trigger).\n *\n * @param expression - Standard 5-part cron: minute hour day month weekday.\n * @example\n * ```ts\n * @Cron('0 * * * *', { name: 'digest', meta: { batch: 500 } })\n * async sendDigest(run: CronRun<{ batch: number }>) {\n * await this.mailer.flush(run.meta.batch)\n * }\n * ```\n */\nexport function Cron<Meta extends Record<string, unknown> = Record<string, unknown>>(\n expression: string,\n options?: CronOptions<Meta>,\n): MethodDecorator {\n return (target, propertyKey) => {\n pushClassMeta<CronJobMeta<Meta>>(CRON_META, target.constructor, {\n ...options,\n expression,\n handlerName: propertyKey as string,\n })\n cronClasses.set(target.constructor.name, target.constructor)\n }\n}\n\n/** Read cron jobs registered on a class */\nexport function getCronJobs(target: any): CronJobMeta[] {\n return getClassMeta<CronJobMeta[]>(CRON_META, target, [])\n}\n\n/** One declared job, ready to run: its class, metadata and resolved name. */\nexport interface CronJob extends CronJobMeta {\n name: string\n /** {@link cronScheduleId} of the expression — how HTTP triggers address it. */\n scheduleId: string\n target: Function\n}\n\n/**\n * Every `@Cron` job whose class the container can resolve. Classes the\n * container doesn't know (not in a module, or left over from before an HMR\n * reload) are skipped.\n */\nexport function listCronJobs(container: { has(token: unknown): boolean }): CronJob[] {\n const jobs: CronJob[] = []\n for (const target of cronClasses.values()) {\n if (!container.has(target)) continue\n for (const meta of getCronJobs(target)) {\n jobs.push({\n ...meta,\n name: meta.name ?? `${target.name}.${meta.handlerName}`,\n scheduleId: cronScheduleId(meta.expression),\n target,\n })\n }\n }\n return jobs\n}\n\n/** Whether the job's `enabled` option currently allows it to run. */\nexport function isCronJobEnabled(job: Pick<CronJobMeta, 'enabled'>): boolean {\n const { enabled } = job\n return typeof enabled === 'function' ? enabled() : enabled !== false\n}\n\nconst running = new Set<string>()\n\n/**\n * Run one job now: resolve its class from the container and call the handler\n * with a {@link CronRun}. Skips a disabled job, and — unless `overlap: true` —\n * a job whose previous run hasn't finished. A failure is logged and reported to\n * the error observers (`source: 'cron'`), then rethrown for the caller.\n * Returns whether the job ran.\n */\nexport async function runCronJob(\n job: CronJob,\n container: { resolve(token: unknown): unknown },\n trigger: CronRun['trigger'],\n): Promise<boolean> {\n if (!isCronJobEnabled(job)) return false\n // Keyed by class + method: `name` is user-chosen and may repeat.\n const key = `${job.target.name}.${job.handlerName}`\n if (!job.overlap && running.has(key)) {\n log.warn(`${job.name} is still running — skipped this tick`)\n return false\n }\n running.add(key)\n try {\n const instance = container.resolve(job.target) as Record<string, (run: CronRun) => unknown>\n await instance[job.handlerName]({\n name: job.name,\n expression: job.expression,\n meta: job.meta ?? {},\n firedAt: new Date(),\n trigger,\n })\n return true\n } catch (err) {\n log.error({ err }, `Cron job ${job.name} failed`)\n reportError(err, {\n source: 'cron',\n context: { job: job.name, expression: job.expression, trigger },\n })\n throw err\n } finally {\n running.delete(key)\n }\n}\n\n/**\n * Run every job matching `filter` (by name, schedule id, or exact expression),\n * in parallel. Resolves once all have settled; never rejects — failures are\n * already reported. Returns how many ran and how many failed.\n */\nexport async function runCronJobs(\n container: { has(token: unknown): boolean; resolve(token: unknown): unknown },\n filter: { name?: string; scheduleId?: string; expression?: string },\n trigger: CronRun['trigger'],\n): Promise<{ ran: number; failed: number }> {\n const jobs = listCronJobs(container).filter(\n (job) =>\n (filter.name === undefined || job.name === filter.name) &&\n (filter.scheduleId === undefined || job.scheduleId === filter.scheduleId) &&\n (filter.expression === undefined || job.expression === filter.expression),\n )\n const results = await Promise.allSettled(jobs.map((job) => runCronJob(job, container, trigger)))\n return {\n ran: results.filter((r) => r.status === 'fulfilled' && r.value).length,\n failed: results.filter((r) => r.status === 'rejected').length,\n }\n}\n\n/**\n * A short, stable id for a cron expression (FNV-1a, base 36) — how HTTP\n * triggers address a schedule. Derived from the expression, not the class or\n * method name, because a minified serverless bundle may rename classes; the\n * CLI computes the same id from source at build time.\n */\nexport function cronScheduleId(expression: string): string {\n let hash = 0x811c9dc5\n for (const char of expression.trim().replace(/\\s+/g, ' ')) {\n hash ^= char.codePointAt(0)!\n hash = Math.imul(hash, 0x01000193) >>> 0\n }\n return hash.toString(36)\n}\n\nexport { CRON_META }\n"],"mappings":";;;;;;;;;;0KAMA,MAAM,UAAY,YAEZ,IAAM,aAAa,UAAU,EA+D7B,YAAc,IAAI,IAsBxB,SAAgB,KACd,WACA,QACiB,CACjB,OAAQ,OAAQ,cAAgB,CAC9B,cAAiC,UAAW,OAAO,YAAa,CAC9D,GAAG,QACH,WACA,YAAa,WACf,CAAC,EACD,YAAY,IAAI,OAAO,YAAY,KAAM,OAAO,WAAW,CAC7D,CACF,CAGA,SAAgB,YAAY,OAA4B,CACtD,OAAO,aAA4B,UAAW,OAAQ,CAAC,CAAC,CAC1D,CAeA,SAAgB,aAAa,UAAwD,CACnF,IAAM,KAAkB,CAAC,EACzB,IAAK,IAAM,UAAU,YAAY,OAAO,EACjC,aAAU,IAAI,MAAM,EACzB,IAAK,IAAM,QAAQ,YAAY,MAAM,EACnC,KAAK,KAAK,CACR,GAAG,KACH,KAAM,KAAK,MAAQ,GAAG,OAAO,KAAK,GAAG,KAAK,cAC1C,WAAY,eAAe,KAAK,UAAU,EAC1C,MACF,CAAC,EAGL,OAAO,IACT,CAGA,SAAgB,iBAAiB,IAA4C,CAC3E,GAAM,CAAE,SAAY,IACpB,OAAO,OAAO,SAAY,WAAa,QAAQ,EAAI,UAAY,EACjE,CAEA,MAAM,QAAU,IAAI,IASpB,eAAsB,WACpB,IACA,UACA,QACkB,CAClB,GAAI,CAAC,iBAAiB,GAAG,EAAG,MAAO,GAEnC,IAAM,IAAM,GAAG,IAAI,OAAO,KAAK,GAAG,IAAI,cACtC,GAAI,CAAC,IAAI,SAAW,QAAQ,IAAI,GAAG,EAEjC,OADA,IAAI,KAAK,GAAG,IAAI,KAAK,sCAAsC,EACpD,GAET,QAAQ,IAAI,GAAG,EACf,GAAI,CASF,OAPA,MADiB,UAAU,QAAQ,IAAI,MAC1B,CAAC,CAAC,IAAI,YAAY,CAAC,CAC9B,KAAM,IAAI,KACV,WAAY,IAAI,WAChB,KAAM,IAAI,MAAQ,CAAC,EACnB,QAAS,IAAI,KACb,OACF,CAAC,EACM,EACT,OAAS,IAAK,CAMZ,MALA,IAAI,MAAM,CAAE,GAAI,EAAG,YAAY,IAAI,KAAK,QAAQ,EAChD,YAAY,IAAK,CACf,OAAQ,OACR,QAAS,CAAE,IAAK,IAAI,KAAM,WAAY,IAAI,WAAY,OAAQ,CAChE,CAAC,EACK,GACR,QAAU,CACR,QAAQ,OAAO,GAAG,CACpB,CACF,CAOA,eAAsB,YACpB,UACA,OACA,QAC0C,CAC1C,IAAM,KAAO,aAAa,SAAS,CAAC,CAAC,OAClC,MACE,OAAO,OAAS,IAAA,IAAa,IAAI,OAAS,OAAO,QACjD,OAAO,aAAe,IAAA,IAAa,IAAI,aAAe,OAAO,cAC7D,OAAO,aAAe,IAAA,IAAa,IAAI,aAAe,OAAO,WAClE,EACM,QAAU,MAAM,QAAQ,WAAW,KAAK,IAAK,KAAQ,WAAW,IAAK,UAAW,OAAO,CAAC,CAAC,EAC/F,MAAO,CACL,IAAK,QAAQ,OAAQ,GAAM,EAAE,SAAW,aAAe,EAAE,KAAK,CAAC,CAAC,OAChE,OAAQ,QAAQ,OAAQ,GAAM,EAAE,SAAW,UAAU,CAAC,CAAC,MACzD,CACF,CAQA,SAAgB,eAAe,WAA4B,CACzD,IAAI,KAAO,WACX,IAAK,IAAM,QAAQ,WAAW,KAAK,CAAC,CAAC,QAAQ,OAAQ,GAAG,EACtD,MAAQ,KAAK,YAAY,CAAC,EAC1B,KAAO,KAAK,KAAK,KAAM,QAAU,IAAM,EAEzC,OAAO,KAAK,SAAS,EAAE,CACzB"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
|
|
2
|
-
import { A as Service, C as Post, D as Put, E as PropertyOrParameterDecorator, M as normalizeApiQueryParamsConfig, N as withBuilder, O as Repository, S as Patch, T as PreDestroy, _ as Get, a as Autowired, b as Middleware, c as ColumnApiQueryParamsConfig, d as Delete, f as Env, g as FileUploadConfig, h as FileUpload, i as AssetKey, j as Value, k as RouteDefinition, l as Component, m as FileTypeFilter, n as ApiQueryParamsConfig, o as BaseUploadOptions, p as EnvKey, r as Asset, s as Builder, t as ApiQueryParams, u as Controller, v as Inject, w as PostConstruct, x as MiddlewareHandler, y as Injectable } from "../decorators-
|
|
2
|
+
import { A as Service, C as Post, D as Put, E as PropertyOrParameterDecorator, M as normalizeApiQueryParamsConfig, N as withBuilder, O as Repository, S as Patch, T as PreDestroy, _ as Get, a as Autowired, b as Middleware, c as ColumnApiQueryParamsConfig, d as Delete, f as Env, g as FileUploadConfig, h as FileUpload, i as AssetKey, j as Value, k as RouteDefinition, l as Component, m as FileTypeFilter, n as ApiQueryParamsConfig, o as BaseUploadOptions, p as EnvKey, r as Asset, s as Builder, t as ApiQueryParams, u as Controller, v as Inject, w as PostConstruct, x as MiddlewareHandler, y as Injectable } from "../decorators-DOcWsXci.mjs";
|
|
3
3
|
export { ApiQueryParams, ApiQueryParamsConfig, Asset, AssetKey, Autowired, BaseUploadOptions, Builder, ColumnApiQueryParamsConfig, Component, Controller, Delete, Env, EnvKey, FileTypeFilter, FileUpload, FileUploadConfig, Get, Inject, Injectable, Middleware, MiddlewareHandler, Patch, Post, PostConstruct, PreDestroy, PropertyOrParameterDecorator, Put, Repository, RouteDefinition, Service, Value, normalizeApiQueryParamsConfig, withBuilder };
|
package/dist/core/decorators.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs
|
|
2
|
+
* @forinda/kickjs v9.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"./interfaces.mjs";import{t as Container}from"../container-
|
|
11
|
+
import{METADATA}from"./interfaces.mjs";import{t as Container}from"../container-Dw45sa0Y.mjs";import{c as pushClassMeta,d as setInMetaMap,f as setInMetaRecord,l as pushMethodMeta,p as setMethodMeta,u as setClassMeta}from"../metadata-BQG1MmqL.mjs";import"reflect-metadata";const pendingRegistrations=[],allRegistrations=new Map;let containerRef=null;function flushPending(container){containerRef=container;for(let{target,scope}of pendingRegistrations)container.has(target)||container.register(target,target,scope);pendingRegistrations.length=0}Container._onReady=flushPending;function seed(container){for(let[,{target,scope}]of allRegistrations)container.has(target)||container.register(target,target,scope)}Container._onReset=container=>{containerRef=container,seed(container)},Container._onCreate=seed;const preDestroyWarned=new WeakSet;function warnIfInertPreDestroy(target,scope){if(scope===`request`)return;let proto=target?.prototype;if(!proto||!Reflect.getMetadata?.(METADATA.PRE_DESTROY,proto)||preDestroyWarned.has(target))return;preDestroyWarned.add(target);let name=target.name||String(target);console.warn(` kickjs: @PreDestroy on ${name} (${scope}) will never run — that hook fires only when a REQUEST scope closes.\n For application-lifetime resources, release them from an adapter's shutdown() hook instead.`)}function registerInContainer(target,scope){setClassMeta(METADATA.INJECTABLE,!0,target),setClassMeta(METADATA.SCOPE,scope,target),warnIfInertPreDestroy(target,scope);let name=target.name||String(target);allRegistrations.set(name,{target,scope}),containerRef?containerRef.has(target)||containerRef.register(target,target,scope):pendingRegistrations.push({target,scope})}function Injectable(options){return target=>{setClassMeta(METADATA.CLASS_KIND,`injectable`,target),registerInContainer(target,options?.scope??`singleton`)}}function Service(options){return target=>{setClassMeta(METADATA.CLASS_KIND,`service`,target),registerInContainer(target,options?.scope??`singleton`)}}function Component(options){return target=>{setClassMeta(METADATA.CLASS_KIND,`component`,target),registerInContainer(target,options?.scope??`singleton`)}}function Repository(options){return target=>{setClassMeta(METADATA.CLASS_KIND,`repository`,target),registerInContainer(target,options?.scope??`singleton`)}}function Controller(){return target=>{setClassMeta(METADATA.CLASS_KIND,`controller`,target),registerInContainer(target,`singleton`)}}function PostConstruct(){return(target,propertyKey)=>{setClassMeta(METADATA.POST_CONSTRUCT,propertyKey,target)}}function PreDestroy(){return(target,propertyKey)=>{setClassMeta(METADATA.PRE_DESTROY,propertyKey,target)}}function applyInjection(token,target,propertyKey,parameterIndex){if(typeof parameterIndex==`number`){setInMetaRecord(METADATA.INJECT,target,parameterIndex,token);return}propertyKey!=null&&setInMetaMap(METADATA.AUTOWIRED,target,propertyKey,token)}function Autowired(token){return((target,propertyKey,parameterIndex)=>{applyInjection(token,target,propertyKey,parameterIndex)})}function Inject(token){return((target,propertyKey,parameterIndex)=>{applyInjection(token,target,propertyKey,parameterIndex)})}function Value(envKey,defaultValue){return(target,propertyKey)=>{setInMetaMap(METADATA.VALUE,target,propertyKey,{envKey,defaultValue})}}function Asset(assetKey){return(target,propertyKey)=>{setInMetaMap(METADATA.ASSET,target,propertyKey,{assetKey})}}function createRouteDecorator(method){return(path,validation)=>(target,propertyKey)=>{pushClassMeta(METADATA.ROUTES,target.constructor,{method,path:path||`/`,handlerName:propertyKey,validation})}}const Get=createRouteDecorator(`GET`),Post=createRouteDecorator(`POST`),Put=createRouteDecorator(`PUT`),Delete=createRouteDecorator(`DELETE`),Patch=createRouteDecorator(`PATCH`);function normalizeApiQueryParamsConfig(config){return`columns`in config&&config.columns&&typeof config.columns==`object`?{filterable:Object.keys(config.columns),sortable:config.sortable?Object.keys(config.sortable):void 0,searchable:config.searchColumns?config.searchColumns.map(col=>col?.name??``).filter(Boolean):void 0}:config}function ApiQueryParams(config){return(target,propertyKey)=>{let normalized=normalizeApiQueryParamsConfig(config);setMethodMeta(METADATA.QUERY_PARAMS,normalized,target.constructor,propertyKey)}}function Middleware(...handlers){return(target,propertyKey)=>{propertyKey?pushMethodMeta(METADATA.METHOD_MIDDLEWARES,target.constructor,propertyKey,...handlers):pushClassMeta(METADATA.CLASS_MIDDLEWARES,target,...handlers)}}function FileUpload(config){return(target,propertyKey)=>{setMethodMeta(METADATA.FILE_UPLOAD,config,target.constructor,propertyKey)}}function attachBuilder(target){setClassMeta(METADATA.BUILDER,!0,target),target.builder=function(){let props={},proxy=new Proxy({},{get(_,key){return key===`build`?()=>Object.assign(new target,props):value=>(props[key]=value,proxy)}});return proxy}}function Builder(target){attachBuilder(target)}function withBuilder(ctor){return attachBuilder(ctor),ctor}export{ApiQueryParams,Asset,Autowired,Builder,Component,Controller,Delete,FileUpload,Get,Inject,Injectable,Middleware,Patch,Post,PostConstruct,PreDestroy,Put,Repository,Service,Value,normalizeApiQueryParamsConfig,withBuilder};
|
|
12
12
|
//# sourceMappingURL=decorators.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"decorators.mjs","names":[],"sources":["../../src/core/decorators.ts"],"sourcesContent":["import 'reflect-metadata'\nimport { METADATA, Scope, type BuilderOf, type ServiceOptions } from './interfaces'\nimport { Container, type KickJsRegistry } from './container'\nimport {\n setClassMeta,\n setMethodMeta,\n pushClassMeta,\n pushMethodMeta,\n setInMetaMap,\n setInMetaRecord,\n} from './metadata'\n\n// ── Decorator Registration System ───────────────────────────────────────\n// Decorators execute at class-definition time (module load). The Container\n// may not exist yet when the first decorator fires. We queue registrations\n// and flush them on first Container.getInstance() call.\n//\n// `allRegistrations` is a persistent registry that survives Container.reset().\n// On reset (HMR), all previously decorated classes are re-registered on the\n// fresh container. This ensures @Service, @Controller, @Repository, etc.\n// all survive HMR without manual re-registration.\n\ntype PendingRegistration = { target: any; scope: Scope }\nconst pendingRegistrations: PendingRegistration[] = []\n// Keyed by class name to prevent memory leaks during HMR — new class with\n// the same name replaces the old entry instead of accumulating references.\nconst allRegistrations = new Map<string, { target: any; scope: Scope }>()\nlet containerRef: any = null\n\nfunction flushPending(container: any): void {\n containerRef = container\n for (const { target, scope } of pendingRegistrations) {\n if (!container.has(target)) {\n container.register(target, target, scope)\n }\n }\n pendingRegistrations.length = 0\n}\n\n// Wire up synchronously — Container._onReady is called on first getInstance()\nContainer._onReady = flushPending\n\n/** Replay every decorator registration onto a container. */\nfunction seed(container: any): void {\n for (const [, { target, scope }] of allRegistrations) {\n if (!container.has(target)) {\n container.register(target, target, scope)\n }\n }\n}\n\n// On Container.reset(), update containerRef and replay ALL decorator\n// registrations on the fresh container. This handles HMR where the container\n// is wiped but not all decorated modules are re-evaluated.\nContainer._onReset = (container: any) => {\n containerRef = container\n seed(container)\n}\n\n// On Container.create(), seed the same way but leave `containerRef` alone: an\n// isolated container should receive what has been registered so far, not\n// become the destination for everything registered next.\nContainer._onCreate = seed\n\n// ── Class Decorators ────────────────────────────────────────────────────\n\n/**\n * Classes already warned about.\n *\n * Keyed by the constructor, not its `name`: distinct classes can share a name —\n * two `DatabaseService` classes in separate modules is ordinary — and deduping\n * by name silently suppressed the second one's warning, which is precisely the\n * failure this warning exists to prevent.\n *\n * A WeakSet means an HMR reload, which builds a NEW class object, warns again.\n * That is correct rather than noisy: the reloaded code still has the problem,\n * and the notice stops as soon as it is fixed.\n */\nconst preDestroyWarned = new WeakSet<object>()\n\n/**\n * `@PreDestroy` only runs for REQUEST-scoped services — the request scope is\n * what closes and triggers it. On a SINGLETON it is silently inert, which\n * reads as a working teardown and is not: the reported case was a Postgres\n * pool never closed on shutdown, invisible in development and surfacing as\n * connection exhaustion under repeated restarts and HMR reloads.\n *\n * The asymmetry is the trap. `@PostConstruct` DOES run for singletons, so the\n * pair looks like init/teardown while one half quietly opts out on a scope the\n * author may never have considered.\n *\n * Warn rather than run it: making it fire on shutdown is a behaviour change\n * whose ordering, timeout and concurrency semantics have to match the adapter\n * shutdown path, and an adapter's `shutdown()` is already the seam for\n * application-lifetime resources.\n */\nfunction warnIfInertPreDestroy(target: any, scope: Scope): void {\n if (scope === Scope.REQUEST) return\n const proto = target?.prototype\n if (!proto) return\n const hook = Reflect.getMetadata?.(METADATA.PRE_DESTROY, proto)\n if (!hook) return\n if (preDestroyWarned.has(target)) return\n preDestroyWarned.add(target)\n const name = target.name || String(target)\n console.warn(\n ` kickjs: @PreDestroy on ${name} (${scope}) will never run — that hook fires ` +\n `only when a REQUEST scope closes.\\n` +\n ` For application-lifetime resources, release them from an adapter's ` +\n `shutdown() hook instead.`,\n )\n}\n\nfunction registerInContainer(target: any, scope: Scope): void {\n setClassMeta(METADATA.INJECTABLE, true, target)\n setClassMeta(METADATA.SCOPE, scope, target)\n // Method decorators run before the class decorator, so PRE_DESTROY metadata\n // is already present by the time we get here.\n warnIfInertPreDestroy(target, scope)\n\n // Track in persistent registry — survives Container.reset() for HMR replay.\n // Keyed by name so HMR class re-creation replaces the old entry.\n const name = target.name || String(target)\n allRegistrations.set(name, { target, scope })\n\n if (containerRef) {\n // Container already initialized — register immediately\n if (!containerRef.has(target)) {\n containerRef.register(target, target, scope)\n }\n } else {\n // Container not ready yet — queue for later\n pendingRegistrations.push({ target, scope })\n }\n}\n\n/** Mark a class as injectable with lifecycle scope */\nexport function Injectable(options?: ServiceOptions): ClassDecorator {\n return (target: any) => {\n setClassMeta(METADATA.CLASS_KIND, 'injectable', target)\n registerInContainer(target, options?.scope ?? Scope.SINGLETON)\n }\n}\n\n/** Mark a class as a service (semantic alias for Injectable) */\nexport function Service(options?: ServiceOptions): ClassDecorator {\n return (target: any) => {\n setClassMeta(METADATA.CLASS_KIND, 'service', target)\n registerInContainer(target, options?.scope ?? Scope.SINGLETON)\n }\n}\n\n/** Mark a class as a generic managed component */\nexport function Component(options?: ServiceOptions): ClassDecorator {\n return (target: any) => {\n setClassMeta(METADATA.CLASS_KIND, 'component', target)\n registerInContainer(target, options?.scope ?? Scope.SINGLETON)\n }\n}\n\n/** Mark a class as a repository */\nexport function Repository(options?: ServiceOptions): ClassDecorator {\n return (target: any) => {\n setClassMeta(METADATA.CLASS_KIND, 'repository', target)\n registerInContainer(target, options?.scope ?? Scope.SINGLETON)\n }\n}\n\n/**\n * Mark a class as an HTTP controller and register it in the DI container.\n *\n * Route prefixes are defined by the module's `routes().path` — the single\n * source of truth for where routes mount. The legacy\n * `@Controller('/path')` parameter was removed in v4; if your code\n * still passes a string, see `docs/guide/migration-v3-to-v4.md` for\n * the rename map.\n */\nexport function Controller(): ClassDecorator {\n return (target: any) => {\n setClassMeta(METADATA.CLASS_KIND, 'controller', target)\n registerInContainer(target, Scope.SINGLETON)\n }\n}\n\n// ── Method Decorators ───────────────────────────────────────────────────\n\n/** Mark a method as a lifecycle hook called after instantiation */\nexport function PostConstruct(): MethodDecorator {\n return (target, propertyKey) => {\n setClassMeta(METADATA.POST_CONSTRUCT, propertyKey, target)\n }\n}\n\n/**\n * Mark a method as a teardown hook — the counterpart to {@link PostConstruct}.\n *\n * For REQUEST-scoped services it runs when the request's scope closes\n * (response finished or aborted): release per-request resources there\n * (transactions, handles, subscriptions). May be async; errors are logged\n * and swallowed so one failing hook can't break request completion.\n *\n * It does NOT run on a SINGLETON — the default scope — because nothing closes\n * a singleton's scope. Unlike {@link PostConstruct}, which does run for\n * singletons, so the pair is not symmetric. Applying it to a singleton logs a\n * warning naming the class rather than failing silently.\n *\n * For application-lifetime resources (a connection pool, a timer, a socket\n * server) use an adapter's `shutdown()` hook, which the framework runs on a\n * real shutdown AND on every HMR reload, time-boxed and concurrent.\n */\nexport function PreDestroy(): MethodDecorator {\n return (target, propertyKey) => {\n setClassMeta(METADATA.PRE_DESTROY, propertyKey, target)\n }\n}\n\n// ── Injection Decorators (property + constructor parameter) ─────────────\n\n/**\n * A decorator that's valid as either a property decorator OR a\n * constructor-parameter decorator. forinda/kick-js#235 §2 — `@Autowired`\n * and `@Inject` previously diverged at the type level (property-only\n * vs parameter-only) and adopters who picked the wrong name for the\n * position got a cryptic TS1240 error. Both names now accept either\n * position and route to the correct metadata bucket at runtime.\n */\nexport interface PropertyOrParameterDecorator {\n /** Property decorator form — `@Autowired(TOKEN) private foo!: Foo`. */\n (target: object, propertyKey: string | symbol): void\n /** Constructor-parameter form — `constructor(@Inject(TOKEN) foo: Foo) {}`. */\n (target: object, propertyKey: string | symbol | undefined, parameterIndex: number): void\n}\n\nfunction applyInjection(\n token: unknown,\n target: object,\n propertyKey: string | symbol | undefined,\n parameterIndex: number | undefined,\n): void {\n // Legacy parameter decorators receive 3 args (target, propertyKey,\n // parameterIndex); property decorators receive 2 (target, propertyKey).\n // The 3rd-arg-number-vs-undefined check is the canonical position\n // detection in TypeScript's legacy decorator runtime.\n if (typeof parameterIndex === 'number') {\n setInMetaRecord(METADATA.INJECT, target, parameterIndex, token)\n return\n }\n if (propertyKey != null) {\n // `target` for a property decorator on a non-static field is the\n // class prototype; the AUTOWIRED bucket is keyed off the prototype\n // identity at resolution time, so pass it through unchanged.\n setInMetaMap(METADATA.AUTOWIRED, target, propertyKey as string, token)\n }\n}\n\n/**\n * Inject a dependency. Works in two positions:\n *\n * - **Property decorator** — `@Autowired(TOKEN) private repo!: UserRepo`.\n * Resolved lazily from the container the first time the property is\n * read.\n * - **Constructor parameter decorator** — `constructor(@Autowired(TOKEN) repo: UserRepo) {}`.\n * Resolved at instantiation; injected into the ctor call.\n *\n * `@Inject` is the same function under another name — they share\n * runtime + types so the two names are interchangeable. Pick whichever\n * reads better at the call site.\n *\n * Like `@Inject`, the typed string-literal overload narrows the token\n * to a key of the augmented `KickJsRegistry`. After `kick typegen`\n * populates the registry, `@Autowired('kick/prisma/Client')` auto-\n * completes the key and typo'd literals become TS2345 errors.\n */\nexport function Autowired<K extends keyof KickJsRegistry & string>(\n token: K,\n): PropertyOrParameterDecorator\nexport function Autowired(token?: unknown): PropertyOrParameterDecorator\nexport function Autowired(token?: unknown): PropertyOrParameterDecorator {\n return ((\n target: object,\n propertyKey: string | symbol | undefined,\n parameterIndex?: number,\n ): void => {\n applyInjection(token, target, propertyKey, parameterIndex)\n }) as PropertyOrParameterDecorator\n}\n\n/**\n * Inject a dependency by token. Same shape as {@link Autowired} —\n * works as either a property decorator or a constructor-parameter\n * decorator. Pick whichever name reads better at the call site.\n *\n * ## Typed string-literal overload\n *\n * When called with a string literal that matches a key of the\n * augmented `KickJsRegistry`, TypeScript narrows the parameter type\n * to the registered type. Typo'd literals become compile errors\n * (the string isn't `keyof KickJsRegistry`). Class identities and\n * `InjectionToken<T>` keep working unchanged via the second overload.\n *\n * @example\n * ```ts\n * import { Service, Inject } from '@forinda/kickjs'\n * import type { PrismaClient } from '@prisma/client'\n *\n * `@Service()`\n * class UserRepo {\n * constructor(@Inject('kick/prisma/Client') private db: PrismaClient) {}\n * // OR property form, same decorator:\n * @Inject('kick/prisma/Client') private db2!: PrismaClient\n * }\n * ```\n *\n * After `kick typegen` runs, the literal autocompletes from the\n * registry and a typo (`@Inject('kick/prisma/Cleint')`) becomes a\n * TS2345 error.\n */\nexport function Inject<K extends keyof KickJsRegistry & string>(\n token: K,\n): PropertyOrParameterDecorator\nexport function Inject(token?: unknown): PropertyOrParameterDecorator\nexport function Inject(token?: unknown): PropertyOrParameterDecorator {\n return ((\n target: object,\n propertyKey: string | symbol | undefined,\n parameterIndex?: number,\n ): void => {\n applyInjection(token, target, propertyKey, parameterIndex)\n }) as PropertyOrParameterDecorator\n}\n\n/**\n * Global ambient registry of typed environment variables, populated by\n * `kick typegen` from the project's env schema (typically\n * `src/env.ts`'s `export default defineEnv(...)`).\n *\n * Empty by default — declarations come from `.kickjs/types/env.ts`\n * generated alongside the rest of the typegen output. Once populated,\n * `@Value('PORT')` autocompletes the key, type-checks the default\n * value, and `process.env.PORT` is also typed via the parallel\n * `NodeJS.ProcessEnv` augmentation in the same generated file.\n */\ndeclare global {\n // eslint-disable-next-line @typescript-eslint/no-empty-object-type\n interface KickEnv {}\n}\n\n/** A string key known to the typed env registry (`never` until typegen runs) */\nexport type EnvKey = keyof KickEnv & string\n\n/**\n * Look up the type of a typed env entry.\n *\n * Decorators can't propagate the property type from the key, so users\n * still need to write the property's type — but `Env<K>` lets them\n * derive it from `KickEnv` instead of duplicating the schema:\n *\n * ```ts\n * @Service()\n * class MyService {\n * @Value('PORT')\n * private port!: Env<'PORT'> // → number, narrowed from KickEnv['PORT']\n * }\n * ```\n */\nexport type Env<K extends EnvKey = EnvKey> = KickEnv[K]\n\n/**\n * Constraint that allows any string when the typed env registry is\n * empty (no `kick typegen` has run yet, so existing code with raw\n * string keys keeps working) but locks the argument down to known\n * `EnvKey` literals once the registry is populated.\n *\n * Implementation: when `EnvKey` is `never`, the conditional resolves\n * to the user's `K`, accepting anything. Once typegen has populated\n * `KickEnv`, `EnvKey` becomes a real union and the conditional forces\n * `K` to be assignable to it.\n */\ntype ValueKey<K extends string> = [EnvKey] extends [never] ? K : K & EnvKey\n\n/**\n * Inject an environment variable value. Evaluated lazily so the env\n * is available at access time, not at decoration time.\n *\n * If no default is provided and the env var is missing, throws at access time\n * to catch misconfiguration early instead of returning undefined.\n *\n * Uses metadata + instance getter to work correctly with `useDefineForClassFields`.\n *\n * Type-safety:\n * - Without `kick typegen` run, any string key is accepted (back-compat).\n * - After typegen, `KickEnv` is populated and unknown keys become a tsc\n * error. The optional `defaultValue` is type-checked against the\n * schema's inferred type for that key.\n */\nexport function Value<K extends string>(\n envKey: ValueKey<K>,\n defaultValue?: K extends EnvKey ? KickEnv[K] : unknown,\n): PropertyDecorator {\n return (target, propertyKey) => {\n setInMetaMap(METADATA.VALUE, target, propertyKey as string, { envKey, defaultValue })\n }\n}\n\n// ── @Asset — typed asset path injection (assets-plan.md PR 3+) ─────────\n\nimport type { KickAssets } from './assets'\n\n/**\n * Flatten the nested `KickAssets` augmentation (typegen-emitted in\n * PR 4) into a string-literal union of dot/slash-joined paths.\n *\n * `interface KickAssets { mails: { welcome: () => string; orders: {\n * confirmation: () => string } } }` becomes\n * `'mails/welcome' | 'mails/orders/confirmation'`.\n *\n * Returns `never` when `KickAssets` is empty (no typegen has run yet)\n * so `AssetKeyArg` falls through to accept any string.\n */\ntype FlattenAssets<T, Prefix extends string = ''> = {\n [K in keyof T & string]: T[K] extends () => string\n ? `${Prefix}${K}`\n : T[K] extends Record<string, unknown>\n ? FlattenAssets<T[K], `${Prefix}${K}/`>\n : never\n}[keyof T & string]\n\n/** Flat union of every known `<namespace>/<key>` path. */\nexport type AssetKey = FlattenAssets<KickAssets>\n\n/**\n * Constraint mirroring `ValueKey<K>` — accepts any string when\n * `AssetKey` is `never` (back-compat for adopters who haven't run\n * `kick typegen` yet) but locks the argument to known literals once\n * the typegen-augmented `KickAssets` interface is populated.\n */\ntype AssetKeyArg<K extends string> = [AssetKey] extends [never] ? K : K & AssetKey\n\n/**\n * Inject the resolved file-system path for a typed asset. Mirrors\n * `@Value`'s lazy-getter pattern — the asset is resolved on every\n * property access, NOT at class instantiation, so:\n *\n * - Tests can swap fixtures + clear the cache without re-instantiating\n * the consuming class.\n * - The first access cost is paid once per process (the manifest cache\n * in `core/assets.ts` makes subsequent calls cheap).\n *\n * Type-safety:\n *\n * - Without typegen, any string key is accepted (back-compat — same\n * trick as `@Value`).\n * - After typegen, `KickAssets` is populated and `AssetKey` is a\n * union of every flattened `<namespace>/<key>` path. Unknown keys\n * become a TS error.\n *\n * @example\n * ```ts\n * import { Service, Asset } from '@forinda/kickjs'\n *\n * `@Service()`\n * class MailService {\n * `@Asset('mails/welcome')`\n * private welcomeTemplate!: string\n *\n * send(user: User) {\n * return ejs.renderFile(this.welcomeTemplate, { user })\n * }\n * }\n * ```\n */\nexport function Asset<K extends string>(assetKey: AssetKeyArg<K>): PropertyDecorator {\n return (target, propertyKey) => {\n setInMetaMap(METADATA.ASSET, target, propertyKey as string, { assetKey })\n }\n}\n\n// ── HTTP Route Decorators ───────────────────────────────────────────────\n\nexport interface RouteDefinition {\n method: string\n path: string\n handlerName: string\n validation?: {\n /** JSON Schema object for validating the request body */\n body?: any\n /** JSON Schema object for validating query parameters */\n query?: any\n /** JSON Schema object for validating URL params */\n params?: any\n /**\n * DECLARED response schema — never validated at runtime. Feeds two\n * consumers from one source of truth: the Swagger adapter's success\n * response schema, and `kick typegen`'s `KickRoutes[...].response`\n * (winning over return-type inference when present).\n */\n response?: any\n /** Schema name in OpenAPI components/schemas for the request body. Auto-generated from handler name if omitted. */\n name?: string\n }\n}\n\nfunction createRouteDecorator(method: string) {\n return (path?: string, validation?: RouteDefinition['validation']): MethodDecorator => {\n return (target, propertyKey) => {\n pushClassMeta<RouteDefinition>(METADATA.ROUTES, target.constructor, {\n method,\n path: path || '/',\n handlerName: propertyKey as string,\n validation,\n })\n }\n }\n}\n\nexport const Get = createRouteDecorator('GET')\nexport const Post = createRouteDecorator('POST')\nexport const Put = createRouteDecorator('PUT')\nexport const Delete = createRouteDecorator('DELETE')\nexport const Patch = createRouteDecorator('PATCH')\n\n// ── Query Params Decorator ─────────────────────────────────────────────\n\nexport interface ApiQueryParamsConfig {\n /** Fields that can be used in filter queries (e.g., `?filter=status:eq:active`) */\n filterable?: string[]\n /** Fields that can be used in sort queries (e.g., `?sort=createdAt:desc`) */\n sortable?: string[]\n /** Fields that are searched with the `?q=` parameter */\n searchable?: string[]\n}\n\n/**\n * Column-object-based query params config (e.g., from DrizzleQueryParamsConfig).\n * `Object.keys()` is used to derive field names for OpenAPI docs.\n */\nexport interface ColumnApiQueryParamsConfig {\n columns: Record<string, any>\n sortable?: Record<string, any>\n searchColumns?: any[]\n [key: string]: any\n}\n\n/**\n * Normalize a query params config to the string-based ApiQueryParamsConfig.\n * Handles both string-based and column-object-based configs.\n */\nexport function normalizeApiQueryParamsConfig(\n config: ApiQueryParamsConfig | ColumnApiQueryParamsConfig,\n): ApiQueryParamsConfig {\n if ('columns' in config && config.columns && typeof config.columns === 'object') {\n return {\n filterable: Object.keys(config.columns),\n sortable: config.sortable ? Object.keys(config.sortable) : undefined,\n searchable: config.searchColumns\n ? config.searchColumns.map((col: any) => col?.name ?? '').filter(Boolean)\n : undefined,\n }\n }\n return config as ApiQueryParamsConfig\n}\n\n/**\n * Document the query parameters accepted by a GET endpoint.\n * Used by SwaggerAdapter to generate `filter`, `sort`, `page`, `limit`, and `q` params\n * in the OpenAPI spec, with descriptions listing the allowed fields.\n *\n * Accepts both string-based configs and column-object configs (e.g., DrizzleQueryParamsConfig).\n *\n * @example\n * ```ts\n * // String-based\n * @ApiQueryParams({\n * filterable: ['status', 'category', 'price'],\n * sortable: ['name', 'createdAt', 'price'],\n * searchable: ['name', 'description'],\n * })\n *\n * // Column-object-based (Drizzle)\n * @ApiQueryParams(TASK_QUERY_CONFIG)\n * ```\n */\nexport function ApiQueryParams(\n config: ApiQueryParamsConfig | ColumnApiQueryParamsConfig,\n): MethodDecorator {\n return (target, propertyKey) => {\n const normalized = normalizeApiQueryParamsConfig(config)\n setMethodMeta(METADATA.QUERY_PARAMS, normalized, target.constructor, propertyKey as string)\n }\n}\n\n// ── Middleware Decorators ───────────────────────────────────────────────\n\n/**\n * Middleware handler function.\n * Generic `TCtx` defaults to `any` — import `RequestContext` from\n * `@forinda/kickjs` for full type safety:\n *\n * ```ts\n * import type { MiddlewareHandler } from '@forinda/kickjs'\n * import type { RequestContext } from '@forinda/kickjs'\n *\n * const auth: MiddlewareHandler<RequestContext> = (ctx, next) => {\n * ctx.body // fully typed\n * next()\n * }\n * ```\n */\nexport type MiddlewareHandler<TCtx = any> = (ctx: TCtx, next: () => void) => void | Promise<void>\n\n/** Attach middleware handlers to a class or method */\nexport function Middleware(...handlers: MiddlewareHandler[]): ClassDecorator & MethodDecorator {\n return (target: any, propertyKey?: string | symbol) => {\n if (propertyKey) {\n pushMethodMeta(\n METADATA.METHOD_MIDDLEWARES,\n target.constructor,\n propertyKey as string,\n ...handlers,\n )\n } else {\n pushClassMeta(METADATA.CLASS_MIDDLEWARES, target, ...handlers)\n }\n }\n}\n\n// ── File Upload Decorator ───────────────────────────────────────────────\n\n/**\n * File filter — accepts short extensions ('jpg'), full MIME types ('image/jpeg'),\n * wildcards ('image/*'), or a function `(mimetype, filename) => boolean` for full control.\n */\nexport type FileTypeFilter = string[] | ((mimetype: string, filename: string) => boolean)\n\n/**\n * Shared upload options used by both the `@FileUpload` decorator and the\n * `upload.single()` / `upload.array()` / `upload.none()` middleware.\n */\nexport interface BaseUploadOptions {\n /** Max file size in bytes (default: 5MB) */\n maxSize?: number\n /**\n * Allowed file types:\n * - **string[]** — short extensions ('jpg'), full MIME types ('image/jpeg'), or wildcards ('image/*')\n * - **function** — `(mimetype, filename) => boolean` for full control\n */\n allowedTypes?: FileTypeFilter\n /** Extend the built-in extension-to-MIME map */\n customMimeMap?: Record<string, string>\n}\n\nexport interface FileUploadConfig extends BaseUploadOptions {\n mode: 'single' | 'array' | 'none'\n /** Form field name (default: 'file') */\n fieldName?: string\n /** Max files for array mode (default: 10) */\n maxCount?: number\n}\n\n/** Configure file upload handling for a controller method */\nexport function FileUpload(config: FileUploadConfig): MethodDecorator {\n return (target, propertyKey) => {\n setMethodMeta(METADATA.FILE_UPLOAD, config, target.constructor, propertyKey as string)\n }\n}\n\n// ── Builder Decorator ───────────────────────────────────────────────────\n\nfunction attachBuilder(target: any): void {\n setClassMeta(METADATA.BUILDER, true, target)\n target.builder = function () {\n const props: Record<string, any> = {}\n const proxy: any = new Proxy(\n {},\n {\n get(_, key) {\n if (key === 'build') {\n return () => Object.assign(new target(), props)\n }\n return (value: any) => {\n props[key as string] = value\n return proxy\n }\n },\n },\n )\n return proxy\n }\n}\n\n/**\n * Add a static `builder()` method for fluent construction.\n *\n * Legacy decorators cannot widen the class type, so by default the\n * caller sees the unmodified shape. Opt into typing by adding a single\n * `declare static` line:\n *\n * ```ts\n * @Builder\n * class UserDto {\n * name!: string\n * email!: string\n * declare static readonly builder: () => BuilderOf<UserDto>\n * }\n * ```\n *\n * `readonly` keeps SonarQube's `typescript:S1444` quiet — the runtime\n * assigns `target.builder` once at decoration time and never again.\n *\n * If you'd rather not write the `declare`, use `withBuilder()` instead —\n * same runtime, types inferred automatically.\n */\nexport function Builder(target: any): void {\n attachBuilder(target)\n}\n\n/**\n * Factory variant of `@Builder`. Wraps the class with the same runtime\n * and returns it intersected with `{ builder(): BuilderOf<T> }` so the\n * static is fully typed without any `declare` boilerplate.\n *\n * ```ts\n * class UserDtoBase {\n * name!: string\n * email!: string\n * }\n * export const UserDto = withBuilder(UserDtoBase)\n * export type UserDto = InstanceType<typeof UserDto>\n *\n * UserDto.builder().name('Alice').email('a@b.com').build() // fully typed\n * ```\n */\nexport function withBuilder<C extends new (...args: any[]) => any>(\n ctor: C,\n): C & { builder(): BuilderOf<InstanceType<C>> } {\n attachBuilder(ctor)\n return ctor as C & { builder(): BuilderOf<InstanceType<C>> }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"decorators.mjs","names":[],"sources":["../../src/core/decorators.ts"],"sourcesContent":["import 'reflect-metadata'\nimport { METADATA, Scope, type BuilderOf, type ServiceOptions } from './interfaces'\nimport { Container, type KickJsRegistry } from './container'\nimport {\n setClassMeta,\n setMethodMeta,\n pushClassMeta,\n pushMethodMeta,\n setInMetaMap,\n setInMetaRecord,\n} from './metadata'\n\n// ── Decorator Registration System ───────────────────────────────────────\n// Decorators execute at class-definition time (module load). The Container\n// may not exist yet when the first decorator fires. We queue registrations\n// and flush them on first Container.getInstance() call.\n//\n// `allRegistrations` is a persistent registry that survives Container.reset().\n// On reset (HMR), all previously decorated classes are re-registered on the\n// fresh container. This ensures @Service, @Controller, @Repository, etc.\n// all survive HMR without manual re-registration.\n\ntype PendingRegistration = { target: any; scope: Scope }\nconst pendingRegistrations: PendingRegistration[] = []\n// Keyed by class name to prevent memory leaks during HMR — new class with\n// the same name replaces the old entry instead of accumulating references.\nconst allRegistrations = new Map<string, { target: any; scope: Scope }>()\nlet containerRef: any = null\n\nfunction flushPending(container: any): void {\n containerRef = container\n for (const { target, scope } of pendingRegistrations) {\n if (!container.has(target)) {\n container.register(target, target, scope)\n }\n }\n pendingRegistrations.length = 0\n}\n\n// Wire up synchronously — Container._onReady is called on first getInstance()\nContainer._onReady = flushPending\n\n/** Replay every decorator registration onto a container. */\nfunction seed(container: any): void {\n for (const [, { target, scope }] of allRegistrations) {\n if (!container.has(target)) {\n container.register(target, target, scope)\n }\n }\n}\n\n// On Container.reset(), update containerRef and replay ALL decorator\n// registrations on the fresh container. This handles HMR where the container\n// is wiped but not all decorated modules are re-evaluated.\nContainer._onReset = (container: any) => {\n containerRef = container\n seed(container)\n}\n\n// On Container.create(), seed the same way but leave `containerRef` alone: an\n// isolated container should receive what has been registered so far, not\n// become the destination for everything registered next.\nContainer._onCreate = seed\n\n// ── Class Decorators ────────────────────────────────────────────────────\n\n/**\n * Classes already warned about.\n *\n * Keyed by the constructor, not its `name`: distinct classes can share a name —\n * two `DatabaseService` classes in separate modules is ordinary — and deduping\n * by name silently suppressed the second one's warning, which is precisely the\n * failure this warning exists to prevent.\n *\n * A WeakSet means an HMR reload, which builds a NEW class object, warns again.\n * That is correct rather than noisy: the reloaded code still has the problem,\n * and the notice stops as soon as it is fixed.\n */\nconst preDestroyWarned = new WeakSet<object>()\n\n/**\n * `@PreDestroy` only runs for REQUEST-scoped services — the request scope is\n * what closes and triggers it. On a SINGLETON it is silently inert, which\n * reads as a working teardown and is not: the reported case was a Postgres\n * pool never closed on shutdown, invisible in development and surfacing as\n * connection exhaustion under repeated restarts and HMR reloads.\n *\n * The asymmetry is the trap. `@PostConstruct` DOES run for singletons, so the\n * pair looks like init/teardown while one half quietly opts out on a scope the\n * author may never have considered.\n *\n * Warn rather than run it: making it fire on shutdown is a behaviour change\n * whose ordering, timeout and concurrency semantics have to match the adapter\n * shutdown path, and an adapter's `shutdown()` is already the seam for\n * application-lifetime resources.\n */\nfunction warnIfInertPreDestroy(target: any, scope: Scope): void {\n if (scope === Scope.REQUEST) return\n const proto = target?.prototype\n if (!proto) return\n const hook = Reflect.getMetadata?.(METADATA.PRE_DESTROY, proto)\n if (!hook) return\n if (preDestroyWarned.has(target)) return\n preDestroyWarned.add(target)\n const name = target.name || String(target)\n console.warn(\n ` kickjs: @PreDestroy on ${name} (${scope}) will never run — that hook fires ` +\n `only when a REQUEST scope closes.\\n` +\n ` For application-lifetime resources, release them from an adapter's ` +\n `shutdown() hook instead.`,\n )\n}\n\nfunction registerInContainer(target: any, scope: Scope): void {\n setClassMeta(METADATA.INJECTABLE, true, target)\n setClassMeta(METADATA.SCOPE, scope, target)\n // Method decorators run before the class decorator, so PRE_DESTROY metadata\n // is already present by the time we get here.\n warnIfInertPreDestroy(target, scope)\n\n // Track in persistent registry — survives Container.reset() for HMR replay.\n // Keyed by name so HMR class re-creation replaces the old entry.\n const name = target.name || String(target)\n allRegistrations.set(name, { target, scope })\n\n if (containerRef) {\n // Container already initialized — register immediately\n if (!containerRef.has(target)) {\n containerRef.register(target, target, scope)\n }\n } else {\n // Container not ready yet — queue for later\n pendingRegistrations.push({ target, scope })\n }\n}\n\n/** Mark a class as injectable with lifecycle scope */\nexport function Injectable(options?: ServiceOptions): ClassDecorator {\n return (target: any) => {\n setClassMeta(METADATA.CLASS_KIND, 'injectable', target)\n registerInContainer(target, options?.scope ?? Scope.SINGLETON)\n }\n}\n\n/** Mark a class as a service (semantic alias for Injectable) */\nexport function Service(options?: ServiceOptions): ClassDecorator {\n return (target: any) => {\n setClassMeta(METADATA.CLASS_KIND, 'service', target)\n registerInContainer(target, options?.scope ?? Scope.SINGLETON)\n }\n}\n\n/** Mark a class as a generic managed component */\nexport function Component(options?: ServiceOptions): ClassDecorator {\n return (target: any) => {\n setClassMeta(METADATA.CLASS_KIND, 'component', target)\n registerInContainer(target, options?.scope ?? Scope.SINGLETON)\n }\n}\n\n/** Mark a class as a repository */\nexport function Repository(options?: ServiceOptions): ClassDecorator {\n return (target: any) => {\n setClassMeta(METADATA.CLASS_KIND, 'repository', target)\n registerInContainer(target, options?.scope ?? Scope.SINGLETON)\n }\n}\n\n/**\n * Mark a class as an HTTP controller and register it in the DI container.\n *\n * Route prefixes are defined by the module's `routes().path` — the single\n * source of truth for where routes mount. The legacy\n * `@Controller('/path')` parameter was removed in v4; if your code\n * still passes a string, see `docs/guide/migration-v3-to-v4.md` for\n * the rename map.\n */\nexport function Controller(): ClassDecorator {\n return (target: any) => {\n setClassMeta(METADATA.CLASS_KIND, 'controller', target)\n registerInContainer(target, Scope.SINGLETON)\n }\n}\n\n// ── Method Decorators ───────────────────────────────────────────────────\n\n/** Mark a method as a lifecycle hook called after instantiation */\nexport function PostConstruct(): MethodDecorator {\n return (target, propertyKey) => {\n setClassMeta(METADATA.POST_CONSTRUCT, propertyKey, target)\n }\n}\n\n/**\n * Mark a method as a teardown hook — the counterpart to {@link PostConstruct}.\n *\n * For REQUEST-scoped services it runs when the request's scope closes\n * (response finished or aborted): release per-request resources there\n * (transactions, handles, subscriptions). May be async; errors are logged\n * and swallowed so one failing hook can't break request completion.\n *\n * It does NOT run on a SINGLETON — the default scope — because nothing closes\n * a singleton's scope. Unlike {@link PostConstruct}, which does run for\n * singletons, so the pair is not symmetric. Applying it to a singleton logs a\n * warning naming the class rather than failing silently.\n *\n * For application-lifetime resources (a connection pool, a timer, a socket\n * server) use an adapter's `shutdown()` hook, which the framework runs on a\n * real shutdown AND on every HMR reload, time-boxed and concurrent.\n */\nexport function PreDestroy(): MethodDecorator {\n return (target, propertyKey) => {\n setClassMeta(METADATA.PRE_DESTROY, propertyKey, target)\n }\n}\n\n// ── Injection Decorators (property + constructor parameter) ─────────────\n\n/**\n * A decorator that's valid as either a property decorator OR a\n * constructor-parameter decorator. forinda/kick-js#235 §2 — `@Autowired`\n * and `@Inject` previously diverged at the type level (property-only\n * vs parameter-only) and adopters who picked the wrong name for the\n * position got a cryptic TS1240 error. Both names now accept either\n * position and route to the correct metadata bucket at runtime.\n */\nexport interface PropertyOrParameterDecorator {\n /** Property decorator form — `@Autowired(TOKEN) private foo!: Foo`. */\n (target: object, propertyKey: string | symbol): void\n /** Constructor-parameter form — `constructor(@Inject(TOKEN) foo: Foo) {}`. */\n (target: object, propertyKey: string | symbol | undefined, parameterIndex: number): void\n}\n\nfunction applyInjection(\n token: unknown,\n target: object,\n propertyKey: string | symbol | undefined,\n parameterIndex: number | undefined,\n): void {\n // Legacy parameter decorators receive 3 args (target, propertyKey,\n // parameterIndex); property decorators receive 2 (target, propertyKey).\n // The 3rd-arg-number-vs-undefined check is the canonical position\n // detection in TypeScript's legacy decorator runtime.\n if (typeof parameterIndex === 'number') {\n setInMetaRecord(METADATA.INJECT, target, parameterIndex, token)\n return\n }\n if (propertyKey != null) {\n // `target` for a property decorator on a non-static field is the\n // class prototype; the AUTOWIRED bucket is keyed off the prototype\n // identity at resolution time, so pass it through unchanged.\n setInMetaMap(METADATA.AUTOWIRED, target, propertyKey as string, token)\n }\n}\n\n/**\n * Inject a dependency. Works in two positions:\n *\n * - **Property decorator** — `@Autowired(TOKEN) private repo!: UserRepo`.\n * Resolved lazily from the container the first time the property is\n * read.\n * - **Constructor parameter decorator** — `constructor(@Autowired(TOKEN) repo: UserRepo) {}`.\n * Resolved at instantiation; injected into the ctor call.\n *\n * `@Inject` is the same function under another name — they share\n * runtime + types so the two names are interchangeable. Pick whichever\n * reads better at the call site.\n *\n * Like `@Inject`, the typed string-literal overload narrows the token\n * to a key of the augmented `KickJsRegistry`. After `kick typegen`\n * populates the registry, `@Autowired('kick/prisma/Client')` auto-\n * completes the key and typo'd literals become TS2345 errors.\n */\nexport function Autowired<K extends keyof KickJsRegistry & string>(\n token: K,\n): PropertyOrParameterDecorator\nexport function Autowired(token?: unknown): PropertyOrParameterDecorator\nexport function Autowired(token?: unknown): PropertyOrParameterDecorator {\n return ((\n target: object,\n propertyKey: string | symbol | undefined,\n parameterIndex?: number,\n ): void => {\n applyInjection(token, target, propertyKey, parameterIndex)\n }) as PropertyOrParameterDecorator\n}\n\n/**\n * Inject a dependency by token. Same shape as {@link Autowired} —\n * works as either a property decorator or a constructor-parameter\n * decorator. Pick whichever name reads better at the call site.\n *\n * ## Typed string-literal overload\n *\n * When called with a string literal that matches a key of the\n * augmented `KickJsRegistry`, TypeScript narrows the parameter type\n * to the registered type. Typo'd literals become compile errors\n * (the string isn't `keyof KickJsRegistry`). Class identities and\n * `InjectionToken<T>` keep working unchanged via the second overload.\n *\n * @example\n * ```ts\n * import { Service, Inject } from '@forinda/kickjs'\n * import type { PrismaClient } from '@prisma/client'\n *\n * `@Service()`\n * class UserRepo {\n * constructor(@Inject('kick/prisma/Client') private db: PrismaClient) {}\n * // OR property form, same decorator:\n * @Inject('kick/prisma/Client') private db2!: PrismaClient\n * }\n * ```\n *\n * After `kick typegen` runs, the literal autocompletes from the\n * registry and a typo (`@Inject('kick/prisma/Cleint')`) becomes a\n * TS2345 error.\n */\nexport function Inject<K extends keyof KickJsRegistry & string>(\n token: K,\n): PropertyOrParameterDecorator\nexport function Inject(token?: unknown): PropertyOrParameterDecorator\nexport function Inject(token?: unknown): PropertyOrParameterDecorator {\n return ((\n target: object,\n propertyKey: string | symbol | undefined,\n parameterIndex?: number,\n ): void => {\n applyInjection(token, target, propertyKey, parameterIndex)\n }) as PropertyOrParameterDecorator\n}\n\n/**\n * Global ambient registry of typed environment variables, populated by\n * `kick typegen` from the project's env schema (typically\n * `src/env.ts`'s `export default defineEnv(...)`).\n *\n * Empty by default — declarations come from `.kickjs/types/env.ts`\n * generated alongside the rest of the typegen output. Once populated,\n * `@Value('PORT')` autocompletes the key, type-checks the default\n * value, and `process.env.PORT` is also typed via the parallel\n * `NodeJS.ProcessEnv` augmentation in the same generated file.\n */\ndeclare global {\n // eslint-disable-next-line @typescript-eslint/no-empty-object-type\n interface KickEnv {}\n}\n\n/** A string key known to the typed env registry (`never` until typegen runs) */\nexport type EnvKey = keyof KickEnv & string\n\n/**\n * Look up the type of a typed env entry.\n *\n * Decorators can't propagate the property type from the key, so users\n * still need to write the property's type — but `Env<K>` lets them\n * derive it from `KickEnv` instead of duplicating the schema:\n *\n * ```ts\n * @Service()\n * class MyService {\n * @Value('PORT')\n * private port!: Env<'PORT'> // → number, narrowed from KickEnv['PORT']\n * }\n * ```\n */\nexport type Env<K extends EnvKey = EnvKey> = KickEnv[K]\n\n/**\n * Constraint that allows any string when the typed env registry is\n * empty (no `kick typegen` has run yet, so existing code with raw\n * string keys keeps working) but locks the argument down to known\n * `EnvKey` literals once the registry is populated.\n *\n * Implementation: when `EnvKey` is `never`, the conditional resolves\n * to the user's `K`, accepting anything. Once typegen has populated\n * `KickEnv`, `EnvKey` becomes a real union and the conditional forces\n * `K` to be assignable to it.\n */\ntype ValueKey<K extends string> = [EnvKey] extends [never] ? K : K & EnvKey\n\n/**\n * Inject an environment variable value. Evaluated lazily so the env\n * is available at access time, not at decoration time.\n *\n * If no default is provided and the env var is missing, throws at access time\n * to catch misconfiguration early instead of returning undefined.\n *\n * Uses metadata + instance getter to work correctly with `useDefineForClassFields`.\n *\n * Type-safety:\n * - Without `kick typegen` run, any string key is accepted (back-compat).\n * - After typegen, `KickEnv` is populated and unknown keys become a tsc\n * error. The optional `defaultValue` is type-checked against the\n * schema's inferred type for that key.\n */\nexport function Value<K extends string>(\n envKey: ValueKey<K>,\n defaultValue?: K extends EnvKey ? KickEnv[K] : unknown,\n): PropertyDecorator {\n return (target, propertyKey) => {\n setInMetaMap(METADATA.VALUE, target, propertyKey as string, { envKey, defaultValue })\n }\n}\n\n// ── @Asset — typed asset path injection ─────────────────────────────────\n\nimport type { KickAssets } from './assets'\n\n/**\n * Flatten the nested `KickAssets` augmentation (typegen-emitted in\n * PR 4) into a string-literal union of dot/slash-joined paths.\n *\n * `interface KickAssets { mails: { welcome: () => string; orders: {\n * confirmation: () => string } } }` becomes\n * `'mails/welcome' | 'mails/orders/confirmation'`.\n *\n * Returns `never` when `KickAssets` is empty (no typegen has run yet)\n * so `AssetKeyArg` falls through to accept any string.\n */\ntype FlattenAssets<T, Prefix extends string = ''> = {\n [K in keyof T & string]: T[K] extends () => string\n ? `${Prefix}${K}`\n : T[K] extends Record<string, unknown>\n ? FlattenAssets<T[K], `${Prefix}${K}/`>\n : never\n}[keyof T & string]\n\n/** Flat union of every known `<namespace>/<key>` path. */\nexport type AssetKey = FlattenAssets<KickAssets>\n\n/**\n * Constraint mirroring `ValueKey<K>` — accepts any string when\n * `AssetKey` is `never` (back-compat for adopters who haven't run\n * `kick typegen` yet) but locks the argument to known literals once\n * the typegen-augmented `KickAssets` interface is populated.\n */\ntype AssetKeyArg<K extends string> = [AssetKey] extends [never] ? K : K & AssetKey\n\n/**\n * Inject the resolved file-system path for a typed asset. Mirrors\n * `@Value`'s lazy-getter pattern — the asset is resolved on every\n * property access, NOT at class instantiation, so:\n *\n * - Tests can swap fixtures + clear the cache without re-instantiating\n * the consuming class.\n * - The first access cost is paid once per process (the manifest cache\n * in `core/assets.ts` makes subsequent calls cheap).\n *\n * Type-safety:\n *\n * - Without typegen, any string key is accepted (back-compat — same\n * trick as `@Value`).\n * - After typegen, `KickAssets` is populated and `AssetKey` is a\n * union of every flattened `<namespace>/<key>` path. Unknown keys\n * become a TS error.\n *\n * @example\n * ```ts\n * import { Service, Asset } from '@forinda/kickjs'\n *\n * `@Service()`\n * class MailService {\n * `@Asset('mails/welcome')`\n * private welcomeTemplate!: string\n *\n * send(user: User) {\n * return ejs.renderFile(this.welcomeTemplate, { user })\n * }\n * }\n * ```\n */\nexport function Asset<K extends string>(assetKey: AssetKeyArg<K>): PropertyDecorator {\n return (target, propertyKey) => {\n setInMetaMap(METADATA.ASSET, target, propertyKey as string, { assetKey })\n }\n}\n\n// ── HTTP Route Decorators ───────────────────────────────────────────────\n\nexport interface RouteDefinition {\n method: string\n path: string\n handlerName: string\n validation?: {\n /** JSON Schema object for validating the request body */\n body?: any\n /** JSON Schema object for validating query parameters */\n query?: any\n /** JSON Schema object for validating URL params */\n params?: any\n /**\n * DECLARED response schema — never validated at runtime. Feeds two\n * consumers from one source of truth: the Swagger adapter's success\n * response schema, and `kick typegen`'s `KickRoutes[...].response`\n * (winning over return-type inference when present).\n */\n response?: any\n /** Schema name in OpenAPI components/schemas for the request body. Auto-generated from handler name if omitted. */\n name?: string\n }\n}\n\nfunction createRouteDecorator(method: string) {\n return (path?: string, validation?: RouteDefinition['validation']): MethodDecorator => {\n return (target, propertyKey) => {\n pushClassMeta<RouteDefinition>(METADATA.ROUTES, target.constructor, {\n method,\n path: path || '/',\n handlerName: propertyKey as string,\n validation,\n })\n }\n }\n}\n\nexport const Get = createRouteDecorator('GET')\nexport const Post = createRouteDecorator('POST')\nexport const Put = createRouteDecorator('PUT')\nexport const Delete = createRouteDecorator('DELETE')\nexport const Patch = createRouteDecorator('PATCH')\n\n// ── Query Params Decorator ─────────────────────────────────────────────\n\nexport interface ApiQueryParamsConfig {\n /** Fields that can be used in filter queries (e.g., `?filter=status:eq:active`) */\n filterable?: string[]\n /** Fields that can be used in sort queries (e.g., `?sort=createdAt:desc`) */\n sortable?: string[]\n /** Fields that are searched with the `?q=` parameter */\n searchable?: string[]\n}\n\n/**\n * Column-object-based query params config (e.g., from DrizzleQueryParamsConfig).\n * `Object.keys()` is used to derive field names for OpenAPI docs.\n */\nexport interface ColumnApiQueryParamsConfig {\n columns: Record<string, any>\n sortable?: Record<string, any>\n searchColumns?: any[]\n [key: string]: any\n}\n\n/**\n * Normalize a query params config to the string-based ApiQueryParamsConfig.\n * Handles both string-based and column-object-based configs.\n */\nexport function normalizeApiQueryParamsConfig(\n config: ApiQueryParamsConfig | ColumnApiQueryParamsConfig,\n): ApiQueryParamsConfig {\n if ('columns' in config && config.columns && typeof config.columns === 'object') {\n return {\n filterable: Object.keys(config.columns),\n sortable: config.sortable ? Object.keys(config.sortable) : undefined,\n searchable: config.searchColumns\n ? config.searchColumns.map((col: any) => col?.name ?? '').filter(Boolean)\n : undefined,\n }\n }\n return config as ApiQueryParamsConfig\n}\n\n/**\n * Document the query parameters accepted by a GET endpoint.\n * Used by SwaggerAdapter to generate `filter`, `sort`, `page`, `limit`, and `q` params\n * in the OpenAPI spec, with descriptions listing the allowed fields.\n *\n * Accepts both string-based configs and column-object configs (e.g., DrizzleQueryParamsConfig).\n *\n * @example\n * ```ts\n * // String-based\n * @ApiQueryParams({\n * filterable: ['status', 'category', 'price'],\n * sortable: ['name', 'createdAt', 'price'],\n * searchable: ['name', 'description'],\n * })\n *\n * // Column-object-based (Drizzle)\n * @ApiQueryParams(TASK_QUERY_CONFIG)\n * ```\n */\nexport function ApiQueryParams(\n config: ApiQueryParamsConfig | ColumnApiQueryParamsConfig,\n): MethodDecorator {\n return (target, propertyKey) => {\n const normalized = normalizeApiQueryParamsConfig(config)\n setMethodMeta(METADATA.QUERY_PARAMS, normalized, target.constructor, propertyKey as string)\n }\n}\n\n// ── Middleware Decorators ───────────────────────────────────────────────\n\n/**\n * Middleware handler function.\n * Generic `TCtx` defaults to `any` — import `RequestContext` from\n * `@forinda/kickjs` for full type safety:\n *\n * ```ts\n * import type { MiddlewareHandler } from '@forinda/kickjs'\n * import type { RequestContext } from '@forinda/kickjs'\n *\n * const auth: MiddlewareHandler<RequestContext> = (ctx, next) => {\n * ctx.body // fully typed\n * next()\n * }\n * ```\n */\nexport type MiddlewareHandler<TCtx = any> = (ctx: TCtx, next: () => void) => void | Promise<void>\n\n/** Attach middleware handlers to a class or method */\nexport function Middleware(...handlers: MiddlewareHandler[]): ClassDecorator & MethodDecorator {\n return (target: any, propertyKey?: string | symbol) => {\n if (propertyKey) {\n pushMethodMeta(\n METADATA.METHOD_MIDDLEWARES,\n target.constructor,\n propertyKey as string,\n ...handlers,\n )\n } else {\n pushClassMeta(METADATA.CLASS_MIDDLEWARES, target, ...handlers)\n }\n }\n}\n\n// ── File Upload Decorator ───────────────────────────────────────────────\n\n/**\n * File filter — accepts short extensions ('jpg'), full MIME types ('image/jpeg'),\n * wildcards ('image/*'), or a function `(mimetype, filename) => boolean` for full control.\n */\nexport type FileTypeFilter = string[] | ((mimetype: string, filename: string) => boolean)\n\n/**\n * Shared upload options used by both the `@FileUpload` decorator and the\n * `upload.single()` / `upload.array()` / `upload.none()` middleware.\n */\nexport interface BaseUploadOptions {\n /** Max file size in bytes (default: 5MB) */\n maxSize?: number\n /**\n * Allowed file types:\n * - **string[]** — short extensions ('jpg'), full MIME types ('image/jpeg'), or wildcards ('image/*')\n * - **function** — `(mimetype, filename) => boolean` for full control\n */\n allowedTypes?: FileTypeFilter\n /** Extend the built-in extension-to-MIME map */\n customMimeMap?: Record<string, string>\n}\n\nexport interface FileUploadConfig extends BaseUploadOptions {\n mode: 'single' | 'array' | 'none'\n /** Form field name (default: 'file') */\n fieldName?: string\n /** Max files for array mode (default: 10) */\n maxCount?: number\n}\n\n/** Configure file upload handling for a controller method */\nexport function FileUpload(config: FileUploadConfig): MethodDecorator {\n return (target, propertyKey) => {\n setMethodMeta(METADATA.FILE_UPLOAD, config, target.constructor, propertyKey as string)\n }\n}\n\n// ── Builder Decorator ───────────────────────────────────────────────────\n\nfunction attachBuilder(target: any): void {\n setClassMeta(METADATA.BUILDER, true, target)\n target.builder = function () {\n const props: Record<string, any> = {}\n const proxy: any = new Proxy(\n {},\n {\n get(_, key) {\n if (key === 'build') {\n return () => Object.assign(new target(), props)\n }\n return (value: any) => {\n props[key as string] = value\n return proxy\n }\n },\n },\n )\n return proxy\n }\n}\n\n/**\n * Add a static `builder()` method for fluent construction.\n *\n * Legacy decorators cannot widen the class type, so by default the\n * caller sees the unmodified shape. Opt into typing by adding a single\n * `declare static` line:\n *\n * ```ts\n * @Builder\n * class UserDto {\n * name!: string\n * email!: string\n * declare static readonly builder: () => BuilderOf<UserDto>\n * }\n * ```\n *\n * `readonly` keeps SonarQube's `typescript:S1444` quiet — the runtime\n * assigns `target.builder` once at decoration time and never again.\n *\n * If you'd rather not write the `declare`, use `withBuilder()` instead —\n * same runtime, types inferred automatically.\n */\nexport function Builder(target: any): void {\n attachBuilder(target)\n}\n\n/**\n * Factory variant of `@Builder`. Wraps the class with the same runtime\n * and returns it intersected with `{ builder(): BuilderOf<T> }` so the\n * static is fully typed without any `declare` boilerplate.\n *\n * ```ts\n * class UserDtoBase {\n * name!: string\n * email!: string\n * }\n * export const UserDto = withBuilder(UserDtoBase)\n * export type UserDto = InstanceType<typeof UserDto>\n *\n * UserDto.builder().name('Alice').email('a@b.com').build() // fully typed\n * ```\n */\nexport function withBuilder<C extends new (...args: any[]) => any>(\n ctor: C,\n): C & { builder(): BuilderOf<InstanceType<C>> } {\n attachBuilder(ctor)\n return ctor as C & { builder(): BuilderOf<InstanceType<C>> }\n}\n"],"mappings":"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"}
|
package/dist/core/errors.mjs
CHANGED
package/dist/core/interfaces.mjs
CHANGED
package/dist/core/logger.mjs
CHANGED
package/dist/core/path.mjs
CHANGED
package/dist/core/plugin.d.mts
CHANGED
package/dist/core/plugin.mjs
CHANGED
package/dist/core/reactivity.mjs
CHANGED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @forinda/kickjs v9.0.0
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) Felix Orinda
|
|
5
|
+
*
|
|
6
|
+
* This source code is licensed under the MIT license found in the
|
|
7
|
+
* LICENSE file in the root directory of this source tree.
|
|
8
|
+
*
|
|
9
|
+
* @license MIT
|
|
10
|
+
*/
|
|
11
|
+
import{l as __require}from"./env-DMAdjj8Z.mjs";import{METADATA}from"./core/interfaces.mjs";import{i as createToken,p as _setAssetResolver}from"./container-Dw45sa0Y.mjs";import"./core/logger.mjs";import{c as pushClassMeta,l as pushMethodMeta}from"./metadata-BQG1MmqL.mjs";import"./core/decorators.mjs";import"./core/cron.mjs";import"./jobs-C8_ESbx0.mjs";import"./core/cache.mjs";import{n as assertFlagTest}from"./route-flag-GipNvQ4O.mjs";import"./contributor-runner-DLvBA96K.mjs";import"reflect-metadata";import{existsSync,readFileSync,readdirSync}from"node:fs";import{isAbsolute,join,resolve,sep}from"node:path";function defineContextDecoratorImpl(spec){if(typeof spec!=`object`||!spec)throw TypeError(`defineContextDecorator: spec must be an object literal. See architecture.md §20.4 for the spec shape.`);if(typeof spec.key!=`string`||spec.key.length===0){let got=typeof spec.key==`string`?`""`:typeof spec.key;throw TypeError(`defineContextDecorator: spec.key must be a non-empty string (got ${got}). Pick a name that matches a key declared on \`ContextMeta\` (or one of the framework keys).`)}if(typeof spec.resolve!=`function`)throw TypeError(`defineContextDecorator(${spec.key}): spec.resolve is required and must be a function (got ${typeof spec.resolve}). Each contributor needs a resolver that produces the value to write into ctx.set(${JSON.stringify(spec.key)}, …).`);if(spec.onError!==void 0&&typeof spec.onError!=`function`)throw TypeError(`defineContextDecorator(${spec.key}): spec.onError must be a function when provided (got ${typeof spec.onError}).`);if(spec.dependsOn!==void 0&&!Array.isArray(spec.dependsOn))throw TypeError(`defineContextDecorator(${spec.key}): spec.dependsOn must be an array when provided (got ${typeof spec.dependsOn}).`);if(spec.requiredParams!==void 0&&!Array.isArray(spec.requiredParams))throw TypeError(`defineContextDecorator(${spec.key}): spec.requiredParams must be an array when provided (got ${typeof spec.requiredParams}).`);if(Array.isArray(spec.requiredParams)&&spec.requiredParams.some(p=>typeof p!=`string`||p.length===0))throw TypeError(`defineContextDecorator(${spec.key}): spec.requiredParams entries must be non-empty strings.`);if(Array.isArray(spec.dependsOn)&&spec.dependsOn.some(dep=>typeof dep!=`string`||dep.length===0))throw TypeError(`defineContextDecorator(${spec.key}): spec.dependsOn entries must be non-empty strings.`);let definedAt=Error().stack,defaults=Object.freeze({...spec.paramDefaults??{}}),sharedDeps=Object.freeze({...spec.deps??{}}),sharedDependsOn=Object.freeze([...spec.dependsOn??[]]),sharedOptional=spec.optional??!1,buildRegistration=params=>{let frozenParams=Object.freeze({...params}),onError=spec.onError?(err,ctx)=>spec.onError(err,ctx,frozenParams):void 0;return spec.skipWhen!==void 0&&assertFlagTest(spec.skipWhen,`contributor '${spec.key}' skipWhen`),spec.onlyWhen!==void 0&&assertFlagTest(spec.onlyWhen,`contributor '${spec.key}' onlyWhen`),Object.freeze({key:spec.key,deps:sharedDeps,dependsOn:sharedDependsOn,optional:sharedOptional,skipWhen:spec.skipWhen,onlyWhen:spec.onlyWhen,beforeValidation:spec.beforeValidation===!0,onError,resolve:(ctx,deps)=>spec.resolve(ctx,deps,frozenParams),definedAt})},defaultRegistration=buildRegistration({...defaults}),isDecoratorCall=args=>args.length===0?!1:args.length>=2||typeof args[0]==`function`,applyAsDecorator=(registration,target,propertyKey)=>{if(propertyKey===void 0)pushClassMeta(METADATA.CLASS_CONTRIBUTORS,target,registration);else{let constructor=target.constructor;pushMethodMeta(METADATA.METHOD_CONTRIBUTORS,constructor,String(propertyKey),registration)}},isPlainObject=value=>{let proto=Object.getPrototypeOf(value);return proto===Object.prototype||proto===null},mergeParams=override=>{if(override===void 0)return{...defaults};if(typeof override!=`object`||!override||Array.isArray(override)||!isPlainObject(override)){let got=override===null?`null`:Array.isArray(override)?`array`:typeof override==`object`?`instance of ${override.constructor?.name??`unknown class`}`:typeof override;throw TypeError(`defineContextDecorator(${spec.key}): factory call requires a plain object literal, got ${got}. Class instances and built-ins (Map, Date, etc) silently spread to empty objects — call the decorator with a spread object literal instead, e.g. \`MyDecorator({ ...someInstance })\`.`)}return{...defaults,...override}},sharedRequiredParams=Object.freeze([...spec.requiredParams??[]]),assertRequiredParams=(params,usage)=>{if(sharedRequiredParams.length===0)return;let missing=sharedRequiredParams.filter(name=>params[name]===void 0);if(missing.length!==0)throw TypeError(`defineContextDecorator(${spec.key}): ${usage} is missing required param(s) ${missing.map(m=>`'${m}'`).join(`, `)}. Supply them at the call site — e.g. \`@${spec.key}({ ${missing[0]}: … })\` — or add a default in \`paramDefaults\` if one is genuinely correct for every route.`)};function decoratorOrFactory(...args){if(isDecoratorCall(args)){let[target,propertyKey]=args;assertRequiredParams({...defaults},"bare `@decorator` usage"),applyAsDecorator(defaultRegistration,target,propertyKey);return}let params=mergeParams(args[0]);assertRequiredParams(params,`factory call`);let registration=buildRegistration(params);return(target,propertyKey)=>{applyAsDecorator(registration,target,propertyKey)}}Object.defineProperty(decoratorOrFactory,"name",{value:`ContextDecorator(${spec.key})`,writable:!1,enumerable:!1,configurable:!0});let decorator=Object.assign(decoratorOrFactory,{with:params=>{let merged=mergeParams(params);return assertRequiredParams(merged,"`.with()`"),{registration:buildRegistration(merged)}}});return Object.defineProperty(decorator,"registration",{get(){return assertRequiredParams({...defaults},"`.registration`"),defaultRegistration},enumerable:!0,configurable:!1}),Object.freeze(decorator)}const defineContextDecorator=Object.freeze(Object.assign(defineContextDecoratorImpl,{withParams:()=>spec=>defineContextDecoratorImpl(spec)}));function groupAssetKeys(namespace,paths,opts={}){let strategy=opts.strategy??`auto`;if(strategy===`with-extension`)return{pairs:paths.map(rel=>({rel,key:`${namespace}/${rel}`})),collisionGroupsResolved:0};if(strategy===`strip`)return{pairs:paths.map(rel=>({rel,key:`${namespace}/${stripExt(rel)}`})),collisionGroupsResolved:0};let groups=new Map;for(let rel of paths){let strippedKey=`${namespace}/${stripExt(rel)}`,arr=groups.get(strippedKey);arr?arr.push(rel):groups.set(strippedKey,[rel])}let collisionGroupsResolved=0,keyByRel=new Map;for(let[strippedKey,rels]of groups)if(rels.length===1)keyByRel.set(rels[0],strippedKey);else{collisionGroupsResolved+=1;for(let rel of rels)keyByRel.set(rel,`${namespace}/${rel}`)}return{pairs:paths.map(rel=>({rel,key:keyByRel.get(rel)??`${namespace}/${rel}`})),collisionGroupsResolved}}function stripExt(path){let lastSlash=path.lastIndexOf(`/`),filename=lastSlash>=0?path.slice(lastSlash+1):path,lastDot=filename.lastIndexOf(`.`);return lastDot<=0?path:(lastSlash>=0?path.slice(0,lastSlash+1):``)+filename.slice(0,lastDot)}const ASSET_MANIFEST_VERSION=1,ASSETS=createToken(`kick/assets/Map`);var UnknownAssetError=class extends Error{namespace;key;constructor(namespace,key){super(`Unknown asset '${namespace}/${key}'. Make sure the namespace exists in kick.config.ts assetMap and that the file is present in the source directory.`),this.name=`UnknownAssetError`,this.namespace=namespace,this.key=key}};let manifestCache;function toPosixPath(p){return p.replaceAll(`\\`,`/`)}function resolveAsset(namespace,key){let resolved=loadManifest();if(!resolved)throw new UnknownAssetError(namespace,key);let entry=resolved.manifest.entries[`${namespace}/${key}`];if(!entry)throw new UnknownAssetError(namespace,key);return toPosixPath(isAbsolute(entry)?entry:resolve(resolved.root,entry))}function clearAssetCache(){manifestCache=void 0}function loadManifest(){return process.env.NODE_ENV===`production`?(manifestCache===void 0&&(manifestCache=discoverManifest()),manifestCache):discoverManifest()}function discoverManifest(){let envRoot=process.env.KICK_ASSETS_ROOT;if(envRoot){let fromEnv=readBuiltManifest(envRoot);if(fromEnv)return fromEnv}let cwd=process.cwd(),config=loadConfigSync(cwd);if(process.env.NODE_ENV!==`production`){let fresh=synthesiseDevManifest(cwd,config);if(fresh)return fresh}let configOutDir=config?.build?.outDir;if(configOutDir&&!STANDARD_OUT_DIRS.includes(configOutDir)){let found=readBuiltManifest(resolve(cwd,configOutDir));if(found)return found}for(let candidate of STANDARD_OUT_DIRS){let found=readBuiltManifest(join(cwd,candidate));if(found)return found}return synthesiseDevManifest(cwd,config)}const STANDARD_OUT_DIRS=[`dist`,`build`,`out`];function readBuiltManifest(dir){let path=join(dir,`.kickjs-assets.json`);if(!existsSync(path))return null;try{let raw=readFileSync(path,`utf-8`),parsed=JSON.parse(raw);return parsed.version!==1||!parsed.entries||typeof parsed.entries!=`object`?null:{manifest:parsed,root:resolve(dir)}}catch{return null}}function synthesiseDevManifest(cwd,preloadedConfig){let config=preloadedConfig===void 0?loadConfigSync(cwd):preloadedConfig;if(!config?.assetMap)return null;let entries={};for(let[namespace,entry]of Object.entries(config.assetMap)){if(!entry||typeof entry.src!=`string`)continue;let srcAbs=resolve(cwd,entry.src);if(!existsSync(srcAbs))continue;let collected=[];walkSync(srcAbs,srcAbs,(relPath,absPath)=>{(!entry.glob||matchesGlobLite(relPath,entry.glob,namespace))&&collected.push({rel:relPath,abs:absPath})}),collected.sort((a,b)=>a.rel.localeCompare(b.rel));let{pairs,collisionGroupsResolved}=groupAssetKeys(namespace,collected.map(c=>c.rel),{strategy:entry.keys??`auto`}),absByRel=new Map(collected.map(c=>[c.rel,c.abs]));for(let{rel,key}of pairs){let abs=absByRel.get(rel);abs&&(entries[key]=abs)}collisionGroupsResolved>0&&process.env.NODE_ENV!==`production`&&console.log(`[kickjs/assets] '${namespace}': ${collisionGroupsResolved} basename collision(s) auto-resolved by keeping extensions (set assetMap.${namespace}.keys to 'strip' or 'with-extension' to override).`)}return{manifest:{version:1,entries},root:cwd}}const warnedGlobs=new Set;function matchesGlobLite(relPath,pattern,namespace){if(pattern===`**/*`||pattern===`**`)return!0;let singleExt=/^\*\*\/\*\.(\w+)$/.exec(pattern);if(singleExt)return relPath.endsWith(`.${singleExt[1]}`);let braceExt=/^\*\*\/\*\.\{([^}]+)\}$/.exec(pattern);if(braceExt)return braceExt[1].split(`,`).map(e=>e.trim()).some(ext=>relPath.endsWith(`.${ext}`));let tag=`${namespace}::${pattern}`;return warnedGlobs.has(tag)||(warnedGlobs.add(tag),console.warn(`[kickjs/assets] Dev-mode glob '${pattern}' on '${namespace}' isn't recognised by the lite matcher. Falling back to '**/*'. Run 'kick build:assets' to use the full glob engine.`)),!0}function walkSync(root,dir,onFile){let entries;try{entries=readdirSync(dir,{withFileTypes:!0})}catch{return}for(let entry of entries){if(entry.name===`node_modules`||entry.name.startsWith(`.`))continue;let full=join(dir,entry.name);entry.isDirectory()?walkSync(root,full,onFile):entry.isFile()&&onFile(full.slice(root.length+1).split(sep).join(`/`),full)}}function loadConfigSync(cwd){let jsonPath=join(cwd,`kick.config.json`);if(existsSync(jsonPath))try{return JSON.parse(readFileSync(jsonPath,`utf-8`))}catch{return null}for(let filename of[`kick.config.cjs`,`kick.config.js`]){let path=join(cwd,filename);if(existsSync(path))try{let{createRequire}=__require(`node:module`),mod=createRequire(join(cwd,`package.json`))(path);return mod.default??mod??null}catch{continue}}let snapshotPath=join(cwd,`.kickjs`,`kick.config.json`);if(existsSync(snapshotPath))try{return JSON.parse(readFileSync(snapshotPath,`utf-8`))}catch{return null}return null}const PROMISE_DETECTION_KEYS=new Set([`then`]),PASSTHROUGH_KEYS=new Set([`toString`,`valueOf`,`constructor`,`prototype`,`name`,`length`,`asymmetricMatch`,`nodeType`,Symbol.toPrimitive,Symbol.toStringTag,Symbol.iterator,Symbol.asyncIterator]);function createNamespaceProxy(parts){return new Proxy(function(){},{apply(){if(parts.length<2)throw new UnknownAssetError(parts[0]??``,``);let[namespace,...rest]=parts;return resolveAsset(namespace,rest.join(`/`))},get(t,prop){if(!PROMISE_DETECTION_KEYS.has(prop)){if(PASSTHROUGH_KEYS.has(prop))return Reflect.get(t,prop);if(typeof prop!=`symbol`)return createNamespaceProxy([...parts,prop])}}})}function createAssetProxy(){return new Proxy({},{get(t,prop){if(!PROMISE_DETECTION_KEYS.has(prop)){if(PASSTHROUGH_KEYS.has(prop))return Reflect.get(t,prop);if(typeof prop!=`symbol`)return createNamespaceProxy([prop])}}})}const assets=createAssetProxy();function useAssets(){return assets}_setAssetResolver(resolveAsset);export{clearAssetCache as a,groupAssetKeys as c,assets as i,stripExt as l,ASSET_MANIFEST_VERSION as n,resolveAsset as o,UnknownAssetError as r,useAssets as s,ASSETS as t,defineContextDecorator as u};
|
|
12
|
+
//# sourceMappingURL=core-CCwlN0Gn.mjs.map
|