lambda-pipe 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -0
- package/build/cjs/cli/index.js +1 -1
- package/build/cjs/server/index.js +1 -0
- package/build/cli/dev.d.ts +5 -3
- package/build/cli/routes.manifest.d.ts +1 -0
- package/build/cli/server.d.ts +13 -0
- package/build/cli/shared.d.ts +49 -0
- package/build/esm/cli/index.js +1 -1
- package/build/esm/server/index.js +1 -0
- package/docs/prod-server.md +397 -0
- package/package.json +7 -1
package/README.md
CHANGED
|
@@ -717,6 +717,56 @@ See: [DOC](https://github.com/delpikye-v/lambda-pipe/blob/main/docs/websocket.md
|
|
|
717
717
|
|
|
718
718
|
---
|
|
719
719
|
|
|
720
|
+
# Server Runtime
|
|
721
|
+
|
|
722
|
+
Production-ready Fastify runtime for LambdaPipe.
|
|
723
|
+
|
|
724
|
+
Designed for:
|
|
725
|
+
|
|
726
|
+
* bundled deployments
|
|
727
|
+
* lazy-loaded route manifests
|
|
728
|
+
* serverless adapters
|
|
729
|
+
* container deployments
|
|
730
|
+
* runtime middleware pipelines
|
|
731
|
+
* runtime plugin lifecycle hooks
|
|
732
|
+
|
|
733
|
+
Unlike the dev runtime, production mode does not scan the filesystem dynamically.
|
|
734
|
+
|
|
735
|
+
Routes are registered through a static manifest for deterministic builds and deployment-safe startup.
|
|
736
|
+
|
|
737
|
+
```ts
|
|
738
|
+
import { bootstrap } from 'lambda-pipe/server'
|
|
739
|
+
|
|
740
|
+
import routes from './routes.manifest'
|
|
741
|
+
|
|
742
|
+
await bootstrap({
|
|
743
|
+
port: 3000,
|
|
744
|
+
|
|
745
|
+
routes,
|
|
746
|
+
})
|
|
747
|
+
|
|
748
|
+
```
|
|
749
|
+
|
|
750
|
+
```ts
|
|
751
|
+
// routes.manifest.ts
|
|
752
|
+
|
|
753
|
+
import type {
|
|
754
|
+
RouteLoader,
|
|
755
|
+
} from 'lambda-pipe/server'
|
|
756
|
+
|
|
757
|
+
const routes: RouteLoader[] = [
|
|
758
|
+
() => import('./routes/health'),
|
|
759
|
+
|
|
760
|
+
() => import('./routes/user'),
|
|
761
|
+
]
|
|
762
|
+
|
|
763
|
+
export default routes
|
|
764
|
+
```
|
|
765
|
+
|
|
766
|
+
See: [DOC](https://github.com/delpikye-v/lambda-pipe/blob/main/docs/websocket.md)
|
|
767
|
+
|
|
768
|
+
---
|
|
769
|
+
|
|
720
770
|
# Philosophy
|
|
721
771
|
|
|
722
772
|
```text
|
package/build/cjs/cli/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var e=require("node:
|
|
1
|
+
"use strict";var e=require("node:fs"),t=require("node:path"),o=require("node:url"),r=require("@fastify/cookie"),n=require("chokidar"),s=require("fastify"),a=require("node:crypto");function i(e,t){return{headers:e.headers,body:e.body,queryStringParameters:e.query,pathParameters:e.params,cookies:e.cookies||{},rawPath:e.url,method:e.method,requestId:a.randomUUID(),request:e,reply:t}}async function c(e=[],t){for(const o of e)await o(t)}async function u(e,t){const o=t.statusCode||200;if(e.code(o),t.headers)for(const[o,r]of Object.entries(t.headers))e.header(o,r);return t.body}async function d(e,t,o=[]){for(const r of o)r.onError&&await r.onError(e,t);return t.reply.code(e.statusCode||500),{error:e.message||"Internal Server Error",stack:"development"===process.env.NODE_ENV?e.stack:void 0}}async function f(e=[],t){for(const o of e)o.onRequest&&await o.onRequest(t)}async function h(e=[],t,o){for(const r of e)r.onResponse&&await r.onResponse(t,o)}const p=[];function y(e){return e.endsWith(".ts")||e.endsWith(".js")||e.endsWith(".mjs")||e.endsWith(".cjs")}async function l(r,n,s){const a=e.readdirSync(n,{withFileTypes:!0});for(const e of a){const a=t.join(n,e.name);if(e.isDirectory()){await l(r,a,s);continue}if(!y(e.name))continue;const w=(await import(o.pathToFileURL(a).href+"?v="+Date.now())).default;w?.__isRoute&&(p.push({method:w.method.toUpperCase(),path:w.path,file:a}),r.route({method:w.method.toUpperCase(),url:w.path,handler:async(e,t)=>{const o=i(e,t);try{await f(s.plugins,o),await c(s.globalMiddlewares,o),await c(w.middlewares,o);const e=await w.handler(o,r);return await h(s.plugins,o,e),u(t,e)}catch(e){return d(e,o,s.plugins)}}}))}}async function w(o={}){const a=s({logger:o.logger??!0});await a.register(r),await async function(e=[],t){for(const o of e)o.onInit&&await o.onInit(t)}(o.plugins,a);const i=(c=o.routesDir,t.resolve(process.cwd(),c||process.env.ROUTES_DIR||"src/routes"));var c;if(!e.existsSync(i))throw Error("Routes directory not found: "+i);return function(e){e.get("/__routes",async()=>({routes:p})),e.get("/__health",async()=>({ok:!0,ts:Date.now()}))}(a),await l(a,i,o),!1!==o.hmr&&function(e){n.watch(e,{ignoreInitial:!0}).on("change",e=>{process.exit(0)})}(i),a}exports.bootstrap=async function(e={}){const t=await w(e),o=function(e){return Number(e||process.env.PORT||3e3)}(e.port),r=function(e){return e||process.env.HOST||"0.0.0.0"}(e.host);return await t.listen({port:o,host:r}),t},exports.createDevApp=w;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var o=require("@fastify/cookie"),t=require("fastify"),e=require("node:crypto");const r=new WeakMap;function n(o,t){return{headers:o.headers,body:o.body,queryStringParameters:o.query,pathParameters:o.params,cookies:o.cookies||{},rawPath:o.url,method:o.method,requestId:e.randomUUID(),request:o,reply:t}}async function a(o=[],t){for(const e of o)await e(t)}async function s(o,t){const e=t.statusCode||200;if(o.code(e),t.headers)for(const[e,r]of Object.entries(t.headers))o.header(e,r);return t.body}async function i(o,t,e=[]){for(const r of e)r.onError&&await r.onError(o,t);return t.reply.code(o.statusCode||500),{error:o.message||"Internal Server Error",stack:"development"===process.env.NODE_ENV?o.stack:void 0}}async function c(o){const t=r.get(o);if(t)return t;const e=(await o()).default;if(!e?.__isRoute)throw Error("Invalid route module");return r.set(o,e),e}async function u(o=[],t){for(const e of o)e.onRequest&&await e.onRequest(t)}async function f(o=[],t,e){for(const r of o)r.onResponse&&await r.onResponse(t,e)}async function d(e){const r=t({logger:e.logger??!0});await r.register(o),await async function(o=[],t){for(const e of o)e.onInit&&await e.onInit(t)}(e.plugins,r);for(const o of e.routes){const t=await c(o);r.log.info(`⚡ ${t.method} ${t.path}`),r.route({method:t.method,url:t.path,handler:async(o,c)=>{const d=n(o,c);try{await u(e.plugins,d),await a(e.globalMiddlewares,d),await a(t.middlewares,d);const o=await t.handler(d,r);return await f(e.plugins,d,o),s(c,o)}catch(o){return i(o,d,e.plugins)}}})}return r}exports.bootstrap=async function(o){const t=await d(o),e=Number(o.port||process.env.PORT||3e3),r=o.host||process.env.HOST||"0.0.0.0";return await t.listen({port:e,host:r}),t.log.info(""),t.log.info("🚀 LambdaPipe Runtime"),t.log.info(`🌐 http://${r}:${e}`),t.log.info("📦 routes: "+o.routes.length),t.log.info(""),t},exports.createRuntimeApp=d;
|
package/build/cli/dev.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { RuntimeMiddleware, RuntimePlugin } from './shared';
|
|
2
2
|
export type CreateDevAppOptions = {
|
|
3
3
|
port?: number;
|
|
4
4
|
host?: string;
|
|
5
5
|
routesDir?: string;
|
|
6
6
|
logger?: boolean;
|
|
7
7
|
hmr?: boolean;
|
|
8
|
+
plugins?: RuntimePlugin[];
|
|
9
|
+
globalMiddlewares?: RuntimeMiddleware[];
|
|
8
10
|
};
|
|
9
|
-
export declare function createDevApp(options?: CreateDevAppOptions): Promise<FastifyInstance<import("node:http").Server<typeof import("node:http").IncomingMessage, typeof import("node:http").ServerResponse>, import("node:http").IncomingMessage, import("node:http").ServerResponse<import("node:http").IncomingMessage>, import("fastify").FastifyBaseLogger, import("fastify").FastifyTypeProviderDefault>>;
|
|
10
|
-
export declare function bootstrap(options?: CreateDevAppOptions): Promise<FastifyInstance<import("node:http").Server<typeof import("node:http").IncomingMessage, typeof import("node:http").ServerResponse>, import("node:http").IncomingMessage, import("node:http").ServerResponse<import("node:http").IncomingMessage>, import("fastify").FastifyBaseLogger, import("fastify").FastifyTypeProviderDefault>>;
|
|
11
|
+
export declare function createDevApp(options?: CreateDevAppOptions): Promise<import("fastify").FastifyInstance<import("node:http").Server<typeof import("node:http").IncomingMessage, typeof import("node:http").ServerResponse>, import("node:http").IncomingMessage, import("node:http").ServerResponse<import("node:http").IncomingMessage>, import("fastify").FastifyBaseLogger, import("fastify").FastifyTypeProviderDefault>>;
|
|
12
|
+
export declare function bootstrap(options?: CreateDevAppOptions): Promise<import("fastify").FastifyInstance<import("node:http").Server<typeof import("node:http").IncomingMessage, typeof import("node:http").ServerResponse>, import("node:http").IncomingMessage, import("node:http").ServerResponse<import("node:http").IncomingMessage>, import("fastify").FastifyBaseLogger, import("fastify").FastifyTypeProviderDefault>>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const routes: (() => Promise<typeof import("../../src/routes/health")>)[];
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { RouteLoader, RuntimeMiddleware, RuntimePlugin } from './shared';
|
|
2
|
+
export type CreateRuntimeAppOptions = {
|
|
3
|
+
routes: RouteLoader[];
|
|
4
|
+
logger?: boolean;
|
|
5
|
+
plugins?: RuntimePlugin[];
|
|
6
|
+
globalMiddlewares?: RuntimeMiddleware[];
|
|
7
|
+
};
|
|
8
|
+
export type BootstrapOptions = CreateRuntimeAppOptions & {
|
|
9
|
+
port?: number;
|
|
10
|
+
host?: string;
|
|
11
|
+
};
|
|
12
|
+
export declare function createRuntimeApp(options: CreateRuntimeAppOptions): Promise<import("fastify").FastifyInstance<import("node:http").Server<typeof import("node:http").IncomingMessage, typeof import("node:http").ServerResponse>, import("node:http").IncomingMessage, import("node:http").ServerResponse<import("node:http").IncomingMessage>, import("fastify").FastifyBaseLogger, import("fastify").FastifyTypeProviderDefault>>;
|
|
13
|
+
export declare function bootstrap(options: BootstrapOptions): Promise<import("fastify").FastifyInstance<import("node:http").Server<typeof import("node:http").IncomingMessage, typeof import("node:http").ServerResponse>, import("node:http").IncomingMessage, import("node:http").ServerResponse<import("node:http").IncomingMessage>, import("fastify").FastifyBaseLogger, import("fastify").FastifyTypeProviderDefault>>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { FastifyReply, FastifyRequest } from 'fastify';
|
|
2
|
+
export type RuntimeContext = {
|
|
3
|
+
headers: Record<string, any>;
|
|
4
|
+
body: unknown;
|
|
5
|
+
queryStringParameters: Record<string, any>;
|
|
6
|
+
pathParameters: Record<string, any>;
|
|
7
|
+
cookies: Record<string, any>;
|
|
8
|
+
rawPath: string;
|
|
9
|
+
method: string;
|
|
10
|
+
requestId: string;
|
|
11
|
+
request: FastifyRequest;
|
|
12
|
+
reply: FastifyReply;
|
|
13
|
+
};
|
|
14
|
+
export type RuntimeResponse = {
|
|
15
|
+
statusCode?: number;
|
|
16
|
+
headers?: Record<string, any>;
|
|
17
|
+
body?: any;
|
|
18
|
+
};
|
|
19
|
+
export type RuntimeMiddleware = (ctx: RuntimeContext) => Promise<void> | void;
|
|
20
|
+
export type RuntimePlugin = {
|
|
21
|
+
name?: string;
|
|
22
|
+
onInit?: (app: any) => Promise<void> | void;
|
|
23
|
+
onRequest?: (ctx: RuntimeContext) => Promise<void> | void;
|
|
24
|
+
onResponse?: (ctx: RuntimeContext, response: RuntimeResponse) => Promise<void> | void;
|
|
25
|
+
onError?: (error: unknown, ctx: RuntimeContext) => Promise<void> | void;
|
|
26
|
+
};
|
|
27
|
+
export type RouteDefinition = {
|
|
28
|
+
__isRoute?: boolean;
|
|
29
|
+
path: string;
|
|
30
|
+
method: string;
|
|
31
|
+
middlewares?: RuntimeMiddleware[];
|
|
32
|
+
handler: (ctx: RuntimeContext, app: any) => Promise<RuntimeResponse>;
|
|
33
|
+
};
|
|
34
|
+
export type RouteModule = {
|
|
35
|
+
default: RouteDefinition;
|
|
36
|
+
};
|
|
37
|
+
export type RouteLoader = () => Promise<RouteModule>;
|
|
38
|
+
export declare const moduleCache: WeakMap<RouteLoader, RouteDefinition>;
|
|
39
|
+
export declare function createRuntimeContext(req: FastifyRequest, reply: FastifyReply): RuntimeContext;
|
|
40
|
+
export declare function runMiddlewares(middlewares: RuntimeMiddleware[] | undefined, ctx: RuntimeContext): Promise<void>;
|
|
41
|
+
export declare function serializeResponse(reply: FastifyReply, response: RuntimeResponse): Promise<any>;
|
|
42
|
+
export declare function handleRuntimeError(error: any, ctx: RuntimeContext, plugins?: RuntimePlugin[]): Promise<{
|
|
43
|
+
error: any;
|
|
44
|
+
stack: any;
|
|
45
|
+
}>;
|
|
46
|
+
export declare function loadRouteModule(load: RouteLoader): Promise<RouteDefinition>;
|
|
47
|
+
export declare function runPluginOnInit(plugins: RuntimePlugin[] | undefined, app: any): Promise<void>;
|
|
48
|
+
export declare function runPluginOnRequest(plugins: RuntimePlugin[] | undefined, ctx: RuntimeContext): Promise<void>;
|
|
49
|
+
export declare function runPluginOnResponse(plugins: RuntimePlugin[] | undefined, ctx: RuntimeContext, response: RuntimeResponse): Promise<void>;
|
package/build/esm/cli/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"node:
|
|
1
|
+
import t from"node:fs";import o from"node:path";import{pathToFileURL as e}from"node:url";import r from"@fastify/cookie";import n from"chokidar";import s from"fastify";import a from"node:crypto";function i(t,o){return{headers:t.headers,body:t.body,queryStringParameters:t.query,pathParameters:t.params,cookies:t.cookies||{},rawPath:t.url,method:t.method,requestId:a.randomUUID(),request:t,reply:o}}async function c(t=[],o){for(const e of t)await e(o)}async function u(t,o){const e=o.statusCode||200;if(t.code(e),o.headers)for(const[e,r]of Object.entries(o.headers))t.header(e,r);return o.body}async function f(t,o,e=[]){for(const r of e)r.onError&&await r.onError(t,o);return o.reply.code(t.statusCode||500),{error:t.message||"Internal Server Error",stack:"development"===process.env.NODE_ENV?t.stack:void 0}}async function d(t=[],o){for(const e of t)e.onRequest&&await e.onRequest(o)}async function p(t=[],o,e){for(const r of t)r.onResponse&&await r.onResponse(o,e)}const h=[];function m(t){return t.endsWith(".ts")||t.endsWith(".js")||t.endsWith(".mjs")||t.endsWith(".cjs")}async function y(r,n,s){const a=t.readdirSync(n,{withFileTypes:!0});for(const t of a){const a=o.join(n,t.name);if(t.isDirectory()){await y(r,a,s);continue}if(!m(t.name))continue;const l=(await import(e(a).href+"?v="+Date.now())).default;l?.__isRoute&&(h.push({method:l.method.toUpperCase(),path:l.path,file:a}),r.route({method:l.method.toUpperCase(),url:l.path,handler:async(t,o)=>{const e=i(t,o);try{await d(s.plugins,e),await c(s.globalMiddlewares,e),await c(l.middlewares,e);const t=await l.handler(e,r);return await p(s.plugins,e,t),u(o,t)}catch(t){return f(t,e,s.plugins)}}}))}}async function l(e={}){const a=s({logger:e.logger??!0});await a.register(r),await async function(t=[],o){for(const e of t)e.onInit&&await e.onInit(o)}(e.plugins,a);const i=(c=e.routesDir,o.resolve(process.cwd(),c||process.env.ROUTES_DIR||"src/routes"));var c;if(!t.existsSync(i))throw Error("Routes directory not found: "+i);return function(t){t.get("/__routes",async()=>({routes:h})),t.get("/__health",async()=>({ok:!0,ts:Date.now()}))}(a),await y(a,i,e),!1!==e.hmr&&function(t){n.watch(t,{ignoreInitial:!0}).on("change",t=>{process.exit(0)})}(i),a}async function w(t={}){const o=await l(t),e=function(t){return Number(t||process.env.PORT||3e3)}(t.port),r=function(t){return t||process.env.HOST||"0.0.0.0"}(t.host);return await o.listen({port:e,host:r}),o}export{w as bootstrap,l as createDevApp};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import o from"@fastify/cookie";import t from"fastify";import e from"node:crypto";const n=new WeakMap;function r(o,t){return{headers:o.headers,body:o.body,queryStringParameters:o.query,pathParameters:o.params,cookies:o.cookies||{},rawPath:o.url,method:o.method,requestId:e.randomUUID(),request:o,reply:t}}async function a(o=[],t){for(const e of o)await e(t)}async function s(o,t){const e=t.statusCode||200;if(o.code(e),t.headers)for(const[e,n]of Object.entries(t.headers))o.header(e,n);return t.body}async function i(o,t,e=[]){for(const n of e)n.onError&&await n.onError(o,t);return t.reply.code(o.statusCode||500),{error:o.message||"Internal Server Error",stack:"development"===process.env.NODE_ENV?o.stack:void 0}}async function c(o){const t=n.get(o);if(t)return t;const e=(await o()).default;if(!e?.__isRoute)throw Error("Invalid route module");return n.set(o,e),e}async function u(o=[],t){for(const e of o)e.onRequest&&await e.onRequest(t)}async function f(o=[],t,e){for(const n of o)n.onResponse&&await n.onResponse(t,e)}async function d(e){const n=t({logger:e.logger??!0});await n.register(o),await async function(o=[],t){for(const e of o)e.onInit&&await e.onInit(t)}(e.plugins,n);for(const o of e.routes){const t=await c(o);n.log.info(`⚡ ${t.method} ${t.path}`),n.route({method:t.method,url:t.path,handler:async(o,c)=>{const d=r(o,c);try{await u(e.plugins,d),await a(e.globalMiddlewares,d),await a(t.middlewares,d);const o=await t.handler(d,n);return await f(e.plugins,d,o),s(c,o)}catch(o){return i(o,d,e.plugins)}}})}return n}async function l(o){const t=await d(o),e=Number(o.port||process.env.PORT||3e3),n=o.host||process.env.HOST||"0.0.0.0";return await t.listen({port:e,host:n}),t.log.info(""),t.log.info("🚀 LambdaPipe Runtime"),t.log.info(`🌐 http://${n}:${e}`),t.log.info("📦 routes: "+o.routes.length),t.log.info(""),t}export{l as bootstrap,d as createRuntimeApp};
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
# Production Runtime production.ts
|
|
2
|
+
|
|
3
|
+
Production-ready Fastify runtime for LambdaPipe.
|
|
4
|
+
|
|
5
|
+
Designed for:
|
|
6
|
+
|
|
7
|
+
* bundled deployments
|
|
8
|
+
* lazy-loaded route manifests
|
|
9
|
+
* serverless adapters
|
|
10
|
+
* container deployments
|
|
11
|
+
* runtime middleware pipelines
|
|
12
|
+
* runtime plugin lifecycle hooks
|
|
13
|
+
|
|
14
|
+
Unlike the dev runtime, production mode does not scan the filesystem dynamically.
|
|
15
|
+
|
|
16
|
+
Routes are registered through a static manifest for deterministic builds and deployment-safe startup.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
# Features
|
|
21
|
+
|
|
22
|
+
* Fastify production runtime
|
|
23
|
+
* Static route manifest system
|
|
24
|
+
* Lazy-loaded route modules
|
|
25
|
+
* Runtime middleware lifecycle
|
|
26
|
+
* Runtime plugin hooks
|
|
27
|
+
* Shared execution context
|
|
28
|
+
* Cookie support
|
|
29
|
+
* Response serialization
|
|
30
|
+
* Route module cache
|
|
31
|
+
* Error handling pipeline
|
|
32
|
+
* Deployment-safe routing
|
|
33
|
+
* Tree-shake friendly architecture
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
# Installation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install fastify @fastify/cookie
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
# Quick Start
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { bootstrap } from 'lambda-pipe/server'
|
|
49
|
+
|
|
50
|
+
import routes from './routes.manifest'
|
|
51
|
+
|
|
52
|
+
await bootstrap({
|
|
53
|
+
port: 3000,
|
|
54
|
+
|
|
55
|
+
routes,
|
|
56
|
+
})
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
# Route Manifest
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
// routes.manifest.ts
|
|
65
|
+
|
|
66
|
+
import type {
|
|
67
|
+
RouteLoader,
|
|
68
|
+
} from 'lambda-pipe/server'
|
|
69
|
+
|
|
70
|
+
const routes: RouteLoader[] = [
|
|
71
|
+
() => import('./routes/health'),
|
|
72
|
+
|
|
73
|
+
() => import('./routes/user'),
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
export default routes
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
# Why Manifest Loaders
|
|
82
|
+
|
|
83
|
+
Each manifest item is only responsible for lazy-importing a route module.
|
|
84
|
+
|
|
85
|
+
Route metadata is defined directly inside `defineRoute()`.
|
|
86
|
+
|
|
87
|
+
This avoids:
|
|
88
|
+
|
|
89
|
+
* duplicated route metadata
|
|
90
|
+
* route mismatch bugs
|
|
91
|
+
* startup filesystem scanning
|
|
92
|
+
* runtime path resolution issues
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
# Project Structure
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
src/
|
|
100
|
+
routes/
|
|
101
|
+
health.ts
|
|
102
|
+
user.ts
|
|
103
|
+
|
|
104
|
+
routes.manifest.ts
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
# Health Route
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
import {
|
|
113
|
+
defineRoute,
|
|
114
|
+
ok,
|
|
115
|
+
} from 'lambda-pipe'
|
|
116
|
+
|
|
117
|
+
export default defineRoute({
|
|
118
|
+
method: 'GET',
|
|
119
|
+
|
|
120
|
+
path: '/health',
|
|
121
|
+
|
|
122
|
+
handler: async () => {
|
|
123
|
+
return ok({
|
|
124
|
+
status: 'ok',
|
|
125
|
+
|
|
126
|
+
uptime: process.uptime(),
|
|
127
|
+
})
|
|
128
|
+
},
|
|
129
|
+
})
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
# Full Runtime Route Example
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
import {
|
|
138
|
+
defineRoute,
|
|
139
|
+
ok,
|
|
140
|
+
HttpError,
|
|
141
|
+
} from 'lambda-pipe'
|
|
142
|
+
|
|
143
|
+
export default defineRoute({
|
|
144
|
+
method: 'GET',
|
|
145
|
+
|
|
146
|
+
path: '/user/:id',
|
|
147
|
+
|
|
148
|
+
middlewares: [
|
|
149
|
+
async (context) => {
|
|
150
|
+
if (!context.headers.authorization) {
|
|
151
|
+
throw new HttpError(
|
|
152
|
+
401,
|
|
153
|
+
'Unauthorized',
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
|
|
159
|
+
handler: async (context) => {
|
|
160
|
+
const userId =
|
|
161
|
+
context.pathParameters.id
|
|
162
|
+
|
|
163
|
+
return ok({
|
|
164
|
+
id: userId,
|
|
165
|
+
|
|
166
|
+
source: 'runtime',
|
|
167
|
+
|
|
168
|
+
requestId:
|
|
169
|
+
context.requestId,
|
|
170
|
+
})
|
|
171
|
+
},
|
|
172
|
+
})
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
# Runtime Plugins
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
import type {
|
|
181
|
+
RuntimePlugin,
|
|
182
|
+
} from 'lambda-pipe/server'
|
|
183
|
+
|
|
184
|
+
export const loggerPlugin: RuntimePlugin = {
|
|
185
|
+
name: 'logger',
|
|
186
|
+
|
|
187
|
+
onInit: async (app) => {
|
|
188
|
+
app.log.info(
|
|
189
|
+
'logger initialized',
|
|
190
|
+
)
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
onRequest: async (context) => {
|
|
194
|
+
context.request.log.info({
|
|
195
|
+
method: context.method,
|
|
196
|
+
|
|
197
|
+
path: context.rawPath,
|
|
198
|
+
})
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
onResponse: async (
|
|
202
|
+
context,
|
|
203
|
+
response,
|
|
204
|
+
) => {
|
|
205
|
+
context.request.log.info({
|
|
206
|
+
statusCode:
|
|
207
|
+
response.statusCode,
|
|
208
|
+
})
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
onError: async (
|
|
212
|
+
error,
|
|
213
|
+
context,
|
|
214
|
+
) => {
|
|
215
|
+
context.request.log.error(
|
|
216
|
+
error,
|
|
217
|
+
)
|
|
218
|
+
},
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
# Using Plugins
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
import { bootstrap } from 'lambda-pipe/server'
|
|
228
|
+
|
|
229
|
+
import routes from './routes.manifest'
|
|
230
|
+
|
|
231
|
+
import { loggerPlugin } from './plugins/logger'
|
|
232
|
+
|
|
233
|
+
await bootstrap({
|
|
234
|
+
routes,
|
|
235
|
+
|
|
236
|
+
plugins: [loggerPlugin],
|
|
237
|
+
})
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
# Global Middleware
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
await bootstrap({
|
|
246
|
+
port: 3000,
|
|
247
|
+
|
|
248
|
+
routes,
|
|
249
|
+
|
|
250
|
+
logger: true,
|
|
251
|
+
|
|
252
|
+
globalMiddlewares: [
|
|
253
|
+
async (context) => {
|
|
254
|
+
context.request.log.info({
|
|
255
|
+
path: context.rawPath,
|
|
256
|
+
})
|
|
257
|
+
},
|
|
258
|
+
],
|
|
259
|
+
})
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
# Runtime Lifecycle
|
|
265
|
+
|
|
266
|
+
```text
|
|
267
|
+
HTTP Request
|
|
268
|
+
↓
|
|
269
|
+
Fastify Runtime
|
|
270
|
+
↓
|
|
271
|
+
Route Manifest Match
|
|
272
|
+
↓
|
|
273
|
+
Lazy Route Import
|
|
274
|
+
↓
|
|
275
|
+
Create Runtime Context
|
|
276
|
+
↓
|
|
277
|
+
Plugin onRequest Hooks
|
|
278
|
+
↓
|
|
279
|
+
Global Middlewares
|
|
280
|
+
↓
|
|
281
|
+
Route Middlewares
|
|
282
|
+
↓
|
|
283
|
+
Route Handler
|
|
284
|
+
↓
|
|
285
|
+
Plugin onResponse Hooks
|
|
286
|
+
↓
|
|
287
|
+
Serialize Response
|
|
288
|
+
↓
|
|
289
|
+
HTTP Response
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
# Error Lifecycle
|
|
295
|
+
|
|
296
|
+
```text
|
|
297
|
+
Handler Error
|
|
298
|
+
↓
|
|
299
|
+
Plugin onError Hooks
|
|
300
|
+
↓
|
|
301
|
+
Runtime Error Handler
|
|
302
|
+
↓
|
|
303
|
+
Serialized Error Response
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
# Response Shape
|
|
309
|
+
|
|
310
|
+
```ts
|
|
311
|
+
return {
|
|
312
|
+
statusCode: 200,
|
|
313
|
+
|
|
314
|
+
headers: {
|
|
315
|
+
'x-runtime': 'lambda-pipe',
|
|
316
|
+
},
|
|
317
|
+
|
|
318
|
+
body: {
|
|
319
|
+
ok: true,
|
|
320
|
+
},
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
# Module Cache
|
|
327
|
+
|
|
328
|
+
Production runtime caches loaded route modules internally using a WeakMap-based loader cache.
|
|
329
|
+
|
|
330
|
+
Benefits:
|
|
331
|
+
|
|
332
|
+
* avoids duplicate imports
|
|
333
|
+
* keeps lazy-loading efficient
|
|
334
|
+
* reduces runtime overhead
|
|
335
|
+
* keeps route identity stable
|
|
336
|
+
|
|
337
|
+
---
|
|
338
|
+
|
|
339
|
+
# Deployment Targets
|
|
340
|
+
|
|
341
|
+
Production runtime works well with:
|
|
342
|
+
|
|
343
|
+
* AWS Lambda
|
|
344
|
+
* ECS / Fargate
|
|
345
|
+
* Docker containers
|
|
346
|
+
* Bun runtime
|
|
347
|
+
* Node.js servers
|
|
348
|
+
* Edge-compatible adapters
|
|
349
|
+
* Kubernetes
|
|
350
|
+
* Railway / Render / Fly.io
|
|
351
|
+
|
|
352
|
+
---
|
|
353
|
+
|
|
354
|
+
# Why Production Runtime Avoids FS Scanning
|
|
355
|
+
|
|
356
|
+
Filesystem scanning is avoided in production because:
|
|
357
|
+
|
|
358
|
+
* faster startup
|
|
359
|
+
* deployment-safe bundling
|
|
360
|
+
* smaller runtime surface
|
|
361
|
+
* deterministic route registration
|
|
362
|
+
* serverless compatibility
|
|
363
|
+
* better tree-shaking support
|
|
364
|
+
* fewer runtime filesystem assumptions
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
# Example Production Bootstrap
|
|
369
|
+
|
|
370
|
+
```ts
|
|
371
|
+
import { bootstrap } from 'lambda-pipe/server'
|
|
372
|
+
|
|
373
|
+
import routes from './routes.manifest'
|
|
374
|
+
|
|
375
|
+
await bootstrap({
|
|
376
|
+
port: 8080,
|
|
377
|
+
|
|
378
|
+
host: '0.0.0.0',
|
|
379
|
+
|
|
380
|
+
routes,
|
|
381
|
+
|
|
382
|
+
logger: true,
|
|
383
|
+
})
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
---
|
|
387
|
+
|
|
388
|
+
# Example Startup Logs
|
|
389
|
+
|
|
390
|
+
```text
|
|
391
|
+
⚡ GET /health
|
|
392
|
+
⚡ GET /user/:id
|
|
393
|
+
|
|
394
|
+
🚀 LambdaPipe Runtime
|
|
395
|
+
🌐 http://0.0.0.0:3000
|
|
396
|
+
📦 routes: 2
|
|
397
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lambda-pipe",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Type-safe plugin & middleware runtime for AWS Lambda and serverless APIs.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Delpi.Kye",
|
|
@@ -38,6 +38,11 @@
|
|
|
38
38
|
"types": "./build/cli/dev.d.ts",
|
|
39
39
|
"import": "./build/esm/cli/index.js",
|
|
40
40
|
"require": "./build/cjs/cli/index.js"
|
|
41
|
+
},
|
|
42
|
+
"./server": {
|
|
43
|
+
"types": "./build/cli/server.d.ts",
|
|
44
|
+
"import": "./build/esm/server/index.js",
|
|
45
|
+
"require": "./build/cjs/server/index.js"
|
|
41
46
|
}
|
|
42
47
|
},
|
|
43
48
|
"files": [
|
|
@@ -47,6 +52,7 @@
|
|
|
47
52
|
"scripts": {
|
|
48
53
|
"clean": "rimraf build",
|
|
49
54
|
"dev": "nodemon --watch src --ext ts --exec tsx src/cli/dev.ts",
|
|
55
|
+
"prod": "nodemon --watch src --ext ts --exec tsx src/cli/server.ts",
|
|
50
56
|
"build": "rollup -c",
|
|
51
57
|
"cb": "npm run clean && npm run build",
|
|
52
58
|
"lint": "eslint \"src/**/*.{ts,tsx}\"",
|