@nifrajs/web 2.2.0 → 2.4.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 +3 -3
- package/dist/build-vite.d.ts +4 -4
- package/dist/build-vite.d.ts.map +1 -1
- package/dist/build-vite.js +20 -74
- package/dist/build-vite.js.map +1 -1
- package/dist/build.d.ts +64 -42
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +100 -78
- 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/chunk-names.js +1 -1
- package/dist/client.d.ts +3 -9
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +201 -29
- package/dist/client.js.map +1 -1
- package/dist/conformance.d.ts +2 -2
- package/dist/conformance.js +3 -3
- package/dist/deferred.d.ts +11 -11
- package/dist/deferred.d.ts.map +1 -1
- package/dist/deferred.js +16 -16
- package/dist/dev-diagnostics.d.ts +20 -0
- package/dist/dev-diagnostics.d.ts.map +1 -0
- package/dist/dev-diagnostics.js +34 -0
- package/dist/dev-diagnostics.js.map +1 -0
- package/dist/dev-error.d.ts +10 -13
- package/dist/dev-error.d.ts.map +1 -1
- package/dist/dev-error.js +51 -37
- package/dist/dev-error.js.map +1 -1
- package/dist/dev-port.d.ts +3 -3
- package/dist/dev-port.d.ts.map +1 -1
- package/dist/dev-port.js +10 -4
- package/dist/dev-port.js.map +1 -1
- package/dist/dev.d.ts +4 -4
- package/dist/dev.d.ts.map +1 -1
- package/dist/dev.js +28 -12
- package/dist/dev.js.map +1 -1
- package/dist/diagnostic.d.ts +93 -0
- package/dist/diagnostic.d.ts.map +1 -0
- package/dist/diagnostic.js +205 -0
- package/dist/diagnostic.js.map +1 -0
- package/dist/draft.d.ts +12 -12
- package/dist/draft.js +11 -11
- package/dist/fn-state.d.ts +56 -0
- package/dist/fn-state.d.ts.map +1 -0
- package/dist/fn-state.js +77 -0
- package/dist/fn-state.js.map +1 -0
- package/dist/fn.d.ts +111 -0
- package/dist/fn.d.ts.map +1 -0
- package/dist/fn.js +125 -0
- package/dist/fn.js.map +1 -0
- package/dist/fonts-google.d.ts +9 -9
- package/dist/fonts-google.js +10 -10
- package/dist/fonts.d.ts +7 -7
- package/dist/fonts.js +1 -1
- package/dist/forms.d.ts +8 -8
- package/dist/fs.d.ts +1 -1
- package/dist/fs.js +1 -1
- package/dist/index.d.ts +151 -70
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +317 -139
- package/dist/index.js.map +1 -1
- package/dist/internal/head-attributes.d.ts +15 -0
- package/dist/internal/head-attributes.d.ts.map +1 -0
- package/dist/internal/head-attributes.js +99 -0
- package/dist/internal/head-attributes.js.map +1 -0
- package/dist/internal/script-types.d.ts +14 -0
- package/dist/internal/script-types.d.ts.map +1 -0
- package/dist/internal/script-types.js +17 -0
- package/dist/internal/script-types.js.map +1 -0
- package/dist/internal/server-boundary.d.ts +60 -0
- package/dist/internal/server-boundary.d.ts.map +1 -0
- package/dist/internal/server-boundary.js +174 -0
- package/dist/internal/server-boundary.js.map +1 -0
- package/dist/internal/timing-safe-equal.d.ts +1 -1
- package/dist/internal/timing-safe-equal.js +1 -1
- package/dist/internal/vite-import.d.ts +63 -0
- package/dist/internal/vite-import.d.ts.map +1 -0
- package/dist/internal/vite-import.js +94 -0
- package/dist/internal/vite-import.js.map +1 -0
- package/dist/islands.d.ts +5 -5
- package/dist/islands.js +5 -5
- package/dist/isr.d.ts +15 -15
- package/dist/isr.js +7 -7
- package/dist/manifest.d.ts +73 -35
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +10 -16
- package/dist/manifest.js.map +1 -1
- package/dist/module-graph.d.ts +5 -5
- package/dist/module-graph.js +4 -4
- package/dist/navigation.d.ts +120 -5
- package/dist/navigation.d.ts.map +1 -1
- package/dist/navigation.js +37 -5
- package/dist/navigation.js.map +1 -1
- package/dist/plugins/css-modules.d.ts +3 -3
- package/dist/plugins/css-modules.js +0 -0
- package/dist/plugins/kit.d.ts +8 -8
- package/dist/plugins/kit.js +9 -9
- package/dist/plugins/scss.js +3 -3
- package/dist/plugins/vite-leak-guard.d.ts +1 -1
- package/dist/plugins/vite-leak-guard.js +1 -1
- package/dist/plugins/vite-server-fn.d.ts +23 -0
- package/dist/plugins/vite-server-fn.d.ts.map +1 -0
- package/dist/plugins/vite-server-fn.js +51 -0
- package/dist/plugins/vite-server-fn.js.map +1 -0
- package/dist/plugins/vite-server-only.d.ts +49 -0
- package/dist/plugins/vite-server-only.d.ts.map +1 -0
- package/dist/plugins/vite-server-only.js +57 -0
- package/dist/plugins/vite-server-only.js.map +1 -0
- package/dist/prerender.d.ts +5 -5
- package/dist/prerender.js +7 -7
- package/dist/query.d.ts +10 -10
- package/dist/query.js +15 -15
- package/dist/route-manifest.d.ts +1 -1
- package/dist/route-manifest.js +2 -2
- package/dist/router.d.ts +30 -21
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +62 -32
- package/dist/router.js.map +1 -1
- package/dist/search.d.ts +94 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +232 -0
- package/dist/search.js.map +1 -0
- package/dist/server-only.d.ts +6 -6
- package/dist/server-only.js +7 -7
- package/dist/service-worker.d.ts +56 -0
- package/dist/service-worker.d.ts.map +1 -0
- package/dist/service-worker.js +127 -0
- package/dist/service-worker.js.map +1 -0
- package/dist/vite.d.ts +16 -14
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +51 -24
- package/dist/vite.js.map +1 -1
- package/package.json +38 -4
- package/server-manifest.d.ts +31 -0
- package/src/build-vite.ts +24 -77
- package/src/build.ts +137 -105
- package/src/bun-dev-entry.ts +1 -1
- package/src/chunk-names.ts +1 -1
- package/src/client.ts +215 -32
- package/src/conformance.ts +3 -3
- package/src/deferred.ts +17 -17
- package/src/dev-diagnostics.ts +47 -0
- package/src/dev-error.ts +64 -33
- package/src/dev-port.ts +17 -5
- package/src/dev.ts +36 -20
- package/src/diagnostic.ts +284 -0
- package/src/draft.ts +15 -15
- package/src/fn-state.ts +107 -0
- package/src/fn.ts +199 -0
- package/src/fonts-google.ts +13 -13
- package/src/fonts.ts +7 -7
- package/src/forms.ts +8 -8
- package/src/fs.ts +2 -2
- package/src/index.ts +460 -181
- package/src/internal/head-attributes.ts +109 -0
- package/src/internal/script-types.ts +18 -0
- package/src/internal/server-boundary.ts +187 -0
- package/src/internal/timing-safe-equal.ts +1 -1
- package/src/internal/vite-import.ts +92 -0
- package/src/islands.ts +7 -7
- package/src/isr.ts +17 -17
- package/src/manifest.ts +83 -42
- package/src/module-graph.ts +6 -6
- package/src/navigation.ts +151 -5
- package/src/plugins/css-modules.ts +0 -0
- package/src/plugins/kit.ts +11 -11
- package/src/plugins/scss.ts +3 -3
- package/src/plugins/vite-leak-guard.ts +2 -2
- package/src/plugins/vite-server-fn.ts +67 -0
- package/src/plugins/vite-server-only.ts +75 -0
- package/src/prerender.ts +10 -10
- package/src/query.ts +22 -22
- package/src/route-manifest.ts +3 -3
- package/src/router.ts +98 -45
- package/src/search.ts +297 -0
- package/src/server-only.ts +7 -7
- package/src/service-worker.ts +165 -0
- package/src/vite.ts +61 -32
package/src/dev-error.ts
CHANGED
|
@@ -1,62 +1,93 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Dev-only error overlay. When a loader/action/render throws during `@nifrajs/web/vite` dev, this
|
|
3
|
-
* renders a readable full-page overlay
|
|
4
|
-
*
|
|
5
|
-
* Dev-only by construction: it's called solely from the
|
|
6
|
-
* production (production maps errors to the `_error` route boundary).
|
|
3
|
+
* renders a readable full-page overlay from a structured [Diagnostic] - the code, the message, a source
|
|
4
|
+
* codeframe around the offending line, the recognised cause/fix when nifra has one, and the stack -
|
|
5
|
+
* instead of a bare `err.stack` text dump. Dev-only by construction: it's called solely from the dev
|
|
6
|
+
* server's catch, never in production (production maps errors to the `_error` route boundary).
|
|
7
|
+
*
|
|
8
|
+
* The overlay and the agent surfaces (`/__nifra/last-error`, `nifra_explain`) render from the SAME
|
|
9
|
+
* `Diagnostic`, so a person and an agent see the identical failure, one as HTML and one as JSON.
|
|
7
10
|
*/
|
|
11
|
+
import type { Diagnostic } from "./diagnostic.ts"
|
|
8
12
|
|
|
9
13
|
const esc = (s: string): string =>
|
|
10
14
|
s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """)
|
|
11
15
|
|
|
12
|
-
/**
|
|
13
|
-
function
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
16
|
+
/** The source codeframe block: numbered lines with the offending one highlighted. */
|
|
17
|
+
function codeframeHtml(diagnostic: Diagnostic): string {
|
|
18
|
+
const cf = diagnostic.codeframe
|
|
19
|
+
if (cf === undefined) return ""
|
|
20
|
+
const loc = `${esc(cf.file)}:${cf.line}${cf.column !== undefined ? `:${cf.column}` : ""}`
|
|
21
|
+
const rows = cf.lines
|
|
22
|
+
.map(
|
|
23
|
+
(l) =>
|
|
24
|
+
`<div class="cf-row${l.caret ? " caret" : ""}"><span class="cf-ln">${l.number}</span><span class="cf-src">${esc(l.text) || " "}</span></div>`,
|
|
25
|
+
)
|
|
26
|
+
.join("")
|
|
27
|
+
return `<h1>Source</h1><div class="cf-loc">${loc}</div><div class="codeframe">${rows}</div>`
|
|
23
28
|
}
|
|
24
29
|
|
|
25
|
-
/**
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
30
|
+
/** The recognised-failure callout: cause + fix + docs anchor, shown only when nifra classified the error. */
|
|
31
|
+
function fixHtml(diagnostic: Diagnostic): string {
|
|
32
|
+
if (diagnostic.fix === undefined) return ""
|
|
33
|
+
const anchor =
|
|
34
|
+
diagnostic.docsAnchor !== undefined
|
|
35
|
+
? `<div class="fix-docs">docs: <code>${esc(diagnostic.docsAnchor)}</code></div>`
|
|
36
|
+
: ""
|
|
37
|
+
const cause =
|
|
38
|
+
diagnostic.cause !== undefined ? `<p class="fix-cause">${esc(diagnostic.cause)}</p>` : ""
|
|
39
|
+
return `<div class="fix"><div class="fix-tag">likely fix</div>${cause}<p class="fix-do">${esc(diagnostic.fix)}</p>${anchor}</div>`
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Render the overlay HTML from a prebuilt Diagnostic (the same object the agent surfaces serve). */
|
|
43
|
+
export function renderDiagnosticOverlay(diagnostic: Diagnostic): string {
|
|
44
|
+
const name = esc(diagnostic.name || "Error")
|
|
45
|
+
const code = esc(diagnostic.code)
|
|
46
|
+
const reqLine =
|
|
47
|
+
diagnostic.request !== undefined
|
|
48
|
+
? `<span class="req">${esc(diagnostic.request.method)} ${esc(diagnostic.request.url)}</span>`
|
|
49
|
+
: ""
|
|
34
50
|
const framesHtml =
|
|
35
|
-
frames.length > 0
|
|
36
|
-
? `<ol class="frames">${frames.map((f) => `<li>${esc(f)}</li>`).join("")}</ol>`
|
|
51
|
+
diagnostic.frames.length > 0
|
|
52
|
+
? `<ol class="frames">${diagnostic.frames.map((f) => `<li>${esc(f.raw)}</li>`).join("")}</ol>`
|
|
37
53
|
: `<p class="no-frames">No stack frames.</p>`
|
|
38
|
-
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>${name}
|
|
54
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>${name} - nifra dev</title>
|
|
39
55
|
<style>
|
|
40
56
|
:root { color-scheme: dark }
|
|
41
57
|
body { margin: 0; background: #0b0d10; color: #e6e6e6; font: 14px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; }
|
|
42
58
|
.bar { background: #2a0e0e; border-bottom: 1px solid #5a1d1d; padding: 10px 20px; font-weight: 700; color: #ff8a8a; display: flex; gap: 12px; align-items: baseline; }
|
|
43
59
|
.bar .tag { font-size: 11px; font-weight: 600; color: #ffb4b4; background: #401616; padding: 2px 8px; border-radius: 99px; letter-spacing: .04em; text-transform: uppercase; }
|
|
60
|
+
.bar .code { font-size: 11px; font-weight: 600; color: #ffd9a0; background: #3a2a12; padding: 2px 8px; border-radius: 99px; letter-spacing: .04em; }
|
|
44
61
|
.bar .req { margin-left: auto; color: #c98; font-weight: 500; font-size: 12px; }
|
|
45
62
|
main { padding: 24px 28px; max-width: 1000px; }
|
|
46
|
-
h1 { font-size: 13px; color: #9aa; font-weight: 600; margin:
|
|
47
|
-
|
|
63
|
+
h1 { font-size: 13px; color: #9aa; font-weight: 600; margin: 24px 0 6px; text-transform: uppercase; letter-spacing: .05em; }
|
|
64
|
+
h1:first-of-type { margin-top: 0; }
|
|
65
|
+
.message { font-size: 18px; color: #ff9b9b; white-space: pre-wrap; margin: 0 0 4px; font-weight: 600; }
|
|
66
|
+
.fix { background: #10241a; border: 1px solid #1f4d38; border-radius: 8px; padding: 12px 16px; margin: 20px 0; }
|
|
67
|
+
.fix-tag { font-size: 11px; font-weight: 700; color: #7fe0af; text-transform: uppercase; letter-spacing: .05em; margin-bottom: 6px; }
|
|
68
|
+
.fix-cause { margin: 0 0 8px; color: #b9c9c1; }
|
|
69
|
+
.fix-do { margin: 0; color: #d6f2e4; }
|
|
70
|
+
.fix-docs { margin-top: 8px; font-size: 12px; color: #6a8; }
|
|
71
|
+
.cf-loc { color: #c98; font-size: 12px; margin-bottom: 8px; }
|
|
72
|
+
.codeframe { background: #0f1319; border: 1px solid #222; border-radius: 8px; overflow: auto; }
|
|
73
|
+
.cf-row { display: flex; white-space: pre; }
|
|
74
|
+
.cf-row.caret { background: #2a1414; }
|
|
75
|
+
.cf-ln { color: #556; text-align: right; width: 48px; padding: 1px 12px 1px 0; user-select: none; flex: none; border-right: 1px solid #222; }
|
|
76
|
+
.cf-row.caret .cf-ln { color: #ff8a8a; }
|
|
77
|
+
.cf-src { padding: 1px 0 1px 12px; color: #cdd6e0; }
|
|
48
78
|
.frames { list-style: none; margin: 0; padding: 0; border-left: 2px solid #333; }
|
|
49
79
|
.frames li { padding: 3px 0 3px 16px; color: #9fb0c0; white-space: pre-wrap; word-break: break-all; }
|
|
50
80
|
.frames li:first-child { color: #d6e2ee; }
|
|
51
81
|
.no-frames { color: #778; }
|
|
52
82
|
footer { margin-top: 28px; color: #667; font-size: 12px; }
|
|
53
83
|
</style></head><body>
|
|
54
|
-
<div class="bar"><span class="tag">nifra dev</span><span>${
|
|
84
|
+
<div class="bar"><span class="tag">nifra dev</span><span class="code">${code}</span><span>${name}</span>${reqLine}</div>
|
|
55
85
|
<main>
|
|
56
|
-
<
|
|
57
|
-
|
|
86
|
+
<p class="message">${esc(diagnostic.message)}</p>
|
|
87
|
+
${fixHtml(diagnostic)}
|
|
88
|
+
${codeframeHtml(diagnostic)}
|
|
58
89
|
<h1>Stack</h1>
|
|
59
90
|
${framesHtml}
|
|
60
|
-
<footer>This overlay is shown only by the dev server. In production this error maps to your <code>_error</code> route boundary.</footer>
|
|
91
|
+
<footer>This overlay is shown only by the dev server. Its structured form is at <code>/__nifra/last-error</code> and via <code>nifra_explain</code>. In production this error maps to your <code>_error</code> route boundary.</footer>
|
|
61
92
|
</main></body></html>`
|
|
62
93
|
}
|
package/src/dev-port.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `@nifrajs/web/dev-port`
|
|
2
|
+
* `@nifrajs/web/dev-port` - what a dev server says when it cannot bind.
|
|
3
3
|
*
|
|
4
4
|
* Both pipelines need this and neither should own it: the Vite server binds through Node's `http`
|
|
5
5
|
* (asynchronous `error` event), the Bun server binds through `Bun.serve` (synchronous throw). Two very
|
|
@@ -36,7 +36,7 @@ const asError = (err: unknown): Error => (err instanceof Error ? err : new Error
|
|
|
36
36
|
export const explainBindFailure = (err: unknown, port: number): Error =>
|
|
37
37
|
isAddressInUse(err) ? new Error(portInUseMessage(port)) : asError(err)
|
|
38
38
|
|
|
39
|
-
/** The bits of a Node server {@link listenOrExplain} touches
|
|
39
|
+
/** The bits of a Node server {@link listenOrExplain} touches - structural, so a test can fake it. */
|
|
40
40
|
export interface ListenTarget {
|
|
41
41
|
listen(port: number, cb: () => void): void
|
|
42
42
|
once(event: "error", cb: (err: unknown) => void): void
|
|
@@ -49,15 +49,27 @@ export interface ListenTarget {
|
|
|
49
49
|
* Without an `error` listener attached, Node throws from deep inside `node:events`, the new process dies
|
|
50
50
|
* in the background, and nothing connects that death to the stale page in front of you.
|
|
51
51
|
*/
|
|
52
|
-
export function listenOrExplain(
|
|
52
|
+
export function listenOrExplain(
|
|
53
|
+
server: ListenTarget,
|
|
54
|
+
port: number,
|
|
55
|
+
hostname?: string,
|
|
56
|
+
): Promise<void> {
|
|
53
57
|
return new Promise<void>((resolve, reject) => {
|
|
54
58
|
const onError = (err: unknown): void => reject(explainBindFailure(err, port))
|
|
55
59
|
server.once("error", onError)
|
|
56
|
-
|
|
60
|
+
const onListening = (): void => {
|
|
57
61
|
// Drop the guard once we're listening: leaving it attached would funnel a LATER server error into
|
|
58
62
|
// an already-settled promise, silently swallowing it instead of surfacing it.
|
|
59
63
|
server.removeListener("error", onError)
|
|
60
64
|
resolve()
|
|
61
|
-
}
|
|
65
|
+
}
|
|
66
|
+
if (hostname === undefined) server.listen(port, onListening)
|
|
67
|
+
else {
|
|
68
|
+
;(
|
|
69
|
+
server as ListenTarget & {
|
|
70
|
+
listen(port: number, hostname: string, cb: () => void): void
|
|
71
|
+
}
|
|
72
|
+
).listen(port, hostname, onListening)
|
|
73
|
+
}
|
|
62
74
|
})
|
|
63
75
|
}
|
package/src/dev.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `@nifrajs/web/dev`
|
|
2
|
+
* `@nifrajs/web/dev` - the **Bun pipeline's** dev server: one toolchain, no Vite in the process.
|
|
3
3
|
*
|
|
4
4
|
* nifra ships two dev pipelines and the rule between them is that a pipeline owns a whole phase. The
|
|
5
5
|
* Vite server (`@nifrajs/web/vite`) is the default - mature framework plugins, and it resolves SSR as
|
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
* and hot-reloads the client while Bun's runtime resolves SSR. Only one toolchain is present, so the two
|
|
8
8
|
* cannot disagree.
|
|
9
9
|
*
|
|
10
|
-
* What you get: React Fast Refresh WITH state preserved
|
|
10
|
+
* What you get: React Fast Refresh WITH state preserved - Bun's dev server applies it natively, no plugin
|
|
11
11
|
* (verified: editing a component-only module swaps its markup while a `useState` counter keeps its value,
|
|
12
12
|
* no reload). The usual boundary rule still applies, and it is the same rule Vite has: a module whose
|
|
13
13
|
* exports are all components is a refresh boundary, so a ROUTE file that also exports `loader`/`meta` is
|
|
14
14
|
* not, and saving it does a clean full reload. Plus no Vite dependency and ONE bundler across dev and
|
|
15
|
-
* production, which is the real prize
|
|
15
|
+
* production, which is the real prize - the dev/prod seam disappears.
|
|
16
16
|
*
|
|
17
17
|
* What you give up: `*.module.css`. Bun's DEV-server bundler has no CSS-Modules transform (its production
|
|
18
18
|
* `Bun.build` does), so the import compiles to a dangling reference. Plain CSS and Tailwind are fine. The
|
|
@@ -54,20 +54,22 @@ import {
|
|
|
54
54
|
import { dirname, relative, resolve } from "node:path"
|
|
55
55
|
import { type BuildClientOptions, buildClient } from "./build.ts"
|
|
56
56
|
import { type DevEntryMatch, resolveDevEntry } from "./bun-dev-entry.ts"
|
|
57
|
-
import {
|
|
57
|
+
import { createDevDiagnostics } from "./dev-diagnostics.ts"
|
|
58
58
|
import { explainBindFailure } from "./dev-port.ts"
|
|
59
59
|
import { discoverRoutes } from "./fs.ts"
|
|
60
60
|
import { DEFAULT_DEV_PORT, generateClientEntry } from "./index.ts"
|
|
61
61
|
import { servePublicDir } from "./public-dir.ts"
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
export { LAST_ERROR_PATH } from "./diagnostic.ts"
|
|
64
|
+
|
|
65
|
+
/** Minimal app surface the dev server needs - `createWebApp(...)` satisfies it. */
|
|
64
66
|
interface FetchApp {
|
|
65
67
|
fetch(request: Request): Response | Promise<Response>
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
export interface DevServerOptions extends Omit<BuildClientOptions, "minify"> {
|
|
69
71
|
/**
|
|
70
|
-
* Build the nifra app for the current client entry. `importQuery` changes on every reload
|
|
72
|
+
* Build the nifra app for the current client entry. `importQuery` changes on every reload - pass it to
|
|
71
73
|
* `discoverRoutes(routesDir, { importQuery })` so SSR re-imports edited route modules instead of Bun's
|
|
72
74
|
* cached copies.
|
|
73
75
|
*/
|
|
@@ -93,7 +95,7 @@ export interface DevServerOptions extends Omit<BuildClientOptions, "minify"> {
|
|
|
93
95
|
export interface DevServer {
|
|
94
96
|
readonly port: number
|
|
95
97
|
/**
|
|
96
|
-
* The URL pages point their client entry at
|
|
98
|
+
* The URL pages point their client entry at - always {@link CLIENT_ENTRY_PATH}, never Bun's hashed
|
|
97
99
|
* chunk. Deliberately not the underlying chunk URL: that one moves on every rebuild, and anything
|
|
98
100
|
* holding onto it is holding onto a URL that will stop working.
|
|
99
101
|
*/
|
|
@@ -128,6 +130,8 @@ type HtmlBundle = unknown
|
|
|
128
130
|
/** The `Bun.serve` surface this uses, typed structurally so the file builds without Bun's ambient types. */
|
|
129
131
|
interface BunServeOptions {
|
|
130
132
|
readonly port: number
|
|
133
|
+
/** Diagnostics contain source paths; keep the dev server private by default. */
|
|
134
|
+
readonly hostname: "127.0.0.1"
|
|
131
135
|
readonly development: { readonly hmr: boolean }
|
|
132
136
|
readonly routes: Record<string, HtmlBundle>
|
|
133
137
|
fetch(request: Request): Promise<Response>
|
|
@@ -154,7 +158,7 @@ function writeIfChanged(path: string, content: string): void {
|
|
|
154
158
|
try {
|
|
155
159
|
if (readFileSync(path, "utf8") === content) return
|
|
156
160
|
} catch {
|
|
157
|
-
// missing or unreadable
|
|
161
|
+
// missing or unreadable - fall through and write
|
|
158
162
|
}
|
|
159
163
|
writeFileSync(path, content)
|
|
160
164
|
}
|
|
@@ -196,13 +200,13 @@ export function writeDevFiles(options: WriteDevFilesOptions): void {
|
|
|
196
200
|
// A bare relative path reads as a PACKAGE specifier to a bundler; `./` is what makes it a file.
|
|
197
201
|
return rel.startsWith(".") ? rel : `./${rel}`
|
|
198
202
|
}
|
|
199
|
-
// No `import.meta.hot.accept` here, deliberately. When an edit lands OUTSIDE a Fast Refresh boundary
|
|
200
|
-
// a route file, which also exports `loader`/`meta`
|
|
203
|
+
// No `import.meta.hot.accept` here, deliberately. When an edit lands OUTSIDE a Fast Refresh boundary -
|
|
204
|
+
// a route file, which also exports `loader`/`meta` - Bun walks up to this generated entry, finds no
|
|
201
205
|
// `accept`, logs "hot update was not accepted" and does a full reload. That reload is correct: the route
|
|
202
206
|
// module's non-component exports changed, so patching it into the live tree would be wrong. Accepting
|
|
203
207
|
// here is worse than the warning: Bun re-evaluates the module BEFORE the accept callback runs, so the
|
|
204
208
|
// entry re-executes against a container React already mounted ("createRoot() on a container that has
|
|
205
|
-
// already been passed to createRoot()") and only then does the callback get to reload
|
|
209
|
+
// already been passed to createRoot()") and only then does the callback get to reload - trading an
|
|
206
210
|
// accurate warning for a real error. Editing a component-only module never reaches this path: Bun
|
|
207
211
|
// applies React Fast Refresh there and state is preserved.
|
|
208
212
|
writeIfChanged(entryPath, generateClientEntry(manifest, { clientModule, resolve: toSpecifier }))
|
|
@@ -243,6 +247,10 @@ export async function createDevServer(options: DevServerOptions): Promise<DevSer
|
|
|
243
247
|
// The app root. `routesDir` is `<app>/routes` by convention, so its parent is the project - the one
|
|
244
248
|
// place a generated entry can sit and still resolve the app's imports.
|
|
245
249
|
const root = resolve(routesDir, "..")
|
|
250
|
+
// The most recent SSR failure as a structured Diagnostic, scoped to THIS server. Served at
|
|
251
|
+
// LAST_ERROR_PATH so an agent driving the dev server reads the exact failure (code, codeframe, fix) as
|
|
252
|
+
// JSON instead of scraping the overlay. Shared with the Vite adapter so the endpoint can't drift.
|
|
253
|
+
const devDiagnostics = createDevDiagnostics(root)
|
|
246
254
|
const devDir = resolve(root, DEV_DIR)
|
|
247
255
|
const entryPath = resolve(devDir, "entry.tsx")
|
|
248
256
|
const htmlPath = resolve(devDir, "entry.html")
|
|
@@ -261,9 +269,9 @@ export async function createDevServer(options: DevServerOptions): Promise<DevSer
|
|
|
261
269
|
const htmlModule = (await import(htmlPath)) as { default: HtmlBundle }
|
|
262
270
|
|
|
263
271
|
let server: BunServerHandle
|
|
264
|
-
// The last resolved entry, with the time it was resolved. Deliberately short-lived
|
|
272
|
+
// The last resolved entry, with the time it was resolved. Deliberately short-lived - see `currentEntry`.
|
|
265
273
|
let cache: { readonly entry: DevEntryMatch; readonly at: number } | undefined
|
|
266
|
-
// The app, tagged with the entry hash it was built against
|
|
274
|
+
// The app, tagged with the entry hash it was built against - see `appFor`.
|
|
267
275
|
let built: { readonly entry: string; readonly app: FetchApp } | undefined
|
|
268
276
|
let building: { readonly entry: string; readonly promise: Promise<FetchApp> } | undefined
|
|
269
277
|
let version = 0
|
|
@@ -337,6 +345,7 @@ export async function createDevServer(options: DevServerOptions): Promise<DevSer
|
|
|
337
345
|
try {
|
|
338
346
|
server = serve({
|
|
339
347
|
port,
|
|
348
|
+
hostname: "127.0.0.1",
|
|
340
349
|
development: { hmr: true },
|
|
341
350
|
// The probe route is the ONLY path Bun owns; everything else falls through to nifra's SSR.
|
|
342
351
|
routes: { [PROBE_PATH]: htmlModule.default },
|
|
@@ -352,6 +361,11 @@ export async function createDevServer(options: DevServerOptions): Promise<DevSer
|
|
|
352
361
|
const entry = await currentEntry()
|
|
353
362
|
return new Response(null, { status: 307, headers: { location: entry.src } })
|
|
354
363
|
}
|
|
364
|
+
if (devDiagnostics.isLastErrorPath(url.pathname)) {
|
|
365
|
+
// The structured form of the overlay, for an agent driving the dev server (shared surface).
|
|
366
|
+
const { body, headers } = devDiagnostics.lastError()
|
|
367
|
+
return new Response(body, { headers })
|
|
368
|
+
}
|
|
355
369
|
// Static probe before routing; a miss returns undefined and falls through, so no route is
|
|
356
370
|
// shadowed and a page render never pays a filesystem stat (extension-less paths skip it).
|
|
357
371
|
const staticFile = await servePublic(req)
|
|
@@ -370,13 +384,15 @@ export async function createDevServer(options: DevServerOptions): Promise<DevSer
|
|
|
370
384
|
headers,
|
|
371
385
|
})
|
|
372
386
|
} catch (err) {
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
387
|
+
// One Diagnostic drives both surfaces: the overlay returned here and the JSON at LAST_ERROR_PATH.
|
|
388
|
+
const html = devDiagnostics.capture(err, {
|
|
389
|
+
method: req.method,
|
|
390
|
+
url: `${url.pathname}${url.search}`,
|
|
391
|
+
})
|
|
392
|
+
return new Response(html, {
|
|
393
|
+
status: 500,
|
|
394
|
+
headers: { "content-type": "text/html; charset=utf-8" },
|
|
395
|
+
})
|
|
380
396
|
}
|
|
381
397
|
},
|
|
382
398
|
})
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The structured diagnostic that every nifra failure resolves to - the single source both the human
|
|
3
|
+
* dev overlay ([dev-error.ts]) and the agent-facing surfaces (`/__nifra/last-error`, `nifra_explain`)
|
|
4
|
+
* render from. A stack trace is a wall of text; a `Diagnostic` is queryable: a stable `code`, the top
|
|
5
|
+
* USER frame (file/line/column), a source codeframe around it, and - when the failure is one nifra
|
|
6
|
+
* recognises - a plain-language `cause` + `fix` + docs anchor.
|
|
7
|
+
*
|
|
8
|
+
* This is the "AI-native" half of error reporting: the same object a person reads in the overlay is the
|
|
9
|
+
* JSON an agent reads to apply the fix, so neither has to scrape the other's format.
|
|
10
|
+
*
|
|
11
|
+
* Pure + DOM-free by construction (it only parses strings and reads source text through an injected
|
|
12
|
+
* reader), so it lives in the root typecheck program and is unit-testable without a dev server or disk.
|
|
13
|
+
*/
|
|
14
|
+
import { readFileSync, realpathSync } from "node:fs"
|
|
15
|
+
import { isAbsolute, relative, resolve } from "node:path"
|
|
16
|
+
|
|
17
|
+
/** Shared endpoint name used by both dev pipelines and the agent-facing MCP tools. */
|
|
18
|
+
export const LAST_ERROR_PATH = "/__nifra/last-error"
|
|
19
|
+
|
|
20
|
+
/** One parsed stack frame. `file`/`line`/`column` are present only when the frame could be located. */
|
|
21
|
+
export interface DiagnosticFrame {
|
|
22
|
+
readonly raw: string
|
|
23
|
+
readonly file?: string
|
|
24
|
+
readonly line?: number
|
|
25
|
+
readonly column?: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** A source window around the offending line. `caret` marks the exact line the top frame points at. */
|
|
29
|
+
export interface Codeframe {
|
|
30
|
+
readonly file: string
|
|
31
|
+
readonly line: number
|
|
32
|
+
readonly column?: number | undefined
|
|
33
|
+
readonly lines: ReadonlyArray<{
|
|
34
|
+
readonly number: number
|
|
35
|
+
readonly text: string
|
|
36
|
+
readonly caret: boolean
|
|
37
|
+
}>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The structured failure. Serialisable as-is to JSON for the agent surfaces. */
|
|
41
|
+
export interface Diagnostic {
|
|
42
|
+
/** Stable, greppable identifier, e.g. `NIFRA_SERVER_ONLY_IN_CLIENT`. `NIFRA_UNHANDLED` when unrecognised. */
|
|
43
|
+
readonly code: string
|
|
44
|
+
readonly name: string
|
|
45
|
+
readonly message: string
|
|
46
|
+
readonly request?: { readonly method: string; readonly url: string } | undefined
|
|
47
|
+
readonly frames: readonly DiagnosticFrame[]
|
|
48
|
+
readonly codeframe?: Codeframe | undefined
|
|
49
|
+
/** Plain-language "why this happened", when the failure is recognised. */
|
|
50
|
+
readonly cause?: string | undefined
|
|
51
|
+
/** Plain-language "do this", when the failure is recognised. */
|
|
52
|
+
readonly fix?: string | undefined
|
|
53
|
+
/** Docs section anchor for the code, e.g. `errors#server-only-in-client`. */
|
|
54
|
+
readonly docsAnchor?: string | undefined
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Reads a source file's text, or returns undefined if it cannot (missing, binary, permission). */
|
|
58
|
+
export type SourceReader = (file: string) => string | undefined
|
|
59
|
+
|
|
60
|
+
const defaultReader: SourceReader = (file) => {
|
|
61
|
+
try {
|
|
62
|
+
return readFileSync(file, "utf8")
|
|
63
|
+
} catch {
|
|
64
|
+
return undefined
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Turns a raw frame path into an absolute filesystem path, or undefined if it isn't one. Handles the
|
|
69
|
+
// `file://` URL form Vite/Node emit and strips the `?v=hash` / `#` suffixes a bundler appends.
|
|
70
|
+
function cleanFramePath(path: string): string | undefined {
|
|
71
|
+
let p = path.trim()
|
|
72
|
+
if (p.startsWith("file://")) {
|
|
73
|
+
try {
|
|
74
|
+
p = decodeURIComponent(new URL(p).pathname)
|
|
75
|
+
} catch {
|
|
76
|
+
return undefined
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Strip the `?v=hash` / `#` suffix without a `.*` regex (ReDoS-safe): cut at the first `?` or `#`.
|
|
80
|
+
const cut = p.search(/[?#]/)
|
|
81
|
+
if (cut !== -1) p = p.slice(0, cut)
|
|
82
|
+
const isAbsolute = p.startsWith("/") || /^[A-Za-z]:[\\/]/.test(p)
|
|
83
|
+
return isAbsolute ? p : undefined
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Parse a V8/Node stack into structured frames. Handles the `at fn (path:line:col)`, bare
|
|
88
|
+
* `at path:line:col`, and `at async fn (...)` shapes; a frame that doesn't match keeps its raw text
|
|
89
|
+
* with no location (so nothing is silently dropped).
|
|
90
|
+
*/
|
|
91
|
+
export function parseFrames(stack: string): DiagnosticFrame[] {
|
|
92
|
+
const frames: DiagnosticFrame[] = []
|
|
93
|
+
for (const line of stack.split("\n")) {
|
|
94
|
+
const raw = line.trim()
|
|
95
|
+
if (!/^at\s/.test(raw)) continue
|
|
96
|
+
// Prefer the parenthesised location; fall back to a trailing `path:line:col`.
|
|
97
|
+
const paren = raw.match(/\(([^()]+):(\d+):(\d+)\)\s*$/)
|
|
98
|
+
const bare = raw.match(/^at\s+(?:async\s+)?(\S.*):(\d+):(\d+)\s*$/)
|
|
99
|
+
const m = paren ?? bare
|
|
100
|
+
if (!m) {
|
|
101
|
+
frames.push({ raw })
|
|
102
|
+
continue
|
|
103
|
+
}
|
|
104
|
+
const file = cleanFramePath(m[1] as string)
|
|
105
|
+
frames.push(
|
|
106
|
+
file === undefined ? { raw } : { raw, file, line: Number(m[2]), column: Number(m[3]) },
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
return frames
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Frames inside dependencies or the runtime are noise for a first look; the user's own code is the signal. */
|
|
113
|
+
function isUserFrame(file: string, root: string | undefined): boolean {
|
|
114
|
+
if (file.includes("/node_modules/") || file.includes("\\node_modules\\")) return false
|
|
115
|
+
if (file.startsWith("node:")) return false
|
|
116
|
+
if (root !== undefined) {
|
|
117
|
+
// A string prefix is not a path boundary (`/app-evil` starts with `/app`) and leaves `..` and
|
|
118
|
+
// symlink escapes unresolved. Resolve lexically first, then realpath existing paths so a codeframe
|
|
119
|
+
// can never read a source file outside the project root merely because a stack frame was crafted.
|
|
120
|
+
const rootPath = canonicalPath(root)
|
|
121
|
+
const framePath = canonicalPath(file)
|
|
122
|
+
const rel = relative(rootPath, framePath)
|
|
123
|
+
if (rel === ".." || rel.startsWith(`..${sepForPath(rel)}`) || isAbsolute(rel)) return false
|
|
124
|
+
}
|
|
125
|
+
return true
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function sepForPath(path: string): string {
|
|
129
|
+
return path.includes("\\") ? "\\" : "/"
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function canonicalPath(path: string): string {
|
|
133
|
+
const lexical = resolve(path)
|
|
134
|
+
try {
|
|
135
|
+
return realpathSync.native(lexical)
|
|
136
|
+
} catch {
|
|
137
|
+
return lexical
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** The first frame that points at the user's own source - what the codeframe should show. */
|
|
142
|
+
export function topUserFrame(
|
|
143
|
+
frames: readonly DiagnosticFrame[],
|
|
144
|
+
root: string | undefined,
|
|
145
|
+
): DiagnosticFrame | undefined {
|
|
146
|
+
return frames.find((f) => f.file !== undefined && isUserFrame(f.file, root))
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Build a source codeframe: `radius` lines either side of `line`, each tagged with its 1-based number
|
|
151
|
+
* and whether it is the offending line. Returns undefined if the source can't be read or the line is
|
|
152
|
+
* out of range - a diagnostic without a codeframe is still useful, so this never throws.
|
|
153
|
+
*/
|
|
154
|
+
export function buildCodeframe(
|
|
155
|
+
file: string,
|
|
156
|
+
line: number,
|
|
157
|
+
column: number | undefined,
|
|
158
|
+
read: SourceReader = defaultReader,
|
|
159
|
+
radius = 3,
|
|
160
|
+
): Codeframe | undefined {
|
|
161
|
+
const source = read(file)
|
|
162
|
+
if (source === undefined) return undefined
|
|
163
|
+
const all = source.split("\n")
|
|
164
|
+
if (line < 1 || line > all.length) return undefined
|
|
165
|
+
const start = Math.max(1, line - radius)
|
|
166
|
+
const end = Math.min(all.length, line + radius)
|
|
167
|
+
const lines: Array<{ number: number; text: string; caret: boolean }> = []
|
|
168
|
+
for (let n = start; n <= end; n++) {
|
|
169
|
+
lines.push({ number: n, text: all[n - 1] ?? "", caret: n === line })
|
|
170
|
+
}
|
|
171
|
+
return { file, line, column, lines }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** A recognised failure shape: a stable code plus the plain-language cause/fix/anchor to attach. */
|
|
175
|
+
interface CatalogEntry {
|
|
176
|
+
readonly code: string
|
|
177
|
+
readonly match: (name: string, message: string) => boolean
|
|
178
|
+
readonly cause: string
|
|
179
|
+
readonly fix: string
|
|
180
|
+
readonly docsAnchor: string
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* The recognised-failure catalog. Seeded with the highest-signal nifra failures; extend it as new
|
|
185
|
+
* classes of error earn a stable code. Order matters only in that the first match wins.
|
|
186
|
+
*/
|
|
187
|
+
export const DIAGNOSTIC_CATALOG: readonly CatalogEntry[] = [
|
|
188
|
+
{
|
|
189
|
+
code: "NIFRA_SERVER_ONLY_IN_CLIENT",
|
|
190
|
+
match: (_n, m) => m.includes("server-only module(s) in the client bundle"),
|
|
191
|
+
cause:
|
|
192
|
+
"A module marked server-only was reachable from a client entry, so it would ship to the browser.",
|
|
193
|
+
fix: "Follow the import chain in the message and move the server-only use behind a loader/action or a `*.server.ts` boundary, so it never enters a client component.",
|
|
194
|
+
docsAnchor: "errors#server-only-in-client",
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
code: "NIFRA_NODE_BUILTIN_IN_CLIENT",
|
|
198
|
+
match: (_n, m) => m.includes("Node built-in(s) in the client bundle"),
|
|
199
|
+
cause: "A `node:` built-in was reached from a client entry; it has no browser implementation.",
|
|
200
|
+
fix: "Move the code using the built-in to the server (loader/action or `*.server.ts`); the message lists the import chain that pulled it in.",
|
|
201
|
+
docsAnchor: "errors#node-builtin-in-client",
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
code: "NIFRA_SCHEMA_PARSE",
|
|
205
|
+
match: (n, m) =>
|
|
206
|
+
n === "SchemaError" ||
|
|
207
|
+
/failed to (parse|validate)|invalid_type|expected .* received/i.test(m),
|
|
208
|
+
cause: "Data crossing a boundary did not match its declared schema.",
|
|
209
|
+
fix: "Check the value against the schema at the failing boundary (loader input, search params, or request body); parse-don't-cast means the shape must match exactly.",
|
|
210
|
+
docsAnchor: "errors#schema-parse",
|
|
211
|
+
},
|
|
212
|
+
]
|
|
213
|
+
|
|
214
|
+
/** Classify an error name+message against the catalog; falls back to the generic unhandled code. */
|
|
215
|
+
export function classify(
|
|
216
|
+
name: string,
|
|
217
|
+
message: string,
|
|
218
|
+
): {
|
|
219
|
+
code: string
|
|
220
|
+
cause?: string
|
|
221
|
+
fix?: string
|
|
222
|
+
docsAnchor?: string
|
|
223
|
+
} {
|
|
224
|
+
const hit = DIAGNOSTIC_CATALOG.find((e) => e.match(name, message))
|
|
225
|
+
if (hit === undefined) return { code: "NIFRA_UNHANDLED" }
|
|
226
|
+
return { code: hit.code, cause: hit.cause, fix: hit.fix, docsAnchor: hit.docsAnchor }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Split an Error's `stack` into its leading message block and its frame lines. */
|
|
230
|
+
function messageAndStack(err: Error): { message: string; stack: string } {
|
|
231
|
+
const stack = err.stack ?? `${err.name}: ${err.message}`
|
|
232
|
+
const lines = stack.split("\n")
|
|
233
|
+
const firstFrame = lines.findIndex((l) => /^\s*at\s/.test(l))
|
|
234
|
+
if (firstFrame === -1) return { message: err.message || stack.trim(), stack: "" }
|
|
235
|
+
return {
|
|
236
|
+
message: lines.slice(0, firstFrame).join("\n").trim(),
|
|
237
|
+
stack: lines.slice(firstFrame).join("\n"),
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export interface BuildDiagnosticOptions {
|
|
242
|
+
readonly request?: { readonly method: string; readonly url: string }
|
|
243
|
+
/** Project root; frames outside it are treated as non-user. Defaults to `process.cwd()`. */
|
|
244
|
+
readonly root?: string
|
|
245
|
+
/** Injectable source reader (tests pass a fake; production reads the filesystem). */
|
|
246
|
+
readonly read?: SourceReader
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Resolve any thrown value into a `Diagnostic`: parse the (already source-mapped) stack, locate the top
|
|
251
|
+
* user frame, attach a codeframe, and classify the failure for a cause/fix. The caller is responsible
|
|
252
|
+
* for running Vite's `ssrFixStacktrace` first so the frames point at real source.
|
|
253
|
+
*/
|
|
254
|
+
export function buildDiagnostic(err: unknown, options: BuildDiagnosticOptions = {}): Diagnostic {
|
|
255
|
+
const error = err instanceof Error ? err : new Error(String(err))
|
|
256
|
+
const root = options.root ?? safeCwd()
|
|
257
|
+
const { message } = messageAndStack(error)
|
|
258
|
+
const frames = parseFrames(error.stack ?? "")
|
|
259
|
+
const top = topUserFrame(frames, root)
|
|
260
|
+
const codeframe =
|
|
261
|
+
top?.file !== undefined && top.line !== undefined
|
|
262
|
+
? buildCodeframe(canonicalPath(top.file), top.line, top.column, options.read)
|
|
263
|
+
: undefined
|
|
264
|
+
const { code, cause, fix, docsAnchor } = classify(error.name || "Error", message)
|
|
265
|
+
return {
|
|
266
|
+
code,
|
|
267
|
+
name: error.name || "Error",
|
|
268
|
+
message,
|
|
269
|
+
request: options.request,
|
|
270
|
+
frames,
|
|
271
|
+
codeframe,
|
|
272
|
+
cause,
|
|
273
|
+
fix,
|
|
274
|
+
docsAnchor,
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function safeCwd(): string | undefined {
|
|
279
|
+
try {
|
|
280
|
+
return process.cwd()
|
|
281
|
+
} catch {
|
|
282
|
+
return undefined
|
|
283
|
+
}
|
|
284
|
+
}
|