peta-docs 0.3.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/README.md +350 -0
- package/dist/hono/index.d.mts +102 -0
- package/dist/hono/index.mjs +435 -0
- package/dist/index.d.mts +30 -0
- package/dist/index.mjs +38 -0
- package/dist/scanner-CU4MsJ2G.d.mts +155 -0
- package/dist/spec-Bobwv7gP.mjs +281 -0
- package/package.json +53 -0
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import { n as setDefaultScanner } from "../spec-Bobwv7gP.mjs";
|
|
2
|
+
import { validator } from "hono/validator";
|
|
3
|
+
import { readdirSync, statSync } from "node:fs";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { Hono } from "hono";
|
|
6
|
+
//#region src/hono/route.ts
|
|
7
|
+
const OPENAPI_META = Symbol("openapi-meta");
|
|
8
|
+
/** Parse a comma-separated string into a trimmed, non-empty array. */
|
|
9
|
+
function parseCommaSeparated(value) {
|
|
10
|
+
return value ? value.split(",").map((s) => s.trim()).filter(Boolean) : [];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Validate a value against a Standard Schema.
|
|
14
|
+
* Returns the validated value, or calls `onError` and returns a Response.
|
|
15
|
+
*/
|
|
16
|
+
async function validateOrError(schema, value, c, onError) {
|
|
17
|
+
const result = await schema["~standard"].validate(value);
|
|
18
|
+
if (Array.isArray(result)) return onError(result, c);
|
|
19
|
+
const r = result;
|
|
20
|
+
if (r.issues) return onError([...r.issues], c);
|
|
21
|
+
if ("value" in r && r.value !== void 0) return r.value;
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
let onValidationError = (issues, c) => {
|
|
25
|
+
return c.json({
|
|
26
|
+
error: "Validation failed",
|
|
27
|
+
issues
|
|
28
|
+
}, 400);
|
|
29
|
+
};
|
|
30
|
+
/** @deprecated Use {@link RouteBuilder.onValidationError} on the route chain instead. Per-route handlers take precedence over the global handler. */
|
|
31
|
+
function setOnValidationError(handler) {
|
|
32
|
+
const prev = onValidationError;
|
|
33
|
+
onValidationError = handler;
|
|
34
|
+
return () => {
|
|
35
|
+
onValidationError = prev;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const onResponseValidationError = (issues, c) => {
|
|
39
|
+
return c.json({
|
|
40
|
+
error: "Response validation failed",
|
|
41
|
+
issues
|
|
42
|
+
}, 500);
|
|
43
|
+
};
|
|
44
|
+
/** @internal */
|
|
45
|
+
function createValidator(target, schema, onError) {
|
|
46
|
+
return validator(target, async (value, c) => {
|
|
47
|
+
const validated = await validateOrError(schema, value, c, onError);
|
|
48
|
+
if (validated instanceof Response) return validated;
|
|
49
|
+
return validated;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Creates a middleware that checks for the presence of auth credentials.
|
|
54
|
+
*
|
|
55
|
+
* This is a mechanism-level guard — it verifies that the expected auth
|
|
56
|
+
* header/cookie exists, but does NOT verify the credential itself
|
|
57
|
+
* (e.g., JWT validity, session data). The app's own middleware or
|
|
58
|
+
* handler is responsible for identity verification.
|
|
59
|
+
*/
|
|
60
|
+
function authGuard(schemes) {
|
|
61
|
+
return async (c, next) => {
|
|
62
|
+
for (const scheme of schemes) {
|
|
63
|
+
if (scheme === "bearerAuth") {
|
|
64
|
+
if (!c.req.header("Authorization")?.startsWith("Bearer ")) return c.json({ error: "unauthorized" }, 401);
|
|
65
|
+
}
|
|
66
|
+
if (scheme === "sessionAuth" || scheme === "cookieAuth") {
|
|
67
|
+
if (!c.req.header("Cookie")?.includes("session=")) return c.json({ error: "unauthorized" }, 401);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return await next();
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
var RouteBuilder = class RouteBuilder {
|
|
74
|
+
_config = { responses: {} };
|
|
75
|
+
static VALIDATOR_MAP = [
|
|
76
|
+
["requestBody", "json"],
|
|
77
|
+
["params", "param"],
|
|
78
|
+
["headers", "header"]
|
|
79
|
+
];
|
|
80
|
+
summary(s) {
|
|
81
|
+
this._config.summary = s;
|
|
82
|
+
return this;
|
|
83
|
+
}
|
|
84
|
+
description(s) {
|
|
85
|
+
this._config.description = s;
|
|
86
|
+
return this;
|
|
87
|
+
}
|
|
88
|
+
operationId(s) {
|
|
89
|
+
this._config.operationId = s;
|
|
90
|
+
return this;
|
|
91
|
+
}
|
|
92
|
+
tags(...t) {
|
|
93
|
+
this._config.tags = t;
|
|
94
|
+
return this;
|
|
95
|
+
}
|
|
96
|
+
deprecated(d = true) {
|
|
97
|
+
this._config.deprecated = d;
|
|
98
|
+
return this;
|
|
99
|
+
}
|
|
100
|
+
requestBody(schema) {
|
|
101
|
+
this._config.requestBody = schema;
|
|
102
|
+
return this;
|
|
103
|
+
}
|
|
104
|
+
query(schema) {
|
|
105
|
+
this._config.query = schema;
|
|
106
|
+
return this;
|
|
107
|
+
}
|
|
108
|
+
params(schema) {
|
|
109
|
+
this._config.params = schema;
|
|
110
|
+
return this;
|
|
111
|
+
}
|
|
112
|
+
headers(schema) {
|
|
113
|
+
this._config.headers = schema;
|
|
114
|
+
return this;
|
|
115
|
+
}
|
|
116
|
+
response(status, value) {
|
|
117
|
+
this._config.responses[String(status)] = value;
|
|
118
|
+
return this;
|
|
119
|
+
}
|
|
120
|
+
auth(scheme = "bearerAuth") {
|
|
121
|
+
const acc = this._config.security ?? [];
|
|
122
|
+
acc.push(scheme);
|
|
123
|
+
this._config.security = acc;
|
|
124
|
+
return this;
|
|
125
|
+
}
|
|
126
|
+
filter(name, schema, options) {
|
|
127
|
+
const acc = this._config.filters ?? [];
|
|
128
|
+
acc.push({
|
|
129
|
+
name,
|
|
130
|
+
schema,
|
|
131
|
+
operators: options?.operators ?? ["eq"]
|
|
132
|
+
});
|
|
133
|
+
this._config.filters = acc;
|
|
134
|
+
return this;
|
|
135
|
+
}
|
|
136
|
+
sort(fields) {
|
|
137
|
+
this._config.sort = fields;
|
|
138
|
+
return this;
|
|
139
|
+
}
|
|
140
|
+
include(relations) {
|
|
141
|
+
this._config.include = relations;
|
|
142
|
+
return this;
|
|
143
|
+
}
|
|
144
|
+
fieldsets(resources) {
|
|
145
|
+
this._config.fieldsets = resources;
|
|
146
|
+
return this;
|
|
147
|
+
}
|
|
148
|
+
onValidationError(handler) {
|
|
149
|
+
this._config.onValidationError = handler;
|
|
150
|
+
return this;
|
|
151
|
+
}
|
|
152
|
+
onResponseValidationError(handler) {
|
|
153
|
+
this._config.onResponseValidationError = handler;
|
|
154
|
+
return this;
|
|
155
|
+
}
|
|
156
|
+
paginated(options) {
|
|
157
|
+
this._config.pagination = {
|
|
158
|
+
maxLimit: options?.maxLimit ?? 100,
|
|
159
|
+
defaultLimit: options?.defaultLimit ?? 20
|
|
160
|
+
};
|
|
161
|
+
return this;
|
|
162
|
+
}
|
|
163
|
+
handle(handler) {
|
|
164
|
+
const onError = this._config.onValidationError ?? onValidationError;
|
|
165
|
+
const validators = this.buildValidators(onError);
|
|
166
|
+
if (this._config.security?.length) validators.unshift(authGuard(this._config.security));
|
|
167
|
+
const routeConfig = this.buildRouteConfig(handler);
|
|
168
|
+
const wrapped = this.composeHandler(validators, handler);
|
|
169
|
+
return this.attachRouteMeta(wrapped, routeConfig);
|
|
170
|
+
}
|
|
171
|
+
buildValidators(onError) {
|
|
172
|
+
const validators = [];
|
|
173
|
+
const querySchema = this._config.query;
|
|
174
|
+
const filters = this._config.filters;
|
|
175
|
+
const sortFields = this._config.sort;
|
|
176
|
+
const includeFields = this._config.include;
|
|
177
|
+
const fieldsetResources = this._config.fieldsets;
|
|
178
|
+
const pag = this._config.pagination;
|
|
179
|
+
if (querySchema || filters?.length || sortFields || includeFields || fieldsetResources || pag) validators.push(validator("query", async (value, c) => {
|
|
180
|
+
let merged = {};
|
|
181
|
+
const raw = value;
|
|
182
|
+
if (querySchema) {
|
|
183
|
+
const validated = await validateOrError(querySchema, value, c, onError);
|
|
184
|
+
if (validated instanceof Response) return validated;
|
|
185
|
+
merged = {
|
|
186
|
+
...merged,
|
|
187
|
+
...validated
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
if (filters) for (const filter of filters) for (const op of filter.operators) {
|
|
191
|
+
const paramName = op === "eq" ? filter.name : `${filter.name}__${op}`;
|
|
192
|
+
const val = raw[paramName];
|
|
193
|
+
if (val === void 0) continue;
|
|
194
|
+
if (op === "in") {
|
|
195
|
+
const items = parseCommaSeparated(val);
|
|
196
|
+
const validatedItems = [];
|
|
197
|
+
for (const item of items) {
|
|
198
|
+
const validated = await validateOrError(filter.schema, item, c, onError);
|
|
199
|
+
if (validated instanceof Response) return validated;
|
|
200
|
+
validatedItems.push(validated);
|
|
201
|
+
}
|
|
202
|
+
merged[paramName] = validatedItems;
|
|
203
|
+
} else {
|
|
204
|
+
const validated = await validateOrError(filter.schema, val, c, onError);
|
|
205
|
+
if (validated instanceof Response) return validated;
|
|
206
|
+
merged[paramName] = validated;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (sortFields) {
|
|
210
|
+
const sortVal = raw.sort;
|
|
211
|
+
if (sortVal !== void 0) {
|
|
212
|
+
const parts = parseCommaSeparated(sortVal);
|
|
213
|
+
const allowed = new Set(sortFields.flatMap((f) => [f, `-${f}`]));
|
|
214
|
+
for (const part of parts) if (!allowed.has(part)) return onError([{ message: `Invalid sort field "${part}". Allowed: ${[...allowed].join(", ")}` }], c);
|
|
215
|
+
merged.sort = parts;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const includeFields = this._config.include;
|
|
219
|
+
if (includeFields) {
|
|
220
|
+
const rawInclude = raw.include;
|
|
221
|
+
if (rawInclude !== void 0) {
|
|
222
|
+
const parts = parseCommaSeparated(rawInclude);
|
|
223
|
+
const allowed = new Set(includeFields);
|
|
224
|
+
for (const part of parts) if (!allowed.has(part)) return onError([{ message: `Invalid include "${part}". Allowed: ${[...allowed].join(", ")}` }], c);
|
|
225
|
+
merged.include = parts;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const fieldsetResources = this._config.fieldsets;
|
|
229
|
+
if (fieldsetResources) for (const resource of fieldsetResources) {
|
|
230
|
+
const paramName = `fields[${resource}]`;
|
|
231
|
+
const val = raw[paramName];
|
|
232
|
+
if (val !== void 0 && typeof val !== "string") return onError([{ message: `"${paramName}" must be a string` }], c);
|
|
233
|
+
if (val !== void 0) merged[paramName] = val;
|
|
234
|
+
}
|
|
235
|
+
if (pag) {
|
|
236
|
+
const page = Math.floor(Number(raw.page ?? "1"));
|
|
237
|
+
const limit = Math.min(pag.maxLimit, Math.max(1, Math.floor(Number(raw.limit ?? String(pag.defaultLimit)))));
|
|
238
|
+
merged.page = Number.isFinite(page) && page >= 1 ? page : 1;
|
|
239
|
+
merged.limit = Number.isFinite(limit) ? limit : pag.defaultLimit;
|
|
240
|
+
merged.offset = (merged.page - 1) * merged.limit;
|
|
241
|
+
}
|
|
242
|
+
return merged;
|
|
243
|
+
}));
|
|
244
|
+
for (const [key, target] of RouteBuilder.VALIDATOR_MAP) {
|
|
245
|
+
const schema = this._config[key];
|
|
246
|
+
if (schema != null) validators.push(createValidator(target, schema, onError));
|
|
247
|
+
}
|
|
248
|
+
return validators;
|
|
249
|
+
}
|
|
250
|
+
buildRouteConfig(handler) {
|
|
251
|
+
return {
|
|
252
|
+
...this._config,
|
|
253
|
+
responses: this._config.responses,
|
|
254
|
+
pagination: this._config.pagination,
|
|
255
|
+
filters: this._config.filters,
|
|
256
|
+
sort: this._config.sort,
|
|
257
|
+
include: this._config.include,
|
|
258
|
+
fieldsets: this._config.fieldsets,
|
|
259
|
+
security: this._config.security,
|
|
260
|
+
handler
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
composeHandler(validators, handler) {
|
|
264
|
+
return async (c, _next) => {
|
|
265
|
+
const run = async (i) => {
|
|
266
|
+
if (i < validators.length) return await validators[i](c, run.bind(null, i + 1)) ?? void 0;
|
|
267
|
+
const response = await handler(c) ?? void 0;
|
|
268
|
+
if (response) {
|
|
269
|
+
const onError = this._config.onResponseValidationError ?? onResponseValidationError;
|
|
270
|
+
return await this.validateResponse(response, c, onError) ?? void 0;
|
|
271
|
+
}
|
|
272
|
+
return response;
|
|
273
|
+
};
|
|
274
|
+
return run(0);
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
async validateResponse(response, c, onError) {
|
|
278
|
+
if (!(response.headers.get("content-type") ?? "").includes("application/json")) return response;
|
|
279
|
+
if (response.status === 204) return response;
|
|
280
|
+
const schema = this._config.responses[String(response.status)];
|
|
281
|
+
if (!schema || typeof schema !== "object" && typeof schema !== "function" || !("~standard" in schema)) return response;
|
|
282
|
+
try {
|
|
283
|
+
const validated = await validateOrError(schema, await response.clone().json(), c, onError);
|
|
284
|
+
if (validated instanceof Response) return validated;
|
|
285
|
+
return response;
|
|
286
|
+
} catch {
|
|
287
|
+
return response;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
attachRouteMeta(handler, config) {
|
|
291
|
+
Object.defineProperty(handler, OPENAPI_META, {
|
|
292
|
+
value: config,
|
|
293
|
+
writable: false
|
|
294
|
+
});
|
|
295
|
+
return handler;
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
function route() {
|
|
299
|
+
return new RouteBuilder();
|
|
300
|
+
}
|
|
301
|
+
function getRouteMeta(handler) {
|
|
302
|
+
if (typeof handler !== "function") return void 0;
|
|
303
|
+
return handler[OPENAPI_META];
|
|
304
|
+
}
|
|
305
|
+
//#endregion
|
|
306
|
+
//#region src/hono/scanner.ts
|
|
307
|
+
function routeProp(obj, key) {
|
|
308
|
+
if (obj == null) return void 0;
|
|
309
|
+
return obj[key];
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Scans a Hono app instance and extracts route metadata.
|
|
313
|
+
*
|
|
314
|
+
* Relies on Hono's internal `app.routes` array. If that structure
|
|
315
|
+
* changes in a future Hono version, this scanner will warn and
|
|
316
|
+
* return an empty array.
|
|
317
|
+
*/
|
|
318
|
+
const honoScanner = { scan(app) {
|
|
319
|
+
const entries = [];
|
|
320
|
+
const raw = routeProp(app, "routes");
|
|
321
|
+
if (!Array.isArray(raw)) {
|
|
322
|
+
console.warn(`[peta-docs] honoScanner: expected app.routes to be an array, got ${typeof raw}. Is this a Hono app? Provide a custom RouteScanner for other frameworks.`);
|
|
323
|
+
return entries;
|
|
324
|
+
}
|
|
325
|
+
for (const r of raw) {
|
|
326
|
+
const handler = routeProp(r, "handler");
|
|
327
|
+
const meta = typeof handler === "function" ? getRouteMeta(handler) : void 0;
|
|
328
|
+
if (meta) {
|
|
329
|
+
const path = typeof routeProp(r, "path") === "string" ? String(routeProp(r, "path")) : "";
|
|
330
|
+
const method = typeof routeProp(r, "method") === "string" ? String(routeProp(r, "method")) : "";
|
|
331
|
+
entries.push({
|
|
332
|
+
path,
|
|
333
|
+
method,
|
|
334
|
+
config: meta
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return entries;
|
|
339
|
+
} };
|
|
340
|
+
//#endregion
|
|
341
|
+
//#region src/hono/loader.ts
|
|
342
|
+
/**
|
|
343
|
+
* Convert a directory name to a URL path segment.
|
|
344
|
+
* `[id]` → `:id`
|
|
345
|
+
* `[postId]` → `:postId`
|
|
346
|
+
* `pets` → `pets`
|
|
347
|
+
*/
|
|
348
|
+
function toPathSegment(entry) {
|
|
349
|
+
const match = entry.match(/^\[(\w+)\]$/);
|
|
350
|
+
return match ? `:${match[1]}` : entry;
|
|
351
|
+
}
|
|
352
|
+
function hasIndex(dir) {
|
|
353
|
+
try {
|
|
354
|
+
return statSync(join(dir, "index.ts")).isFile();
|
|
355
|
+
} catch {
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
async function walkDir(parentRouter, dir, accumulatedPath) {
|
|
360
|
+
let entries;
|
|
361
|
+
try {
|
|
362
|
+
entries = readdirSync(dir);
|
|
363
|
+
} catch {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
for (const entry of entries) {
|
|
367
|
+
const fullPath = join(dir, entry);
|
|
368
|
+
if (!statSync(fullPath).isDirectory()) continue;
|
|
369
|
+
if (entry.startsWith(".") || entry === "node_modules") continue;
|
|
370
|
+
const segment = toPathSegment(entry);
|
|
371
|
+
if (hasIndex(fullPath)) try {
|
|
372
|
+
const mod = await import(join(fullPath, "index.ts"));
|
|
373
|
+
const router = mod.default ?? mod.routes ?? mod.router;
|
|
374
|
+
const honoRouter = router instanceof Hono ? router : typeof router === "function" ? router() : null;
|
|
375
|
+
if (honoRouter) {
|
|
376
|
+
await walkDir(honoRouter, fullPath, "");
|
|
377
|
+
const mountPath = accumulatedPath ? `${accumulatedPath}/${segment}` : `/${segment}`;
|
|
378
|
+
parentRouter.route(mountPath, honoRouter);
|
|
379
|
+
}
|
|
380
|
+
} catch (err) {
|
|
381
|
+
console.warn(`[peta-docs] could not load routes from "${entry}": ${err instanceof Error ? err.message : err}`);
|
|
382
|
+
}
|
|
383
|
+
else await walkDir(parentRouter, fullPath, accumulatedPath ? `${accumulatedPath}/${segment}` : `/${segment}`);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Load routes from a directory tree. Each subdirectory with an `index.ts`
|
|
388
|
+
* exporting a Hono instance (default export) is mounted as a sub-router.
|
|
389
|
+
*
|
|
390
|
+
* Directories named `[param]` are converted to `:param` path segments for
|
|
391
|
+
* dynamic routing. Directories without `index.ts` accumulate their path
|
|
392
|
+
* until a child directory with `index.ts` is found (gap pattern).
|
|
393
|
+
*
|
|
394
|
+
* Convention:
|
|
395
|
+
* routes/
|
|
396
|
+
* pets/
|
|
397
|
+
* index.ts mounted at {basePath}/pets
|
|
398
|
+
* [id]/
|
|
399
|
+
* index.ts mounted at {basePath}/pets/:id
|
|
400
|
+
* comments/
|
|
401
|
+
* index.ts mounted at {basePath}/pets/:id/comments
|
|
402
|
+
* species/
|
|
403
|
+
* index.ts mounted at {basePath}/species
|
|
404
|
+
*
|
|
405
|
+
* @param app Hono application to mount routes on
|
|
406
|
+
* @param dir Path to the routes directory
|
|
407
|
+
* @param options.basePath URL prefix (default "/api")
|
|
408
|
+
*/
|
|
409
|
+
async function loadRoutes(app, dir, options) {
|
|
410
|
+
const basePath = (options?.basePath ?? "/api").replace(/\/+$/, "");
|
|
411
|
+
const resolvedDir = resolve(dir);
|
|
412
|
+
try {
|
|
413
|
+
readdirSync(resolvedDir);
|
|
414
|
+
} catch {
|
|
415
|
+
console.warn(`[peta-docs] could not read routes directory: ${dir}`);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
await walkDir(app, resolvedDir, basePath);
|
|
419
|
+
}
|
|
420
|
+
//#endregion
|
|
421
|
+
//#region src/hono/index.ts
|
|
422
|
+
/**
|
|
423
|
+
* peta-docs/hono — Hono framework adapter.
|
|
424
|
+
*
|
|
425
|
+
* Provides the RouteBuilder fluent API, a Hono route scanner,
|
|
426
|
+
* and a filesystem-based route loader.
|
|
427
|
+
*
|
|
428
|
+
* Importing this module also registers the Hono scanner as the
|
|
429
|
+
* default scanner for `getOpenAPISpec()`.
|
|
430
|
+
*
|
|
431
|
+
* @module
|
|
432
|
+
*/
|
|
433
|
+
setDefaultScanner(honoScanner);
|
|
434
|
+
//#endregion
|
|
435
|
+
export { RouteBuilder, getRouteMeta, honoScanner, loadRoutes, route, setOnValidationError };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { a as FilterFields, c as OpenAPIObject, d as ResponseValue, f as RouteConfig, g as TypedContext, h as StatusCode, i as FilterDef, l as Pagination, m as SchemaObject, n as ArkTypeSchema, o as FilterOperator, p as RouteEntry, r as FieldsetParams, s as InfoObject, t as RouteScanner, u as PathItemObject } from "./scanner-CU4MsJ2G.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/scalar.d.ts
|
|
4
|
+
interface ScalarUIOptions {
|
|
5
|
+
specUrl: string;
|
|
6
|
+
title?: string;
|
|
7
|
+
theme?: string;
|
|
8
|
+
showSidebar?: boolean;
|
|
9
|
+
cdnUrl?: string;
|
|
10
|
+
}
|
|
11
|
+
declare function serveScalarUI(options: ScalarUIOptions): (c: {
|
|
12
|
+
html(html: string): Response | Promise<Response>;
|
|
13
|
+
}) => Response | Promise<Response>;
|
|
14
|
+
//#endregion
|
|
15
|
+
//#region src/spec/builder.d.ts
|
|
16
|
+
declare function buildOpenAPISpec(routes: RouteEntry[], info: InfoObject, options?: {
|
|
17
|
+
basePath?: string;
|
|
18
|
+
components?: Record<string, unknown>;
|
|
19
|
+
}): OpenAPIObject;
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/spec/schema.d.ts
|
|
22
|
+
declare function toOpenAPISchema(schema: unknown): SchemaObject;
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region src/spec.d.ts
|
|
25
|
+
declare function getOpenAPISpec(app: unknown, info: InfoObject, scanner?: RouteScanner, options?: {
|
|
26
|
+
basePath?: string;
|
|
27
|
+
components?: Record<string, unknown>;
|
|
28
|
+
}): OpenAPIObject;
|
|
29
|
+
//#endregion
|
|
30
|
+
export { type ArkTypeSchema, type FieldsetParams, type FilterDef, type FilterFields, type FilterOperator, type InfoObject, type OpenAPIObject, type Pagination, type PathItemObject, type ResponseValue, type RouteConfig, type RouteEntry, type RouteScanner, type ScalarUIOptions, type SchemaObject, type StatusCode, type TypedContext, buildOpenAPISpec, getOpenAPISpec, serveScalarUI, toOpenAPISchema };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { i as toOpenAPISchema, r as buildOpenAPISpec, t as getOpenAPISpec } from "./spec-Bobwv7gP.mjs";
|
|
2
|
+
//#region src/scalar.ts
|
|
3
|
+
function escapeHtml(str) {
|
|
4
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
5
|
+
}
|
|
6
|
+
function generateScalarHTML(options) {
|
|
7
|
+
const specUrl = escapeHtml(options.specUrl);
|
|
8
|
+
const title = options.title ?? "API Reference";
|
|
9
|
+
const theme = options.theme ?? "purple";
|
|
10
|
+
const showSidebar = options.showSidebar ?? true;
|
|
11
|
+
const cdnUrl = options.cdnUrl ?? "https://cdn.jsdelivr.net/npm/@scalar/api-reference";
|
|
12
|
+
const config = escapeHtml(JSON.stringify({
|
|
13
|
+
theme,
|
|
14
|
+
showSidebar
|
|
15
|
+
}));
|
|
16
|
+
return `<!doctype html>
|
|
17
|
+
<html lang="en">
|
|
18
|
+
<head>
|
|
19
|
+
<meta charset="UTF-8" />
|
|
20
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
21
|
+
<title>${escapeHtml(title)}</title>
|
|
22
|
+
<style>
|
|
23
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
24
|
+
html, body { height: 100%; }
|
|
25
|
+
</style>
|
|
26
|
+
</head>
|
|
27
|
+
<body>
|
|
28
|
+
<div id="api-reference" data-url="${specUrl}" data-configuration="${config}"></div>
|
|
29
|
+
<script src="${escapeHtml(cdnUrl)}" crossorigin><\/script>
|
|
30
|
+
</body>
|
|
31
|
+
</html>`;
|
|
32
|
+
}
|
|
33
|
+
function serveScalarUI(options) {
|
|
34
|
+
const html = generateScalarHTML(options);
|
|
35
|
+
return (c) => c.html(html);
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
38
|
+
export { buildOpenAPISpec, getOpenAPISpec, serveScalarUI, toOpenAPISchema };
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
//#region src/types.d.ts
|
|
2
|
+
type ArkTypeResult = {
|
|
3
|
+
issues?: Iterable<unknown>;
|
|
4
|
+
value?: unknown;
|
|
5
|
+
} | Iterable<unknown>;
|
|
6
|
+
interface ArkTypeSchema {
|
|
7
|
+
toJsonSchema(): unknown;
|
|
8
|
+
infer: unknown;
|
|
9
|
+
"~standard": {
|
|
10
|
+
validate: (v: unknown) => ArkTypeResult | Promise<ArkTypeResult>;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
type Pagination = {
|
|
14
|
+
page: number;
|
|
15
|
+
limit: number;
|
|
16
|
+
offset: number;
|
|
17
|
+
};
|
|
18
|
+
type FilterOperator = "eq" | "ne" | "gte" | "gt" | "lte" | "lt" | "contains" | "startsWith" | "endsWith" | "in";
|
|
19
|
+
interface FilterDef {
|
|
20
|
+
name: string;
|
|
21
|
+
schema: ArkTypeSchema;
|
|
22
|
+
operators: FilterOperator[];
|
|
23
|
+
}
|
|
24
|
+
type FilterFields<N extends string, S extends ArkTypeSchema, O extends FilterOperator[]> = ("eq" extends O[number] ? { [K in N]?: S["infer"] } : Record<string, unknown>) & { [K in `${N}__${Extract<O[number], Exclude<FilterOperator, "eq">>}`]?: S["infer"] };
|
|
25
|
+
type FieldsetParams<R extends string[]> = { [K in R[number] as `fields[${K}]`]?: string };
|
|
26
|
+
type HonoContext = import("hono").Context;
|
|
27
|
+
type TypedContext<B, Q, P, Hd, Pg extends Pagination | undefined = undefined, F = Record<string, unknown>, Sr = Record<string, unknown>, Ir = Record<string, unknown>, Fs = Record<string, unknown>> = Omit<HonoContext, "req"> & {
|
|
28
|
+
req: Omit<HonoContext["req"], "valid"> & {
|
|
29
|
+
valid: {
|
|
30
|
+
(type: "json"): [B] extends [ArkTypeSchema] ? B["infer"] : never;
|
|
31
|
+
(type: "query"): [Q] extends [ArkTypeSchema] ? Pg extends Pagination ? Q["infer"] & Pg & F & Sr & Ir & Fs : Q["infer"] & F & Sr & Ir & Fs : Pg extends Pagination ? Pg & F & Sr & Ir & Fs : F & Sr & Ir & Fs;
|
|
32
|
+
(type: "param"): [P] extends [ArkTypeSchema] ? P["infer"] : never;
|
|
33
|
+
(type: "header"): [Hd] extends [ArkTypeSchema] ? Hd["infer"] : never;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
type SchemaObject = Record<string, unknown>;
|
|
38
|
+
interface InfoObject {
|
|
39
|
+
title: string;
|
|
40
|
+
version: string;
|
|
41
|
+
description?: string;
|
|
42
|
+
summary?: string;
|
|
43
|
+
}
|
|
44
|
+
interface ServerObject {
|
|
45
|
+
url: string;
|
|
46
|
+
description?: string;
|
|
47
|
+
}
|
|
48
|
+
interface ParameterObject {
|
|
49
|
+
name: string;
|
|
50
|
+
in: "query" | "path" | "header" | "cookie";
|
|
51
|
+
description?: string;
|
|
52
|
+
required?: boolean;
|
|
53
|
+
deprecated?: boolean;
|
|
54
|
+
schema: SchemaObject;
|
|
55
|
+
}
|
|
56
|
+
interface MediaTypeObject {
|
|
57
|
+
schema: SchemaObject;
|
|
58
|
+
}
|
|
59
|
+
interface RequestBodyObject {
|
|
60
|
+
description?: string;
|
|
61
|
+
required?: boolean;
|
|
62
|
+
content: Record<string, MediaTypeObject>;
|
|
63
|
+
}
|
|
64
|
+
interface ResponseObject {
|
|
65
|
+
description: string;
|
|
66
|
+
content?: Record<string, MediaTypeObject>;
|
|
67
|
+
}
|
|
68
|
+
type ResponsesObject = Record<string, ResponseObject>;
|
|
69
|
+
interface OperationObject {
|
|
70
|
+
summary?: string;
|
|
71
|
+
description?: string;
|
|
72
|
+
operationId?: string;
|
|
73
|
+
tags?: string[];
|
|
74
|
+
parameters?: ParameterObject[];
|
|
75
|
+
requestBody?: RequestBodyObject;
|
|
76
|
+
responses?: ResponsesObject;
|
|
77
|
+
deprecated?: boolean;
|
|
78
|
+
security?: Record<string, string[]>[];
|
|
79
|
+
}
|
|
80
|
+
interface PathItemObject {
|
|
81
|
+
summary?: string;
|
|
82
|
+
description?: string;
|
|
83
|
+
get?: OperationObject;
|
|
84
|
+
post?: OperationObject;
|
|
85
|
+
put?: OperationObject;
|
|
86
|
+
delete?: OperationObject;
|
|
87
|
+
patch?: OperationObject;
|
|
88
|
+
parameters?: ParameterObject[];
|
|
89
|
+
}
|
|
90
|
+
type PathsObject = Record<string, PathItemObject>;
|
|
91
|
+
interface OpenAPIObject {
|
|
92
|
+
openapi: string;
|
|
93
|
+
info: InfoObject;
|
|
94
|
+
jsonSchemaDialect?: string;
|
|
95
|
+
servers?: ServerObject[];
|
|
96
|
+
paths?: PathsObject;
|
|
97
|
+
components?: Record<string, unknown>;
|
|
98
|
+
security?: Record<string, string[]>[];
|
|
99
|
+
tags?: {
|
|
100
|
+
name: string;
|
|
101
|
+
description?: string;
|
|
102
|
+
}[];
|
|
103
|
+
}
|
|
104
|
+
type StatusCode = "200" | "201" | "202" | "204" | "301" | "304" | "400" | "401" | "403" | "404" | "405" | "409" | "422" | "429" | "500" | "502" | "503" | (string & {});
|
|
105
|
+
type ResponseValue = string | {
|
|
106
|
+
description?: string;
|
|
107
|
+
content?: Record<string, {
|
|
108
|
+
schema: unknown;
|
|
109
|
+
}>;
|
|
110
|
+
};
|
|
111
|
+
interface RouteConfig {
|
|
112
|
+
summary?: string;
|
|
113
|
+
description?: string;
|
|
114
|
+
operationId?: string;
|
|
115
|
+
tags?: string[];
|
|
116
|
+
query?: unknown;
|
|
117
|
+
params?: unknown;
|
|
118
|
+
headers?: unknown;
|
|
119
|
+
requestBody?: unknown | {
|
|
120
|
+
description?: string;
|
|
121
|
+
required?: boolean;
|
|
122
|
+
content: Record<string, {
|
|
123
|
+
schema: unknown;
|
|
124
|
+
}>;
|
|
125
|
+
};
|
|
126
|
+
pagination?: {
|
|
127
|
+
maxLimit: number;
|
|
128
|
+
defaultLimit: number;
|
|
129
|
+
};
|
|
130
|
+
filters?: FilterDef[];
|
|
131
|
+
sort?: string[];
|
|
132
|
+
include?: string[];
|
|
133
|
+
fieldsets?: string[];
|
|
134
|
+
security?: string[];
|
|
135
|
+
responses: Partial<Record<StatusCode, ResponseValue>>;
|
|
136
|
+
handler: (...args: unknown[]) => unknown;
|
|
137
|
+
}
|
|
138
|
+
interface RouteEntry {
|
|
139
|
+
path: string;
|
|
140
|
+
method: string;
|
|
141
|
+
config: RouteConfig;
|
|
142
|
+
}
|
|
143
|
+
//#endregion
|
|
144
|
+
//#region src/scanner.d.ts
|
|
145
|
+
/**
|
|
146
|
+
* Scans a framework app instance and extracts registered routes.
|
|
147
|
+
*
|
|
148
|
+
* Each framework adapter implements this interface to bridge the
|
|
149
|
+
* framework-specific route registry to the generic OpenAPI pipeline.
|
|
150
|
+
*/
|
|
151
|
+
interface RouteScanner {
|
|
152
|
+
scan(app: unknown): RouteEntry[];
|
|
153
|
+
}
|
|
154
|
+
//#endregion
|
|
155
|
+
export { FilterFields as a, OpenAPIObject as c, ResponseValue as d, RouteConfig as f, TypedContext as g, StatusCode as h, FilterDef as i, Pagination as l, SchemaObject as m, ArkTypeSchema as n, FilterOperator as o, RouteEntry as p, FieldsetParams as r, InfoObject as s, RouteScanner as t, PathItemObject as u };
|