@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 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 / Scalar)
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 { swaggerUiHtml, htmlResponse } from "@daloyjs/core/docs";
189
- // returns a self-contained HTML page that loads /openapi.json
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
- Mount at `/docs` and the UI is always contract-accurate never stale.
193
- `create-daloy@0.2.0` mounts Swagger UI at `/docs` and the live spec at `/openapi.json` by default.
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":"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"}
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
- const child = new App(this.options);
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