princejs 1.9.2 → 1.9.4

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 CHANGED
@@ -105,6 +105,55 @@ const app = prince();
105
105
  app.plugin(usersPlugin, { prefix: "/api" });
106
106
  ```
107
107
 
108
+ ### OpenAPI + Scalar Docs
109
+
110
+ Auto-generate an OpenAPI 3.0 spec and serve a beautiful [Scalar](https://scalar.com) API reference UI — all from a single `app.openapi()` call. Routes, validation, and docs stay in sync automatically.
111
+
112
+ ```ts
113
+ import { prince } from "princejs";
114
+ import { z } from "zod";
115
+
116
+ const app = prince();
117
+
118
+ const api = app.openapi({ title: "My API", version: "1.0.0" }, "/docs", { theme: "moon" });
119
+
120
+ api.route("GET", "/users/:id", {
121
+ summary: "Get user by ID",
122
+ tags: ["users"],
123
+ schema: {
124
+ response: z.object({ id: z.string(), name: z.string() }),
125
+ },
126
+ }, (req) => ({ id: req.params!.id, name: "Alice" }));
127
+
128
+ api.route("POST", "/users", {
129
+ summary: "Create user",
130
+ tags: ["users"],
131
+ schema: {
132
+ body: z.object({ name: z.string().min(2), email: z.string().email() }),
133
+ response: z.object({ id: z.string(), name: z.string(), email: z.string() }),
134
+ },
135
+ }, (req) => ({ id: crypto.randomUUID(), ...req.parsedBody }));
136
+
137
+ app.listen(3000);
138
+ // → GET /docs Scalar UI
139
+ // → GET /docs.json Raw OpenAPI JSON
140
+ ```
141
+
142
+ **`api.route()` does three things at once:**
143
+ - Registers the route on PrinceJS (same as `app.get()` / `app.post()`)
144
+ - Auto-wires `validate(schema.body)` middleware — no separate import needed
145
+ - Writes the full OpenAPI spec entry including path params, request body, query params, and response schema
146
+
147
+ | `schema` key | Runtime effect | Scalar docs |
148
+ |---|---|---|
149
+ | `body` | ✅ Validates & strips via `validate()` | ✅ requestBody model |
150
+ | `query` | ❌ docs only | ✅ typed query params |
151
+ | `response` | ❌ docs only | ✅ 200 response model |
152
+
153
+ Routes registered with `app.get()` / `app.post()` directly never appear in the docs — useful for internal health checks, webhooks, and admin endpoints.
154
+
155
+ **Available themes:** `default` · `moon` · `purple` · `solarized` · `bluePlanet` · `deepSpace` · `saturn` · `kepler` · `mars`
156
+
108
157
  ### Database (SQLite)
109
158
 
110
159
  ### End to End Type-Safety
@@ -203,7 +252,7 @@ import { prince } from "princejs";
203
252
  import { cors, logger, rateLimit, auth, apiKey, jwt, session, compress, serve } from "princejs/middleware";
204
253
  import { validate } from "princejs/validation";
205
254
  import { cache, upload, sse } from "princejs/helpers";
206
- import { cron } from "princejs/scheduler";
255
+ import { cron, openapi } from "princejs/scheduler";
207
256
  import { Html, Head, Body, H1, P, render } from "princejs/jsx"
208
257
  import { db } from "princejs/db";
209
258
  import { z } from "zod";
@@ -285,6 +334,17 @@ app.get("/users", () => users.query("SELECT * FROM users"));
285
334
 
286
335
  cron("*/1 * * * *", () => console.log("PrinceJS heartbeat"));
287
336
 
337
+ // OpenAPI + Scalar
338
+ const api = app.openapi({ title: "PrinceJS App", version: "1.0.0" }, "/docs");
339
+ api.route("GET", "/items", {
340
+ summary: "List items",
341
+ tags: ["items"],
342
+ schema: {
343
+ query: z.object({ q: z.string().optional() }),
344
+ response: z.array(z.object({ id: z.string(), name: z.string() })),
345
+ },
346
+ }, () => [{ id: "1", name: "Widget" }]);
347
+
288
348
  app.listen(3000);
289
349
  ```
290
350
 
package/dist/prince.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { OpenAPIBuilder, ScalarOptions } from "./scheduler";
2
+ import { z } from "zod";
1
3
  type Next = () => Promise<Response>;
2
4
  type Middleware = (req: PrinceRequest, next: Next) => Promise<Response | undefined> | Response | undefined;
3
5
  type HandlerResult = Response | Record<string, any> | string | Uint8Array;
@@ -32,13 +34,30 @@ declare class ResponseBuilder {
32
34
  redirect(url: string, status?: number): Response;
33
35
  build(): Response;
34
36
  }
