@daloyjs/core 0.12.0 → 0.13.1
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 +63 -6
- package/bin/daloy.mjs +36 -0
- package/dist/app.d.ts +128 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +265 -1
- package/dist/app.js.map +1 -1
- package/dist/cli.d.ts +41 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +143 -9
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/openapi.d.ts +9 -0
- package/dist/openapi.d.ts.map +1 -1
- package/dist/openapi.js +106 -0
- package/dist/openapi.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
> A **runtime-portable TypeScript web framework** with built-in **contract-first routing**, **validation**, **OpenAPI (Hey API)**, **typed client generation**, **large-scale maintainability**, and **security-focused runtime plus supply-chain posture**.
|
|
10
10
|
|
|
11
|
+
**One-line API docs.** `new App({ openapi: { info: ... }, docs: true })` auto-mounts `GET /docs` (Scalar), `GET /openapi.json`, and `GET /openapi.yaml` — the same DX as FastAPI, without leaving TypeScript.
|
|
12
|
+
|
|
11
13
|
DaloyJS is maintained in the GitHub organization at <https://github.com/daloyjs>; the canonical framework repository is <https://github.com/daloyjs/daloy>.
|
|
12
14
|
|
|
13
15
|
---
|
|
@@ -16,7 +18,7 @@ DaloyJS exists to be the framework you'd build if you took the best ideas from e
|
|
|
16
18
|
|
|
17
19
|
| You want | Today's best-of | What DaloyJS gives you |
|
|
18
20
|
| ------------------------------------------------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
|
|
19
|
-
| Best **OpenAPI ergonomics** | [FastAPI](https://fastapi.tiangolo.com) | First-class OpenAPI 3.1 generation from a single route definition. |
|
|
21
|
+
| Best **OpenAPI ergonomics** | [FastAPI](https://fastapi.tiangolo.com) | First-class OpenAPI 3.1 generation from a single route definition; one-line `docs: true` auto-mounts `/docs` and `/openapi.json`. |
|
|
20
22
|
| Best **Vercel / serverless / edge fit** | [Hono](https://hono.dev/docs/) | Web-standard `Request → Response` core, multi-runtime adapters. |
|
|
21
23
|
| Mature **Swagger / docs / ops** in Node | [Fastify](https://fastify.dev/docs/latest/Reference/) | Encapsulated plugins, structured logger, graceful shutdown, request ids, hooks. |
|
|
22
24
|
| Modern **TS-first DX**, Bun acceptable | [Elysia](https://elysiajs.com/at-glance.html) | End-to-end typed handlers, typed context, typed client. |
|
|
@@ -182,15 +184,67 @@ const r = await client.getBookById({ params: { id: "1" } });
|
|
|
182
184
|
|
|
183
185
|
---
|
|
184
186
|
|
|
185
|
-
## Built-in docs UI (Swagger UI
|
|
187
|
+
## Built-in docs UI (Scalar / Swagger UI)
|
|
188
|
+
|
|
189
|
+
FastAPI-style. One line on the `App` constructor mounts `GET /docs`, `GET /openapi.json`, and `GET /openapi.yaml` for you,
|
|
190
|
+
with a strict CSP and CDN-hosted assets:
|
|
186
191
|
|
|
187
192
|
```ts
|
|
188
|
-
import {
|
|
189
|
-
|
|
193
|
+
import { App } from "@daloyjs/core";
|
|
194
|
+
|
|
195
|
+
const app = new App({
|
|
196
|
+
openapi: { info: { title: "My API", version: "1.0.0" } },
|
|
197
|
+
docs: true, // mounts GET /docs (Scalar), GET /openapi.json, GET /openapi.yaml
|
|
198
|
+
});
|
|
190
199
|
```
|
|
191
200
|
|
|
192
|
-
|
|
193
|
-
|
|
201
|
+
Use `docs: "auto"` to mount only when `production: false`, or the object form for full control:
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
new App({
|
|
205
|
+
openapi: { info: { title: "My API", version: "1.0.0" } },
|
|
206
|
+
docs: {
|
|
207
|
+
ui: "swagger",
|
|
208
|
+
path: "/reference",
|
|
209
|
+
openapiPath: "/spec.json",
|
|
210
|
+
openapiYamlPath: "/spec.yaml", // or `false` to disable the YAML route
|
|
211
|
+
tags: ["Docs"],
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Prefer to mount manually? Import the helpers directly:
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
import { swaggerUiHtml, scalarHtml, htmlResponse } from "@daloyjs/core/docs";
|
|
220
|
+
import { generateOpenAPI } from "@daloyjs/core/openapi";
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
The UI is always contract-accurate — never stale. `create-daloy` templates opt in with `docs: true`.
|
|
224
|
+
|
|
225
|
+
If you omit `openapi.info.title` / `info.version`, Daloy reads your project's `package.json` (`name`, `version`, `description`) automatically — no boilerplate. Deno projects without a `package.json` fall back to `deno.json` / `deno.jsonc`. Explicit values always win.
|
|
226
|
+
|
|
227
|
+
Prefer a factory? `createApp(options)` is exported as an alias of `new App(options)`.
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
import { createApp } from "@daloyjs/core";
|
|
231
|
+
|
|
232
|
+
const app = createApp({ docs: true });
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### `daloy dev` — one-command watch mode
|
|
236
|
+
|
|
237
|
+
`daloy dev [entry]` delegates to the host runtime's native watch tool, with no extra config:
|
|
238
|
+
|
|
239
|
+
| Runtime | Spawned command |
|
|
240
|
+
| ------- | --------------------------------------------------------------- |
|
|
241
|
+
| Node | `node --import tsx --watch <entry>` |
|
|
242
|
+
| Bun | `bun --hot <entry>` |
|
|
243
|
+
| Deno | `deno run --watch --allow-net --allow-env --allow-read <entry>` |
|
|
244
|
+
|
|
245
|
+
Entry defaults to `src/index.ts`, `src/main.ts`, `src/server.ts`, or `src/app.ts`. Install `tsx` as a dev dependency on Node for TypeScript entries.
|
|
246
|
+
|
|
247
|
+
Pass `--runtime <node|bun|deno>` to override runtime detection. This is required when running `daloy dev` from a `package.json` script on Bun or Deno, because the CLI binary's `#!/usr/bin/env node` shebang otherwise forces Node detection. The `bun-basic` template ships `"dev": "daloy dev --runtime bun"` for this reason.
|
|
194
248
|
|
|
195
249
|
---
|
|
196
250
|
|
|
@@ -321,6 +375,9 @@ What works today, at a glance:
|
|
|
321
375
|
- WebSocket primitives with the same Bun-style handler shape (`open`/`message`/`close`/`drain`/`error`) running on both Node and Bun adapters, plus typed `app.ws(path, handler)` registration and route-table awareness so the upgrade listener is only installed when WS routes exist.
|
|
322
376
|
- Pretty `printStartupBanner()` / `formatStartupBanner()` startup helpers at `@daloyjs/core/banner`, used by every starter template so `pnpm dev` greets you with a colorized boxed panel (TTY + `NO_COLOR` / `FORCE_COLOR` aware, with an ASCII fallback for dumb terminals).
|
|
323
377
|
- In-process test client (`app.request()`), contract-test runner, in-process typed client, and Hey API codegen via `pnpm gen`.
|
|
378
|
+
- One-command watch loop: `daloy dev` delegates to the host runtime's native watcher (`node --import tsx --watch`, `bun --hot`, or `deno run --watch`) with a `--runtime` override for cross-runtime `package.json` scripts.
|
|
379
|
+
- Zero-config OpenAPI `info` autofill from `package.json` (Node / Bun) or `deno.json` / `deno.jsonc` (Deno) — explicit `openapi.info` values always win.
|
|
380
|
+
- Live OpenAPI 3.1 spec served as both JSON (`GET /openapi.json`) and YAML (`GET /openapi.yaml`) when `docs: true` — covers Swagger UI's `swagger.yaml` convention out of the box.
|
|
324
381
|
- `pnpm create daloy` scaffolder with Node, Bun, Deno, Cloudflare Worker, and Vercel Edge templates, plus optional `--with-ci` GitHub Actions / Dependabot / CODEOWNERS / SECURITY.md hardening.
|
|
325
382
|
- Plugin encapsulation, decorators, structured logging, request-id propagation, lifecycle events (`onPluginInstalled`, `onShutdown`, `onClose`), and graceful shutdown.
|
|
326
383
|
- Integration guides for transactional email providers — AWS SES, SendGrid, Resend, Postmark, Mailgun, and Mailtrap — with a common `EmailSender` plugin pattern and runtime-compatibility matrix.
|
package/bin/daloy.mjs
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { pathToFileURL, fileURLToPath } from "node:url";
|
|
11
11
|
import { resolve, dirname } from "node:path";
|
|
12
12
|
import { existsSync, readFileSync } from "node:fs";
|
|
13
|
+
import { spawn } from "node:child_process";
|
|
13
14
|
import { runCli } from "../dist/cli.js";
|
|
14
15
|
|
|
15
16
|
const PKG = JSON.parse(
|
|
@@ -42,11 +43,46 @@ async function importEntry(specifier) {
|
|
|
42
43
|
return import(pathToFileURL(abs).href);
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
function spawnDev(command, args) {
|
|
47
|
+
return new Promise((resolvePromise, reject) => {
|
|
48
|
+
const child = spawn(command, args, { stdio: "inherit", shell: false });
|
|
49
|
+
const forward = (sig) => {
|
|
50
|
+
try {
|
|
51
|
+
child.kill(sig);
|
|
52
|
+
} catch {
|
|
53
|
+
/* ignore */
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
process.on("SIGINT", forward);
|
|
57
|
+
process.on("SIGTERM", forward);
|
|
58
|
+
child.on("error", (err) => {
|
|
59
|
+
if (err && err.code === "ENOENT") {
|
|
60
|
+
reject(
|
|
61
|
+
new Error(
|
|
62
|
+
`\`${command}\` was not found on PATH. ` +
|
|
63
|
+
(command === "node"
|
|
64
|
+
? "Install `tsx` as a dev dependency (`pnpm add -D tsx`) and ensure Node.js is on PATH."
|
|
65
|
+
: `Install ${command} or run daloy dev from the runtime that hosts it.`)
|
|
66
|
+
)
|
|
67
|
+
);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
reject(err);
|
|
71
|
+
});
|
|
72
|
+
child.on("exit", (code, signal) => {
|
|
73
|
+
process.off("SIGINT", forward);
|
|
74
|
+
process.off("SIGTERM", forward);
|
|
75
|
+
resolvePromise(signal ? 1 : (code ?? 0));
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
45
80
|
const result = await runCli(process.argv.slice(2), {
|
|
46
81
|
stdout: (chunk) => process.stdout.write(chunk),
|
|
47
82
|
stderr: (chunk) => process.stderr.write(chunk),
|
|
48
83
|
importEntry,
|
|
49
84
|
version: PKG.version,
|
|
85
|
+
spawn: spawnDev,
|
|
50
86
|
});
|
|
51
87
|
|
|
52
88
|
process.exit(result.exitCode);
|
package/dist/app.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { WebSocketRegistry, type WebSocketHandler } from "./websocket.js";
|
|
2
2
|
import { type Logger } from "./logger.js";
|
|
3
3
|
import type { HttpMethod, Hooks, PathString, RequestSchemas, ResponsesMap, RouteDefinition } from "./types.js";
|
|
4
|
+
import { type OpenAPIInfo, type OpenAPIOptions } from "./openapi.js";
|
|
5
|
+
import { type DocsContentSecurityPolicyOptions } from "./docs.js";
|
|
4
6
|
/**
|
|
5
7
|
* Configuration accepted by {@link App}'s constructor. Every field is
|
|
6
8
|
* optional; sensible production defaults are applied.
|
|
@@ -47,6 +49,93 @@ export interface AppOptions {
|
|
|
47
49
|
mockMode?: boolean;
|
|
48
50
|
/** Global hooks applied to every route. */
|
|
49
51
|
hooks?: Hooks;
|
|
52
|
+
/**
|
|
53
|
+
* OpenAPI 3.1 document metadata used by the built-in auto-mounted
|
|
54
|
+
* `/openapi.json` route and any other consumer that calls
|
|
55
|
+
* `generateOpenAPI(app)` without its own options. When `info.title` /
|
|
56
|
+
* `info.version` are omitted they fall back to the top-level
|
|
57
|
+
* {@link AppOptions.title} / {@link AppOptions.version} fields, then to
|
|
58
|
+
* `"DaloyJS API"` / `"0.0.0"`.
|
|
59
|
+
*
|
|
60
|
+
* @since 0.3.0
|
|
61
|
+
*/
|
|
62
|
+
openapi?: AppOpenAPIOptions;
|
|
63
|
+
/**
|
|
64
|
+
* Enable the built-in API documentation surface — a `/openapi.json` route
|
|
65
|
+
* that serves the live OpenAPI 3.1 spec and a `/docs` route that serves a
|
|
66
|
+
* Scalar (default) or Swagger UI HTML page that loads it. This is the
|
|
67
|
+
* one-line equivalent of FastAPI's automatic `/docs` UI.
|
|
68
|
+
*
|
|
69
|
+
* - `false` (default) — never mount. Write your own with `generateOpenAPI`
|
|
70
|
+
* + `swaggerUiHtml` / `scalarHtml` for full control.
|
|
71
|
+
* - `true` — always mount on `/openapi.json` and `/docs`.
|
|
72
|
+
* - `"auto"` — mount when `NODE_ENV !== "production"`; otherwise skip. This
|
|
73
|
+
* is a "secure by default" choice: production deployments should opt in
|
|
74
|
+
* explicitly so internal APIs do not accidentally publish a browsable
|
|
75
|
+
* schema.
|
|
76
|
+
* - object — full configuration (custom paths, UI choice, title, CSP
|
|
77
|
+
* overrides). The `enabled` field on the object can override the
|
|
78
|
+
* auto/prod rule.
|
|
79
|
+
*
|
|
80
|
+
* The default is `false` so adding a new `App({ ... })` to an existing app
|
|
81
|
+
* never silently changes its public surface; scaffolded projects from
|
|
82
|
+
* `create-daloy` set `docs: true` for the auto-mount experience.
|
|
83
|
+
*
|
|
84
|
+
* @since 0.3.0
|
|
85
|
+
*/
|
|
86
|
+
docs?: boolean | "auto" | DocsRouteOptions;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Subset of {@link OpenAPIOptions} accepted by `new App({ openapi })`. All
|
|
90
|
+
* fields are optional; `info.title` / `info.version` fall back to the
|
|
91
|
+
* top-level {@link AppOptions.title} / {@link AppOptions.version}.
|
|
92
|
+
*
|
|
93
|
+
* @since 0.3.0
|
|
94
|
+
*/
|
|
95
|
+
export interface AppOpenAPIOptions {
|
|
96
|
+
info?: Partial<OpenAPIInfo>;
|
|
97
|
+
servers?: OpenAPIOptions["servers"];
|
|
98
|
+
securitySchemes?: OpenAPIOptions["securitySchemes"];
|
|
99
|
+
webhooks?: OpenAPIOptions["webhooks"];
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Configuration for the auto-mounted docs surface. Pass to
|
|
103
|
+
* `new App({ docs: { ... } })` to override defaults; omit to take all
|
|
104
|
+
* defaults.
|
|
105
|
+
*
|
|
106
|
+
* @since 0.3.0
|
|
107
|
+
*/
|
|
108
|
+
export interface DocsRouteOptions {
|
|
109
|
+
/** Path the docs UI is served from. Default `"/docs"`. */
|
|
110
|
+
path?: PathString;
|
|
111
|
+
/** Path the OpenAPI 3.1 JSON spec is served from. Default `"/openapi.json"`. */
|
|
112
|
+
openapiPath?: PathString;
|
|
113
|
+
/**
|
|
114
|
+
* Path the OpenAPI 3.1 YAML spec is served from. Default `"/openapi.yaml"`.
|
|
115
|
+
* Set to `false` to disable the YAML route.
|
|
116
|
+
*
|
|
117
|
+
* @since 0.13.1
|
|
118
|
+
*/
|
|
119
|
+
openapiYamlPath?: PathString | false;
|
|
120
|
+
/** Which built-in UI to render. Default `"scalar"` (smaller payload, modern UI). */
|
|
121
|
+
ui?: "scalar" | "swagger";
|
|
122
|
+
/** Page `<title>`. Defaults to the resolved OpenAPI `info.title`. */
|
|
123
|
+
title?: string;
|
|
124
|
+
/**
|
|
125
|
+
* Force the docs to mount regardless of `NODE_ENV`. When `"auto"` (default
|
|
126
|
+
* for the object form), behaves like the top-level `docs: "auto"` setting.
|
|
127
|
+
*/
|
|
128
|
+
enabled?: boolean | "auto";
|
|
129
|
+
/**
|
|
130
|
+
* Tags attached to the auto-mounted operations in the generated spec.
|
|
131
|
+
* Default: `["Docs"]`. Pass an empty array to omit tags entirely.
|
|
132
|
+
*/
|
|
133
|
+
tags?: string[];
|
|
134
|
+
/**
|
|
135
|
+
* Override the Content-Security-Policy applied to the docs HTML response.
|
|
136
|
+
* Forwarded to {@link docsContentSecurityPolicy}.
|
|
137
|
+
*/
|
|
138
|
+
csp?: DocsContentSecurityPolicyOptions;
|
|
50
139
|
}
|
|
51
140
|
/** Information passed to {@link App.onPluginInstalled} listeners. */
|
|
52
141
|
export interface PluginInstalledEvent {
|
|
@@ -161,6 +250,21 @@ export declare class App {
|
|
|
161
250
|
private inflight;
|
|
162
251
|
private draining;
|
|
163
252
|
constructor(options?: AppOptions);
|
|
253
|
+
/**
|
|
254
|
+
* Resolve whether the app is running in production. Honours the explicit
|
|
255
|
+
* `production` option first, then falls back to `NODE_ENV === "production"`.
|
|
256
|
+
* Used by the docs auto-mount and error response detail stripping.
|
|
257
|
+
*/
|
|
258
|
+
private isProduction;
|
|
259
|
+
/**
|
|
260
|
+
* Resolve the {@link AppOptions.docs} option and, when enabled, register
|
|
261
|
+
* the `/openapi.json` + `/docs` routes. Called once during construction so
|
|
262
|
+
* the routes appear in `app.routes` for introspection and so the spec
|
|
263
|
+
* served at runtime includes every route registered afterwards (the spec
|
|
264
|
+
* is generated lazily inside the request handler).
|
|
265
|
+
*/
|
|
266
|
+
private maybeMountDocs;
|
|
267
|
+
private mountDocs;
|
|
164
268
|
/**
|
|
165
269
|
* Register a single route on the application.
|
|
166
270
|
*
|
|
@@ -386,4 +490,28 @@ export declare class App {
|
|
|
386
490
|
*/
|
|
387
491
|
shutdown(timeoutMs?: number, reason?: string): Promise<void>;
|
|
388
492
|
}
|
|
493
|
+
/**
|
|
494
|
+
* Factory alias for `new App(options)`. Lets callers who prefer a
|
|
495
|
+
* functional style (or who avoid `new`) write:
|
|
496
|
+
*
|
|
497
|
+
* ```ts
|
|
498
|
+
* import { createApp } from "@daloyjs/core";
|
|
499
|
+
*
|
|
500
|
+
* const app = createApp({ openapi: { info: { title: "My API", version: "1.0.0" } }, docs: true });
|
|
501
|
+
* ```
|
|
502
|
+
*
|
|
503
|
+
* Behaviour is identical to `new App(options)` — the alias exists purely
|
|
504
|
+
* for ergonomics and matches the factory pattern used by Express, Fastify,
|
|
505
|
+
* and Hono adapters.
|
|
506
|
+
*
|
|
507
|
+
* @since 0.3.0
|
|
508
|
+
*/
|
|
509
|
+
export declare function createApp(options?: AppOptions): App;
|
|
510
|
+
/**
|
|
511
|
+
* Test helper: clear the cached package.json read so each test starts
|
|
512
|
+
* from a fresh lookup. Not part of the public API.
|
|
513
|
+
*
|
|
514
|
+
* @internal
|
|
515
|
+
*/
|
|
516
|
+
export declare function _resetPackageJsonCacheForTests(): void;
|
|
389
517
|
//# sourceMappingURL=app.d.ts.map
|
package/dist/app.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,KAAK,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAc1E,OAAO,EAA4B,KAAK,MAAM,EAAE,MAAM,aAAa,CAAC;AACpE,OAAO,KAAK,EAEV,UAAU,EACV,KAAK,EACL,UAAU,EACV,cAAc,EACd,YAAY,EACZ,eAAe,EAChB,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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"}
|
package/dist/app.js
CHANGED
|
@@ -4,6 +4,8 @@ import { BadRequestError, HttpError, InternalError, MethodNotAllowedError, NotFo
|
|
|
4
4
|
import { validate } from "./schema.js";
|
|
5
5
|
import { readBodyLimited, safeJsonParse, randomId } from "./security.js";
|
|
6
6
|
import { createLogger, noopLogger } from "./logger.js";
|
|
7
|
+
import { generateOpenAPI, openapiToYAML, } from "./openapi.js";
|
|
8
|
+
import { docsContentSecurityPolicy, scalarHtml, swaggerUiHtml, } from "./docs.js";
|
|
7
9
|
const DEFAULTS = {
|
|
8
10
|
bodyLimitBytes: 1024 * 1024,
|
|
9
11
|
requestTimeoutMs: 30_000,
|
|
@@ -96,6 +98,144 @@ export class App {
|
|
|
96
98
|
typeof options.logger.info === "function"
|
|
97
99
|
? options.logger
|
|
98
100
|
: createLogger({ level: options.logger?.level ?? "info" });
|
|
101
|
+
this.maybeMountDocs();
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Resolve whether the app is running in production. Honours the explicit
|
|
105
|
+
* `production` option first, then falls back to `NODE_ENV === "production"`.
|
|
106
|
+
* Used by the docs auto-mount and error response detail stripping.
|
|
107
|
+
*/
|
|
108
|
+
isProduction() {
|
|
109
|
+
if (this.options.production !== undefined)
|
|
110
|
+
return this.options.production;
|
|
111
|
+
return (typeof process !== "undefined" &&
|
|
112
|
+
typeof process.env !== "undefined" &&
|
|
113
|
+
process.env.NODE_ENV === "production");
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Resolve the {@link AppOptions.docs} option and, when enabled, register
|
|
117
|
+
* the `/openapi.json` + `/docs` routes. Called once during construction so
|
|
118
|
+
* the routes appear in `app.routes` for introspection and so the spec
|
|
119
|
+
* served at runtime includes every route registered afterwards (the spec
|
|
120
|
+
* is generated lazily inside the request handler).
|
|
121
|
+
*/
|
|
122
|
+
maybeMountDocs() {
|
|
123
|
+
const raw = this.options.docs;
|
|
124
|
+
if (raw === undefined || raw === false)
|
|
125
|
+
return;
|
|
126
|
+
let resolvedOpts;
|
|
127
|
+
if (raw === true) {
|
|
128
|
+
resolvedOpts = {};
|
|
129
|
+
}
|
|
130
|
+
else if (raw === "auto") {
|
|
131
|
+
if (this.isProduction())
|
|
132
|
+
return;
|
|
133
|
+
resolvedOpts = {};
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
// object form
|
|
137
|
+
const enabled = raw.enabled ?? true;
|
|
138
|
+
if (enabled === false)
|
|
139
|
+
return;
|
|
140
|
+
if (enabled === "auto" && this.isProduction())
|
|
141
|
+
return;
|
|
142
|
+
resolvedOpts = raw;
|
|
143
|
+
}
|
|
144
|
+
this.mountDocs(resolvedOpts);
|
|
145
|
+
}
|
|
146
|
+
mountDocs(opts) {
|
|
147
|
+
const openapiPath = (opts.openapiPath ?? "/openapi.json");
|
|
148
|
+
const openapiYamlPath = opts.openapiYamlPath === false
|
|
149
|
+
? null
|
|
150
|
+
: (opts.openapiYamlPath ?? "/openapi.yaml");
|
|
151
|
+
const docsPath = (opts.path ?? "/docs");
|
|
152
|
+
const ui = opts.ui ?? "scalar";
|
|
153
|
+
const tags = opts.tags ?? ["Docs"];
|
|
154
|
+
// Best-effort lazy read of the host project's package.json so that
|
|
155
|
+
// `new App({ docs: true })` with no explicit `openapi.info` still produces
|
|
156
|
+
// a spec titled after the user's package (`name` → title,
|
|
157
|
+
// `version` → version, `description` → description). Silently skipped on
|
|
158
|
+
// edge runtimes that lack `node:fs`.
|
|
159
|
+
const resolveInfo = async () => {
|
|
160
|
+
const fromOpenapi = this.options.openapi?.info ?? {};
|
|
161
|
+
const fromPkg = await readHostPackageJsonInfo();
|
|
162
|
+
const title = fromOpenapi.title ?? this.options.title ?? fromPkg.title ?? "DaloyJS API";
|
|
163
|
+
const version = fromOpenapi.version ?? this.options.version ?? fromPkg.version ?? "0.0.0";
|
|
164
|
+
const description = fromOpenapi.description ?? this.options.description ?? fromPkg.description;
|
|
165
|
+
return description ? { title, version, description } : { title, version };
|
|
166
|
+
};
|
|
167
|
+
const generate = async () => generateOpenAPI(this, {
|
|
168
|
+
info: await resolveInfo(),
|
|
169
|
+
...(this.options.openapi?.servers
|
|
170
|
+
? { servers: this.options.openapi.servers }
|
|
171
|
+
: {}),
|
|
172
|
+
...(this.options.openapi?.securitySchemes
|
|
173
|
+
? { securitySchemes: this.options.openapi.securitySchemes }
|
|
174
|
+
: {}),
|
|
175
|
+
...(this.options.openapi?.webhooks
|
|
176
|
+
? { webhooks: this.options.openapi.webhooks }
|
|
177
|
+
: {}),
|
|
178
|
+
});
|
|
179
|
+
this.route({
|
|
180
|
+
method: "GET",
|
|
181
|
+
path: openapiPath,
|
|
182
|
+
operationId: "getOpenAPIDocument",
|
|
183
|
+
...(tags.length ? { tags } : {}),
|
|
184
|
+
summary: "OpenAPI 3.1 document",
|
|
185
|
+
responses: {
|
|
186
|
+
200: { description: "OpenAPI 3.1 document for this application." },
|
|
187
|
+
},
|
|
188
|
+
handler: async () => ({
|
|
189
|
+
status: 200,
|
|
190
|
+
body: await generate(),
|
|
191
|
+
}),
|
|
192
|
+
});
|
|
193
|
+
if (openapiYamlPath) {
|
|
194
|
+
this.route({
|
|
195
|
+
method: "GET",
|
|
196
|
+
path: openapiYamlPath,
|
|
197
|
+
operationId: "getOpenAPIDocumentYaml",
|
|
198
|
+
...(tags.length ? { tags } : {}),
|
|
199
|
+
summary: "OpenAPI 3.1 document (YAML)",
|
|
200
|
+
responses: {
|
|
201
|
+
200: { description: "OpenAPI 3.1 document for this application, in YAML." },
|
|
202
|
+
},
|
|
203
|
+
handler: async () => ({
|
|
204
|
+
status: 200,
|
|
205
|
+
body: openapiToYAML(await generate()),
|
|
206
|
+
headers: {
|
|
207
|
+
"content-type": "application/yaml; charset=utf-8",
|
|
208
|
+
"x-content-type-options": "nosniff",
|
|
209
|
+
},
|
|
210
|
+
}),
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
this.route({
|
|
214
|
+
method: "GET",
|
|
215
|
+
path: docsPath,
|
|
216
|
+
operationId: "getDocsUI",
|
|
217
|
+
...(tags.length ? { tags } : {}),
|
|
218
|
+
summary: "Interactive API reference",
|
|
219
|
+
responses: {
|
|
220
|
+
200: { description: "Interactive API documentation UI." },
|
|
221
|
+
},
|
|
222
|
+
handler: async () => {
|
|
223
|
+
const title = opts.title ?? (await resolveInfo()).title;
|
|
224
|
+
const html = ui === "swagger"
|
|
225
|
+
? swaggerUiHtml({ specUrl: openapiPath, title })
|
|
226
|
+
: scalarHtml({ specUrl: openapiPath, title });
|
|
227
|
+
return {
|
|
228
|
+
status: 200,
|
|
229
|
+
body: html,
|
|
230
|
+
headers: {
|
|
231
|
+
"content-type": "text/html; charset=utf-8",
|
|
232
|
+
"content-security-policy": docsContentSecurityPolicy(opts.csp),
|
|
233
|
+
"x-content-type-options": "nosniff",
|
|
234
|
+
"referrer-policy": "no-referrer",
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
},
|
|
238
|
+
});
|
|
99
239
|
}
|
|
100
240
|
// ---------- registration ----------
|
|
101
241
|
/**
|
|
@@ -172,7 +312,10 @@ export class App {
|
|
|
172
312
|
* @returns This `App` instance for chaining.
|
|
173
313
|
*/
|
|
174
314
|
group(prefix, config, register) {
|
|
175
|
-
|
|
315
|
+
// Child apps share the parent's router/routes/etc. Disable docs auto-mount
|
|
316
|
+
// on the child so it does not re-register the parent's `/openapi.json` and
|
|
317
|
+
// `/docs` routes (which would throw "Duplicate route").
|
|
318
|
+
const child = new App({ ...this.options, docs: false });
|
|
176
319
|
child.router = this.router;
|
|
177
320
|
child.routes = this.routes;
|
|
178
321
|
child.webSocketRoutes = this.webSocketRoutes;
|
|
@@ -884,4 +1027,125 @@ function serializeErr(err) {
|
|
|
884
1027
|
}
|
|
885
1028
|
return { value: String(err) };
|
|
886
1029
|
}
|
|
1030
|
+
/**
|
|
1031
|
+
* Factory alias for `new App(options)`. Lets callers who prefer a
|
|
1032
|
+
* functional style (or who avoid `new`) write:
|
|
1033
|
+
*
|
|
1034
|
+
* ```ts
|
|
1035
|
+
* import { createApp } from "@daloyjs/core";
|
|
1036
|
+
*
|
|
1037
|
+
* const app = createApp({ openapi: { info: { title: "My API", version: "1.0.0" } }, docs: true });
|
|
1038
|
+
* ```
|
|
1039
|
+
*
|
|
1040
|
+
* Behaviour is identical to `new App(options)` — the alias exists purely
|
|
1041
|
+
* for ergonomics and matches the factory pattern used by Express, Fastify,
|
|
1042
|
+
* and Hono adapters.
|
|
1043
|
+
*
|
|
1044
|
+
* @since 0.3.0
|
|
1045
|
+
*/
|
|
1046
|
+
export function createApp(options = {}) {
|
|
1047
|
+
return new App(options);
|
|
1048
|
+
}
|
|
1049
|
+
const PACKAGE_JSON_CACHE = {};
|
|
1050
|
+
/**
|
|
1051
|
+
* Best-effort lazy read of the host project's `package.json` so that
|
|
1052
|
+
* `new App({ docs: true })` with no explicit `openapi.info` still produces
|
|
1053
|
+
* a spec titled after the user's package. Reads `package.json` first; if
|
|
1054
|
+
* none is found while walking up from `process.cwd()`, falls back to
|
|
1055
|
+
* `deno.json` / `deno.jsonc` so Deno projects get the same DX without a
|
|
1056
|
+
* `package.json`. Returns an empty object on edge runtimes (Cloudflare
|
|
1057
|
+
* Workers, Vercel Edge) where `node:fs` is absent, on any I/O or parse
|
|
1058
|
+
* error, and when nothing is found.
|
|
1059
|
+
*
|
|
1060
|
+
* The result is memoized at module scope: manifests do not change
|
|
1061
|
+
* during a process lifetime and we never want this to add latency to
|
|
1062
|
+
* subsequent docs requests.
|
|
1063
|
+
*/
|
|
1064
|
+
function readHostPackageJsonInfo() {
|
|
1065
|
+
if (PACKAGE_JSON_CACHE.value !== undefined)
|
|
1066
|
+
return PACKAGE_JSON_CACHE.value;
|
|
1067
|
+
const promise = (async () => {
|
|
1068
|
+
const empty = {};
|
|
1069
|
+
const proc = globalThis.process;
|
|
1070
|
+
if (!proc || typeof proc.cwd !== "function")
|
|
1071
|
+
return empty;
|
|
1072
|
+
let fs;
|
|
1073
|
+
let path;
|
|
1074
|
+
try {
|
|
1075
|
+
fs = await import("node:fs");
|
|
1076
|
+
path = await import("node:path");
|
|
1077
|
+
}
|
|
1078
|
+
catch {
|
|
1079
|
+
return empty;
|
|
1080
|
+
}
|
|
1081
|
+
let dir;
|
|
1082
|
+
try {
|
|
1083
|
+
dir = proc.cwd();
|
|
1084
|
+
}
|
|
1085
|
+
catch {
|
|
1086
|
+
return empty;
|
|
1087
|
+
}
|
|
1088
|
+
const parseManifest = (raw, allowComments) => {
|
|
1089
|
+
// deno.jsonc allows // line comments and /* block */ comments. Strip
|
|
1090
|
+
// them before parsing — naively, but well enough for typical manifests.
|
|
1091
|
+
const text = allowComments
|
|
1092
|
+
? raw
|
|
1093
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
1094
|
+
.replace(/(^|[^:\\])\/\/.*$/gm, "$1")
|
|
1095
|
+
: raw;
|
|
1096
|
+
return JSON.parse(text);
|
|
1097
|
+
};
|
|
1098
|
+
const extractInfo = (json) => {
|
|
1099
|
+
const result = {};
|
|
1100
|
+
if (typeof json.name === "string" && json.name.length > 0) {
|
|
1101
|
+
result.title = json.name;
|
|
1102
|
+
}
|
|
1103
|
+
if (typeof json.version === "string" && json.version.length > 0) {
|
|
1104
|
+
result.version = json.version;
|
|
1105
|
+
}
|
|
1106
|
+
if (typeof json.description === "string" && json.description.length > 0) {
|
|
1107
|
+
result.description = json.description;
|
|
1108
|
+
}
|
|
1109
|
+
return result;
|
|
1110
|
+
};
|
|
1111
|
+
// Walk up to the filesystem root looking for a manifest. Cap depth so
|
|
1112
|
+
// a deeply-nested cwd can't cause excessive stat calls. At each level,
|
|
1113
|
+
// prefer package.json, then deno.json, then deno.jsonc.
|
|
1114
|
+
for (let i = 0; i < 12; i++) {
|
|
1115
|
+
const pkg = path.join(dir, "package.json");
|
|
1116
|
+
const denoJson = path.join(dir, "deno.json");
|
|
1117
|
+
const denoJsonc = path.join(dir, "deno.jsonc");
|
|
1118
|
+
try {
|
|
1119
|
+
if (fs.existsSync(pkg)) {
|
|
1120
|
+
return extractInfo(parseManifest(fs.readFileSync(pkg, "utf8"), false));
|
|
1121
|
+
}
|
|
1122
|
+
if (fs.existsSync(denoJson)) {
|
|
1123
|
+
return extractInfo(parseManifest(fs.readFileSync(denoJson, "utf8"), false));
|
|
1124
|
+
}
|
|
1125
|
+
if (fs.existsSync(denoJsonc)) {
|
|
1126
|
+
return extractInfo(parseManifest(fs.readFileSync(denoJsonc, "utf8"), true));
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
catch {
|
|
1130
|
+
return empty;
|
|
1131
|
+
}
|
|
1132
|
+
const parent = path.dirname(dir);
|
|
1133
|
+
if (parent === dir)
|
|
1134
|
+
break;
|
|
1135
|
+
dir = parent;
|
|
1136
|
+
}
|
|
1137
|
+
return empty;
|
|
1138
|
+
})();
|
|
1139
|
+
PACKAGE_JSON_CACHE.value = promise;
|
|
1140
|
+
return promise;
|
|
1141
|
+
}
|
|
1142
|
+
/**
|
|
1143
|
+
* Test helper: clear the cached package.json read so each test starts
|
|
1144
|
+
* from a fresh lookup. Not part of the public API.
|
|
1145
|
+
*
|
|
1146
|
+
* @internal
|
|
1147
|
+
*/
|
|
1148
|
+
export function _resetPackageJsonCacheForTests() {
|
|
1149
|
+
PACKAGE_JSON_CACHE.value = undefined;
|
|
1150
|
+
}
|
|
887
1151
|
//# sourceMappingURL=app.js.map
|