@mandujs/core 0.28.0 → 0.29.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +9 -1
- package/src/bundler/generate-static-params.ts +290 -0
- package/src/bundler/prerender.ts +242 -69
- package/src/client/hydrate.ts +340 -0
- package/src/client/index.ts +11 -0
- package/src/config/mandu.ts +40 -0
- package/src/config/validate.ts +36 -0
- package/src/dev-error-overlay/__tests__/overlay-injector.test.ts +241 -0
- package/src/dev-error-overlay/index.ts +30 -0
- package/src/dev-error-overlay/overlay-client.ts +300 -0
- package/src/dev-error-overlay/overlay-injector.ts +243 -0
- package/src/dev-error-overlay/overlay-styles.ts +52 -0
- package/src/dev-error-overlay/types.ts +66 -0
- package/src/middleware/bridge.ts +147 -0
- package/src/middleware/compose.ts +134 -0
- package/src/middleware/define.ts +132 -0
- package/src/middleware/index.ts +76 -50
- package/src/router/fs-patterns.test.ts +96 -0
- package/src/router/fs-routes.ts +1 -0
- package/src/router/fs-scanner.ts +10 -1
- package/src/router/fs-types.ts +8 -0
- package/src/runtime/server.ts +310 -10
- package/src/runtime/ssr.ts +70 -2
- package/src/spec/schema.ts +6 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 18.α — Dev Error Overlay regression tests.
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - Dev injection emits both <style> and <script> tags
|
|
6
|
+
* - Production (NODE_ENV=production) suppresses injection
|
|
7
|
+
* - Explicit `enabled: false` suppresses injection even in dev
|
|
8
|
+
* - Stack parsing handles Chrome + Firefox shapes + fallthrough
|
|
9
|
+
* - Error embed escapes `</script>` sequences
|
|
10
|
+
* - Payload builder tolerates non-Error throws
|
|
11
|
+
* - buildOverlayErrorHtml wraps the embed in a valid doctype doc
|
|
12
|
+
* - Client IIFE mounts the mounted-flag guard to prevent double-mount
|
|
13
|
+
*/
|
|
14
|
+
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
|
|
15
|
+
import {
|
|
16
|
+
buildOverlayHeadTag,
|
|
17
|
+
maybeInjectDevOverlay,
|
|
18
|
+
shouldInjectOverlay,
|
|
19
|
+
parseStackFrames,
|
|
20
|
+
buildPayloadFromError,
|
|
21
|
+
buildOverlayErrorEmbed,
|
|
22
|
+
buildOverlayErrorHtml,
|
|
23
|
+
} from "../overlay-injector";
|
|
24
|
+
import {
|
|
25
|
+
OVERLAY_CLIENT_SCRIPT,
|
|
26
|
+
_testOnly_buildOverlayClientScript,
|
|
27
|
+
} from "../overlay-client";
|
|
28
|
+
import {
|
|
29
|
+
OVERLAY_CUSTOM_EVENT,
|
|
30
|
+
OVERLAY_MOUNTED_FLAG,
|
|
31
|
+
OVERLAY_PAYLOAD_ELEMENT_ID,
|
|
32
|
+
} from "../types";
|
|
33
|
+
|
|
34
|
+
describe("shouldInjectOverlay", () => {
|
|
35
|
+
const prevEnv = process.env.NODE_ENV;
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
if (prevEnv === undefined) delete process.env.NODE_ENV;
|
|
38
|
+
else process.env.NODE_ENV = prevEnv;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("returns true in dev with default config", () => {
|
|
42
|
+
process.env.NODE_ENV = "development";
|
|
43
|
+
expect(shouldInjectOverlay({ isDev: true })).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("returns false when isDev is false", () => {
|
|
47
|
+
process.env.NODE_ENV = "development";
|
|
48
|
+
expect(shouldInjectOverlay({ isDev: false })).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("returns false when NODE_ENV=production even if isDev lies", () => {
|
|
52
|
+
process.env.NODE_ENV = "production";
|
|
53
|
+
expect(shouldInjectOverlay({ isDev: true })).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("returns false when user opts out via enabled: false", () => {
|
|
57
|
+
process.env.NODE_ENV = "development";
|
|
58
|
+
expect(shouldInjectOverlay({ isDev: true, enabled: false })).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("returns true when user explicitly enables in dev", () => {
|
|
62
|
+
process.env.NODE_ENV = "development";
|
|
63
|
+
expect(shouldInjectOverlay({ isDev: true, enabled: true })).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("buildOverlayHeadTag", () => {
|
|
68
|
+
const prevEnv = process.env.NODE_ENV;
|
|
69
|
+
afterEach(() => {
|
|
70
|
+
if (prevEnv === undefined) delete process.env.NODE_ENV;
|
|
71
|
+
else process.env.NODE_ENV = prevEnv;
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("emits <style> and <script> in dev", () => {
|
|
75
|
+
process.env.NODE_ENV = "development";
|
|
76
|
+
const out = buildOverlayHeadTag({ isDev: true });
|
|
77
|
+
expect(out).toContain("<style");
|
|
78
|
+
expect(out).toContain("id=\"__mandu-dev-overlay-style\"");
|
|
79
|
+
expect(out).toContain("<script");
|
|
80
|
+
expect(out).toContain("id=\"__mandu-dev-overlay-client\"");
|
|
81
|
+
expect(out).toContain(".mandu-dev-overlay");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("emits empty string in production", () => {
|
|
85
|
+
process.env.NODE_ENV = "production";
|
|
86
|
+
expect(buildOverlayHeadTag({ isDev: true })).toBe("");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("emits empty string when opted out", () => {
|
|
90
|
+
process.env.NODE_ENV = "development";
|
|
91
|
+
expect(buildOverlayHeadTag({ isDev: true, enabled: false })).toBe("");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("maybeInjectDevOverlay is alias with same behavior", () => {
|
|
95
|
+
process.env.NODE_ENV = "development";
|
|
96
|
+
expect(maybeInjectDevOverlay({ isDev: true })).toBe(
|
|
97
|
+
buildOverlayHeadTag({ isDev: true }),
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("does not contain the sentinel flag leaking as a global assignment outside the IIFE", () => {
|
|
102
|
+
process.env.NODE_ENV = "development";
|
|
103
|
+
const out = buildOverlayHeadTag({ isDev: true });
|
|
104
|
+
// The sentinel is only ever referenced via `window[MOUNTED]`, never as a bare identifier assignment.
|
|
105
|
+
expect(out.includes(`var ${OVERLAY_MOUNTED_FLAG}=`)).toBe(false);
|
|
106
|
+
// But the string literal MUST appear at least once (inside the IIFE).
|
|
107
|
+
expect(out).toContain(OVERLAY_MOUNTED_FLAG);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe("parseStackFrames", () => {
|
|
112
|
+
it("parses Chrome-style frames", () => {
|
|
113
|
+
const stack = `TypeError: boom
|
|
114
|
+
at myFn (/src/app/page.tsx:42:10)
|
|
115
|
+
at Other (/src/app/layout.tsx:7:5)`;
|
|
116
|
+
const frames = parseStackFrames(stack);
|
|
117
|
+
expect(frames.length).toBe(2);
|
|
118
|
+
expect(frames[0].fn).toBe("myFn");
|
|
119
|
+
expect(frames[0].file).toBe("/src/app/page.tsx");
|
|
120
|
+
expect(frames[0].line).toBe(42);
|
|
121
|
+
expect(frames[0].column).toBe(10);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("parses Firefox-style frames", () => {
|
|
125
|
+
const stack = `TypeError: boom
|
|
126
|
+
myFn@/src/app/page.tsx:42:10
|
|
127
|
+
@/src/app/layout.tsx:7:5`;
|
|
128
|
+
const frames = parseStackFrames(stack);
|
|
129
|
+
expect(frames.length).toBeGreaterThanOrEqual(1);
|
|
130
|
+
expect(frames[0].file).toBe("/src/app/page.tsx");
|
|
131
|
+
expect(frames[0].line).toBe(42);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("tolerates empty / null input", () => {
|
|
135
|
+
expect(parseStackFrames(undefined)).toEqual([]);
|
|
136
|
+
expect(parseStackFrames(null)).toEqual([]);
|
|
137
|
+
expect(parseStackFrames("")).toEqual([]);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("falls through with <anonymous> for unparseable lines", () => {
|
|
141
|
+
const stack = `SomeWeirdRuntime\nopaque frame line`;
|
|
142
|
+
const frames = parseStackFrames(stack);
|
|
143
|
+
expect(frames.length).toBeGreaterThan(0);
|
|
144
|
+
expect(frames.some((f) => f.fn === "<anonymous>")).toBe(true);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe("buildPayloadFromError", () => {
|
|
149
|
+
it("extracts name, message, and stack from an Error", () => {
|
|
150
|
+
const err = new Error("boom");
|
|
151
|
+
err.name = "CustomError";
|
|
152
|
+
const p = buildPayloadFromError(err, { kind: "ssr", routeId: "r1" });
|
|
153
|
+
expect(p.name).toBe("CustomError");
|
|
154
|
+
expect(p.message).toBe("boom");
|
|
155
|
+
expect(p.kind).toBe("ssr");
|
|
156
|
+
expect(p.routeId).toBe("r1");
|
|
157
|
+
expect(typeof p.timestamp).toBe("number");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("tolerates string throws", () => {
|
|
161
|
+
const p = buildPayloadFromError("just a string");
|
|
162
|
+
expect(p.message).toBe("just a string");
|
|
163
|
+
expect(p.name).toBe("Error");
|
|
164
|
+
expect(p.frames).toEqual([]);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("tolerates null and undefined", () => {
|
|
168
|
+
const p1 = buildPayloadFromError(null);
|
|
169
|
+
expect(p1.name).toBe("Error");
|
|
170
|
+
const p2 = buildPayloadFromError(undefined);
|
|
171
|
+
expect(p2.name).toBe("Error");
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe("buildOverlayErrorEmbed", () => {
|
|
176
|
+
it("emits a JSON script tag with the payload id", () => {
|
|
177
|
+
const p = buildPayloadFromError(new Error("test"), { kind: "ssr" });
|
|
178
|
+
const embed = buildOverlayErrorEmbed(p);
|
|
179
|
+
expect(embed).toContain(`id="${OVERLAY_PAYLOAD_ELEMENT_ID}"`);
|
|
180
|
+
expect(embed).toContain("application/json");
|
|
181
|
+
expect(embed).toContain(OVERLAY_CUSTOM_EVENT);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("escapes </script> in the payload so it cannot break out", () => {
|
|
185
|
+
const err = new Error("</script><script>alert(1)</script>");
|
|
186
|
+
const p = buildPayloadFromError(err);
|
|
187
|
+
const embed = buildOverlayErrorEmbed(p);
|
|
188
|
+
// The literal </script> sequence must not appear in the serialized JSON.
|
|
189
|
+
// escapeJsonForInlineScript breaks it into a harmless form.
|
|
190
|
+
const between = embed.split(`id="${OVERLAY_PAYLOAD_ELEMENT_ID}"`)[1] ?? "";
|
|
191
|
+
const payloadChunk = between.split("</script>")[0] ?? "";
|
|
192
|
+
expect(payloadChunk.toLowerCase().includes("<script>alert(1)")).toBe(false);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe("buildOverlayErrorHtml", () => {
|
|
197
|
+
const prevEnv = process.env.NODE_ENV;
|
|
198
|
+
afterEach(() => {
|
|
199
|
+
if (prevEnv === undefined) delete process.env.NODE_ENV;
|
|
200
|
+
else process.env.NODE_ENV = prevEnv;
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("wraps the embed in a valid dev HTML document", () => {
|
|
204
|
+
process.env.NODE_ENV = "development";
|
|
205
|
+
const p = buildPayloadFromError(new Error("ssr boom"), { kind: "ssr", routeId: "home" });
|
|
206
|
+
const html = buildOverlayErrorHtml(p);
|
|
207
|
+
expect(html.startsWith("<!doctype html>")).toBe(true);
|
|
208
|
+
expect(html).toContain("<title>");
|
|
209
|
+
expect(html).toContain(`id="${OVERLAY_PAYLOAD_ELEMENT_ID}"`);
|
|
210
|
+
expect(html).toContain(".mandu-dev-overlay");
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe("OVERLAY_CLIENT_SCRIPT shape", () => {
|
|
215
|
+
it("is a well-formed IIFE", () => {
|
|
216
|
+
expect(OVERLAY_CLIENT_SCRIPT.startsWith("(function(){")).toBe(true);
|
|
217
|
+
expect(OVERLAY_CLIENT_SCRIPT.endsWith("})();")).toBe(true);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("references the mounted-flag guard (prevents double-mount on HMR)", () => {
|
|
221
|
+
expect(OVERLAY_CLIENT_SCRIPT).toContain(OVERLAY_MOUNTED_FLAG);
|
|
222
|
+
expect(OVERLAY_CLIENT_SCRIPT).toContain("window[MOUNTED]=true");
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("wires all three error entry points", () => {
|
|
226
|
+
expect(OVERLAY_CLIENT_SCRIPT).toContain(`"error"`);
|
|
227
|
+
expect(OVERLAY_CLIENT_SCRIPT).toContain("unhandledrejection");
|
|
228
|
+
expect(OVERLAY_CLIENT_SCRIPT).toContain(OVERLAY_CUSTOM_EVENT);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("stays under the 10 KB-gzip target (rough byte budget check)", () => {
|
|
232
|
+
// Uncompressed budget: 64 KB is a generous ceiling; gzip typically
|
|
233
|
+
// achieves 4-5x on this sort of string-heavy JS. We assert uncompressed
|
|
234
|
+
// < 32 KB as a hard ceiling — the current IIFE is ~7 KB uncompressed.
|
|
235
|
+
expect(OVERLAY_CLIENT_SCRIPT.length).toBeLessThan(32 * 1024);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("is recomputable — _testOnly_buildOverlayClientScript yields identical output", () => {
|
|
239
|
+
expect(_testOnly_buildOverlayClientScript()).toBe(OVERLAY_CLIENT_SCRIPT);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 18.α — Dev Error Overlay public surface.
|
|
3
|
+
*
|
|
4
|
+
* Consumed by:
|
|
5
|
+
* - `runtime/ssr.ts` (injects `<style>` + `<script>` into `<head>` in dev)
|
|
6
|
+
* - `runtime/server.ts` (embeds an SSR 500 payload so the overlay can
|
|
7
|
+
* mount even when the React tree never rendered)
|
|
8
|
+
*
|
|
9
|
+
* Production never imports from here at runtime — tree-shaking drops
|
|
10
|
+
* the IIFE string because `shouldInjectOverlay` returns `false`.
|
|
11
|
+
*/
|
|
12
|
+
export {
|
|
13
|
+
buildOverlayHeadTag,
|
|
14
|
+
maybeInjectDevOverlay,
|
|
15
|
+
shouldInjectOverlay,
|
|
16
|
+
parseStackFrames,
|
|
17
|
+
buildPayloadFromError,
|
|
18
|
+
buildOverlayErrorEmbed,
|
|
19
|
+
buildOverlayErrorHtml,
|
|
20
|
+
type DevOverlayInjectorConfig,
|
|
21
|
+
} from "./overlay-injector";
|
|
22
|
+
export { OVERLAY_CLIENT_SCRIPT } from "./overlay-client";
|
|
23
|
+
export { OVERLAY_STYLES } from "./overlay-styles";
|
|
24
|
+
export {
|
|
25
|
+
OVERLAY_PAYLOAD_ELEMENT_ID,
|
|
26
|
+
OVERLAY_CUSTOM_EVENT,
|
|
27
|
+
OVERLAY_MOUNTED_FLAG,
|
|
28
|
+
type DevErrorPayload,
|
|
29
|
+
type DevErrorStackFrame,
|
|
30
|
+
} from "./types";
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 18.α — Dev Error Overlay client IIFE.
|
|
3
|
+
*
|
|
4
|
+
* This file EXPORTS a string (`OVERLAY_CLIENT_SCRIPT`) that is inlined
|
|
5
|
+
* into the SSR `<head>` by the injector. It is NOT compiled as a
|
|
6
|
+
* module; the whole body runs in parse order on every dev page load.
|
|
7
|
+
*
|
|
8
|
+
* Size target: < 10 KB gzipped (current ≈ 2 KB gz). No dependencies.
|
|
9
|
+
* Only touches `window`, `document`, `navigator.clipboard`, and a tiny
|
|
10
|
+
* WeakSet.
|
|
11
|
+
*
|
|
12
|
+
* Listeners installed:
|
|
13
|
+
* - `window.onerror` → uncaught script errors
|
|
14
|
+
* - `unhandledrejection` → Promise rejections
|
|
15
|
+
* - custom event `__MANDU_ERROR__` → the server embeds a 500 payload
|
|
16
|
+
* in the HTML, fires this event on DOMContentLoaded, and the
|
|
17
|
+
* overlay picks it up. User code can also `dispatchEvent` it to
|
|
18
|
+
* surface synthetic errors.
|
|
19
|
+
*
|
|
20
|
+
* Everything scoped in an IIFE — NO globals leak except the single
|
|
21
|
+
* `__MANDU_OVERLAY_MOUNTED__` sentinel that prevents double-mount on
|
|
22
|
+
* HMR.
|
|
23
|
+
*/
|
|
24
|
+
import { OVERLAY_STYLES } from "./overlay-styles";
|
|
25
|
+
import {
|
|
26
|
+
OVERLAY_CUSTOM_EVENT,
|
|
27
|
+
OVERLAY_MOUNTED_FLAG,
|
|
28
|
+
OVERLAY_PAYLOAD_ELEMENT_ID,
|
|
29
|
+
} from "./types";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Build the client-side IIFE body as a string. Parameterised by the
|
|
33
|
+
* style block and the two sentinel names so tests can verify the exact
|
|
34
|
+
* shape without re-declaring the constants.
|
|
35
|
+
*/
|
|
36
|
+
function buildOverlayClientScript(): string {
|
|
37
|
+
// The IIFE below references the constants through string interpolation,
|
|
38
|
+
// NOT through imports — because it runs in the browser and has no
|
|
39
|
+
// module loader.
|
|
40
|
+
return `(function(){
|
|
41
|
+
var MOUNTED=${JSON.stringify(OVERLAY_MOUNTED_FLAG)};
|
|
42
|
+
var EVT=${JSON.stringify(OVERLAY_CUSTOM_EVENT)};
|
|
43
|
+
var PAYLOAD_ID=${JSON.stringify(OVERLAY_PAYLOAD_ELEMENT_ID)};
|
|
44
|
+
if(typeof window==="undefined"||typeof document==="undefined")return;
|
|
45
|
+
if(window[MOUNTED]===true)return;
|
|
46
|
+
window[MOUNTED]=true;
|
|
47
|
+
|
|
48
|
+
var STYLE=${JSON.stringify(OVERLAY_STYLES)};
|
|
49
|
+
var seen=new WeakSet();
|
|
50
|
+
|
|
51
|
+
function parseFrames(stack){
|
|
52
|
+
if(typeof stack!=="string"||stack.length===0)return [];
|
|
53
|
+
var lines=stack.split("\\n");
|
|
54
|
+
var out=[];
|
|
55
|
+
for(var i=0;i<lines.length;i++){
|
|
56
|
+
var raw=lines[i];
|
|
57
|
+
var t=raw.trim();
|
|
58
|
+
if(!t||t===stack.split("\\n")[0]&&/^[A-Z][a-zA-Z]*(?:Error)?:/.test(t))continue;
|
|
59
|
+
// Chrome: " at fn (file:line:col)"
|
|
60
|
+
// Firefox: "fn@file:line:col"
|
|
61
|
+
var m=t.match(/^at\\s+(.+?)\\s+\\((.+):(\\d+):(\\d+)\\)$/);
|
|
62
|
+
if(!m)m=t.match(/^at\\s+(.+):(\\d+):(\\d+)$/);
|
|
63
|
+
if(m&&m.length===5){
|
|
64
|
+
out.push({fn:m[1],file:m[2],line:parseInt(m[3],10),column:parseInt(m[4],10),raw:raw});
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if(m&&m.length===4){
|
|
68
|
+
out.push({fn:"<anonymous>",file:m[1],line:parseInt(m[2],10),column:parseInt(m[3],10),raw:raw});
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
var ff=t.match(/^(.*?)@(.+):(\\d+):(\\d+)$/);
|
|
72
|
+
if(ff){
|
|
73
|
+
out.push({fn:ff[1]||"<anonymous>",file:ff[2],line:parseInt(ff[3],10),column:parseInt(ff[4],10),raw:raw});
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
out.push({fn:"<anonymous>",file:t,line:null,column:null,raw:raw});
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function toPayload(err,kind){
|
|
82
|
+
var name="Error",message="",stack="";
|
|
83
|
+
if(err&&typeof err==="object"){
|
|
84
|
+
name=err.name||"Error";
|
|
85
|
+
message=typeof err.message==="string"?err.message:String(err.message||"");
|
|
86
|
+
stack=typeof err.stack==="string"?err.stack:"";
|
|
87
|
+
}else if(typeof err==="string"){
|
|
88
|
+
message=err;
|
|
89
|
+
}else if(err!=null){
|
|
90
|
+
try{message=String(err);}catch(_){message="<unserializable error>";}
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
name:name,
|
|
94
|
+
message:message,
|
|
95
|
+
frames:parseFrames(stack),
|
|
96
|
+
stack:stack,
|
|
97
|
+
kind:kind,
|
|
98
|
+
timestamp:Date.now(),
|
|
99
|
+
url:typeof location!=="undefined"?location.pathname+location.search:undefined,
|
|
100
|
+
userAgent:typeof navigator!=="undefined"?navigator.userAgent:undefined
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function esc(s){
|
|
105
|
+
if(s==null)return "";
|
|
106
|
+
return String(s).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function fileHref(file,line,col){
|
|
110
|
+
// vscode://file/<abs>:<line>:<col> — falls back gracefully when the
|
|
111
|
+
// host OS has no handler. Relative paths get "//" prefix so the URL
|
|
112
|
+
// still parses, even though the editor may refuse to jump.
|
|
113
|
+
var p=file;
|
|
114
|
+
if(!/^[a-zA-Z]:|^\\//.test(p))p="/"+p;
|
|
115
|
+
var u="vscode://file"+p;
|
|
116
|
+
if(line)u+=":"+line;
|
|
117
|
+
if(col)u+=":"+col;
|
|
118
|
+
return u;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function renderFrames(frames){
|
|
122
|
+
if(!frames||frames.length===0)return '<li class="mandu-dev-overlay__frame"><span class="mandu-dev-overlay__frame-fn"><no stack></span></li>';
|
|
123
|
+
var items=[];
|
|
124
|
+
for(var i=0;i<frames.length&&i<40;i++){
|
|
125
|
+
var f=frames[i];
|
|
126
|
+
var loc=esc(f.file)+(f.line?":"+f.line:"")+(f.column?":"+f.column:"");
|
|
127
|
+
var href=f.file?fileHref(f.file,f.line,f.column):"";
|
|
128
|
+
var locHtml=href?'<a class="mandu-dev-overlay__frame-link" href="'+esc(href)+'">'+loc+'</a>':loc;
|
|
129
|
+
items.push('<li class="mandu-dev-overlay__frame"><span class="mandu-dev-overlay__frame-fn">'+esc(f.fn||"<anonymous>")+'</span><span class="mandu-dev-overlay__frame-loc">'+locHtml+'</span></li>');
|
|
130
|
+
}
|
|
131
|
+
return items.join("");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function ensureStyleTag(){
|
|
135
|
+
if(document.getElementById("__mandu-dev-overlay-style"))return;
|
|
136
|
+
var s=document.createElement("style");
|
|
137
|
+
s.id="__mandu-dev-overlay-style";
|
|
138
|
+
s.textContent=STYLE;
|
|
139
|
+
(document.head||document.documentElement).appendChild(s);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function copyForAI(payload,btn){
|
|
143
|
+
var text="# Mandu dev error snapshot\\n\\n"
|
|
144
|
+
+"- kind: "+payload.kind+"\\n"
|
|
145
|
+
+"- name: "+payload.name+"\\n"
|
|
146
|
+
+"- message: "+payload.message+"\\n"
|
|
147
|
+
+"- url: "+(payload.url||"")+"\\n"
|
|
148
|
+
+"- routeId: "+(payload.routeId||"")+"\\n"
|
|
149
|
+
+"- timestamp: "+new Date(payload.timestamp).toISOString()+"\\n\\n"
|
|
150
|
+
+"## Stack\\n\\n\`\`\`\\n"+payload.stack+"\\n\`\`\`\\n";
|
|
151
|
+
var done=function(){
|
|
152
|
+
var prev=btn.textContent;
|
|
153
|
+
btn.textContent="Copied!";
|
|
154
|
+
setTimeout(function(){btn.textContent=prev;},1500);
|
|
155
|
+
};
|
|
156
|
+
if(navigator.clipboard&&navigator.clipboard.writeText){
|
|
157
|
+
navigator.clipboard.writeText(text).then(done,function(){
|
|
158
|
+
fallbackCopy(text);done();
|
|
159
|
+
});
|
|
160
|
+
}else{
|
|
161
|
+
fallbackCopy(text);done();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function fallbackCopy(text){
|
|
166
|
+
try{
|
|
167
|
+
var ta=document.createElement("textarea");
|
|
168
|
+
ta.value=text;ta.style.position="fixed";ta.style.left="-9999px";
|
|
169
|
+
document.body.appendChild(ta);ta.select();
|
|
170
|
+
try{document.execCommand("copy");}catch(_){}
|
|
171
|
+
document.body.removeChild(ta);
|
|
172
|
+
}catch(_){}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function mount(payload){
|
|
176
|
+
if(seen.has(payload))return;
|
|
177
|
+
seen.add(payload);
|
|
178
|
+
ensureStyleTag();
|
|
179
|
+
var root=document.getElementById("__mandu-dev-overlay-root");
|
|
180
|
+
if(!root){
|
|
181
|
+
root=document.createElement("div");
|
|
182
|
+
root.id="__mandu-dev-overlay-root";
|
|
183
|
+
root.className="mandu-dev-overlay";
|
|
184
|
+
root.setAttribute("role","dialog");
|
|
185
|
+
root.setAttribute("aria-modal","true");
|
|
186
|
+
root.setAttribute("aria-label","Mandu dev error overlay");
|
|
187
|
+
(document.body||document.documentElement).appendChild(root);
|
|
188
|
+
}
|
|
189
|
+
var kindLabel=({ssr:"SSR render failed",window:"Uncaught error",unhandledrejection:"Unhandled promise rejection",manual:"Error"})[payload.kind]||"Error";
|
|
190
|
+
var meta="";
|
|
191
|
+
if(payload.routeId||payload.url){
|
|
192
|
+
meta='<div class="mandu-dev-overlay__meta">'
|
|
193
|
+
+(payload.routeId?'<div class="mandu-dev-overlay__meta-item"><span class="mandu-dev-overlay__meta-label">Route</span><span class="mandu-dev-overlay__meta-value">'+esc(payload.routeId)+'</span></div>':'')
|
|
194
|
+
+(payload.url?'<div class="mandu-dev-overlay__meta-item"><span class="mandu-dev-overlay__meta-label">URL</span><span class="mandu-dev-overlay__meta-value">'+esc(payload.url)+'</span></div>':'')
|
|
195
|
+
+'<div class="mandu-dev-overlay__meta-item"><span class="mandu-dev-overlay__meta-label">Time</span><span class="mandu-dev-overlay__meta-value">'+esc(new Date(payload.timestamp).toLocaleTimeString())+'</span></div>'
|
|
196
|
+
+'</div>';
|
|
197
|
+
}
|
|
198
|
+
root.innerHTML='<div class="mandu-dev-overlay__panel">'
|
|
199
|
+
+'<div class="mandu-dev-overlay__header">'
|
|
200
|
+
+'<div class="mandu-dev-overlay__title">'
|
|
201
|
+
+'<span class="mandu-dev-overlay__kind">'+esc(kindLabel)+'</span>'
|
|
202
|
+
+'<span class="mandu-dev-overlay__name">'+esc(payload.name)+'</span>'
|
|
203
|
+
+'</div>'
|
|
204
|
+
+'<div class="mandu-dev-overlay__actions">'
|
|
205
|
+
+'<button type="button" class="mandu-dev-overlay__btn mandu-dev-overlay__btn--primary" data-action="copy">Copy for AI</button>'
|
|
206
|
+
+'<button type="button" class="mandu-dev-overlay__btn" data-action="close" aria-label="Close overlay">Close</button>'
|
|
207
|
+
+'</div>'
|
|
208
|
+
+'</div>'
|
|
209
|
+
+'<div class="mandu-dev-overlay__body">'
|
|
210
|
+
+'<p class="mandu-dev-overlay__message">'+esc(payload.message||"(no message)")+'</p>'
|
|
211
|
+
+meta
|
|
212
|
+
+'<h3 class="mandu-dev-overlay__section-title">Stack frames</h3>'
|
|
213
|
+
+'<ol class="mandu-dev-overlay__frames">'+renderFrames(payload.frames)+'</ol>'
|
|
214
|
+
+(payload.stack?'<h3 class="mandu-dev-overlay__section-title" style="margin-top:16px">Raw stack</h3><pre class="mandu-dev-overlay__stack">'+esc(payload.stack)+'</pre>':'')
|
|
215
|
+
+'</div>'
|
|
216
|
+
+'<div class="mandu-dev-overlay__footer">Dev-only overlay. Press <code>Esc</code> to dismiss. Never shipped in production builds.</div>'
|
|
217
|
+
+'</div>';
|
|
218
|
+
root.hidden=false;
|
|
219
|
+
|
|
220
|
+
var onClick=function(e){
|
|
221
|
+
var t=e.target;
|
|
222
|
+
if(!t||!t.getAttribute)return;
|
|
223
|
+
var a=t.getAttribute("data-action");
|
|
224
|
+
if(a==="close")hide();
|
|
225
|
+
else if(a==="copy")copyForAI(payload,t);
|
|
226
|
+
};
|
|
227
|
+
root.onclick=onClick;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function hide(){
|
|
231
|
+
var root=document.getElementById("__mandu-dev-overlay-root");
|
|
232
|
+
if(root)root.hidden=true;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function handle(err,kind,extra){
|
|
236
|
+
try{
|
|
237
|
+
var p=toPayload(err,kind);
|
|
238
|
+
if(extra&&typeof extra==="object"){
|
|
239
|
+
if(extra.routeId)p.routeId=extra.routeId;
|
|
240
|
+
if(extra.url)p.url=extra.url;
|
|
241
|
+
}
|
|
242
|
+
mount(p);
|
|
243
|
+
}catch(_){
|
|
244
|
+
// overlay must never throw — silent drop.
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
window.addEventListener("error",function(ev){
|
|
249
|
+
if(ev&&ev.error)handle(ev.error,"window");
|
|
250
|
+
else if(ev)handle(ev.message||"Script error",(\"window\"));
|
|
251
|
+
});
|
|
252
|
+
window.addEventListener("unhandledrejection",function(ev){
|
|
253
|
+
handle(ev&&ev.reason,"unhandled-rejection");
|
|
254
|
+
});
|
|
255
|
+
window.addEventListener(EVT,function(ev){
|
|
256
|
+
var d=ev&&ev.detail;
|
|
257
|
+
if(!d)return;
|
|
258
|
+
if(d.name||d.message||d.stack){
|
|
259
|
+
var p={
|
|
260
|
+
name:d.name||"Error",
|
|
261
|
+
message:d.message||"",
|
|
262
|
+
frames:d.frames||parseFrames(d.stack||""),
|
|
263
|
+
stack:d.stack||"",
|
|
264
|
+
kind:d.kind||"manual",
|
|
265
|
+
timestamp:d.timestamp||Date.now(),
|
|
266
|
+
routeId:d.routeId,
|
|
267
|
+
url:d.url||(typeof location!=="undefined"?location.pathname+location.search:undefined),
|
|
268
|
+
userAgent:typeof navigator!=="undefined"?navigator.userAgent:undefined
|
|
269
|
+
};
|
|
270
|
+
mount(p);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
document.addEventListener("keydown",function(e){
|
|
274
|
+
if(e.key==="Escape")hide();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
function readEmbedded(){
|
|
278
|
+
var el=document.getElementById(PAYLOAD_ID);
|
|
279
|
+
if(!el)return;
|
|
280
|
+
try{
|
|
281
|
+
var data=JSON.parse(el.textContent||"null");
|
|
282
|
+
if(data&&typeof data==="object")mount(data);
|
|
283
|
+
}catch(_){}
|
|
284
|
+
}
|
|
285
|
+
if(document.readyState==="loading"){
|
|
286
|
+
document.addEventListener("DOMContentLoaded",readEmbedded);
|
|
287
|
+
}else{
|
|
288
|
+
readEmbedded();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Public surface (tests + user code)
|
|
292
|
+
window.__MANDU_DEV_OVERLAY__={show:function(e){handle(e,"manual");},hide:hide};
|
|
293
|
+
})();`;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** The serialized IIFE. Computed once at module-load. */
|
|
297
|
+
export const OVERLAY_CLIENT_SCRIPT = buildOverlayClientScript();
|
|
298
|
+
|
|
299
|
+
/** @internal test-only accessor so tests can recompute / inspect the script. */
|
|
300
|
+
export const _testOnly_buildOverlayClientScript = buildOverlayClientScript;
|