@pajh/buldng 0.0.3 → 0.0.5
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 +19 -10
- package/src/MIME.json +11 -0
- package/src/buldng-async.js +20 -0
- package/src/buldng-files.js +58 -0
- package/src/buldng-html.js +262 -0
- package/src/buldng-lib.js +90 -0
- package/src/buldng-modal.ts +348 -0
- package/{dist → src}/buldng-ops.js +8 -8
- package/src/buldng-typescript.js +77 -0
- package/src/buldng-web.ts +142 -0
- package/{dist → src}/buldng.css +130 -0
- package/src/dev-errors.js +16 -0
- package/src/layout-err.ts +171 -0
- package/{dist → src}/layout.ts +4 -0
- package/src/serve-lib.js +567 -0
- package/src/serve.js +193 -0
- package/src/types/global.d.ts +19 -0
- package/{dist → src}/wrapper.html +2 -0
- package/dist/buldng-lib.js +0 -408
- package/dist/types/global.d.ts +0 -10
- /package/{dist → src}/buldng-validate.js +0 -0
- /package/{dist → src}/clean.sh +0 -0
- /package/{dist → src}/init.js +0 -0
- /package/{dist → src}/workfile.js +0 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
export type ModalType =
|
|
2
|
+
| "info"
|
|
3
|
+
| "confirmation"
|
|
4
|
+
| "destructive"
|
|
5
|
+
| "error";
|
|
6
|
+
|
|
7
|
+
export type ModalIcon = "info" | "warning" | "error" | null;
|
|
8
|
+
|
|
9
|
+
export type ModalResult = "ok" | "cancel" | "dismiss";
|
|
10
|
+
|
|
11
|
+
export interface ModalSettings {
|
|
12
|
+
title?: string;
|
|
13
|
+
icon?: ModalIcon;
|
|
14
|
+
autoDismiss?: number;
|
|
15
|
+
dismissOnEsc?: boolean;
|
|
16
|
+
dismissOnBackdrop?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const MODAL_ROOT_ID = "b-modal-root";
|
|
20
|
+
let modalId = 0;
|
|
21
|
+
|
|
22
|
+
interface ModalRoot {
|
|
23
|
+
element: HTMLElement;
|
|
24
|
+
created: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getModalRoot(): ModalRoot {
|
|
28
|
+
const existingRoot = document.getElementById(MODAL_ROOT_ID);
|
|
29
|
+
if (existingRoot instanceof HTMLElement) {
|
|
30
|
+
return { element: existingRoot, created: false };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const createdRoot = document.createElement("div");
|
|
34
|
+
createdRoot.id = MODAL_ROOT_ID;
|
|
35
|
+
document.body.appendChild(createdRoot);
|
|
36
|
+
return { element: createdRoot, created: true };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getIconText(icon: ModalIcon): string {
|
|
40
|
+
if (icon === "info") {
|
|
41
|
+
return "i";
|
|
42
|
+
}
|
|
43
|
+
if (icon === "warning") {
|
|
44
|
+
return "!";
|
|
45
|
+
}
|
|
46
|
+
if (icon === "error") {
|
|
47
|
+
return "!";
|
|
48
|
+
}
|
|
49
|
+
return "";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getDefaultIcon(type: ModalType): ModalIcon {
|
|
53
|
+
if (type === "destructive") {
|
|
54
|
+
return "warning";
|
|
55
|
+
}
|
|
56
|
+
if (type === "error") {
|
|
57
|
+
return "error";
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getButtonLabels(type: ModalType): { primary: string; secondary: string | null } {
|
|
63
|
+
if (type === "confirmation") {
|
|
64
|
+
return { primary: "OK", secondary: "Cancel" };
|
|
65
|
+
}
|
|
66
|
+
if (type === "destructive") {
|
|
67
|
+
return { primary: "Delete", secondary: "Cancel" };
|
|
68
|
+
}
|
|
69
|
+
return { primary: "OK", secondary: null };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function getDefaultDismissal(type: ModalType): boolean {
|
|
73
|
+
return type !== "destructive";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function getInitialFocus(dialog: HTMLElement, primaryButton: HTMLButtonElement): HTMLElement {
|
|
77
|
+
const focusable = dialog.querySelector<HTMLElement>(
|
|
78
|
+
"button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])"
|
|
79
|
+
);
|
|
80
|
+
if (focusable !== null) {
|
|
81
|
+
return focusable;
|
|
82
|
+
}
|
|
83
|
+
return primaryButton;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function isInsideDialog(target: EventTarget | null, dialog: HTMLElement): boolean {
|
|
87
|
+
return target instanceof Node && dialog.contains(target);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
class ModalController {
|
|
91
|
+
private readonly type: ModalType;
|
|
92
|
+
private readonly content: string;
|
|
93
|
+
private readonly settings: ModalSettings;
|
|
94
|
+
private readonly root: HTMLElement;
|
|
95
|
+
private readonly ownsRoot: boolean;
|
|
96
|
+
private readonly previousFocus: HTMLElement | null;
|
|
97
|
+
private readonly dialog: HTMLElement;
|
|
98
|
+
private readonly backdrop: HTMLElement;
|
|
99
|
+
private readonly primaryButton: HTMLButtonElement;
|
|
100
|
+
private readonly secondaryButton: HTMLButtonElement | null;
|
|
101
|
+
private readonly keydownHandler: (event: KeyboardEvent) => void;
|
|
102
|
+
private timeoutId: number | null;
|
|
103
|
+
private settled: boolean;
|
|
104
|
+
private resolveResult: ((result: ModalResult) => void) | null;
|
|
105
|
+
|
|
106
|
+
public constructor(
|
|
107
|
+
type: ModalType,
|
|
108
|
+
content: string,
|
|
109
|
+
settings: ModalSettings
|
|
110
|
+
) {
|
|
111
|
+
this.type = type;
|
|
112
|
+
this.content = content;
|
|
113
|
+
this.settings = settings;
|
|
114
|
+
const modalRoot = getModalRoot();
|
|
115
|
+
this.root = modalRoot.element;
|
|
116
|
+
this.ownsRoot = modalRoot.created;
|
|
117
|
+
this.previousFocus = document.activeElement instanceof HTMLElement
|
|
118
|
+
? document.activeElement
|
|
119
|
+
: null;
|
|
120
|
+
this.dialog = this.createDialog();
|
|
121
|
+
this.backdrop = this.createBackdrop();
|
|
122
|
+
this.primaryButton = this.createPrimaryButton();
|
|
123
|
+
this.secondaryButton = this.createSecondaryButton();
|
|
124
|
+
this.addHeader();
|
|
125
|
+
this.addContent();
|
|
126
|
+
this.addFooter();
|
|
127
|
+
this.timeoutId = null;
|
|
128
|
+
this.settled = false;
|
|
129
|
+
this.resolveResult = null;
|
|
130
|
+
this.keydownHandler = (event: KeyboardEvent) => {
|
|
131
|
+
this.handleKeydown(event);
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
public open(): Promise<ModalResult> {
|
|
136
|
+
this.root.appendChild(this.backdrop);
|
|
137
|
+
this.backdrop.appendChild(this.dialog);
|
|
138
|
+
this.registerListeners();
|
|
139
|
+
|
|
140
|
+
const initialFocus = getInitialFocus(this.dialog, this.primaryButton);
|
|
141
|
+
initialFocus.focus();
|
|
142
|
+
|
|
143
|
+
if (this.settings.autoDismiss !== undefined && this.settings.autoDismiss > 0) {
|
|
144
|
+
this.timeoutId = window.setTimeout(() => {
|
|
145
|
+
this.finish("dismiss");
|
|
146
|
+
}, this.settings.autoDismiss);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return new Promise<ModalResult>((resolve) => {
|
|
150
|
+
this.resolveResult = resolve;
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private createDialog(): HTMLElement {
|
|
155
|
+
const dialog = document.createElement("div");
|
|
156
|
+
dialog.className = `buldng-modal ${this.type}`;
|
|
157
|
+
dialog.setAttribute("role", "dialog");
|
|
158
|
+
dialog.setAttribute("aria-modal", "true");
|
|
159
|
+
dialog.tabIndex = -1;
|
|
160
|
+
return dialog;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private createBackdrop(): HTMLElement {
|
|
164
|
+
const backdrop = document.createElement("div");
|
|
165
|
+
backdrop.className = "buldng-modal-backdrop";
|
|
166
|
+
backdrop.setAttribute("aria-hidden", "true");
|
|
167
|
+
return backdrop;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private createPrimaryButton(): HTMLButtonElement {
|
|
171
|
+
const button = document.createElement("button");
|
|
172
|
+
button.type = "button";
|
|
173
|
+
button.className = "buldng-modal-button buldng-modal-button-primary";
|
|
174
|
+
button.textContent = getButtonLabels(this.type).primary;
|
|
175
|
+
return button;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private createSecondaryButton(): HTMLButtonElement | null {
|
|
179
|
+
const labels = getButtonLabels(this.type);
|
|
180
|
+
if (labels.secondary === null) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const button = document.createElement("button");
|
|
185
|
+
button.type = "button";
|
|
186
|
+
button.className = "buldng-modal-button buldng-modal-button-secondary";
|
|
187
|
+
button.textContent = labels.secondary;
|
|
188
|
+
return button;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private addHeader(): void {
|
|
192
|
+
const hasTitle = this.settings.title !== undefined && this.settings.title.length > 0;
|
|
193
|
+
const icon = this.settings.icon === undefined
|
|
194
|
+
? getDefaultIcon(this.type)
|
|
195
|
+
: this.settings.icon;
|
|
196
|
+
const hasIcon = icon !== null;
|
|
197
|
+
|
|
198
|
+
if (!hasTitle && !hasIcon) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const header = document.createElement("div");
|
|
203
|
+
header.className = "buldng-modal-header";
|
|
204
|
+
|
|
205
|
+
if (hasIcon) {
|
|
206
|
+
const iconElement = document.createElement("span");
|
|
207
|
+
iconElement.className = "buldng-modal-icon";
|
|
208
|
+
iconElement.setAttribute("aria-hidden", "true");
|
|
209
|
+
iconElement.textContent = getIconText(icon);
|
|
210
|
+
header.appendChild(iconElement);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (hasTitle) {
|
|
214
|
+
const title = document.createElement("h2");
|
|
215
|
+
title.className = "buldng-modal-title";
|
|
216
|
+
title.textContent = this.settings.title as string;
|
|
217
|
+
modalId += 1;
|
|
218
|
+
const titleId = `buldng-modal-title-${modalId}`;
|
|
219
|
+
title.id = titleId;
|
|
220
|
+
this.dialog.setAttribute("aria-labelledby", titleId);
|
|
221
|
+
header.appendChild(title);
|
|
222
|
+
this.dialog.appendChild(header);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
this.dialog.appendChild(header);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private addContent(): void {
|
|
230
|
+
const content = document.createElement("div");
|
|
231
|
+
content.className = "buldng-modal-content";
|
|
232
|
+
content.innerHTML = this.content;
|
|
233
|
+
this.dialog.appendChild(content);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private addFooter(): void {
|
|
237
|
+
const footer = document.createElement("div");
|
|
238
|
+
footer.className = "buldng-modal-footer";
|
|
239
|
+
|
|
240
|
+
if (this.secondaryButton !== null) {
|
|
241
|
+
footer.appendChild(this.secondaryButton);
|
|
242
|
+
}
|
|
243
|
+
footer.appendChild(this.primaryButton);
|
|
244
|
+
this.dialog.appendChild(footer);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private registerListeners(): void {
|
|
248
|
+
this.primaryButton.addEventListener("click", () => {
|
|
249
|
+
this.finish("ok");
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
if (this.secondaryButton !== null) {
|
|
253
|
+
this.secondaryButton.addEventListener("click", () => {
|
|
254
|
+
this.finish("cancel");
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
this.backdrop.addEventListener("click", (event: MouseEvent) => {
|
|
259
|
+
if (isInsideDialog(event.target, this.dialog)) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
const dismissOnBackdrop = this.settings.dismissOnBackdrop === undefined
|
|
263
|
+
? getDefaultDismissal(this.type)
|
|
264
|
+
: this.settings.dismissOnBackdrop;
|
|
265
|
+
if (dismissOnBackdrop === true) {
|
|
266
|
+
this.finish("dismiss");
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
document.addEventListener("keydown", this.keydownHandler);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
private handleKeydown(event: KeyboardEvent): void {
|
|
274
|
+
if (event.key === "Escape") {
|
|
275
|
+
const dismissOnEsc = this.settings.dismissOnEsc === undefined
|
|
276
|
+
? getDefaultDismissal(this.type)
|
|
277
|
+
: this.settings.dismissOnEsc;
|
|
278
|
+
if (dismissOnEsc === true) {
|
|
279
|
+
event.preventDefault();
|
|
280
|
+
this.finish("dismiss");
|
|
281
|
+
}
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (event.key === "Tab") {
|
|
286
|
+
this.keepFocusInside(event);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private keepFocusInside(event: KeyboardEvent): void {
|
|
291
|
+
const focusable = Array.from(this.dialog.querySelectorAll<HTMLElement>(
|
|
292
|
+
"button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])"
|
|
293
|
+
));
|
|
294
|
+
if (focusable.length === 0) {
|
|
295
|
+
event.preventDefault();
|
|
296
|
+
this.dialog.focus();
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const first = focusable[0];
|
|
301
|
+
const last = focusable[focusable.length - 1];
|
|
302
|
+
const active = document.activeElement;
|
|
303
|
+
|
|
304
|
+
if (event.shiftKey === true && active === first) {
|
|
305
|
+
event.preventDefault();
|
|
306
|
+
last.focus();
|
|
307
|
+
} else if (event.shiftKey === false && active === last) {
|
|
308
|
+
event.preventDefault();
|
|
309
|
+
first.focus();
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
private finish(result: ModalResult): void {
|
|
314
|
+
if (this.settled === true) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
this.settled = true;
|
|
318
|
+
|
|
319
|
+
if (this.timeoutId !== null) {
|
|
320
|
+
window.clearTimeout(this.timeoutId);
|
|
321
|
+
this.timeoutId = null;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
document.removeEventListener("keydown", this.keydownHandler);
|
|
325
|
+
this.backdrop.remove();
|
|
326
|
+
if (this.ownsRoot === true && this.root.childElementCount === 0) {
|
|
327
|
+
this.root.remove();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (this.previousFocus !== null && document.contains(this.previousFocus)) {
|
|
331
|
+
this.previousFocus.focus();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (this.resolveResult !== null) {
|
|
335
|
+
this.resolveResult(result);
|
|
336
|
+
this.resolveResult = null;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export function showModal(
|
|
342
|
+
type: ModalType,
|
|
343
|
+
content: string,
|
|
344
|
+
settings: ModalSettings = {}
|
|
345
|
+
): Promise<ModalResult> {
|
|
346
|
+
const controller = new ModalController(type, content, settings);
|
|
347
|
+
return controller.open();
|
|
348
|
+
}
|
|
@@ -93,6 +93,12 @@ export function op_compile(config, children) {
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
const child = children[0];
|
|
96
|
+
const compileOptions = {
|
|
97
|
+
minify: config.flags?.minify ?? true,
|
|
98
|
+
sourcemap: config.flags?.sourcemap ?? true,
|
|
99
|
+
format: config.flags?.format ?? "esm",
|
|
100
|
+
target: config.flags?.target ?? "esnext",
|
|
101
|
+
};
|
|
96
102
|
|
|
97
103
|
// If child is a file WorkFile, use its absolute path directly
|
|
98
104
|
if (child.op === "file" && child.config.absPath) {
|
|
@@ -101,10 +107,7 @@ export function op_compile(config, children) {
|
|
|
101
107
|
const result = esbuild.buildSync({
|
|
102
108
|
entryPoints: [entry],
|
|
103
109
|
bundle: true,
|
|
104
|
-
|
|
105
|
-
sourcemap: true,
|
|
106
|
-
format: "esm",
|
|
107
|
-
target: "esnext",
|
|
110
|
+
...compileOptions,
|
|
108
111
|
write: false
|
|
109
112
|
});
|
|
110
113
|
|
|
@@ -125,10 +128,7 @@ export function op_compile(config, children) {
|
|
|
125
128
|
sourcefile: "input.ts"
|
|
126
129
|
},
|
|
127
130
|
bundle: true,
|
|
128
|
-
|
|
129
|
-
sourcemap: true,
|
|
130
|
-
format: "esm",
|
|
131
|
-
target: "esnext",
|
|
131
|
+
...compileOptions,
|
|
132
132
|
write: false
|
|
133
133
|
});
|
|
134
134
|
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// buldng-typescript.js
|
|
2
|
+
// typescript build support for buldng
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import { createRequire } from "module";
|
|
8
|
+
import { execSync } from "child_process";
|
|
9
|
+
|
|
10
|
+
function findTSCFromResolved(tsPath) {
|
|
11
|
+
// tsPath → /.../typescript/lib/typescript.js
|
|
12
|
+
let dir = path.dirname(tsPath); // /.../typescript/lib
|
|
13
|
+
dir = path.dirname(dir); // /.../typescript
|
|
14
|
+
|
|
15
|
+
while (true) {
|
|
16
|
+
const candidate = path.join(dir, ".bin", "tsc");
|
|
17
|
+
if (fs.existsSync(candidate)) {
|
|
18
|
+
return candidate;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const parent = path.dirname(dir);
|
|
22
|
+
if (parent === dir) break; // reached filesystem root
|
|
23
|
+
dir = parent;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function findGlobalTSC() {
|
|
30
|
+
try {
|
|
31
|
+
const globalTSC = execSync("which tsc").toString().trim();
|
|
32
|
+
return globalTSC || null;
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function typescriptCheck() {
|
|
39
|
+
const require = createRequire(import.meta.url);
|
|
40
|
+
|
|
41
|
+
let tsPath = null;
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
tsPath = require.resolve("typescript");
|
|
45
|
+
} catch {
|
|
46
|
+
tsPath = null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let tscPath = null;
|
|
50
|
+
|
|
51
|
+
if (tsPath) {
|
|
52
|
+
// Local / nested / linked TypeScript
|
|
53
|
+
tscPath = findTSCFromResolved(tsPath);
|
|
54
|
+
} else {
|
|
55
|
+
// Try global TypeScript
|
|
56
|
+
tscPath = findGlobalTSC();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!tscPath) {
|
|
60
|
+
console.log("TypeScript not found — skipping TS checks");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
console.log("Using TypeScript compiler:", tscPath);
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
execSync(`${tscPath} --noEmit`, { stdio: "inherit" });
|
|
68
|
+
} catch (err) {
|
|
69
|
+
// If tsc exists but cannot run → internal error
|
|
70
|
+
if (err.code === "ENOENT") {
|
|
71
|
+
throw new Error("Internal TypeScript error — 'tsc' binary exists but cannot be executed");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// If tsc runs but reports TS errors → build failure
|
|
75
|
+
throw new Error("TypeScript check failed");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// buldng-web.ts
|
|
2
|
+
// Runtime-only listener installer for BULDR.
|
|
3
|
+
// This module contains NO production logic and NO module-level state.
|
|
4
|
+
// It is safe to be bundled multiple times.
|
|
5
|
+
|
|
6
|
+
export { showModal } from "./buldng-modal";
|
|
7
|
+
export type {
|
|
8
|
+
ModalIcon,
|
|
9
|
+
ModalResult,
|
|
10
|
+
ModalSettings,
|
|
11
|
+
ModalType,
|
|
12
|
+
} from "./buldng-modal";
|
|
13
|
+
|
|
14
|
+
export function setErrorPage(page: string) {
|
|
15
|
+
if (typeof page !== "string" || !page.length) {
|
|
16
|
+
throw new Error("setErrorPage: page must be a non-empty string");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Capture `page` in closure — NOT stored on window
|
|
20
|
+
window.addEventListener("error", (event) => {
|
|
21
|
+
window.__error_handler?.(event, page);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
window.addEventListener("unhandledrejection", (event) => {
|
|
25
|
+
window.__error_handler?.(event, page);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
console.log(`BULDR: Error handler listener installed, redirect ${page}`);
|
|
29
|
+
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ------------------------------------------------------------
|
|
33
|
+
// ERROR PAGE SUPPORT — moved from layout-err.ts
|
|
34
|
+
// ------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
import type { ErrorPayload } from "./layout-err"; // or wherever you export it
|
|
37
|
+
|
|
38
|
+
export function readErrorPayload(id: string | null): ErrorPayload | null {
|
|
39
|
+
if (!id) return null;
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const raw = localStorage.getItem(storageKey(id));
|
|
43
|
+
if (!raw) return null;
|
|
44
|
+
|
|
45
|
+
const payload: unknown = JSON.parse(raw);
|
|
46
|
+
return isErrorPayload(payload) ? payload : null;
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function consumeErrorPayload(id: string | null): ErrorPayload | null {
|
|
53
|
+
const payload = readErrorPayload(id);
|
|
54
|
+
if (id) {
|
|
55
|
+
try {
|
|
56
|
+
localStorage.removeItem(storageKey(id));
|
|
57
|
+
} catch {}
|
|
58
|
+
}
|
|
59
|
+
return payload;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function renderErrorPayload(
|
|
63
|
+
elementId: string,
|
|
64
|
+
payload: ErrorPayload | null
|
|
65
|
+
): ErrorPayload | null {
|
|
66
|
+
const container = document.getElementById(elementId);
|
|
67
|
+
if (!container) return null;
|
|
68
|
+
|
|
69
|
+
container.replaceChildren();
|
|
70
|
+
|
|
71
|
+
if (!payload) {
|
|
72
|
+
const empty = document.createElement("p");
|
|
73
|
+
empty.textContent = "Error details are no longer available.";
|
|
74
|
+
container.appendChild(empty);
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const details: Array<[string, string]> = [
|
|
79
|
+
["Type", payload.type],
|
|
80
|
+
["Message", payload.message],
|
|
81
|
+
["Time", payload.time],
|
|
82
|
+
["Page", payload.url],
|
|
83
|
+
["User agent", payload.userAgent],
|
|
84
|
+
["Platform", payload.platform],
|
|
85
|
+
["Language", payload.language],
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
const list = document.createElement("dl");
|
|
89
|
+
|
|
90
|
+
for (const [label, value] of details) {
|
|
91
|
+
const term = document.createElement("dt");
|
|
92
|
+
term.textContent = label;
|
|
93
|
+
|
|
94
|
+
const description = document.createElement("dd");
|
|
95
|
+
description.textContent = value;
|
|
96
|
+
|
|
97
|
+
list.append(term, description);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (payload.stack) {
|
|
101
|
+
const stackLabel = document.createElement("h3");
|
|
102
|
+
stackLabel.textContent = "Stack";
|
|
103
|
+
|
|
104
|
+
const stack = document.createElement("pre");
|
|
105
|
+
stack.textContent = payload.stack;
|
|
106
|
+
|
|
107
|
+
list.append(stackLabel, stack);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
container.appendChild(list);
|
|
111
|
+
return payload;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function getErrorId(): string | null {
|
|
115
|
+
return new URLSearchParams(location.search).get("errorId");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ------------------------------------------------------------
|
|
119
|
+
// TYPE GUARDS & UTILITIES — also needed by error page
|
|
120
|
+
// ------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
function isErrorPayload(value: unknown): value is ErrorPayload {
|
|
123
|
+
if (!value || typeof value !== "object") return false;
|
|
124
|
+
|
|
125
|
+
const p = value as Partial<ErrorPayload>;
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
typeof p.id === "string" &&
|
|
129
|
+
(p.type === "error" || p.type === "unhandledrejection") &&
|
|
130
|
+
typeof p.message === "string" &&
|
|
131
|
+
typeof p.stack === "string" &&
|
|
132
|
+
typeof p.time === "string" &&
|
|
133
|
+
typeof p.url === "string" &&
|
|
134
|
+
typeof p.userAgent === "string" &&
|
|
135
|
+
typeof p.platform === "string" &&
|
|
136
|
+
typeof p.language === "string"
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function storageKey(id: string) {
|
|
141
|
+
return `buldng:error:${id}`;
|
|
142
|
+
}
|