@nifrajs/web 3.5.0 → 4.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/backend-only.d.ts +15 -0
- package/dist/backend-only.d.ts.map +1 -0
- package/dist/backend-only.js +16 -0
- package/dist/backend-only.js.map +1 -0
- package/dist/boundary.d.ts +5 -0
- package/dist/boundary.d.ts.map +1 -1
- package/dist/boundary.js +11 -2
- package/dist/boundary.js.map +1 -1
- package/dist/build-plan.d.ts +18 -6
- package/dist/build-plan.d.ts.map +1 -1
- package/dist/build-plan.js +82 -34
- package/dist/build-plan.js.map +1 -1
- package/dist/build-vite.d.ts +4 -0
- package/dist/build-vite.d.ts.map +1 -1
- package/dist/build-vite.js +68 -13
- package/dist/build-vite.js.map +1 -1
- package/dist/build.d.ts +57 -23
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +356 -85
- package/dist/build.js.map +1 -1
- package/dist/bun-dev-entry.d.ts +1 -1
- package/dist/bun-dev-entry.js +1 -1
- package/dist/cdn.d.ts +74 -0
- package/dist/cdn.d.ts.map +1 -0
- package/dist/cdn.js +132 -0
- package/dist/cdn.js.map +1 -0
- package/dist/client.d.ts +3 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +136 -35
- package/dist/client.js.map +1 -1
- package/dist/conformance.d.ts +4 -3
- package/dist/conformance.d.ts.map +1 -1
- package/dist/conformance.js +12 -3
- package/dist/conformance.js.map +1 -1
- package/dist/csp.d.ts +93 -0
- package/dist/csp.d.ts.map +1 -0
- package/dist/csp.js +122 -0
- package/dist/csp.js.map +1 -0
- package/dist/dev-client.d.ts +76 -0
- package/dist/dev-client.d.ts.map +1 -0
- package/dist/dev-client.js +280 -0
- package/dist/dev-client.js.map +1 -0
- package/dist/dev-diagnostics.d.ts +26 -2
- package/dist/dev-diagnostics.d.ts.map +1 -1
- package/dist/dev-diagnostics.js +38 -7
- package/dist/dev-diagnostics.js.map +1 -1
- package/dist/dev-error.d.ts +4 -13
- package/dist/dev-error.d.ts.map +1 -1
- package/dist/dev-error.js +46 -4
- package/dist/dev-error.js.map +1 -1
- package/dist/dev-feed.d.ts +298 -0
- package/dist/dev-feed.d.ts.map +1 -0
- package/dist/dev-feed.js +797 -0
- package/dist/dev-feed.js.map +1 -0
- package/dist/dev-indicator.d.ts +38 -0
- package/dist/dev-indicator.d.ts.map +1 -0
- package/dist/dev-indicator.js +93 -0
- package/dist/dev-indicator.js.map +1 -0
- package/dist/dev-session.d.ts +77 -0
- package/dist/dev-session.d.ts.map +1 -0
- package/dist/dev-session.js +580 -0
- package/dist/dev-session.js.map +1 -0
- package/dist/dev-sourcemap.d.ts +27 -0
- package/dist/dev-sourcemap.d.ts.map +1 -0
- package/dist/dev-sourcemap.js +235 -0
- package/dist/dev-sourcemap.js.map +1 -0
- package/dist/dev-ssr-graph.d.ts.map +1 -1
- package/dist/dev-ssr-graph.js +13 -5
- package/dist/dev-ssr-graph.js.map +1 -1
- package/dist/dev.d.ts +15 -2
- package/dist/dev.d.ts.map +1 -1
- package/dist/dev.js +69 -44
- package/dist/dev.js.map +1 -1
- package/dist/diagnostic-catalog.d.ts +35 -0
- package/dist/diagnostic-catalog.d.ts.map +1 -0
- package/dist/diagnostic-catalog.js +203 -0
- package/dist/diagnostic-catalog.js.map +1 -0
- package/dist/diagnostic-prompt.d.ts +49 -0
- package/dist/diagnostic-prompt.d.ts.map +1 -0
- package/dist/diagnostic-prompt.js +303 -0
- package/dist/diagnostic-prompt.js.map +1 -0
- package/dist/diagnostic.d.ts +17 -23
- package/dist/diagnostic.d.ts.map +1 -1
- package/dist/diagnostic.js +31 -39
- package/dist/diagnostic.js.map +1 -1
- package/dist/draft.d.ts.map +1 -1
- package/dist/draft.js +4 -1
- package/dist/draft.js.map +1 -1
- package/dist/fn.d.ts +5 -0
- package/dist/fn.d.ts.map +1 -1
- package/dist/fn.js +13 -3
- package/dist/fn.js.map +1 -1
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +4 -2
- package/dist/fs.js.map +1 -1
- package/dist/index.d.ts +13 -15
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -12
- package/dist/index.js.map +1 -1
- package/dist/internal/cdn/cloudflare.d.ts +48 -0
- package/dist/internal/cdn/cloudflare.d.ts.map +1 -0
- package/dist/internal/cdn/cloudflare.js +94 -0
- package/dist/internal/cdn/cloudflare.js.map +1 -0
- package/dist/internal/cdn/core.d.ts +91 -0
- package/dist/internal/cdn/core.d.ts.map +1 -0
- package/dist/internal/cdn/core.js +220 -0
- package/dist/internal/cdn/core.js.map +1 -0
- package/dist/internal/cdn/fastly.d.ts +17 -0
- package/dist/internal/cdn/fastly.d.ts.map +1 -0
- package/dist/internal/cdn/fastly.js +43 -0
- package/dist/internal/cdn/fastly.js.map +1 -0
- package/dist/internal/cdn/vercel.d.ts +31 -0
- package/dist/internal/cdn/vercel.d.ts.map +1 -0
- package/dist/internal/cdn/vercel.js +56 -0
- package/dist/internal/cdn/vercel.js.map +1 -0
- package/dist/internal/code-unit-order.d.ts +4 -0
- package/dist/internal/code-unit-order.d.ts.map +1 -0
- package/dist/internal/code-unit-order.js +6 -0
- package/dist/internal/code-unit-order.js.map +1 -0
- package/dist/internal/codegen.d.ts.map +1 -1
- package/dist/internal/codegen.js +141 -25
- package/dist/internal/codegen.js.map +1 -1
- package/dist/internal/dev-csp.d.ts +24 -0
- package/dist/internal/dev-csp.d.ts.map +1 -0
- package/dist/internal/dev-csp.js +160 -0
- package/dist/internal/dev-csp.js.map +1 -0
- package/dist/internal/dev-reserved.d.ts +25 -0
- package/dist/internal/dev-reserved.d.ts.map +1 -0
- package/dist/internal/dev-reserved.js +39 -0
- package/dist/internal/dev-reserved.js.map +1 -0
- package/dist/internal/html-spans.d.ts +10 -0
- package/dist/internal/html-spans.d.ts.map +1 -0
- package/dist/internal/html-spans.js +46 -0
- package/dist/internal/html-spans.js.map +1 -0
- package/dist/internal/loading-runtime.d.ts +44 -0
- package/dist/internal/loading-runtime.d.ts.map +1 -0
- package/dist/internal/loading-runtime.js +206 -0
- package/dist/internal/loading-runtime.js.map +1 -0
- package/dist/internal/matches-runtime.d.ts +8 -0
- package/dist/internal/matches-runtime.d.ts.map +1 -0
- package/dist/internal/matches-runtime.js +61 -0
- package/dist/internal/matches-runtime.js.map +1 -0
- package/dist/internal/mount-shadow.d.ts +36 -0
- package/dist/internal/mount-shadow.d.ts.map +1 -0
- package/dist/internal/mount-shadow.js +65 -0
- package/dist/internal/mount-shadow.js.map +1 -0
- package/dist/internal/node-builtins.d.ts +3 -0
- package/dist/internal/node-builtins.d.ts.map +1 -0
- package/dist/internal/node-builtins.js +79 -0
- package/dist/internal/node-builtins.js.map +1 -0
- package/dist/internal/output-guard.d.ts +65 -0
- package/dist/internal/output-guard.d.ts.map +1 -0
- package/dist/internal/output-guard.js +630 -0
- package/dist/internal/output-guard.js.map +1 -0
- package/dist/internal/page-execution.d.ts +20 -8
- package/dist/internal/page-execution.d.ts.map +1 -1
- package/dist/internal/page-execution.js +571 -105
- package/dist/internal/page-execution.js.map +1 -1
- package/dist/internal/parity.d.ts +21 -0
- package/dist/internal/parity.d.ts.map +1 -1
- package/dist/internal/parity.js +104 -22
- package/dist/internal/parity.js.map +1 -1
- package/dist/internal/private-env.d.ts +12 -0
- package/dist/internal/private-env.d.ts.map +1 -0
- package/dist/internal/private-env.js +335 -0
- package/dist/internal/private-env.js.map +1 -0
- package/dist/internal/public-url.d.ts +3 -0
- package/dist/internal/public-url.d.ts.map +1 -0
- package/dist/internal/public-url.js +18 -0
- package/dist/internal/public-url.js.map +1 -0
- package/dist/internal/render-document.d.ts +25 -19
- package/dist/internal/render-document.d.ts.map +1 -1
- package/dist/internal/render-document.js +77 -61
- package/dist/internal/render-document.js.map +1 -1
- package/dist/internal/response-controls.d.ts +75 -0
- package/dist/internal/response-controls.d.ts.map +1 -0
- package/dist/internal/response-controls.js +256 -0
- package/dist/internal/response-controls.js.map +1 -0
- package/dist/internal/runtime-contract.d.ts +11 -16
- package/dist/internal/runtime-contract.d.ts.map +1 -1
- package/dist/internal/runtime-contract.js.map +1 -1
- package/dist/internal/secret-scan.d.ts +104 -0
- package/dist/internal/secret-scan.d.ts.map +1 -0
- package/dist/internal/secret-scan.js +503 -0
- package/dist/internal/secret-scan.js.map +1 -0
- package/dist/internal/server-boundary.d.ts +6 -29
- package/dist/internal/server-boundary.d.ts.map +1 -1
- package/dist/internal/server-boundary.js +6 -66
- package/dist/internal/server-boundary.js.map +1 -1
- package/dist/internal/source-map.d.ts +47 -0
- package/dist/internal/source-map.d.ts.map +1 -0
- package/dist/internal/source-map.js +225 -0
- package/dist/internal/source-map.js.map +1 -0
- package/dist/internal/target-compat.d.ts +27 -0
- package/dist/internal/target-compat.d.ts.map +1 -0
- package/dist/internal/target-compat.js +77 -0
- package/dist/internal/target-compat.js.map +1 -0
- package/dist/internal/timing-safe-equal.d.ts +6 -0
- package/dist/internal/timing-safe-equal.d.ts.map +1 -1
- package/dist/internal/timing-safe-equal.js +10 -0
- package/dist/internal/timing-safe-equal.js.map +1 -1
- package/dist/internal/vitals-runtime.d.ts +18 -0
- package/dist/internal/vitals-runtime.d.ts.map +1 -0
- package/dist/internal/vitals-runtime.js +38 -0
- package/dist/internal/vitals-runtime.js.map +1 -0
- package/dist/internal/web-app.d.ts +38 -9
- package/dist/internal/web-app.d.ts.map +1 -1
- package/dist/internal/web-app.js +199 -59
- package/dist/internal/web-app.js.map +1 -1
- package/dist/internal/zone-graph.d.ts +89 -0
- package/dist/internal/zone-graph.d.ts.map +1 -0
- package/dist/internal/zone-graph.js +492 -0
- package/dist/internal/zone-graph.js.map +1 -0
- package/dist/isr.d.ts +83 -6
- package/dist/isr.d.ts.map +1 -1
- package/dist/isr.js +263 -36
- package/dist/isr.js.map +1 -1
- package/dist/llms-txt.d.ts.map +1 -1
- package/dist/llms-txt.js +31 -6
- package/dist/llms-txt.js.map +1 -1
- package/dist/manifest.d.ts +220 -13
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +394 -9
- package/dist/manifest.js.map +1 -1
- package/dist/module-graph.d.ts +26 -5
- package/dist/module-graph.d.ts.map +1 -1
- package/dist/module-graph.js +17 -7
- package/dist/module-graph.js.map +1 -1
- package/dist/navigation.d.ts +17 -3
- package/dist/navigation.d.ts.map +1 -1
- package/dist/navigation.js +11 -5
- package/dist/navigation.js.map +1 -1
- package/dist/nonce.d.ts +47 -0
- package/dist/nonce.d.ts.map +1 -0
- package/dist/nonce.js +63 -0
- package/dist/nonce.js.map +1 -0
- package/dist/plugins/css-modules.d.ts.map +1 -1
- package/dist/plugins/css-modules.js +4 -3
- package/dist/plugins/css-modules.js.map +1 -1
- package/dist/plugins/kit.d.ts +9 -0
- package/dist/plugins/kit.d.ts.map +1 -1
- package/dist/plugins/kit.js +18 -0
- package/dist/plugins/kit.js.map +1 -1
- package/dist/plugins/svg.d.ts +22 -3
- package/dist/plugins/svg.d.ts.map +1 -1
- package/dist/plugins/svg.js +112 -1
- package/dist/plugins/svg.js.map +1 -1
- package/dist/plugins/vite-leak-guard.d.ts +107 -3
- package/dist/plugins/vite-leak-guard.d.ts.map +1 -1
- package/dist/plugins/vite-leak-guard.js +378 -17
- package/dist/plugins/vite-leak-guard.js.map +1 -1
- package/dist/plugins/vite-zone-guard.d.ts +61 -0
- package/dist/plugins/vite-zone-guard.d.ts.map +1 -0
- package/dist/plugins/vite-zone-guard.js +208 -0
- package/dist/plugins/vite-zone-guard.js.map +1 -0
- package/dist/plugins/zone-guard.d.ts +27 -0
- package/dist/plugins/zone-guard.d.ts.map +1 -0
- package/dist/plugins/zone-guard.js +80 -0
- package/dist/plugins/zone-guard.js.map +1 -0
- package/dist/prerender.d.ts +10 -0
- package/dist/prerender.d.ts.map +1 -1
- package/dist/prerender.js +35 -3
- package/dist/prerender.js.map +1 -1
- package/dist/public-dir.d.ts +9 -0
- package/dist/public-dir.d.ts.map +1 -1
- package/dist/public-dir.js +67 -9
- package/dist/public-dir.js.map +1 -1
- package/dist/pwa-manifest.d.ts +68 -0
- package/dist/pwa-manifest.d.ts.map +1 -0
- package/dist/pwa-manifest.js +178 -0
- package/dist/pwa-manifest.js.map +1 -0
- package/dist/query.js +4 -4
- package/dist/query.js.map +1 -1
- package/dist/render-seam.d.ts +42 -2
- package/dist/render-seam.d.ts.map +1 -1
- package/dist/render-seam.js +6 -0
- package/dist/render-seam.js.map +1 -1
- package/dist/route-manifest.d.ts +3 -0
- package/dist/route-manifest.d.ts.map +1 -1
- package/dist/route-manifest.js +26 -3
- package/dist/route-manifest.js.map +1 -1
- package/dist/route-types.d.ts +24 -0
- package/dist/route-types.d.ts.map +1 -0
- package/dist/route-types.js +195 -0
- package/dist/route-types.js.map +1 -0
- package/dist/router.d.ts +28 -6
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +129 -40
- package/dist/router.js.map +1 -1
- package/dist/vitals.d.ts +33 -0
- package/dist/vitals.d.ts.map +1 -0
- package/dist/vitals.js +16 -0
- package/dist/vitals.js.map +1 -0
- package/dist/vite.d.ts +16 -1
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +248 -43
- package/dist/vite.js.map +1 -1
- package/dist/zones.d.ts +70 -0
- package/dist/zones.d.ts.map +1 -0
- package/dist/zones.js +365 -0
- package/dist/zones.js.map +1 -0
- package/package.json +67 -14
- package/src/backend-only.ts +16 -0
- package/src/boundary.ts +18 -5
- package/src/build-plan.ts +90 -35
- package/src/build-vite.ts +77 -11
- package/src/build.ts +466 -96
- package/src/bun-dev-entry.ts +1 -1
- package/src/cdn.ts +216 -0
- package/src/client.ts +135 -36
- package/src/conformance.ts +16 -3
- package/src/csp.ts +194 -0
- package/src/dev-client.ts +349 -0
- package/src/dev-diagnostics.ts +60 -7
- package/src/dev-error.ts +49 -5
- package/src/dev-feed.ts +1140 -0
- package/src/dev-indicator.ts +113 -0
- package/src/dev-session.ts +729 -0
- package/src/dev-sourcemap.ts +281 -0
- package/src/dev-ssr-graph.ts +16 -5
- package/src/dev.ts +86 -43
- package/src/diagnostic-catalog.ts +243 -0
- package/src/diagnostic-prompt.ts +355 -0
- package/src/diagnostic.ts +63 -66
- package/src/draft.ts +4 -1
- package/src/fn.ts +19 -3
- package/src/fs.ts +13 -2
- package/src/index.ts +42 -10
- package/src/internal/cdn/cloudflare.ts +146 -0
- package/src/internal/cdn/core.ts +316 -0
- package/src/internal/cdn/fastly.ts +62 -0
- package/src/internal/cdn/vercel.ts +95 -0
- package/src/internal/code-unit-order.ts +5 -0
- package/src/internal/codegen.ts +161 -24
- package/src/internal/dev-csp.ts +176 -0
- package/src/internal/dev-reserved.ts +52 -0
- package/src/internal/html-spans.ts +50 -0
- package/src/internal/loading-runtime.ts +244 -0
- package/src/internal/matches-runtime.ts +62 -0
- package/src/internal/mount-shadow.ts +88 -0
- package/src/internal/node-builtins.ts +79 -0
- package/src/internal/output-guard.ts +701 -0
- package/src/internal/page-execution.ts +795 -185
- package/src/internal/parity.ts +150 -36
- package/src/internal/private-env.ts +334 -0
- package/src/internal/public-url.ts +22 -0
- package/src/internal/render-document.ts +114 -67
- package/src/internal/response-controls.ts +284 -0
- package/src/internal/runtime-contract.ts +10 -15
- package/src/internal/secret-scan.ts +638 -0
- package/src/internal/server-boundary.ts +6 -69
- package/src/internal/source-map.ts +256 -0
- package/src/internal/target-compat.ts +92 -0
- package/src/internal/timing-safe-equal.ts +13 -0
- package/src/internal/vitals-runtime.ts +53 -0
- package/src/internal/web-app.ts +262 -84
- package/src/internal/zone-graph.ts +593 -0
- package/src/isr.ts +325 -37
- package/src/llms-txt.ts +28 -4
- package/src/manifest.ts +682 -16
- package/src/module-graph.ts +53 -9
- package/src/navigation.ts +29 -6
- package/src/nonce.ts +104 -0
- package/src/plugins/css-modules.ts +5 -3
- package/src/plugins/kit.ts +24 -0
- package/src/plugins/svg.ts +135 -4
- package/src/plugins/vite-leak-guard.ts +529 -16
- package/src/plugins/vite-zone-guard.ts +263 -0
- package/src/plugins/zone-guard.ts +119 -0
- package/src/prerender.ts +49 -3
- package/src/public-dir.ts +69 -8
- package/src/pwa-manifest.ts +226 -0
- package/src/query.ts +4 -4
- package/src/render-seam.ts +46 -1
- package/src/route-manifest.ts +15 -3
- package/src/route-types.ts +264 -0
- package/src/router.ts +163 -63
- package/src/vitals.ts +44 -0
- package/src/vite.ts +257 -42
- package/src/zones.ts +430 -0
- package/dist/plugins/vite-server-only.d.ts +0 -49
- package/dist/plugins/vite-server-only.d.ts.map +0 -1
- package/dist/plugins/vite-server-only.js +0 -57
- package/dist/plugins/vite-server-only.js.map +0 -1
- package/dist/server-only.d.ts +0 -21
- package/dist/server-only.d.ts.map +0 -1
- package/dist/server-only.js +0 -25
- package/dist/server-only.js.map +0 -1
- package/src/plugins/vite-server-only.ts +0 -75
- package/src/server-only.ts +0 -25
package/dist/dev-feed.js
ADDED
|
@@ -0,0 +1,797 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nifrajs/web/dev-feed` - what a running dev server saw, kept so the agent driving it can ask.
|
|
3
|
+
*
|
|
4
|
+
* Both dev servers (`./dev.ts`, `./vite.ts`) record into one {@link DevFeed}: every error with its
|
|
5
|
+
* structured {@link Diagnostic} (failures a `_error` boundary rendered, backend 500s, build and guard
|
|
6
|
+
* failures, browser errors, crashes), every console line, and every request. `nifra errors`,
|
|
7
|
+
* `nifra logs` and `nifra inspect` (and their MCP tools) read it back over the dev server's own HTTP
|
|
8
|
+
* surface, found through the discovery record this module also owns.
|
|
9
|
+
*
|
|
10
|
+
* Three properties hold for everything stored here:
|
|
11
|
+
* - Redacted on the way in. Secret formats and non-public env values are scrubbed before an entry
|
|
12
|
+
* exists, so nothing unredacted is ever served, persisted, or handed to an agent.
|
|
13
|
+
* - Correlated. An entry recorded while a request is in flight carries that request's id.
|
|
14
|
+
* - Edit-aware. Every entry has a monotonic `seq` (query with `since`) and the `generation` it was last
|
|
15
|
+
* seen in; a file change bumps the generation, so an error recorded before the latest edit reads as
|
|
16
|
+
* `stale` instead of current.
|
|
17
|
+
*
|
|
18
|
+
* Dev-only by construction: no production entry imports this module.
|
|
19
|
+
*/
|
|
20
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
21
|
+
import { randomBytes } from "node:crypto";
|
|
22
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
23
|
+
import { dirname, join } from "node:path";
|
|
24
|
+
import { formatWithOptions, stripVTControlCharacters } from "node:util";
|
|
25
|
+
import { buildDiagnostic, } from "./diagnostic.js";
|
|
26
|
+
import { createRedactor } from "./internal/secret-scan.js";
|
|
27
|
+
import { originalPosition, ssrSourceMaps } from "./internal/source-map.js";
|
|
28
|
+
/** Version of the HTTP + record contract between a dev server and the tools that read it. */
|
|
29
|
+
export const DEV_FEED_SCHEMA = 1;
|
|
30
|
+
/** The dev server's agent-facing endpoints. All live under `/__nifra/`, which no route can produce. */
|
|
31
|
+
export const DEV_FEED_PATHS = Object.freeze({
|
|
32
|
+
identity: "/__nifra/dev",
|
|
33
|
+
errors: "/__nifra/errors",
|
|
34
|
+
logs: "/__nifra/logs",
|
|
35
|
+
requests: "/__nifra/requests",
|
|
36
|
+
clientEvent: "/__nifra/client-event",
|
|
37
|
+
indicator: "/__nifra/dev-indicator.js",
|
|
38
|
+
});
|
|
39
|
+
/** Request header carrying the agent token from the discovery record. */
|
|
40
|
+
export const DEV_TOKEN_HEADER = "x-nifra-dev-token";
|
|
41
|
+
/** Response header naming the request id every entry recorded during that request carries. */
|
|
42
|
+
export const DEV_REQUEST_ID_HEADER = "x-nifra-request-id";
|
|
43
|
+
/** Response header marking a body as dev-feed JSON (so a tool can tell it from an app route). */
|
|
44
|
+
export const DEV_FEED_HEADER = "x-nifra-dev-feed";
|
|
45
|
+
/** Discovery record and persisted log, relative to the project root. `.nifra/` is gitignored. */
|
|
46
|
+
export const DEV_SERVER_RECORD_FILE = ".nifra/dev-server.json";
|
|
47
|
+
export const DEV_SERVER_LOG_FILE = ".nifra/dev-server.log";
|
|
48
|
+
/**
|
|
49
|
+
* Every file the feed writes (record, its temp file, log, rotated log): what a file watcher skips. A
|
|
50
|
+
* RegExp, not a glob: a glob's `**` does not cross a dot-directory such as the project's own path.
|
|
51
|
+
*/
|
|
52
|
+
export const DEV_SERVER_FILES = /[\\/]\.nifra[\\/]dev-server\./;
|
|
53
|
+
export const DEV_ERROR_CATEGORIES = [
|
|
54
|
+
"ssr",
|
|
55
|
+
"page",
|
|
56
|
+
"api",
|
|
57
|
+
"build",
|
|
58
|
+
"browser",
|
|
59
|
+
"hydration",
|
|
60
|
+
"process",
|
|
61
|
+
];
|
|
62
|
+
export const DEV_LOG_LEVELS = ["debug", "info", "log", "warn", "error"];
|
|
63
|
+
/** Narrow a string to a {@link DevLogLevel}. */
|
|
64
|
+
export function isDevLogLevel(value) {
|
|
65
|
+
return DEV_LOG_LEVELS.some((level) => level === value);
|
|
66
|
+
}
|
|
67
|
+
/** Narrow a string to a {@link DevErrorCategory}. */
|
|
68
|
+
export function isDevErrorCategory(value) {
|
|
69
|
+
return DEV_ERROR_CATEGORIES.some((category) => category === value);
|
|
70
|
+
}
|
|
71
|
+
export const DEFAULT_DEV_FEED_LIMITS = Object.freeze({
|
|
72
|
+
errors: 100,
|
|
73
|
+
logs: 2000,
|
|
74
|
+
logBytes: 1024 * 1024,
|
|
75
|
+
requests: 500,
|
|
76
|
+
message: 8 * 1024,
|
|
77
|
+
stack: 16 * 1024,
|
|
78
|
+
});
|
|
79
|
+
// One store per process: a second copy of this module (a dual install) must still see the request the
|
|
80
|
+
// first one started, or its log lines would land untagged.
|
|
81
|
+
const ALS_KEY = Symbol.for("nifra.devFeed.requestContext");
|
|
82
|
+
const requestContext = sharedRequestContext();
|
|
83
|
+
function sharedRequestContext() {
|
|
84
|
+
const existing = Reflect.get(globalThis, ALS_KEY);
|
|
85
|
+
if (existing instanceof AsyncLocalStorage)
|
|
86
|
+
return existing;
|
|
87
|
+
const created = new AsyncLocalStorage();
|
|
88
|
+
Reflect.set(globalThis, ALS_KEY, created);
|
|
89
|
+
return created;
|
|
90
|
+
}
|
|
91
|
+
/** Run `fn` as the handling of one request: entries recorded inside it carry `ctx.requestId`. */
|
|
92
|
+
export function runWithDevRequest(ctx, fn) {
|
|
93
|
+
return requestContext.run(ctx, fn);
|
|
94
|
+
}
|
|
95
|
+
/** The request being handled on this async path, if any. */
|
|
96
|
+
export function currentDevRequest() {
|
|
97
|
+
return requestContext.getStore();
|
|
98
|
+
}
|
|
99
|
+
/** The request in flight on this async path, when it belongs to the feed `feedId`. */
|
|
100
|
+
function currentRequestFor(feedId) {
|
|
101
|
+
const ctx = requestContext.getStore();
|
|
102
|
+
return ctx?.feedId === feedId ? ctx : undefined;
|
|
103
|
+
}
|
|
104
|
+
const nowIso = () => new Date().toISOString();
|
|
105
|
+
const clampLimit = (value, fallback, max) => value === undefined || !Number.isFinite(value) || value < 1
|
|
106
|
+
? fallback
|
|
107
|
+
: Math.min(Math.floor(value), max);
|
|
108
|
+
/** FNV-1a: a stable short id for a failure fingerprint. Not a security boundary. */
|
|
109
|
+
function fnv1a(text) {
|
|
110
|
+
let hash = 0x811c9dc5;
|
|
111
|
+
for (let i = 0; i < text.length; i++) {
|
|
112
|
+
hash ^= text.charCodeAt(i);
|
|
113
|
+
hash = Math.imul(hash, 0x01000193);
|
|
114
|
+
}
|
|
115
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
116
|
+
}
|
|
117
|
+
const cap = (text, max) => text.length <= max ? text : `${text.slice(0, max)}... [${text.length - max} more chars]`;
|
|
118
|
+
const firstLine = (text) => {
|
|
119
|
+
const end = text.indexOf("\n");
|
|
120
|
+
return end === -1 ? text : text.slice(0, end);
|
|
121
|
+
};
|
|
122
|
+
function snapshotError(record, currentGeneration) {
|
|
123
|
+
const entry = {
|
|
124
|
+
seq: record.seq,
|
|
125
|
+
id: record.id,
|
|
126
|
+
category: record.category,
|
|
127
|
+
source: record.source,
|
|
128
|
+
diagnostic: record.diagnostic,
|
|
129
|
+
count: record.count,
|
|
130
|
+
firstAt: record.firstAt,
|
|
131
|
+
lastAt: record.lastAt,
|
|
132
|
+
generation: record.generation,
|
|
133
|
+
stale: record.generation < currentGeneration,
|
|
134
|
+
};
|
|
135
|
+
if (record.requestId !== undefined)
|
|
136
|
+
entry.requestId = record.requestId;
|
|
137
|
+
if (record.route !== undefined)
|
|
138
|
+
entry.route = record.route;
|
|
139
|
+
if (record.page !== undefined)
|
|
140
|
+
entry.page = record.page;
|
|
141
|
+
if (record.resolvedAt !== undefined)
|
|
142
|
+
entry.resolvedAt = record.resolvedAt;
|
|
143
|
+
return entry;
|
|
144
|
+
}
|
|
145
|
+
function snapshotTrace(trace) {
|
|
146
|
+
const entry = {
|
|
147
|
+
seq: trace.seq,
|
|
148
|
+
requestId: trace.requestId,
|
|
149
|
+
method: trace.method,
|
|
150
|
+
path: trace.path,
|
|
151
|
+
at: trace.at,
|
|
152
|
+
errorIds: [...trace.errorIds],
|
|
153
|
+
logCount: trace.logCount,
|
|
154
|
+
};
|
|
155
|
+
if (trace.status !== undefined)
|
|
156
|
+
entry.status = trace.status;
|
|
157
|
+
if (trace.durationMs !== undefined)
|
|
158
|
+
entry.durationMs = trace.durationMs;
|
|
159
|
+
if (trace.bytes !== undefined)
|
|
160
|
+
entry.bytes = trace.bytes;
|
|
161
|
+
if (trace.isr !== undefined)
|
|
162
|
+
entry.isr = trace.isr;
|
|
163
|
+
return entry;
|
|
164
|
+
}
|
|
165
|
+
function evictOldest(map, max) {
|
|
166
|
+
while (map.size > max) {
|
|
167
|
+
const oldest = map.keys().next();
|
|
168
|
+
if (oldest.done === true)
|
|
169
|
+
return;
|
|
170
|
+
map.delete(oldest.value);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
let feedCounter = 0;
|
|
174
|
+
/** Create the store a dev server records into. */
|
|
175
|
+
export function createDevFeed(options) {
|
|
176
|
+
const limits = { ...DEFAULT_DEV_FEED_LIMITS, ...options.limits };
|
|
177
|
+
const root = options.root;
|
|
178
|
+
const redactText = createRedactor(options.env ?? process.env, options.publicEnvPrefix);
|
|
179
|
+
const persist = options.persist === false ? undefined : createPersistence(root);
|
|
180
|
+
feedCounter += 1;
|
|
181
|
+
const id = `feed${feedCounter}-${randomBytes(3).toString("hex")}`;
|
|
182
|
+
const errors = new Map();
|
|
183
|
+
const traces = new Map();
|
|
184
|
+
let logs = [];
|
|
185
|
+
let logBytes = 0;
|
|
186
|
+
let dropped = 0;
|
|
187
|
+
let seq = 0;
|
|
188
|
+
let generation = 0;
|
|
189
|
+
const redactFrame = (frame) => ({
|
|
190
|
+
...frame,
|
|
191
|
+
raw: cap(redactText(frame.raw), 1024),
|
|
192
|
+
});
|
|
193
|
+
const redactDiagnostic = (diagnostic) => {
|
|
194
|
+
const redacted = {
|
|
195
|
+
...diagnostic,
|
|
196
|
+
message: cap(redactText(diagnostic.message), limits.message),
|
|
197
|
+
frames: diagnostic.frames.slice(0, 50).map(redactFrame),
|
|
198
|
+
};
|
|
199
|
+
if (diagnostic.request !== undefined) {
|
|
200
|
+
redacted.request = {
|
|
201
|
+
method: diagnostic.request.method,
|
|
202
|
+
url: cap(redactText(diagnostic.request.url), 2048),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
if (diagnostic.codeframe !== undefined) {
|
|
206
|
+
redacted.codeframe = {
|
|
207
|
+
...diagnostic.codeframe,
|
|
208
|
+
lines: diagnostic.codeframe.lines.map((line) => ({
|
|
209
|
+
...line,
|
|
210
|
+
text: cap(redactText(line.text), 400),
|
|
211
|
+
})),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
return redacted;
|
|
215
|
+
};
|
|
216
|
+
const recordDiagnostic = (raw, meta) => {
|
|
217
|
+
const diagnostic = redactDiagnostic(raw);
|
|
218
|
+
const requestId = meta.requestId ?? currentRequestFor(id)?.requestId;
|
|
219
|
+
const top = diagnostic.frames.find((frame) => frame.file !== undefined);
|
|
220
|
+
const fingerprint = [
|
|
221
|
+
meta.category,
|
|
222
|
+
diagnostic.code,
|
|
223
|
+
diagnostic.name,
|
|
224
|
+
firstLine(diagnostic.message).slice(0, 300),
|
|
225
|
+
top === undefined ? "" : `${top.file}:${top.line}`,
|
|
226
|
+
].join("|");
|
|
227
|
+
const key = `e_${fnv1a(fingerprint)}`;
|
|
228
|
+
const at = nowIso();
|
|
229
|
+
seq += 1;
|
|
230
|
+
const existing = errors.get(key);
|
|
231
|
+
const record = existing ?? {
|
|
232
|
+
seq,
|
|
233
|
+
id: key,
|
|
234
|
+
category: meta.category,
|
|
235
|
+
source: meta.source ?? "server",
|
|
236
|
+
diagnostic,
|
|
237
|
+
count: 0,
|
|
238
|
+
firstAt: at,
|
|
239
|
+
lastAt: at,
|
|
240
|
+
generation,
|
|
241
|
+
requestId: undefined,
|
|
242
|
+
route: undefined,
|
|
243
|
+
page: undefined,
|
|
244
|
+
resolvedAt: undefined,
|
|
245
|
+
};
|
|
246
|
+
record.seq = seq;
|
|
247
|
+
record.diagnostic = diagnostic;
|
|
248
|
+
record.count += 1;
|
|
249
|
+
record.lastAt = at;
|
|
250
|
+
record.generation = generation;
|
|
251
|
+
record.resolvedAt = undefined;
|
|
252
|
+
if (requestId !== undefined)
|
|
253
|
+
record.requestId = requestId;
|
|
254
|
+
if (meta.route !== undefined)
|
|
255
|
+
record.route = meta.route;
|
|
256
|
+
if (meta.page !== undefined)
|
|
257
|
+
record.page = meta.page;
|
|
258
|
+
// Re-insert: Map order is recency order, which eviction relies on.
|
|
259
|
+
errors.delete(key);
|
|
260
|
+
errors.set(key, record);
|
|
261
|
+
evictOldest(errors, limits.errors);
|
|
262
|
+
if (requestId !== undefined) {
|
|
263
|
+
const trace = traces.get(requestId);
|
|
264
|
+
if (trace !== undefined && !trace.errorIds.includes(key))
|
|
265
|
+
trace.errorIds.push(key);
|
|
266
|
+
}
|
|
267
|
+
const entry = snapshotError(record, generation);
|
|
268
|
+
persist?.write({ t: "error", ...entry }, true);
|
|
269
|
+
return entry;
|
|
270
|
+
};
|
|
271
|
+
const recordLog = (level, message, meta = {}) => {
|
|
272
|
+
const text = cap(redactText(message), limits.message);
|
|
273
|
+
if (text.length === 0)
|
|
274
|
+
return;
|
|
275
|
+
const requestId = meta.requestId ?? currentRequestFor(id)?.requestId;
|
|
276
|
+
seq += 1;
|
|
277
|
+
const entry = {
|
|
278
|
+
seq,
|
|
279
|
+
at: nowIso(),
|
|
280
|
+
level,
|
|
281
|
+
source: meta.source ?? "server",
|
|
282
|
+
message: text,
|
|
283
|
+
};
|
|
284
|
+
if (requestId !== undefined)
|
|
285
|
+
entry.requestId = requestId;
|
|
286
|
+
if (meta.page !== undefined)
|
|
287
|
+
entry.page = meta.page;
|
|
288
|
+
logs.push(entry);
|
|
289
|
+
logBytes += text.length;
|
|
290
|
+
if (logs.length > limits.logs || logBytes > limits.logBytes) {
|
|
291
|
+
// Past the count cap, evict an extra tenth so a full ring does not re-slice on every line.
|
|
292
|
+
const byCount = logs.length > limits.logs
|
|
293
|
+
? Math.min(logs.length - 1, logs.length - limits.logs + Math.floor(limits.logs / 10))
|
|
294
|
+
: 0;
|
|
295
|
+
let cut = 0;
|
|
296
|
+
let freed = 0;
|
|
297
|
+
while (cut < logs.length - 1 && (cut < byCount || logBytes - freed > limits.logBytes)) {
|
|
298
|
+
freed += logs[cut]?.message.length ?? 0;
|
|
299
|
+
cut += 1;
|
|
300
|
+
}
|
|
301
|
+
logs = logs.slice(cut);
|
|
302
|
+
logBytes -= freed;
|
|
303
|
+
dropped += cut;
|
|
304
|
+
}
|
|
305
|
+
if (requestId !== undefined) {
|
|
306
|
+
const trace = traces.get(requestId);
|
|
307
|
+
if (trace !== undefined)
|
|
308
|
+
trace.logCount += 1;
|
|
309
|
+
}
|
|
310
|
+
persist?.write({ t: "log", ...entry }, level === "error");
|
|
311
|
+
};
|
|
312
|
+
return {
|
|
313
|
+
id,
|
|
314
|
+
root,
|
|
315
|
+
get generation() {
|
|
316
|
+
return generation;
|
|
317
|
+
},
|
|
318
|
+
get cursor() {
|
|
319
|
+
return seq;
|
|
320
|
+
},
|
|
321
|
+
recordError(error, meta) {
|
|
322
|
+
const diagnosticOptions = { root };
|
|
323
|
+
if (meta.request !== undefined)
|
|
324
|
+
diagnosticOptions.request = meta.request;
|
|
325
|
+
if (options.showSource !== undefined)
|
|
326
|
+
diagnosticOptions.showSource = options.showSource;
|
|
327
|
+
if (meta.source !== "browser")
|
|
328
|
+
diagnosticOptions.remap = ssrRemap;
|
|
329
|
+
return recordDiagnostic(buildDiagnostic(error, diagnosticOptions), meta);
|
|
330
|
+
},
|
|
331
|
+
recordDiagnostic,
|
|
332
|
+
recordLog,
|
|
333
|
+
startRequest(requestId, method, path) {
|
|
334
|
+
seq += 1;
|
|
335
|
+
traces.set(requestId, {
|
|
336
|
+
seq,
|
|
337
|
+
requestId,
|
|
338
|
+
method,
|
|
339
|
+
path: cap(redactText(path), 2048),
|
|
340
|
+
at: nowIso(),
|
|
341
|
+
status: undefined,
|
|
342
|
+
durationMs: undefined,
|
|
343
|
+
bytes: undefined,
|
|
344
|
+
isr: undefined,
|
|
345
|
+
errorIds: [],
|
|
346
|
+
logCount: 0,
|
|
347
|
+
});
|
|
348
|
+
evictOldest(traces, limits.requests);
|
|
349
|
+
},
|
|
350
|
+
finishRequest(requestId, response, durationMs) {
|
|
351
|
+
const trace = traces.get(requestId);
|
|
352
|
+
if (trace === undefined)
|
|
353
|
+
return;
|
|
354
|
+
trace.status = response.status;
|
|
355
|
+
trace.durationMs = Math.round(durationMs * 100) / 100;
|
|
356
|
+
trace.bytes = response.bytes;
|
|
357
|
+
trace.isr = response.isr;
|
|
358
|
+
},
|
|
359
|
+
markChange() {
|
|
360
|
+
generation += 1;
|
|
361
|
+
},
|
|
362
|
+
resolve(category) {
|
|
363
|
+
const at = nowIso();
|
|
364
|
+
for (const record of errors.values()) {
|
|
365
|
+
if (record.category === category && record.resolvedAt === undefined)
|
|
366
|
+
record.resolvedAt = at;
|
|
367
|
+
}
|
|
368
|
+
},
|
|
369
|
+
errors(query = {}) {
|
|
370
|
+
const since = query.since ?? 0;
|
|
371
|
+
const selected = [];
|
|
372
|
+
for (const record of errors.values()) {
|
|
373
|
+
if (record.seq <= since)
|
|
374
|
+
continue;
|
|
375
|
+
if (query.categories !== undefined && !query.categories.includes(record.category))
|
|
376
|
+
continue;
|
|
377
|
+
if (query.includeResolved !== true && record.resolvedAt !== undefined)
|
|
378
|
+
continue;
|
|
379
|
+
if (query.includeStale === false && record.generation < generation)
|
|
380
|
+
continue;
|
|
381
|
+
if (query.requestId !== undefined && record.requestId !== query.requestId)
|
|
382
|
+
continue;
|
|
383
|
+
selected.push(snapshotError(record, generation));
|
|
384
|
+
}
|
|
385
|
+
const limit = clampLimit(query.limit, 50, limits.errors);
|
|
386
|
+
return { errors: selected.slice(-limit), cursor: seq, generation };
|
|
387
|
+
},
|
|
388
|
+
logs(query = {}) {
|
|
389
|
+
const since = query.since ?? 0;
|
|
390
|
+
const needle = query.grep?.toLowerCase();
|
|
391
|
+
const selected = logs.filter((entry) => entry.seq > since &&
|
|
392
|
+
(query.levels === undefined || query.levels.includes(entry.level)) &&
|
|
393
|
+
(query.source === undefined || entry.source === query.source) &&
|
|
394
|
+
(query.requestId === undefined || entry.requestId === query.requestId) &&
|
|
395
|
+
(needle === undefined || entry.message.toLowerCase().includes(needle)));
|
|
396
|
+
const limit = clampLimit(query.limit, 200, limits.logs);
|
|
397
|
+
return { logs: selected.slice(-limit), cursor: seq, generation, dropped };
|
|
398
|
+
},
|
|
399
|
+
requests(query = {}) {
|
|
400
|
+
const since = query.since ?? 0;
|
|
401
|
+
const selected = [];
|
|
402
|
+
for (const trace of traces.values()) {
|
|
403
|
+
if (trace.seq <= since)
|
|
404
|
+
continue;
|
|
405
|
+
if (query.path !== undefined && !trace.path.startsWith(query.path))
|
|
406
|
+
continue;
|
|
407
|
+
if (query.requestId !== undefined && trace.requestId !== query.requestId)
|
|
408
|
+
continue;
|
|
409
|
+
selected.push(snapshotTrace(trace));
|
|
410
|
+
}
|
|
411
|
+
const limit = clampLimit(query.limit, 50, limits.requests);
|
|
412
|
+
return { requests: selected.slice(-limit), cursor: seq, generation };
|
|
413
|
+
},
|
|
414
|
+
counts: () => ({ errors: errors.size, logs: logs.length, requests: traces.size }),
|
|
415
|
+
redact: redactText,
|
|
416
|
+
flush: () => persist?.flush(),
|
|
417
|
+
close: () => persist?.close(),
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
// ---------------------------------------------------------------------------------------------------
|
|
421
|
+
// Persistence
|
|
422
|
+
// ---------------------------------------------------------------------------------------------------
|
|
423
|
+
const LOG_ROTATE_BYTES = 2 * 1024 * 1024;
|
|
424
|
+
const LOG_FLUSH_MS = 250;
|
|
425
|
+
/**
|
|
426
|
+
* NDJSON on disk so a crashed dev server still leaves a record. Error-level writes are synchronous (the
|
|
427
|
+
* line most worth keeping is the one written just before a crash); the rest is buffered briefly. One
|
|
428
|
+
* rotation (`.1`) bounds the footprint. A write failure disables persistence rather than breaking dev.
|
|
429
|
+
*/
|
|
430
|
+
function createPersistence(root) {
|
|
431
|
+
const path = join(root, DEV_SERVER_LOG_FILE);
|
|
432
|
+
let buffer = [];
|
|
433
|
+
let size = 0;
|
|
434
|
+
let timer;
|
|
435
|
+
let disabled = false;
|
|
436
|
+
try {
|
|
437
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
438
|
+
size = existsSync(path) ? statSync(path).size : 0;
|
|
439
|
+
}
|
|
440
|
+
catch {
|
|
441
|
+
disabled = true;
|
|
442
|
+
}
|
|
443
|
+
const flush = () => {
|
|
444
|
+
if (timer !== undefined) {
|
|
445
|
+
clearTimeout(timer);
|
|
446
|
+
timer = undefined;
|
|
447
|
+
}
|
|
448
|
+
if (disabled || buffer.length === 0)
|
|
449
|
+
return;
|
|
450
|
+
const chunk = buffer.join("");
|
|
451
|
+
buffer = [];
|
|
452
|
+
try {
|
|
453
|
+
if (size + chunk.length > LOG_ROTATE_BYTES) {
|
|
454
|
+
renameSync(path, `${path}.1`);
|
|
455
|
+
size = 0;
|
|
456
|
+
}
|
|
457
|
+
appendFileSync(path, chunk);
|
|
458
|
+
size += chunk.length;
|
|
459
|
+
}
|
|
460
|
+
catch {
|
|
461
|
+
disabled = true;
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
return {
|
|
465
|
+
write(entry, sync) {
|
|
466
|
+
if (disabled)
|
|
467
|
+
return;
|
|
468
|
+
buffer.push(`${JSON.stringify(entry)}\n`);
|
|
469
|
+
if (sync)
|
|
470
|
+
flush();
|
|
471
|
+
else if (timer === undefined) {
|
|
472
|
+
timer = setTimeout(flush, LOG_FLUSH_MS);
|
|
473
|
+
timer.unref?.();
|
|
474
|
+
}
|
|
475
|
+
},
|
|
476
|
+
flush,
|
|
477
|
+
close: () => {
|
|
478
|
+
flush();
|
|
479
|
+
disabled = true;
|
|
480
|
+
},
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
484
|
+
function isPersistedError(value) {
|
|
485
|
+
return (isRecord(value) &&
|
|
486
|
+
typeof value.id === "string" &&
|
|
487
|
+
typeof value.seq === "number" &&
|
|
488
|
+
isDevErrorCategory(value.category) &&
|
|
489
|
+
isRecord(value.diagnostic));
|
|
490
|
+
}
|
|
491
|
+
function isPersistedLog(value) {
|
|
492
|
+
return (isRecord(value) &&
|
|
493
|
+
typeof value.message === "string" &&
|
|
494
|
+
typeof value.seq === "number" &&
|
|
495
|
+
isDevLogLevel(value.level));
|
|
496
|
+
}
|
|
497
|
+
/** The persisted record of a dev server that may no longer be running: newest last. */
|
|
498
|
+
export function readPersistedFeed(root, limit = 200) {
|
|
499
|
+
const path = join(root, DEV_SERVER_LOG_FILE);
|
|
500
|
+
const errors = new Map();
|
|
501
|
+
const logs = [];
|
|
502
|
+
for (const file of [`${path}.1`, path]) {
|
|
503
|
+
let text;
|
|
504
|
+
try {
|
|
505
|
+
text = readFileSync(file, "utf8");
|
|
506
|
+
}
|
|
507
|
+
catch {
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
for (const line of text.split("\n")) {
|
|
511
|
+
if (line.length === 0)
|
|
512
|
+
continue;
|
|
513
|
+
let value;
|
|
514
|
+
try {
|
|
515
|
+
value = JSON.parse(line);
|
|
516
|
+
}
|
|
517
|
+
catch {
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
if (!isRecord(value))
|
|
521
|
+
continue;
|
|
522
|
+
const { t, ...entry } = value;
|
|
523
|
+
if (t === "error" && isPersistedError(entry)) {
|
|
524
|
+
errors.delete(entry.id);
|
|
525
|
+
errors.set(entry.id, entry);
|
|
526
|
+
}
|
|
527
|
+
else if (t === "log" && isPersistedLog(entry)) {
|
|
528
|
+
logs.push(entry);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return { errors: [...errors.values()].slice(-limit), logs: logs.slice(-limit) };
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Record a dev server process that died, from the process that supervised it (`nifra dev` re-execs the
|
|
536
|
+
* Bun pipeline and pipes the child's stderr). The child's own monitor never sees an unhandled rejection
|
|
537
|
+
* on Bun, and a native crash print bypasses the console, so the supervisor's view of stderr is the
|
|
538
|
+
* record that survives. Persisted only: there is no live server left to serve it.
|
|
539
|
+
*/
|
|
540
|
+
export function recordDevCrash(root, exitCode, stderrTail) {
|
|
541
|
+
const feed = createDevFeed({ root });
|
|
542
|
+
const summary = `the dev server exited with code ${exitCode ?? "unknown"}`;
|
|
543
|
+
const error = new Error(summary);
|
|
544
|
+
error.name = "DevServerExit";
|
|
545
|
+
error.stack = `DevServerExit: ${summary}\n${stderrTail.slice(-DEFAULT_DEV_FEED_LIMITS.stack)}`;
|
|
546
|
+
feed.recordError(error, { category: "process" });
|
|
547
|
+
feed.close();
|
|
548
|
+
}
|
|
549
|
+
/** A fresh 256-bit token. */
|
|
550
|
+
export function createDevToken() {
|
|
551
|
+
return randomBytes(32).toString("base64url");
|
|
552
|
+
}
|
|
553
|
+
/** Write the record atomically (temp file + rename) and owner-only. */
|
|
554
|
+
export function writeDevServerRecord(root, record) {
|
|
555
|
+
const path = join(root, DEV_SERVER_RECORD_FILE);
|
|
556
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
557
|
+
const temp = `${path}.${process.pid}.tmp`;
|
|
558
|
+
writeFileSync(temp, `${JSON.stringify(record, null, 2)}\n`, { mode: 0o600 });
|
|
559
|
+
renameSync(temp, path);
|
|
560
|
+
}
|
|
561
|
+
/** Remove the record only while it is still this server's: a newer server may have replaced it. */
|
|
562
|
+
export function removeDevServerRecord(root, token) {
|
|
563
|
+
// @nifra-gate-reviewed: the server comparing its own token with a file on this machine; no request
|
|
564
|
+
// reaches this comparison.
|
|
565
|
+
if (readDevServerRecord(root)?.token !== token)
|
|
566
|
+
return;
|
|
567
|
+
rmSync(join(root, DEV_SERVER_RECORD_FILE), { force: true });
|
|
568
|
+
}
|
|
569
|
+
const isPort = (value) => typeof value === "number" && Number.isSafeInteger(value) && value > 0 && value < 65536;
|
|
570
|
+
function isDevServerRecord(value) {
|
|
571
|
+
return (isRecord(value) &&
|
|
572
|
+
typeof value.schema === "number" &&
|
|
573
|
+
typeof value.pid === "number" &&
|
|
574
|
+
Number.isSafeInteger(value.pid) &&
|
|
575
|
+
isPort(value.port) &&
|
|
576
|
+
typeof value.url === "string" &&
|
|
577
|
+
(value.pipeline === "bun" || value.pipeline === "vite") &&
|
|
578
|
+
typeof value.root === "string" &&
|
|
579
|
+
typeof value.startedAt === "string" &&
|
|
580
|
+
typeof value.token === "string" &&
|
|
581
|
+
value.token.length >= 32);
|
|
582
|
+
}
|
|
583
|
+
/** The record at `root`, or undefined when absent or malformed. Says nothing about liveness. */
|
|
584
|
+
export function readDevServerRecord(root) {
|
|
585
|
+
let value;
|
|
586
|
+
try {
|
|
587
|
+
value = JSON.parse(readFileSync(join(root, DEV_SERVER_RECORD_FILE), "utf8"));
|
|
588
|
+
}
|
|
589
|
+
catch {
|
|
590
|
+
return undefined;
|
|
591
|
+
}
|
|
592
|
+
return isDevServerRecord(value) ? value : undefined;
|
|
593
|
+
}
|
|
594
|
+
/** Whether a process with `pid` exists (EPERM means it exists but is not ours). */
|
|
595
|
+
export function isProcessAlive(pid) {
|
|
596
|
+
try {
|
|
597
|
+
process.kill(pid, 0);
|
|
598
|
+
return true;
|
|
599
|
+
}
|
|
600
|
+
catch (error) {
|
|
601
|
+
return error instanceof Error && "code" in error && error.code === "EPERM";
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
const CONSOLE_METHODS = ["debug", "info", "log", "warn", "error", "trace"];
|
|
605
|
+
const CONSOLE_LEVELS = {
|
|
606
|
+
debug: "debug",
|
|
607
|
+
info: "info",
|
|
608
|
+
log: "log",
|
|
609
|
+
warn: "warn",
|
|
610
|
+
error: "error",
|
|
611
|
+
trace: "log",
|
|
612
|
+
};
|
|
613
|
+
// Structural, not `instanceof`: a second copy of this module must join the first one's patch rather
|
|
614
|
+
// than layer another on top and capture every line twice.
|
|
615
|
+
const isCaptureState = (value) => isRecord(value) &&
|
|
616
|
+
value.sinks instanceof Set &&
|
|
617
|
+
typeof value.depth === "number" &&
|
|
618
|
+
typeof value.restore === "function";
|
|
619
|
+
const CAPTURE_KEY = Symbol.for("nifra.devFeed.capture");
|
|
620
|
+
const MAX_PARTIAL_LINE = 16 * 1024;
|
|
621
|
+
/** Format console arguments the way the console itself would, without color. */
|
|
622
|
+
export function formatConsoleArgs(args) {
|
|
623
|
+
try {
|
|
624
|
+
return formatWithOptions({ colors: false, depth: 4, breakLength: Number.POSITIVE_INFINITY, maxArrayLength: 50 }, ...args);
|
|
625
|
+
}
|
|
626
|
+
catch {
|
|
627
|
+
return args.map((arg) => String(arg)).join(" ");
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
const isCoreLogEntry = (value) => isRecord(value) && typeof value.level === "string" && typeof value.message === "string";
|
|
631
|
+
/** Parse a line core's default JSON logger wrote, or undefined for anything else. */
|
|
632
|
+
export function parseCoreLogLine(line) {
|
|
633
|
+
if (!line.startsWith("{") || !line.endsWith("}"))
|
|
634
|
+
return undefined;
|
|
635
|
+
try {
|
|
636
|
+
const value = JSON.parse(line);
|
|
637
|
+
return isCoreLogEntry(value) ? value : undefined;
|
|
638
|
+
}
|
|
639
|
+
catch {
|
|
640
|
+
return undefined;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
/** The thrown error core's `unhandled request error` log line describes, rebuilt for a Diagnostic. */
|
|
644
|
+
export function errorFromCoreLog(entry) {
|
|
645
|
+
if (entry.message !== "unhandled request error")
|
|
646
|
+
return undefined;
|
|
647
|
+
const name = typeof entry.name === "string" ? entry.name : "Error";
|
|
648
|
+
const detail = typeof entry.detail === "string" ? entry.detail : name;
|
|
649
|
+
const error = new Error(detail);
|
|
650
|
+
error.name = name;
|
|
651
|
+
error.stack = typeof entry.stack === "string" ? entry.stack : `${name}: ${detail}`;
|
|
652
|
+
return error;
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Tee the process's console and stdout/stderr into `sink`, leaving the real output untouched. Global
|
|
656
|
+
* and reference-counted: several dev servers (tests start many) share one patch, and the originals come
|
|
657
|
+
* back when the last sink detaches. Returns the detach function.
|
|
658
|
+
*/
|
|
659
|
+
export function installCapture(sink) {
|
|
660
|
+
const existing = Reflect.get(globalThis, CAPTURE_KEY);
|
|
661
|
+
const state = isCaptureState(existing) ? existing : patchProcess();
|
|
662
|
+
Reflect.set(globalThis, CAPTURE_KEY, state);
|
|
663
|
+
state.sinks.add(sink);
|
|
664
|
+
let attached = true;
|
|
665
|
+
return () => {
|
|
666
|
+
if (!attached)
|
|
667
|
+
return;
|
|
668
|
+
attached = false;
|
|
669
|
+
state.sinks.delete(sink);
|
|
670
|
+
if (state.sinks.size === 0 && Reflect.get(globalThis, CAPTURE_KEY) === state) {
|
|
671
|
+
state.restore();
|
|
672
|
+
Reflect.deleteProperty(globalThis, CAPTURE_KEY);
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
function patchProcess() {
|
|
677
|
+
const state = { sinks: new Set(), depth: 0, restore: () => { } };
|
|
678
|
+
const dispatch = (level, message, structured) => {
|
|
679
|
+
const request = requestContext.getStore();
|
|
680
|
+
for (const sink of state.sinks) {
|
|
681
|
+
if (request !== undefined && request.feedId !== sink.feedId)
|
|
682
|
+
continue;
|
|
683
|
+
try {
|
|
684
|
+
if (structured !== undefined && sink.structured !== undefined) {
|
|
685
|
+
sink.structured(structured, request);
|
|
686
|
+
}
|
|
687
|
+
else
|
|
688
|
+
sink.line(level, message, request);
|
|
689
|
+
}
|
|
690
|
+
catch {
|
|
691
|
+
// A sink must never break the console it observes.
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
};
|
|
695
|
+
const consoleOriginals = new Map();
|
|
696
|
+
for (const method of CONSOLE_METHODS) {
|
|
697
|
+
const original = console[method];
|
|
698
|
+
const patched = function (...args) {
|
|
699
|
+
state.depth += 1;
|
|
700
|
+
try {
|
|
701
|
+
original.apply(this, args);
|
|
702
|
+
}
|
|
703
|
+
finally {
|
|
704
|
+
state.depth -= 1;
|
|
705
|
+
}
|
|
706
|
+
dispatch(CONSOLE_LEVELS[method], stripVTControlCharacters(formatConsoleArgs(args)));
|
|
707
|
+
};
|
|
708
|
+
consoleOriginals.set(method, { original, patched });
|
|
709
|
+
console[method] = patched;
|
|
710
|
+
}
|
|
711
|
+
const streamOriginals = [];
|
|
712
|
+
for (const [stream, level] of [
|
|
713
|
+
[process.stdout, "log"],
|
|
714
|
+
[process.stderr, "error"],
|
|
715
|
+
]) {
|
|
716
|
+
const original = Reflect.get(stream, "write");
|
|
717
|
+
if (typeof original !== "function")
|
|
718
|
+
continue;
|
|
719
|
+
let partial = "";
|
|
720
|
+
const patched = function (chunk, ...rest) {
|
|
721
|
+
if (state.depth === 0) {
|
|
722
|
+
partial +=
|
|
723
|
+
typeof chunk === "string"
|
|
724
|
+
? chunk
|
|
725
|
+
: chunk instanceof Uint8Array
|
|
726
|
+
? new TextDecoder().decode(chunk)
|
|
727
|
+
: "";
|
|
728
|
+
let newline = partial.indexOf("\n");
|
|
729
|
+
while (newline !== -1) {
|
|
730
|
+
const line = stripVTControlCharacters(partial.slice(0, newline)).trimEnd();
|
|
731
|
+
partial = partial.slice(newline + 1);
|
|
732
|
+
if (line.length > 0) {
|
|
733
|
+
const structured = parseCoreLogLine(line);
|
|
734
|
+
dispatch(structured !== undefined && isDevLogLevel(structured.level)
|
|
735
|
+
? structured.level
|
|
736
|
+
: level, line, structured);
|
|
737
|
+
}
|
|
738
|
+
newline = partial.indexOf("\n");
|
|
739
|
+
}
|
|
740
|
+
if (partial.length > MAX_PARTIAL_LINE) {
|
|
741
|
+
dispatch(level, stripVTControlCharacters(partial));
|
|
742
|
+
partial = "";
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
return Reflect.apply(original, this ?? stream, [chunk, ...rest]) !== false;
|
|
746
|
+
};
|
|
747
|
+
streamOriginals.push({ stream, original, patched });
|
|
748
|
+
Reflect.set(stream, "write", patched);
|
|
749
|
+
}
|
|
750
|
+
state.restore = () => {
|
|
751
|
+
for (const [method, { original, patched }] of consoleOriginals) {
|
|
752
|
+
// Only undo our own patch: a later patch layered on top stays, still forwarding to ours.
|
|
753
|
+
if (console[method] === patched)
|
|
754
|
+
console[method] = original;
|
|
755
|
+
}
|
|
756
|
+
for (const { stream, original, patched } of streamOriginals) {
|
|
757
|
+
if (Reflect.get(stream, "write") === patched)
|
|
758
|
+
Reflect.set(stream, "write", original);
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
return state;
|
|
762
|
+
}
|
|
763
|
+
const structuredText = (entry) => {
|
|
764
|
+
const { level: _level, message, time: _time, ...fields } = entry;
|
|
765
|
+
return Object.keys(fields).length === 0 ? message : `${message} ${JSON.stringify(fields)}`;
|
|
766
|
+
};
|
|
767
|
+
/** A server frame's authored position, through the map a dev plugin registered for the file it compiled. */
|
|
768
|
+
const ssrRemap = (file, line, column) => {
|
|
769
|
+
const map = ssrSourceMaps().get(file.replaceAll("\\", "/"));
|
|
770
|
+
return map === undefined ? undefined : originalPosition(map, line, column);
|
|
771
|
+
};
|
|
772
|
+
/**
|
|
773
|
+
* Record the process's console and stream output into `feed`: every line as a log entry, and core's
|
|
774
|
+
* `unhandled request error` line additionally as an `api` error with a full Diagnostic. The one sink
|
|
775
|
+
* both dev servers and `nifra_run` use. Returns the detach function.
|
|
776
|
+
*/
|
|
777
|
+
export function captureInto(feed) {
|
|
778
|
+
return installCapture({
|
|
779
|
+
feedId: feed.id,
|
|
780
|
+
line: (level, message, request) => feed.recordLog(level, message, { requestId: request?.requestId }),
|
|
781
|
+
structured: (entry, request) => {
|
|
782
|
+
const level = isDevLogLevel(entry.level) ? entry.level : "log";
|
|
783
|
+
feed.recordLog(level, structuredText(entry), { requestId: request?.requestId });
|
|
784
|
+
const error = errorFromCoreLog(entry);
|
|
785
|
+
if (error === undefined)
|
|
786
|
+
return;
|
|
787
|
+
const method = typeof entry.method === "string" ? entry.method : request?.method;
|
|
788
|
+
const path = typeof entry.path === "string" ? entry.path : request?.path;
|
|
789
|
+
feed.recordError(error, {
|
|
790
|
+
category: "api",
|
|
791
|
+
requestId: request?.requestId,
|
|
792
|
+
request: method !== undefined && path !== undefined ? { method, url: path } : undefined,
|
|
793
|
+
});
|
|
794
|
+
},
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
//# sourceMappingURL=dev-feed.js.map
|