@mandujs/core 0.33.1 → 0.34.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/package.json +17 -2
- package/src/a11y/__tests__/run-audit.test.ts +333 -0
- package/src/a11y/fix-hints.ts +76 -0
- package/src/a11y/index.ts +18 -0
- package/src/a11y/run-audit.ts +394 -0
- package/src/a11y/types.ts +125 -0
- package/src/bundler/analyzer.ts +119 -4
- package/src/bundler/budget.ts +404 -0
- package/src/bundler/prerender.ts +20 -2
- package/src/config/mandu.ts +34 -0
- package/src/config/validate.ts +39 -0
- package/src/diagnose/__tests__/checks.test.ts +4 -2
- package/src/diagnose/checks.ts +121 -0
- package/src/diagnose/index.ts +1 -0
- package/src/diagnose/run.ts +7 -3
- package/src/perf/__tests__/user-marks.test.ts +354 -0
- package/src/perf/index.ts +29 -0
- package/src/perf/user-marks.ts +553 -0
- package/src/runtime/server.ts +111 -11
package/src/runtime/server.ts
CHANGED
|
@@ -57,11 +57,17 @@ import { eventBus } from "../observability/event-bus";
|
|
|
57
57
|
import {
|
|
58
58
|
HEAP_ENDPOINT,
|
|
59
59
|
METRICS_ENDPOINT,
|
|
60
|
-
buildHeapResponse,
|
|
61
60
|
buildMetricsResponse,
|
|
61
|
+
collectHeapSnapshot,
|
|
62
62
|
isObservabilityExposed,
|
|
63
63
|
recordHttpRequest,
|
|
64
64
|
} from "../observability/metrics";
|
|
65
|
+
// Phase 18.ψ — user-facing perf marks dashboard append. We include a
|
|
66
|
+
// `perf` block on the `/_mandu/heap` payload when the feature is active
|
|
67
|
+
// so operators see a unified view (heap + caches + perf histogram).
|
|
68
|
+
// Importing the snapshot collector (rather than the whole module) keeps
|
|
69
|
+
// the tree-shake footprint tight when perf is gated off.
|
|
70
|
+
import { collectPerfSnapshot } from "../perf/user-marks";
|
|
65
71
|
// Phase 18.θ — request tracing. Tracer lifecycle is owned by
|
|
66
72
|
// `startServer()`; `runWithSpan` is used at the absolute TOP of the
|
|
67
73
|
// request handler so every downstream await (middleware, filling
|
|
@@ -499,11 +505,13 @@ export interface ServerOptions {
|
|
|
499
505
|
* When enabled (default), the server looks for a prerender index
|
|
500
506
|
* under `<rootDir>/<dir>/_manifest.json` (written by `mandu build`)
|
|
501
507
|
* and, for every request whose pathname maps to a prerendered file,
|
|
502
|
-
* serves that HTML directly, bypassing SSR entirely, with a
|
|
503
|
-
* `Cache-Control`
|
|
508
|
+
* serves that HTML directly, bypassing SSR entirely, with a
|
|
509
|
+
* conditional-GET friendly `Cache-Control` + strong `ETag` pair.
|
|
504
510
|
*
|
|
505
511
|
* - `true` enabled with defaults (dir `.mandu/prerendered`,
|
|
506
|
-
* Cache-Control `public, max-age=
|
|
512
|
+
* Cache-Control `public, max-age=0, must-revalidate`
|
|
513
|
+
* — Issue #221; prerendered URLs are stable, so
|
|
514
|
+
* `immutable` would pin stale HTML across deploys).
|
|
507
515
|
* - `false` disabled. Every request goes through SSR.
|
|
508
516
|
* - object overrides. `dir` chooses a different output;
|
|
509
517
|
* `cacheControl` lets adapters tune the CDN hint.
|
|
@@ -3491,14 +3499,28 @@ function buildRouteCacheKey(routeId: string, url: URL): string {
|
|
|
3491
3499
|
* surface as a 500).
|
|
3492
3500
|
*
|
|
3493
3501
|
* Responses are stamped with `Cache-Control` from the registry
|
|
3494
|
-
* settings
|
|
3495
|
-
*
|
|
3496
|
-
*
|
|
3502
|
+
* settings and an `X-Mandu-Cache: PRERENDERED` tag for observability /
|
|
3503
|
+
* log parity with the ISR cache path.
|
|
3504
|
+
*
|
|
3505
|
+
* Issue #221 — prerendered HTML lives at a **stable URL** (route →
|
|
3506
|
+
* file, no content hash in the path). Serving it with `immutable`
|
|
3507
|
+
* breaks new-deploy rollout exactly like #218: browsers pin the
|
|
3508
|
+
* stale HTML for up to a year. The fix mirrors #218's static-file
|
|
3509
|
+
* policy:
|
|
3510
|
+
*
|
|
3511
|
+
* 1. Default `Cache-Control` → `public, max-age=0, must-revalidate`
|
|
3512
|
+
* (via `computeStaticCacheControl` — no hash in filename ⇒
|
|
3513
|
+
* must-revalidate). User-supplied `PrerenderSettings.cacheControl`
|
|
3514
|
+
* still wins so adapters can tune for their CDN.
|
|
3515
|
+
* 2. Emit a strong ETag (`Bun.hash` over the HTML bytes).
|
|
3516
|
+
* 3. On `If-None-Match` match → `304 Not Modified` with empty body,
|
|
3517
|
+
* `Cache-Control` + `ETag` preserved for intermediaries.
|
|
3497
3518
|
*/
|
|
3498
3519
|
async function tryServePrerendered(
|
|
3499
3520
|
pathname: string,
|
|
3500
3521
|
settings: ServerRegistrySettings,
|
|
3501
|
-
method: string
|
|
3522
|
+
method: string,
|
|
3523
|
+
request?: Request
|
|
3502
3524
|
): Promise<Response | null> {
|
|
3503
3525
|
const p = settings.prerender;
|
|
3504
3526
|
if (!p) return null;
|
|
@@ -3517,6 +3539,67 @@ async function tryServePrerendered(
|
|
|
3517
3539
|
const filePath = resolvePrerenderedFile(p.index, settings.rootDir, p.dir, pathname);
|
|
3518
3540
|
if (!filePath) return null;
|
|
3519
3541
|
|
|
3542
|
+
// Load via `Bun.file` so we can reuse the #218 ETag helper (which
|
|
3543
|
+
// keys the hash cache on absolute path + size + mtime). `exists()`
|
|
3544
|
+
// guards the rare race where the index points at a file that was
|
|
3545
|
+
// removed after load.
|
|
3546
|
+
const file = Bun.file(filePath);
|
|
3547
|
+
let exists = false;
|
|
3548
|
+
try {
|
|
3549
|
+
exists = await file.exists();
|
|
3550
|
+
} catch {
|
|
3551
|
+
return null;
|
|
3552
|
+
}
|
|
3553
|
+
if (!exists) return null;
|
|
3554
|
+
|
|
3555
|
+
// Strong ETag derived from HTML bytes — same wyhash primitive the
|
|
3556
|
+
// static-asset dispatch uses, sharing the same LRU cache.
|
|
3557
|
+
let etag: string;
|
|
3558
|
+
try {
|
|
3559
|
+
etag = await computeStrongEtag(filePath, file);
|
|
3560
|
+
} catch {
|
|
3561
|
+
return null;
|
|
3562
|
+
}
|
|
3563
|
+
|
|
3564
|
+
// Cache-Control resolution (Issue #221):
|
|
3565
|
+
//
|
|
3566
|
+
// - When `p.cacheControl` is framework-chosen (either the current
|
|
3567
|
+
// must-revalidate default or the pre-#221 `immutable` default,
|
|
3568
|
+
// which we treat as "caller never opted out"), delegate to
|
|
3569
|
+
// `computeStaticCacheControl` so dev-mode gets `no-cache,
|
|
3570
|
+
// no-store, must-revalidate` and prod gets must-revalidate
|
|
3571
|
+
// (prerendered filenames never carry a content hash, so the
|
|
3572
|
+
// hash-aware policy always lands on the revalidating form).
|
|
3573
|
+
// - Otherwise honour the override verbatim — adapters in front of
|
|
3574
|
+
// a CDN with per-deploy invalidation may legitimately want
|
|
3575
|
+
// aggressive caching.
|
|
3576
|
+
//
|
|
3577
|
+
// The pre-#221 `immutable` string is treated as a framework default
|
|
3578
|
+
// so projects upgrading from a persisted registry state get the fix
|
|
3579
|
+
// automatically rather than staying on the broken policy.
|
|
3580
|
+
const isFrameworkDefault =
|
|
3581
|
+
p.cacheControl === DEFAULT_PRERENDER_CACHE_CONTROL ||
|
|
3582
|
+
p.cacheControl === "public, max-age=31536000, immutable" ||
|
|
3583
|
+
p.cacheControl === "";
|
|
3584
|
+
const cacheControl = isFrameworkDefault
|
|
3585
|
+
? computeStaticCacheControl(path.basename(filePath), settings.isDev)
|
|
3586
|
+
: p.cacheControl;
|
|
3587
|
+
|
|
3588
|
+
// Conditional GET — RFC 7232 §3.2. Covers `"<etag>"`, `W/"<etag>"`,
|
|
3589
|
+
// comma-separated lists, and `*`. 304 keeps ETag + Cache-Control so
|
|
3590
|
+
// downstream caches update their freshness state.
|
|
3591
|
+
const ifNoneMatch = request?.headers.get("If-None-Match");
|
|
3592
|
+
if (ifNoneMatch && matchesEtag(ifNoneMatch, etag)) {
|
|
3593
|
+
return new Response(null, {
|
|
3594
|
+
status: 304,
|
|
3595
|
+
headers: {
|
|
3596
|
+
"ETag": etag,
|
|
3597
|
+
"Cache-Control": cacheControl,
|
|
3598
|
+
"X-Mandu-Cache": "PRERENDERED",
|
|
3599
|
+
},
|
|
3600
|
+
});
|
|
3601
|
+
}
|
|
3602
|
+
|
|
3520
3603
|
let html: string;
|
|
3521
3604
|
try {
|
|
3522
3605
|
html = await fs.readFile(filePath, "utf-8");
|
|
@@ -3526,7 +3609,8 @@ async function tryServePrerendered(
|
|
|
3526
3609
|
|
|
3527
3610
|
const headers = new Headers({
|
|
3528
3611
|
"Content-Type": "text/html; charset=utf-8",
|
|
3529
|
-
"Cache-Control":
|
|
3612
|
+
"Cache-Control": cacheControl,
|
|
3613
|
+
"ETag": etag,
|
|
3530
3614
|
"X-Mandu-Cache": "PRERENDERED",
|
|
3531
3615
|
});
|
|
3532
3616
|
const body = method === "HEAD" ? null : html;
|
|
@@ -3664,7 +3748,7 @@ async function handleRequestInternal(
|
|
|
3664
3748
|
// Must run BEFORE static-file serving and route dispatch so that
|
|
3665
3749
|
// `mandu build`-emitted HTML short-circuits SSR. No-op if the
|
|
3666
3750
|
// feature is disabled or the path wasn't prerendered.
|
|
3667
|
-
const prerendered = await tryServePrerendered(pathname, settings, req.method);
|
|
3751
|
+
const prerendered = await tryServePrerendered(pathname, settings, req.method, req);
|
|
3668
3752
|
if (prerendered) {
|
|
3669
3753
|
if (settings.cors && isCorsRequest(req)) {
|
|
3670
3754
|
const corsOptions: CorsOptions = typeof settings.cors === 'object' ? settings.cors : {};
|
|
@@ -3807,7 +3891,23 @@ async function handleRequestInternal(
|
|
|
3807
3891
|
// `docs/ops/metrics.md` for the operator-facing guide.
|
|
3808
3892
|
if (pathname === HEAP_ENDPOINT) {
|
|
3809
3893
|
if (isObservabilityExposed(settings.isDev, settings.heapEndpoint)) {
|
|
3810
|
-
|
|
3894
|
+
// Phase 18.ψ — augment the Phase 17 payload with user-perf data.
|
|
3895
|
+
// We append (never restructure) the `perf` key so consumers that
|
|
3896
|
+
// rely on `.process`, `.caches`, `.bun` continue to parse. Keeping
|
|
3897
|
+
// the composition here (not in metrics.ts) avoids a metrics→perf
|
|
3898
|
+
// module dep — metrics stays a pure exposition layer.
|
|
3899
|
+
const base = collectHeapSnapshot();
|
|
3900
|
+
const perf = collectPerfSnapshot();
|
|
3901
|
+
const body = { ...base, perf };
|
|
3902
|
+
return ok(
|
|
3903
|
+
new Response(JSON.stringify(body, null, 2), {
|
|
3904
|
+
status: 200,
|
|
3905
|
+
headers: {
|
|
3906
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
3907
|
+
"Cache-Control": "no-store",
|
|
3908
|
+
},
|
|
3909
|
+
}),
|
|
3910
|
+
);
|
|
3811
3911
|
}
|
|
3812
3912
|
}
|
|
3813
3913
|
if (pathname === METRICS_ENDPOINT) {
|