@axiomify/cli 4.0.0 → 6.0.0-rc.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 +114 -47
- package/dist/chunk-YZPZCUKZ.mjs +41 -0
- package/dist/dist-PKSGYMK7.mjs +357 -0
- package/dist/index.js +5070 -137
- package/dist/index.mjs +4663 -140
- package/dist/uws_darwin_arm64_108-CLFXMYPI.node +0 -0
- package/dist/uws_darwin_arm64_115-7FFEG3YF.node +0 -0
- package/dist/uws_darwin_arm64_120-GFZT7CLS.node +0 -0
- package/dist/uws_darwin_arm64_127-KHC2FVAM.node +0 -0
- package/dist/uws_darwin_x64_108-BRGT45AT.node +0 -0
- package/dist/uws_darwin_x64_115-4HGPQGDD.node +0 -0
- package/dist/uws_darwin_x64_120-C2SGUHP4.node +0 -0
- package/dist/uws_darwin_x64_127-NHKQMMST.node +0 -0
- package/dist/uws_linux_arm64_108-YHK7ACON.node +0 -0
- package/dist/uws_linux_arm64_115-EIAAY4WO.node +0 -0
- package/dist/uws_linux_arm64_120-OADY5FIN.node +0 -0
- package/dist/uws_linux_arm64_127-U2SRLYQM.node +0 -0
- package/dist/uws_linux_arm_108-BKVITVZA.node +0 -0
- package/dist/uws_linux_arm_115-7IORQF77.node +0 -0
- package/dist/uws_linux_arm_120-LCX4ED5F.node +0 -0
- package/dist/uws_linux_arm_127-6UTO7TL6.node +0 -0
- package/dist/uws_linux_x64_108-QSNE6XWU.node +0 -0
- package/dist/uws_linux_x64_115-7AAZWMWE.node +0 -0
- package/dist/uws_linux_x64_120-AIZ6RIW2.node +0 -0
- package/dist/uws_linux_x64_127-HBA6RNSU.node +0 -0
- package/dist/uws_win32_x64_108-J6KONPDM.node +0 -0
- package/dist/uws_win32_x64_115-V5N4NHJ5.node +0 -0
- package/dist/uws_win32_x64_120-XH4MVJGN.node +0 -0
- package/dist/uws_win32_x64_127-JBAEKR4X.node +0 -0
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -1,85 +1,152 @@
|
|
|
1
1
|
# @axiomify/cli
|
|
2
2
|
|
|
3
|
-
The official
|
|
3
|
+
The official CLI for the Axiomify framework — scaffold projects, run the
|
|
4
|
+
dev server, build production bundles, inspect routes, generate OpenAPI
|
|
5
|
+
specs, and audit production readiness.
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
## Install
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
```bash
|
|
10
|
+
npm install -D @axiomify/cli
|
|
11
|
+
# or invoke without installing:
|
|
12
|
+
npx @axiomify/cli init my-api
|
|
13
|
+
```
|
|
8
14
|
|
|
9
|
-
|
|
15
|
+
Per-project install is recommended so the CLI version stays pinned to
|
|
16
|
+
the same major as your `@axiomify/*` runtime packages.
|
|
10
17
|
|
|
11
|
-
|
|
12
|
-
npm install @axiomify/cli -D
|
|
13
|
-
````
|
|
18
|
+
## Commands at a glance
|
|
14
19
|
|
|
15
|
-
|
|
20
|
+
| Command | Purpose |
|
|
21
|
+
|---|---|
|
|
22
|
+
| `axiomify init [directory]` | Bootstrap a new project |
|
|
23
|
+
| `axiomify dev [entry]` | Hot-reload dev server (esbuild watch) |
|
|
24
|
+
| `axiomify build [entry]` | Compile a production bundle to `dist/` |
|
|
25
|
+
| `axiomify routes [entry]` | Inspect every HTTP + WebSocket route |
|
|
26
|
+
| `axiomify openapi [entry]` | Generate the OpenAPI 3.0.3 spec |
|
|
27
|
+
| `axiomify check [entry]` | Static production-readiness audit |
|
|
28
|
+
| `axiomify doctor` | Diagnose the host environment |
|
|
16
29
|
|
|
17
|
-
|
|
18
|
-
npm install -g @axiomify/cli
|
|
19
|
-
```
|
|
30
|
+
`[entry]` defaults to `src/index.ts` everywhere it's accepted.
|
|
20
31
|
|
|
21
|
-
|
|
32
|
+
For the full reference (flags, exit codes, CI examples), see
|
|
33
|
+
[`docs/packages/cli.md`](../../docs/packages/cli.md).
|
|
22
34
|
|
|
23
|
-
|
|
24
|
-
| :--- | :--- |
|
|
25
|
-
| `axiomify init` | Scaffolds a new, production-ready Axiomify project. |
|
|
26
|
-
| `axiomify dev <entry>` | Starts the development server with hot-module reloading (HMR). |
|
|
27
|
-
| `axiomify build <entry>` | Compiles your TypeScript application for production. |
|
|
28
|
-
| `axiomify routes <entry>`| Inspects your app and prints a visual table of all registered routes. |
|
|
35
|
+
## `axiomify init`
|
|
29
36
|
|
|
30
|
-
|
|
37
|
+
```bash
|
|
38
|
+
axiomify init my-api
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Interactive scaffolder. Prompts for project name (when no directory
|
|
42
|
+
argument is given), description, package manager (npm / pnpm / yarn),
|
|
43
|
+
optional ESLint + Prettier + EditorConfig, git initialisation, and
|
|
44
|
+
whether to run install automatically.
|
|
31
45
|
|
|
32
|
-
|
|
46
|
+
The generated `src/index.ts` registers `helmet`, `cors`, `security`,
|
|
47
|
+
`rate-limit`, `fingerprint`, and `logger` with sane defaults. Pass
|
|
48
|
+
`-f, --force` to overwrite existing files.
|
|
33
49
|
|
|
34
|
-
|
|
50
|
+
## `axiomify dev` / `axiomify build`
|
|
35
51
|
|
|
36
52
|
```bash
|
|
37
|
-
|
|
53
|
+
axiomify dev # watches src/, restarts on change
|
|
54
|
+
axiomify build # bundles to dist/index.js
|
|
38
55
|
```
|
|
39
56
|
|
|
40
|
-
|
|
57
|
+
Both use esbuild. `dev` sends SIGTERM first so your `gracefulShutdown`
|
|
58
|
+
hooks can drain, with a SIGKILL fallback after 3 seconds.
|
|
41
59
|
|
|
42
|
-
|
|
60
|
+
## `axiomify routes`
|
|
61
|
+
|
|
62
|
+
Inspects the app *without* booting a listener. Prints a
|
|
63
|
+
Unicode-bordered table with colour-coded HTTP methods, validation
|
|
64
|
+
badges, OpenAPI tags + `operationId`, plugin count, timeout, and
|
|
65
|
+
deprecation marker.
|
|
43
66
|
|
|
44
|
-
```bash
|
|
45
|
-
npx axiomify dev src/index.ts
|
|
46
67
|
```
|
|
68
|
+
🧭 Axiomify routes
|
|
69
|
+
|
|
70
|
+
┌─────────┬──────────────────────┬───────────────┬───────────────────────────────────────┐
|
|
71
|
+
│ METHOD │ PATH │ VALIDATION │ META │
|
|
72
|
+
├─────────┼──────────────────────┼───────────────┼───────────────────────────────────────┤
|
|
73
|
+
│ WS │ /chat │ Message │ — │
|
|
74
|
+
│ GET │ /health │ — │ — │
|
|
75
|
+
│ POST │ /users ⊘ DEPRECATED │ Body,Response │ op:createUser #Users 5000ms +1 plugin │
|
|
76
|
+
│ GET │ /users/:id │ Params │ op:getUser #Users │
|
|
77
|
+
│ DELETE │ /users/:id │ Params │ — │
|
|
78
|
+
└─────────┴──────────────────────┴───────────────┴───────────────────────────────────────┘
|
|
79
|
+
|
|
80
|
+
✓ 5 routes DELETE 1 · GET 2 · POST 1 · WS 1
|
|
81
|
+
└ 1 WebSocket route included
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Flags: `--json`, `--method GET,POST,WS`, `--filter "/api/v1/*"`,
|
|
85
|
+
`--sort path|method`.
|
|
47
86
|
|
|
48
|
-
|
|
87
|
+
WebSocket routes (`app.ws(...)`) appear under the `WS` pseudo-method
|
|
88
|
+
alongside HTTP routes — earlier CLI versions silently omitted them.
|
|
49
89
|
|
|
50
|
-
|
|
90
|
+
## `axiomify openapi`
|
|
51
91
|
|
|
52
92
|
```bash
|
|
53
|
-
|
|
93
|
+
axiomify openapi # stdout, pretty JSON
|
|
94
|
+
axiomify openapi -o openapi.json
|
|
95
|
+
axiomify openapi --format yaml -o api.yml
|
|
96
|
+
axiomify openapi --minify > spec.min.json
|
|
97
|
+
axiomify openapi --title "My API" --spec-version "$(git describe)"
|
|
54
98
|
```
|
|
55
99
|
|
|
56
|
-
|
|
100
|
+
Generates the OpenAPI 3.0.3 spec from the app's registered routes.
|
|
101
|
+
Useful in CI for client codegen pipelines (`openapi-typescript`,
|
|
102
|
+
`openapi-generator`, `oazapfts`) without booting an HTTP listener.
|
|
103
|
+
|
|
104
|
+
Requires `@axiomify/openapi` to be installed; dynamic-imports it at
|
|
105
|
+
runtime and prints a clean error if missing.
|
|
57
106
|
|
|
58
|
-
|
|
107
|
+
## `axiomify check`
|
|
59
108
|
|
|
60
109
|
```bash
|
|
61
|
-
|
|
110
|
+
axiomify check
|
|
62
111
|
```
|
|
63
112
|
|
|
64
|
-
|
|
113
|
+
Static production-readiness audit. Loads the app (no listener) and
|
|
114
|
+
flags:
|
|
115
|
+
|
|
116
|
+
- ✓ pass — configuration is correct
|
|
117
|
+
- ⚠ warn — non-fatal smell
|
|
118
|
+
- ✗ fail — real defect that blocks ship
|
|
119
|
+
|
|
120
|
+
Checks include: `enableRequestId()` called, env vars referenced in
|
|
121
|
+
source actually set, routes with body schemas declare response schemas,
|
|
122
|
+
no deprecated `meta:` field usage, health check registered, OpenAPI docs
|
|
123
|
+
protected, security plugins active.
|
|
65
124
|
|
|
66
|
-
|
|
125
|
+
Exit code 1 on any fail — wire into CI to gate deploys.
|
|
67
126
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
"build": "axiomify build src/index.ts",
|
|
73
|
-
"start": "node dist/index.js",
|
|
74
|
-
"routes": "axiomify routes src/index.ts"
|
|
75
|
-
}
|
|
76
|
-
}
|
|
127
|
+
## `axiomify doctor`
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
axiomify doctor
|
|
77
131
|
```
|
|
78
132
|
|
|
79
|
-
|
|
133
|
+
Diagnoses the host environment: Node version vs uWS prebuilt support,
|
|
134
|
+
platform (Linux ✓ for `SO_REUSEPORT` clustering), `@axiomify/*` package
|
|
135
|
+
alignment, uWS bindings load successfully, recent build artefact, port
|
|
136
|
+
3000 (or `$PORT`) availability.
|
|
80
137
|
|
|
81
|
-
|
|
138
|
+
Run on a fresh clone or new CI runner before chasing test failures that
|
|
139
|
+
turn out to be Node-version mismatches.
|
|
82
140
|
|
|
83
|
-
##
|
|
141
|
+
## CI example
|
|
142
|
+
|
|
143
|
+
```yaml
|
|
144
|
+
- run: npx axiomify doctor # environment sanity
|
|
145
|
+
- run: npx axiomify check # static readiness audit
|
|
146
|
+
- run: npx axiomify build
|
|
147
|
+
- run: npx axiomify openapi -o ./openapi.json --spec-version "$GITHUB_SHA"
|
|
148
|
+
- run: npx axiomify routes --json > routes.json # surface snapshot
|
|
149
|
+
```
|
|
84
150
|
|
|
85
|
-
|
|
151
|
+
Diff `routes.json` between commits to detect accidental API changes
|
|
152
|
+
before they reach production.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
8
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
9
|
+
}) : x)(function(x) {
|
|
10
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
11
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
12
|
+
});
|
|
13
|
+
var __glob = (map) => (path) => {
|
|
14
|
+
var fn = map[path];
|
|
15
|
+
if (fn) return fn();
|
|
16
|
+
throw new Error("Module not found in bundle: " + path);
|
|
17
|
+
};
|
|
18
|
+
var __esm = (fn, res) => function __init() {
|
|
19
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
20
|
+
};
|
|
21
|
+
var __commonJS = (cb, mod) => function __require2() {
|
|
22
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
23
|
+
};
|
|
24
|
+
var __copyProps = (to, from, except, desc) => {
|
|
25
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
26
|
+
for (let key of __getOwnPropNames(from))
|
|
27
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
28
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
29
|
+
}
|
|
30
|
+
return to;
|
|
31
|
+
};
|
|
32
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
33
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
34
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
35
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
36
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
37
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
38
|
+
mod
|
|
39
|
+
));
|
|
40
|
+
|
|
41
|
+
export { __commonJS, __esm, __glob, __require, __toESM };
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import { __require } from './chunk-YZPZCUKZ.mjs';
|
|
2
|
+
|
|
3
|
+
// ../openapi/dist/index.mjs
|
|
4
|
+
var __require2 = /* @__PURE__ */ ((x) => typeof __require !== "undefined" ? __require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
5
|
+
get: (a, b) => (typeof __require !== "undefined" ? __require : a)[b]
|
|
6
|
+
}) : x)(function(x) {
|
|
7
|
+
if (typeof __require !== "undefined") return __require.apply(this, arguments);
|
|
8
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
9
|
+
});
|
|
10
|
+
function zodSchemaToOpenApi(schema) {
|
|
11
|
+
const s = schema;
|
|
12
|
+
if (typeof s.toJSONSchema === "function") {
|
|
13
|
+
const full = s.toJSONSchema({ target: "openApi3_1" });
|
|
14
|
+
const { $schema: _dropped, ...rest } = full;
|
|
15
|
+
return rest;
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
const { zodToJsonSchema } = __require2("zod-to-json-schema");
|
|
19
|
+
return zodToJsonSchema(schema, { target: "openApi3" });
|
|
20
|
+
} catch {
|
|
21
|
+
return { type: "object" };
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function isZodSchema(value) {
|
|
25
|
+
return typeof value === "object" && value !== null && typeof value.safeParse === "function";
|
|
26
|
+
}
|
|
27
|
+
var OpenApiGenerator = class {
|
|
28
|
+
constructor(app, options) {
|
|
29
|
+
this.app = app;
|
|
30
|
+
this.options = options;
|
|
31
|
+
}
|
|
32
|
+
app;
|
|
33
|
+
options;
|
|
34
|
+
generate() {
|
|
35
|
+
const spec = {
|
|
36
|
+
openapi: "3.0.3",
|
|
37
|
+
info: this.options.info,
|
|
38
|
+
paths: {}
|
|
39
|
+
};
|
|
40
|
+
if (this.options.components) spec.components = this.options.components;
|
|
41
|
+
if (this.options.security) spec.security = this.options.security;
|
|
42
|
+
for (const route of this.app.registeredRoutes) {
|
|
43
|
+
const openApiPath = this.formatPath(route.path);
|
|
44
|
+
const method = route.method.toLowerCase();
|
|
45
|
+
const paths = spec.paths;
|
|
46
|
+
if (!paths[openApiPath]) paths[openApiPath] = {};
|
|
47
|
+
const op = route.openapi ?? void 0;
|
|
48
|
+
const operation = {
|
|
49
|
+
// OAS §4.7.10.2 — summary. Default to `${method} ${path}` so the
|
|
50
|
+
// docs UI always has a human-readable title even without user input.
|
|
51
|
+
summary: op?.summary ?? `${route.method} ${route.path}`,
|
|
52
|
+
// OAS §4.7.10.5 — operationId. Client codegen tools
|
|
53
|
+
// (openapi-typescript, openapi-generator) use this to name the
|
|
54
|
+
// generated function. When the user doesn't supply one we
|
|
55
|
+
// synthesise a stable name from method+path:
|
|
56
|
+
// GET /users/:id → "getUsersById"
|
|
57
|
+
// POST /users → "postUsers"
|
|
58
|
+
// Determinism matters here — codegen output should not drift
|
|
59
|
+
// between releases unless method+path actually change.
|
|
60
|
+
operationId: op?.operationId ?? this.synthesiseOperationId(route.method, route.path),
|
|
61
|
+
parameters: this.extractParameters(route),
|
|
62
|
+
responses: this.extractResponses(route)
|
|
63
|
+
};
|
|
64
|
+
const legacySchema = route.schema ?? {};
|
|
65
|
+
const description = op?.description ?? legacySchema.description;
|
|
66
|
+
const tags = op?.tags ?? legacySchema.tags;
|
|
67
|
+
const security = op?.security ?? legacySchema.security;
|
|
68
|
+
if (description) operation.description = description;
|
|
69
|
+
if (tags) operation.tags = tags;
|
|
70
|
+
if (security !== void 0) operation.security = security;
|
|
71
|
+
if (op?.deprecated) operation.deprecated = true;
|
|
72
|
+
if (op?.externalDocs) operation.externalDocs = op.externalDocs;
|
|
73
|
+
if (op?.servers) operation.servers = op.servers;
|
|
74
|
+
if (op?.callbacks) operation.callbacks = op.callbacks;
|
|
75
|
+
const body = this.extractBody(route);
|
|
76
|
+
if (body) {
|
|
77
|
+
if (op?.requestBodyDescription) {
|
|
78
|
+
body.description = op.requestBodyDescription;
|
|
79
|
+
}
|
|
80
|
+
operation.requestBody = body;
|
|
81
|
+
}
|
|
82
|
+
paths[openApiPath][method] = operation;
|
|
83
|
+
}
|
|
84
|
+
return spec;
|
|
85
|
+
}
|
|
86
|
+
/** Translates Axiomify path syntax to OpenAPI: `/users/:id` → `/users/{id}` */
|
|
87
|
+
formatPath(path) {
|
|
88
|
+
return path.replace(/:([a-zA-Z0-9_]+)/g, "{$1}");
|
|
89
|
+
}
|
|
90
|
+
extractParameters(route) {
|
|
91
|
+
const parameters = [];
|
|
92
|
+
if (route.schema?.params) {
|
|
93
|
+
const paramSchema = zodSchemaToOpenApi(route.schema.params);
|
|
94
|
+
const properties = paramSchema.properties ?? {};
|
|
95
|
+
for (const [key, prop] of Object.entries(properties)) {
|
|
96
|
+
parameters.push({ name: key, in: "path", required: true, schema: prop });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (route.schema?.query) {
|
|
100
|
+
const querySchema = zodSchemaToOpenApi(route.schema.query);
|
|
101
|
+
const properties = querySchema.properties ?? {};
|
|
102
|
+
const required = querySchema.required ?? [];
|
|
103
|
+
for (const [key, prop] of Object.entries(properties)) {
|
|
104
|
+
parameters.push({
|
|
105
|
+
name: key,
|
|
106
|
+
in: "query",
|
|
107
|
+
required: required.includes(key),
|
|
108
|
+
schema: prop
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return parameters;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Synthesise a stable, codegen-friendly operationId from method+path
|
|
116
|
+
* when the route definition doesn't supply one. Example outputs:
|
|
117
|
+
* GET /users/:id → getUsersById
|
|
118
|
+
* POST /users → postUsers
|
|
119
|
+
* GET /users/:id/posts/:pid → getUsersByIdPostsByPid
|
|
120
|
+
*
|
|
121
|
+
* Determinism matters here — client codegen produces the same function
|
|
122
|
+
* names on every run as long as method+path are stable.
|
|
123
|
+
*/
|
|
124
|
+
synthesiseOperationId(method, path) {
|
|
125
|
+
const verb = method.toLowerCase();
|
|
126
|
+
const parts = [];
|
|
127
|
+
for (const seg of path.split("/")) {
|
|
128
|
+
if (!seg) continue;
|
|
129
|
+
if (seg.startsWith(":")) {
|
|
130
|
+
const name = seg.slice(1);
|
|
131
|
+
parts.push("By", name.charAt(0).toUpperCase() + name.slice(1));
|
|
132
|
+
} else if (seg === "*") {
|
|
133
|
+
parts.push("All");
|
|
134
|
+
} else {
|
|
135
|
+
parts.push(seg.charAt(0).toUpperCase() + seg.slice(1));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return verb + parts.join("");
|
|
139
|
+
}
|
|
140
|
+
extractBody(route) {
|
|
141
|
+
if (!route.schema?.body && !route.schema?.files) return void 0;
|
|
142
|
+
const hasFiles = !!route.schema.files;
|
|
143
|
+
const contentType = hasFiles ? "multipart/form-data" : "application/json";
|
|
144
|
+
let finalSchema = { type: "object", properties: {} };
|
|
145
|
+
if (route.schema.body) {
|
|
146
|
+
const bodySchema = zodSchemaToOpenApi(route.schema.body);
|
|
147
|
+
if (bodySchema.type === "object") {
|
|
148
|
+
finalSchema.properties = { ...bodySchema.properties };
|
|
149
|
+
if (bodySchema.required) finalSchema.required = bodySchema.required;
|
|
150
|
+
if (bodySchema.additionalProperties !== void 0) {
|
|
151
|
+
finalSchema.additionalProperties = bodySchema.additionalProperties;
|
|
152
|
+
}
|
|
153
|
+
} else {
|
|
154
|
+
finalSchema = hasFiles ? { type: "object", properties: { payload: bodySchema } } : bodySchema;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (hasFiles) {
|
|
158
|
+
const files = route.schema.files;
|
|
159
|
+
const props = finalSchema.properties ?? {};
|
|
160
|
+
for (const [fieldName, config] of Object.entries(files)) {
|
|
161
|
+
props[fieldName] = {
|
|
162
|
+
type: "string",
|
|
163
|
+
format: "binary",
|
|
164
|
+
...config.description ? { description: config.description } : {},
|
|
165
|
+
...config.maxSize ? { description: `Max size: ${config.maxSize} bytes` } : {}
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
finalSchema.properties = props;
|
|
169
|
+
}
|
|
170
|
+
return { required: true, content: { [contentType]: { schema: finalSchema } } };
|
|
171
|
+
}
|
|
172
|
+
extractResponses(route) {
|
|
173
|
+
const op = route.openapi;
|
|
174
|
+
const descriptions = op?.responseDescriptions ?? {};
|
|
175
|
+
const defaultResponse = {
|
|
176
|
+
"200": {
|
|
177
|
+
description: descriptions["200"] ?? "Successful response",
|
|
178
|
+
content: { "application/json": { schema: { type: "object" } } }
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
if (!route.schema?.response) return defaultResponse;
|
|
182
|
+
const responseSchema = route.schema.response;
|
|
183
|
+
const responses = {};
|
|
184
|
+
if (isZodSchema(responseSchema)) {
|
|
185
|
+
responses["200"] = {
|
|
186
|
+
description: descriptions["200"] ?? "Successful response",
|
|
187
|
+
content: {
|
|
188
|
+
"application/json": { schema: zodSchemaToOpenApi(responseSchema) }
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
} else if (typeof responseSchema === "object" && responseSchema !== null) {
|
|
192
|
+
for (const [code, schema] of Object.entries(
|
|
193
|
+
responseSchema
|
|
194
|
+
)) {
|
|
195
|
+
responses[code] = {
|
|
196
|
+
description: descriptions[code] ?? `Response ${code}`,
|
|
197
|
+
content: {
|
|
198
|
+
"application/json": { schema: zodSchemaToOpenApi(schema) }
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return Object.keys(responses).length > 0 ? responses : defaultResponse;
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
var DOCS_CSP = "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://unpkg.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdn.jsdelivr.net https://cdnjs.cloudflare.com; font-src 'self' https://fonts.gstatic.com data:; img-src 'self' data: https://validator.swagger.io; worker-src 'self' blob:;";
|
|
207
|
+
function escapeHtml(s) {
|
|
208
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
209
|
+
}
|
|
210
|
+
function escapeJsString(s) {
|
|
211
|
+
return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
212
|
+
}
|
|
213
|
+
function defineSecuritySchemes(schemes) {
|
|
214
|
+
return {
|
|
215
|
+
schemes,
|
|
216
|
+
require: (name, scopes = []) => [{ [name]: scopes }],
|
|
217
|
+
requireMultiple: (requirements) => {
|
|
218
|
+
const combined = {};
|
|
219
|
+
requirements.forEach((req) => {
|
|
220
|
+
combined[req] = [];
|
|
221
|
+
});
|
|
222
|
+
return [combined];
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
var Security = defineSecuritySchemes({
|
|
227
|
+
bearerAuth: { type: "http", scheme: "bearer" },
|
|
228
|
+
apiKey: { type: "apiKey", in: "header", name: "X-API-KEY" },
|
|
229
|
+
basicAuth: { type: "http", scheme: "basic" }
|
|
230
|
+
});
|
|
231
|
+
function inferSchemaFromPayload(data, depth = 0) {
|
|
232
|
+
if (depth > 32) return { type: "object" };
|
|
233
|
+
if (data === null) return { type: "null" };
|
|
234
|
+
if (Array.isArray(data)) {
|
|
235
|
+
return {
|
|
236
|
+
type: "array",
|
|
237
|
+
items: data.length > 0 ? inferSchemaFromPayload(data[0], depth + 1) : { type: "object" }
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
if (typeof data === "object") {
|
|
241
|
+
const properties = {};
|
|
242
|
+
for (const [key, value] of Object.entries(data)) {
|
|
243
|
+
properties[key] = inferSchemaFromPayload(value, depth + 1);
|
|
244
|
+
}
|
|
245
|
+
return { type: "object", properties };
|
|
246
|
+
}
|
|
247
|
+
return { type: typeof data };
|
|
248
|
+
}
|
|
249
|
+
function useOpenAPI(app, options) {
|
|
250
|
+
const rawPrefix = options.prefix ?? "/docs";
|
|
251
|
+
const normalizedPrefix = rawPrefix.startsWith("/") ? rawPrefix : `/${rawPrefix}`;
|
|
252
|
+
const prefix = normalizedPrefix === "/" ? "/" : normalizedPrefix.endsWith("/") ? normalizedPrefix.slice(0, -1) : normalizedPrefix;
|
|
253
|
+
const docsPaths = prefix === "/" ? ["/"] : [prefix, `${prefix}/`];
|
|
254
|
+
const docsPathSet = new Set(docsPaths);
|
|
255
|
+
const specPath = prefix === "/" ? "/openapi.json" : `${prefix}/openapi.json`;
|
|
256
|
+
const generator = new OpenApiGenerator(app, options);
|
|
257
|
+
let cachedSpec = null;
|
|
258
|
+
let cachedSpecJson = null;
|
|
259
|
+
let emittedPublicDocsWarning = false;
|
|
260
|
+
if (options.autoInferResponses) {
|
|
261
|
+
app.addHook("onPostHandler", (req, res, match) => {
|
|
262
|
+
if (!match?.route) return;
|
|
263
|
+
if (req.path === specPath || docsPathSet.has(req.path)) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const payload = res.payload;
|
|
267
|
+
if (payload === void 0) return;
|
|
268
|
+
if (!cachedSpec) cachedSpec = generator.generate();
|
|
269
|
+
const path = generator.formatPath(match.route.path);
|
|
270
|
+
const method = match.route.method.toLowerCase();
|
|
271
|
+
const statusCode = String(res.statusCode);
|
|
272
|
+
const existingResponse = cachedSpec.paths[path]?.[method]?.responses?.[statusCode];
|
|
273
|
+
const isDefault = existingResponse?.description === "Successful response" && existingResponse?.content?.["application/json"]?.schema?.type === "object";
|
|
274
|
+
if (existingResponse && !isDefault) return;
|
|
275
|
+
let parsedData = payload;
|
|
276
|
+
if (typeof payload === "string") {
|
|
277
|
+
try {
|
|
278
|
+
parsedData = JSON.parse(payload);
|
|
279
|
+
} catch {
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (cachedSpec.paths[path]?.[method]) {
|
|
283
|
+
cachedSpec.paths[path][method].responses[statusCode] = {
|
|
284
|
+
description: "Auto-inferred response",
|
|
285
|
+
content: {
|
|
286
|
+
"application/json": { schema: inferSchemaFromPayload(parsedData) }
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
cachedSpecJson = null;
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
const guard = async (req) => {
|
|
294
|
+
if (!options.protect) {
|
|
295
|
+
if (process.env.NODE_ENV === "production") {
|
|
296
|
+
if (!emittedPublicDocsWarning) {
|
|
297
|
+
emittedPublicDocsWarning = true;
|
|
298
|
+
console.warn(
|
|
299
|
+
"[axiomify/openapi] OpenAPI endpoints are not protected. Production access is denied by default. Provide a `protect` function or set `allowPublicInProduction: true` explicitly."
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
return options.allowPublicInProduction === true;
|
|
303
|
+
}
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
return Boolean(await options.protect(req));
|
|
307
|
+
};
|
|
308
|
+
app.route({
|
|
309
|
+
method: "GET",
|
|
310
|
+
path: specPath,
|
|
311
|
+
handler: async (req, res) => {
|
|
312
|
+
if (!await guard(req)) return res.status(403).send(null, "Forbidden");
|
|
313
|
+
if (!cachedSpec) cachedSpec = generator.generate();
|
|
314
|
+
if (!cachedSpecJson) cachedSpecJson = JSON.stringify(cachedSpec);
|
|
315
|
+
res.status(200).sendRaw(cachedSpecJson, "application/json");
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
const docsHandler = async (req, res) => {
|
|
319
|
+
if (!await guard(req)) return res.status(403).send(null, "Forbidden");
|
|
320
|
+
if (typeof res.setHeader === "function") {
|
|
321
|
+
res.setHeader("Content-Security-Policy", DOCS_CSP);
|
|
322
|
+
} else if (typeof res.header === "function") {
|
|
323
|
+
res.header("Content-Security-Policy", DOCS_CSP);
|
|
324
|
+
}
|
|
325
|
+
const isDev = process.env.NODE_ENV !== "production";
|
|
326
|
+
const specUrl = isDev ? `${specPath}?t=${Date.now()}` : specPath;
|
|
327
|
+
const html = `<!DOCTYPE html>
|
|
328
|
+
<html lang="en">
|
|
329
|
+
<head>
|
|
330
|
+
<meta charset="utf-8" />
|
|
331
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
332
|
+
<title>${escapeHtml(options.info.title)} - API Docs</title>
|
|
333
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/5.11.0/swagger-ui.min.css" integrity="sha384-bIuUyBV7i6P7z/kPAs1oeBIf8PMIqVkPVDzzaOL+QH7kWmvCT9HDTWwGVs0L4/9Q" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
|
334
|
+
</head>
|
|
335
|
+
<body style="margin: 0; padding: 0;">
|
|
336
|
+
<div id="swagger-ui"></div>
|
|
337
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/5.11.0/swagger-ui-bundle.min.js" integrity="sha384-XHDYRdiHvBq7oL4CtkiJKfdVVA5PydxYtssHVtRrvPlha1m+zz8kboiyx/MAsyl3" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
|
338
|
+
<script>
|
|
339
|
+
window.onload = () => {
|
|
340
|
+
window.ui = SwaggerUIBundle({
|
|
341
|
+
url: '${escapeJsString(specUrl)}',
|
|
342
|
+
dom_id: '#swagger-ui',
|
|
343
|
+
});
|
|
344
|
+
};
|
|
345
|
+
</script>
|
|
346
|
+
</body>
|
|
347
|
+
</html>`;
|
|
348
|
+
res.status(200).sendRaw(html, "text/html");
|
|
349
|
+
};
|
|
350
|
+
app.route({
|
|
351
|
+
method: "GET",
|
|
352
|
+
path: prefix,
|
|
353
|
+
handler: docsHandler
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export { OpenApiGenerator, Security, defineSecuritySchemes, useOpenAPI };
|