37
+ export interface RouteSchema {
38
+ /** Zod schema for the request body — auto-wires validate() middleware */
39
+ body?: z.ZodTypeAny;
40
+ /** Zod schema for query parameters — written into the OpenAPI spec */
41
+ query?: z.ZodObject<any>;
42
+ /** Zod schema for the response body — written into the OpenAPI spec */
43
+ response?: z.ZodTypeAny;
44
+ }
45
+ export interface RouteOperation {
46
+ summary?: string;
47
+ description?: string;
48
+ tags?: string[];
49
+ /** Attach Zod schemas to auto-generate request/response models in Scalar */
50
+ schema?: RouteSchema;
51
+ responses?: Record<string, unknown>;
52
+ [key: string]: unknown;
53
+ }
35
54
  export declare class Prince {
36
55
  private devMode;
37
56
  private rawRoutes;
38
57
  private middlewares;
39
58
  private errorHandler?;
40
59
  private wsRoutes;
41
- private openapiData;
60
+ private openapiSpec;
42
61
  private router;
43
62
  private staticRoutes;
44
63
  private staticMiddlewares;
@@ -77,6 +96,37 @@ export declare class Prince {
77
96
  private executeHandler;
78
97
  handleFetch(req: Request): Promise<Response>;
79
98
  fetch(req: Request): Promise<Response>;
99
+ /**
100
+ * Mounts an OpenAPI spec + Scalar UI onto this Prince app.
101
+ *
102
+ * Calling this method returns an `OpenAPIBuilder` whose `.route()` method
103
+ * registers a route on the Prince app **and** writes the matching path entry
104
+ * into the OpenAPI spec simultaneously — so the two can never drift.
105
+ *
106
+ * Two routes are auto-registered:
107
+ * GET `docsPath` → Scalar UI (e.g. /docs)
108
+ * GET `docsPath`.json → Raw spec (e.g. /docs.json)
109
+ *
110
+ * @example
111
+ * const app = prince();
112
+ * const api = app.openapi({ title: "My API", version: "1.0.0" }, "/docs");
113
+ *
114
+ * // Registers GET /hello on Prince AND adds it to the OpenAPI spec
115
+ * api.route("GET", "/hello", {
116
+ * summary: "Say hello",
117
+ * responses: { 200: { description: "OK" } },
118
+ * }, (req) => ({ message: "hello" }));
119
+ *
120
+ * app.listen(3000);
121
+ * // → GET /docs → Scalar UI
122
+ * // → GET /docs.json → raw OpenAPI JSON
123
+ */
124
+ openapi(info: {
125
+ title: string;
126
+ version: string;
127
+ }, docsPath?: string, scalarOptions?: ScalarOptions): OpenAPIBuilder & {
128
+ route: (method: string, path: string, operation: Record<string, unknown>, ...args: (RouteHandler | Middleware)[]) => OpenAPIBuilder;
129
+ };
80
130
  listen(port?: number): void;
81
131
  }
82
132
  export declare const prince: (dev?: boolean) => Prince;
@@ -1 +1 @@
1
- {"version":3,"file":"prince.d.ts","sourceRoot":"","sources":["../src/prince.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"prince.d.ts","sourceRoot":"","sources":["../src/prince.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAe,cAAc,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE9E,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,KAAK,IAAI,GAAG,MAAM,OAAO,CAAC,QAAQ,CAAC,CAAC;AACpC,KAAK,UAAU,GAAG,CAAC,GAAG,EAAE,aAAa,EAAE,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC,GAAG,QAAQ,GAAG,SAAS,CAAC;AAC3G,KAAK,aAAa,GAAG,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,GAAG,UAAU,CAAC;AAE1E,MAAM,WAAW,aAAc,SAAQ,OAAO;IAC5C,UAAU,CAAC,EAAE,GAAG,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IAC7B,IAAI,CAAC,EAAE,GAAG,CAAC;IACX,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,KAAK,CAAC,EAAE,eAAe,CAAC;IACxB,OAAO,CAAC,EAAE,GAAG,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3D,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AAED,UAAU,gBAAgB;IACxB,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,GAAG,KAAK,IAAI,CAAC;IACzB,OAAO,CAAC,EAAE,CAAC,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,KAAK,IAAI,CAAC;IAClD,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,GAAG,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1D,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,GAAG,KAAK,IAAI,CAAC;CAC3B;AAED,KAAK,YAAY,GAAG,CAAC,GAAG,EAAE,aAAa,KAAK,OAAO,CAAC,aAAa,CAAC,GAAG,aAAa,CAAC;AAqBnF,MAAM,MAAM,YAAY,CAAC,QAAQ,GAAG,GAAG,IAAI,CACzC,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,QAAQ,KACf,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAE1B,cAAM,eAAe;IACnB,OAAO,CAAC,OAAO,CAAO;IACtB,OAAO,CAAC,QAAQ,CAA8B;IAC9C,OAAO,CAAC,KAAK,CAAa;IAE1B,MAAM,CAAC,IAAI,EAAE,MAAM;IAKnB,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;IAKjC,IAAI,CAAC,IAAI,EAAE,GAAG;IAMd,IAAI,CAAC,IAAI,EAAE,MAAM;IAMjB,IAAI,CAAC,IAAI,EAAE,MAAM;IAMjB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,SAAM;IAMlC,KAAK;CAGN;AAsLD,MAAM,WAAW,WAAW;IAC1B,yEAAyE;IACzE,IAAI,CAAC,EAAE,CAAC,CAAC,UAAU,CAAC;IACpB,sEAAsE;IACtE,KAAK,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;IACzB,uEAAuE;IACvE,QAAQ,CAAC,EAAE,CAAC,CAAC,UAAU,CAAC;CACzB;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,4EAA4E;IAC5E,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,qBAAa,MAAM;IAgBL,OAAO,CAAC,OAAO;IAf3B,OAAO,CAAC,SAAS,CAAoB;IACrC,OAAO,CAAC,WAAW,CAAoB;IACvC,OAAO,CAAC,YAAY,CAAC,CAA6C;IAClE,OAAO,CAAC,QAAQ,CAAwC;IACxD,OAAO,CAAC,WAAW,CAA4B;IAC/C,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,YAAY,CAAwC;IAC5D,OAAO,CAAC,iBAAiB,CAAwC;IACjE,OAAO,CAAC,UAAU,CAKb;gBAEe,OAAO,UAAQ;IAEnC,GAAG,CAAC,EAAE,EAAE,UAAU;IAKlB;;;;;;;;;;OAUG;IACH,MAAM,CAAC,QAAQ,GAAG,GAAG,EAAE,MAAM,EAAE,YAAY,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC,EAAE,QAAQ;IAOzE,KAAK,CAAC,EAAE,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,aAAa,KAAK,QAAQ;IAKpD,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE,MAAM,SAAM;IAO5B,QAAQ;IAKR,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,CAAC,YAAY,GAAG,UAAU,CAAC,EAAE;IACxD,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,CAAC,YAAY,GAAG,UAAU,CAAC,EAAE;IACzD,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,CAAC,YAAY,GAAG,UAAU,CAAC,EAAE;IACxD,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,CAAC,YAAY,GAAG,UAAU,CAAC,EAAE;IAC3D,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,CAAC,YAAY,GAAG,UAAU,CAAC,EAAE;IAC1D,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,CAAC,YAAY,GAAG,UAAU,CAAC,EAAE;IAC5D,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,gBAAgB;IAK3C,OAAO,CAAC,GAAG;IA6BX,OAAO,CAAC,WAAW;IAqBnB,OAAO,CAAC,WAAW;IA0CnB,OAAO,CAAC,SAAS;IAkEjB,OAAO,CAAC,SAAS;IAyBjB,OAAO,CAAC,UAAU;YA+CJ,SAAS;YAoCT,cAAc;IAmDtB,WAAW,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IA4B5C,KAAK,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IAa5C;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACH,OAAO,CACL,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,EACxC,QAAQ,SAAU,EAClB,aAAa,GAAE,aAAkB,GAChC,cAAc,GAAG;QAAE,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,IAAI,EAAE,CAAC,YAAY,GAAG,UAAU,CAAC,EAAE,KAAK,cAAc,CAAA;KAAE;IAuF3J,MAAM,CAAC,IAAI,SAAO;CA8CnB;AAiCD,eAAO,MAAM,MAAM,GAAI,aAAW,WAAoB,CAAC"}
package/dist/prince.js CHANGED
@@ -1,4 +1,123 @@
1
1
  // @bun
2
+ // src/scheduler.ts
3
+ var openapi = (info) => {
4
+ const spec = { openapi: "3.0.0", info, paths: {} };
5
+ const renderScalarHtml = (options = {}) => {
6
+ const {
7
+ pageTitle = spec.info.title,
8
+ theme = "default",
9
+ layout = "modern",
10
+ hideDownloadButton = false,
11
+ customCss = ""
12
+ } = options;
13
+ return `<!doctype html>
14
+ <html lang="en">
15
+ <head>
16
+ <meta charset="utf-8" />
17
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
18
+ <title>${pageTitle}</title>
19
+ ${customCss ? `<style>${customCss}</style>` : ""}
20
+ </head>
21
+ <body>
22
+ <script
23
+ id="api-reference"
24
+ type="application/json"
25
+ data-theme="${theme}"
26
+ data-layout="${layout}"
27
+ ${hideDownloadButton ? 'data-hide-download-button="true"' : ""}
28
+ >${JSON.stringify(spec)}</script>
29
+ <script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
30
+ </body>
31
+ </html>`;
32
+ };
33
+ return {
34
+ spec,
35
+ get openapi() {
36
+ return spec.openapi;
37
+ },
38
+ get info() {
39
+ return spec.info;
40
+ },
41
+ get paths() {
42
+ return spec.paths;
43
+ },
44
+ scalar(options = {}) {
45
+ return (_req, res) => {
46
+ const html = renderScalarHtml(options);
47
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
48
+ res.end(html);
49
+ };
50
+ },
51
+ json() {
52
+ return (_req, res) => {
53
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
54
+ res.end(JSON.stringify(spec, null, 2));
55
+ };
56
+ }
57
+ };
58
+ };
59
+
60
+ // src/prince.ts
61
+ import { z } from "zod";
62
+
63
+ // src/middleware.ts
64
+ import { jwtVerify, SignJWT } from "jose";
65
+ var validate = (schema) => {
66
+ return async (req, next) => {
67
+ let bodyData = req.parsedBody;
68
+ let bodyParsed = false;
69
+ if (!bodyData && ["POST", "PUT", "PATCH"].includes(req.method)) {
70
+ const ct = req.headers.get("content-type") || "";
71
+ if (ct.includes("application/json")) {
72
+ try {
73
+ const clonedReq = req.clone();
74
+ const text = await clonedReq.text();
75
+ if (text) {
76
+ bodyData = JSON.parse(text);
77
+ bodyParsed = true;
78
+ }
79
+ } catch (parseError) {
80
+ return new Response(JSON.stringify({ error: "Invalid JSON" }), { status: 400, headers: { "Content-Type": "application/json" } });
81
+ }
82
+ } else if (ct.includes("application/x-www-form-urlencoded")) {
83
+ try {
84
+ const clonedReq = req.clone();
85
+ const text = await clonedReq.text();
86
+ bodyData = Object.fromEntries(new URLSearchParams(text));
87
+ bodyParsed = true;
88
+ } catch (parseError) {
89
+ return new Response(JSON.stringify({ error: "Invalid form data" }), { status: 400, headers: { "Content-Type": "application/json" } });
90
+ }
91
+ }
92
+ }
93
+ if (bodyData) {
94
+ const result = schema.safeParse(bodyData);
95
+ if (!result.success) {
96
+ if (bodyParsed) {
97
+ req.parsedBody = bodyData;
98
+ }
99
+ return new Response(JSON.stringify({
100
+ error: "Validation failed",
101
+ details: result.error.issues.map((e) => ({
102
+ path: e.path.join("."),
103
+ message: e.message
104
+ }))
105
+ }), {
106
+ status: 400,
107
+ headers: { "Content-Type": "application/json" }
108
+ });
109
+ }
110
+ req.parsedBody = result.data;
111
+ Object.defineProperty(req, "body", {
112
+ value: result.data,
113
+ writable: true,
114
+ configurable: true
115
+ });
116
+ }
117
+ return next();
118
+ };
119
+ };
120
+
2
121
  // src/prince.ts
3
122
  class ResponseBuilder {
4
123
  _status = 200;
@@ -36,6 +155,149 @@ class ResponseBuilder {
36
155
  return new Response(this._body, { status: this._status, headers: this._headers });
37
156
  }
38
157
  }
158
+ function zodToJsonSchema(schema) {
159
+ const d = schema._def;
160
+ const typeName = d?.typeName ?? d?.type ?? "";
161
+ if (schema instanceof z.ZodString || typeName === "ZodString") {
162
+ const s = { type: "string" };
163
+ const checks = d.checks ?? [];
164
+ for (const c of checks) {
165
+ if (c.kind === "min")
166
+ s.minLength = c.value;
167
+ if (c.kind === "max")
168
+ s.maxLength = c.value;
169
+ if (c.kind === "email")
170
+ s.format = "email";
171
+ if (c.kind === "url")
172
+ s.format = "uri";
173
+ if (c.kind === "uuid")
174
+ s.format = "uuid";
175
+ if (c.kind === "regex")
176
+ s.pattern = c.regex?.source ?? c.pattern;
177
+ const def = c._zod?.def ?? {};
178
+ if (def.check === "min_length")
179
+ s.minLength = def.minimum;
180
+ if (def.check === "max_length")
181
+ s.maxLength = def.maximum;
182
+ if (def.check === "string_format") {
183
+ if (def.format === "email")
184
+ s.format = "email";
185
+ if (def.format === "url")
186
+ s.format = "uri";
187
+ if (def.format === "uuid")
188
+ s.format = "uuid";
189
+ if (def.format === "regex")
190
+ s.pattern = def.pattern;
191
+ }
192
+ }
193
+ return s;
194
+ }
195
+ if (schema instanceof z.ZodNumber || typeName === "ZodNumber") {
196
+ const s = { type: "number" };
197
+ const checks = d.checks ?? [];
198
+ for (const c of checks) {
199
+ if (c.kind === "min")
200
+ s.minimum = c.value ?? c.minimum;
201
+ if (c.kind === "max")
202
+ s.maximum = c.value ?? c.maximum;
203
+ if (c.kind === "int")
204
+ s.type = "integer";
205
+ if (c.kind === "multipleOf")
206
+ s.multipleOf = c.value;
207
+ const def = c._zod?.def ?? {};
208
+ if (def.check === "greater_than" || def.check === "greater_than_or_equal")
209
+ s.minimum = def.value;
210
+ if (def.check === "less_than" || def.check === "less_than_or_equal")
211
+ s.maximum = def.value;
212
+ if (def.check === "number_format" && (def.format === "safeint" || def.format === "int32" || def.format === "int64"))
213
+ s.type = "integer";
214
+ if (def.check === "multiple_of")
215
+ s.multipleOf = def.value;
216
+ }
217
+ return s;
218
+ }
219
+ if (schema instanceof z.ZodBoolean || typeName === "ZodBoolean")
220
+ return { type: "boolean" };
221
+ if (schema instanceof z.ZodNull || typeName === "ZodNull")
222
+ return { type: "null" };
223
+ if (schema instanceof z.ZodLiteral || typeName === "ZodLiteral") {
224
+ const val = d.value ?? d.values?.[0];
225
+ return { enum: [val] };
226
+ }
227
+ if (schema instanceof z.ZodEnum || typeName === "ZodEnum") {
228
+ const vals = d.values ?? (d.entries ? Object.values(d.entries) : undefined) ?? d.options ?? [];
229
+ return { type: "string", enum: vals };
230
+ }
231
+ if (schema instanceof z.ZodArray || typeName === "ZodArray") {
232
+ const items = d.element ?? d.type ?? d.items;
233
+ return { type: "array", items: items ? zodToJsonSchema(items) : {} };
234
+ }
235
+ if (schema instanceof z.ZodOptional || typeName === "ZodOptional" || schema instanceof z.ZodNullable || typeName === "ZodNullable") {
236
+ return zodToJsonSchema(d.innerType ?? d.type);
237
+ }
238
+ if (schema instanceof z.ZodDefault || typeName === "ZodDefault") {
239
+ const inner = zodToJsonSchema(d.innerType ?? d.type);
240
+ const dv = d.defaultValue ?? d.default;
241
+ return { ...inner, default: typeof dv === "function" ? dv() : dv };
242
+ }
243
+ if (schema instanceof z.ZodObject || typeName === "ZodObject") {
244
+ const shape = d.shape ?? {};
245
+ const properties = {};
246
+ const required = [];
247
+ for (const [key, val] of Object.entries(shape)) {
248
+ const v = val;
249
+ const vd = v._def;
250
+ const vType = vd?.typeName ?? vd?.type ?? "";
251
+ properties[key] = zodToJsonSchema(v);
252
+ const isOptional = v instanceof z.ZodOptional || vType === "ZodOptional" || v instanceof z.ZodDefault || vType === "ZodDefault";
253
+ if (!isOptional)
254
+ required.push(key);
255
+ }
256
+ return { type: "object", properties, ...required.length ? { required } : {} };
257
+ }
258
+ if (schema instanceof z.ZodUnion || typeName === "ZodUnion") {
259
+ return { oneOf: (d.options ?? []).map(zodToJsonSchema) };
260
+ }
261
+ if (schema instanceof z.ZodIntersection || typeName === "ZodIntersection") {
262
+ return { allOf: [d.left, d.right].map(zodToJsonSchema) };
263
+ }
264
+ if (schema instanceof z.ZodRecord || typeName === "ZodRecord") {
265
+ const valueSchema = d.valueType ?? d.valueSchema ?? d.element ?? (d.keyType && !d.valueType ? d.keyType : undefined);
266
+ return { type: "object", additionalProperties: valueSchema ? zodToJsonSchema(valueSchema) : {} };
267
+ }
268
+ return {};
269
+ }
270
+ function zodObjectToQueryParams(schema) {
271
+ const d = schema._def;
272
+ const shape = d.shape ?? {};
273
+ return Object.entries(shape).map(([name, val]) => {
274
+ const v = val;
275
+ const vTypeName = v._def?.typeName ?? v._def?.type ?? "";
276
+ const isOptional = v instanceof z.ZodOptional || vTypeName === "ZodOptional" || v instanceof z.ZodDefault || vTypeName === "ZodDefault";
277
+ return {
278
+ name,
279
+ in: "query",
280
+ required: !isOptional,
281
+ schema: zodToJsonSchema(v)
282
+ };
283
+ });
284
+ }
285
+ function buildResponses(responseSchema, existingResponses) {
286
+ const base = existingResponses ?? {};
287
+ if (!responseSchema)
288
+ return Object.keys(base).length ? base : { 200: { description: "OK" } };
289
+ return {
290
+ 200: {
291
+ description: "OK",
292
+ content: {
293
+ "application/json": {
294
+ schema: zodToJsonSchema(responseSchema)
295
+ }
296
+ }
297
+ },
298
+ ...base
299
+ };
300
+ }
39
301
 
40
302
  class Prince {
41
303
  devMode;
@@ -43,7 +305,7 @@ class Prince {
43
305
  middlewares = [];
44
306
  errorHandler;
45
307
  wsRoutes = {};
46
- openapiData = null;
308
+ openapiSpec = null;
47
309
  router = null;
48
310
  staticRoutes = new Map;
49
311
  staticMiddlewares = new Map;
@@ -383,6 +645,56 @@ class Prince {
383
645
  return this.json({ error: "Internal Server Error" }, 500);
384
646
  }
385
647
  }
648
+ openapi(info, docsPath = "/docs", scalarOptions = {}) {
649
+ const builder = openapi(info);
650
+ this.openapiSpec = builder.spec;
651
+ const toOpenAPIPath = (p) => p.replace(/:([^/]+)/g, "{$1}");
652
+ this.get(docsPath, (req) => {
653
+ return new Response(renderScalarHTML(builder.spec, scalarOptions), { headers: { "Content-Type": "text/html; charset=utf-8" } });
654
+ });
655
+ const jsonPath = docsPath.replace(/\/$/, "") + ".json";
656
+ this.get(jsonPath, (_req) => new Response(JSON.stringify(builder.spec, null, 2), {
657
+ headers: { "Content-Type": "application/json; charset=utf-8" }
658
+ }));
659
+ const self = this;
660
+ builder.route = function(method, path, operation, ...args) {
661
+ const m = method.toUpperCase();
662
+ const { schema, responses: manualResponses, ...operationRest } = operation;
663
+ const middlewares = [];
664
+ if (schema?.body) {
665
+ middlewares.push(validate(schema.body));
666
+ }
667
+ middlewares.push(...args);
668
+ self.add(m, path, middlewares);
669
+ const oaPath = toOpenAPIPath(path);
670
+ if (!builder.spec.paths[oaPath])
671
+ builder.spec.paths[oaPath] = {};
672
+ const pathParams = [...path.matchAll(/:([^/]+)/g)].map(([, name]) => ({
673
+ name,
674
+ in: "path",
675
+ required: true,
676
+ schema: { type: "string" }
677
+ }));
678
+ const queryParams = schema?.query ? zodObjectToQueryParams(schema.query) : [];
679
+ const requestBody = schema?.body ? {
680
+ required: true,
681
+ content: {
682
+ "application/json": {
683
+ schema: zodToJsonSchema(schema.body)
684
+ }
685
+ }
686
+ } : undefined;
687
+ const responses = buildResponses(schema?.response, manualResponses);
688
+ builder.spec.paths[oaPath][m.toLowerCase()] = {
689
+ parameters: [...pathParams, ...queryParams],
690
+ ...requestBody ? { requestBody } : {},
691
+ responses,
692
+ ...operationRest
693
+ };
694
+ return builder;
695
+ };
696
+ return builder;
697
+ }
386
698
  listen(port = 3000) {
387
699
  const self = this;
388
700
  Bun.serve({
@@ -426,6 +738,34 @@ class Prince {
426
738
  console.log(`\uD83D\uDE80 PrinceJS running on http://localhost:${port}`);
427
739
  }
428
740
  }
741
+ function renderScalarHTML(spec, options = {}) {
742
+ const {
743
+ pageTitle = spec.info.title ?? "API Reference",
744
+ theme = "default",
745
+ layout = "modern",
746
+ hideDownloadButton = false,
747
+ customCss = ""
748
+ } = options;
749
+ return `<!doctype html>
750
+ <html lang="en">
751
+ <head>
752
+ <meta charset="utf-8" />
753
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
754
+ <title>${pageTitle}</title>
755
+ ${customCss ? `<style>${customCss}</style>` : ""}
756
+ </head>
757
+ <body>
758
+ <script
759
+ id="api-reference"
760
+ type="application/json"
761
+ data-theme="${theme}"
762
+ data-layout="${layout}"
763
+ ${hideDownloadButton ? 'data-hide-download-button="true"' : ""}
764
+ >${JSON.stringify(spec)}</script>
765
+ <script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
766
+ </body>
767
+ </html>`;
768
+ }
429
769
  var prince = (dev = false) => new Prince(dev);
430
770
  export {
431
771
  prince,
@@ -1,13 +1,90 @@
1
1
  export declare const cron: (pattern: string, task: () => void) => void;
2
- export declare const openapi: (info: {
3
- title: string;
4
- version: string;
5
- }) => {
2
+ export interface OpenAPISpec {
6
3
  openapi: string;
7
4
  info: {
8
5
  title: string;
9
6
  version: string;
7
+ [key: string]: unknown;
10
8
  };
11
- paths: {};
12
- };
9
+ paths: Record<string, unknown>;
10
+ components?: Record<string, unknown>;
11
+ [key: string]: unknown;
12
+ }
13
+ export interface ScalarOptions {
14
+ /** Scalar theme. Options: "default" | "alternate" | "moon" | "purple" | "solarized" | "bluePlanet" | "deepSpace" | "saturn" | "kepler" | "mars" | "none" */
15
+ theme?: string;
16
+ /** Page title shown in the browser tab. Defaults to the spec's info.title. */
17
+ pageTitle?: string;
18
+ /** Layout mode. "modern" (default) or "classic" */
19
+ layout?: "modern" | "classic";
20
+ /** Hide the spec download button. */
21
+ hideDownloadButton?: boolean;
22
+ /** Custom CSS injected into the page. */
23
+ customCss?: string;
24
+ }
25
+ export interface OpenAPIBuilder {
26
+ /** The raw spec object — mutate this to add paths, components, etc. */
27
+ spec: OpenAPISpec;
28
+ /** Backward-compatible direct accessors — mirrors spec.openapi / .info / .paths */
29
+ readonly openapi: string;
30
+ readonly info: OpenAPISpec["info"];
31
+ readonly paths: OpenAPISpec["paths"];
32
+ /**
33
+ * Returns a plain-function route handler that serves the Scalar UI.
34
+ * Mount it on any path in your router.
35
+ *
36
+ * Works with any framework that uses `(req, res) => void` handlers
37
+ * (Node http, Express, Fastify inject, etc.).
38
+ *
39
+ * @example
40
+ * // Plain Node http
41
+ * server.on("request", (req, res) => {
42
+ * if (req.url === "/docs") return api.scalar()(req, res);
43
+ * if (req.url === "/openapi.json") return api.json()(req, res);
44
+ * });
45
+ *
46
+ * @example
47
+ * // Express / Hono-style
48
+ * app.get("/docs", api.scalar({ theme: "moon" }));
49
+ * app.get("/openapi.json", api.json());
50
+ */
51
+ scalar(options?: ScalarOptions): (req: unknown, res: {
52
+ writeHead(status: number, headers: Record<string, string>): void;
53
+ end(body: string): void;
54
+ }) => void;
55
+ /**
56
+ * Returns a route handler that serves the raw OpenAPI spec as JSON.
57
+ *
58
+ * @example
59
+ * app.get("/openapi.json", api.json());
60
+ */
61
+ json(): (req: unknown, res: {
62
+ writeHead(status: number, headers: Record<string, string>): void;
63
+ end(body: string): void;
64
+ }) => void;
65
+ }
66
+ /**
67
+ * Creates an OpenAPI builder with a `.scalar()` and `.json()` route handler,
68
+ * mirroring the Hono / @scalar/hono-api-reference middleware pattern —
69
+ * but framework-agnostic.
70
+ *
71
+ * @example
72
+ * import http from "http";
73
+ * import { openapi } from "./scheduler";
74
+ *
75
+ * const api = openapi({ title: "My API", version: "1.0.0" });
76
+ *
77
+ * api.spec.paths["/hello"] = {
78
+ * get: { summary: "Say hello", responses: { 200: { description: "OK" } } },
79
+ * };
80
+ *
81
+ * http.createServer((req, res) => {
82
+ * if (req.url === "/docs") return api.scalar({ theme: "moon" })(req, res);
83
+ * if (req.url === "/openapi.json") return api.json()(req, res);
84
+ * }).listen(3000);
85
+ */
86
+ export declare const openapi: (info: {
87
+ title: string;
88
+ version: string;
89
+ }) => OpenAPIBuilder;
13
90
  //# sourceMappingURL=scheduler.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"scheduler.d.ts","sourceRoot":"","sources":["../src/scheduler.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,IAAI,GAAI,SAAS,MAAM,EAAE,MAAM,MAAM,IAAI,SAiCrD,CAAC;AAGF,eAAO,MAAM,OAAO,GAAI,MAAM;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE;;;eAAzB,MAAM;iBAAW,MAAM;;;CAE7D,CAAC"}
1
+ {"version":3,"file":"scheduler.d.ts","sourceRoot":"","sources":["../src/scheduler.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,IAAI,GAAI,SAAS,MAAM,EAAE,MAAM,MAAM,IAAI,SAiCrD,CAAC;AAIF,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAC;IACjE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,aAAa;IAC5B,4JAA4J;IAC5J,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,8EAA8E;IAC9E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mDAAmD;IACnD,MAAM,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAC;IAC9B,qCAAqC;IACrC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,yCAAyC;IACzC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC7B,uEAAuE;IACvE,IAAI,EAAE,WAAW,CAAC;IAElB,mFAAmF;IACnF,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACnC,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IAErC;;;;;;;;;;;;;;;;;;OAkBG;IACH,MAAM,CAAC,OAAO,CAAC,EAAE,aAAa,GAAG,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE;QACnD,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;QACjE,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;KACzB,KAAK,IAAI,CAAC;IAEX;;;;;OAKG;IACH,IAAI,IAAI,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE;QAC1B,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;QACjE,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;KACzB,KAAK,IAAI,CAAC;CACZ;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,OAAO,GAAI,MAAM;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,KAAG,cAyDlE,CAAC"}
package/dist/scheduler.js CHANGED
@@ -23,7 +23,60 @@ var cron = (pattern, task) => {
23
23
  setInterval(check, 60000);
24
24
  };
25
25
  var openapi = (info) => {
26
- return { openapi: "3.0.0", info, paths: {} };
26
+ const spec = { openapi: "3.0.0", info, paths: {} };
27
+ const renderScalarHtml = (options = {}) => {
28
+ const {
29
+ pageTitle = spec.info.title,
30
+ theme = "default",
31
+ layout = "modern",
32
+ hideDownloadButton = false,
33
+ customCss = ""
34
+ } = options;
35
+ return `<!doctype html>
36
+ <html lang="en">
37
+ <head>
38
+ <meta charset="utf-8" />
39
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
40
+ <title>${pageTitle}</title>
41
+ ${customCss ? `<style>${customCss}</style>` : ""}
42
+ </head>
43
+ <body>
44
+ <script
45
+ id="api-reference"
46
+ type="application/json"
47
+ data-theme="${theme}"
48
+ data-layout="${layout}"
49
+ ${hideDownloadButton ? 'data-hide-download-button="true"' : ""}
50
+ >${JSON.stringify(spec)}</script>
51
+ <script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
52
+ </body>
53
+ </html>`;
54
+ };
55
+ return {
56
+ spec,
57
+ get openapi() {
58
+ return spec.openapi;
59
+ },
60
+ get info() {
61
+ return spec.info;
62
+ },
63
+ get paths() {
64
+ return spec.paths;
65
+ },
66
+ scalar(options = {}) {
67
+ return (_req, res) => {
68
+ const html = renderScalarHtml(options);
69
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
70
+ res.end(html);
71
+ };
72
+ },
73
+ json() {
74
+ return (_req, res) => {
75
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
76
+ res.end(JSON.stringify(spec, null, 2));
77
+ };
78
+ }
79
+ };
27
80
  };
28
81
  export {
29
82
  openapi,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "princejs",
3
- "version": "1.9.2",
3
+ "version": "1.9.4",
4
4
  "description": "An easy and fast backend framework that is among the top three — by a 13yo developer, for developers.",
5
5
  "main": "dist/prince.js",
6
6
  "types": "dist/prince.d.ts",
@@ -111,7 +111,7 @@
111
111
  "jose": "^6.1.2"
112
112
  },
113
113
  "scripts": {
114
- "build:js": "bun build src/prince.ts --outdir dist --target bun && bun build src/middleware.ts --outdir dist --target bun && bun build src/helpers.ts --outdir dist --target bun && bun build src/scheduler.ts --outdir dist --target bun && bun build bin/create.ts --outdir dist --target bun && bun build src/jsx.ts --outdir dist --target bun && bun build src/db.ts --outdir dist --target bun --format esm && bun build src/client.ts --outdir dist --format esm && bun build src/adapters/vercel.ts --outdir dist/adapters --format esm && bun build src/adapters/cloudflare.ts --outdir dist/adapters --format esm && bun build src/adapters/deno.ts --outdir dist/adapters --format esm",
114
+ "build:js": "bun build src/prince.ts --outdir dist --target bun --external zod --external jose && bun build src/middleware.ts --outdir dist --target bun && bun build src/helpers.ts --outdir dist --target bun && bun build src/scheduler.ts --outdir dist --target bun && bun build bin/create.ts --outdir dist --target bun && bun build src/jsx.ts --outdir dist --target bun && bun build src/db.ts --outdir dist --target bun --format esm && bun build src/client.ts --outdir dist --format esm && bun build src/adapters/vercel.ts --outdir dist/adapters --format esm && bun build src/adapters/cloudflare.ts --outdir dist/adapters --format esm && bun build src/adapters/deno.ts --outdir dist/adapters --format esm",
115
115
  "build:types": "tsc --emitDeclarationOnly --skipLibCheck",
116
116
  "build": "bun run build:js && bun run build:types",
117
117
  "test": "bun test",