@mandujs/core 0.53.3 → 0.54.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 +654 -654
- package/package.json +2 -2
- package/src/a11y/__tests__/run-audit.test.ts +333 -333
- package/src/a11y/fix-hints.ts +76 -76
- package/src/a11y/index.ts +18 -18
- package/src/a11y/run-audit.ts +394 -394
- package/src/a11y/types.ts +125 -125
- package/src/auth/__tests__/login.test.ts +1 -1
- package/src/auth/__tests__/password.test.ts +122 -122
- package/src/auth/__tests__/tokens.test.ts +274 -274
- package/src/auth/__tests__/verification.test.ts +274 -274
- package/src/auth/index.ts +76 -76
- package/src/auth/login.ts +225 -225
- package/src/auth/password.ts +120 -120
- package/src/auth/reset.ts +243 -243
- package/src/auth/tokens.ts +612 -612
- package/src/auth/verification.ts +253 -253
- package/src/brain/__tests__/redactor.test.ts +94 -94
- package/src/brain/adapters/__tests__/_helpers.ts +83 -83
- package/src/brain/adapters/__tests__/anthropic-oauth.test.ts +196 -196
- package/src/brain/adapters/__tests__/chatgpt-auth.test.ts +193 -193
- package/src/brain/adapters/__tests__/openai-oauth.test.ts +209 -209
- package/src/brain/adapters/__tests__/resolver.test.ts +143 -143
- package/src/brain/adapters/anthropic-oauth.ts +1 -1
- package/src/brain/adapters/chatgpt-auth.ts +300 -300
- package/src/brain/adapters/index.ts +319 -319
- package/src/brain/adapters/oauth-flow.ts +439 -439
- package/src/brain/consent.ts +240 -240
- package/src/brain/credentials.ts +396 -396
- package/src/bundler/__tests__/build-runner.ts +113 -113
- package/src/bundler/__tests__/dev-reliability.test.ts +619 -619
- package/src/bundler/__tests__/extended-watch.test.ts +711 -711
- package/src/bundler/__tests__/fast-refresh.test.ts +10 -10
- package/src/bundler/__tests__/hdr.test.ts +353 -353
- package/src/bundler/__tests__/hmr-client.test.ts +532 -532
- package/src/bundler/__tests__/manifest-schema.test.ts +266 -266
- package/src/bundler/__tests__/prod-smoke.test.ts +138 -138
- package/src/bundler/__tests__/reverse-import-graph.test.ts +519 -519
- package/src/bundler/__tests__/slot-dispatch.test.ts +573 -573
- package/src/bundler/__tests__/url-cap-and-slot-regex.test.ts +286 -286
- package/src/bundler/__tests__/vendor-cache.test.ts +455 -455
- package/src/bundler/budget.ts +404 -404
- package/src/bundler/build.test.ts +179 -179
- package/src/bundler/build.ts +55 -55
- package/src/bundler/dev.ts +42 -42
- package/src/bundler/manifest-schema.ts +301 -301
- package/src/bundler/plugins/__tests__/block-generated-imports.test.ts +263 -263
- package/src/bundler/plugins/__tests__/react-compiler-config.test.ts +83 -83
- package/src/bundler/plugins/__tests__/react-compiler-lint.test.ts +110 -110
- package/src/bundler/plugins/block-generated-imports.ts +155 -155
- package/src/bundler/plugins/index.ts +83 -83
- package/src/bundler/plugins/react-compiler-config.ts +108 -108
- package/src/bundler/plugins/react-compiler-lint.ts +253 -253
- package/src/bundler/plugins/react-compiler.ts +162 -162
- package/src/bundler/reverse-import-graph.ts +339 -339
- package/src/bundler/safe-build.test.ts +201 -201
- package/src/bundler/safe-build.ts +103 -103
- package/src/bundler/scenario-matrix.ts +229 -229
- package/src/bundler/types.ts +10 -10
- package/src/bundler/vendor-cache-types.ts +130 -130
- package/src/bundler/vendor-cache.ts +526 -526
- package/src/change/snapshot.ts +18 -18
- package/src/client/Form.tsx +105 -105
- package/src/client/Link.tsx +9 -9
- package/src/client/__tests__/use-sse.test.ts +153 -153
- package/src/client/globals.ts +1 -1
- package/src/client/hooks.ts +362 -362
- package/src/client/hydrate.ts +340 -340
- package/src/client/prefetch-helper.ts +55 -55
- package/src/client/router.ts +11 -11
- package/src/client/runtime.ts +47 -47
- package/src/client/serialize.ts +404 -404
- package/src/client/use-fetch.ts +6 -6
- package/src/client/use-head.ts +197 -197
- package/src/client/use-sse.ts +378 -378
- package/src/client/window-state.ts +101 -101
- package/src/components/Image.tsx +162 -162
- package/src/config/validate.ts +3 -3
- package/src/config/watcher.ts +311 -311
- package/src/constants.ts +40 -40
- package/src/content/collection.ts +8 -8
- package/src/content/content-layer.ts +7 -7
- package/src/content/data-store.ts +245 -245
- package/src/content/frontmatter.ts +189 -189
- package/src/content/loader-context.ts +171 -171
- package/src/content/loaders/api.ts +216 -216
- package/src/content/loaders/file.ts +172 -172
- package/src/content/loaders/glob.ts +253 -253
- package/src/content/loaders/index.ts +34 -34
- package/src/content/loaders/types.ts +137 -137
- package/src/content/meta-store.ts +209 -209
- package/src/content/prebuild.test.ts +571 -571
- package/src/content/prebuild.ts +636 -636
- package/src/content/schema.ts +20 -20
- package/src/content/sidebar.ts +630 -630
- package/src/content/slug.ts +110 -110
- package/src/content/types.ts +282 -282
- package/src/content/watcher.ts +135 -135
- package/src/contract/client-safe.test.ts +42 -42
- package/src/contract/client-safe.ts +114 -114
- package/src/contract/define.ts +11 -11
- package/src/contract/index.ts +1 -1
- package/src/contract/normalize.test.ts +276 -276
- package/src/contract/normalize.ts +410 -410
- package/src/contract/registry.test.ts +206 -206
- package/src/contract/route-helpers.ts +1 -1
- package/src/contract/rpc.ts +443 -443
- package/src/contract/schema.ts +48 -48
- package/src/contract/types.ts +58 -58
- package/src/db/__tests__/db.test.ts +482 -482
- package/src/db/index.ts +138 -138
- package/src/db/migrations/history-table.ts +345 -345
- package/src/db/migrations/index.ts +3 -3
- package/src/db/migrations/lock.ts +324 -324
- package/src/db/migrations/runner.ts +650 -650
- package/src/deploy/cache.ts +140 -140
- package/src/deploy/compile/vercel.ts +344 -344
- package/src/deploy/index.ts +87 -87
- package/src/deploy/inference/brain.ts +268 -268
- package/src/deploy/inference/context.ts +182 -182
- package/src/deploy/inference/filling-extract.ts +245 -245
- package/src/deploy/inference/heuristic.ts +182 -182
- package/src/deploy/intent.ts +173 -173
- package/src/deploy/plan.ts +178 -178
- package/src/design/__tests__/agents-link.test.ts +109 -109
- package/src/design/__tests__/extract-patch-diff.test.ts +265 -265
- package/src/design/__tests__/lint.test.ts +110 -110
- package/src/design/__tests__/parser.test.ts +195 -195
- package/src/design/__tests__/tailwind-theme.test.ts +229 -229
- package/src/design/agents-link.ts +165 -165
- package/src/design/diff.ts +138 -138
- package/src/design/extract.ts +285 -285
- package/src/design/index.ts +102 -102
- package/src/design/lint.ts +209 -209
- package/src/design/parser.ts +555 -555
- package/src/design/patch.ts +241 -241
- package/src/design/scaffold.ts +147 -147
- package/src/design/tailwind-theme.ts +441 -441
- package/src/design/types.ts +210 -210
- package/src/desktop/__tests__/webview-fallback.test.ts +254 -254
- package/src/desktop/__tests__/window.test.ts +248 -248
- package/src/desktop/__tests__/worker.test.ts +266 -266
- package/src/desktop/index.ts +43 -43
- package/src/desktop/types.ts +158 -158
- package/src/desktop/worker.ts +180 -180
- package/src/dev-error-overlay/__tests__/overlay-injector.test.ts +241 -241
- package/src/dev-error-overlay/index.ts +30 -30
- package/src/dev-error-overlay/overlay-injector.ts +243 -243
- package/src/dev-error-overlay/overlay-styles.ts +52 -52
- package/src/dev-error-overlay/types.ts +66 -66
- package/src/devtools/ai/context-builder.ts +375 -375
- package/src/devtools/ai/index.ts +25 -25
- package/src/devtools/ai/mcp-connector.ts +25 -25
- package/src/devtools/client/catchers/error-catcher.ts +344 -344
- package/src/devtools/client/catchers/index.ts +18 -18
- package/src/devtools/client/components/index.ts +39 -39
- package/src/devtools/client/components/mandu-character.tsx +331 -331
- package/src/devtools/client/components/overlay.tsx +368 -368
- package/src/devtools/client/components/panel/errors-panel.tsx +259 -259
- package/src/devtools/client/components/panel/guard-panel.tsx +30 -30
- package/src/devtools/client/components/panel/islands-panel.tsx +320 -320
- package/src/devtools/client/components/panel/network-panel.tsx +291 -291
- package/src/devtools/client/components/panel/panel-container.tsx +500 -500
- package/src/devtools/client/components/panel/preview-panel.tsx +46 -46
- package/src/devtools/client/filters/context-filters.ts +282 -282
- package/src/devtools/client/filters/index.ts +16 -16
- package/src/devtools/client/index.ts +63 -63
- package/src/devtools/client/persistence.ts +335 -335
- package/src/devtools/hook/create-hook.ts +207 -207
- package/src/devtools/hook/index.ts +13 -13
- package/src/devtools/index.ts +439 -439
- package/src/devtools/init.ts +265 -265
- package/src/devtools/protocol.ts +237 -237
- package/src/devtools/server/index.ts +17 -17
- package/src/devtools/server/source-context.ts +450 -450
- package/src/devtools/types.ts +35 -35
- package/src/devtools/worker/index.ts +25 -25
- package/src/devtools/worker/redaction-worker.ts +233 -233
- package/src/devtools/worker/worker-manager.ts +410 -410
- package/src/diagnose/__tests__/checks.test.ts +451 -451
- package/src/diagnose/checks.ts +832 -832
- package/src/diagnose/index.ts +17 -17
- package/src/diagnose/run.ts +93 -93
- package/src/diagnose/types.ts +53 -53
- package/src/email/__tests__/email.test.ts +355 -355
- package/src/email/index.ts +282 -282
- package/src/email/smtp.ts +64 -64
- package/src/error/domains.ts +265 -265
- package/src/error/result.ts +60 -60
- package/src/error/types.ts +6 -6
- package/src/errors/extractor.ts +409 -409
- package/src/errors/index.ts +19 -19
- package/src/filling/__tests__/session-sqlite.test.ts +454 -454
- package/src/filling/auth.ts +308 -308
- package/src/filling/cookie-codec.ts +299 -299
- package/src/filling/deps.ts +265 -265
- package/src/filling/session-sqlite.ts +617 -617
- package/src/filling/sse.ts +5 -5
- package/src/filling/ws.ts +78 -78
- package/src/generator/index.ts +3 -3
- package/src/guard/__tests__/design-inline-class.test.ts +219 -219
- package/src/guard/__tests__/tsgolint-bridge.test.ts +347 -347
- package/src/guard/analyzer.ts +360 -360
- package/src/guard/auto-correct.ts +1 -1
- package/src/guard/contract-guard.ts +9 -9
- package/src/guard/define-rule.ts +243 -243
- package/src/guard/design-inline-class.ts +393 -393
- package/src/guard/file-type.test.ts +24 -24
- package/src/guard/graph.ts +1 -1
- package/src/guard/healing.ts +36 -36
- package/src/guard/presets/atomic.ts +70 -70
- package/src/guard/presets/clean.ts +77 -77
- package/src/guard/presets/fsd.ts +79 -79
- package/src/guard/presets/hexagonal.ts +68 -68
- package/src/guard/reporter.ts +442 -442
- package/src/guard/rule-presets.ts +379 -379
- package/src/guard/semantic-slots.ts +1 -1
- package/src/guard/suggestions.ts +358 -358
- package/src/guard/tsgolint-bridge.ts +512 -512
- package/src/guard/types.ts +348 -348
- package/src/guard/watcher.ts +405 -405
- package/src/i18n/define.ts +126 -126
- package/src/i18n/index.ts +52 -52
- package/src/i18n/message-registry.ts +173 -173
- package/src/i18n/types.ts +112 -112
- package/src/id/index.ts +105 -105
- package/src/index.ts +2 -2
- package/src/kitchen/api/agent-devtools-api.ts +779 -544
- package/src/kitchen/api/errors-grouping.ts +126 -0
- package/src/kitchen/kitchen-handler.ts +192 -63
- package/src/kitchen/kitchen-ui.ts +842 -464
- package/src/logging/index.ts +22 -22
- package/src/logging/transports.ts +365 -365
- package/src/middleware/bridge.ts +147 -147
- package/src/middleware/compose.ts +134 -134
- package/src/middleware/compress.ts +62 -62
- package/src/middleware/cors.ts +47 -47
- package/src/middleware/csrf.ts +328 -328
- package/src/middleware/define.ts +132 -132
- package/src/middleware/jwt.ts +134 -134
- package/src/middleware/logger.ts +58 -58
- package/src/middleware/oauth/__tests__/oauth.test.ts +1 -1
- package/src/middleware/oauth/index.ts +505 -505
- package/src/middleware/oauth/providers.ts +115 -115
- package/src/middleware/rate-limit/index.ts +522 -522
- package/src/middleware/rate-limit/sqlite-store.ts +382 -382
- package/src/middleware/scheduler-cron.ts +96 -96
- package/src/middleware/secure/__tests__/secure.test.ts +360 -360
- package/src/middleware/secure/csp.ts +193 -193
- package/src/middleware/session.ts +174 -174
- package/src/middleware/timeout.ts +55 -55
- package/src/observability/logger-adapter.ts +36 -36
- package/src/observability/sqlite-store.ts +254 -254
- package/src/openapi/generator.ts +1 -1
- package/src/openapi/openapi.test.ts +43 -43
- package/src/perf/__tests__/user-marks.test.ts +354 -354
- package/src/perf/index.ts +133 -133
- package/src/plugins/__tests__/lifecycle-integration.test.ts +272 -272
- package/src/plugins/__tests__/runner.test.ts +409 -409
- package/src/plugins/define.ts +124 -124
- package/src/plugins/examples/dep-check-plugin.ts +80 -80
- package/src/plugins/examples/prerender-cache-plugin.ts +111 -111
- package/src/plugins/examples/sitemap-plugin.ts +65 -65
- package/src/plugins/runner.ts +361 -361
- package/src/plugins/types.ts +368 -368
- package/src/report/index.ts +1 -1
- package/src/resource/__tests__/generator.test.ts +32 -32
- package/src/resource/ddl/__tests__/diff.test.ts +639 -639
- package/src/resource/ddl/__tests__/emit.test.ts +823 -823
- package/src/resource/ddl/__tests__/snapshot.test.ts +499 -499
- package/src/resource/ddl/emit.ts +559 -559
- package/src/resource/ddl/persistence-types.ts +218 -218
- package/src/resource/ddl/type-map.ts +223 -223
- package/src/resource/ddl/types.ts +232 -232
- package/src/resource/generator-repo.ts +630 -630
- package/src/resource/schema.ts +1 -1
- package/src/router/fs-patterns.test.ts +96 -96
- package/src/router/fs-routes.ts +6 -6
- package/src/routes/index.ts +74 -74
- package/src/routes/metadata-routes.ts +427 -427
- package/src/routes/types.ts +341 -341
- package/src/runtime/__tests__/error-boundary-redaction.test.ts +141 -141
- package/src/runtime/__tests__/hdr-client.test.ts +223 -223
- package/src/runtime/__tests__/http-errors.test.ts +117 -117
- package/src/runtime/__tests__/not-found.test.ts +152 -152
- package/src/runtime/adapter.ts +47 -47
- package/src/runtime/boundary.tsx +252 -252
- package/src/runtime/cache.ts +494 -494
- package/src/runtime/compose.ts +222 -222
- package/src/runtime/fast-refresh-runtime.ts +322 -322
- package/src/runtime/handler.ts +65 -65
- package/src/runtime/handlers.ts +300 -300
- package/src/runtime/http-errors.ts +113 -113
- package/src/runtime/image-handler.ts +1 -1
- package/src/runtime/lifecycle.ts +381 -381
- package/src/runtime/logger.test.ts +345 -345
- package/src/runtime/middleware.ts +264 -264
- package/src/runtime/not-found.ts +93 -93
- package/src/runtime/openapi-endpoint.ts +236 -236
- package/src/runtime/ppr.ts +74 -74
- package/src/runtime/registry.ts +171 -171
- package/src/runtime/router.ts +105 -105
- package/src/runtime/server.ts +67 -67
- package/src/runtime/shims.ts +48 -48
- package/src/runtime/trace.ts +144 -144
- package/src/scheduler/validate.ts +169 -169
- package/src/seo/index.ts +219 -219
- package/src/seo/integration/ssr.ts +306 -306
- package/src/seo/render/basic.ts +435 -435
- package/src/seo/render/index.ts +143 -143
- package/src/seo/render/jsonld.ts +539 -539
- package/src/seo/render/opengraph.ts +197 -197
- package/src/seo/render/robots.ts +116 -116
- package/src/seo/render/sitemap.ts +137 -137
- package/src/seo/render/twitter.ts +127 -127
- package/src/seo/resolve/opengraph.ts +143 -143
- package/src/seo/resolve/robots.ts +73 -73
- package/src/seo/resolve/title.ts +94 -94
- package/src/seo/resolve/twitter.ts +73 -73
- package/src/seo/resolve/url.ts +104 -104
- package/src/seo/routes/index.ts +290 -290
- package/src/seo/types.ts +588 -588
- package/src/slot/validator.ts +39 -39
- package/src/storage/s3/__tests__/s3.test.ts +479 -479
- package/src/storage/s3/index.ts +412 -412
- package/src/testing/__tests__/assertions.test.ts +632 -632
- package/src/testing/__tests__/reporter.test.ts +454 -454
- package/src/testing/assertions.ts +986 -986
- package/src/testing/db.ts +157 -157
- package/src/testing/mocks.ts +203 -203
- package/src/testing/session.ts +190 -190
- package/src/types/branded.ts +56 -56
- package/src/types/index.ts +1 -1
- package/src/utils/safe-io.ts +188 -188
- package/src/utils/string-safe.ts +298 -298
- package/src/watcher/watcher.ts +18 -18
package/src/runtime/not-found.ts
CHANGED
|
@@ -1,93 +1,93 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mandu notFound() helper for SSR loaders and handlers.
|
|
3
|
-
*
|
|
4
|
-
* Symmetric with `redirect()` — both short-circuit the SSR pipeline by
|
|
5
|
-
* returning (or throwing) a marked `Response`. Where `redirect()` tells
|
|
6
|
-
* the runtime "navigate somewhere else", `notFound()` tells the runtime
|
|
7
|
-
* "this resource does not exist; render the 404 surface".
|
|
8
|
-
*
|
|
9
|
-
* ## Usage
|
|
10
|
-
*
|
|
11
|
-
* ```ts
|
|
12
|
-
* import { Mandu, notFound } from "@mandujs/core";
|
|
13
|
-
*
|
|
14
|
-
* export const filling = Mandu.filling().loader(async (ctx) => {
|
|
15
|
-
* const post = await db.post.find(ctx.params.slug);
|
|
16
|
-
* if (!post) return notFound(); // or: throw notFound();
|
|
17
|
-
* return { post };
|
|
18
|
-
* });
|
|
19
|
-
* ```
|
|
20
|
-
*
|
|
21
|
-
* The SSR pipeline (server.ts `loadPageData`) checks each loader result
|
|
22
|
-
* with `isNotFoundResponse()`. On a hit it:
|
|
23
|
-
*
|
|
24
|
-
* 1. Prefers `app/not-found.tsx` (if registered) — rendered as a normal
|
|
25
|
-
* page with status 404 and any pending cookies preserved.
|
|
26
|
-
* 2. Falls back to the framework's built-in 404 JSON error.
|
|
27
|
-
*
|
|
28
|
-
* ## Why a branded Response
|
|
29
|
-
*
|
|
30
|
-
* A bare `new Response(null, { status: 404 })` is NOT treated as a
|
|
31
|
-
* notFound sentinel. That's intentional: a loader that accidentally
|
|
32
|
-
* returns a generic 404 Response (e.g. proxying an upstream fetch) must
|
|
33
|
-
* NOT hijack the page to show our 404 page. Only values minted through
|
|
34
|
-
* `notFound()` carry the internal brand.
|
|
35
|
-
*
|
|
36
|
-
* The brand is a non-enumerable WeakSet membership stamped on the
|
|
37
|
-
* Response object. Never serialised, never visible to clients.
|
|
38
|
-
*/
|
|
39
|
-
|
|
40
|
-
/** Internal brand — identifies Response objects minted by `notFound()`. */
|
|
41
|
-
export const NOT_FOUND_BRAND: unique symbol = Symbol.for("@mandujs/core/not-found");
|
|
42
|
-
|
|
43
|
-
/** WeakSet of Response instances tagged as notFound. Avoids property writes. */
|
|
44
|
-
const brandedNotFoundResponses = new WeakSet<Response>();
|
|
45
|
-
|
|
46
|
-
/** Options for tuning a notFound response. */
|
|
47
|
-
export interface NotFoundOptions {
|
|
48
|
-
/** Optional human-readable message. Surfaced to the 404 page via body. */
|
|
49
|
-
message?: string;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Create a `Response` that signals "not found" to the Mandu SSR pipeline.
|
|
54
|
-
*
|
|
55
|
-
* Returns a real `Response` (status 404) for three reasons:
|
|
56
|
-
*
|
|
57
|
-
* 1. Loaders that `return` or `throw` it are treated identically — no
|
|
58
|
-
* extra plumbing for the "deep call stack wants to bail out" case.
|
|
59
|
-
* 2. Consumers outside an SSR loader (route handlers, middleware) can
|
|
60
|
-
* use the same helper without a separate API.
|
|
61
|
-
* 3. A hostile or buggy loader returning a bare `new Response(null, {status:404})`
|
|
62
|
-
* does NOT trigger the framework's 404 page path — it falls through
|
|
63
|
-
* the existing error channel like any other unexpected Response.
|
|
64
|
-
*
|
|
65
|
-
* @param options - Optional `{ message }`. Message is serialised into the
|
|
66
|
-
* response body as `text/plain; charset=utf-8`. If omitted, the body
|
|
67
|
-
* defaults to `"Not Found"`.
|
|
68
|
-
*/
|
|
69
|
-
export function notFound(options: NotFoundOptions = {}): Response {
|
|
70
|
-
const message = typeof options.message === "string" && options.message.length > 0
|
|
71
|
-
? options.message
|
|
72
|
-
: "Not Found";
|
|
73
|
-
|
|
74
|
-
const response = new Response(message, {
|
|
75
|
-
status: 404,
|
|
76
|
-
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
|
77
|
-
});
|
|
78
|
-
brandedNotFoundResponses.add(response);
|
|
79
|
-
return response;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* True when `value` is a Response produced by `notFound()`.
|
|
84
|
-
*
|
|
85
|
-
* Deliberately strict — only branded responses match. A bare
|
|
86
|
-
* `new Response(null, { status: 404 })` is NOT recognised, nor is a
|
|
87
|
-
* redirect Response (even one with a 404-like status, which would be
|
|
88
|
-
* malformed but shouldn't confuse us).
|
|
89
|
-
*/
|
|
90
|
-
export function isNotFoundResponse(value: unknown): value is Response {
|
|
91
|
-
if (!(value instanceof Response)) return false;
|
|
92
|
-
return brandedNotFoundResponses.has(value);
|
|
93
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Mandu notFound() helper for SSR loaders and handlers.
|
|
3
|
+
*
|
|
4
|
+
* Symmetric with `redirect()` — both short-circuit the SSR pipeline by
|
|
5
|
+
* returning (or throwing) a marked `Response`. Where `redirect()` tells
|
|
6
|
+
* the runtime "navigate somewhere else", `notFound()` tells the runtime
|
|
7
|
+
* "this resource does not exist; render the 404 surface".
|
|
8
|
+
*
|
|
9
|
+
* ## Usage
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { Mandu, notFound } from "@mandujs/core";
|
|
13
|
+
*
|
|
14
|
+
* export const filling = Mandu.filling().loader(async (ctx) => {
|
|
15
|
+
* const post = await db.post.find(ctx.params.slug);
|
|
16
|
+
* if (!post) return notFound(); // or: throw notFound();
|
|
17
|
+
* return { post };
|
|
18
|
+
* });
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* The SSR pipeline (server.ts `loadPageData`) checks each loader result
|
|
22
|
+
* with `isNotFoundResponse()`. On a hit it:
|
|
23
|
+
*
|
|
24
|
+
* 1. Prefers `app/not-found.tsx` (if registered) — rendered as a normal
|
|
25
|
+
* page with status 404 and any pending cookies preserved.
|
|
26
|
+
* 2. Falls back to the framework's built-in 404 JSON error.
|
|
27
|
+
*
|
|
28
|
+
* ## Why a branded Response
|
|
29
|
+
*
|
|
30
|
+
* A bare `new Response(null, { status: 404 })` is NOT treated as a
|
|
31
|
+
* notFound sentinel. That's intentional: a loader that accidentally
|
|
32
|
+
* returns a generic 404 Response (e.g. proxying an upstream fetch) must
|
|
33
|
+
* NOT hijack the page to show our 404 page. Only values minted through
|
|
34
|
+
* `notFound()` carry the internal brand.
|
|
35
|
+
*
|
|
36
|
+
* The brand is a non-enumerable WeakSet membership stamped on the
|
|
37
|
+
* Response object. Never serialised, never visible to clients.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/** Internal brand — identifies Response objects minted by `notFound()`. */
|
|
41
|
+
export const NOT_FOUND_BRAND: unique symbol = Symbol.for("@mandujs/core/not-found");
|
|
42
|
+
|
|
43
|
+
/** WeakSet of Response instances tagged as notFound. Avoids property writes. */
|
|
44
|
+
const brandedNotFoundResponses = new WeakSet<Response>();
|
|
45
|
+
|
|
46
|
+
/** Options for tuning a notFound response. */
|
|
47
|
+
export interface NotFoundOptions {
|
|
48
|
+
/** Optional human-readable message. Surfaced to the 404 page via body. */
|
|
49
|
+
message?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Create a `Response` that signals "not found" to the Mandu SSR pipeline.
|
|
54
|
+
*
|
|
55
|
+
* Returns a real `Response` (status 404) for three reasons:
|
|
56
|
+
*
|
|
57
|
+
* 1. Loaders that `return` or `throw` it are treated identically — no
|
|
58
|
+
* extra plumbing for the "deep call stack wants to bail out" case.
|
|
59
|
+
* 2. Consumers outside an SSR loader (route handlers, middleware) can
|
|
60
|
+
* use the same helper without a separate API.
|
|
61
|
+
* 3. A hostile or buggy loader returning a bare `new Response(null, {status:404})`
|
|
62
|
+
* does NOT trigger the framework's 404 page path — it falls through
|
|
63
|
+
* the existing error channel like any other unexpected Response.
|
|
64
|
+
*
|
|
65
|
+
* @param options - Optional `{ message }`. Message is serialised into the
|
|
66
|
+
* response body as `text/plain; charset=utf-8`. If omitted, the body
|
|
67
|
+
* defaults to `"Not Found"`.
|
|
68
|
+
*/
|
|
69
|
+
export function notFound(options: NotFoundOptions = {}): Response {
|
|
70
|
+
const message = typeof options.message === "string" && options.message.length > 0
|
|
71
|
+
? options.message
|
|
72
|
+
: "Not Found";
|
|
73
|
+
|
|
74
|
+
const response = new Response(message, {
|
|
75
|
+
status: 404,
|
|
76
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
|
77
|
+
});
|
|
78
|
+
brandedNotFoundResponses.add(response);
|
|
79
|
+
return response;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* True when `value` is a Response produced by `notFound()`.
|
|
84
|
+
*
|
|
85
|
+
* Deliberately strict — only branded responses match. A bare
|
|
86
|
+
* `new Response(null, { status: 404 })` is NOT recognised, nor is a
|
|
87
|
+
* redirect Response (even one with a 404-like status, which would be
|
|
88
|
+
* malformed but shouldn't confuse us).
|
|
89
|
+
*/
|
|
90
|
+
export function isNotFoundResponse(value: unknown): value is Response {
|
|
91
|
+
if (!(value instanceof Response)) return false;
|
|
92
|
+
return brandedNotFoundResponses.has(value);
|
|
93
|
+
}
|
|
@@ -1,236 +1,236 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Runtime OpenAPI endpoint.
|
|
3
|
-
*
|
|
4
|
-
* Serves the build-time `.mandu/openapi.json` / `.mandu/openapi.yaml`
|
|
5
|
-
* artifacts at a stable URL (default `/__mandu/openapi.json` and
|
|
6
|
-
* `.yaml`) so API consumers (Postman, codegen, Swagger UI proxies) can
|
|
7
|
-
* fetch a canonical spec without reaching into the framework's dev
|
|
8
|
-
* Kitchen dashboard.
|
|
9
|
-
*
|
|
10
|
-
* Contract:
|
|
11
|
-
* - Disabled by default — the server dispatcher gates this handler
|
|
12
|
-
* behind `ManduConfig.openapi.enabled` or the
|
|
13
|
-
* `MANDU_OPENAPI_ENABLED=1` env var.
|
|
14
|
-
* - Lazy-load artifacts on first request; in-memory cache survives
|
|
15
|
-
* for the lifetime of the server instance. Re-deploy to invalidate.
|
|
16
|
-
* - If artifacts are missing on disk, fall back to live generation
|
|
17
|
-
* from the registered manifest so `mandu dev` users still get a
|
|
18
|
-
* valid response without running `mandu build` first.
|
|
19
|
-
* - ETag = SHA-256 of the JSON body. Supports `If-None-Match` 304
|
|
20
|
-
* short-circuiting so downstream caches (CDN, reverse proxy) behave
|
|
21
|
-
* correctly.
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
import type { RoutesManifest } from "../spec/schema";
|
|
25
|
-
import {
|
|
26
|
-
generateOpenAPIDocument,
|
|
27
|
-
hashOpenAPIJSON,
|
|
28
|
-
openAPIToJSON,
|
|
29
|
-
openAPIToYAML,
|
|
30
|
-
readOpenAPIArtifacts,
|
|
31
|
-
} from "../openapi/generator";
|
|
32
|
-
|
|
33
|
-
export const DEFAULT_OPENAPI_BASE_PATH = "/__mandu/openapi";
|
|
34
|
-
const DEFAULT_ARTIFACT_DIR = ".mandu";
|
|
35
|
-
const CACHE_CONTROL = "public, max-age=0, must-revalidate";
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Runtime-resolved OpenAPI endpoint configuration. Mirrors the shape
|
|
39
|
-
* the server threads through `ServerRegistrySettings` so the hot-path
|
|
40
|
-
* dispatch can stay allocation-free.
|
|
41
|
-
*/
|
|
42
|
-
export interface OpenAPIEndpointSettings {
|
|
43
|
-
/** Base path without the trailing `.json`/`.yaml`. Default `/__mandu/openapi`. */
|
|
44
|
-
basePath: string;
|
|
45
|
-
/** Absolute directory containing `openapi.json` / `openapi.yaml` artifacts. */
|
|
46
|
-
artifactDir: string;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
interface CacheEntry {
|
|
50
|
-
json: string;
|
|
51
|
-
yaml: string;
|
|
52
|
-
hash: string;
|
|
53
|
-
etag: string;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** Module-scoped cache — invalidated by `invalidateOpenAPIEndpointCache()`. */
|
|
57
|
-
let cache: CacheEntry | null = null;
|
|
58
|
-
let pending: Promise<CacheEntry | null> | null = null;
|
|
59
|
-
|
|
60
|
-
/** Test / HMR hook: drop the cached spec so the next request recomputes. */
|
|
61
|
-
export function invalidateOpenAPIEndpointCache(): void {
|
|
62
|
-
cache = null;
|
|
63
|
-
pending = null;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Resolve the OpenAPI body, either from disk artifacts (preferred) or
|
|
68
|
-
* by generating live from the manifest. Concurrent callers share one
|
|
69
|
-
* in-flight load so we never rebuild the spec twice on a thundering
|
|
70
|
-
* herd.
|
|
71
|
-
*/
|
|
72
|
-
async function loadSpec(
|
|
73
|
-
manifest: RoutesManifest,
|
|
74
|
-
rootDir: string,
|
|
75
|
-
settings: OpenAPIEndpointSettings
|
|
76
|
-
): Promise<CacheEntry | null> {
|
|
77
|
-
if (cache) return cache;
|
|
78
|
-
if (pending) return pending;
|
|
79
|
-
|
|
80
|
-
pending = (async (): Promise<CacheEntry | null> => {
|
|
81
|
-
try {
|
|
82
|
-
// 1. Prefer on-disk artifacts (produced by `mandu build`). Keeps
|
|
83
|
-
// request-time cost at a single file read instead of walking
|
|
84
|
-
// every contract module again.
|
|
85
|
-
const fromDisk = await readOpenAPIArtifacts(settings.artifactDir, rootDir);
|
|
86
|
-
if (fromDisk) {
|
|
87
|
-
const entry: CacheEntry = {
|
|
88
|
-
json: fromDisk.json,
|
|
89
|
-
yaml: fromDisk.yaml,
|
|
90
|
-
hash: fromDisk.hash,
|
|
91
|
-
etag: `"${fromDisk.hash}"`,
|
|
92
|
-
};
|
|
93
|
-
cache = entry;
|
|
94
|
-
return entry;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// 2. Fallback: generate live. Useful in `mandu dev` before the
|
|
98
|
-
// user has run `mandu build`, or in test harnesses that boot a
|
|
99
|
-
// server directly from a manifest fixture.
|
|
100
|
-
const doc = await generateOpenAPIDocument(manifest, rootDir);
|
|
101
|
-
const json = openAPIToJSON(doc);
|
|
102
|
-
const yaml = openAPIToYAML(doc);
|
|
103
|
-
const hash = await hashOpenAPIJSON(json);
|
|
104
|
-
const entry: CacheEntry = {
|
|
105
|
-
json,
|
|
106
|
-
yaml,
|
|
107
|
-
hash,
|
|
108
|
-
etag: `"${hash}"`,
|
|
109
|
-
};
|
|
110
|
-
cache = entry;
|
|
111
|
-
return entry;
|
|
112
|
-
} catch {
|
|
113
|
-
// Swallow the error — a 500 here would be worse DX than a 404.
|
|
114
|
-
// Invalidate so the next request retries.
|
|
115
|
-
cache = null;
|
|
116
|
-
return null;
|
|
117
|
-
} finally {
|
|
118
|
-
pending = null;
|
|
119
|
-
}
|
|
120
|
-
})();
|
|
121
|
-
|
|
122
|
-
return pending;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Handle a GET request for the OpenAPI endpoint.
|
|
127
|
-
*
|
|
128
|
-
* Returns `null` when the pathname does not match — the dispatcher
|
|
129
|
-
* should fall through to the normal route resolution pipeline. Returns
|
|
130
|
-
* a `Response` for both hit (200 + spec) and miss (404 when the spec
|
|
131
|
-
* cannot be materialized). Only `GET` and `HEAD` are accepted; every
|
|
132
|
-
* other method gets a 405 with `Allow: GET, HEAD`.
|
|
133
|
-
*/
|
|
134
|
-
export async function handleOpenAPIRequest(
|
|
135
|
-
req: Request,
|
|
136
|
-
pathname: string,
|
|
137
|
-
manifest: RoutesManifest,
|
|
138
|
-
rootDir: string,
|
|
139
|
-
settings: OpenAPIEndpointSettings
|
|
140
|
-
): Promise<Response | null> {
|
|
141
|
-
const jsonPath = `${settings.basePath}.json`;
|
|
142
|
-
const yamlPath = `${settings.basePath}.yaml`;
|
|
143
|
-
|
|
144
|
-
let variant: "json" | "yaml";
|
|
145
|
-
if (pathname === jsonPath) variant = "json";
|
|
146
|
-
else if (pathname === yamlPath) variant = "yaml";
|
|
147
|
-
else return null;
|
|
148
|
-
|
|
149
|
-
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
150
|
-
return new Response("Method Not Allowed", {
|
|
151
|
-
status: 405,
|
|
152
|
-
headers: {
|
|
153
|
-
Allow: "GET, HEAD",
|
|
154
|
-
"Cache-Control": "no-store",
|
|
155
|
-
},
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const entry = await loadSpec(manifest, rootDir, settings);
|
|
160
|
-
if (!entry) {
|
|
161
|
-
return new Response("Not Found", { status: 404 });
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Conditional-GET: honour `If-None-Match` for CDN / browser caches.
|
|
165
|
-
const ifNoneMatch = req.headers.get("if-none-match");
|
|
166
|
-
if (ifNoneMatch && ifNoneMatch === entry.etag) {
|
|
167
|
-
return new Response(null, {
|
|
168
|
-
status: 304,
|
|
169
|
-
headers: {
|
|
170
|
-
ETag: entry.etag,
|
|
171
|
-
"Cache-Control": CACHE_CONTROL,
|
|
172
|
-
},
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const body = variant === "json" ? entry.json : entry.yaml;
|
|
177
|
-
const contentType =
|
|
178
|
-
variant === "json"
|
|
179
|
-
? "application/json; charset=utf-8"
|
|
180
|
-
: "application/yaml; charset=utf-8";
|
|
181
|
-
|
|
182
|
-
// HEAD responses carry the headers but drop the body.
|
|
183
|
-
const responseBody = req.method === "HEAD" ? null : body;
|
|
184
|
-
return new Response(responseBody, {
|
|
185
|
-
status: 200,
|
|
186
|
-
headers: {
|
|
187
|
-
"Content-Type": contentType,
|
|
188
|
-
"Cache-Control": CACHE_CONTROL,
|
|
189
|
-
ETag: entry.etag,
|
|
190
|
-
// Expose ETag to browser JS so API explorer UIs can display the
|
|
191
|
-
// deploy identifier without a round-trip.
|
|
192
|
-
"Access-Control-Expose-Headers": "ETag",
|
|
193
|
-
},
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Resolve the effective endpoint settings for a server boot. Normalizes
|
|
199
|
-
* the `path` option (users may pass with or without leading slash, and
|
|
200
|
-
* with or without the `.json` suffix) and chooses the artifact
|
|
201
|
-
* directory default.
|
|
202
|
-
*/
|
|
203
|
-
export function resolveOpenAPIEndpointSettings(
|
|
204
|
-
rootDir: string,
|
|
205
|
-
path?: string
|
|
206
|
-
): OpenAPIEndpointSettings {
|
|
207
|
-
let basePath = path ?? DEFAULT_OPENAPI_BASE_PATH;
|
|
208
|
-
if (!basePath.startsWith("/")) basePath = `/${basePath}`;
|
|
209
|
-
// Strip trailing `.json` / `.yaml` / trailing slash so the handler can
|
|
210
|
-
// append suffixes uniformly.
|
|
211
|
-
basePath = basePath.replace(/\.(json|yaml|yml)$/i, "").replace(/\/+$/, "");
|
|
212
|
-
if (basePath === "") basePath = DEFAULT_OPENAPI_BASE_PATH;
|
|
213
|
-
|
|
214
|
-
// POSIX-style join — artifact paths are treated as absolute by
|
|
215
|
-
// `readOpenAPIArtifacts`, which itself uses `node:path` for portability.
|
|
216
|
-
const normalizedRoot = rootDir.replace(/[\\/]+$/, "");
|
|
217
|
-
const separator = normalizedRoot.includes("\\") ? "\\" : "/";
|
|
218
|
-
const artifactDir = `${normalizedRoot}${separator}${DEFAULT_ARTIFACT_DIR}`;
|
|
219
|
-
return { basePath, artifactDir };
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Decide whether the OpenAPI endpoint should be active for this
|
|
224
|
-
* server instance. The config flag wins; an explicit `false` still
|
|
225
|
-
* disables the endpoint even when the env var is set (explicit > env).
|
|
226
|
-
* Absent config + truthy env var (`MANDU_OPENAPI_ENABLED=1`) opts in.
|
|
227
|
-
*/
|
|
228
|
-
export function isOpenAPIEndpointEnabled(
|
|
229
|
-
enabled: boolean | undefined,
|
|
230
|
-
env: NodeJS.ProcessEnv | Record<string, string | undefined> = process.env
|
|
231
|
-
): boolean {
|
|
232
|
-
if (enabled === true) return true;
|
|
233
|
-
if (enabled === false) return false;
|
|
234
|
-
const raw = env.MANDU_OPENAPI_ENABLED;
|
|
235
|
-
return raw === "1" || raw === "true";
|
|
236
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Runtime OpenAPI endpoint.
|
|
3
|
+
*
|
|
4
|
+
* Serves the build-time `.mandu/openapi.json` / `.mandu/openapi.yaml`
|
|
5
|
+
* artifacts at a stable URL (default `/__mandu/openapi.json` and
|
|
6
|
+
* `.yaml`) so API consumers (Postman, codegen, Swagger UI proxies) can
|
|
7
|
+
* fetch a canonical spec without reaching into the framework's dev
|
|
8
|
+
* Kitchen dashboard.
|
|
9
|
+
*
|
|
10
|
+
* Contract:
|
|
11
|
+
* - Disabled by default — the server dispatcher gates this handler
|
|
12
|
+
* behind `ManduConfig.openapi.enabled` or the
|
|
13
|
+
* `MANDU_OPENAPI_ENABLED=1` env var.
|
|
14
|
+
* - Lazy-load artifacts on first request; in-memory cache survives
|
|
15
|
+
* for the lifetime of the server instance. Re-deploy to invalidate.
|
|
16
|
+
* - If artifacts are missing on disk, fall back to live generation
|
|
17
|
+
* from the registered manifest so `mandu dev` users still get a
|
|
18
|
+
* valid response without running `mandu build` first.
|
|
19
|
+
* - ETag = SHA-256 of the JSON body. Supports `If-None-Match` 304
|
|
20
|
+
* short-circuiting so downstream caches (CDN, reverse proxy) behave
|
|
21
|
+
* correctly.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import type { RoutesManifest } from "../spec/schema";
|
|
25
|
+
import {
|
|
26
|
+
generateOpenAPIDocument,
|
|
27
|
+
hashOpenAPIJSON,
|
|
28
|
+
openAPIToJSON,
|
|
29
|
+
openAPIToYAML,
|
|
30
|
+
readOpenAPIArtifacts,
|
|
31
|
+
} from "../openapi/generator";
|
|
32
|
+
|
|
33
|
+
export const DEFAULT_OPENAPI_BASE_PATH = "/__mandu/openapi";
|
|
34
|
+
const DEFAULT_ARTIFACT_DIR = ".mandu";
|
|
35
|
+
const CACHE_CONTROL = "public, max-age=0, must-revalidate";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Runtime-resolved OpenAPI endpoint configuration. Mirrors the shape
|
|
39
|
+
* the server threads through `ServerRegistrySettings` so the hot-path
|
|
40
|
+
* dispatch can stay allocation-free.
|
|
41
|
+
*/
|
|
42
|
+
export interface OpenAPIEndpointSettings {
|
|
43
|
+
/** Base path without the trailing `.json`/`.yaml`. Default `/__mandu/openapi`. */
|
|
44
|
+
basePath: string;
|
|
45
|
+
/** Absolute directory containing `openapi.json` / `openapi.yaml` artifacts. */
|
|
46
|
+
artifactDir: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface CacheEntry {
|
|
50
|
+
json: string;
|
|
51
|
+
yaml: string;
|
|
52
|
+
hash: string;
|
|
53
|
+
etag: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Module-scoped cache — invalidated by `invalidateOpenAPIEndpointCache()`. */
|
|
57
|
+
let cache: CacheEntry | null = null;
|
|
58
|
+
let pending: Promise<CacheEntry | null> | null = null;
|
|
59
|
+
|
|
60
|
+
/** Test / HMR hook: drop the cached spec so the next request recomputes. */
|
|
61
|
+
export function invalidateOpenAPIEndpointCache(): void {
|
|
62
|
+
cache = null;
|
|
63
|
+
pending = null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Resolve the OpenAPI body, either from disk artifacts (preferred) or
|
|
68
|
+
* by generating live from the manifest. Concurrent callers share one
|
|
69
|
+
* in-flight load so we never rebuild the spec twice on a thundering
|
|
70
|
+
* herd.
|
|
71
|
+
*/
|
|
72
|
+
async function loadSpec(
|
|
73
|
+
manifest: RoutesManifest,
|
|
74
|
+
rootDir: string,
|
|
75
|
+
settings: OpenAPIEndpointSettings
|
|
76
|
+
): Promise<CacheEntry | null> {
|
|
77
|
+
if (cache) return cache;
|
|
78
|
+
if (pending) return pending;
|
|
79
|
+
|
|
80
|
+
pending = (async (): Promise<CacheEntry | null> => {
|
|
81
|
+
try {
|
|
82
|
+
// 1. Prefer on-disk artifacts (produced by `mandu build`). Keeps
|
|
83
|
+
// request-time cost at a single file read instead of walking
|
|
84
|
+
// every contract module again.
|
|
85
|
+
const fromDisk = await readOpenAPIArtifacts(settings.artifactDir, rootDir);
|
|
86
|
+
if (fromDisk) {
|
|
87
|
+
const entry: CacheEntry = {
|
|
88
|
+
json: fromDisk.json,
|
|
89
|
+
yaml: fromDisk.yaml,
|
|
90
|
+
hash: fromDisk.hash,
|
|
91
|
+
etag: `"${fromDisk.hash}"`,
|
|
92
|
+
};
|
|
93
|
+
cache = entry;
|
|
94
|
+
return entry;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 2. Fallback: generate live. Useful in `mandu dev` before the
|
|
98
|
+
// user has run `mandu build`, or in test harnesses that boot a
|
|
99
|
+
// server directly from a manifest fixture.
|
|
100
|
+
const doc = await generateOpenAPIDocument(manifest, rootDir);
|
|
101
|
+
const json = openAPIToJSON(doc);
|
|
102
|
+
const yaml = openAPIToYAML(doc);
|
|
103
|
+
const hash = await hashOpenAPIJSON(json);
|
|
104
|
+
const entry: CacheEntry = {
|
|
105
|
+
json,
|
|
106
|
+
yaml,
|
|
107
|
+
hash,
|
|
108
|
+
etag: `"${hash}"`,
|
|
109
|
+
};
|
|
110
|
+
cache = entry;
|
|
111
|
+
return entry;
|
|
112
|
+
} catch {
|
|
113
|
+
// Swallow the error — a 500 here would be worse DX than a 404.
|
|
114
|
+
// Invalidate so the next request retries.
|
|
115
|
+
cache = null;
|
|
116
|
+
return null;
|
|
117
|
+
} finally {
|
|
118
|
+
pending = null;
|
|
119
|
+
}
|
|
120
|
+
})();
|
|
121
|
+
|
|
122
|
+
return pending;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Handle a GET request for the OpenAPI endpoint.
|
|
127
|
+
*
|
|
128
|
+
* Returns `null` when the pathname does not match — the dispatcher
|
|
129
|
+
* should fall through to the normal route resolution pipeline. Returns
|
|
130
|
+
* a `Response` for both hit (200 + spec) and miss (404 when the spec
|
|
131
|
+
* cannot be materialized). Only `GET` and `HEAD` are accepted; every
|
|
132
|
+
* other method gets a 405 with `Allow: GET, HEAD`.
|
|
133
|
+
*/
|
|
134
|
+
export async function handleOpenAPIRequest(
|
|
135
|
+
req: Request,
|
|
136
|
+
pathname: string,
|
|
137
|
+
manifest: RoutesManifest,
|
|
138
|
+
rootDir: string,
|
|
139
|
+
settings: OpenAPIEndpointSettings
|
|
140
|
+
): Promise<Response | null> {
|
|
141
|
+
const jsonPath = `${settings.basePath}.json`;
|
|
142
|
+
const yamlPath = `${settings.basePath}.yaml`;
|
|
143
|
+
|
|
144
|
+
let variant: "json" | "yaml";
|
|
145
|
+
if (pathname === jsonPath) variant = "json";
|
|
146
|
+
else if (pathname === yamlPath) variant = "yaml";
|
|
147
|
+
else return null;
|
|
148
|
+
|
|
149
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
150
|
+
return new Response("Method Not Allowed", {
|
|
151
|
+
status: 405,
|
|
152
|
+
headers: {
|
|
153
|
+
Allow: "GET, HEAD",
|
|
154
|
+
"Cache-Control": "no-store",
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const entry = await loadSpec(manifest, rootDir, settings);
|
|
160
|
+
if (!entry) {
|
|
161
|
+
return new Response("Not Found", { status: 404 });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Conditional-GET: honour `If-None-Match` for CDN / browser caches.
|
|
165
|
+
const ifNoneMatch = req.headers.get("if-none-match");
|
|
166
|
+
if (ifNoneMatch && ifNoneMatch === entry.etag) {
|
|
167
|
+
return new Response(null, {
|
|
168
|
+
status: 304,
|
|
169
|
+
headers: {
|
|
170
|
+
ETag: entry.etag,
|
|
171
|
+
"Cache-Control": CACHE_CONTROL,
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const body = variant === "json" ? entry.json : entry.yaml;
|
|
177
|
+
const contentType =
|
|
178
|
+
variant === "json"
|
|
179
|
+
? "application/json; charset=utf-8"
|
|
180
|
+
: "application/yaml; charset=utf-8";
|
|
181
|
+
|
|
182
|
+
// HEAD responses carry the headers but drop the body.
|
|
183
|
+
const responseBody = req.method === "HEAD" ? null : body;
|
|
184
|
+
return new Response(responseBody, {
|
|
185
|
+
status: 200,
|
|
186
|
+
headers: {
|
|
187
|
+
"Content-Type": contentType,
|
|
188
|
+
"Cache-Control": CACHE_CONTROL,
|
|
189
|
+
ETag: entry.etag,
|
|
190
|
+
// Expose ETag to browser JS so API explorer UIs can display the
|
|
191
|
+
// deploy identifier without a round-trip.
|
|
192
|
+
"Access-Control-Expose-Headers": "ETag",
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Resolve the effective endpoint settings for a server boot. Normalizes
|
|
199
|
+
* the `path` option (users may pass with or without leading slash, and
|
|
200
|
+
* with or without the `.json` suffix) and chooses the artifact
|
|
201
|
+
* directory default.
|
|
202
|
+
*/
|
|
203
|
+
export function resolveOpenAPIEndpointSettings(
|
|
204
|
+
rootDir: string,
|
|
205
|
+
path?: string
|
|
206
|
+
): OpenAPIEndpointSettings {
|
|
207
|
+
let basePath = path ?? DEFAULT_OPENAPI_BASE_PATH;
|
|
208
|
+
if (!basePath.startsWith("/")) basePath = `/${basePath}`;
|
|
209
|
+
// Strip trailing `.json` / `.yaml` / trailing slash so the handler can
|
|
210
|
+
// append suffixes uniformly.
|
|
211
|
+
basePath = basePath.replace(/\.(json|yaml|yml)$/i, "").replace(/\/+$/, "");
|
|
212
|
+
if (basePath === "") basePath = DEFAULT_OPENAPI_BASE_PATH;
|
|
213
|
+
|
|
214
|
+
// POSIX-style join — artifact paths are treated as absolute by
|
|
215
|
+
// `readOpenAPIArtifacts`, which itself uses `node:path` for portability.
|
|
216
|
+
const normalizedRoot = rootDir.replace(/[\\/]+$/, "");
|
|
217
|
+
const separator = normalizedRoot.includes("\\") ? "\\" : "/";
|
|
218
|
+
const artifactDir = `${normalizedRoot}${separator}${DEFAULT_ARTIFACT_DIR}`;
|
|
219
|
+
return { basePath, artifactDir };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Decide whether the OpenAPI endpoint should be active for this
|
|
224
|
+
* server instance. The config flag wins; an explicit `false` still
|
|
225
|
+
* disables the endpoint even when the env var is set (explicit > env).
|
|
226
|
+
* Absent config + truthy env var (`MANDU_OPENAPI_ENABLED=1`) opts in.
|
|
227
|
+
*/
|
|
228
|
+
export function isOpenAPIEndpointEnabled(
|
|
229
|
+
enabled: boolean | undefined,
|
|
230
|
+
env: NodeJS.ProcessEnv | Record<string, string | undefined> = process.env
|
|
231
|
+
): boolean {
|
|
232
|
+
if (enabled === true) return true;
|
|
233
|
+
if (enabled === false) return false;
|
|
234
|
+
const raw = env.MANDU_OPENAPI_ENABLED;
|
|
235
|
+
return raw === "1" || raw === "true";
|
|
236
|
+
}
|