@forinda/kickjs 9.2.0 → 10.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{adapter-DjfRMob8.d.mts → adapter-ClEh-Oos.d.mts} +24 -2
- package/dist/adapter-ClEh-Oos.d.mts.map +1 -0
- package/dist/application-Dv514EeR.mjs +12 -0
- package/dist/application-Dv514EeR.mjs.map +1 -0
- package/dist/{application-CGxZCYl1.d.mts → application-oIPzgjfk.d.mts} +38 -11
- package/dist/application-oIPzgjfk.d.mts.map +1 -0
- package/dist/bootstrap-BdyBlBMe.mjs +12 -0
- package/dist/bootstrap-BdyBlBMe.mjs.map +1 -0
- package/dist/{client-ip-pcINkkPr.mjs → client-ip-BbLDp03S.mjs} +2 -2
- package/dist/{client-ip-pcINkkPr.mjs.map → client-ip-BbLDp03S.mjs.map} +1 -1
- package/dist/config/index.mjs +2 -2
- package/dist/{config-DmGiAIuH.mjs → config-CJiGeOca.mjs} +3 -3
- package/dist/{config-DmGiAIuH.mjs.map → config-CJiGeOca.mjs.map} +1 -1
- package/dist/{container-CAPN7pBG.mjs → container-CS9v9UFU.mjs} +4 -4
- package/dist/{container-CAPN7pBG.mjs.map → container-CS9v9UFU.mjs.map} +1 -1
- package/dist/context-CtL9RxXp.mjs +12 -0
- package/dist/context-CtL9RxXp.mjs.map +1 -0
- package/dist/{context-errors-DYJjhalQ.mjs → context-errors-7C5TBYn7.mjs} +2 -2
- package/dist/{context-errors-DYJjhalQ.mjs.map → context-errors-7C5TBYn7.mjs.map} +1 -1
- package/dist/contributor-runner-DBpRR5dl.mjs +12 -0
- package/dist/{contributor-runner-vk9XlOED.mjs.map → contributor-runner-DBpRR5dl.mjs.map} +1 -1
- package/dist/core/adapter.d.mts +1 -1
- package/dist/core/adapter.mjs +1 -1
- package/dist/core/app-module.mjs +1 -1
- package/dist/core/cache.mjs +2 -2
- package/dist/core/cache.mjs.map +1 -1
- package/dist/core/container.mjs +2 -2
- package/dist/core/cron.mjs +2 -2
- package/dist/core/cron.mjs.map +1 -1
- package/dist/core/decorators.mjs +2 -2
- package/dist/core/decorators.mjs.map +1 -1
- package/dist/core/errors.mjs +1 -1
- package/dist/core/interfaces.mjs +2 -2
- package/dist/core/interfaces.mjs.map +1 -1
- package/dist/core/logger.mjs +2 -2
- package/dist/core/logger.mjs.map +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 +2 -2
- package/dist/core/reactivity.mjs.map +1 -1
- package/dist/core-B0JmdBvX.mjs +12 -0
- package/dist/{core-DKrKVLfj.mjs.map → core-B0JmdBvX.mjs.map} +1 -1
- package/dist/{csrf-CxJItZVa.mjs → csrf--1t4qDov.mjs} +3 -3
- package/dist/{csrf-CxJItZVa.mjs.map → csrf--1t4qDov.mjs.map} +1 -1
- package/dist/{decorate-DfH1NIqV.mjs → decorate-BHkdkV4l.mjs} +1 -1
- package/dist/{define-adapter-C6lFhCL-.mjs → define-adapter-BLKlUbSL.mjs} +2 -2
- package/dist/{define-adapter-C6lFhCL-.mjs.map → define-adapter-BLKlUbSL.mjs.map} +1 -1
- package/dist/disposables-CuY2S3bw.mjs +12 -0
- package/dist/{disposables-NlW6Mibk.mjs.map → disposables-CuY2S3bw.mjs.map} +1 -1
- package/dist/env-BwzF9MkZ.mjs +13 -0
- package/dist/env-BwzF9MkZ.mjs.map +1 -0
- package/dist/{error-handler-itG65lj0.d.mts → error-handler-BB9wAElm.d.mts} +2 -2
- package/dist/{error-handler-itG65lj0.d.mts.map → error-handler-BB9wAElm.d.mts.map} +1 -1
- package/dist/handler-zKfIgNo4.mjs +13 -0
- package/dist/handler-zKfIgNo4.mjs.map +1 -0
- 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 +2 -2
- package/dist/http/middleware/cors.mjs.map +1 -1
- package/dist/http/middleware/csrf.mjs +2 -2
- package/dist/http/middleware/error-handler.d.mts +1 -1
- package/dist/http/middleware/error-handler.mjs +2 -2
- package/dist/http/middleware/helmet.mjs +1 -1
- package/dist/http/middleware/rate-limit.mjs +2 -2
- package/dist/http/middleware/rate-limit.mjs.map +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.d.mts +1 -1
- package/dist/http/middleware/session.mjs +2 -2
- package/dist/http/middleware/session.mjs.map +1 -1
- package/dist/http/middleware/spa.d.mts +1 -1
- package/dist/http/middleware/spa.mjs +2 -2
- package/dist/http/middleware/spa.mjs.map +1 -1
- package/dist/http/middleware/upload.d.mts +1 -1
- package/dist/http/middleware/upload.mjs +2 -2
- package/dist/http/middleware/validate.mjs +1 -1
- package/dist/http/middleware/views.d.mts +1 -1
- package/dist/http/middleware/views.mjs +2 -2
- package/dist/http/middleware/views.mjs.map +1 -1
- package/dist/http/query/index.mjs +2 -2
- package/dist/http/router-builder.d.mts +1 -1
- package/dist/http/router-builder.mjs +2 -2
- package/dist/http/runtimes/fastify.d.mts +2 -1
- package/dist/http/runtimes/fastify.d.mts.map +1 -1
- package/dist/http/runtimes/fastify.mjs +3 -3
- package/dist/http/runtimes/fastify.mjs.map +1 -1
- package/dist/http/runtimes/h3-web.d.mts +2 -49
- package/dist/http/runtimes/h3-web.mjs +2 -3
- package/dist/http/runtimes/h3.d.mts +48 -18
- package/dist/http/runtimes/h3.d.mts.map +1 -1
- package/dist/http/runtimes/h3.mjs +2 -2
- package/dist/http/runtimes/h3.mjs.map +1 -1
- package/dist/index.d.mts +8 -8
- package/dist/index.mjs +2 -2
- package/dist/index.mjs.map +1 -1
- package/dist/jobs-BQ4t3TAb.mjs +12 -0
- package/dist/{jobs-BmpeFvi0.mjs.map → jobs-BQ4t3TAb.mjs.map} +1 -1
- package/dist/{kv-stores-BZTe23Tw.d.mts → kv-stores-Co5k2PYA.d.mts} +21 -6
- package/dist/kv-stores-Co5k2PYA.d.mts.map +1 -0
- package/dist/kv-stores-DsfVy2fk.mjs +12 -0
- package/dist/{kv-stores-DJwxntyw.mjs.map → kv-stores-DsfVy2fk.mjs.map} +1 -1
- package/dist/{metadata-CzzRUnAx.mjs → metadata-zvd6AWkE.mjs} +3 -3
- package/dist/{metadata-CzzRUnAx.mjs.map → metadata-zvd6AWkE.mjs.map} +1 -1
- package/dist/{module-registry-BnmRNy-3.mjs → module-registry-Daf_P2oD.mjs} +2 -2
- package/dist/{module-registry-BnmRNy-3.mjs.map → module-registry-Daf_P2oD.mjs.map} +1 -1
- package/dist/{observers-COlQfinD.mjs → observers-DZgs48UM.mjs} +2 -2
- package/dist/{observers-COlQfinD.mjs.map → observers-DZgs48UM.mjs.map} +1 -1
- package/dist/query-B_k9qn0R.mjs +12 -0
- package/dist/{query-B5DTq_4W.mjs.map → query-B_k9qn0R.mjs.map} +1 -1
- package/dist/reply-BDK9xt7Z.mjs +12 -0
- package/dist/{reply-CbTYIj_e.mjs.map → reply-BDK9xt7Z.mjs.map} +1 -1
- package/dist/{respond-wbMSqHYo.mjs → respond-DFRj2-7q.mjs} +3 -3
- package/dist/{respond-wbMSqHYo.mjs.map → respond-DFRj2-7q.mjs.map} +1 -1
- package/dist/route-flag-BWuvJsGZ.mjs +12 -0
- package/dist/{route-flag-Bb7n9Mnp.mjs.map → route-flag-BWuvJsGZ.mjs.map} +1 -1
- package/dist/route-policy-B3vM6bSf.mjs +12 -0
- package/dist/{route-policy-C_Wsc-b1.mjs.map → route-policy-B3vM6bSf.mjs.map} +1 -1
- package/dist/router-builder-JvMTp50k.mjs +12 -0
- package/dist/{router-builder-DawnRzlp.mjs.map → router-builder-JvMTp50k.mjs.map} +1 -1
- package/dist/{router-builder-CDkW88KQ.d.mts → router-builder-zWuVIOmd.d.mts} +2 -2
- package/dist/{router-builder-CDkW88KQ.d.mts.map → router-builder-zWuVIOmd.d.mts.map} +1 -1
- package/dist/{session-4Tb6q0Ny.d.mts → session-B4hOZj90.d.mts} +13 -4
- package/dist/session-B4hOZj90.d.mts.map +1 -0
- package/dist/{upload-Cy7qQy7M.d.mts → upload-CiMtdXWD.d.mts} +2 -2
- package/dist/upload-CiMtdXWD.d.mts.map +1 -0
- package/dist/{upload-config-D01Ty40f.mjs → upload-config-Dz2INCaa.mjs} +2 -2
- package/dist/{upload-config-D01Ty40f.mjs.map → upload-config-Dz2INCaa.mjs.map} +1 -1
- package/dist/web.d.mts +9 -7
- package/dist/web.d.mts.map +1 -1
- package/dist/web.mjs +2 -2
- package/dist/web.mjs.map +1 -1
- package/package.json +7 -8
- package/dist/adapter-DjfRMob8.d.mts.map +0 -1
- package/dist/application-BSJpaiSY.mjs +0 -12
- package/dist/application-BSJpaiSY.mjs.map +0 -1
- package/dist/application-CGxZCYl1.d.mts.map +0 -1
- package/dist/bootstrap-CbTF6xvv.mjs +0 -12
- package/dist/bootstrap-CbTF6xvv.mjs.map +0 -1
- package/dist/context-gpvcMlG5.mjs +0 -12
- package/dist/context-gpvcMlG5.mjs.map +0 -1
- package/dist/contributor-runner-vk9XlOED.mjs +0 -12
- package/dist/core-DKrKVLfj.mjs +0 -12
- package/dist/disposables-NlW6Mibk.mjs +0 -12
- package/dist/env-__fzPsHD.mjs +0 -13
- package/dist/env-__fzPsHD.mjs.map +0 -1
- package/dist/handler-DnZbcleS.mjs +0 -13
- package/dist/handler-DnZbcleS.mjs.map +0 -1
- package/dist/http/runtimes/h3-web.d.mts.map +0 -1
- package/dist/http/runtimes/h3-web.mjs.map +0 -1
- package/dist/jobs-BmpeFvi0.mjs +0 -12
- package/dist/kv-stores-BZTe23Tw.d.mts.map +0 -1
- package/dist/kv-stores-DJwxntyw.mjs +0 -12
- package/dist/query-B5DTq_4W.mjs +0 -12
- package/dist/reply-CbTYIj_e.mjs +0 -12
- package/dist/route-flag-Bb7n9Mnp.mjs +0 -12
- package/dist/route-policy-C_Wsc-b1.mjs +0 -12
- package/dist/router-builder-DawnRzlp.mjs +0 -12
- package/dist/session-4Tb6q0Ny.d.mts.map +0 -1
- package/dist/upload-Cy7qQy7M.d.mts.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs
|
|
2
|
+
* @forinda/kickjs v10.1.0
|
|
3
3
|
*
|
|
4
4
|
* Copyright (c) Felix Orinda
|
|
5
5
|
*
|
|
@@ -8,5 +8,5 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @license MIT
|
|
10
10
|
*/
|
|
11
|
-
import{Logger}from"../../core/logger.mjs";import"../../core-
|
|
11
|
+
import{Logger}from"../../core/logger.mjs";import"../../core-B0JmdBvX.mjs";import{t as defineAdapter}from"../../define-adapter-BLKlUbSL.mjs";import{existsSync,readFileSync,readdirSync}from"node:fs";import{dirname,isAbsolute,join,resolve}from"node:path";const log=Logger.for(`SpaAdapter`);function nearestPackageRoot(from){let dir=from;for(;;){if(existsSync(join(dir,`package.json`)))return dir;let parent=dirname(dir);if(parent===dir)return null;dir=parent}}function resolveClientDir(clientDir,cwd=process.cwd(),entry=process.argv[1]){if(isAbsolute(clientDir))return resolve(clientDir);let fromCwd=resolve(cwd,clientDir);if(existsSync(fromCwd))return fromCwd;if(entry){let root=nearestPackageRoot(dirname(resolve(entry)));if(root){let fromEntry=resolve(root,clientDir);if(existsSync(fromEntry))return fromEntry}}return fromCwd}function pathnameOf(url){if(!url)return`/`;let q=url.indexOf(`?`),h=url.indexOf(`#`),end=Math.min(q===-1?url.length:q,h===-1?url.length:h);return url.slice(0,end)||`/`}function isUnder(pathname,prefix){if(!prefix||prefix===`/`)return!0;let p=prefix.endsWith(`/`)?prefix.slice(0,-1):prefix;return pathname===p||pathname.startsWith(`${p}/`)}const HTML_TYPES=[`text/html`,`application/xhtml+xml`];function qualityOf(params){let q=params.find(param=>param.startsWith(`q=`));if(!q)return 1;let value=Number.parseFloat(q.slice(2));return Number.isNaN(value)?1:value}function acceptsHtml(accept){let raw=Array.isArray(accept)?accept.join(`,`):accept??``;if(!raw)return!1;let ranges=raw.split(`,`).map(part=>part.split(`;`).map(t=>t.trim().toLowerCase())).filter(([type])=>!!type);return HTML_TYPES.some(htmlType=>{let[group]=htmlType.split(`/`),exact=ranges.find(([type])=>type===htmlType),wildcard=ranges.find(([type])=>type===`${group}/*`),match=exact??wildcard;return match?qualityOf(match.slice(1))>0:!1})}const SpaAdapter=defineAdapter({name:`SpaAdapter`,defaults:{clientDir:`dist/client`,apiPrefix:`/api`,exclude:[],cacheControl:`public, max-age=31536000, immutable`,indexCacheControl:`no-cache`,alwaysFallback:!1},build:config=>{let clientDir=resolveClientDir(config.clientDir??`dist/client`),rawPrefix=config.apiPrefix??`/api`,apiPrefixes=Array.isArray(rawPrefix)?rawPrefix:[rawPrefix],excludePaths=config.exclude??[],cacheControl=config.cacheControl??`public, max-age=31536000, immutable`,indexCacheControl=config.indexCacheControl??`no-cache`,alwaysFallback=config.alwaysFallback??!1,indexHtml=null,assetPaths=/* @__PURE__ */ new Set;function snapshotClientDir(){let paths=/* @__PURE__ */ new Set,walk=(absDir,prefix)=>{for(let entry of readdirSync(absDir,{withFileTypes:!0})){let child=`${prefix}/${entry.name}`;entry.isDirectory()?walk(join(absDir,entry.name),child):entry.isFile()&&paths.add(child)}};try{walk(clientDir,``)}catch{return/* @__PURE__ */ new Set}return paths}let classifyPath=pathname=>{let decoded;try{decoded=decodeURIComponent(pathname)}catch{return null}if(decoded.includes(`\0`))return null;let asIndex=decoded.endsWith(`/`)?`${decoded}index.html`:null;return asIndex&&assetPaths.has(asIndex)?`index`:assetPaths.has(decoded)?decoded.endsWith(`.html`)?`index`:`file`:null},isReserved=pathname=>apiPrefixes.some(p=>isUnder(pathname,p))||excludePaths.some(p=>isUnder(pathname,p));return{beforeMount({http}){if(!existsSync(clientDir)){log.warn(`SPA client directory not found: ${clientDir}`),log.warn(`Build your frontend first, or set clientDir to the correct path (relative paths resolve from ${process.cwd()}).`);return}let indexPath=join(clientDir,`index.html`);existsSync(indexPath)?(indexHtml=readFileSync(indexPath,`utf-8`),assetPaths=snapshotClientDir(),cacheControl!==!1&&http.use(((req,res,next)=>{let pathname=pathnameOf(req.url),kind=isReserved(pathname)?null:classifyPath(pathname);kind&&res.setHeader(`Cache-Control`,kind===`index`?indexCacheControl:cacheControl),next()})),http.serveStatic(`/`,clientDir),log.debug(`Serving SPA from ${clientDir}`)):log.warn(`index.html not found in ${clientDir}`)},beforeStart({http}){indexHtml&&http.use(((req,res,next)=>{let method=(req.method??`GET`).toUpperCase();if(method!==`GET`&&method!==`HEAD`)return next();let pathname=pathnameOf(req.url);if(isReserved(pathname))return next();let accept=req.headers?.accept;if(!alwaysFallback&&!acceptsHtml(accept))return next();let body=indexHtml;res.setHeader(`Content-Type`,`text/html; charset=utf-8`),res.setHeader(`Cache-Control`,indexCacheControl),res.setHeader(`Content-Length`,String(Buffer.byteLength(body))),res.statusCode=200,res.end(method===`HEAD`?void 0:body)}))}}}});export{SpaAdapter,resolveClientDir};
|
|
12
12
|
//# sourceMappingURL=spa.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"spa.mjs","names":[],"sources":["../../../src/http/middleware/spa.ts"],"sourcesContent":["import { existsSync, readdirSync, readFileSync } from 'node:fs'\nimport { resolve, join, dirname, isAbsolute } from 'node:path'\nimport { Logger, defineAdapter } from '../../core'\nimport type { ConnectMiddleware } from '../runtime'\n\nconst log = Logger.for('SpaAdapter')\n\nexport interface SpaAdapterOptions {\n /**\n * Directory containing the built SPA files (index.html, assets, etc.)\n * Default: 'dist/client'\n *\n * @example\n * ```ts\n * // Vue: 'dist'\n * // React (Vite): 'dist'\n * // React (CRA): 'build'\n * // Svelte: 'build'\n * // Angular: 'dist/my-app/browser'\n * ```\n */\n clientDir?: string\n\n /**\n * URL prefix for API routes. SPA fallback only applies to\n * non-API routes (routes NOT starting with this prefix).\n * Default: '/api'\n *\n * Matching is segment-aware: `/api` excludes `/api` and `/api/users`, but\n * NOT `/apidocs` — a plain `startsWith` swallowed any path that merely\n * began with the same letters.\n *\n * Set to an array for multiple prefixes:\n * ```ts\n * apiPrefix: ['/api', '/graphql', '/_debug']\n * ```\n */\n apiPrefix?: string | string[]\n\n /**\n * Additional paths to exclude from SPA fallback.\n * These paths will NOT serve index.html. Segment-aware, like `apiPrefix`.\n *\n * @example\n * ```ts\n * exclude: ['/health', '/metrics', '/ws']\n * ```\n */\n exclude?: string[]\n\n /**\n * Cache-Control header for static assets (default: 'public, max-age=31536000, immutable')\n * Set to false to disable caching headers.\n */\n cacheControl?: string | false\n\n /**\n * Cache-Control for index.html (default: 'no-cache')\n * index.html should not be cached to ensure clients get fresh builds.\n */\n indexCacheControl?: string\n\n /**\n * Serve index.html even when the client did not ask for HTML.\n * Default: false.\n *\n * By default the fallback only fires for requests whose `Accept` header\n * includes `text/html` — the rule `connect-history-api-fallback` uses. That\n * keeps a missing `/assets/app.js` a 404 instead of handing back an HTML\n * document that the browser then fails to parse as JavaScript, which is a\n * confusing way to discover a broken build.\n *\n * Turn this on if you have non-browser clients that deep-link into SPA\n * routes without an `Accept` header.\n */\n alwaysFallback?: boolean\n}\n\n/**\n * Nearest directory at or above `from` that holds a `package.json`.\n * `null` when the walk reaches the filesystem root without finding one.\n */\nfunction nearestPackageRoot(from: string): string | null {\n let dir = from\n for (;;) {\n if (existsSync(join(dir, 'package.json'))) return dir\n const parent = dirname(dir)\n if (parent === dir) return null\n dir = parent\n }\n}\n\n/**\n * Where to look for the built SPA.\n *\n * `process.cwd()` is the framework's convention (assets and env files resolve\n * the same way) and stays the primary base. But a relative `clientDir` then\n * only works when the process happens to be started from the app directory —\n * `node server/dist/index.js` run from a monorepo root resolved `../web/dist`\n * against the root and found nothing, serving no SPA with only a warning.\n *\n * So: try cwd first, and only if that misses, retry against the package root\n * of the entry script. Both are checked for existence before being chosen, so\n * this can never silently pick a directory that is not there — a genuinely\n * missing build still lands on the cwd path, which is what the warning names.\n *\n * Deliberately not entry-relative FIRST: under `kick dev` the entry is the CLI\n * binary, whose package root is the CLI itself.\n *\n * Exported for tests only — not re-exported from the package index.\n */\nexport function resolveClientDir(\n clientDir: string,\n // Injected rather than read from the process so tests can exercise the\n // monorepo-root case without `process.chdir` / `process.argv` mutation —\n // neither is supported under vitest's `threads` pool, and the root config\n // uses exactly that.\n cwd: string = process.cwd(),\n entry: string | undefined = process.argv[1],\n): string {\n // `resolve` even for an already-absolute path: `resolvesToFile` compares its\n // (always normalized) target against this string, so a trailing slash or a\n // `.`/`..` segment here would make the containment check never match and\n // silently drop the Cache-Control header on every request. Fails closed, so\n // it never weakened the traversal guard — it just stopped doing its job.\n if (isAbsolute(clientDir)) return resolve(clientDir)\n const fromCwd = resolve(cwd, clientDir)\n if (existsSync(fromCwd)) return fromCwd\n\n if (entry) {\n const root = nearestPackageRoot(dirname(resolve(entry)))\n if (root) {\n const fromEntry = resolve(root, clientDir)\n if (existsSync(fromEntry)) return fromEntry\n }\n }\n return fromCwd\n}\n\n/**\n * The slice of the request/response this adapter touches, spelled from the\n * node `http` shapes rather than Express's. `ConnectMiddleware` is typed as\n * Express's `RequestHandler`, but under Fastify and h3 these handlers receive\n * the raw node objects (or a driver), so relying on `req.path` / `res.send`\n * is what made the previous version Express-only.\n */\ninterface SpaRequest {\n url?: string\n method?: string\n headers?: Record<string, string | string[] | undefined>\n}\n\ninterface SpaResponse {\n setHeader(name: string, value: string): void\n statusCode: number\n end(chunk?: string): void\n}\n\ntype SpaHandler = (req: SpaRequest, res: SpaResponse, next: () => void) => void\n\n/** Pathname only — `req.url` carries the query string, and `req.path` is Express-only. */\nfunction pathnameOf(url: string | undefined): string {\n if (!url) return '/'\n const q = url.indexOf('?')\n const h = url.indexOf('#')\n const end = Math.min(q === -1 ? url.length : q, h === -1 ? url.length : h)\n return url.slice(0, end) || '/'\n}\n\n/**\n * Segment-aware prefix test: `/api` covers `/api` and `/api/users`, not\n * `/apidocs`. `startsWith` alone made any same-prefixed route disappear from\n * the SPA fallback and 404 instead.\n */\nfunction isUnder(pathname: string, prefix: string): boolean {\n if (!prefix || prefix === '/') return true\n const p = prefix.endsWith('/') ? prefix.slice(0, -1) : prefix\n return pathname === p || pathname.startsWith(`${p}/`)\n}\n\n/** Media ranges that count as \"the client wants a document\". */\nconst HTML_TYPES = ['text/html', 'application/xhtml+xml']\n\n/** `q` for a media range, defaulting to 1 when absent or unparseable. */\nfunction qualityOf(params: string[]): number {\n const q = params.find((param) => param.startsWith('q='))\n if (!q) return 1\n const value = Number.parseFloat(q.slice(2))\n return Number.isNaN(value) ? 1 : value\n}\n\n/**\n * Does the client want an HTML document back?\n *\n * Parses media ranges with q-values rather than substring-matching:\n * `Accept: text/html;q=0` explicitly says HTML is NOT acceptable (RFC 9110\n * §12.5.1 — \"a value of 0 means not acceptable\"), and a substring check read\n * that as a yes and returned the document anyway.\n *\n * Wildcards are matched by specificity, as RFC 9110 §12.5.1 requires: an exact\n * `text/html` beats `text/*`, so `Accept: text/*, text/html;q=0` correctly\n * rejects HTML even though the wildcard would have accepted it.\n *\n * A bare `*` / `*` deliberately does NOT count, at any q. Assets are fetched\n * with `Accept: * / *`, and treating that as a document request is what makes\n * a missing script come back as HTML — the very thing the fallback rules\n * exist to prevent.\n */\nfunction acceptsHtml(accept: string | string[] | undefined): boolean {\n const raw = Array.isArray(accept) ? accept.join(',') : (accept ?? '')\n if (!raw) return false\n\n const ranges = raw\n .split(',')\n .map((part) => part.split(';').map((t) => t.trim().toLowerCase()))\n .filter(([type]) => Boolean(type))\n\n return HTML_TYPES.some((htmlType) => {\n const [group] = htmlType.split('/')\n // Most specific match wins: exact type, else `type/*`. `*/*` is skipped.\n const exact = ranges.find(([type]) => type === htmlType)\n const wildcard = ranges.find(([type]) => type === `${group}/*`)\n const match = exact ?? wildcard\n return match ? qualityOf(match.slice(1)) > 0 : false\n })\n}\n\n/**\n * SPA adapter — serve a Vue, React, Svelte, or Angular build alongside\n * your KickJS API. API routes are handled by controllers; everything else\n * falls back to index.html for client-side routing.\n *\n * Written against the engine-agnostic `http` surface (`serveStatic` / `use`),\n * so it runs under Express, Fastify, and h3 alike. The previous version\n * reached for `express.static`, `req.path`, and `res.send`, all of which are\n * Express-only — it silently served nothing on the other runtimes.\n *\n * Migrated to the `defineAdapter()` factory — call without `new`, as\n * `ViewAdapter` has since v4.\n *\n * @example\n * ```ts\n * import { SpaAdapter } from '@forinda/kickjs/spa'\n *\n * bootstrap({\n * modules,\n * adapters: [\n * SpaAdapter({\n * clientDir: 'dist/client', // or 'build', 'dist', etc.\n * apiPrefix: '/api',\n * }),\n * ],\n * })\n * ```\n *\n * File structure:\n * ```\n * dist/\n * client/ ← SPA build output\n * index.html\n * assets/\n * app.js\n * style.css\n * server/ ← KickJS server\n * ```\n */\nexport const SpaAdapter = defineAdapter<SpaAdapterOptions>({\n name: 'SpaAdapter',\n defaults: {\n clientDir: 'dist/client',\n apiPrefix: '/api',\n exclude: [],\n cacheControl: 'public, max-age=31536000, immutable',\n indexCacheControl: 'no-cache',\n alwaysFallback: false,\n },\n build: (config) => {\n const clientDir = resolveClientDir(config.clientDir ?? 'dist/client')\n const rawPrefix = config.apiPrefix ?? '/api'\n const apiPrefixes = Array.isArray(rawPrefix) ? rawPrefix : [rawPrefix]\n const excludePaths = config.exclude ?? []\n const cacheControl = config.cacheControl ?? 'public, max-age=31536000, immutable'\n const indexCacheControl = config.indexCacheControl ?? 'no-cache'\n const alwaysFallback = config.alwaysFallback ?? false\n\n let indexHtml: string | null = null\n\n /**\n * Every file in the build, as request paths (`/assets/app.js`), captured\n * once at mount.\n *\n * This used to be a `statSync` per request. That is a synchronous\n * filesystem call on the hot path of every non-reserved request, and a\n * slow or contended disk blocks the event loop for unrelated requests.\n * The directory is a static build — its contents are already snapshotted\n * at boot, since `index.html` is read into memory there — so the lookup\n * belongs in memory too. No filesystem access remains in the request path.\n *\n * A build added to after boot is not picked up, which is the same\n * contract `index.html` already had.\n */\n let assetPaths: ReadonlySet<string> = new Set()\n\n function snapshotClientDir(): ReadonlySet<string> {\n const paths = new Set<string>()\n // A hand-rolled walk rather than `readdirSync(dir, { recursive: true })`\n // with `Dirent.parentPath`. Both are newer than the package's declared\n // `node >=20.0`: the `recursive` option landed in 20.1, `parentPath` in\n // 20.12, and the older `Dirent.path` alias it replaced is already gone\n // on Node 24 — so no single spelling covers the supported range. The\n // failure would have been silent, too: the surrounding `catch` turns a\n // `TypeError` from `join(undefined, name)` into an empty snapshot, which\n // reads as \"no files\" and drops every Cache-Control header.\n //\n // `readdirSync(dir, { withFileTypes: true })` is ancient and stable, and\n // tracking the parent ourselves needs no new API.\n const walk = (absDir: string, prefix: string): void => {\n for (const entry of readdirSync(absDir, { withFileTypes: true })) {\n const child = `${prefix}/${entry.name}`\n // Symlinks report as neither file nor directory here (readdir does\n // not follow them), so a symlinked tree is skipped rather than\n // risking a cycle. A symlinked asset simply goes unlabelled — the\n // static layer still serves it, it just misses the cache header.\n if (entry.isDirectory()) walk(join(absDir, entry.name), child)\n else if (entry.isFile()) paths.add(child)\n }\n }\n try {\n walk(clientDir, '')\n } catch {\n // Unreadable build directory — fall back to labelling nothing rather\n // than failing the boot. Serving still works; caching just opts out.\n return new Set()\n }\n return paths\n }\n\n /**\n * How the static layer will answer this path: as the index document, as a\n * long-lived asset, or not at all.\n *\n * Membership of the snapshot IS the containment guard — a traversal like\n * `/../../etc/passwd` simply is not a key — so no prefix comparison is\n * needed and the whole check is one `Set.has`.\n */\n const classifyPath = (pathname: string): 'file' | 'index' | null => {\n let decoded: string\n try {\n decoded = decodeURIComponent(pathname)\n } catch {\n return null\n }\n if (decoded.includes('\\0')) return null\n\n // A directory is served from its `index.html`, so `/` — the single most\n // common request — is an index request, not an asset. Reporting it as\n // neither left the root document without the configured policy.\n const asIndex = decoded.endsWith('/') ? `${decoded}index.html` : null\n if (asIndex && assetPaths.has(asIndex)) return 'index'\n if (!assetPaths.has(decoded)) return null\n return decoded.endsWith('.html') ? 'index' : 'file'\n }\n\n /** Skipped by the fallback: API routes, opted-out paths. */\n const isReserved = (pathname: string): boolean =>\n apiPrefixes.some((p) => isUnder(pathname, p)) ||\n excludePaths.some((p) => isUnder(pathname, p))\n\n return {\n beforeMount({ http }) {\n if (!existsSync(clientDir)) {\n log.warn(`SPA client directory not found: ${clientDir}`)\n log.warn(\n `Build your frontend first, or set clientDir to the correct path ` +\n `(relative paths resolve from ${process.cwd()}).`,\n )\n return\n }\n\n const indexPath = join(clientDir, 'index.html')\n if (!existsSync(indexPath)) {\n log.warn(`index.html not found in ${clientDir}`)\n return\n }\n indexHtml = readFileSync(indexPath, 'utf-8')\n assetPaths = snapshotClientDir()\n\n // Cache-Control runs as its own middleware ahead of the static\n // handler: the engine-agnostic `serveStatic` takes no header hook,\n // and headers set before the static layer writes still land on the\n // response. Keyed off the request path rather than the resolved file,\n // which is the same decision `express.static`'s `setHeaders` made.\n if (cacheControl !== false) {\n http.use(((req, res, next) => {\n const pathname = pathnameOf(req.url)\n // Only label a response the static layer will actually serve.\n // `express.static`'s `setHeaders` fired on hits only; running as\n // a separate middleware loses that, and an unconditional header\n // put `immutable, max-age=31536000` on the 404 for a missing\n // asset — telling every cache to keep that miss for a year.\n const kind = isReserved(pathname) ? null : classifyPath(pathname)\n if (kind) {\n res.setHeader('Cache-Control', kind === 'index' ? indexCacheControl : cacheControl)\n }\n next()\n }) satisfies SpaHandler as unknown as ConnectMiddleware)\n }\n\n http.serveStatic('/', clientDir)\n log.debug(`Serving SPA from ${clientDir}`)\n },\n\n beforeStart({ http }) {\n if (!indexHtml) return\n\n http.use(((req, res, next) => {\n // GET and HEAD both navigate. HEAD used to fall through to a 404,\n // which broke proxies and link checkers probing SPA routes.\n const method = (req.method ?? 'GET').toUpperCase()\n if (method !== 'GET' && method !== 'HEAD') return next()\n\n const pathname = pathnameOf(req.url)\n if (isReserved(pathname)) return next()\n\n // Content negotiation instead of a \"does the path contain a dot\"\n // guess. The old heuristic 404'd every legitimate route carrying a\n // dot (`/users/john.doe`, `/v1.2/spec`) while still handing HTML to\n // genuinely missing assets whose path had no extension.\n const accept = req.headers?.['accept'] as string | string[] | undefined\n if (!alwaysFallback && !acceptsHtml(accept)) return next()\n\n const body = indexHtml as string\n res.setHeader('Content-Type', 'text/html; charset=utf-8')\n res.setHeader('Cache-Control', indexCacheControl)\n res.setHeader('Content-Length', String(Buffer.byteLength(body)))\n res.statusCode = 200\n // HEAD must carry identical headers and no body.\n res.end(method === 'HEAD' ? undefined : body)\n }) satisfies SpaHandler as unknown as ConnectMiddleware)\n },\n }\n },\n})\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"spa.mjs","names":["\"accept\""],"sources":["../../../src/http/middleware/spa.ts"],"sourcesContent":["import { existsSync, readdirSync, readFileSync } from 'node:fs'\nimport { resolve, join, dirname, isAbsolute } from 'node:path'\nimport { Logger, defineAdapter } from '../../core'\nimport type { ConnectMiddleware } from '../runtime'\n\nconst log = Logger.for('SpaAdapter')\n\nexport interface SpaAdapterOptions {\n /**\n * Directory containing the built SPA files (index.html, assets, etc.)\n * Default: 'dist/client'\n *\n * @example\n * ```ts\n * // Vue: 'dist'\n * // React (Vite): 'dist'\n * // React (CRA): 'build'\n * // Svelte: 'build'\n * // Angular: 'dist/my-app/browser'\n * ```\n */\n clientDir?: string\n\n /**\n * URL prefix for API routes. SPA fallback only applies to\n * non-API routes (routes NOT starting with this prefix).\n * Default: '/api'\n *\n * Matching is segment-aware: `/api` excludes `/api` and `/api/users`, but\n * NOT `/apidocs` — a plain `startsWith` swallowed any path that merely\n * began with the same letters.\n *\n * Set to an array for multiple prefixes:\n * ```ts\n * apiPrefix: ['/api', '/graphql', '/_debug']\n * ```\n */\n apiPrefix?: string | string[]\n\n /**\n * Additional paths to exclude from SPA fallback.\n * These paths will NOT serve index.html. Segment-aware, like `apiPrefix`.\n *\n * @example\n * ```ts\n * exclude: ['/health', '/metrics', '/ws']\n * ```\n */\n exclude?: string[]\n\n /**\n * Cache-Control header for static assets (default: 'public, max-age=31536000, immutable')\n * Set to false to disable caching headers.\n */\n cacheControl?: string | false\n\n /**\n * Cache-Control for index.html (default: 'no-cache')\n * index.html should not be cached to ensure clients get fresh builds.\n */\n indexCacheControl?: string\n\n /**\n * Serve index.html even when the client did not ask for HTML.\n * Default: false.\n *\n * By default the fallback only fires for requests whose `Accept` header\n * includes `text/html` — the rule `connect-history-api-fallback` uses. That\n * keeps a missing `/assets/app.js` a 404 instead of handing back an HTML\n * document that the browser then fails to parse as JavaScript, which is a\n * confusing way to discover a broken build.\n *\n * Turn this on if you have non-browser clients that deep-link into SPA\n * routes without an `Accept` header.\n */\n alwaysFallback?: boolean\n}\n\n/**\n * Nearest directory at or above `from` that holds a `package.json`.\n * `null` when the walk reaches the filesystem root without finding one.\n */\nfunction nearestPackageRoot(from: string): string | null {\n let dir = from\n for (;;) {\n if (existsSync(join(dir, 'package.json'))) return dir\n const parent = dirname(dir)\n if (parent === dir) return null\n dir = parent\n }\n}\n\n/**\n * Where to look for the built SPA.\n *\n * `process.cwd()` is the framework's convention (assets and env files resolve\n * the same way) and stays the primary base. But a relative `clientDir` then\n * only works when the process happens to be started from the app directory —\n * `node server/dist/index.js` run from a monorepo root resolved `../web/dist`\n * against the root and found nothing, serving no SPA with only a warning.\n *\n * So: try cwd first, and only if that misses, retry against the package root\n * of the entry script. Both are checked for existence before being chosen, so\n * this can never silently pick a directory that is not there — a genuinely\n * missing build still lands on the cwd path, which is what the warning names.\n *\n * Deliberately not entry-relative FIRST: under `kick dev` the entry is the CLI\n * binary, whose package root is the CLI itself.\n *\n * Exported for tests only — not re-exported from the package index.\n */\nexport function resolveClientDir(\n clientDir: string,\n // Injected rather than read from the process so tests can exercise the\n // monorepo-root case without `process.chdir` / `process.argv` mutation —\n // neither is supported under vitest's `threads` pool, and the root config\n // uses exactly that.\n cwd: string = process.cwd(),\n entry: string | undefined = process.argv[1],\n): string {\n // `resolve` even for an already-absolute path: `resolvesToFile` compares its\n // (always normalized) target against this string, so a trailing slash or a\n // `.`/`..` segment here would make the containment check never match and\n // silently drop the Cache-Control header on every request. Fails closed, so\n // it never weakened the traversal guard — it just stopped doing its job.\n if (isAbsolute(clientDir)) return resolve(clientDir)\n const fromCwd = resolve(cwd, clientDir)\n if (existsSync(fromCwd)) return fromCwd\n\n if (entry) {\n const root = nearestPackageRoot(dirname(resolve(entry)))\n if (root) {\n const fromEntry = resolve(root, clientDir)\n if (existsSync(fromEntry)) return fromEntry\n }\n }\n return fromCwd\n}\n\n/**\n * The slice of the request/response this adapter touches, spelled from the\n * node `http` shapes rather than Express's. `ConnectMiddleware` is typed as\n * Express's `RequestHandler`, but under Fastify and h3 these handlers receive\n * the raw node objects (or a driver), so relying on `req.path` / `res.send`\n * is what made the previous version Express-only.\n */\ninterface SpaRequest {\n url?: string\n method?: string\n headers?: Record<string, string | string[] | undefined>\n}\n\ninterface SpaResponse {\n setHeader(name: string, value: string): void\n statusCode: number\n end(chunk?: string): void\n}\n\ntype SpaHandler = (req: SpaRequest, res: SpaResponse, next: () => void) => void\n\n/** Pathname only — `req.url` carries the query string, and `req.path` is Express-only. */\nfunction pathnameOf(url: string | undefined): string {\n if (!url) return '/'\n const q = url.indexOf('?')\n const h = url.indexOf('#')\n const end = Math.min(q === -1 ? url.length : q, h === -1 ? url.length : h)\n return url.slice(0, end) || '/'\n}\n\n/**\n * Segment-aware prefix test: `/api` covers `/api` and `/api/users`, not\n * `/apidocs`. `startsWith` alone made any same-prefixed route disappear from\n * the SPA fallback and 404 instead.\n */\nfunction isUnder(pathname: string, prefix: string): boolean {\n if (!prefix || prefix === '/') return true\n const p = prefix.endsWith('/') ? prefix.slice(0, -1) : prefix\n return pathname === p || pathname.startsWith(`${p}/`)\n}\n\n/** Media ranges that count as \"the client wants a document\". */\nconst HTML_TYPES = ['text/html', 'application/xhtml+xml']\n\n/** `q` for a media range, defaulting to 1 when absent or unparseable. */\nfunction qualityOf(params: string[]): number {\n const q = params.find((param) => param.startsWith('q='))\n if (!q) return 1\n const value = Number.parseFloat(q.slice(2))\n return Number.isNaN(value) ? 1 : value\n}\n\n/**\n * Does the client want an HTML document back?\n *\n * Parses media ranges with q-values rather than substring-matching:\n * `Accept: text/html;q=0` explicitly says HTML is NOT acceptable (RFC 9110\n * §12.5.1 — \"a value of 0 means not acceptable\"), and a substring check read\n * that as a yes and returned the document anyway.\n *\n * Wildcards are matched by specificity, as RFC 9110 §12.5.1 requires: an exact\n * `text/html` beats `text/*`, so `Accept: text/*, text/html;q=0` correctly\n * rejects HTML even though the wildcard would have accepted it.\n *\n * A bare `*` / `*` deliberately does NOT count, at any q. Assets are fetched\n * with `Accept: * / *`, and treating that as a document request is what makes\n * a missing script come back as HTML — the very thing the fallback rules\n * exist to prevent.\n */\nfunction acceptsHtml(accept: string | string[] | undefined): boolean {\n const raw = Array.isArray(accept) ? accept.join(',') : (accept ?? '')\n if (!raw) return false\n\n const ranges = raw\n .split(',')\n .map((part) => part.split(';').map((t) => t.trim().toLowerCase()))\n .filter(([type]) => Boolean(type))\n\n return HTML_TYPES.some((htmlType) => {\n const [group] = htmlType.split('/')\n // Most specific match wins: exact type, else `type/*`. `*/*` is skipped.\n const exact = ranges.find(([type]) => type === htmlType)\n const wildcard = ranges.find(([type]) => type === `${group}/*`)\n const match = exact ?? wildcard\n return match ? qualityOf(match.slice(1)) > 0 : false\n })\n}\n\n/**\n * SPA adapter — serve a Vue, React, Svelte, or Angular build alongside\n * your KickJS API. API routes are handled by controllers; everything else\n * falls back to index.html for client-side routing.\n *\n * Written against the engine-agnostic `http` surface (`serveStatic` / `use`),\n * so it runs under Express, Fastify, and h3 alike. The previous version\n * reached for `express.static`, `req.path`, and `res.send`, all of which are\n * Express-only — it silently served nothing on the other runtimes.\n *\n * Migrated to the `defineAdapter()` factory — call without `new`, as\n * `ViewAdapter` has since v4.\n *\n * @example\n * ```ts\n * import { SpaAdapter } from '@forinda/kickjs/spa'\n *\n * bootstrap({\n * modules,\n * adapters: [\n * SpaAdapter({\n * clientDir: 'dist/client', // or 'build', 'dist', etc.\n * apiPrefix: '/api',\n * }),\n * ],\n * })\n * ```\n *\n * File structure:\n * ```\n * dist/\n * client/ ← SPA build output\n * index.html\n * assets/\n * app.js\n * style.css\n * server/ ← KickJS server\n * ```\n */\nexport const SpaAdapter = defineAdapter<SpaAdapterOptions>({\n name: 'SpaAdapter',\n defaults: {\n clientDir: 'dist/client',\n apiPrefix: '/api',\n exclude: [],\n cacheControl: 'public, max-age=31536000, immutable',\n indexCacheControl: 'no-cache',\n alwaysFallback: false,\n },\n build: (config) => {\n const clientDir = resolveClientDir(config.clientDir ?? 'dist/client')\n const rawPrefix = config.apiPrefix ?? '/api'\n const apiPrefixes = Array.isArray(rawPrefix) ? rawPrefix : [rawPrefix]\n const excludePaths = config.exclude ?? []\n const cacheControl = config.cacheControl ?? 'public, max-age=31536000, immutable'\n const indexCacheControl = config.indexCacheControl ?? 'no-cache'\n const alwaysFallback = config.alwaysFallback ?? false\n\n let indexHtml: string | null = null\n\n /**\n * Every file in the build, as request paths (`/assets/app.js`), captured\n * once at mount.\n *\n * This used to be a `statSync` per request. That is a synchronous\n * filesystem call on the hot path of every non-reserved request, and a\n * slow or contended disk blocks the event loop for unrelated requests.\n * The directory is a static build — its contents are already snapshotted\n * at boot, since `index.html` is read into memory there — so the lookup\n * belongs in memory too. No filesystem access remains in the request path.\n *\n * A build added to after boot is not picked up, which is the same\n * contract `index.html` already had.\n */\n let assetPaths: ReadonlySet<string> = new Set()\n\n function snapshotClientDir(): ReadonlySet<string> {\n const paths = new Set<string>()\n // A hand-rolled walk rather than `readdirSync(dir, { recursive: true })`\n // with `Dirent.parentPath`. Both are newer than the package's declared\n // `node >=20.0`: the `recursive` option landed in 20.1, `parentPath` in\n // 20.12, and the older `Dirent.path` alias it replaced is already gone\n // on Node 24 — so no single spelling covers the supported range. The\n // failure would have been silent, too: the surrounding `catch` turns a\n // `TypeError` from `join(undefined, name)` into an empty snapshot, which\n // reads as \"no files\" and drops every Cache-Control header.\n //\n // `readdirSync(dir, { withFileTypes: true })` is ancient and stable, and\n // tracking the parent ourselves needs no new API.\n const walk = (absDir: string, prefix: string): void => {\n for (const entry of readdirSync(absDir, { withFileTypes: true })) {\n const child = `${prefix}/${entry.name}`\n // Symlinks report as neither file nor directory here (readdir does\n // not follow them), so a symlinked tree is skipped rather than\n // risking a cycle. A symlinked asset simply goes unlabelled — the\n // static layer still serves it, it just misses the cache header.\n if (entry.isDirectory()) walk(join(absDir, entry.name), child)\n else if (entry.isFile()) paths.add(child)\n }\n }\n try {\n walk(clientDir, '')\n } catch {\n // Unreadable build directory — fall back to labelling nothing rather\n // than failing the boot. Serving still works; caching just opts out.\n return new Set()\n }\n return paths\n }\n\n /**\n * How the static layer will answer this path: as the index document, as a\n * long-lived asset, or not at all.\n *\n * Membership of the snapshot IS the containment guard — a traversal like\n * `/../../etc/passwd` simply is not a key — so no prefix comparison is\n * needed and the whole check is one `Set.has`.\n */\n const classifyPath = (pathname: string): 'file' | 'index' | null => {\n let decoded: string\n try {\n decoded = decodeURIComponent(pathname)\n } catch {\n return null\n }\n if (decoded.includes('\\0')) return null\n\n // A directory is served from its `index.html`, so `/` — the single most\n // common request — is an index request, not an asset. Reporting it as\n // neither left the root document without the configured policy.\n const asIndex = decoded.endsWith('/') ? `${decoded}index.html` : null\n if (asIndex && assetPaths.has(asIndex)) return 'index'\n if (!assetPaths.has(decoded)) return null\n return decoded.endsWith('.html') ? 'index' : 'file'\n }\n\n /** Skipped by the fallback: API routes, opted-out paths. */\n const isReserved = (pathname: string): boolean =>\n apiPrefixes.some((p) => isUnder(pathname, p)) ||\n excludePaths.some((p) => isUnder(pathname, p))\n\n return {\n beforeMount({ http }) {\n if (!existsSync(clientDir)) {\n log.warn(`SPA client directory not found: ${clientDir}`)\n log.warn(\n `Build your frontend first, or set clientDir to the correct path ` +\n `(relative paths resolve from ${process.cwd()}).`,\n )\n return\n }\n\n const indexPath = join(clientDir, 'index.html')\n if (!existsSync(indexPath)) {\n log.warn(`index.html not found in ${clientDir}`)\n return\n }\n indexHtml = readFileSync(indexPath, 'utf-8')\n assetPaths = snapshotClientDir()\n\n // Cache-Control runs as its own middleware ahead of the static\n // handler: the engine-agnostic `serveStatic` takes no header hook,\n // and headers set before the static layer writes still land on the\n // response. Keyed off the request path rather than the resolved file,\n // which is the same decision `express.static`'s `setHeaders` made.\n if (cacheControl !== false) {\n http.use(((req, res, next) => {\n const pathname = pathnameOf(req.url)\n // Only label a response the static layer will actually serve.\n // `express.static`'s `setHeaders` fired on hits only; running as\n // a separate middleware loses that, and an unconditional header\n // put `immutable, max-age=31536000` on the 404 for a missing\n // asset — telling every cache to keep that miss for a year.\n const kind = isReserved(pathname) ? null : classifyPath(pathname)\n if (kind) {\n res.setHeader('Cache-Control', kind === 'index' ? indexCacheControl : cacheControl)\n }\n next()\n }) satisfies SpaHandler as unknown as ConnectMiddleware)\n }\n\n http.serveStatic('/', clientDir)\n log.debug(`Serving SPA from ${clientDir}`)\n },\n\n beforeStart({ http }) {\n if (!indexHtml) return\n\n http.use(((req, res, next) => {\n // GET and HEAD both navigate. HEAD used to fall through to a 404,\n // which broke proxies and link checkers probing SPA routes.\n const method = (req.method ?? 'GET').toUpperCase()\n if (method !== 'GET' && method !== 'HEAD') return next()\n\n const pathname = pathnameOf(req.url)\n if (isReserved(pathname)) return next()\n\n // Content negotiation instead of a \"does the path contain a dot\"\n // guess. The old heuristic 404'd every legitimate route carrying a\n // dot (`/users/john.doe`, `/v1.2/spec`) while still handing HTML to\n // genuinely missing assets whose path had no extension.\n const accept = req.headers?.['accept'] as string | string[] | undefined\n if (!alwaysFallback && !acceptsHtml(accept)) return next()\n\n const body = indexHtml as string\n res.setHeader('Content-Type', 'text/html; charset=utf-8')\n res.setHeader('Cache-Control', indexCacheControl)\n res.setHeader('Content-Length', String(Buffer.byteLength(body)))\n res.statusCode = 200\n // HEAD must carry identical headers and no body.\n res.end(method === 'HEAD' ? undefined : body)\n }) satisfies SpaHandler as unknown as ConnectMiddleware)\n },\n }\n },\n})\n"],"mappings":"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"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
|
|
2
|
-
import { a as RawUploadPart, c as buildFileTypeFilter, i as upload, l as resolveMimeTypes, n as buildUploadMiddleware, o as UploadedFileLike, r as cleanupFiles, s as applyUploadConfig, t as UploadOptions } from "../../upload-
|
|
2
|
+
import { a as RawUploadPart, c as buildFileTypeFilter, i as upload, l as resolveMimeTypes, n as buildUploadMiddleware, o as UploadedFileLike, r as cleanupFiles, s as applyUploadConfig, t as UploadOptions } from "../../upload-CiMtdXWD.mjs";
|
|
3
3
|
export { type RawUploadPart, UploadOptions, type UploadedFileLike, applyUploadConfig, buildFileTypeFilter, buildUploadMiddleware, cleanupFiles, resolveMimeTypes, upload };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs
|
|
2
|
+
* @forinda/kickjs v10.1.0
|
|
3
3
|
*
|
|
4
4
|
* Copyright (c) Felix Orinda
|
|
5
5
|
*
|
|
@@ -8,6 +8,6 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @license MIT
|
|
10
10
|
*/
|
|
11
|
-
import{HttpException,HttpStatus}from"../../core/errors.mjs";import{n as buildFileTypeFilter,r as resolveMimeTypes,t as applyUploadConfig}from"../../upload-config-
|
|
11
|
+
import{HttpException,HttpStatus}from"../../core/errors.mjs";import{n as buildFileTypeFilter,r as resolveMimeTypes,t as applyUploadConfig}from"../../upload-config-Dz2INCaa.mjs";import{createRequire}from"node:module";import{unlink}from"node:fs/promises";const requireFromHere=createRequire(import.meta.url);let _multer;function loadMulter(){if(_multer)return _multer;try{return _multer=requireFromHere(`multer`),_multer}catch{throw Error(`@forinda/kickjs: file uploads require the 'multer' package, which is not installed.
|
|
12
12
|
Install it as a peer dependency: pnpm add multer (or npm i multer / yarn add multer).`)}}function translateMulterErrors(mw,options){let maxSize=options.maxSize??5242880;return(req,res,next)=>{mw(req,res,err=>{if(err&&err.code===`LIMIT_FILE_SIZE`){let name=err.field??`file`;return next(new HttpException(HttpStatus.PAYLOAD_TOO_LARGE,`File ${name} exceeds the ${maxSize}-byte limit`))}next(err)})}}function createMulter(options){let limits={fileSize:options.maxSize??5242880},fileFilter;if(options.allowedTypes){let typeAllowed=buildFileTypeFilter(options.allowedTypes,options.customMimeMap);fileFilter=(_req,file,cb)=>{typeAllowed(file.mimetype,file.originalname)?cb(null,!0):cb(new HttpException(HttpStatus.UNSUPPORTED_MEDIA_TYPE,`File type ${file.mimetype} is not allowed`))}}let multerOptions={limits,...fileFilter?{fileFilter}:{},...options.storage?{storage:options.storage}:{},...options.dest?{dest:options.dest}:{}};return loadMulter()(multerOptions)}function bothConventions(handler){return((first,second,third)=>third===void 0&&typeof second==`function`&&typeof first==`object`&&first&&`req`in first&&`res`in first?new Promise((resolve,reject)=>{handler(first.req,first.res,err=>{if(err)return reject(err);resolve(),second()})}):handler(first,second,third))}function single(fieldName,options={}){return bothConventions(translateMulterErrors(createMulter(options).single(fieldName),options))}function array(fieldName,maxCount=10,options={}){return bothConventions(translateMulterErrors(createMulter(options).array(fieldName,maxCount),options))}function none(options={}){return bothConventions(translateMulterErrors(createMulter(options).none(),options))}function cleanupFiles(){return(req,res,next)=>{res.on(`finish`,async()=>{let files=[];if(req.file?.path&&files.push(req.file),Array.isArray(req.files))for(let f of req.files)f?.path&&files.push(f);for(let file of files)try{await unlink(file.path)}catch{}}),next()}}function buildUploadMiddleware(config){let options={};config.maxSize&&(options.maxSize=config.maxSize),config.allowedTypes&&(options.allowedTypes=config.allowedTypes),config.customMimeMap&&(options.customMimeMap=config.customMimeMap);let fieldName=config.fieldName??`file`;switch(config.mode){case`single`:return single(fieldName,options);case`array`:return array(fieldName,config.maxCount??10,options);case`none`:return none(options)}}const upload={single,array,none};export{applyUploadConfig,buildFileTypeFilter,buildUploadMiddleware,cleanupFiles,resolveMimeTypes,upload};
|
|
13
13
|
//# sourceMappingURL=upload.mjs.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs
|
|
2
|
+
* @forinda/kickjs v10.1.0
|
|
3
3
|
*
|
|
4
4
|
* Copyright (c) Felix Orinda
|
|
5
5
|
*
|
|
@@ -8,5 +8,5 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @license MIT
|
|
10
10
|
*/
|
|
11
|
-
import{Logger}from"../../core/logger.mjs";import"../../core-
|
|
11
|
+
import{Logger}from"../../core/logger.mjs";import"../../core-B0JmdBvX.mjs";import{t as defineAdapter}from"../../define-adapter-BLKlUbSL.mjs";import{resolve}from"node:path";const log=Logger.for(`ViewEngine`),ViewAdapter=defineAdapter({name:`ViewAdapter`,build:options=>({beforeMount({app}){let{engine,ext,viewsDir=`src/views`}=options;engine.__express?app.engine(ext,engine.__express):typeof engine.renderFile==`function`?app.engine(ext,(path,opts,callback)=>{engine.renderFile(path,opts,callback)}):(typeof engine==`function`||log.warn(`Engine for .${ext} does not have __express or renderFile. Trying as-is.`),app.engine(ext,engine)),app.set(`view engine`,ext),app.set(`views`,resolve(viewsDir)),log.debug(`View engine: ${ext} (${resolve(viewsDir)})`)}})});export{ViewAdapter};
|
|
12
12
|
//# sourceMappingURL=views.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"views.mjs","names":[],"sources":["../../../src/http/middleware/views.ts"],"sourcesContent":["import { resolve } from 'node:path'\nimport { Logger, defineAdapter } from '../../core'\n\nconst log = Logger.for('ViewEngine')\n\nexport interface ViewAdapterOptions {\n /**\n * Template engine — pass the engine module or a render function.\n * Supported engines: ejs, pug, handlebars, nunjucks, or any\n * Express-compatible engine with a __express property.\n *\n * @example\n * ```ts\n * import ejs from 'ejs'\n * ViewAdapter({ engine: ejs, ext: 'ejs' })\n *\n * import pug from 'pug'\n * ViewAdapter({ engine: pug, ext: 'pug' })\n * ```\n */\n engine: any\n\n /** File extension for templates (e.g., 'ejs', 'pug', 'hbs') */\n ext: string\n\n /** Directory containing template files (default: 'src/views') */\n viewsDir?: string\n\n /** Default layout template (optional — depends on engine) */\n layout?: string\n}\n\n/**\n * View/template adapter — pluggable template engine support for KickJS.\n *\n * Registers an Express view engine and sets the views directory.\n * Use `ctx.render()` in controllers to render templates.\n *\n * Migrated to the `defineAdapter()` factory in v4 — call without `new`.\n * The pre-v4 `new ViewAdapter({...})` form is gone; see\n * `docs/guide/migration-v3-to-v4.md` for the rename.\n *\n * @example\n * ```ts\n * import ejs from 'ejs'\n * import { ViewAdapter } from '@forinda/kickjs'\n *\n * bootstrap({\n * modules,\n * adapters: [\n * ViewAdapter({ engine: ejs, ext: 'ejs', viewsDir: 'src/views' }),\n * ],\n * })\n *\n * // In a controller:\n * @Get('/dashboard')\n * async dashboard(ctx: RequestContext) {\n * ctx.render('dashboard', { user: currentUser, title: 'Dashboard' })\n * }\n * ```\n */\nexport const ViewAdapter = defineAdapter<ViewAdapterOptions>({\n name: 'ViewAdapter',\n build: (options) => ({\n beforeMount({ app }) {\n const { engine, ext, viewsDir = 'src/views' } = options\n\n // Register the engine. Three resolution paths cover every\n // mainstream template engine + a fall-through for custom ones.\n if (engine.__express) {\n // EJS, Pug — have __express method\n app.engine(ext, engine.__express)\n } else if (typeof engine.renderFile === 'function') {\n // Engines with renderFile (nunjucks-style)\n app.engine(\n ext,\n (path: string, opts: any, callback: (err: Error | null, html?: string) => void) => {\n engine.renderFile(path, opts, callback)\n },\n )\n } else if (typeof engine === 'function') {\n // Custom render function: (path, options, callback) => void\n app.engine(ext, engine)\n } else {\n log.warn(`Engine for .${ext} does not have __express or renderFile. Trying as-is.`)\n app.engine(ext, engine)\n }\n\n app.set('view engine', ext)\n app.set('views', resolve(viewsDir))\n\n log.debug(`View engine: ${ext} (${resolve(viewsDir)})`)\n },\n }),\n})\n"],"mappings":";;;;;;;;;;2KAGA,MAAM,IAAM,OAAO,IAAI,YAAY,EA0DtB,YAAc,cAAkC,CAC3D,KAAM,cACN,MAAQ,UAAa,CACnB,YAAY,CAAE,KAAO,CACnB,GAAM,CAAE,OAAQ,IAAK,SAAW,aAAgB,QAI5C,OAAO,UAET,IAAI,OAAO,IAAK,OAAO,SAAS,EACvB,OAAO,OAAO,YAAe,WAEtC,IAAI,OACF,KACC,KAAc,KAAW,WAAyD,CACjF,OAAO,WAAW,KAAM,KAAM,QAAQ,CACxC,CACF,GACS,OAAO,QAAW,YAI3B,IAAI,KAAK,eAAe,IAAI,sDAAsD,
|
|
1
|
+
{"version":3,"file":"views.mjs","names":[],"sources":["../../../src/http/middleware/views.ts"],"sourcesContent":["import { resolve } from 'node:path'\nimport { Logger, defineAdapter } from '../../core'\n\nconst log = Logger.for('ViewEngine')\n\nexport interface ViewAdapterOptions {\n /**\n * Template engine — pass the engine module or a render function.\n * Supported engines: ejs, pug, handlebars, nunjucks, or any\n * Express-compatible engine with a __express property.\n *\n * @example\n * ```ts\n * import ejs from 'ejs'\n * ViewAdapter({ engine: ejs, ext: 'ejs' })\n *\n * import pug from 'pug'\n * ViewAdapter({ engine: pug, ext: 'pug' })\n * ```\n */\n engine: any\n\n /** File extension for templates (e.g., 'ejs', 'pug', 'hbs') */\n ext: string\n\n /** Directory containing template files (default: 'src/views') */\n viewsDir?: string\n\n /** Default layout template (optional — depends on engine) */\n layout?: string\n}\n\n/**\n * View/template adapter — pluggable template engine support for KickJS.\n *\n * Registers an Express view engine and sets the views directory.\n * Use `ctx.render()` in controllers to render templates.\n *\n * Migrated to the `defineAdapter()` factory in v4 — call without `new`.\n * The pre-v4 `new ViewAdapter({...})` form is gone; see\n * `docs/guide/migration-v3-to-v4.md` for the rename.\n *\n * @example\n * ```ts\n * import ejs from 'ejs'\n * import { ViewAdapter } from '@forinda/kickjs'\n *\n * bootstrap({\n * modules,\n * adapters: [\n * ViewAdapter({ engine: ejs, ext: 'ejs', viewsDir: 'src/views' }),\n * ],\n * })\n *\n * // In a controller:\n * @Get('/dashboard')\n * async dashboard(ctx: RequestContext) {\n * ctx.render('dashboard', { user: currentUser, title: 'Dashboard' })\n * }\n * ```\n */\nexport const ViewAdapter = defineAdapter<ViewAdapterOptions>({\n name: 'ViewAdapter',\n build: (options) => ({\n beforeMount({ app }) {\n const { engine, ext, viewsDir = 'src/views' } = options\n\n // Register the engine. Three resolution paths cover every\n // mainstream template engine + a fall-through for custom ones.\n if (engine.__express) {\n // EJS, Pug — have __express method\n app.engine(ext, engine.__express)\n } else if (typeof engine.renderFile === 'function') {\n // Engines with renderFile (nunjucks-style)\n app.engine(\n ext,\n (path: string, opts: any, callback: (err: Error | null, html?: string) => void) => {\n engine.renderFile(path, opts, callback)\n },\n )\n } else if (typeof engine === 'function') {\n // Custom render function: (path, options, callback) => void\n app.engine(ext, engine)\n } else {\n log.warn(`Engine for .${ext} does not have __express or renderFile. Trying as-is.`)\n app.engine(ext, engine)\n }\n\n app.set('view engine', ext)\n app.set('views', resolve(viewsDir))\n\n log.debug(`View engine: ${ext} (${resolve(viewsDir)})`)\n },\n }),\n})\n"],"mappings":";;;;;;;;;;2KAGA,MAAM,IAAM,OAAO,IAAI,YAAY,EA0DtB,YAAc,cAAkC,CAC3D,KAAM,cACN,MAAQ,UAAa,CACnB,YAAY,CAAE,KAAO,CACnB,GAAM,CAAE,OAAQ,IAAK,SAAW,aAAgB,QAI5C,OAAO,UAET,IAAI,OAAO,IAAK,OAAO,SAAS,EACvB,OAAO,OAAO,YAAe,WAEtC,IAAI,OACF,KACC,KAAc,KAAW,WAAyD,CACjF,OAAO,WAAW,KAAM,KAAM,QAAQ,CACxC,CACF,GACS,OAAO,QAAW,YAI3B,IAAI,KAAK,eAAe,IAAI,sDAAsD,EAClF,IAAI,OAAO,IAAK,MAAM,GAGxB,IAAI,IAAI,cAAe,GAAG,EAC1B,IAAI,IAAI,QAAS,QAAQ,QAAQ,CAAC,EAElC,IAAI,MAAM,gBAAgB,IAAI,IAAI,QAAQ,QAAQ,EAAE,EAAE,CACxD,CACF,EACF,CAAC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs
|
|
2
|
+
* @forinda/kickjs v10.1.0
|
|
3
3
|
*
|
|
4
4
|
* Copyright (c) Felix Orinda
|
|
5
5
|
*
|
|
@@ -8,4 +8,4 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @license MIT
|
|
10
10
|
*/
|
|
11
|
-
|
|
11
|
+
export{u as FILTER_OPERATORS,t as buildQueryParams,n as getQueryParsingDefaults,r as parseFilters,i as parsePagination,a as parseQuery,o as parseSearchQuery,s as parseSort,c as resetQueryParsingDefaults,l as setQueryParsingDefaults}from"../../query-B_k9qn0R.mjs";
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
|
|
2
|
-
import { i as buildRouteTable, n as _setExternalContributorSources, r as assertRouteUnique, t as BuildRoutesOptions } from "../router-builder-
|
|
2
|
+
import { i as buildRouteTable, n as _setExternalContributorSources, r as assertRouteUnique, t as BuildRoutesOptions } from "../router-builder-zWuVIOmd.mjs";
|
|
3
3
|
export { BuildRoutesOptions, _setExternalContributorSources, assertRouteUnique, buildRouteTable };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs
|
|
2
|
+
* @forinda/kickjs v10.1.0
|
|
3
3
|
*
|
|
4
4
|
* Copyright (c) Felix Orinda
|
|
5
5
|
*
|
|
@@ -8,4 +8,4 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @license MIT
|
|
10
10
|
*/
|
|
11
|
-
|
|
11
|
+
export{t as _setExternalContributorSources,n as assertRouteUnique,r as buildRouteTable}from"../router-builder-JvMTp50k.mjs";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
import { p as HttpRuntime } from "../../adapter-
|
|
2
|
+
import { p as HttpRuntime } from "../../adapter-ClEh-Oos.mjs";
|
|
3
3
|
import { IncomingMessage, ServerResponse } from "node:http";
|
|
4
4
|
//#region src/http/runtimes/fastify.d.ts
|
|
5
5
|
interface FastifyReplyLike {
|
|
@@ -75,4 +75,5 @@ export interface FastifyRuntimeTypes {
|
|
|
75
75
|
app: import('fastify').FastifyInstance;
|
|
76
76
|
}
|
|
77
77
|
//#endregion
|
|
78
|
+
export {};
|
|
78
79
|
//# sourceMappingURL=fastify.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fastify.d.mts","names":[],"sources":["../../../src/http/runtimes/fastify.ts"],"mappings":";;;;UAgDU;EACR,KAAK,qBAAqB;EAC1B,KAAK,oBAAoB;EACzB,OAAO,cAAc,iBAAiB;EACtC,KAAK,sBAAsB;EAC3B,WAAW,KAAK,8BAA8B;EAC9C,UAAU;EACV;WACS;WACA,KAAK;;UAEN;EACR,KAAK;EACL;EACA;EACA;;EAEA,UAAU;;EAEV,cAAc,sBAAsB;EACpC;;KAEG,kBAAkB,SAAS,oBAAoB,OAAO,4BAA4B;UAC7E;EACR,SAAS,iBAAiB,iBAAiB,gBAAgB;EAC3D,IAAI,eAAe;EACnB,MAAM;IAAQ;IAAgB;IAAa,SAAS;;EACpD,mBAAmB,SAAS;EAC5B,gBACE,UAAU,cAAc,KAAK,oBAAoB,OAAO;EAE1D,SAAS;EACT,QAAQ,KAAK,iBAAiB,KAAK;;;;;;EAMnC,sBACE,kBAAkB,mBAClB;IAAQ;KACR,QAAQ;;;KAQP,qBACH,KAAK,oBACL,eAAe,QACf,OAAO,KAAK,cAAc;
|
|
1
|
+
{"version":3,"file":"fastify.d.mts","names":[],"sources":["../../../src/http/runtimes/fastify.ts"],"mappings":";;;;UAgDU;EACR,KAAK,qBAAqB;EAC1B,KAAK,oBAAoB;EACzB,OAAO,cAAc,iBAAiB;EACtC,KAAK,sBAAsB;EAC3B,WAAW,KAAK,8BAA8B;EAC9C,UAAU;EACV;WACS;WACA,KAAK;;UAEN;EACR,KAAK;EACL;EACA;EACA;;EAEA,UAAU;;EAEV,cAAc,sBAAsB;EACpC;;KAEG,kBAAkB,SAAS,oBAAoB,OAAO,4BAA4B;UAC7E;EACR,SAAS,iBAAiB,iBAAiB,gBAAgB;EAC3D,IAAI,eAAe;EACnB,MAAM;IAAQ;IAAgB;IAAa,SAAS;;EACpD,mBAAmB,SAAS;EAC5B,gBACE,UAAU,cAAc,KAAK,oBAAoB,OAAO;EAE1D,SAAS;EACT,QAAQ,KAAK,iBAAiB,KAAK;;;;;;EAMnC,sBACE,kBAAkB,mBAClB;IAAQ;KACR,QAAQ;;;KAQP,qBACH,KAAK,oBACL,eAAe,QACf,OAAO,KAAK,cAAc;UAyBlB;EACR;EACA;EACA;EACA;EACA;EACA,iBAAiB,QAAQ;;;;;;;wBAqMX,kBAAkB,YAAY;;;;;;;;iBAuN7B;EACf,2BAA2B;EAC3B,4BAA4B;EAC5B,uBAAuB"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs
|
|
2
|
+
* @forinda/kickjs v10.1.0
|
|
3
3
|
*
|
|
4
4
|
* Copyright (c) Felix Orinda
|
|
5
5
|
*
|
|
@@ -8,6 +8,6 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @license MIT
|
|
10
10
|
*/
|
|
11
|
-
import{HttpException}from"../../core/errors.mjs";import{
|
|
12
|
-
Install it as a peer dependency: pnpm add @fastify/multipart.`);let rawParts=[],fields={};for await(let part of request.parts())part.type===`file`&&part.toBuffer?rawParts.push({fieldname:part.fieldname,filename:part.filename??``,mimetype:part.mimetype??`application/octet-stream`,buffer:await part.toBuffer()}):fields[part.fieldname]=part.value;let{file,files}=applyUploadConfig(rawParts,entry.meta.upload);raw.file=file,raw.files=files,raw.body={...raw.body,...fields}}let headerId=raw.headers[`x-request-id`],requestId=(Array.isArray(headerId)?headerId[0]:headerId)||raw.requestId,store=createRequestStore(requestId);raw.requestId=store.requestId,reply.raw.once(`close`,()=>disposeRequestStore(store)),await requestStore.run(store,async()=>{publishMatchedRoute(raw,entry);let ctx=new RequestContext(raw,reply,NOOP_NEXT,replyDriver(reply));if(entry.earlyContributorRunner&&(await entry.earlyContributorRunner(ctx),reply.sent))return;validator&&await new Promise((resolve,reject)=>{validator(raw,void 0,err=>err?reject(err):resolve())});for(let mw of entry.middlewares){let advanced=!1;if(await new Promise((resolve,reject)=>{Promise.resolve(mw(ctx,err=>{advanced=!0,err?reject(err):resolve()})).catch(reject)}),!advanced||reply.sent)return}if(entry.contributorRunner&&await entry.contributorRunner(ctx),reply.sent)return;let result=await entry.handler(ctx);reply.sent||applyHandlerResult(ctx,result)})}}function fastifyRuntime(){return{name:`fastify`,createApp(options={}){let app=loadPeer(`fastify`)({logger:!1,routerOptions:{ignoreTrailingSlash:!0},trustProxy:typeof options.trustProxy!=`function`&&options.trustProxy});app[QUEUE]=[];let addParser=app.addContentTypeParser?.bind(app);return addParser&&(addParser(/^application\/[\w.+-]+\+json$/,{parseAs:`string`},(_req,body,done)=>{if(body===``)return done(null,void 0);try{done(null,JSON.parse(body))}catch(err){done(HttpException.badRequest(err instanceof Error?err.message:`Request body could not be parsed`),void 0)}}),addParser(`application/x-www-form-urlencoded`,{parseAs:`string`},(_req,body,done)=>{done(null,Object.fromEntries(new URLSearchParams(body)))}),addParser(/^text\//,{parseAs:`string`},(_req,body,done)=>{done(null,body)}),addParser(`*`,{parseAs:`string`},(req,body,done)=>{if(!body)return done(null,void 0);done(unsupportedMediaTypeError(req.headers?.[`content-type`]),void 0)})),app},nodeHandler(app){let state=app;state[READY]||(state[READY]=(async()=>{if(state[NEEDS_MULTIPART]){let multipart=loadPeer(`@fastify/multipart`);await app.register(multipart,{limits:{fileSize:2**53-1}})}let middie=loadPeer(`@fastify/middie`);await app.register(middie);for(let{mw,opts}of state[QUEUE]??[])opts?.path===void 0?app.use(mw):app.use(opts.path,mw);await app.ready()})());let readyP=state[READY];return(req,res,next)=>{next&&(req[NEXT]=next),readyP.then(()=>app.routing(req,res)).catch(err=>{next&&next(err)})}},mountRoutes(app,table){for(let{mountPath,routes}of table)for(let entry of routes){entry.meta.upload&&(app[NEEDS_MULTIPART]=!0);let url=joinPath(mountPath,entry.path);app.route({method:entry.method,url,handler:routeHandler(entry)})}},useConnect(app,mw,opts){app[QUEUE]?.push({mw,opts})},serveStatic(app,prefix,dir){let serveStatic=loadPeer(`serve-static`);this.useConnect(app,serveStatic(dir),{path:prefix})},setNotFound(app,mw){app.setNotFoundHandler((request,reply)=>{let next=request.raw[NEXT];
|
|
11
|
+
import{HttpException}from"../../core/errors.mjs";import{n as applyHandlerResult,o as createRequestStore,p as unsupportedMediaTypeError,s as disposeRequestStore}from"../../reply-BDK9xt7Z.mjs";import{d as requestStore,i as stringifyJson,r as publishMatchedRoute,t as RequestContext}from"../../context-CtL9RxXp.mjs";import{validate}from"../middleware/validate.mjs";import{t as applyUploadConfig}from"../../upload-config-Dz2INCaa.mjs";import"../middleware/upload.mjs";import{createRequire}from"node:module";const peerRequire=createRequire(import.meta.url),loadPeer=name=>{let mod=peerRequire(name);return mod&&mod.__esModule&&mod.default?mod.default:mod.default??mod},QUEUE=Symbol(`kickjs.fastify.connectQueue`),READY=Symbol(`kickjs.fastify.ready`),NEXT=Symbol(`kickjs.fastify.next`),NEEDS_MULTIPART=Symbol(`kickjs.fastify.needsMultipart`),NOOP_NEXT=()=>{};function settleOrFail(reply,returned){typeof returned?.then==`function`&&Promise.resolve(returned).then(void 0,()=>{!reply.sent&&!reply.raw.headersSent&&reply.code(500).send()})}const NO_FALLTHROUGH=Symbol(`kickjs.fastify.noFallthrough`);var FastifyReplyDriver=class{reply;constructor(reply){this.reply=reply}status(code){return this.reply.code(code),this}json(data){return this.reply.getHeader(`content-type`)||this.reply.type(`application/json; charset=utf-8`),this.reply.serializer(stringifyJson).send(data),this}send(data){return this.reply.send(data),this}type(contentType){return this.reply.type(contentType),this}setHeader(name,value){return this.reply.header(name,value),this}render(){throw Error(`ctx.render() is not supported on the Fastify runtime (no view engine).`)}writeHead(statusCode,headers){return this.reply.hijack(),this.reply.raw.writeHead(statusCode,headers),this}flushHeaders(){return this.reply.raw.flushHeaders?.(),this}write(chunk){return this.reply.raw.write(chunk)}end(data){return this.reply.raw.headersSent?this.reply.raw.end(data):this.reply.send(data??``),this}once(event,listener){return this.reply.raw.once(event,listener),this}get headersSent(){return this.reply.sent||this.reply.raw.headersSent}};function replyDriver(reply){return new FastifyReplyDriver(reply)}function routeHandler(entry){let validator=entry.meta.validation?validate(entry.meta.validation):void 0;return async(request,reply)=>{let raw=request.raw;if(raw.body=request.body,raw.params=request.params,raw.query=request.query,entry.meta.upload&&entry.meta.upload.mode!==`none`){if(typeof request.parts!=`function`)throw Error(`@forinda/kickjs: @FileUpload on the Fastify runtime requires '@fastify/multipart'.
|
|
12
|
+
Install it as a peer dependency: pnpm add @fastify/multipart.`);let rawParts=[],fields={};for await(let part of request.parts())part.type===`file`&&part.toBuffer?rawParts.push({fieldname:part.fieldname,filename:part.filename??``,mimetype:part.mimetype??`application/octet-stream`,buffer:await part.toBuffer()}):fields[part.fieldname]=part.value;let{file,files}=applyUploadConfig(rawParts,entry.meta.upload);raw.file=file,raw.files=files,raw.body={...raw.body,...fields}}let headerId=raw.headers[`x-request-id`],requestId=(Array.isArray(headerId)?headerId[0]:headerId)||raw.requestId,store=createRequestStore(requestId);raw.requestId=store.requestId,reply.raw.once(`close`,()=>disposeRequestStore(store)),await requestStore.run(store,async()=>{publishMatchedRoute(raw,entry);let ctx=new RequestContext(raw,reply,NOOP_NEXT,replyDriver(reply));if(entry.earlyContributorRunner&&(await entry.earlyContributorRunner(ctx),reply.sent))return;validator&&await new Promise((resolve,reject)=>{validator(raw,void 0,err=>err?reject(err):resolve())});for(let mw of entry.middlewares){let advanced=!1;if(await new Promise((resolve,reject)=>{Promise.resolve(mw(ctx,err=>{advanced=!0,err?reject(err):resolve()})).catch(reject)}),!advanced||reply.sent)return}if(entry.contributorRunner&&await entry.contributorRunner(ctx),reply.sent)return;let result=await entry.handler(ctx);reply.sent||await applyHandlerResult(ctx,result)})}}function fastifyRuntime(){return{name:`fastify`,createApp(options={}){let app=loadPeer(`fastify`)({logger:!1,routerOptions:{ignoreTrailingSlash:!0},trustProxy:typeof options.trustProxy!=`function`&&options.trustProxy});app[QUEUE]=[];let addParser=app.addContentTypeParser?.bind(app);return addParser&&(addParser(/^application\/[\w.+-]+\+json$/,{parseAs:`string`},(_req,body,done)=>{if(body===``)return done(null,void 0);try{done(null,JSON.parse(body))}catch(err){done(HttpException.badRequest(err instanceof Error?err.message:`Request body could not be parsed`),void 0)}}),addParser(`application/x-www-form-urlencoded`,{parseAs:`string`},(_req,body,done)=>{done(null,Object.fromEntries(new URLSearchParams(body)))}),addParser(/^text\//,{parseAs:`string`},(_req,body,done)=>{done(null,body)}),addParser(`*`,{parseAs:`string`},(req,body,done)=>{if(!body)return done(null,void 0);done(unsupportedMediaTypeError(req.headers?.[`content-type`]),void 0)})),options.notFoundFallthrough===!1&&(app[NO_FALLTHROUGH]=!0),app},nodeHandler(app){let state=app;state[READY]||(state[READY]=(async()=>{if(state[NEEDS_MULTIPART]){let multipart=loadPeer(`@fastify/multipart`);await app.register(multipart,{limits:{fileSize:2**53-1}})}let middie=loadPeer(`@fastify/middie`);await app.register(middie);for(let{mw,opts}of state[QUEUE]??[])opts?.path===void 0?app.use(mw):app.use(opts.path,mw);await app.ready()})());let readyP=state[READY];return(req,res,next)=>{next&&(req[NEXT]=next),readyP.then(()=>app.routing(req,res)).catch(err=>{next&&next(err)})}},mountRoutes(app,table){for(let{mountPath,routes}of table)for(let entry of routes){entry.meta.upload&&(app[NEEDS_MULTIPART]=!0);let url=joinPath(mountPath,entry.path);app.route({method:entry.method,url,handler:routeHandler(entry)})}},useConnect(app,mw,opts){app[QUEUE]?.push({mw,opts})},serveStatic(app,prefix,dir){let serveStatic=loadPeer(`serve-static`);this.useConnect(app,serveStatic(dir),{path:prefix})},setNotFound(app,mw){app.setNotFoundHandler((request,reply)=>{let next=request.raw[NEXT];next&&!app[NO_FALLTHROUGH]?(reply.hijack(),next()):settleOrFail(reply,mw(request.raw,replyDriver(reply),NOOP_NEXT))})},setErrorHandler(app,mw){app.setErrorHandler((err,request,reply)=>{settleOrFail(reply,mw(err,request.raw,replyDriver(reply),NOOP_NEXT))})},capabilities:{render:!1,uploads:!0,connectMiddleware:!0,nativeBodyParsing:!0,http2:!0}}}function joinPath(mountPath,path){let a=mountPath.endsWith(`/`)?mountPath.slice(0,-1):mountPath;return path===`/`||path===``?a===``?`/`:a:`${a}${path.startsWith(`/`)?path:`/${path}`}`}export{fastifyRuntime};
|
|
13
13
|
//# sourceMappingURL=fastify.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fastify.mjs","names":[],"sources":["../../../src/http/runtimes/fastify.ts"],"sourcesContent":["// The Fastify runtime — `@forinda/kickjs/fastify`.\n//\n// Implements the same HttpRuntime contract as expressRuntime, over Fastify 5.\n// KickJS still owns decorators → RouteTable, the contributor pipeline, and the\n// RequestContext surface; this runtime materializes routes as NATIVE Fastify\n// routes and wraps `reply` in a RuntimeResponse so `ctx.json` / `ctx.html` /\n// `ctx.sse` work unchanged.\n//\n// `fastify`, `@fastify/middie` are optional peers — install them alongside\n// `@forinda/kickjs` to use this subpath.\n//\n// Lifecycle note: Fastify registration (`register` / `use` / `route`) is\n// deferred until `ready()`. Connect middleware needs `@fastify/middie`\n// registered first, so `useConnect` only QUEUES middleware; `nodeHandler`\n// registers middie, then the queued middleware, then awaits `ready()`.\n\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport { createRequire } from 'node:module'\n\nimport { HttpException } from '../../core/errors'\nimport { unsupportedMediaTypeError } from '../body-policy'\nimport { RequestContext } from '../context'\nimport { publishMatchedRoute, stringifyJson } from '../runtime'\nimport { applyHandlerResult } from '../reply'\nimport { requestStore } from '../request-store'\nimport { createRequestStore, disposeRequestStore } from '../middleware/request-scope'\nimport { validate } from '../middleware/validate'\nimport { applyUploadConfig, type RawUploadPart } from '../middleware/upload'\nimport type {\n ConnectMiddleware,\n HttpRuntime,\n RouteEntry,\n RouteTable,\n RuntimeAppOptions,\n RuntimeResponse,\n UseConnectOptions,\n} from '../runtime'\n\n// ESM-safe require for the optional peers (fastify / @fastify/middie /\n// serve-static), resolved from wherever this module is installed.\nconst peerRequire = createRequire(import.meta.url)\nconst loadPeer = (name: string): any => {\n const mod = peerRequire(name)\n return mod && mod.__esModule && mod.default ? mod.default : (mod.default ?? mod)\n}\n\n// Minimal structural surface we use from Fastify — avoids a hard type dep so\n// the root package never needs `fastify` installed to typecheck.\ninterface FastifyReplyLike {\n code(statusCode: number): FastifyReplyLike\n send(payload?: unknown): FastifyReplyLike\n header(name: string, value: unknown): FastifyReplyLike\n type(contentType: string): FastifyReplyLike\n serializer(fn: (payload: unknown) => string): FastifyReplyLike\n getHeader(name: string): unknown\n hijack(): void\n readonly sent: boolean\n readonly raw: ServerResponse\n}\ninterface FastifyRequestLike {\n raw: IncomingMessage\n body?: unknown\n params?: unknown\n query?: unknown\n /** Request headers, as Fastify normalises them (lowercased keys). */\n headers?: Record<string, string | string[] | undefined>\n /** Present once @fastify/multipart is registered — async iterator of parts. */\n parts?: () => AsyncIterableIterator<MultipartPart>\n isMultipart?: () => boolean\n}\ntype FastifyHandler = (request: FastifyRequestLike, reply: FastifyReplyLike) => void | Promise<void>\ninterface FastifyAppLike {\n register(plugin: unknown, opts?: unknown): Promise<void> | FastifyAppLike\n use(path: unknown, mw?: unknown): unknown\n route(opts: { method: string; url: string; handler: FastifyHandler }): unknown\n setNotFoundHandler(handler: FastifyHandler): unknown\n setErrorHandler(\n handler: (err: unknown, req: FastifyRequestLike, reply: FastifyReplyLike) => void,\n ): unknown\n ready(): Promise<void>\n routing(req: IncomingMessage, res: ServerResponse): void\n /**\n * Fastify's content-type parser registry. Typed here rather than reached for\n * with a cast so the callback shape is checked at the call site — these\n * parsers decide what `ctx.body` is on this runtime.\n */\n addContentTypeParser?(\n matcher: string | RegExp | string[],\n opts: { parseAs: 'string' | 'buffer' },\n parser: FastifyBodyParser,\n ): unknown\n}\n\n/** The `done` callback a Fastify content-type parser calls. */\ntype FastifyParserDone = (err: Error | null, body?: unknown) => void\n\n/** `done`-style callback Fastify hands a content-type parser. */\ntype FastifyBodyParser = (\n req: FastifyRequestLike,\n body: string | Buffer,\n done: (err: Error | null, body?: unknown) => void,\n) => void\n\nconst QUEUE = Symbol('kickjs.fastify.connectQueue')\nconst READY = Symbol('kickjs.fastify.ready')\nconst NEXT = Symbol('kickjs.fastify.next')\n// Set during mountRoutes when any route carries @FileUpload metadata, so\n// nodeHandler knows to register @fastify/multipart before ready().\nconst NEEDS_MULTIPART = Symbol('kickjs.fastify.needsMultipart')\nconst NOOP_NEXT = (): void => {}\n\n// A @fastify/multipart part as we consume it from `request.parts()`.\ninterface MultipartPart {\n type: 'file' | 'field'\n fieldname: string\n filename?: string\n mimetype?: string\n value?: unknown\n toBuffer?: () => Promise<Buffer>\n}\n\n/**\n * Wrap a Fastify reply so the RequestContext response helpers drive it.\n * A class so the ~12 driver methods live on a shared prototype — the previous\n * object-literal version re-allocated every method closure on each request.\n */\nclass FastifyReplyDriver implements RuntimeResponse {\n constructor(private readonly reply: FastifyReplyLike) {}\n status(code: number): this {\n this.reply.code(code)\n return this\n }\n json(data: unknown): this {\n // A custom serializer drops Fastify's default content-type; keep one the\n // caller chose (`application/problem+json`).\n if (!this.reply.getHeader('content-type')) this.reply.type('application/json; charset=utf-8')\n this.reply.serializer(stringifyJson).send(data)\n return this\n }\n send(data: unknown): this {\n this.reply.send(data)\n return this\n }\n type(contentType: string): this {\n this.reply.type(contentType)\n return this\n }\n setHeader(name: string, value: unknown): this {\n this.reply.header(name, value)\n return this\n }\n render(): never {\n throw new Error('ctx.render() is not supported on the Fastify runtime (no view engine).')\n }\n writeHead(statusCode: number, headers?: Record<string, string | number | string[]>): this {\n // Streaming / SSE: take over the raw socket so Fastify doesn't try to\n // serialize a reply we're writing by hand.\n this.reply.hijack()\n this.reply.raw.writeHead(statusCode, headers)\n return this\n }\n flushHeaders(): this {\n ;(this.reply.raw as ServerResponse & { flushHeaders?: () => void }).flushHeaders?.()\n return this\n }\n write(chunk: unknown): boolean {\n return this.reply.raw.write(chunk as never)\n }\n end(data?: unknown): this {\n this.reply.raw.end(data as never)\n return this\n }\n once(event: string, listener: (...args: unknown[]) => void): this {\n this.reply.raw.once(event, listener)\n return this\n }\n get headersSent(): boolean {\n return this.reply.sent || this.reply.raw.headersSent\n }\n}\n\nfunction replyDriver(reply: FastifyReplyLike): RuntimeResponse {\n return new FastifyReplyDriver(reply) as unknown as RuntimeResponse\n}\n\n/** Build the Fastify per-route handler that runs the kickjs pipeline. */\nfunction routeHandler(entry: RouteEntry): FastifyHandler {\n // Validation middleware is built ONCE per route (parity with the Express\n // materializer) — `validate()` constructs a fresh closure, so calling it\n // per request was pure allocation waste.\n const validator = entry.meta.validation ? validate(entry.meta.validation) : undefined\n return async (request, reply) => {\n // Fastify runs the route handler OUTSIDE the connect-middleware chain, so\n // (unlike Express) the requestScopeMiddleware ALS frame isn't active here.\n // Establish it around the pipeline so REQUEST-scoped DI, contributors, and\n // ctx.set/get work. Reuse the inbound x-request-id, or the id a connect\n // middleware (requestId / requestScope, run earlier via middie) already\n // stamped on the raw request, before generating a fresh one.\n // Use the raw node request as `ctx.req`: it natively provides the stream\n // events (`on('close')` / `once`) that SSE and `ctx.signal` need — Fastify's\n // request object doesn't. Copy Fastify's parsed body/params/query onto it so\n // `ctx.body` / `ctx.params` / `ctx.query` keep working.\n const raw = request.raw as IncomingMessage & {\n requestId?: string\n body?: unknown\n params?: unknown\n query?: unknown\n file?: unknown\n files?: unknown\n }\n raw.body = request.body\n raw.params = request.params\n raw.query = request.query\n\n // @FileUpload: buffer multipart parts into the multer-shaped file(s) that\n // `ctx.file` / `ctx.files` expose. Fastify doesn't parse multipart natively\n // — @fastify/multipart (registered in nodeHandler when any route needs it)\n // provides `request.parts()`. Non-file fields are merged onto `raw.body` so\n // validation / `ctx.body` see them, matching multer's req.body behavior.\n if (entry.meta.upload && entry.meta.upload.mode !== 'none') {\n if (typeof request.parts !== 'function') {\n throw new Error(\n \"@forinda/kickjs: @FileUpload on the Fastify runtime requires '@fastify/multipart'.\\n\" +\n 'Install it as a peer dependency: pnpm add @fastify/multipart.',\n )\n }\n const rawParts: RawUploadPart[] = []\n const fields: Record<string, unknown> = {}\n for await (const part of request.parts()) {\n if (part.type === 'file' && part.toBuffer) {\n rawParts.push({\n fieldname: part.fieldname,\n filename: part.filename ?? '',\n mimetype: part.mimetype ?? 'application/octet-stream',\n buffer: await part.toBuffer(),\n })\n } else {\n fields[part.fieldname] = part.value\n }\n }\n const { file, files } = applyUploadConfig(rawParts, entry.meta.upload)\n raw.file = file\n raw.files = files\n raw.body = { ...(raw.body as Record<string, unknown> | undefined), ...fields }\n }\n\n const headerId = raw.headers['x-request-id']\n const requestId = (Array.isArray(headerId) ? headerId[0] : headerId) || raw.requestId\n const store = createRequestStore(requestId)\n // Propagate the resolved id back onto the raw request so anything reading\n // `req.requestId` inside the frame (e.g. request-logger) matches the store\n // and the X-Request-Id response header — parity with Express.\n raw.requestId = store.requestId\n\n // @PreDestroy teardown for REQUEST-scoped instances once the response\n // closes (finished or aborted) — parity with requestScopeMiddleware.\n reply.raw.once('close', () => disposeRequestStore(store))\n\n await requestStore.run(store, async () => {\n publishMatchedRoute(raw, entry)\n const ctx = new RequestContext(raw as never, reply as never, NOOP_NEXT, replyDriver(reply))\n\n // `beforeValidation` contributors (#677) — ahead of validation and middleware.\n if (entry.earlyContributorRunner) {\n await entry.earlyContributorRunner(ctx)\n if (reply.sent) return\n }\n\n // Validation (from @Get(path, schema) / route.validation). `validator` is a\n // connect-style middleware built once at route registration; it mutates\n // req.body/query/params to the parsed value and calls next(err) on failure\n // — it never touches `res`, so it runs cleanly here. A rejection\n // propagates to the error handler (422).\n if (validator) {\n await new Promise<void>((resolve, reject) => {\n validator(raw as never, undefined as never, (err?: unknown) =>\n err ? reject(err) : resolve(),\n )\n })\n }\n\n // Class + method middleware — `(ctx, next)`; await each, stop if it ended\n // the response or called next(err).\n for (const mw of entry.middlewares) {\n let advanced = false\n await new Promise<void>((resolve, reject) => {\n const next = (err?: unknown): void => {\n advanced = true\n if (err) reject(err)\n else resolve()\n }\n Promise.resolve(mw(ctx, next)).catch(reject)\n })\n if (!advanced || reply.sent) return\n }\n\n if (entry.contributorRunner) await entry.contributorRunner(ctx)\n if (reply.sent) return\n const result = await entry.handler(ctx)\n // Return-value handlers (reply.ts): auto-send when nothing was written.\n if (!reply.sent) applyHandlerResult(ctx, result)\n })\n }\n}\n\n/**\n * The Fastify HTTP runtime. Pass to `bootstrap({ runtime: fastifyRuntime() })`.\n * Per spec §10: Fastify's built-in pino logger is disabled (`logger: false`) so\n * the kickjs `requestLogger` middleware stays the single log format.\n */\nexport function fastifyRuntime(): HttpRuntime<FastifyAppLike> {\n return {\n name: 'fastify',\n\n createApp(options: RuntimeAppOptions = {}): FastifyAppLike {\n const Fastify = loadPeer('fastify')\n const app = Fastify({\n logger: false,\n // Match Express / h3 routing leniency: `/api/v1/hello` and\n // `/api/v1/hello/` resolve to the same route. Without this, Fastify's\n // strict router 404s a trailing-slash request — e.g. a controller's\n // root `@Get('/')` (mounted at the prefix) misses `${prefix}/`. Set under\n // `routerOptions` (Fastify 5+; top-level `ignoreTrailingSlash` is\n // deprecated, removed in Fastify 6).\n routerOptions: { ignoreTrailingSlash: true },\n // trustProxy passed through for now; core X-Forwarded-For\n // normalization (spec §10 Q1) is a follow-up.\n trustProxy: typeof options.trustProxy === 'function' ? false : options.trustProxy,\n }) as FastifyAppLike & {\n [QUEUE]?: Array<{ mw: ConnectMiddleware; opts?: UseConnectOptions }>\n }\n app[QUEUE] = []\n\n // Fastify ships parsers for `application/json` and `text/plain` only, and\n // answers 415 for everything else. That made a form post work on Express\n // and h3 but fail on Fastify alone (#590). Register the rest of the\n // framework's supported set so all engines agree.\n //\n // Deliberately NOT a `'*'` catch-all: that would swallow multipart, and\n // `@fastify/multipart` is itself the multipart content-type parser,\n // registered conditionally in `nodeHandler` when a route needs it.\n const addParser = app.addContentTypeParser?.bind(app)\n if (addParser) {\n // `application/*+json` — RFC 6838 §4.2.8 guarantees the bytes are JSON.\n addParser(\n /^application\\/[\\w.+-]+\\+json$/,\n { parseAs: 'string' },\n (_req: FastifyRequestLike, body: string | Buffer, done: FastifyParserDone) => {\n if (body === '') return done(null, undefined)\n try {\n done(null, JSON.parse(body as string))\n } catch (err) {\n // A bare SyntaxError carries no status, so the shared error handler\n // would call it a 500 — Fastify's own JSON parser answers 400\n // (FST_ERR_CTP_INVALID_JSON_BODY), and so must this one.\n done(\n HttpException.badRequest(\n err instanceof Error ? err.message : 'Request body could not be parsed',\n ),\n undefined,\n )\n }\n },\n )\n addParser(\n 'application/x-www-form-urlencoded',\n { parseAs: 'string' },\n (_req: FastifyRequestLike, body: string | Buffer, done: FastifyParserDone) => {\n done(null, Object.fromEntries(new URLSearchParams(body as string)))\n },\n )\n // `text/*` beyond Fastify's built-in `text/plain`. Raw string, never\n // JSON: `text/plain` is CORS-safelisted, so parsing it as JSON would\n // re-open no-preflight CSRF against a JSON API.\n addParser(\n /^text\\//,\n { parseAs: 'string' },\n (_req: FastifyRequestLike, body: string | Buffer, done: FastifyParserDone) => {\n done(null, body)\n },\n )\n // Fastify already answers 415 for a type it has no parser for, but with\n // its own message and no `Accept`. `'*'` is Fastify's documented\n // fallback — consulted only when nothing more specific matches — so it\n // does not shadow the parsers above, nor `@fastify/multipart`, which\n // registers `multipart/form-data` when an upload route needs it.\n addParser(\n '*',\n { parseAs: 'string' },\n (req: FastifyRequestLike, body: string | Buffer, done: FastifyParserDone) => {\n // No body is not an unsupported body. Fastify calls the parser even\n // for an empty payload, so without this a bodyless POST carrying an\n // unrelated content type would be rejected.\n if (!body) return done(null, undefined)\n done(unsupportedMediaTypeError(req.headers?.['content-type']), undefined)\n },\n )\n }\n\n return app\n },\n\n nodeHandler(app) {\n // Register middie + queued connect middleware + finalize `ready()` exactly\n // ONCE per app, even though Application may call nodeHandler per request.\n const state = app as {\n [QUEUE]?: Array<{ mw: ConnectMiddleware; opts?: UseConnectOptions }>\n [READY]?: Promise<void>\n [NEEDS_MULTIPART]?: boolean\n }\n if (!state[READY]) {\n state[READY] = (async () => {\n // Register @fastify/multipart first when any route uses @FileUpload so\n // `request.parts()` is available. Our applyUploadConfig enforces the\n // real per-file maxSize, so give the plugin a generous fileSize ceiling\n // (its 1MB default would silently truncate larger uploads first).\n if (state[NEEDS_MULTIPART]) {\n const multipart = loadPeer('@fastify/multipart')\n await app.register(multipart, { limits: { fileSize: Number.MAX_SAFE_INTEGER } })\n }\n const middie = loadPeer('@fastify/middie')\n await app.register(middie)\n for (const { mw, opts } of state[QUEUE] ?? []) {\n if (opts?.path !== undefined)\n (app.use as (p: unknown, m: unknown) => void)(opts.path, mw)\n else (app.use as (m: unknown) => void)(mw)\n }\n await app.ready()\n })()\n }\n const readyP = state[READY]\n\n return (req: IncomingMessage, res: ServerResponse, next?: (err?: unknown) => void) => {\n if (next) (req as IncomingMessage & { [NEXT]?: (err?: unknown) => void })[NEXT] = next\n readyP\n .then(() => app.routing(req, res))\n .catch((err) => {\n if (next) next(err)\n })\n }\n },\n\n mountRoutes(app, table: RouteTable) {\n for (const { mountPath, routes } of table) {\n for (const entry of routes) {\n if (entry.meta.upload) (app as { [NEEDS_MULTIPART]?: boolean })[NEEDS_MULTIPART] = true\n const url = joinPath(mountPath, entry.path)\n app.route({ method: entry.method, url, handler: routeHandler(entry) })\n }\n }\n },\n\n useConnect(app, mw: ConnectMiddleware, opts?: UseConnectOptions) {\n const queue = (\n app as { [QUEUE]?: Array<{ mw: ConnectMiddleware; opts?: UseConnectOptions }> }\n )[QUEUE]\n queue?.push({ mw, opts })\n },\n\n serveStatic(app, prefix, dir) {\n // Serve via `serve-static` (the standalone connect middleware behind\n // express.static) so the Fastify runtime carries no `express` dependency —\n // queued like any other connect middleware, bridged through middie.\n const serveStatic = loadPeer('serve-static')\n this.useConnect(app, serveStatic(dir) as ConnectMiddleware, { path: prefix })\n },\n\n setNotFound(app, mw: ConnectMiddleware) {\n app.setNotFoundHandler((request, reply) => {\n const next = (request.raw as IncomingMessage & { [NEXT]?: (err?: unknown) => void })[NEXT]\n if (next) {\n // Vite dev fall-through: hand the request back to the outer chain.\n reply.hijack()\n next()\n return\n }\n // Pass the reply DRIVER (not raw res) so the connect-style\n // notFoundHandler's `res.status().json()` lands on the Fastify reply.\n ;(mw as (req: unknown, res: unknown, next: () => void) => void)(\n request.raw,\n replyDriver(reply),\n NOOP_NEXT,\n )\n })\n },\n\n setErrorHandler(app, mw: ConnectMiddleware) {\n app.setErrorHandler((err, request, reply) => {\n // Reply driver (not raw res) so the connect-style errorHandler's\n // `res.status().json()` / `setHeader` land on the Fastify reply.\n ;(mw as (e: unknown, req: unknown, res: unknown, next: () => void) => void)(\n err,\n request.raw,\n replyDriver(reply),\n NOOP_NEXT,\n )\n })\n },\n\n capabilities: {\n render: false,\n uploads: true,\n connectMiddleware: true,\n nativeBodyParsing: true,\n http2: true,\n },\n }\n}\n\n/**\n * The Fastify runtime's engine-native types — what the runtime-typed escape\n * hatches resolve to once the `kick/runtime` typegen emits a `KickRuntimeRegister`\n * augmentation pointing here (spec §4.3b). `AdapterContext.app` / `getRuntimeApp()`\n * become `FastifyInstance`; `ctx.req` / `ctx.res` the Fastify request / reply.\n * Mirrors {@link import('../runtime').ExpressRuntimeTypes} for the default engine.\n */\nexport interface FastifyRuntimeTypes {\n request: import('fastify').FastifyRequest\n response: import('fastify').FastifyReply\n app: import('fastify').FastifyInstance\n}\n\n/** Join a mount prefix and a route path into one URL, collapsing slashes. */\nfunction joinPath(mountPath: string, path: string): string {\n const a = mountPath.endsWith('/') ? mountPath.slice(0, -1) : mountPath\n // A root route path ('/' or '') maps to the mount prefix itself — otherwise a\n // controller `@Post('/')` would register at `${prefix}/`, and a request to\n // `${prefix}` (no trailing slash) wouldn't match under Fastify's strict router.\n if (path === '/' || path === '') return a === '' ? '/' : a\n const b = path.startsWith('/') ? path : `/${path}`\n return `${a}${b}`\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"fastify.mjs","names":[],"sources":["../../../src/http/runtimes/fastify.ts"],"sourcesContent":["// The Fastify runtime — `@forinda/kickjs/fastify`.\n//\n// Implements the same HttpRuntime contract as expressRuntime, over Fastify 5.\n// KickJS still owns decorators → RouteTable, the contributor pipeline, and the\n// RequestContext surface; this runtime materializes routes as NATIVE Fastify\n// routes and wraps `reply` in a RuntimeResponse so `ctx.json` / `ctx.html` /\n// `ctx.sse` work unchanged.\n//\n// `fastify`, `@fastify/middie` are optional peers — install them alongside\n// `@forinda/kickjs` to use this subpath.\n//\n// Lifecycle note: Fastify registration (`register` / `use` / `route`) is\n// deferred until `ready()`. Connect middleware needs `@fastify/middie`\n// registered first, so `useConnect` only QUEUES middleware; `nodeHandler`\n// registers middie, then the queued middleware, then awaits `ready()`.\n\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport { createRequire } from 'node:module'\n\nimport { HttpException } from '../../core/errors'\nimport { unsupportedMediaTypeError } from '../body-policy'\nimport { RequestContext } from '../context'\nimport { publishMatchedRoute, stringifyJson } from '../runtime'\nimport { applyHandlerResult } from '../reply'\nimport { requestStore } from '../request-store'\nimport { createRequestStore, disposeRequestStore } from '../middleware/request-scope'\nimport { validate } from '../middleware/validate'\nimport { applyUploadConfig, type RawUploadPart } from '../middleware/upload'\nimport type {\n ConnectMiddleware,\n HttpRuntime,\n RouteEntry,\n RouteTable,\n RuntimeAppOptions,\n RuntimeResponse,\n UseConnectOptions,\n} from '../runtime'\n\n// ESM-safe require for the optional peers (fastify / @fastify/middie /\n// serve-static), resolved from wherever this module is installed.\nconst peerRequire = createRequire(import.meta.url)\nconst loadPeer = (name: string): any => {\n const mod = peerRequire(name)\n return mod && mod.__esModule && mod.default ? mod.default : (mod.default ?? mod)\n}\n\n// Minimal structural surface we use from Fastify — avoids a hard type dep so\n// the root package never needs `fastify` installed to typecheck.\ninterface FastifyReplyLike {\n code(statusCode: number): FastifyReplyLike\n send(payload?: unknown): FastifyReplyLike\n header(name: string, value: unknown): FastifyReplyLike\n type(contentType: string): FastifyReplyLike\n serializer(fn: (payload: unknown) => string): FastifyReplyLike\n getHeader(name: string): unknown\n hijack(): void\n readonly sent: boolean\n readonly raw: ServerResponse\n}\ninterface FastifyRequestLike {\n raw: IncomingMessage\n body?: unknown\n params?: unknown\n query?: unknown\n /** Request headers, as Fastify normalises them (lowercased keys). */\n headers?: Record<string, string | string[] | undefined>\n /** Present once @fastify/multipart is registered — async iterator of parts. */\n parts?: () => AsyncIterableIterator<MultipartPart>\n isMultipart?: () => boolean\n}\ntype FastifyHandler = (request: FastifyRequestLike, reply: FastifyReplyLike) => void | Promise<void>\ninterface FastifyAppLike {\n register(plugin: unknown, opts?: unknown): Promise<void> | FastifyAppLike\n use(path: unknown, mw?: unknown): unknown\n route(opts: { method: string; url: string; handler: FastifyHandler }): unknown\n setNotFoundHandler(handler: FastifyHandler): unknown\n setErrorHandler(\n handler: (err: unknown, req: FastifyRequestLike, reply: FastifyReplyLike) => void,\n ): unknown\n ready(): Promise<void>\n routing(req: IncomingMessage, res: ServerResponse): void\n /**\n * Fastify's content-type parser registry. Typed here rather than reached for\n * with a cast so the callback shape is checked at the call site — these\n * parsers decide what `ctx.body` is on this runtime.\n */\n addContentTypeParser?(\n matcher: string | RegExp | string[],\n opts: { parseAs: 'string' | 'buffer' },\n parser: FastifyBodyParser,\n ): unknown\n}\n\n/** The `done` callback a Fastify content-type parser calls. */\ntype FastifyParserDone = (err: Error | null, body?: unknown) => void\n\n/** `done`-style callback Fastify hands a content-type parser. */\ntype FastifyBodyParser = (\n req: FastifyRequestLike,\n body: string | Buffer,\n done: (err: Error | null, body?: unknown) => void,\n) => void\n\nconst QUEUE = Symbol('kickjs.fastify.connectQueue')\nconst READY = Symbol('kickjs.fastify.ready')\nconst NEXT = Symbol('kickjs.fastify.next')\n// Set during mountRoutes when any route carries @FileUpload metadata, so\n// nodeHandler knows to register @fastify/multipart before ready().\nconst NEEDS_MULTIPART = Symbol('kickjs.fastify.needsMultipart')\nconst NOOP_NEXT = (): void => {}\n\n/**\n * An async error / not-found handler that rejects never answers; answer 500\n * so the request ends instead of hanging until the client gives up.\n */\nfunction settleOrFail(reply: FastifyReplyLike, returned: unknown): void {\n if (typeof (returned as PromiseLike<void> | undefined)?.then !== 'function') return\n Promise.resolve(returned).then(undefined, () => {\n if (!reply.sent && !reply.raw.headersSent) reply.code(500).send()\n })\n}\n/** `notFoundFallthrough: false` — answer unmatched requests instead of handing them back. */\nconst NO_FALLTHROUGH = Symbol('kickjs.fastify.noFallthrough')\n\n// A @fastify/multipart part as we consume it from `request.parts()`.\ninterface MultipartPart {\n type: 'file' | 'field'\n fieldname: string\n filename?: string\n mimetype?: string\n value?: unknown\n toBuffer?: () => Promise<Buffer>\n}\n\n/**\n * Wrap a Fastify reply so the RequestContext response helpers drive it.\n * A class so the ~12 driver methods live on a shared prototype — the previous\n * object-literal version re-allocated every method closure on each request.\n */\nclass FastifyReplyDriver implements RuntimeResponse {\n constructor(private readonly reply: FastifyReplyLike) {}\n status(code: number): this {\n this.reply.code(code)\n return this\n }\n json(data: unknown): this {\n // A custom serializer drops Fastify's default content-type; keep one the\n // caller chose (`application/problem+json`).\n if (!this.reply.getHeader('content-type')) this.reply.type('application/json; charset=utf-8')\n this.reply.serializer(stringifyJson).send(data)\n return this\n }\n send(data: unknown): this {\n this.reply.send(data)\n return this\n }\n type(contentType: string): this {\n this.reply.type(contentType)\n return this\n }\n setHeader(name: string, value: unknown): this {\n this.reply.header(name, value)\n return this\n }\n render(): never {\n throw new Error('ctx.render() is not supported on the Fastify runtime (no view engine).')\n }\n writeHead(statusCode: number, headers?: Record<string, string | number | string[]>): this {\n // Streaming / SSE: take over the raw socket so Fastify doesn't try to\n // serialize a reply we're writing by hand.\n this.reply.hijack()\n this.reply.raw.writeHead(statusCode, headers)\n return this\n }\n flushHeaders(): this {\n ;(this.reply.raw as ServerResponse & { flushHeaders?: () => void }).flushHeaders?.()\n return this\n }\n write(chunk: unknown): boolean {\n return this.reply.raw.write(chunk as never)\n }\n end(data?: unknown): this {\n // After writeHead (streaming, SSE) the reply is hijacked and the raw\n // socket is ours to finish. Otherwise finish through the reply, so the\n // status and headers set on it are sent — `ctx.redirect()` writes its\n // Location with setHeader() and then calls end().\n if (this.reply.raw.headersSent) this.reply.raw.end(data as never)\n else this.reply.send(data ?? '')\n return this\n }\n once(event: string, listener: (...args: unknown[]) => void): this {\n this.reply.raw.once(event, listener)\n return this\n }\n get headersSent(): boolean {\n return this.reply.sent || this.reply.raw.headersSent\n }\n}\n\nfunction replyDriver(reply: FastifyReplyLike): RuntimeResponse {\n return new FastifyReplyDriver(reply) as unknown as RuntimeResponse\n}\n\n/** Build the Fastify per-route handler that runs the kickjs pipeline. */\nfunction routeHandler(entry: RouteEntry): FastifyHandler {\n // Validation middleware is built ONCE per route (parity with the Express\n // materializer) — `validate()` constructs a fresh closure, so calling it\n // per request was pure allocation waste.\n const validator = entry.meta.validation ? validate(entry.meta.validation) : undefined\n return async (request, reply) => {\n // Fastify runs the route handler OUTSIDE the connect-middleware chain, so\n // (unlike Express) the requestScopeMiddleware ALS frame isn't active here.\n // Establish it around the pipeline so REQUEST-scoped DI, contributors, and\n // ctx.set/get work. Reuse the inbound x-request-id, or the id a connect\n // middleware (requestId / requestScope, run earlier via middie) already\n // stamped on the raw request, before generating a fresh one.\n // Use the raw node request as `ctx.req`: it natively provides the stream\n // events (`on('close')` / `once`) that SSE and `ctx.signal` need — Fastify's\n // request object doesn't. Copy Fastify's parsed body/params/query onto it so\n // `ctx.body` / `ctx.params` / `ctx.query` keep working.\n const raw = request.raw as IncomingMessage & {\n requestId?: string\n body?: unknown\n params?: unknown\n query?: unknown\n file?: unknown\n files?: unknown\n }\n raw.body = request.body\n raw.params = request.params\n raw.query = request.query\n\n // @FileUpload: buffer multipart parts into the multer-shaped file(s) that\n // `ctx.file` / `ctx.files` expose. Fastify doesn't parse multipart natively\n // — @fastify/multipart (registered in nodeHandler when any route needs it)\n // provides `request.parts()`. Non-file fields are merged onto `raw.body` so\n // validation / `ctx.body` see them, matching multer's req.body behavior.\n if (entry.meta.upload && entry.meta.upload.mode !== 'none') {\n if (typeof request.parts !== 'function') {\n throw new Error(\n \"@forinda/kickjs: @FileUpload on the Fastify runtime requires '@fastify/multipart'.\\n\" +\n 'Install it as a peer dependency: pnpm add @fastify/multipart.',\n )\n }\n const rawParts: RawUploadPart[] = []\n const fields: Record<string, unknown> = {}\n for await (const part of request.parts()) {\n if (part.type === 'file' && part.toBuffer) {\n rawParts.push({\n fieldname: part.fieldname,\n filename: part.filename ?? '',\n mimetype: part.mimetype ?? 'application/octet-stream',\n buffer: await part.toBuffer(),\n })\n } else {\n fields[part.fieldname] = part.value\n }\n }\n const { file, files } = applyUploadConfig(rawParts, entry.meta.upload)\n raw.file = file\n raw.files = files\n raw.body = { ...(raw.body as Record<string, unknown> | undefined), ...fields }\n }\n\n const headerId = raw.headers['x-request-id']\n const requestId = (Array.isArray(headerId) ? headerId[0] : headerId) || raw.requestId\n const store = createRequestStore(requestId)\n // Propagate the resolved id back onto the raw request so anything reading\n // `req.requestId` inside the frame (e.g. request-logger) matches the store\n // and the X-Request-Id response header — parity with Express.\n raw.requestId = store.requestId\n\n // @PreDestroy teardown for REQUEST-scoped instances once the response\n // closes (finished or aborted) — parity with requestScopeMiddleware.\n reply.raw.once('close', () => disposeRequestStore(store))\n\n await requestStore.run(store, async () => {\n publishMatchedRoute(raw, entry)\n const ctx = new RequestContext(raw as never, reply as never, NOOP_NEXT, replyDriver(reply))\n\n // `beforeValidation` contributors (#677) — ahead of validation and middleware.\n if (entry.earlyContributorRunner) {\n await entry.earlyContributorRunner(ctx)\n if (reply.sent) return\n }\n\n // Validation (from @Get(path, schema) / route.validation). `validator` is a\n // connect-style middleware built once at route registration; it mutates\n // req.body/query/params to the parsed value and calls next(err) on failure\n // — it never touches `res`, so it runs cleanly here. A rejection\n // propagates to the error handler (422).\n if (validator) {\n await new Promise<void>((resolve, reject) => {\n validator(raw as never, undefined as never, (err?: unknown) =>\n err ? reject(err) : resolve(),\n )\n })\n }\n\n // Class + method middleware — `(ctx, next)`; await each, stop if it ended\n // the response or called next(err).\n for (const mw of entry.middlewares) {\n let advanced = false\n await new Promise<void>((resolve, reject) => {\n const next = (err?: unknown): void => {\n advanced = true\n if (err) reject(err)\n else resolve()\n }\n Promise.resolve(mw(ctx, next)).catch(reject)\n })\n if (!advanced || reply.sent) return\n }\n\n if (entry.contributorRunner) await entry.contributorRunner(ctx)\n if (reply.sent) return\n const result = await entry.handler(ctx)\n // Return-value handlers (reply.ts): auto-send when nothing was written.\n if (!reply.sent) await applyHandlerResult(ctx, result)\n })\n }\n}\n\n/**\n * The Fastify HTTP runtime. Pass to `bootstrap({ runtime: fastifyRuntime() })`.\n * Per spec §10: Fastify's built-in pino logger is disabled (`logger: false`) so\n * the kickjs `requestLogger` middleware stays the single log format.\n */\nexport function fastifyRuntime(): HttpRuntime<FastifyAppLike> {\n return {\n name: 'fastify',\n\n createApp(options: RuntimeAppOptions = {}): FastifyAppLike {\n const Fastify = loadPeer('fastify')\n const app = Fastify({\n logger: false,\n // Match Express / h3 routing leniency: `/api/v1/hello` and\n // `/api/v1/hello/` resolve to the same route. Without this, Fastify's\n // strict router 404s a trailing-slash request — e.g. a controller's\n // root `@Get('/')` (mounted at the prefix) misses `${prefix}/`. Set under\n // `routerOptions` (Fastify 5+; top-level `ignoreTrailingSlash` is\n // deprecated, removed in Fastify 6).\n routerOptions: { ignoreTrailingSlash: true },\n // trustProxy passed through for now; core X-Forwarded-For\n // normalization (spec §10 Q1) is a follow-up.\n trustProxy: typeof options.trustProxy === 'function' ? false : options.trustProxy,\n }) as FastifyAppLike & {\n [QUEUE]?: Array<{ mw: ConnectMiddleware; opts?: UseConnectOptions }>\n }\n app[QUEUE] = []\n\n // Fastify ships parsers for `application/json` and `text/plain` only, and\n // answers 415 for everything else. That made a form post work on Express\n // and h3 but fail on Fastify alone (#590). Register the rest of the\n // framework's supported set so all engines agree.\n //\n // Deliberately NOT a `'*'` catch-all: that would swallow multipart, and\n // `@fastify/multipart` is itself the multipart content-type parser,\n // registered conditionally in `nodeHandler` when a route needs it.\n const addParser = app.addContentTypeParser?.bind(app)\n if (addParser) {\n // `application/*+json` — RFC 6838 §4.2.8 guarantees the bytes are JSON.\n addParser(\n /^application\\/[\\w.+-]+\\+json$/,\n { parseAs: 'string' },\n (_req: FastifyRequestLike, body: string | Buffer, done: FastifyParserDone) => {\n if (body === '') return done(null, undefined)\n try {\n done(null, JSON.parse(body as string))\n } catch (err) {\n // A bare SyntaxError carries no status, so the shared error handler\n // would call it a 500 — Fastify's own JSON parser answers 400\n // (FST_ERR_CTP_INVALID_JSON_BODY), and so must this one.\n done(\n HttpException.badRequest(\n err instanceof Error ? err.message : 'Request body could not be parsed',\n ),\n undefined,\n )\n }\n },\n )\n addParser(\n 'application/x-www-form-urlencoded',\n { parseAs: 'string' },\n (_req: FastifyRequestLike, body: string | Buffer, done: FastifyParserDone) => {\n done(null, Object.fromEntries(new URLSearchParams(body as string)))\n },\n )\n // `text/*` beyond Fastify's built-in `text/plain`. Raw string, never\n // JSON: `text/plain` is CORS-safelisted, so parsing it as JSON would\n // re-open no-preflight CSRF against a JSON API.\n addParser(\n /^text\\//,\n { parseAs: 'string' },\n (_req: FastifyRequestLike, body: string | Buffer, done: FastifyParserDone) => {\n done(null, body)\n },\n )\n // Fastify already answers 415 for a type it has no parser for, but with\n // its own message and no `Accept`. `'*'` is Fastify's documented\n // fallback — consulted only when nothing more specific matches — so it\n // does not shadow the parsers above, nor `@fastify/multipart`, which\n // registers `multipart/form-data` when an upload route needs it.\n addParser(\n '*',\n { parseAs: 'string' },\n (req: FastifyRequestLike, body: string | Buffer, done: FastifyParserDone) => {\n // No body is not an unsupported body. Fastify calls the parser even\n // for an empty payload, so without this a bodyless POST carrying an\n // unrelated content type would be rejected.\n if (!body) return done(null, undefined)\n done(unsupportedMediaTypeError(req.headers?.['content-type']), undefined)\n },\n )\n }\n\n if (options.notFoundFallthrough === false) {\n ;(app as { [NO_FALLTHROUGH]?: boolean })[NO_FALLTHROUGH] = true\n }\n return app\n },\n\n nodeHandler(app) {\n // Register middie + queued connect middleware + finalize `ready()` exactly\n // ONCE per app, even though Application may call nodeHandler per request.\n const state = app as {\n [QUEUE]?: Array<{ mw: ConnectMiddleware; opts?: UseConnectOptions }>\n [READY]?: Promise<void>\n [NEEDS_MULTIPART]?: boolean\n }\n if (!state[READY]) {\n state[READY] = (async () => {\n // Register @fastify/multipart first when any route uses @FileUpload so\n // `request.parts()` is available. Our applyUploadConfig enforces the\n // real per-file maxSize, so give the plugin a generous fileSize ceiling\n // (its 1MB default would silently truncate larger uploads first).\n if (state[NEEDS_MULTIPART]) {\n const multipart = loadPeer('@fastify/multipart')\n await app.register(multipart, { limits: { fileSize: Number.MAX_SAFE_INTEGER } })\n }\n const middie = loadPeer('@fastify/middie')\n await app.register(middie)\n for (const { mw, opts } of state[QUEUE] ?? []) {\n if (opts?.path !== undefined)\n (app.use as (p: unknown, m: unknown) => void)(opts.path, mw)\n else (app.use as (m: unknown) => void)(mw)\n }\n await app.ready()\n })()\n }\n const readyP = state[READY]\n\n return (req: IncomingMessage, res: ServerResponse, next?: (err?: unknown) => void) => {\n if (next) (req as IncomingMessage & { [NEXT]?: (err?: unknown) => void })[NEXT] = next\n readyP\n .then(() => app.routing(req, res))\n .catch((err) => {\n if (next) next(err)\n })\n }\n },\n\n mountRoutes(app, table: RouteTable) {\n for (const { mountPath, routes } of table) {\n for (const entry of routes) {\n if (entry.meta.upload) (app as { [NEEDS_MULTIPART]?: boolean })[NEEDS_MULTIPART] = true\n const url = joinPath(mountPath, entry.path)\n app.route({ method: entry.method, url, handler: routeHandler(entry) })\n }\n }\n },\n\n useConnect(app, mw: ConnectMiddleware, opts?: UseConnectOptions) {\n const queue = (\n app as { [QUEUE]?: Array<{ mw: ConnectMiddleware; opts?: UseConnectOptions }> }\n )[QUEUE]\n queue?.push({ mw, opts })\n },\n\n serveStatic(app, prefix, dir) {\n // Serve via `serve-static` (the standalone connect middleware behind\n // express.static) so the Fastify runtime carries no `express` dependency —\n // queued like any other connect middleware, bridged through middie.\n const serveStatic = loadPeer('serve-static')\n this.useConnect(app, serveStatic(dir) as ConnectMiddleware, { path: prefix })\n },\n\n setNotFound(app, mw: ConnectMiddleware) {\n app.setNotFoundHandler((request, reply) => {\n const next = (request.raw as IncomingMessage & { [NEXT]?: (err?: unknown) => void })[NEXT]\n if (next && !(app as { [NO_FALLTHROUGH]?: boolean })[NO_FALLTHROUGH]) {\n // Vite dev fall-through: hand the request back to the outer chain.\n reply.hijack()\n next()\n return\n }\n // Pass the reply DRIVER (not raw res) so the connect-style\n // notFoundHandler's `res.status().json()` lands on the Fastify reply.\n settleOrFail(\n reply,\n (mw as (req: unknown, res: unknown, next: () => void) => unknown)(\n request.raw,\n replyDriver(reply),\n NOOP_NEXT,\n ),\n )\n })\n },\n\n setErrorHandler(app, mw: ConnectMiddleware) {\n app.setErrorHandler((err, request, reply) => {\n // Reply driver (not raw res) so the connect-style errorHandler's\n // `res.status().json()` / `setHeader` land on the Fastify reply.\n settleOrFail(\n reply,\n (mw as (e: unknown, req: unknown, res: unknown, next: () => void) => unknown)(\n err,\n request.raw,\n replyDriver(reply),\n NOOP_NEXT,\n ),\n )\n })\n },\n\n capabilities: {\n render: false,\n uploads: true,\n connectMiddleware: true,\n nativeBodyParsing: true,\n http2: true,\n },\n }\n}\n\n/**\n * The Fastify runtime's engine-native types — what the runtime-typed escape\n * hatches resolve to once the `kick/runtime` typegen emits a `KickRuntimeRegister`\n * augmentation pointing here (spec §4.3b). `AdapterContext.app` / `getRuntimeApp()`\n * become `FastifyInstance`; `ctx.req` / `ctx.res` the Fastify request / reply.\n * Mirrors {@link import('../runtime').ExpressRuntimeTypes} for the default engine.\n */\nexport interface FastifyRuntimeTypes {\n request: import('fastify').FastifyRequest\n response: import('fastify').FastifyReply\n app: import('fastify').FastifyInstance\n}\n\n/** Join a mount prefix and a route path into one URL, collapsing slashes. */\nfunction joinPath(mountPath: string, path: string): string {\n const a = mountPath.endsWith('/') ? mountPath.slice(0, -1) : mountPath\n // A root route path ('/' or '') maps to the mount prefix itself — otherwise a\n // controller `@Post('/')` would register at `${prefix}/`, and a request to\n // `${prefix}` (no trailing slash) wouldn't match under Fastify's strict router.\n if (path === '/' || path === '') return a === '' ? '/' : a\n const b = path.startsWith('/') ? path : `/${path}`\n return `${a}${b}`\n}\n"],"mappings":"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"}
|
|
@@ -1,50 +1,3 @@
|
|
|
1
1
|
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
//#region src/http/runtimes/h3-web.d.ts
|
|
5
|
-
interface H3AppLike {
|
|
6
|
-
on(method: string, path: string, handler: (event: H3EventLike) => unknown): unknown;
|
|
7
|
-
all(path: string, handler: (event: H3EventLike) => unknown): unknown;
|
|
8
|
-
use(...args: unknown[]): unknown;
|
|
9
|
-
fetch(request: Request): Promise<Response>;
|
|
10
|
-
}
|
|
11
|
-
interface H3EventLike {
|
|
12
|
-
req: Request & {
|
|
13
|
-
runtime?: {
|
|
14
|
-
node?: {
|
|
15
|
-
req: IncomingMessage;
|
|
16
|
-
res: ServerResponse;
|
|
17
|
-
};
|
|
18
|
-
};
|
|
19
|
-
};
|
|
20
|
-
url: URL;
|
|
21
|
-
context: {
|
|
22
|
-
params?: Record<string, string>;
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
export interface H3WebRuntimeOptions {
|
|
26
|
-
/**
|
|
27
|
-
* Pre-imported h3 v2 module. Two audiences:
|
|
28
|
-
* - Edge/worker bundlers with no `createRequire` at runtime:
|
|
29
|
-
* `h3WebRuntime({ h3: await import('h3') })`
|
|
30
|
-
* - Tests running both h3 majors side by side via an npm alias.
|
|
31
|
-
* Omitted → the peer is loaded via `createRequire` (node bootstrap path).
|
|
32
|
-
*/
|
|
33
|
-
h3?: unknown;
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* The h3 v2 HTTP runtime. Pass to `bootstrap({ runtime: h3WebRuntime() })`.
|
|
37
|
-
* Requires the h3 v2 peer; fails fast with guidance when v1 is installed.
|
|
38
|
-
*/
|
|
39
|
-
export declare function h3WebRuntime(options?: H3WebRuntimeOptions): HttpRuntime<H3AppLike>;
|
|
40
|
-
/**
|
|
41
|
-
* h3-web runtime engine types for the `kick/runtime` typegen augmentation —
|
|
42
|
-
* mirrors {@link import('./h3').H3RuntimeTypes} for the v1 runtime.
|
|
43
|
-
*/
|
|
44
|
-
export interface H3WebRuntimeTypes {
|
|
45
|
-
request: Request;
|
|
46
|
-
response: Response;
|
|
47
|
-
app: unknown;
|
|
48
|
-
}
|
|
49
|
-
//#endregion
|
|
50
|
-
//# sourceMappingURL=h3-web.d.mts.map
|
|
2
|
+
import { H3WebRuntimeOptions, H3WebRuntimeTypes, h3WebRuntime } from "./h3.mjs";
|
|
3
|
+
export { type H3WebRuntimeOptions, type H3WebRuntimeTypes, h3WebRuntime };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs
|
|
2
|
+
* @forinda/kickjs v10.1.0
|
|
3
3
|
*
|
|
4
4
|
* Copyright (c) Felix Orinda
|
|
5
5
|
*
|
|
@@ -8,5 +8,4 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @license MIT
|
|
10
10
|
*/
|
|
11
|
-
|
|
12
|
-
//# sourceMappingURL=h3-web.mjs.map
|
|
11
|
+
export{h3WebRuntime}from"./h3.mjs";
|