@rexeus/typeweaver-server 0.6.4 → 0.7.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/index.cjs +3 -1
- package/dist/index.mjs +3 -1
- package/dist/index.mjs.map +1 -1
- package/dist/lib/Router.ts +10 -0
- package/dist/lib/ServerContext.ts +4 -0
- package/dist/lib/TypeweaverApp.ts +13 -5
- package/dist/lib/TypeweaverRouter.ts +3 -0
- package/dist/lib/index.ts +1 -0
- package/dist/templates/Router.ejs +1 -0
- package/package.json +5 -5
package/dist/index.cjs
CHANGED
|
@@ -66,8 +66,10 @@ var RouterGenerator = class {
|
|
|
66
66
|
context.writeFile(relativePath, content);
|
|
67
67
|
}
|
|
68
68
|
static createOperationData(resource) {
|
|
69
|
-
const
|
|
69
|
+
const operationId = resource.definition.operationId;
|
|
70
|
+
const className = case$1.default.pascal(operationId);
|
|
70
71
|
return {
|
|
72
|
+
operationId,
|
|
71
73
|
className,
|
|
72
74
|
handlerName: `handle${className}Request`,
|
|
73
75
|
method: resource.definition.method,
|
package/dist/index.mjs
CHANGED
|
@@ -37,8 +37,10 @@ var RouterGenerator = class {
|
|
|
37
37
|
context.writeFile(relativePath, content);
|
|
38
38
|
}
|
|
39
39
|
static createOperationData(resource) {
|
|
40
|
-
const
|
|
40
|
+
const operationId = resource.definition.operationId;
|
|
41
|
+
const className = Case.pascal(operationId);
|
|
41
42
|
return {
|
|
43
|
+
operationId,
|
|
42
44
|
className,
|
|
43
45
|
handlerName: `handle${className}Request`,
|
|
44
46
|
method: resource.definition.method,
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/RouterGenerator.ts","../src/index.ts"],"sourcesContent":["import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { HttpMethod } from \"@rexeus/typeweaver-core\";\nimport { Path } from \"@rexeus/typeweaver-gen\";\nimport type {\n GeneratorContext,\n OperationResource,\n} from \"@rexeus/typeweaver-gen\";\nimport Case from \"case\";\n\ntype OperationData = {\n readonly className: string;\n readonly handlerName: string;\n readonly method: string;\n readonly path: string;\n};\n\n/**\n * Generates TypeweaverRouter subclasses from API definitions.\n *\n * For each resource (e.g., `Todo`, `Account`), produces a `<ResourceName>Router.ts`\n * file that extends `TypeweaverRouter` and registers all operations as routes.\n */\nexport class RouterGenerator {\n /**\n * Generates router files for all resources in the given context.\n *\n * @param context - The generator context containing resources, templates, and output configuration\n */\n public static generate(context: GeneratorContext): void {\n const moduleDir = path.dirname(fileURLToPath(import.meta.url));\n const templateFile = path.join(moduleDir, \"templates\", \"Router.ejs\");\n\n for (const [entityName, entityResource] of Object.entries(\n context.resources.entityResources\n )) {\n this.writeRouter(\n entityName,\n templateFile,\n entityResource.operations,\n context\n );\n }\n }\n\n private static writeRouter(\n entityName: string,\n templateFile: string,\n operationResources: OperationResource[],\n context: GeneratorContext\n ): void {\n const pascalCaseEntityName = Case.pascal(entityName);\n const outputDir = path.join(context.outputDir, entityName);\n const outputPath = path.join(outputDir, `${pascalCaseEntityName}Router.ts`);\n\n const operations = operationResources\n .filter(resource => resource.definition.method !== HttpMethod.HEAD)\n .map(resource => this.createOperationData(resource))\n .sort((a, b) => this.compareRoutes(a, b));\n\n const content = context.renderTemplate(templateFile, {\n coreDir: Path.relative(outputDir, context.outputDir),\n entityName,\n pascalCaseEntityName,\n operations,\n });\n\n const relativePath = path.relative(context.outputDir, outputPath);\n context.writeFile(relativePath, content);\n }\n\n private static createOperationData(\n resource: OperationResource\n ): OperationData {\n const className = Case.pascal(
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/RouterGenerator.ts","../src/index.ts"],"sourcesContent":["import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { HttpMethod } from \"@rexeus/typeweaver-core\";\nimport { Path } from \"@rexeus/typeweaver-gen\";\nimport type {\n GeneratorContext,\n OperationResource,\n} from \"@rexeus/typeweaver-gen\";\nimport Case from \"case\";\n\ntype OperationData = {\n readonly operationId: string;\n readonly className: string;\n readonly handlerName: string;\n readonly method: string;\n readonly path: string;\n};\n\n/**\n * Generates TypeweaverRouter subclasses from API definitions.\n *\n * For each resource (e.g., `Todo`, `Account`), produces a `<ResourceName>Router.ts`\n * file that extends `TypeweaverRouter` and registers all operations as routes.\n */\nexport class RouterGenerator {\n /**\n * Generates router files for all resources in the given context.\n *\n * @param context - The generator context containing resources, templates, and output configuration\n */\n public static generate(context: GeneratorContext): void {\n const moduleDir = path.dirname(fileURLToPath(import.meta.url));\n const templateFile = path.join(moduleDir, \"templates\", \"Router.ejs\");\n\n for (const [entityName, entityResource] of Object.entries(\n context.resources.entityResources\n )) {\n this.writeRouter(\n entityName,\n templateFile,\n entityResource.operations,\n context\n );\n }\n }\n\n private static writeRouter(\n entityName: string,\n templateFile: string,\n operationResources: OperationResource[],\n context: GeneratorContext\n ): void {\n const pascalCaseEntityName = Case.pascal(entityName);\n const outputDir = path.join(context.outputDir, entityName);\n const outputPath = path.join(outputDir, `${pascalCaseEntityName}Router.ts`);\n\n const operations = operationResources\n .filter(resource => resource.definition.method !== HttpMethod.HEAD)\n .map(resource => this.createOperationData(resource))\n .sort((a, b) => this.compareRoutes(a, b));\n\n const content = context.renderTemplate(templateFile, {\n coreDir: Path.relative(outputDir, context.outputDir),\n entityName,\n pascalCaseEntityName,\n operations,\n });\n\n const relativePath = path.relative(context.outputDir, outputPath);\n context.writeFile(relativePath, content);\n }\n\n private static createOperationData(\n resource: OperationResource\n ): OperationData {\n const operationId = resource.definition.operationId;\n const className = Case.pascal(operationId);\n\n return {\n operationId,\n className,\n handlerName: `handle${className}Request`,\n method: resource.definition.method,\n path: resource.definition.path,\n };\n }\n\n private static compareRoutes(a: OperationData, b: OperationData): number {\n const aSegments = a.path.split(\"/\").filter(s => s);\n const bSegments = b.path.split(\"/\").filter(s => s);\n\n // 1. Compare by depth first (shallow to deep)\n if (aSegments.length !== bSegments.length) {\n return aSegments.length - bSegments.length;\n }\n\n // 2. Compare segment by segment\n for (let i = 0; i < aSegments.length; i++) {\n const aSegment = aSegments[i]!;\n const bSegment = bSegments[i]!;\n\n const aIsParam = aSegment.startsWith(\":\");\n const bIsParam = bSegment.startsWith(\":\");\n\n // Static segments before parameters\n if (aIsParam !== bIsParam) {\n return aIsParam ? 1 : -1;\n }\n\n // Within same type, alphabetical order\n if (aSegment !== bSegment) {\n return aSegment.localeCompare(bSegment);\n }\n }\n\n // 3. Same path = sort by HTTP method priority\n return this.getMethodPriority(a.method) - this.getMethodPriority(b.method);\n }\n\n private static readonly METHOD_PRIORITY: Record<string, number> = {\n GET: 1,\n POST: 2,\n PUT: 3,\n PATCH: 4,\n DELETE: 5,\n OPTIONS: 6,\n HEAD: 7,\n };\n\n private static getMethodPriority(method: string): number {\n return this.METHOD_PRIORITY[method] ?? 999;\n }\n}\n","import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { BasePlugin } from \"@rexeus/typeweaver-gen\";\nimport type { GeneratorContext } from \"@rexeus/typeweaver-gen\";\nimport { RouterGenerator } from \"./RouterGenerator\";\n\nconst moduleDir = path.dirname(fileURLToPath(import.meta.url));\n\n/**\n * Typeweaver plugin that generates a lightweight, dependency-free server\n * with built-in routing and middleware support.\n *\n * Copies the runtime library files (`TypeweaverApp`, `TypeweaverRouter`, `Router`,\n * `Middleware`, etc.) and generates typed router classes for each resource.\n */\nexport default class ServerPlugin extends BasePlugin {\n public name = \"server\";\n\n /**\n * Generates the server runtime and typed routers for all resources.\n *\n * @param context - The generator context\n */\n public override generate(context: GeneratorContext): void {\n const libSourceDir = path.join(moduleDir, \"lib\");\n this.copyLibFiles(context, libSourceDir, this.name);\n\n RouterGenerator.generate(context);\n }\n}\n"],"mappings":";;;;;;;;;;;;;AAwBA,IAAa,kBAAb,MAA6B;;;;;;CAM3B,OAAc,SAAS,SAAiC;EACtD,MAAM,YAAY,KAAK,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC;EAC9D,MAAM,eAAe,KAAK,KAAK,WAAW,aAAa,aAAa;AAEpE,OAAK,MAAM,CAAC,YAAY,mBAAmB,OAAO,QAChD,QAAQ,UAAU,gBACnB,CACC,MAAK,YACH,YACA,cACA,eAAe,YACf,QACD;;CAIL,OAAe,YACb,YACA,cACA,oBACA,SACM;EACN,MAAM,uBAAuB,KAAK,OAAO,WAAW;EACpD,MAAM,YAAY,KAAK,KAAK,QAAQ,WAAW,WAAW;EAC1D,MAAM,aAAa,KAAK,KAAK,WAAW,GAAG,qBAAqB,WAAW;EAE3E,MAAM,aAAa,mBAChB,QAAO,aAAY,SAAS,WAAW,WAAW,WAAW,KAAK,CAClE,KAAI,aAAY,KAAK,oBAAoB,SAAS,CAAC,CACnD,MAAM,GAAG,MAAM,KAAK,cAAc,GAAG,EAAE,CAAC;EAE3C,MAAM,UAAU,QAAQ,eAAe,cAAc;GACnD,SAAS,KAAK,SAAS,WAAW,QAAQ,UAAU;GACpD;GACA;GACA;GACD,CAAC;EAEF,MAAM,eAAe,KAAK,SAAS,QAAQ,WAAW,WAAW;AACjE,UAAQ,UAAU,cAAc,QAAQ;;CAG1C,OAAe,oBACb,UACe;EACf,MAAM,cAAc,SAAS,WAAW;EACxC,MAAM,YAAY,KAAK,OAAO,YAAY;AAE1C,SAAO;GACL;GACA;GACA,aAAa,SAAS,UAAU;GAChC,QAAQ,SAAS,WAAW;GAC5B,MAAM,SAAS,WAAW;GAC3B;;CAGH,OAAe,cAAc,GAAkB,GAA0B;EACvE,MAAM,YAAY,EAAE,KAAK,MAAM,IAAI,CAAC,QAAO,MAAK,EAAE;EAClD,MAAM,YAAY,EAAE,KAAK,MAAM,IAAI,CAAC,QAAO,MAAK,EAAE;AAGlD,MAAI,UAAU,WAAW,UAAU,OACjC,QAAO,UAAU,SAAS,UAAU;AAItC,OAAK,IAAI,IAAI,GAAG,IAAI,UAAU,QAAQ,KAAK;GACzC,MAAM,WAAW,UAAU;GAC3B,MAAM,WAAW,UAAU;GAE3B,MAAM,WAAW,SAAS,WAAW,IAAI;AAIzC,OAAI,aAHa,SAAS,WAAW,IAAI,CAIvC,QAAO,WAAW,IAAI;AAIxB,OAAI,aAAa,SACf,QAAO,SAAS,cAAc,SAAS;;AAK3C,SAAO,KAAK,kBAAkB,EAAE,OAAO,GAAG,KAAK,kBAAkB,EAAE,OAAO;;CAG5E,OAAwB,kBAA0C;EAChE,KAAK;EACL,MAAM;EACN,KAAK;EACL,OAAO;EACP,QAAQ;EACR,SAAS;EACT,MAAM;EACP;CAED,OAAe,kBAAkB,QAAwB;AACvD,SAAO,KAAK,gBAAgB,WAAW;;;;;;AC5H3C,MAAM,YAAY,KAAK,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC;;;;;;;;AAS9D,IAAqB,eAArB,cAA0C,WAAW;CACnD,AAAO,OAAO;;;;;;CAOd,AAAgB,SAAS,SAAiC;EACxD,MAAM,eAAe,KAAK,KAAK,WAAW,MAAM;AAChD,OAAK,aAAa,SAAS,cAAc,KAAK,KAAK;AAEnD,kBAAgB,SAAS,QAAQ"}
|
package/dist/lib/Router.ts
CHANGED
|
@@ -14,10 +14,20 @@ import type {
|
|
|
14
14
|
import type { RequestHandler } from "./RequestHandler";
|
|
15
15
|
import type { ServerContext } from "./ServerContext";
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Metadata about a matched route, available in middleware and handlers via `ctx.route`.
|
|
19
|
+
*/
|
|
20
|
+
export type RouteMetadata = {
|
|
21
|
+
readonly operationId: string;
|
|
22
|
+
readonly method: HttpMethod;
|
|
23
|
+
readonly path: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
17
26
|
/**
|
|
18
27
|
* A registered route with its method, path pattern, validator, and handler.
|
|
19
28
|
*/
|
|
20
29
|
export type RouteDefinition = {
|
|
30
|
+
readonly operationId: string;
|
|
21
31
|
readonly method: HttpMethod;
|
|
22
32
|
readonly path: string;
|
|
23
33
|
readonly validator: IRequestValidator;
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { IHttpRequest } from "@rexeus/typeweaver-core";
|
|
9
|
+
import type { RouteMetadata } from "./Router";
|
|
9
10
|
import type { StateMap } from "./StateMap";
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -51,4 +52,7 @@ export type ServerContext<
|
|
|
51
52
|
|
|
52
53
|
/** Type-safe key-value store for sharing data between middleware and handlers. */
|
|
53
54
|
readonly state: StateMap<TState>;
|
|
55
|
+
|
|
56
|
+
/** Metadata about the matched route, or `undefined` for 404/405 requests. */
|
|
57
|
+
readonly route: RouteMetadata | undefined;
|
|
54
58
|
};
|
|
@@ -17,6 +17,7 @@ import type { RequestHandler } from "./RequestHandler";
|
|
|
17
17
|
import type {
|
|
18
18
|
HttpResponseErrorHandler,
|
|
19
19
|
RouteDefinition,
|
|
20
|
+
RouteMatch,
|
|
20
21
|
UnknownErrorHandler,
|
|
21
22
|
ValidationErrorHandler,
|
|
22
23
|
} from "./Router";
|
|
@@ -186,15 +187,24 @@ export class TypeweaverApp<TState extends Record<string, unknown> = {}> {
|
|
|
186
187
|
const url = new URL(request.url);
|
|
187
188
|
const httpRequest = await this.adapter.toRequest(request, url);
|
|
188
189
|
|
|
190
|
+
const match = this.router.match(request.method, url.pathname);
|
|
191
|
+
|
|
189
192
|
const ctx: ServerContext = {
|
|
190
193
|
request: httpRequest,
|
|
191
194
|
state: new StateMap(),
|
|
195
|
+
route: match
|
|
196
|
+
? {
|
|
197
|
+
operationId: match.route.operationId,
|
|
198
|
+
method: match.route.method,
|
|
199
|
+
path: match.route.path,
|
|
200
|
+
}
|
|
201
|
+
: undefined,
|
|
192
202
|
};
|
|
193
203
|
|
|
194
204
|
const response = await executeMiddlewarePipeline(
|
|
195
205
|
this.middlewares,
|
|
196
206
|
ctx,
|
|
197
|
-
() => this.resolveAndExecute(
|
|
207
|
+
() => this.resolveAndExecute(match, url.pathname, ctx)
|
|
198
208
|
);
|
|
199
209
|
|
|
200
210
|
return request.method.toUpperCase() === "HEAD"
|
|
@@ -203,16 +213,14 @@ export class TypeweaverApp<TState extends Record<string, unknown> = {}> {
|
|
|
203
213
|
}
|
|
204
214
|
|
|
205
215
|
/**
|
|
206
|
-
*
|
|
216
|
+
* Execute the matched route handler, or produce 404/405 responses.
|
|
207
217
|
* Called as the final handler in the middleware pipeline.
|
|
208
218
|
*/
|
|
209
219
|
private async resolveAndExecute(
|
|
210
|
-
|
|
220
|
+
match: RouteMatch | undefined,
|
|
211
221
|
pathname: string,
|
|
212
222
|
ctx: ServerContext
|
|
213
223
|
): Promise<IHttpResponse> {
|
|
214
|
-
const match = this.router.match(method, pathname);
|
|
215
|
-
|
|
216
224
|
if (match) {
|
|
217
225
|
const routeCtx = this.withPathParams(ctx, match.params);
|
|
218
226
|
try {
|
|
@@ -109,18 +109,21 @@ export abstract class TypeweaverRouter<
|
|
|
109
109
|
/**
|
|
110
110
|
* Register a route. Called by generated subclasses in their constructor.
|
|
111
111
|
*
|
|
112
|
+
* @param operationId - Unique operation identifier from the API definition
|
|
112
113
|
* @param method - HTTP method (GET, POST, PUT, DELETE, etc.)
|
|
113
114
|
* @param path - Path pattern with `:param` placeholders
|
|
114
115
|
* @param validator - Request validator for this operation
|
|
115
116
|
* @param handler - Type-safe request handler
|
|
116
117
|
*/
|
|
117
118
|
protected route(
|
|
119
|
+
operationId: string,
|
|
118
120
|
method: HttpMethod,
|
|
119
121
|
path: string,
|
|
120
122
|
validator: IRequestValidator,
|
|
121
123
|
handler: RequestHandler<any, any, any>
|
|
122
124
|
): void {
|
|
123
125
|
this.routes.push({
|
|
126
|
+
operationId,
|
|
124
127
|
method,
|
|
125
128
|
path,
|
|
126
129
|
validator,
|
package/dist/lib/index.ts
CHANGED
|
@@ -32,6 +32,7 @@ export class <%- pascalCaseEntityName %>Router<
|
|
|
32
32
|
protected setupRoutes(): void {
|
|
33
33
|
<% for (const operation of operations) { %>
|
|
34
34
|
this.route(
|
|
35
|
+
'<%- operation.operationId %>',
|
|
35
36
|
HttpMethod.<%- operation.method %>,
|
|
36
37
|
'<%- operation.path %>',
|
|
37
38
|
new <%- operation.className %>RequestValidator(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rexeus/typeweaver-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Generates a lightweight, dependency-free server with built-in routing and middleware from your API definitions. Powered by Typeweaver.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -47,15 +47,15 @@
|
|
|
47
47
|
},
|
|
48
48
|
"homepage": "https://github.com/rexeus/typeweaver#readme",
|
|
49
49
|
"peerDependencies": {
|
|
50
|
-
"@rexeus/typeweaver-
|
|
51
|
-
"@rexeus/typeweaver-
|
|
50
|
+
"@rexeus/typeweaver-core": "^0.7.0",
|
|
51
|
+
"@rexeus/typeweaver-gen": "^0.7.0"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"get-port": "^7.1.0",
|
|
55
55
|
"test-utils": "file:../test-utils",
|
|
56
56
|
"tsx": "^4.21.0",
|
|
57
|
-
"@rexeus/typeweaver-
|
|
58
|
-
"@rexeus/typeweaver-
|
|
57
|
+
"@rexeus/typeweaver-core": "^0.7.0",
|
|
58
|
+
"@rexeus/typeweaver-gen": "^0.7.0"
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
61
|
"case": "^1.6.3"
|