@forinda/kickjs 5.12.0 → 5.13.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/dist/{adapter-BRvrS9xR.d.mts → adapter-D1NzavIt.d.mts} +4 -4
- package/dist/{adapter-BRvrS9xR.d.mts.map → adapter-D1NzavIt.d.mts.map} +1 -1
- package/dist/{app-module-BE7hAASB.d.mts → app-module-iAA20Cc6.d.mts} +3 -3
- package/dist/{app-module-BE7hAASB.d.mts.map → app-module-iAA20Cc6.d.mts.map} +1 -1
- package/dist/application-CbRBh5Dd.mjs +12 -0
- package/dist/application-CbRBh5Dd.mjs.map +1 -0
- package/dist/{application-BNxhBgji.d.mts → application-ChL6ufgq.d.mts} +6 -6
- package/dist/application-ChL6ufgq.d.mts.map +1 -0
- package/dist/{bootstrap-qvftPBfr.d.mts → bootstrap-Cgt_j-0Q.d.mts} +2 -2
- package/dist/{bootstrap-qvftPBfr.d.mts.map → bootstrap-Cgt_j-0Q.d.mts.map} +1 -1
- package/dist/{bootstrap-DpnpCnL0.mjs → bootstrap-DK8o5euq.mjs} +3 -3
- package/dist/{bootstrap-DpnpCnL0.mjs.map → bootstrap-DK8o5euq.mjs.map} +1 -1
- package/dist/config/index.d.mts +1 -1
- package/dist/config/index.mjs +2 -2
- package/dist/{config-DF9KJwp8.mjs → config-CfIpCoJs.mjs} +3 -3
- package/dist/{config-DF9KJwp8.mjs.map → config-CfIpCoJs.mjs.map} +1 -1
- package/dist/{container-BZKeHc5X.d.mts → container-B51bkskE.d.mts} +2 -2
- package/dist/container-B51bkskE.d.mts.map +1 -0
- package/dist/container-CBeLvm_9.mjs +108 -0
- package/dist/container-CBeLvm_9.mjs.map +1 -0
- package/dist/{context-C2yPZ22U.d.mts → context-D7sGbb8E.d.mts} +98 -1
- package/dist/context-D7sGbb8E.d.mts.map +1 -0
- package/dist/context-DpIvcO8X.mjs +12 -0
- package/dist/context-DpIvcO8X.mjs.map +1 -0
- package/dist/{context-decorator-BYf8HdMl.d.mts → context-decorator-BzcOhlej.d.mts} +3 -3
- package/dist/{context-decorator-BYf8HdMl.d.mts.map → context-decorator-BzcOhlej.d.mts.map} +1 -1
- package/dist/core/adapter.d.mts +1 -1
- package/dist/core/adapter.mjs +1 -1
- package/dist/core/app-module.d.mts +1 -1
- package/dist/core/app-module.mjs +1 -1
- package/dist/core/cache.mjs +1 -1
- package/dist/core/container.d.mts +1 -1
- package/dist/core/container.mjs +2 -2
- package/dist/core/cron.mjs +2 -2
- package/dist/core/decorators.d.mts +1 -1
- package/dist/core/decorators.mjs +2 -2
- package/dist/core/errors.d.mts +2 -2
- package/dist/core/errors.mjs +2 -2
- package/dist/core/errors.mjs.map +1 -1
- package/dist/core/interfaces.d.mts +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-pTiSM6v2.mjs → core-Cs7Djz9w.mjs} +3 -3
- package/dist/{core-pTiSM6v2.mjs.map → core-Cs7Djz9w.mjs.map} +1 -1
- package/dist/cors-BfgqfLPd.d.mts.map +1 -1
- package/dist/csrf-DiK9zept.d.mts.map +1 -1
- package/dist/{decorators-BYi8lB37.d.mts → decorators-KbaPUBXd.d.mts} +8 -8
- package/dist/decorators-KbaPUBXd.d.mts.map +1 -0
- package/dist/{env-iO4nTPjc.mjs → env-B9_HjyB6.mjs} +3 -3
- package/dist/{env-iO4nTPjc.mjs.map → env-B9_HjyB6.mjs.map} +1 -1
- package/dist/{error-handler-Xud_3Lah.d.mts → error-handler-C4SG5o_i.d.mts} +6 -1
- package/dist/error-handler-C4SG5o_i.d.mts.map +1 -0
- package/dist/errors-DytV77Hb.d.mts +241 -0
- package/dist/errors-DytV77Hb.d.mts.map +1 -0
- package/dist/helmet-CaMbjzBG.d.mts.map +1 -1
- package/dist/http/application.d.mts +1 -1
- 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 +1 -1
- package/dist/http/context.mjs +2 -2
- package/dist/http/middleware/cors.mjs +1 -1
- package/dist/http/middleware/csrf.mjs +1 -1
- 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 +1 -1
- package/dist/http/middleware/request-id.mjs +1 -1
- package/dist/http/middleware/request-logger.mjs +2 -2
- package/dist/http/middleware/session.mjs +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 +1 -1
- package/dist/http/middleware/validate.mjs +2 -2
- 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/{index-DM1PteZT.d.mts → index-BM32LMqZ.d.mts} +4 -4
- package/dist/{index-DM1PteZT.d.mts.map → index-BM32LMqZ.d.mts.map} +1 -1
- package/dist/{index-qZqR_Mxr.d.mts → index-BYLVLFA8.d.mts} +63 -7
- package/dist/index-BYLVLFA8.d.mts.map +1 -0
- package/dist/index.d.mts +19 -19
- package/dist/index.mjs +2 -2
- package/dist/index.mjs.map +1 -1
- package/dist/{interfaces-B50n7dwl.d.mts → interfaces-ClcFeDhd.d.mts} +20 -20
- package/dist/{interfaces-B50n7dwl.d.mts.map → interfaces-ClcFeDhd.d.mts.map} +1 -1
- package/dist/logger-SKhhzmU2.d.mts.map +1 -1
- package/dist/{metadata-PnPdY-2N.mjs → metadata-epuUfh0i.mjs} +2 -2
- package/dist/{metadata-PnPdY-2N.mjs.map → metadata-epuUfh0i.mjs.map} +1 -1
- package/dist/{plugin-CyoGnRbq.d.mts → plugin-DG_jFA6h.d.mts} +5 -5
- package/dist/{plugin-CyoGnRbq.d.mts.map → plugin-DG_jFA6h.d.mts.map} +1 -1
- package/dist/{query-bbA6sG2R.mjs → query-BXkCd9cg.mjs} +2 -2
- package/dist/{query-bbA6sG2R.mjs.map → query-BXkCd9cg.mjs.map} +1 -1
- package/dist/rate-limit-DUwoICtI.d.mts.map +1 -1
- package/dist/request-id-BgglvZHN.d.mts.map +1 -1
- package/dist/request-logger-CWHbAzTE.d.mts.map +1 -1
- package/dist/{router-builder-R6dyZ-A3.d.mts → router-builder-CSAOc9MR.d.mts} +2 -2
- package/dist/{router-builder-R6dyZ-A3.d.mts.map → router-builder-CSAOc9MR.d.mts.map} +1 -1
- package/dist/session-N0hActOO.d.mts.map +1 -1
- package/dist/{spa-DFxiK7jQ.d.mts → spa-DNOAvuFy.d.mts} +2 -2
- package/dist/{spa-DFxiK7jQ.d.mts.map → spa-DNOAvuFy.d.mts.map} +1 -1
- package/dist/{upload-5goxbO9A.d.mts → upload-7RRWSKkI.d.mts} +2 -2
- package/dist/{upload-5goxbO9A.d.mts.map → upload-7RRWSKkI.d.mts.map} +1 -1
- package/dist/validate-HwAyWTwH.d.mts.map +1 -1
- package/dist/{views-BvKu3uGv.d.mts → views-srW0E4Ll.d.mts} +4 -4
- package/dist/{views-BvKu3uGv.d.mts.map → views-srW0E4Ll.d.mts.map} +1 -1
- package/package.json +9 -22
- package/dist/application-BAVtRjLt.mjs +0 -12
- package/dist/application-BAVtRjLt.mjs.map +0 -1
- package/dist/application-BNxhBgji.d.mts.map +0 -1
- package/dist/container-BZKeHc5X.d.mts.map +0 -1
- package/dist/container-Dwl5vSSQ.mjs +0 -12
- package/dist/container-Dwl5vSSQ.mjs.map +0 -1
- package/dist/context-C2yPZ22U.d.mts.map +0 -1
- package/dist/context-Cm6r9rSR.mjs +0 -12
- package/dist/context-Cm6r9rSR.mjs.map +0 -1
- package/dist/decorators-BYi8lB37.d.mts.map +0 -1
- package/dist/error-handler-Xud_3Lah.d.mts.map +0 -1
- package/dist/errors-uaHCeXRz.d.mts +0 -81
- package/dist/errors-uaHCeXRz.d.mts.map +0 -1
- package/dist/index-qZqR_Mxr.d.mts.map +0 -1
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @forinda/kickjs v5.13.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{METADATA}from"./core/interfaces.mjs";import{createLogger}from"./core/logger.mjs";import{i as getMetaRecord,n as getClassMetaOrUndefined,o as getMethodMetaOrUndefined,r as getMetaMap,t as getClassMeta}from"./metadata-epuUfh0i.mjs";import{createRequire}from"node:module";import"reflect-metadata";import{existsSync,readFileSync,readdirSync}from"node:fs";import{isAbsolute,join,resolve,sep}from"node:path";var __require=createRequire(import.meta.url);const INJECTION_TOKEN=Symbol.for(`@kickjs/InjectionToken`);function createToken(name){return Object.freeze({[INJECTION_TOKEN]:!0,name})}function isInjectionToken(value){return typeof value==`object`&&!!value&&value[INJECTION_TOKEN]===!0}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),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}}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}var KickError=class extends Error{code;summary;cause;fix;docsUrl;context;constructor(init){super(formatKickError(init,{color:!1})),this.name=`KickError`,this.code=init.code,this.summary=init.summary,init.cause!==void 0&&(this.cause=init.cause),init.fix!==void 0&&(this.fix=init.fix),init.docsUrl!==void 0&&(this.docsUrl=init.docsUrl),init.context!==void 0&&(this.context=init.context)}};function ansiEnabled(){return process.env.NO_COLOR?!1:process.env.FORCE_COLOR?!0:process.stderr?.isTTY===!0}function paint(text,code,enabled){return enabled?`[${code}m${text}[0m`:text}const c={red:(s,enabled)=>paint(s,`31`,enabled),bold:(s,enabled)=>paint(s,`1`,enabled),dim:(s,enabled)=>paint(s,`2`,enabled),cyan:(s,enabled)=>paint(s,`36`,enabled),blue:(s,enabled)=>paint(s,`34`,enabled),underline:(s,enabled)=>paint(s,`4`,enabled)};function formatKickError(err,options={}){let color=options.color??ansiEnabled(),lines=[],code=c.bold(c.red(err.code,color),color);if(lines.push(`${code}: ${err.summary}`),err.cause){lines.push(``,` ${c.bold(c.dim(`Cause:`,color),color)}`);for(let line of err.cause.split(`
|
|
12
|
+
`))lines.push(` ${highlightInline(line,color)}`)}if(err.fix){lines.push(``,` ${c.bold(c.dim(`Fix:`,color),color)}`);for(let line of err.fix.split(`
|
|
13
|
+
`))lines.push(` ${highlightInline(line,color)}`)}return err.docsUrl&&(lines.push(``,` ${c.bold(c.dim(`Docs:`,color),color)}`),lines.push(` ${c.underline(c.blue(err.docsUrl,color),color)}`)),lines.join(`
|
|
14
|
+
`)}function highlightInline(line,color){return color?line.replace(/`([^`]+)`/g,(_,inner)=>`\`${c.cyan(inner,!0)}\``):line}const DOCS_BASE=`https://forinda.github.io/kick-js`;function noProviderError(tokenName){return new KickError({code:`KICK001`,summary:`No provider for ${tokenName}`,cause:`\`${tokenName}\` was requested from the DI container but no binding is registered.
|
|
15
|
+
This usually means one of:
|
|
16
|
+
• The class is decorated with @Service() / @Repository() / @Controller(),
|
|
17
|
+
but its enclosing module isn't passed to bootstrap({ modules: [...] }).
|
|
18
|
+
• The class isn't decorated at all (decorators register the binding).
|
|
19
|
+
• You're injecting a token (created with createToken()) that nothing
|
|
20
|
+
provides — add a Container.register(TOKEN, ...) call or a module that
|
|
21
|
+
binds it.`,fix:`If \`${tokenName}\` lives in a module, add the module to bootstrap:
|
|
22
|
+
|
|
23
|
+
bootstrap({
|
|
24
|
+
modules: [
|
|
25
|
+
UsersModule, // add this
|
|
26
|
+
OtherModule,
|
|
27
|
+
],
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
If it's a custom token, register it explicitly:
|
|
31
|
+
|
|
32
|
+
const TENANT_REPO = createToken<TenantRepo>('TENANT_REPO')
|
|
33
|
+
Container.getInstance().register(TENANT_REPO, { useClass: PrismaTenantRepo })`,docsUrl:`${DOCS_BASE}/guide/dependency-injection#registering-services`,context:{token:tokenName}})}function requestScopeMiddlewareMissingError(tokenName){return new KickError({code:`KICK002`,summary:`Cannot resolve REQUEST-scoped "${tokenName}" — request scope middleware not mounted`,cause:`\`${tokenName}\` has \`scope: Scope.REQUEST\` but no AsyncLocalStorage frame is wired.
|
|
34
|
+
The framework's request-scope middleware sets up that frame; without it,
|
|
35
|
+
REQUEST-scoped resolutions have nowhere to cache the per-request instance.`,fix:`Add \`requestScopeMiddleware()\` to your global middleware pipeline,
|
|
36
|
+
or let the framework auto-mount it by using \`contextStore: 'auto'\`
|
|
37
|
+
(the default — only check this if you set it to 'manual' explicitly):
|
|
38
|
+
|
|
39
|
+
bootstrap({
|
|
40
|
+
modules,
|
|
41
|
+
// contextStore: 'auto', // default; no need to set
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
If you set \`contextStore: 'manual'\` deliberately (rare), you own the
|
|
45
|
+
ALS wrapping — call \`requestStore.run({...}, () => ...)\` around the
|
|
46
|
+
code that resolves the binding.`,docsUrl:`${DOCS_BASE}/guide/dependency-injection#scopes`,context:{token:tokenName}})}function requestScopeOutsideRequestError(tokenName){return new KickError({code:`KICK003`,summary:`Cannot resolve REQUEST-scoped "${tokenName}" outside an HTTP request`,cause:`\`${tokenName}\` has \`scope: Scope.REQUEST\` and was resolved outside
|
|
47
|
+
any HTTP request context. The request-scope middleware is wired, but
|
|
48
|
+
the call happened before a request started (e.g. in a top-level
|
|
49
|
+
import, a constructor, a cron job, a test without setup).`,fix:`If the caller is request-scoped (controller, request-scoped service),
|
|
50
|
+
make sure the call happens inside the request lifecycle, not at module
|
|
51
|
+
load time.
|
|
52
|
+
|
|
53
|
+
In tests, wrap setup in an ALS frame manually:
|
|
54
|
+
|
|
55
|
+
import { requestStore } from '@forinda/kickjs'
|
|
56
|
+
|
|
57
|
+
requestStore.run(
|
|
58
|
+
{ requestId: 'test', instances: new Map(), values: new Map() },
|
|
59
|
+
() => {
|
|
60
|
+
const svc = container.resolve(MyRequestScopedService)
|
|
61
|
+
// ...
|
|
62
|
+
},
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
If the caller is a SINGLETON, change \`${tokenName}\` to TRANSIENT or
|
|
66
|
+
SINGLETON scope, or inject a factory instead of the instance directly.`,docsUrl:`${DOCS_BASE}/guide/dependency-injection#scopes`,context:{token:tokenName}})}function envValueMissingError(envKey){return new KickError({code:`KICK004`,summary:`@Value('${envKey}'): environment variable not set and no default provided`,cause:`\`@Value('${envKey}')\` resolved but the env var isn't present and the
|
|
67
|
+
decorator didn't declare a default. Possible reasons:
|
|
68
|
+
• The variable is genuinely missing from your environment / .env file.
|
|
69
|
+
• Your \`src/env.ts\` calls \`loadEnv(envSchema)\` but \`src/index.ts\`
|
|
70
|
+
doesn't import \`./env\` before \`bootstrap()\`. Without that import
|
|
71
|
+
the schema never runs and ConfigService stays empty — \`@Value\`
|
|
72
|
+
only finds the var via the \`process.env\` fallback.`,fix:`Either set the variable in your shell / .env file:
|
|
73
|
+
|
|
74
|
+
${envKey}=...
|
|
75
|
+
|
|
76
|
+
Or add a default at the decorator:
|
|
77
|
+
|
|
78
|
+
@Value('${envKey}', { default: 'fallback' })
|
|
79
|
+
private value!: string
|
|
80
|
+
|
|
81
|
+
If you're using a schema, make sure \`src/env.ts\` is imported from
|
|
82
|
+
\`src/index.ts\` BEFORE bootstrap runs:
|
|
83
|
+
|
|
84
|
+
// src/index.ts
|
|
85
|
+
import 'reflect-metadata'
|
|
86
|
+
import './env' // ← this line is required
|
|
87
|
+
import { bootstrap } from '@forinda/kickjs'
|
|
88
|
+
// ...`,docsUrl:`${DOCS_BASE}/guide/configuration#wiring-the-schema-at-startup`,context:{envKey}})}function moduleRouteMissingControllerError(mountPath){return new KickError({code:`KICK005`,summary:`Module route at ${mountPath} requires either 'controller' or 'router'`,cause:`A module's \`routes()\` returned an entry for \`${mountPath}\` but didn't
|
|
89
|
+
specify how to build the router. The framework needs either a decorated
|
|
90
|
+
controller class (it calls \`buildRoutes()\` internally) or a hand-built
|
|
91
|
+
Express Router.`,fix:`Pass \`controller:\` for the common case:
|
|
92
|
+
|
|
93
|
+
routes() {
|
|
94
|
+
return {
|
|
95
|
+
path: '${mountPath}',
|
|
96
|
+
controller: UsersController,
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
Or pass \`router:\` for full control:
|
|
101
|
+
|
|
102
|
+
routes() {
|
|
103
|
+
return {
|
|
104
|
+
path: '${mountPath}',
|
|
105
|
+
router: myExpressRouter,
|
|
106
|
+
}
|
|
107
|
+
}`,docsUrl:`${DOCS_BASE}/guide/modules`,context:{mountPath}})}const log=createLogger(`Container`);function createReg(fields){return{kind:`unknown`,resolveCount:0,postConstructStatus:`skipped`,dependencies:[],...fields}}function tokenName(token){return typeof token==`symbol`?token.toString():isInjectionToken(token)?token.name:token?.name||String(token)}function getPersistentStore(){return globalThis.__kickjs_persistent||(globalThis.__kickjs_persistent={factories:[],instances:[],resolvedInstances:new Map}),globalThis.__kickjs_persistent}var Container=class Container{static instance;registrations=new Map;resolving=new Set;changeListeners=new Set;pendingChanges=[];notifyTimer=null;static DEBOUNCE_MS=50;static _onReady=null;static _onReset=null;static _envResolver=null;static _requestStoreProvider=null;static getInstance(){return Container.instance||=new Container,Container._onReady&&=(Container._onReady(Container.instance),null),Container.instance}static reset(){Container.instance=new Container,Container._onReset?.(Container.instance);let store=getPersistentStore();for(let{token,factory,scope}of store.factories)if(!Container.instance.has(token)){let name=tokenName(token),existing=store.resolvedInstances.get(name);Container.instance.registrations.set(token,createReg({target:Object,scope,factory,kind:`factory`,instance:existing,resolveCount:+!!existing,postConstructStatus:existing?`completed`:`skipped`}))}for(let{token,instance}of store.instances)Container.instance.has(token)||Container.instance.registrations.set(token,createReg({target:instance.constructor,scope:`singleton`,instance,kind:`instance`,postConstructStatus:`completed`,resolveCount:1}))}static create(){return new Container}register(token,target,scope=`singleton`){let kind=getClassMeta(METADATA.CLASS_KIND,target,`unknown`),reg=createReg({target,scope,kind,dependencies:this.extractDependencies(target)});this.registrations.set(token,reg),typeof token==`function`&&token.name&&this.registrations.set(`__hmr__${token.name}`,reg),this.emit(tokenName(token),`registered`,kind)}registerFactory(token,factory,scope=`singleton`){let store=getPersistentStore(),name=tokenName(token),factoryUnchanged=store.factories.find(r=>tokenName(r.token)===name)?.factory===factory,existingInstance=factoryUnchanged?store.resolvedInstances.get(name):void 0;factoryUnchanged||store.resolvedInstances.delete(name),this.registrations.set(token,createReg({target:Object,scope,factory,kind:`factory`,instance:existingInstance,resolveCount:+!!existingInstance,postConstructStatus:existingInstance?`completed`:`skipped`}));let idx=store.factories.findIndex(r=>tokenName(r.token)===name);idx>=0?store.factories[idx]={token,factory,scope}:store.factories.push({token,factory,scope}),this.emit(name,`registered`,`factory`)}registerInstance(token,instance){let store=getPersistentStore(),name=tokenName(token);this.registrations.set(token,createReg({target:instance.constructor,scope:`singleton`,instance,kind:`instance`,postConstructStatus:`completed`,resolveCount:1,firstResolvedAt:Date.now(),lastResolvedAt:Date.now()})),store.resolvedInstances.set(name,instance);let idx=store.instances.findIndex(r=>tokenName(r.token)===name);idx>=0?store.instances[idx]={token,instance}:store.instances.push({token,instance}),this.emit(name,`registered`,`instance`)}has(token){return this.registrations.has(token)}getRegistrations(){let entries=[];for(let[token,reg]of this.registrations)entries.push({token:tokenName(token),scope:reg.scope,kind:reg.kind,instantiated:reg.scope===`singleton`?reg.instance!==void 0:reg.resolveCount>0,resolveCount:reg.resolveCount,lastResolvedAt:reg.lastResolvedAt,firstResolvedAt:reg.firstResolvedAt,resolveDurationMs:reg.resolveDurationMs,postConstructStatus:reg.postConstructStatus,dependencies:reg.dependencies});return entries}resolve(token){let reg=this.registrations.get(token);if(!reg&&typeof token==`function`&&token.name&&(reg=this.registrations.get(`__hmr__${token.name}`)),!reg)throw noProviderError(tokenName(token));if(reg.scope===`singleton`&®.instance!==void 0)return reg.resolveCount++,reg.lastResolvedAt=Date.now(),this.emit(tokenName(token),`resolved`,reg.kind),reg.instance;if(reg.scope===`request`){if(!Container._requestStoreProvider)throw requestScopeMiddlewareMissingError(tokenName(token));let store=Container._requestStoreProvider();if(!store)throw requestScopeOutsideRequestError(tokenName(token));if(store.values.has(token))return reg.resolveCount++,reg.lastResolvedAt=Date.now(),this.emit(tokenName(token),`resolved`,reg.kind),store.values.get(token);if(store.instances.has(token))return reg.resolveCount++,reg.lastResolvedAt=Date.now(),this.emit(tokenName(token),`resolved`,reg.kind),store.instances.get(token);if(reg.factory){let start=performance.now(),instance=reg.factory();return reg.resolveDurationMs=performance.now()-start,reg.resolveCount++,reg.lastResolvedAt=Date.now(),reg.firstResolvedAt||=Date.now(),store.instances.set(token,instance),this.emit(tokenName(token),`resolved`,reg.kind),instance}let start=performance.now(),instance=this.createInstance(reg);return reg.resolveDurationMs=performance.now()-start,reg.resolveCount++,reg.lastResolvedAt=Date.now(),reg.firstResolvedAt||=Date.now(),store.instances.set(token,instance),this.emit(tokenName(token),`resolved`,reg.kind),instance}if(reg.factory){let start=performance.now(),instance=reg.factory();return reg.resolveDurationMs=performance.now()-start,reg.resolveCount++,reg.lastResolvedAt=Date.now(),reg.firstResolvedAt||=Date.now(),reg.scope===`singleton`&&(reg.instance=instance,getPersistentStore().resolvedInstances.set(tokenName(token),instance)),this.emit(tokenName(token),`resolved`,reg.kind),instance}if(this.resolving.has(token)){let chain=[...this.resolving].map(tokenName);throw chain.push(tokenName(token)),Error(`Circular dependency detected: ${chain.join(` -> `)}`)}this.resolving.add(token);try{let start=performance.now(),instance=this.createInstance(reg);return reg.resolveDurationMs=performance.now()-start,reg.resolveCount++,reg.lastResolvedAt=Date.now(),reg.firstResolvedAt||=Date.now(),reg.scope===`singleton`&&(reg.instance=instance),this.emit(tokenName(token),`resolved`,reg.kind),instance}finally{this.resolving.delete(token)}}bootstrap(){}onChange(callback){return this.changeListeners.add(callback),()=>this.changeListeners.delete(callback)}invalidate(token){let name=tokenName(token),reg=this.registrations.get(token)??this.registrations.get(`__hmr__${name}`);if(reg&&!getPersistentStore().resolvedInstances.has(name)){reg.instance=void 0,reg.resolveCount=0,reg.postConstructStatus=`skipped`,this.emit(name,`invalidated`,reg.kind);for(let[depToken,depReg]of this.registrations)depReg.dependencies.includes(name)&&this.invalidate(depToken)}}emit(token,event,kind){let existing=this.pendingChanges.find(p=>p.token===token&&p.event===event);existing?existing.timestamp=Date.now():this.pendingChanges.push({token,event,kind,timestamp:Date.now()}),this.notifyTimer&&clearTimeout(this.notifyTimer),this.notifyTimer=setTimeout(()=>{let batch=this.pendingChanges.splice(0);if(batch.length!==0)for(let listener of this.changeListeners)try{listener(batch)}catch{}},Container.DEBOUNCE_MS)}flushChanges(){this.notifyTimer&&=(clearTimeout(this.notifyTimer),null);let batch=this.pendingChanges.splice(0);if(batch.length!==0)for(let listener of this.changeListeners)try{listener(batch)}catch{}}createInstance(reg){let args=getClassMeta(METADATA.PARAM_TYPES,reg.target,[]).map((paramType,index)=>{let token=getMetaRecord(METADATA.INJECT,reg.target)[index]||paramType;if(reg.scope===`singleton`){let depReg=this.registrations.get(token);if(depReg&&depReg.scope===`request`)throw Error(`Cannot inject REQUEST-scoped "${tokenName(token)}" into SINGLETON "${tokenName(reg.target)}". Singletons outlive requests. Use TRANSIENT or REQUEST scope for the parent.`)}return this.resolve(token)}),instance=new reg.target(...args);this.injectProperties(instance,reg.target);let postConstruct=getClassMetaOrUndefined(METADATA.POST_CONSTRUCT,reg.target.prototype);if(postConstruct&&typeof instance[postConstruct]==`function`)try{let result=instance[postConstruct]();result&&typeof result.then==`function`?(reg.postConstructStatus=`pending`,result.then(()=>{reg.postConstructStatus=`completed`}).catch(()=>{reg.postConstructStatus=`failed`}),log.warn(`@PostConstruct on ${tokenName(reg.target)}.${String(postConstruct)}() returned a Promise but the container is synchronous. The async operation will not be awaited.`)):reg.postConstructStatus=`completed`}catch(err){reg.postConstructStatus=`failed`,log.error(err,`@PostConstruct on ${tokenName(reg.target)}.${String(postConstruct)}() threw an error`)}return instance}extractDependencies(target){let paramTypes=getClassMeta(METADATA.PARAM_TYPES,target,[]),injectTokens=getMetaRecord(METADATA.INJECT,target),ctorDeps=paramTypes.map((type,index)=>tokenName(injectTokens[index]||type)),autowiredProps=getMetaMap(METADATA.AUTOWIRED,target.prototype),propDeps=[];for(let[prop,token]of autowiredProps){let resolvedToken=token||getMethodMetaOrUndefined(METADATA.PROPERTY_TYPE,target.prototype,prop);resolvedToken&&propDeps.push(tokenName(resolvedToken))}return[...new Set([...ctorDeps,...propDeps])]}injectProperties(instance,target){let autowiredProps=getMetaMap(METADATA.AUTOWIRED,target.prototype);for(let[prop,token]of autowiredProps){let resolvedToken=token||getMethodMetaOrUndefined(METADATA.PROPERTY_TYPE,target.prototype,prop);resolvedToken&&Object.defineProperty(instance,prop,{get:()=>this.resolve(resolvedToken),enumerable:!0,configurable:!0})}let valueProps=getMetaMap(METADATA.VALUE,target.prototype);for(let[prop,config]of valueProps)Object.defineProperty(instance,prop,{get(){if(Container._envResolver){let val=Container._envResolver(config.envKey);if(val!==void 0)return val}let val=process.env[config.envKey];if(val!==void 0)return val;if(config.defaultValue!==void 0)return config.defaultValue;throw envValueMissingError(config.envKey)},enumerable:!0,configurable:!0});let assetProps=getMetaMap(METADATA.ASSET,target.prototype);if(assetProps.size>0)for(let[prop,config]of assetProps){let slashIdx=config.assetKey.indexOf(`/`);if(slashIdx===-1)throw Error(`@Asset('${config.assetKey}'): asset key must include a '/' separator (e.g. 'mails/welcome').`);let namespace=config.assetKey.slice(0,slashIdx),key=config.assetKey.slice(slashIdx+1);Object.defineProperty(instance,prop,{get(){return resolveAsset(namespace,key)},enumerable:!0,configurable:!0})}}};export{isInjectionToken as _,formatKickError as a,UnknownAssetError as c,resolveAsset as d,useAssets as f,createToken as g,INJECTION_TOKEN as h,KickError as i,assets as l,stripExt as m,tokenName as n,ASSETS as o,groupAssetKeys as p,moduleRouteMissingControllerError as r,ASSET_MANIFEST_VERSION as s,Container as t,clearAssetCache as u,__require as v};
|
|
108
|
+
//# sourceMappingURL=container-CBeLvm_9.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"container-CBeLvm_9.mjs","names":[],"sources":["../src/core/token.ts","../src/core/asset-keys.ts","../src/core/assets.ts","../src/core/kick-error.ts","../src/core/kick-errors.ts","../src/core/container.ts"],"sourcesContent":["/**\n * Collision-safe injection tokens with phantom type parameters.\n *\n * `createToken<T>(name)` is the recommended way to declare any DI token\n * that isn't a class. Each call returns a unique frozen object — even if\n * two files write `createToken<X>('Logger')`, they produce two distinct\n * references that the container treats as different tokens. Collisions\n * become impossible by construction, not by convention.\n *\n * The phantom type parameter `T` lets `container.resolve(token)` infer\n * the correct return type without any codegen.\n *\n * ## Hardening layers\n *\n * KickJS has four DI token kinds, listed from safest to riskiest:\n *\n * 1. **Class identity** — `container.resolve(UserService)`. Reference\n * equality, never collides. Type-safe via `Constructor<T>` overload.\n * 2. **`createToken<T>(name)`** — frozen object identity, never collides.\n * Type-safe via the `InjectionToken<T>` overload.\n * 3. **Symbol** — `Symbol('foo')` is unique per call but `Symbol.for('foo')`\n * is interned and CAN collide. Discouraged.\n * 4. **Raw string `@Inject('foo')`** — high collision risk, untyped.\n * Reserved for legacy code and runtime-computed token names.\n *\n * Prefer (1) and (2). Use (4) only when you have to.\n *\n * @example\n * ```ts\n * // src/tokens.ts\n * export const DATABASE_URL = createToken<string>('config.database.url')\n * export const FEATURE_FLAGS = createToken<FeatureFlags>('app.features')\n *\n * // bootstrap\n * container.registerInstance(DATABASE_URL, process.env.DATABASE_URL!)\n *\n * // anywhere else\n * const url = container.resolve(DATABASE_URL) // typed as `string`\n * ```\n *\n * @module @forinda/kickjs/core/token\n */\n\n/**\n * Symbol marker stamped on every InjectionToken so the container can\n * detect them at runtime regardless of which package created them.\n *\n * Uses `Symbol.for()` so cross-package isolation (different copies of\n * `@forinda/kickjs` loaded under different module identities) still\n * shares the same marker.\n */\nexport const INJECTION_TOKEN: unique symbol = Symbol.for('@kickjs/InjectionToken') as never\n\n/**\n * A type-safe DI token. Each `createToken()` call returns a unique frozen\n * object identified by reference, not by the `name` string. Two\n * `createToken<X>('foo')` calls in different files produce two distinct\n * tokens that the container treats independently.\n *\n * The phantom `_type` parameter is never assigned at runtime — it exists\n * solely to thread the type through `container.resolve(token)`.\n */\nexport interface InjectionToken<T> {\n readonly [INJECTION_TOKEN]: true\n /** Descriptive name used in error messages, logs, and devtools */\n readonly name: string\n /** Phantom — never assigned at runtime */\n readonly _type?: T\n}\n\n/**\n * Create a collision-safe injection token.\n *\n * The returned token is a frozen object identified by reference, not by\n * the `name` string. The `name` is used only for error messages, logs,\n * and debugging.\n */\nexport function createToken<T>(name: string): InjectionToken<T> {\n return Object.freeze({\n [INJECTION_TOKEN]: true as const,\n name,\n })\n}\n\n/** Type guard — returns `true` if `value` is an InjectionToken */\nexport function isInjectionToken(value: unknown): value is InjectionToken<unknown> {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as Record<symbol, unknown>)[INJECTION_TOKEN] === true\n )\n}\n","// Shared asset-key strategy — used by the build pipeline, the\n// runtime dev resolver, and the typegen emitter so all three layers\n// agree on what the manifest looks like.\n//\n// Three strategies:\n//\n// - `'strip'` — drop the final extension. `pages/index.pug` →\n// `pages/index`. Best for namespaces where every basename is\n// unique (mail templates, pdf invoices). Shortest autocomplete.\n//\n// - `'with-extension'` — keep the extension. `pages/index.pug` →\n// `pages/index.pug`. Required when extension siblings coexist.\n//\n// - `'auto'` (default) — strip when safe, keep extensions when not.\n// The grouping pass collects files by stripped basename; groups\n// of one keep the stripped key, groups of more than one keep the\n// full path. Backward compatible for projects with no\n// extension-siblings; lossless for projects that have them.\n//\n// The function returns the path / key pairs in walk order (input\n// order preserved) so callers can iterate without re-sorting. Sort\n// in the caller if a stable manifest order is needed.\n\nexport type AssetKeyStrategy = 'auto' | 'strip' | 'with-extension'\n\nexport interface AssetKeyPair {\n /** Source-relative path the file was discovered at. */\n rel: string\n /** Final logical key for the manifest, including the namespace prefix. */\n key: string\n}\n\nexport interface GroupAssetKeysOptions {\n /**\n * Strategy applied to this namespace. Default `'auto'`.\n *\n * Adopters override per-namespace via `assetMap.<ns>.keys` in\n * kick.config.ts when they want uniform behaviour (e.g. always\n * include the extension regardless of collisions).\n */\n strategy?: AssetKeyStrategy\n}\n\nexport interface GroupAssetKeysResult {\n pairs: AssetKeyPair[]\n /**\n * Number of collision groups resolved by keeping extensions.\n * Surfaces as an informational log (\"auto-resolved N collisions\")\n * so adopters know whether they're relying on auto-mode or not.\n * Always 0 for `'strip'` (overwrites) and `'with-extension'`\n * (extensions always preserved, no collision possible).\n */\n collisionGroupsResolved: number\n}\n\n/**\n * Compute final manifest keys for a namespace's discovered paths,\n * given the chosen strategy. Pure function — no fs / config / globals\n * involved.\n *\n * @param namespace assetMap key — e.g. `'mails'`, `'pages'`.\n * @param paths source-relative file paths (POSIX separators) discovered\n * under the namespace's `src` directory.\n * @param opts grouping options.\n */\nexport function groupAssetKeys(\n namespace: string,\n paths: readonly string[],\n opts: GroupAssetKeysOptions = {},\n): GroupAssetKeysResult {\n const strategy = opts.strategy ?? 'auto'\n\n if (strategy === 'with-extension') {\n return {\n pairs: paths.map((rel) => ({ rel, key: `${namespace}/${rel}` })),\n collisionGroupsResolved: 0,\n }\n }\n\n if (strategy === 'strip') {\n // Strict strip — last-walk-order wins on collision; no\n // collision tracking here because the docs document this as the\n // adopter's choice. The caller (build / runtime / typegen) can\n // still warn if it wants; this function just shapes keys.\n return {\n pairs: paths.map((rel) => ({ rel, key: `${namespace}/${stripExt(rel)}` })),\n collisionGroupsResolved: 0,\n }\n }\n\n // strategy === 'auto' — group by stripped basename, keep\n // extensions only on collision groups.\n const groups = new Map<string, string[]>()\n for (const rel of paths) {\n const strippedKey = `${namespace}/${stripExt(rel)}`\n const arr = groups.get(strippedKey)\n if (arr) {\n arr.push(rel)\n } else {\n groups.set(strippedKey, [rel])\n }\n }\n\n let collisionGroupsResolved = 0\n // Build a per-rel decision table so we preserve input ordering in\n // the output (manifest writers rely on stable ordering).\n const keyByRel = new Map<string, string>()\n for (const [strippedKey, rels] of groups) {\n if (rels.length === 1) {\n keyByRel.set(rels[0], strippedKey)\n } else {\n collisionGroupsResolved += 1\n for (const rel of rels) {\n keyByRel.set(rel, `${namespace}/${rel}`)\n }\n }\n }\n\n return {\n pairs: paths.map((rel) => ({\n rel,\n key: keyByRel.get(rel) ?? `${namespace}/${rel}`,\n })),\n collisionGroupsResolved,\n }\n}\n\n/** Strip the final extension from a path. `'mails/welcome.ejs'` → `'mails/welcome'`. */\nexport function stripExt(path: string): string {\n // Inline the logic instead of importing extname so this module\n // stays usable in pure-browser contexts. Same algorithm: find the\n // last dot AFTER the last separator, slice up to it.\n const lastSlash = path.lastIndexOf('/')\n const filename = lastSlash >= 0 ? path.slice(lastSlash + 1) : path\n const lastDot = filename.lastIndexOf('.')\n if (lastDot <= 0) return path // no ext, or hidden file like `.env`\n const dirPart = lastSlash >= 0 ? path.slice(0, lastSlash + 1) : ''\n return dirPart + filename.slice(0, lastDot)\n}\n","/**\n * Asset manager runtime (assets-plan.md PR 3).\n *\n * Three accessor surfaces over one resolver engine:\n *\n * - `assets.x.y()` — Proxy ambient, autocomplete-friendly default.\n * - `useAssets()` — hook returning the same Proxy; mockable per-call.\n * - `resolveAsset(ns, k)` — string resolver for dynamic dispatch.\n *\n * Resolution path:\n *\n * 1. Load the build-time manifest (`<outDir>/.kickjs-assets.json`)\n * when present. Manifest existence = \"we're running from a built\n * dist\". Fast path; mapped paths returned verbatim.\n * 2. Otherwise (dev / test) walk the assetMap src directories from\n * `kick.config.{json,js,cjs}` and synthesise the same manifest\n * shape in memory. First call pays the walk cost; subsequent\n * calls hit the cache.\n * 3. `KICK_ASSETS_ROOT` env var overrides everything — the resolver\n * treats the path as the manifest directory and skips discovery.\n *\n * @module @forinda/kickjs/core/assets\n */\n\nimport { existsSync, readFileSync, readdirSync } from 'node:fs'\nimport { isAbsolute, join, resolve, sep } from 'node:path'\nimport { createToken, type InjectionToken } from './token'\nimport { groupAssetKeys } from './asset-keys'\n\nexport const ASSET_MANIFEST_VERSION = 1 as const\n\ninterface AssetManifest {\n version: typeof ASSET_MANIFEST_VERSION\n entries: Record<string, string>\n}\n\ninterface ResolvedManifest {\n manifest: AssetManifest\n root: string\n}\n\n/**\n * Augmentable interface — `kick typegen` (assets-plan.md PR 4) emits\n * a `declare global` augmentation listing every namespace + key for\n * the project's `assetMap`. Adopters who haven't run typegen yet\n * still get a runtime-correct Proxy; only typed autocomplete waits\n * on the augmentation.\n */\nexport interface KickAssets {}\n\n/**\n * DI token for the `assets` Proxy. Adopter code that wants to\n * receive assets via constructor injection registers via\n * `container.registerInstance(ASSETS, useAssets())` in `bootstrap()`\n * and injects with `@Inject(ASSETS)`.\n */\nexport const ASSETS: InjectionToken<KickAssets> = createToken<KickAssets>('kick/assets/Map')\n\nexport class UnknownAssetError extends Error {\n readonly namespace: string\n readonly key: string\n constructor(namespace: string, key: string) {\n super(\n `Unknown asset '${namespace}/${key}'. ` +\n `Make sure the namespace exists in kick.config.ts assetMap and that the file is present in the source directory.`,\n )\n this.name = 'UnknownAssetError'\n this.namespace = namespace\n this.key = key\n }\n}\n\nlet manifestCache: ResolvedManifest | null | undefined = undefined\n\n/**\n * Convert a filesystem path to its posix (forward-slash) form. The\n * asset manifest and the public `resolveAsset` / `assets.x.y()` /\n * `useAssets()` contract returns posix paths on every platform so\n * adopters can:\n *\n * - pass the result to `express.static` / `res.sendFile` / `ejs.renderFile`\n * without worrying about Windows vs. posix separators\n * - splice the path into URLs (`href`, `src`, CDN keys) without\n * replacing `\\` with `/` by hand\n * - compare two paths from `resolveAsset` for equality on any host\n *\n * Node's `fs.*` and Express's path-handling APIs accept either\n * separator on Windows, so this normalisation is safe — the only\n * adopter code it could break is something explicitly parsing\n * Windows backslashes back out of the result, which would already be\n * brittle.\n */\nfunction toPosixPath(p: string): string {\n return p.replaceAll('\\\\', '/')\n}\n\nexport function resolveAsset(namespace: string, key: string): string {\n const resolved = loadManifest()\n if (!resolved) {\n throw new UnknownAssetError(namespace, key)\n }\n const entry = resolved.manifest.entries[`${namespace}/${key}`]\n if (!entry) {\n throw new UnknownAssetError(namespace, key)\n }\n const abs = isAbsolute(entry) ? entry : resolve(resolved.root, entry)\n return toPosixPath(abs)\n}\n\n/** Reset the manifest cache. Tests use this; production code shouldn't need it. */\nexport function clearAssetCache(): void {\n manifestCache = undefined\n}\n\nfunction loadManifest(): ResolvedManifest | null {\n // In development we deliberately skip the cache so adding or removing\n // a template file (e.g. dropping `welcome-back.ejs` into\n // `src/templates/mails/`) takes effect without a server restart. The\n // synthesise-dev-manifest path walks ~10 files; the cost is negligible\n // compared to the user-visible surprise of \"I added a file but\n // assets.x.y() throws\". Production keeps the cache because the built\n // manifest under `dist/.kickjs-assets.json` doesn't change at runtime.\n if (process.env.NODE_ENV !== 'production') {\n return discoverManifest()\n }\n if (manifestCache !== undefined) return manifestCache\n manifestCache = discoverManifest()\n return manifestCache\n}\n\nfunction discoverManifest(): ResolvedManifest | null {\n const envRoot = process.env.KICK_ASSETS_ROOT\n if (envRoot) {\n const fromEnv = readBuiltManifest(envRoot)\n if (fromEnv) return fromEnv\n }\n\n const cwd = process.cwd()\n\n // Honour `kick.config.build.outDir` BEFORE the standard probe list\n // so adopters with non-default Vite outputs (e.g. `output/`,\n // `target/`, anything else) don't have to set KICK_ASSETS_ROOT\n // every time. Cached config load — same loader the dev fallback\n // uses. Skip when outDir matches one of the standard candidates;\n // it'd be probed redundantly otherwise.\n const config = loadConfigSync(cwd)\n const configOutDir = config?.build?.outDir\n if (configOutDir && !(STANDARD_OUT_DIRS as readonly string[]).includes(configOutDir)) {\n const found = readBuiltManifest(resolve(cwd, configOutDir))\n if (found) return found\n }\n\n for (const candidate of STANDARD_OUT_DIRS) {\n const found = readBuiltManifest(join(cwd, candidate))\n if (found) return found\n }\n\n return synthesiseDevManifest(cwd, config)\n}\n\nconst STANDARD_OUT_DIRS = ['dist', 'build', 'out'] as const\n\nfunction readBuiltManifest(dir: string): ResolvedManifest | null {\n const path = join(dir, '.kickjs-assets.json')\n if (!existsSync(path)) return null\n try {\n const raw = readFileSync(path, 'utf-8')\n const parsed = JSON.parse(raw) as Partial<AssetManifest>\n if (parsed.version !== ASSET_MANIFEST_VERSION) return null\n if (!parsed.entries || typeof parsed.entries !== 'object') return null\n return { manifest: parsed as AssetManifest, root: resolve(dir) }\n } catch {\n return null\n }\n}\n\ninterface AssetMapConfigShape {\n assetMap?: Record<\n string,\n {\n src: string\n dest?: string\n glob?: string\n keys?: 'auto' | 'strip' | 'with-extension'\n }\n >\n build?: { outDir?: string }\n}\n\nfunction synthesiseDevManifest(\n cwd: string,\n preloadedConfig?: AssetMapConfigShape | null,\n): ResolvedManifest | null {\n const config = preloadedConfig === undefined ? loadConfigSync(cwd) : preloadedConfig\n if (!config?.assetMap) return null\n\n const entries: Record<string, string> = {}\n // Walk order is filesystem-dependent on Linux/macOS, so we collect\n // every match per-namespace first, then sort + feed `groupAssetKeys`\n // for stable manifest output. Build pipeline does the same — both\n // layers agree on the resulting key set.\n for (const [namespace, entry] of Object.entries(config.assetMap)) {\n if (!entry || typeof entry.src !== 'string') continue\n const srcAbs = resolve(cwd, entry.src)\n if (!existsSync(srcAbs)) continue\n\n const collected: Array<{ rel: string; abs: string }> = []\n walkSync(srcAbs, srcAbs, (relPath, absPath) => {\n if (entry.glob && !matchesGlobLite(relPath, entry.glob, namespace)) return\n collected.push({ rel: relPath, abs: absPath })\n })\n collected.sort((a, b) => a.rel.localeCompare(b.rel))\n\n const { pairs, collisionGroupsResolved } = groupAssetKeys(\n namespace,\n collected.map((c) => c.rel),\n { strategy: entry.keys ?? 'auto' },\n )\n const absByRel = new Map(collected.map((c) => [c.rel, c.abs]))\n for (const { rel, key } of pairs) {\n const abs = absByRel.get(rel)\n if (abs) entries[key] = abs\n }\n if (collisionGroupsResolved > 0 && process.env.NODE_ENV !== 'production') {\n // Dev-only nudge — production runs read the build manifest where\n // the collision was already resolved at build time.\n console.log(\n `[kickjs/assets] '${namespace}': ${collisionGroupsResolved} basename collision(s) auto-resolved by keeping extensions ` +\n `(set assetMap.${namespace}.keys to 'strip' or 'with-extension' to override).`,\n )\n }\n }\n\n return { manifest: { version: ASSET_MANIFEST_VERSION, entries }, root: cwd }\n}\n\nconst warnedGlobs = new Set<string>()\n\n/**\n * Tiny glob matcher used only by dev manifest synthesis. Covers the\n * common assetMap glob patterns (`**\\/*`, `**\\/*.ext`,\n * `**\\/*.{a,b}`). Anything exotic prints a one-time warning + falls\n * through to `**\\/*` semantics rather than silently dropping files.\n *\n * The build pipeline (`packages/cli/src/asset-manager/build.ts`) uses\n * the full `glob` package; adopters who hit the dev-mode warning\n * should run `kick build:assets` to produce a real manifest.\n */\nfunction matchesGlobLite(relPath: string, pattern: string, namespace: string): boolean {\n if (pattern === '**/*' || pattern === '**') return true\n const singleExt = /^\\*\\*\\/\\*\\.(\\w+)$/.exec(pattern)\n if (singleExt) return relPath.endsWith(`.${singleExt[1]}`)\n const braceExt = /^\\*\\*\\/\\*\\.\\{([^}]+)\\}$/.exec(pattern)\n if (braceExt) {\n const exts = braceExt[1].split(',').map((e) => e.trim())\n return exts.some((ext) => relPath.endsWith(`.${ext}`))\n }\n const tag = `${namespace}::${pattern}`\n if (!warnedGlobs.has(tag)) {\n warnedGlobs.add(tag)\n console.warn(\n `[kickjs/assets] Dev-mode glob '${pattern}' on '${namespace}' isn't recognised by the lite matcher. ` +\n `Falling back to '**/*'. Run 'kick build:assets' to use the full glob engine.`,\n )\n }\n return true\n}\n\nfunction walkSync(root: string, dir: string, onFile: (rel: string, abs: string) => void): void {\n let entries\n try {\n entries = readdirSync(dir, { withFileTypes: true })\n } catch {\n return\n }\n for (const entry of entries) {\n if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue\n const full = join(dir, entry.name)\n if (entry.isDirectory()) {\n walkSync(root, full, onFile)\n } else if (entry.isFile()) {\n const rel = full\n .slice(root.length + 1)\n .split(sep)\n .join('/')\n onFile(rel, full)\n }\n }\n}\n\n/**\n * Sync `kick.config.{json,cjs,js}` loader for dev manifest\n * synthesis. Avoids dynamic `import()` (would force every assets\n * lookup to be async). TypeScript configs (`kick.config.ts`) are\n * skipped here — adopters who use them should run `kick build:assets`\n * to produce a real manifest the resolver can read.\n */\nfunction loadConfigSync(cwd: string): AssetMapConfigShape | null {\n const jsonPath = join(cwd, 'kick.config.json')\n if (existsSync(jsonPath)) {\n try {\n return JSON.parse(readFileSync(jsonPath, 'utf-8')) as AssetMapConfigShape\n } catch {\n return null\n }\n }\n for (const filename of ['kick.config.cjs', 'kick.config.js']) {\n const path = join(cwd, filename)\n if (!existsSync(path)) continue\n try {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const { createRequire } = require('node:module') as typeof import('node:module')\n const req = createRequire(join(cwd, 'package.json'))\n const mod = req(path) as { default?: AssetMapConfigShape } & AssetMapConfigShape\n return mod.default ?? mod ?? null\n } catch {\n continue\n }\n }\n return null\n}\n\n/**\n * Keys we MUST return `undefined` for: returning anything truthy\n * here causes Promise resolution to treat the Proxy as a thenable\n * and `await` it (resolving to whatever `then.call(...)` produces).\n * Two-element list deliberately — every other coercion-related key\n * goes through {@link PASSTHROUGH_KEYS} below so `String(assets)`,\n * template literals, and `util.inspect` keep working.\n */\nconst PROMISE_DETECTION_KEYS: ReadonlySet<string | symbol> = new Set<string | symbol>(['then'])\n\n/**\n * Keys we let fall through to the proxy target (a function-shaped\n * object). The default `Object.prototype` implementations of\n * `toString` / `valueOf` etc. are correct for these — returning\n * `undefined` here would break `String(assets)` and template\n * literals (`${assets}`) because `Symbol.toPrimitive` falling through\n * to a missing `toString` throws. Adopters that hit `assets.toString`\n * get `'function () {}'` back, which is fine — the contract is\n * \"asset access returns paths\"; everything else is incidental.\n */\nconst PASSTHROUGH_KEYS: ReadonlySet<string | symbol> = new Set<string | symbol>([\n 'toString',\n 'valueOf',\n 'constructor',\n 'prototype',\n 'name',\n 'length',\n 'asymmetricMatch',\n 'nodeType',\n Symbol.toPrimitive,\n Symbol.toStringTag,\n Symbol.iterator,\n Symbol.asyncIterator,\n])\n\nfunction createNamespaceProxy(parts: readonly string[]): unknown {\n const target = function () {} as unknown as Record<string, unknown>\n return new Proxy(target, {\n apply() {\n if (parts.length < 2) {\n throw new UnknownAssetError(parts[0] ?? '', '')\n }\n const [namespace, ...rest] = parts\n return resolveAsset(namespace, rest.join('/'))\n },\n get(t, prop) {\n if (PROMISE_DETECTION_KEYS.has(prop)) return undefined\n if (PASSTHROUGH_KEYS.has(prop)) return Reflect.get(t, prop)\n if (typeof prop === 'symbol') return undefined\n return createNamespaceProxy([...parts, prop])\n },\n })\n}\n\nfunction createAssetProxy(): KickAssets {\n return new Proxy({} as Record<string, unknown>, {\n get(t, prop) {\n if (PROMISE_DETECTION_KEYS.has(prop)) return undefined\n if (PASSTHROUGH_KEYS.has(prop)) return Reflect.get(t, prop)\n if (typeof prop === 'symbol') return undefined\n return createNamespaceProxy([prop])\n },\n }) as unknown as KickAssets\n}\n\n/**\n * Variant A — the ambient Proxy. Default accessor for static call\n * sites where the namespace + key are literals known at compile time.\n *\n * @example\n * ```ts\n * import { assets } from '@forinda/kickjs'\n * const path = assets.mails.welcome()\n * ```\n */\nexport const assets: KickAssets = createAssetProxy()\n\n/**\n * Variant B — the hook accessor. Returns the same Proxy as `assets`;\n * factory shape exists for testability + DI. Mock via\n * `vi.mock('@forinda/kickjs', () => ({ useAssets: () => fakeAssets }))`.\n *\n * @example\n * ```ts\n * import { useAssets } from '@forinda/kickjs'\n * class MailService {\n * private assets = useAssets()\n * send(name: string) {\n * return ejs.renderFile(this.assets.mails[name](), {})\n * }\n * }\n * ```\n */\nexport function useAssets(): KickAssets {\n return assets\n}\n","/**\n * Structured framework error with code, cause, and actionable fix hint.\n *\n * The goal is \"the developer who hits this knows what to do next.\" A\n * cryptic `Error('No binding found for: UserService')` becomes a\n * multi-line explanation pointing at the likely cause, the exact code\n * change to apply, and a docs link.\n *\n * `.message` carries the full multi-line plain-text body so Node's\n * default `Error.toString()` and unhandled-exception printing still\n * surface the helpful version. Call {@link formatKickError} to render a\n * colorized version for terminals.\n *\n * Per-error factories live in `kick-errors.ts` — those are the public\n * entry points framework code uses. `KickError` is exported so adopters\n * can build their own structured errors using the same shape.\n */\nexport interface KickErrorInit {\n /** Short stable identifier (e.g. `KICK001`, `NO_PROVIDER`). */\n code: string\n /** One-line headline. */\n summary: string\n /** Multi-line explanation of why this happened. */\n cause?: string\n /** Multi-line actionable fix — show exact code if possible. */\n fix?: string\n /** Absolute URL to the docs page that covers this error. */\n docsUrl?: string\n /** Arbitrary structured fields for log consumers / debug tooling. */\n context?: Record<string, unknown>\n}\n\nexport class KickError extends Error {\n public readonly code: string\n public readonly summary: string\n public readonly cause?: string\n public readonly fix?: string\n public readonly docsUrl?: string\n public readonly context?: Record<string, unknown>\n\n constructor(init: KickErrorInit) {\n super(formatKickError(init, { color: false }))\n this.name = 'KickError'\n this.code = init.code\n this.summary = init.summary\n if (init.cause !== undefined) this.cause = init.cause\n if (init.fix !== undefined) this.fix = init.fix\n if (init.docsUrl !== undefined) this.docsUrl = init.docsUrl\n if (init.context !== undefined) this.context = init.context\n }\n}\n\n// ── ANSI colour helpers ────────────────────────────────────────────────\n\n/**\n * `true` when the runtime can render ANSI escape codes on stderr.\n * Honors `NO_COLOR` (https://no-color.org) and `FORCE_COLOR`.\n */\nfunction ansiEnabled(): boolean {\n if (process.env.NO_COLOR) return false\n if (process.env.FORCE_COLOR) return true\n // Node sets `isTTY` on the writable streams when stdout/stderr is a\n // terminal. KickError output goes to stderr conceptually (it's an\n // error), so we follow that stream's affordances.\n return process.stderr?.isTTY === true\n}\n\n// Use the Unicode-escape form (`\u001b`) rather than the hex-escape (`\\x1b`)\n// for the ANSI ESC byte — both produce the same character, but oxlint's\n// `no-control-character` rule flags the hex form as \"unexpected control\n// character in string literal\". Functionally identical, lint-clean.\nconst ESC = '\u001b['\nconst RESET = `${ESC}0m`\n\nfunction paint(text: string, code: string, enabled: boolean): string {\n return enabled ? `${ESC}${code}m${text}${RESET}` : text\n}\n\nconst c = {\n red: (s: string, enabled: boolean) => paint(s, '31', enabled),\n bold: (s: string, enabled: boolean) => paint(s, '1', enabled),\n dim: (s: string, enabled: boolean) => paint(s, '2', enabled),\n cyan: (s: string, enabled: boolean) => paint(s, '36', enabled),\n blue: (s: string, enabled: boolean) => paint(s, '34', enabled),\n underline: (s: string, enabled: boolean) => paint(s, '4', enabled),\n}\n\n// ── Formatter ─────────────────────────────────────────────────────────\n\nexport interface FormatKickErrorOptions {\n /**\n * Render ANSI colour codes. Defaults to TTY detection (`NO_COLOR` /\n * `FORCE_COLOR` env vars honoured). Pass `false` for log files,\n * structured loggers, or HTTP response bodies.\n */\n color?: boolean\n}\n\n/**\n * Render a {@link KickErrorInit} (or {@link KickError} instance) into a\n * multi-line human-readable string. Identical layout regardless of\n * colour — the colour flag only toggles ANSI codes.\n */\nexport function formatKickError(\n err: KickErrorInit | KickError,\n options: FormatKickErrorOptions = {},\n): string {\n const color = options.color ?? ansiEnabled()\n const lines: string[] = []\n\n // Headline: `KICK001: No provider for UserService`\n const code = c.bold(c.red(err.code, color), color)\n lines.push(`${code}: ${err.summary}`)\n\n if (err.cause) {\n lines.push('', ` ${c.bold(c.dim('Cause:', color), color)}`)\n for (const line of err.cause.split('\\n')) {\n lines.push(` ${highlightInline(line, color)}`)\n }\n }\n\n if (err.fix) {\n lines.push('', ` ${c.bold(c.dim('Fix:', color), color)}`)\n for (const line of err.fix.split('\\n')) {\n lines.push(` ${highlightInline(line, color)}`)\n }\n }\n\n if (err.docsUrl) {\n lines.push('', ` ${c.bold(c.dim('Docs:', color), color)}`)\n lines.push(` ${c.underline(c.blue(err.docsUrl, color), color)}`)\n }\n\n return lines.join('\\n')\n}\n\n/**\n * Highlight inline `code spans` in a line of cause/fix text. Anything\n * between backticks gets cyan when colour is enabled; backticks stay\n * in the output so the plain-text rendering still distinguishes\n * literals.\n */\nfunction highlightInline(line: string, color: boolean): string {\n if (!color) return line\n return line.replace(/`([^`]+)`/g, (_, inner: string) => `\\`${c.cyan(inner, true)}\\``)\n}\n","/**\n * Catalog of framework errors with structured cause + fix hints.\n *\n * Each factory function returns a {@link KickError} with a stable\n * `code` (KICK001, KICK002, …), a human-readable summary, an\n * explanation of the likely cause, an actionable fix, and a docs URL.\n *\n * Adding a new error: pick the next free code, add the factory below,\n * call it from the framework site that used to throw a bare `Error`.\n * Keep the public function name descriptive — `noProviderError`,\n * `envValueMissingError` — so call sites grep cleanly.\n *\n * Codes are stable. Reordering or renumbering breaks adopters whose\n * tools key on `err.code`.\n */\nimport { KickError } from './kick-error'\n\nconst DOCS_BASE = 'https://forinda.github.io/kick-js'\n\n// ── DI container ──────────────────────────────────────────────────────\n\n/**\n * KICK001 — No provider registered for a token.\n *\n * Thrown by `Container.resolve()` when no binding can be found for the\n * requested token. The most common cause is forgetting to add the\n * enclosing module to `bootstrap({ modules })`.\n */\nexport function noProviderError(tokenName: string): KickError {\n return new KickError({\n code: 'KICK001',\n summary: `No provider for ${tokenName}`,\n cause: `\\`${tokenName}\\` was requested from the DI container but no binding is registered.\nThis usually means one of:\n • The class is decorated with @Service() / @Repository() / @Controller(),\n but its enclosing module isn't passed to bootstrap({ modules: [...] }).\n • The class isn't decorated at all (decorators register the binding).\n • You're injecting a token (created with createToken()) that nothing\n provides — add a Container.register(TOKEN, ...) call or a module that\n binds it.`,\n fix: `If \\`${tokenName}\\` lives in a module, add the module to bootstrap:\n\n bootstrap({\n modules: [\n UsersModule, // add this\n OtherModule,\n ],\n })\n\nIf it's a custom token, register it explicitly:\n\n const TENANT_REPO = createToken<TenantRepo>('TENANT_REPO')\n Container.getInstance().register(TENANT_REPO, { useClass: PrismaTenantRepo })`,\n docsUrl: `${DOCS_BASE}/guide/dependency-injection#registering-services`,\n context: { token: tokenName },\n })\n}\n\n/**\n * KICK002 — REQUEST-scoped binding resolved without a request store\n * provider configured.\n */\nexport function requestScopeMiddlewareMissingError(tokenName: string): KickError {\n return new KickError({\n code: 'KICK002',\n summary: `Cannot resolve REQUEST-scoped \"${tokenName}\" — request scope middleware not mounted`,\n cause: `\\`${tokenName}\\` has \\`scope: Scope.REQUEST\\` but no AsyncLocalStorage frame is wired.\nThe framework's request-scope middleware sets up that frame; without it,\nREQUEST-scoped resolutions have nowhere to cache the per-request instance.`,\n fix: `Add \\`requestScopeMiddleware()\\` to your global middleware pipeline,\nor let the framework auto-mount it by using \\`contextStore: 'auto'\\`\n(the default — only check this if you set it to 'manual' explicitly):\n\n bootstrap({\n modules,\n // contextStore: 'auto', // default; no need to set\n })\n\nIf you set \\`contextStore: 'manual'\\` deliberately (rare), you own the\nALS wrapping — call \\`requestStore.run({...}, () => ...)\\` around the\ncode that resolves the binding.`,\n docsUrl: `${DOCS_BASE}/guide/dependency-injection#scopes`,\n context: { token: tokenName },\n })\n}\n\n/**\n * KICK003 — REQUEST-scoped binding resolved outside any HTTP request\n * context (no ALS store active).\n */\nexport function requestScopeOutsideRequestError(tokenName: string): KickError {\n return new KickError({\n code: 'KICK003',\n summary: `Cannot resolve REQUEST-scoped \"${tokenName}\" outside an HTTP request`,\n cause: `\\`${tokenName}\\` has \\`scope: Scope.REQUEST\\` and was resolved outside\nany HTTP request context. The request-scope middleware is wired, but\nthe call happened before a request started (e.g. in a top-level\nimport, a constructor, a cron job, a test without setup).`,\n fix: `If the caller is request-scoped (controller, request-scoped service),\nmake sure the call happens inside the request lifecycle, not at module\nload time.\n\nIn tests, wrap setup in an ALS frame manually:\n\n import { requestStore } from '@forinda/kickjs'\n\n requestStore.run(\n { requestId: 'test', instances: new Map(), values: new Map() },\n () => {\n const svc = container.resolve(MyRequestScopedService)\n // ...\n },\n )\n\nIf the caller is a SINGLETON, change \\`${tokenName}\\` to TRANSIENT or\nSINGLETON scope, or inject a factory instead of the instance directly.`,\n docsUrl: `${DOCS_BASE}/guide/dependency-injection#scopes`,\n context: { token: tokenName },\n })\n}\n\n// ── Config / env wiring ───────────────────────────────────────────────\n\n/**\n * KICK004 — `@Value('X')` resolved but the env var isn't set and no\n * default was provided.\n *\n * This is the canonical \"you forgot to wire env\" symptom — surfaces\n * when ConfigService can't find the key. The fix usually involves\n * either setting the env var or wiring `loadEnv(envSchema)` at startup.\n */\nexport function envValueMissingError(envKey: string): KickError {\n return new KickError({\n code: 'KICK004',\n summary: `@Value('${envKey}'): environment variable not set and no default provided`,\n cause: `\\`@Value('${envKey}')\\` resolved but the env var isn't present and the\ndecorator didn't declare a default. Possible reasons:\n • The variable is genuinely missing from your environment / .env file.\n • Your \\`src/env.ts\\` calls \\`loadEnv(envSchema)\\` but \\`src/index.ts\\`\n doesn't import \\`./env\\` before \\`bootstrap()\\`. Without that import\n the schema never runs and ConfigService stays empty — \\`@Value\\`\n only finds the var via the \\`process.env\\` fallback.`,\n fix: `Either set the variable in your shell / .env file:\n\n ${envKey}=...\n\nOr add a default at the decorator:\n\n @Value('${envKey}', { default: 'fallback' })\n private value!: string\n\nIf you're using a schema, make sure \\`src/env.ts\\` is imported from\n\\`src/index.ts\\` BEFORE bootstrap runs:\n\n // src/index.ts\n import 'reflect-metadata'\n import './env' // ← this line is required\n import { bootstrap } from '@forinda/kickjs'\n // ...`,\n docsUrl: `${DOCS_BASE}/guide/configuration#wiring-the-schema-at-startup`,\n context: { envKey },\n })\n}\n\n// ── Module setup ──────────────────────────────────────────────────────\n\n/**\n * KICK005 — A module's `routes()` return value declares a mount path\n * but provides neither a `controller` class nor a pre-built `router`.\n */\nexport function moduleRouteMissingControllerError(mountPath: string): KickError {\n return new KickError({\n code: 'KICK005',\n summary: `Module route at ${mountPath} requires either 'controller' or 'router'`,\n cause: `A module's \\`routes()\\` returned an entry for \\`${mountPath}\\` but didn't\nspecify how to build the router. The framework needs either a decorated\ncontroller class (it calls \\`buildRoutes()\\` internally) or a hand-built\nExpress Router.`,\n fix: `Pass \\`controller:\\` for the common case:\n\n routes() {\n return {\n path: '${mountPath}',\n controller: UsersController,\n }\n }\n\nOr pass \\`router:\\` for full control:\n\n routes() {\n return {\n path: '${mountPath}',\n router: myExpressRouter,\n }\n }`,\n docsUrl: `${DOCS_BASE}/guide/modules`,\n context: { mountPath },\n })\n}\n","import 'reflect-metadata'\nimport {\n Constructor,\n Scope,\n METADATA,\n type ClassKind,\n type PostConstructStatus,\n} from './interfaces'\nimport { resolveAsset } from './assets'\nimport {\n envValueMissingError,\n noProviderError,\n requestScopeMiddlewareMissingError,\n requestScopeOutsideRequestError,\n} from './kick-errors'\nimport { createLogger } from './logger'\nimport {\n getClassMeta,\n getClassMetaOrUndefined,\n getMetaMap,\n getMetaRecord,\n getMethodMetaOrUndefined,\n} from './metadata'\nimport { isInjectionToken, type InjectionToken } from './token'\n\nconst log = createLogger('Container')\n\n/** Internal registration entry that tracks how a dependency should be resolved */\ninterface Registration {\n target: Constructor\n scope: Scope\n instance?: any\n factory?: () => any\n // Observability fields\n kind: ClassKind\n resolveCount: number\n lastResolvedAt?: number\n firstResolvedAt?: number\n resolveDurationMs?: number\n postConstructStatus: PostConstructStatus\n dependencies: string[]\n}\n\n/** Create a Registration with observability defaults */\nfunction createReg(\n fields: Partial<Registration> & Pick<Registration, 'target' | 'scope'>,\n): Registration {\n return {\n kind: 'unknown',\n resolveCount: 0,\n postConstructStatus: 'skipped',\n dependencies: [],\n ...fields,\n }\n}\n\n/** Format a token for display in error messages */\nexport function tokenName(token: any): string {\n if (typeof token === 'symbol') return token.toString()\n if (isInjectionToken(token)) return token.name\n return token?.name || String(token)\n}\n\n// ── Persistent Store ────────────────────────────────────────────────────\n// globalThis-based storage that survives BOTH Container.reset() AND Vite's\n// ssrLoadModule() re-evaluation. When Vite re-evaluates a module, the\n// Container class gets a new identity (new statics). But globalThis persists.\n//\n// This is how DB connections, Redis clients, WsAdapter instances, and any\n// other stateful bindings survive HMR without being dropped.\n//\n// Pattern from: React Router (globalThis for state), Vinxi (globalThis.app)\n\n// ── Change Event System ─────────────────────────────────────────────────\n// Batched (50ms debounce) so `kick g module` creating 10+ files emits ONE\n// notification, not 10. Any adapter can subscribe: Swagger, DevTools, WS, etc.\n\n/** A single container change event */\nexport interface ContainerChangeEvent {\n token: string\n event: 'registered' | 'resolved' | 'invalidated'\n kind?: ClassKind\n timestamp: number\n}\n\n/** Listener that receives batched change events */\nexport type ContainerChangeListener = (changes: ContainerChangeEvent[]) => void\n\n/**\n * Type-safe registry of string DI tokens. Augmented by `kick typegen` —\n * the generated `.kickjs/types/registry.d.ts` adds entries like:\n *\n * ```ts\n * declare module '@forinda/kickjs' {\n * interface KickJsRegistry {\n * UserService: import('../../src/modules/users/user.service').UserService\n * }\n * }\n * ```\n *\n * Once typegen has run, `container.resolve('UserService')` returns the\n * correct type instead of `any`. Class-token resolution\n * (`container.resolve(UserService)`) is already type-safe via the\n * `Constructor<T>` overload and does not need typegen.\n */\nexport interface KickJsRegistry {}\n\ninterface PersistentStore {\n factories: Array<{ token: any; factory: () => any; scope: Scope }>\n instances: Array<{ token: any; instance: any }>\n /** Resolved instances keyed by string name — survives class identity changes */\n resolvedInstances: Map<string, any>\n}\n\nfunction getPersistentStore(): PersistentStore {\n if (!(globalThis as any).__kickjs_persistent) {\n ;(globalThis as any).__kickjs_persistent = {\n factories: [],\n instances: [],\n resolvedInstances: new Map(),\n } satisfies PersistentStore\n }\n return (globalThis as any).__kickjs_persistent\n}\n\n/**\n * Inversion-of-Control (IoC) container that manages dependency registration,\n * resolution, and lifecycle. Implements the Singleton pattern so all parts of\n * the application share a single container instance.\n *\n * Supports constructor injection, property injection (@Autowired),\n * factory registrations, and lifecycle hooks (@PostConstruct).\n *\n * Persistent state (DB connections, Redis, etc.) is stored on globalThis so\n * it survives both Container.reset() and Vite module re-evaluation.\n */\nexport class Container {\n private static instance: Container\n private registrations = new Map<any, Registration>()\n private resolving = new Set<any>()\n\n // ── Reactive change tracking ──────────────────────────────────────\n private changeListeners = new Set<ContainerChangeListener>()\n private pendingChanges: ContainerChangeEvent[] = []\n private notifyTimer: ReturnType<typeof setTimeout> | null = null\n private static readonly DEBOUNCE_MS = 50\n\n /** Callback set by the decorators module to flush pending registrations */\n static _onReady: ((container: Container) => void) | null = null\n /** Callback invoked on reset so decorators can update their container reference */\n static _onReset: ((container: Container) => void) | null = null\n /**\n * Environment resolver for @Value decorator. Set by the unified\n * `@forinda/kickjs` config layer to return Zod-validated, type-coerced\n * env values instead of raw process.env strings.\n */\n static _envResolver: ((key: string) => any) | null = null\n /**\n * Request store provider for REQUEST-scoped DI. Set by the HTTP layer\n * inside `@forinda/kickjs` to return the current request's\n * AsyncLocalStorage store. Returns { instances: Map, values: Map } or\n * null if outside a request.\n */\n static _requestStoreProvider:\n | (() => { instances: Map<any, any>; values: Map<any, any> } | null)\n | null = null\n\n static getInstance(): Container {\n if (!Container.instance) {\n Container.instance = new Container()\n }\n // Flush any decorator registrations that queued before the container existed\n if (Container._onReady) {\n Container._onReady(Container.instance)\n Container._onReady = null\n }\n return Container.instance\n }\n\n /**\n * Resets the container by replacing the singleton with a fresh instance.\n * Persistent registrations (DB, Redis, etc.) are replayed from globalThis\n * so they survive both reset() and Vite module re-evaluation.\n */\n static reset(): void {\n Container.instance = new Container()\n // Notify decorators so they update their container reference\n Container._onReset?.(Container.instance)\n // Replay persistent registrations from globalThis store\n const store = getPersistentStore()\n for (const { token, factory, scope } of store.factories) {\n if (!Container.instance.has(token)) {\n // Check if we have a previously resolved instance for this token\n const name = tokenName(token)\n const existing = store.resolvedInstances.get(name)\n Container.instance.registrations.set(\n token,\n createReg({\n target: Object as any,\n scope,\n factory,\n kind: 'factory',\n instance: existing, // Reuse resolved instance if available\n resolveCount: existing ? 1 : 0,\n postConstructStatus: existing ? 'completed' : 'skipped',\n }),\n )\n }\n }\n for (const { token, instance } of store.instances) {\n if (!Container.instance.has(token)) {\n Container.instance.registrations.set(\n token,\n createReg({\n target: instance.constructor,\n scope: Scope.SINGLETON,\n instance,\n kind: 'instance',\n postConstructStatus: 'completed',\n resolveCount: 1,\n }),\n )\n }\n }\n }\n\n /**\n * Create an isolated container instance (not the global singleton).\n * Useful for concurrent tests that must not share DI state.\n *\n * @example\n * ```ts\n * const container = Container.create()\n * container.register(UserService, UserService)\n * const svc = container.resolve(UserService)\n * ```\n */\n static create(): Container {\n return new Container()\n }\n\n /** Register a class constructor under the given token */\n register(token: any, target: Constructor, scope: Scope = Scope.SINGLETON): void {\n const kind = getClassMeta<ClassKind>(METADATA.CLASS_KIND, target, 'unknown')\n const dependencies = this.extractDependencies(target)\n const reg = createReg({ target, scope, kind, dependencies })\n this.registrations.set(token, reg)\n // Store a name-based fallback so HMR class re-creation (new identity)\n // can still resolve by the original class name.\n if (typeof token === 'function' && token.name) {\n this.registrations.set(`__hmr__${token.name}`, reg)\n }\n this.emit(tokenName(token), 'registered', kind)\n }\n\n /** Register a factory function under the given token */\n registerFactory(token: any, factory: () => any, scope: Scope = Scope.SINGLETON): void {\n const store = getPersistentStore()\n const name = tokenName(token)\n // Check if we already have a resolved instance from a previous HMR cycle.\n // Only reuse if the factory hasn't changed (same reference). When a new\n // factory is provided (e.g. swapping implementations), clear the stale\n // instance so the new factory runs on next resolve.\n const existingEntry = store.factories.find((r) => tokenName(r.token) === name)\n const factoryUnchanged = existingEntry?.factory === factory\n const existingInstance = factoryUnchanged ? store.resolvedInstances.get(name) : undefined\n\n // Clear stale resolved instance when factory changes\n if (!factoryUnchanged) {\n store.resolvedInstances.delete(name)\n }\n\n this.registrations.set(\n token,\n createReg({\n target: Object as any,\n scope,\n factory,\n kind: 'factory',\n instance: existingInstance,\n resolveCount: existingInstance ? 1 : 0,\n postConstructStatus: existingInstance ? 'completed' : 'skipped',\n }),\n )\n // Track in globalThis for persistence across HMR + module re-evaluation\n const idx = store.factories.findIndex((r) => tokenName(r.token) === name)\n if (idx >= 0) {\n store.factories[idx] = { token, factory, scope }\n } else {\n store.factories.push({ token, factory, scope })\n }\n this.emit(name, 'registered', 'factory')\n }\n\n /** Register a pre-constructed singleton instance */\n registerInstance(token: any, instance: any): void {\n const store = getPersistentStore()\n const name = tokenName(token)\n\n this.registrations.set(\n token,\n createReg({\n target: instance.constructor,\n scope: Scope.SINGLETON,\n instance,\n kind: 'instance',\n postConstructStatus: 'completed',\n resolveCount: 1,\n firstResolvedAt: Date.now(),\n lastResolvedAt: Date.now(),\n }),\n )\n // Track in globalThis for persistence across HMR + module re-evaluation\n store.resolvedInstances.set(name, instance)\n const idx = store.instances.findIndex((r) => tokenName(r.token) === name)\n if (idx >= 0) {\n store.instances[idx] = { token, instance }\n } else {\n store.instances.push({ token, instance })\n }\n this.emit(name, 'registered', 'instance')\n }\n\n /** Check if a binding exists for the given token */\n has(token: any): boolean {\n return this.registrations.has(token)\n }\n\n /** Return a snapshot of all registered tokens with their scope and instantiation status */\n getRegistrations(): Array<{\n token: string\n scope: string\n kind: ClassKind\n instantiated: boolean\n resolveCount: number\n lastResolvedAt?: number\n firstResolvedAt?: number\n resolveDurationMs?: number\n postConstructStatus: PostConstructStatus\n dependencies: string[]\n }> {\n const entries: Array<ReturnType<Container['getRegistrations']>[number]> = []\n for (const [token, reg] of this.registrations) {\n entries.push({\n token: tokenName(token),\n scope: reg.scope,\n kind: reg.kind,\n // Singletons check cached instance; transients check resolveCount\n instantiated:\n reg.scope === Scope.SINGLETON ? reg.instance !== undefined : reg.resolveCount > 0,\n resolveCount: reg.resolveCount,\n lastResolvedAt: reg.lastResolvedAt,\n firstResolvedAt: reg.firstResolvedAt,\n resolveDurationMs: reg.resolveDurationMs,\n postConstructStatus: reg.postConstructStatus,\n dependencies: reg.dependencies,\n })\n }\n return entries\n }\n\n /**\n * Resolve a dependency by its token.\n *\n * Four overloads, in order of preference:\n * - **`InjectionToken<T>`** (from `createToken<T>(name)`) → returns `T`.\n * Collision-safe by construction; no codegen required.\n * - **Class constructor** → returns an instance of that class. Always\n * type-safe; no codegen required.\n * - **String token in `KickJsRegistry`** (populated by `kick typegen`) →\n * returns the augmented type.\n * - **Anything else** → returns `any`. Use this only for legacy code or\n * string tokens that haven't been registered with typegen yet.\n */\n resolve<T>(token: InjectionToken<T>): T\n resolve<T>(token: Constructor<T>): T\n resolve<K extends keyof KickJsRegistry & string>(token: K): KickJsRegistry[K]\n resolve<T = any>(token: any): T\n resolve<T = any>(token: any): T {\n let reg = this.registrations.get(token)\n // HMR fallback: when Vite re-evaluates a module, decorated classes get new\n // identity. Try resolving by class name if the primary token lookup fails.\n if (!reg && typeof token === 'function' && token.name) {\n reg = this.registrations.get(`__hmr__${token.name}`)\n }\n if (!reg) {\n throw noProviderError(tokenName(token))\n }\n\n if (reg.scope === Scope.SINGLETON && reg.instance !== undefined) {\n reg.resolveCount++\n reg.lastResolvedAt = Date.now()\n this.emit(tokenName(token), 'resolved', reg.kind)\n return reg.instance\n }\n\n // REQUEST scope: one instance per HTTP request, cached in AsyncLocalStorage\n if (reg.scope === Scope.REQUEST) {\n if (!Container._requestStoreProvider) {\n throw requestScopeMiddlewareMissingError(tokenName(token))\n }\n const store = Container._requestStoreProvider()\n if (!store) {\n throw requestScopeOutsideRequestError(tokenName(token))\n }\n // Check for pre-registered request values (user, tenant, etc.)\n if (store.values.has(token)) {\n reg.resolveCount++\n reg.lastResolvedAt = Date.now()\n this.emit(tokenName(token), 'resolved', reg.kind)\n return store.values.get(token) as T\n }\n // Check per-request instance cache\n if (store.instances.has(token)) {\n reg.resolveCount++\n reg.lastResolvedAt = Date.now()\n this.emit(tokenName(token), 'resolved', reg.kind)\n return store.instances.get(token) as T\n }\n // Factory-registered binding: invoke the factory once per request\n // and cache the result alongside class-instantiated REQUEST tokens.\n // Without this branch, registerFactory(token, fn, Scope.REQUEST)\n // silently falls through to createInstance(reg) which `new`s the\n // factory's placeholder `target: Object` and returns an empty {}\n // — the factory function never runs.\n if (reg.factory) {\n const start = performance.now()\n const instance = reg.factory()\n reg.resolveDurationMs = performance.now() - start\n reg.resolveCount++\n reg.lastResolvedAt = Date.now()\n if (!reg.firstResolvedAt) reg.firstResolvedAt = Date.now()\n store.instances.set(token, instance)\n this.emit(tokenName(token), 'resolved', reg.kind)\n return instance as T\n }\n // Create instance and cache for this request only\n const start = performance.now()\n const instance = this.createInstance(reg)\n reg.resolveDurationMs = performance.now() - start\n reg.resolveCount++\n reg.lastResolvedAt = Date.now()\n if (!reg.firstResolvedAt) reg.firstResolvedAt = Date.now()\n store.instances.set(token, instance)\n this.emit(tokenName(token), 'resolved', reg.kind)\n return instance as T\n }\n\n if (reg.factory) {\n const start = performance.now()\n const instance = reg.factory()\n reg.resolveDurationMs = performance.now() - start\n reg.resolveCount++\n reg.lastResolvedAt = Date.now()\n if (!reg.firstResolvedAt) reg.firstResolvedAt = Date.now()\n if (reg.scope === Scope.SINGLETON) {\n reg.instance = instance\n // Persist resolved factory instances so they survive module re-evaluation\n getPersistentStore().resolvedInstances.set(tokenName(token), instance)\n }\n this.emit(tokenName(token), 'resolved', reg.kind)\n return instance\n }\n\n if (this.resolving.has(token)) {\n const chain = [...this.resolving].map(tokenName)\n chain.push(tokenName(token))\n throw new Error(`Circular dependency detected: ${chain.join(' -> ')}`)\n }\n this.resolving.add(token)\n\n try {\n const start = performance.now()\n const instance = this.createInstance(reg)\n reg.resolveDurationMs = performance.now() - start\n reg.resolveCount++\n reg.lastResolvedAt = Date.now()\n if (!reg.firstResolvedAt) reg.firstResolvedAt = Date.now()\n if (reg.scope === Scope.SINGLETON) {\n reg.instance = instance\n }\n this.emit(tokenName(token), 'resolved', reg.kind)\n return instance\n } finally {\n this.resolving.delete(token)\n }\n }\n\n /** Lifecycle hook called during Application.setup() after module registration */\n bootstrap(): void {\n // Reserved for future use — adapters and modules register via\n // container.register(), registerFactory(), and registerInstance().\n }\n\n // ── Reactive Change API ─────────────────────────────────────────────\n\n /**\n * Subscribe to container changes. Returns an unsubscribe function.\n *\n * Changes are batched (50ms debounce) so bulk operations like\n * `kick g module` creating 10+ files emit ONE batch, not 10 events.\n *\n * Any adapter can subscribe:\n * - SwaggerAdapter: re-build spec when controllers change\n * - DevToolsAdapter: push SSE events to dashboard\n * - WsAdapter: re-discover namespaces when controllers change\n * - OtelAdapter: emit tracing spans for DI resolution\n * - Custom user adapters\n *\n * @example\n * ```ts\n * const unsub = container.onChange((changes) => {\n * for (const c of changes) {\n * console.log(`${c.event}: ${c.token} (${c.kind})`)\n * }\n * })\n * // later:\n * unsub()\n * ```\n */\n onChange(callback: ContainerChangeListener): () => void {\n this.changeListeners.add(callback)\n return () => this.changeListeners.delete(callback)\n }\n\n /**\n * Invalidate a specific registration. Clears the cached instance so the\n * next resolve() creates a fresh one. Also invalidates dependents\n * (anything that injected this token) by walking the dependency graph.\n *\n * Called by the Vite HMR plugin when a module file changes.\n * Skips persistent registrations (DB connections, etc.).\n */\n invalidate(token: any): void {\n const name = tokenName(token)\n // Try both the token itself and the HMR fallback name\n const reg = this.registrations.get(token) ?? this.registrations.get(`__hmr__${name}`)\n if (!reg) return\n\n // Don't invalidate persistent registrations (DB, Redis, etc.)\n const store = getPersistentStore()\n if (store.resolvedInstances.has(name)) return\n\n // Clear cached instance — next resolve() will re-create\n reg.instance = undefined\n reg.resolveCount = 0\n reg.postConstructStatus = 'skipped'\n this.emit(name, 'invalidated', reg.kind)\n\n // Walk dependency graph — invalidate anything that injected this token\n for (const [depToken, depReg] of this.registrations) {\n if (depReg.dependencies.includes(name)) {\n this.invalidate(depToken)\n }\n }\n }\n\n /** Batch-emit a change event (debounced, flushed after 50ms of quiet) */\n private emit(token: string, event: ContainerChangeEvent['event'], kind?: ClassKind): void {\n // Dedupe within the pending batch: a high-throughput TRANSIENT\n // token that resolves 1000× per request would otherwise queue\n // 1000 identical 'resolved' events. The downstream listener only\n // cares that the token's state changed, so collapse to one entry\n // (the earliest timestamp wins; the latest update is reflected\n // in resolveCount/lastResolvedAt on the registration itself,\n // which the listener reads at notify time).\n const existing = this.pendingChanges.find((p) => p.token === token && p.event === event)\n if (existing) {\n existing.timestamp = Date.now()\n } else {\n this.pendingChanges.push({ token, event, kind, timestamp: Date.now() })\n }\n\n if (this.notifyTimer) clearTimeout(this.notifyTimer)\n this.notifyTimer = setTimeout(() => {\n const batch = this.pendingChanges.splice(0)\n if (batch.length === 0) return\n for (const listener of this.changeListeners) {\n try {\n listener(batch)\n } catch {\n // Don't let one broken listener break others\n }\n }\n }, Container.DEBOUNCE_MS)\n }\n\n /** Flush pending change events immediately (useful in tests) */\n flushChanges(): void {\n if (this.notifyTimer) {\n clearTimeout(this.notifyTimer)\n this.notifyTimer = null\n }\n const batch = this.pendingChanges.splice(0)\n if (batch.length === 0) return\n for (const listener of this.changeListeners) {\n try {\n listener(batch)\n } catch {\n // Don't let one broken listener break others\n }\n }\n }\n\n private createInstance(reg: Registration): any {\n const paramTypes = getClassMeta<Constructor[]>(METADATA.PARAM_TYPES, reg.target, [])\n\n const args = paramTypes.map((paramType, index) => {\n // Check for @Inject token override on constructor parameter\n const injectTokens = getMetaRecord<any>(METADATA.INJECT, reg.target)\n const token = injectTokens[index] || paramType\n // Scope validation: SINGLETON cannot inject REQUEST-scoped dependencies\n if (reg.scope === Scope.SINGLETON) {\n const depReg = this.registrations.get(token)\n if (depReg && depReg.scope === Scope.REQUEST) {\n throw new Error(\n `Cannot inject REQUEST-scoped \"${tokenName(token)}\" into SINGLETON \"${tokenName(reg.target)}\". ` +\n `Singletons outlive requests. Use TRANSIENT or REQUEST scope for the parent.`,\n )\n }\n }\n return this.resolve(token)\n })\n\n const instance = new reg.target(...args)\n\n // Property injection via @Autowired\n this.injectProperties(instance, reg.target)\n\n // @PostConstruct lifecycle hook\n const postConstruct = getClassMetaOrUndefined<string | symbol>(\n METADATA.POST_CONSTRUCT,\n reg.target.prototype,\n )\n if (postConstruct && typeof instance[postConstruct] === 'function') {\n try {\n const result = instance[postConstruct]()\n if (result && typeof result.then === 'function') {\n reg.postConstructStatus = 'pending'\n ;(result as Promise<void>)\n .then(() => {\n reg.postConstructStatus = 'completed'\n })\n .catch(() => {\n reg.postConstructStatus = 'failed'\n })\n log.warn(\n `@PostConstruct on ${tokenName(reg.target)}.${String(postConstruct)}() returned a Promise ` +\n `but the container is synchronous. The async operation will not be awaited.`,\n )\n } else {\n reg.postConstructStatus = 'completed'\n }\n } catch (err) {\n reg.postConstructStatus = 'failed'\n log.error(\n err,\n `@PostConstruct on ${tokenName(reg.target)}.${String(postConstruct)}() threw an error`,\n )\n }\n }\n\n return instance\n }\n\n /**\n * Extract every dependency token name a class declares — both\n * constructor params (`@Inject` + emitted `design:paramtypes`) and\n * `@Autowired` properties (read lazily at injection time, but\n * captured here so devtools surfaces a complete dependency graph).\n *\n * Without the property branch, classes that rely entirely on\n * `@Autowired` show `dependencies: []` in the introspection panel\n * even though they have real DI deps — see /_debug topology.\n */\n private extractDependencies(target: Constructor): string[] {\n const paramTypes = getClassMeta<Constructor[]>(METADATA.PARAM_TYPES, target, [])\n const injectTokens = getMetaRecord<any>(METADATA.INJECT, target)\n const ctorDeps = paramTypes.map((type, index) => {\n const token = injectTokens[index] || type\n return tokenName(token)\n })\n\n // @Autowired properties live on `target.prototype`. Token may be\n // explicit (`@Autowired('SOME_TOKEN')`) or fall back to the\n // emitted `design:type` metadata of the property.\n const autowiredProps = getMetaMap<string, any>(METADATA.AUTOWIRED, target.prototype)\n const propDeps: string[] = []\n for (const [prop, token] of autowiredProps) {\n const resolvedToken =\n token || getMethodMetaOrUndefined(METADATA.PROPERTY_TYPE, target.prototype, prop)\n if (resolvedToken) propDeps.push(tokenName(resolvedToken))\n }\n\n // De-dupe — a class can declare the same token in both surfaces.\n return [...new Set([...ctorDeps, ...propDeps])]\n }\n\n private injectProperties(instance: any, target: Constructor): void {\n // @Autowired — lazy DI property injection\n const autowiredProps = getMetaMap<string, any>(METADATA.AUTOWIRED, target.prototype)\n\n for (const [prop, token] of autowiredProps) {\n const resolvedToken =\n token || getMethodMetaOrUndefined(METADATA.PROPERTY_TYPE, target.prototype, prop)\n if (resolvedToken) {\n Object.defineProperty(instance, prop, {\n get: () => this.resolve(resolvedToken),\n enumerable: true,\n configurable: true,\n })\n }\n }\n\n // @Value — lazy environment variable injection\n const valueProps = getMetaMap<string, { envKey: string; defaultValue?: any }>(\n METADATA.VALUE,\n target.prototype,\n )\n\n for (const [prop, config] of valueProps) {\n Object.defineProperty(instance, prop, {\n get() {\n // Use the registered env resolver if available (wired by the\n // config layer in @forinda/kickjs). Returns Zod-validated,\n // type-coerced values (e.g. PORT as number).\n if (Container._envResolver) {\n const val = Container._envResolver(config.envKey)\n if (val !== undefined) return val\n }\n\n // Fallback to raw process.env for apps that haven't called loadEnv()\n const val = process.env[config.envKey]\n if (val !== undefined) return val\n if (config.defaultValue !== undefined) return config.defaultValue\n throw envValueMissingError(config.envKey)\n },\n enumerable: true,\n configurable: true,\n })\n }\n\n // @Asset — lazy asset path injection (assets-plan.md). Same lazy-\n // getter pattern as @Value: resolve on every property access via\n // the asset manager's cached resolver, not at class instantiation.\n // Splits the key on the FIRST '/' so `mails/orders/confirmation`\n // becomes namespace=`mails` + key=`orders/confirmation`.\n const assetProps = getMetaMap<string, { assetKey: string }>(METADATA.ASSET, target.prototype)\n\n if (assetProps.size > 0) {\n for (const [prop, config] of assetProps) {\n const slashIdx = config.assetKey.indexOf('/')\n if (slashIdx === -1) {\n throw new Error(\n `@Asset('${config.assetKey}'): asset key must include a '/' separator (e.g. 'mails/welcome').`,\n )\n }\n const namespace = config.assetKey.slice(0, slashIdx)\n const key = config.assetKey.slice(slashIdx + 1)\n Object.defineProperty(instance, prop, {\n get() {\n return resolveAsset(namespace, key)\n },\n enumerable: true,\n configurable: true,\n })\n }\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,9 +1,64 @@
|
|
|
1
1
|
|
|
2
2
|
import { n as ExecutionContext, r as MetaValue } from "./execution-context-DKnt61pY.mjs";
|
|
3
|
+
import { i as ProblemDetails } from "./errors-DytV77Hb.mjs";
|
|
3
4
|
import { g as QueryFieldConfig, y as TypedParsedQuery } from "./index-B9cyOQAe.mjs";
|
|
4
5
|
import { NextFunction, Request, Response } from "express";
|
|
5
6
|
|
|
6
7
|
//#region src/http/context.d.ts
|
|
8
|
+
/**
|
|
9
|
+
* Input shape for the typed problem-detail convenience methods on
|
|
10
|
+
* {@link ProblemSender} (`ctx.problem.notFound`, `.badRequest`, etc.) —
|
|
11
|
+
* RFC 9457 details minus the `status`, which each method pre-fills.
|
|
12
|
+
*/
|
|
13
|
+
type ProblemInput = Omit<ProblemDetails, 'status'>;
|
|
14
|
+
/**
|
|
15
|
+
* Callable response helper for RFC 9457 — Problem Details responses.
|
|
16
|
+
* Returned by `ctx.problem`. Invoke directly for a fully-specified
|
|
17
|
+
* payload, or use the named convenience methods to pre-fill `status` +
|
|
18
|
+
* `title` for the common codes.
|
|
19
|
+
*
|
|
20
|
+
* Each call sets `Content-Type: application/problem+json` and serializes
|
|
21
|
+
* the canonical shape (defaulting `type` to `about:blank` and `title` to
|
|
22
|
+
* the IANA reason phrase per RFC 9457 §3.1.1 / §3.1.4).
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* ctx.problem({
|
|
27
|
+
* type: 'https://api.example.com/problems/out-of-credit',
|
|
28
|
+
* status: 403,
|
|
29
|
+
* detail: 'Your balance is 30, but that costs 50.',
|
|
30
|
+
* balance: 30, // extension per §3.2
|
|
31
|
+
* })
|
|
32
|
+
*
|
|
33
|
+
* ctx.problem.notFound({ detail: 'User abc not found' })
|
|
34
|
+
* ctx.problem.validation(zodResult.error.issues)
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
interface ProblemSender {
|
|
38
|
+
(input: ProblemDetails): Response;
|
|
39
|
+
/** 400 Bad Request — RFC 9457. */
|
|
40
|
+
badRequest(input?: ProblemInput): Response;
|
|
41
|
+
/** 401 Unauthorized — RFC 9457. */
|
|
42
|
+
unauthorized(input?: ProblemInput): Response;
|
|
43
|
+
/** 403 Forbidden — RFC 9457. */
|
|
44
|
+
forbidden(input?: ProblemInput): Response;
|
|
45
|
+
/** 404 Not Found — RFC 9457. */
|
|
46
|
+
notFound(input?: ProblemInput): Response;
|
|
47
|
+
/** 409 Conflict — RFC 9457. */
|
|
48
|
+
conflict(input?: ProblemInput): Response;
|
|
49
|
+
/** 422 Unprocessable Entity — RFC 9457. */
|
|
50
|
+
unprocessable(input?: ProblemInput): Response;
|
|
51
|
+
/** 429 Too Many Requests — RFC 9457. */
|
|
52
|
+
tooManyRequests(input?: ProblemInput): Response;
|
|
53
|
+
/** 500 Internal Server Error — RFC 9457. */
|
|
54
|
+
internal(input?: ProblemInput): Response;
|
|
55
|
+
/**
|
|
56
|
+
* 422 with Zod issues serialized into the `errors` extension per
|
|
57
|
+
* RFC 9457 §3.2's array convention. Pass `error.issues` from a Zod
|
|
58
|
+
* `safeParse` failure or `error.issues` from a thrown ZodError.
|
|
59
|
+
*/
|
|
60
|
+
validation(issues: any[], input?: ProblemInput): Response;
|
|
61
|
+
}
|
|
7
62
|
/**
|
|
8
63
|
* Recursively marks every property of `T` as `readonly`. Preserves
|
|
9
64
|
* `Function`, `Date`, and `RegExp` unchanged, and converts `Map` / `Set`
|
|
@@ -222,8 +277,50 @@ declare class RequestContext<TBody = any, TParams = any, TQuery = any> implement
|
|
|
222
277
|
json(data: any, status?: number): Response<any, Record<string, any>>;
|
|
223
278
|
created(data: any): Response<any, Record<string, any>>;
|
|
224
279
|
noContent(): Response<any, Record<string, any>>;
|
|
280
|
+
/**
|
|
281
|
+
* @deprecated Prefer {@link ProblemSender.notFound | ctx.problem.notFound()},
|
|
282
|
+
* which emits an RFC 9457 — Problem Details response. This helper stays
|
|
283
|
+
* for backward compatibility and will not be removed.
|
|
284
|
+
*/
|
|
225
285
|
notFound(message?: string): Response<any, Record<string, any>>;
|
|
286
|
+
/**
|
|
287
|
+
* @deprecated Prefer {@link ProblemSender.badRequest | ctx.problem.badRequest()},
|
|
288
|
+
* which emits an RFC 9457 — Problem Details response. This helper stays
|
|
289
|
+
* for backward compatibility and will not be removed.
|
|
290
|
+
*/
|
|
226
291
|
badRequest(message: string): Response<any, Record<string, any>>;
|
|
292
|
+
/**
|
|
293
|
+
* RFC 9457 — Problem Details for HTTP APIs response helper.
|
|
294
|
+
* https://datatracker.ietf.org/doc/html/rfc9457
|
|
295
|
+
*
|
|
296
|
+
* `ctx.problem(...)` sends a canonical problem-detail response with
|
|
297
|
+
* `Content-Type: application/problem+json`. The convenience methods
|
|
298
|
+
* (`ctx.problem.notFound`, `.badRequest`, etc.) pre-fill `status` +
|
|
299
|
+
* `title` for the common codes.
|
|
300
|
+
*
|
|
301
|
+
* `type` defaults to `about:blank` (§3.1.1); `title` defaults to the
|
|
302
|
+
* IANA reason phrase for `status` (§3.1.4). Extension members per §3.2
|
|
303
|
+
* pass through unchanged.
|
|
304
|
+
*
|
|
305
|
+
* @example
|
|
306
|
+
* ```ts
|
|
307
|
+
* ctx.problem({
|
|
308
|
+
* type: 'https://api.example.com/problems/out-of-credit',
|
|
309
|
+
* status: 403,
|
|
310
|
+
* detail: 'Your balance is 30, but that costs 50.',
|
|
311
|
+
* instance: `/account/${ctx.user.id}/messages/${ctx.params.id}`,
|
|
312
|
+
* balance: 30,
|
|
313
|
+
* })
|
|
314
|
+
*
|
|
315
|
+
* ctx.problem.notFound({ detail: 'User abc not found' })
|
|
316
|
+
* ctx.problem.validation(zodResult.error.issues)
|
|
317
|
+
* ```
|
|
318
|
+
*
|
|
319
|
+
* @see {@link ProblemException} for the exception-throwing equivalent
|
|
320
|
+
* (use it from services where `ctx` isn't in scope).
|
|
321
|
+
*/
|
|
322
|
+
get problem(): ProblemSender;
|
|
323
|
+
private sendProblem;
|
|
227
324
|
html(content: string, status?: number): Response<any, Record<string, any>>;
|
|
228
325
|
download(buffer: Buffer, filename: string, contentType?: string): Response<any, Record<string, any>>;
|
|
229
326
|
/**
|
|
@@ -334,4 +431,4 @@ declare global {
|
|
|
334
431
|
} //# sourceMappingURL=context.d.ts.map
|
|
335
432
|
//#endregion
|
|
336
433
|
export { RouteShape as i, DeepReadonly as n, RequestContext as r, Ctx as t };
|
|
337
|
-
//# sourceMappingURL=context-
|
|
434
|
+
//# sourceMappingURL=context-D7sGbb8E.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context-D7sGbb8E.d.mts","names":[],"sources":["../src/http/context.ts"],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @forinda/kickjs v5.13.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{normalizeProblem}from"./core/errors.mjs";import{i as parseQuery}from"./query-BXkCd9cg.mjs";import{AsyncLocalStorage}from"node:async_hooks";const requestStore=new AsyncLocalStorage;function getRequestStore(){let store=requestStore.getStore();if(!store)throw Error(`No active request context. REQUEST-scoped services can only be resolved during an HTTP request.`);return store}function getRequestValue(key){return requestStore.getStore()?.values.get(key)}const signalControllerKey=Symbol.for(`kickjs.requestContext.signalController`),readOnlyProxyCacheKey=Symbol.for(`kickjs.requestContext.readOnlyProxyCache`);function isPlainObjectOrArray(v){if(typeof v!=`object`||!v)return!1;if(Array.isArray(v))return!0;let proto=Object.getPrototypeOf(v);return proto===Object.prototype||proto===null}function makeReadOnlyProxy(target,req,label){if(process.env.NODE_ENV===`production`||!isPlainObjectOrArray(target))return target;let reqObj=req,cache=reqObj[readOnlyProxyCacheKey];cache||(cache=new WeakMap,reqObj[readOnlyProxyCacheKey]=cache);let targetObj=target,cached=cache.get(targetObj);if(cached)return cached;let warnWrite=(verb,prop)=>{console.warn(`[kickjs] Attempted to ${verb} \`ctx.${label}.${String(prop)}\` — request data is read-only. Stash computed values via \`ctx.set('key', value)\` or a Context Contributor, or reach \`ctx.req.${label}\` if you genuinely need the raw mutable handle.`)},proxy=new Proxy(targetObj,{get(t,prop,receiver){let value=Reflect.get(t,prop,receiver);return isPlainObjectOrArray(value)?makeReadOnlyProxy(value,req,`${label}.${String(prop)}`):value},set(_t,prop){return warnWrite(`assign`,prop),!0},deleteProperty(_t,prop){return warnWrite(`delete`,prop),!0},defineProperty(_t,prop){return warnWrite(`Object.defineProperty on`,prop),!0}});return cache.set(targetObj,proxy),proxy}var RequestContext=class{req;res;next;constructor(req,res,next){this.req=req,this.res=res,this.next=next}metadataReadOnly(){return requestStore.getStore()?.values??null}metadataForWrite(){let store=requestStore.getStore();if(store)return store.values;throw Error(`RequestContext.set called outside an AsyncLocalStorage frame. Mount requestScopeMiddleware() before constructing RequestContext, or wrap test setup in requestStore.run({ requestId, instances: new Map(), values: new Map() }, () => ...).`)}get body(){return makeReadOnlyProxy(this.req.body,this.req,`body`)}get params(){return makeReadOnlyProxy(this.req.params,this.req,`params`)}get query(){return makeReadOnlyProxy(this.req.query,this.req,`query`)}get headers(){return makeReadOnlyProxy(this.req.headers,this.req,`headers`)}get requestId(){return this.req.requestId??this.req.headers[`x-request-id`]}get signal(){let req=this.req,cached=req[signalControllerKey];if(cached)return cached.signal;let ctrl=new AbortController;req[signalControllerKey]=ctrl;let abort=()=>ctrl.abort(`request closed`);return this.req.once(`close`,abort),this.res.once(`close`,abort),ctrl.signal}get session(){return this.req.session}get user(){return this.metadataReadOnly()?.get(`user`)??this.req.user}get tenantId(){return this.user?.tenantId}get roles(){let u=this.user;return u?.tenantRoles??u?.roles??[]}qs(fieldConfig){return parseQuery(this.req.query,fieldConfig)}get file(){let f=this.req.file;return f?makeReadOnlyProxy(f,this.req,`file`):void 0}get files(){let f=this.req.files;if(Array.isArray(f))return makeReadOnlyProxy(f,this.req,`files`)}get(key){return this.metadataReadOnly()?.get(key)}set(key,value){this.metadataForWrite().set(key,value)}json(data,status=200){return this.res.status(status).json(data)}created(data){return this.res.status(201).json(data)}noContent(){return this.res.status(204).end()}notFound(message=`Not Found`){return this.res.status(404).json({message})}badRequest(message){return this.res.status(400).json({message})}get problem(){let send=input=>this.sendProblem(input);return send.badRequest=(input={})=>this.sendProblem({status:400,...input}),send.unauthorized=(input={})=>this.sendProblem({status:401,...input}),send.forbidden=(input={})=>this.sendProblem({status:403,...input}),send.notFound=(input={})=>this.sendProblem({status:404,...input}),send.conflict=(input={})=>this.sendProblem({status:409,...input}),send.unprocessable=(input={})=>this.sendProblem({status:422,...input}),send.tooManyRequests=(input={})=>this.sendProblem({status:429,...input}),send.internal=(input={})=>this.sendProblem({status:500,...input}),send.validation=(issues,input={})=>{let errors=(issues||[]).map(issue=>({field:issue.path?.join?.(`.`)??``,message:issue.message??``,code:issue.code}));return this.sendProblem({status:422,detail:issues?.[0]?.message,...input,errors})},send}sendProblem(input){let body=normalizeProblem(input);return this.res.setHeader(`Content-Type`,`application/problem+json`),this.res.status(body.status).json(body)}html(content,status=200){return this.res.status(status).type(`html`).send(content)}download(buffer,filename,contentType=`application/octet-stream`){return this.res.setHeader(`Content-Disposition`,`attachment; filename="${filename}"`),this.res.setHeader(`Content-Type`,contentType),this.res.send(buffer)}render(template,data={}){return this.res.render(template,data)}async paginate(fetcher,fieldConfig){let parsed=this.qs(fieldConfig),{data,total}=await fetcher(parsed),{page,limit}=parsed.pagination,totalPages=Math.ceil(total/limit)||1,response={data,meta:{page,limit,total,totalPages,hasNext:page<totalPages,hasPrev:page>1}};return this.json(response)}sse(){this.res.writeHead(200,{"Content-Type":`text/event-stream`,"Cache-Control":`no-cache`,Connection:`keep-alive`,"X-Accel-Buffering":`no`}),this.res.flushHeaders();let closeCallbacks=[];return this.req.on(`close`,()=>{for(let cb of closeCallbacks)cb()}),{send:(data,event,id)=>{id&&this.res.write(`id: ${id}\n`),event&&this.res.write(`event: ${event}\n`),this.res.write(`data: ${JSON.stringify(data)}\n\n`)},comment:text=>{this.res.write(`: ${text}\n\n`)},onClose:fn=>{closeCallbacks.push(fn)},close:()=>{this.res.end()}}}};export{requestStore as i,getRequestStore as n,getRequestValue as r,RequestContext as t};
|
|
12
|
+
//# sourceMappingURL=context-DpIvcO8X.mjs.map
|