@webstir-io/webstir 0.1.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 +69 -0
- package/assets/features/client_nav/client_nav.ts +469 -0
- package/assets/features/content_nav/content_nav.css +170 -0
- package/assets/features/content_nav/content_nav.ts +358 -0
- package/assets/features/router/router-types.ts +6 -0
- package/assets/features/router/router.ts +118 -0
- package/assets/features/search/search.css +204 -0
- package/assets/features/search/search.ts +627 -0
- package/assets/templates/api/src/backend/index.ts +13 -0
- package/assets/templates/api/src/backend/tsconfig.json +15 -0
- package/assets/templates/api/src/shared/router-types.ts +23 -0
- package/assets/templates/api/src/shared/tsconfig.json +10 -0
- package/assets/templates/api/src/shared/types/index.ts +4 -0
- package/assets/templates/full/src/backend/index.ts +13 -0
- package/assets/templates/full/src/backend/tsconfig.json +15 -0
- package/assets/templates/full/src/frontend/app/app.css +65 -0
- package/assets/templates/full/src/frontend/app/app.html +13 -0
- package/assets/templates/full/src/frontend/app/app.ts +188 -0
- package/assets/templates/full/src/frontend/app/error.ts +127 -0
- package/assets/templates/full/src/frontend/app/hmr.js +355 -0
- package/assets/templates/full/src/frontend/app/navigation.ts +8 -0
- package/assets/templates/full/src/frontend/app/refresh.js +114 -0
- package/assets/templates/full/src/frontend/app/router.ts +126 -0
- package/assets/templates/full/src/frontend/app/styles/base.css +2 -0
- package/assets/templates/full/src/frontend/app/styles/reset.css +48 -0
- package/assets/templates/full/src/frontend/pages/home/index.css +21 -0
- package/assets/templates/full/src/frontend/pages/home/index.html +10 -0
- package/assets/templates/full/src/frontend/pages/home/index.ts +18 -0
- package/assets/templates/full/src/frontend/pages/home/tests/home.test.ts +21 -0
- package/assets/templates/full/src/frontend/tsconfig.json +20 -0
- package/assets/templates/full/src/shared/router-types.ts +23 -0
- package/assets/templates/full/src/shared/tsconfig.json +10 -0
- package/assets/templates/full/src/shared/types/index.ts +4 -0
- package/assets/templates/shared/Errors.404.html +23 -0
- package/assets/templates/shared/Errors.500.html +23 -0
- package/assets/templates/shared/Errors.default.html +23 -0
- package/assets/templates/shared/types/global.d.ts +32 -0
- package/assets/templates/shared/types.global.d.ts +32 -0
- package/assets/templates/spa/src/frontend/app/app.css +65 -0
- package/assets/templates/spa/src/frontend/app/app.html +13 -0
- package/assets/templates/spa/src/frontend/app/app.ts +188 -0
- package/assets/templates/spa/src/frontend/app/error.ts +127 -0
- package/assets/templates/spa/src/frontend/app/hmr.js +355 -0
- package/assets/templates/spa/src/frontend/app/navigation.ts +8 -0
- package/assets/templates/spa/src/frontend/app/refresh.js +114 -0
- package/assets/templates/spa/src/frontend/app/router.ts +126 -0
- package/assets/templates/spa/src/frontend/app/styles/base.css +2 -0
- package/assets/templates/spa/src/frontend/app/styles/reset.css +48 -0
- package/assets/templates/spa/src/frontend/pages/home/index.css +21 -0
- package/assets/templates/spa/src/frontend/pages/home/index.html +10 -0
- package/assets/templates/spa/src/frontend/pages/home/index.ts +18 -0
- package/assets/templates/spa/src/frontend/pages/home/tests/home.test.ts +21 -0
- package/assets/templates/spa/src/frontend/tsconfig.json +20 -0
- package/assets/templates/spa/src/shared/router-types.ts +23 -0
- package/assets/templates/spa/src/shared/tsconfig.json +10 -0
- package/assets/templates/spa/src/shared/types/index.ts +4 -0
- package/assets/templates/ssg/src/frontend/app/app.css +12 -0
- package/assets/templates/ssg/src/frontend/app/app.html +43 -0
- package/assets/templates/ssg/src/frontend/app/app.ts +190 -0
- package/assets/templates/ssg/src/frontend/app/error.ts +127 -0
- package/assets/templates/ssg/src/frontend/app/hmr.js +370 -0
- package/assets/templates/ssg/src/frontend/app/refresh.js +163 -0
- package/assets/templates/ssg/src/frontend/app/scripts/components/drawer.ts +183 -0
- package/assets/templates/ssg/src/frontend/app/scripts/components/menu.ts +75 -0
- package/assets/templates/ssg/src/frontend/app/styles/base.css +77 -0
- package/assets/templates/ssg/src/frontend/app/styles/components/buttons.css +108 -0
- package/assets/templates/ssg/src/frontend/app/styles/components/drawer.css +12 -0
- package/assets/templates/ssg/src/frontend/app/styles/components/header.css +164 -0
- package/assets/templates/ssg/src/frontend/app/styles/components/markdown.css +25 -0
- package/assets/templates/ssg/src/frontend/app/styles/layout.css +41 -0
- package/assets/templates/ssg/src/frontend/app/styles/reset.css +56 -0
- package/assets/templates/ssg/src/frontend/app/styles/tokens.css +72 -0
- package/assets/templates/ssg/src/frontend/app/styles/utilities.css +14 -0
- package/assets/templates/ssg/src/frontend/content/_sidebar.json +14 -0
- package/assets/templates/ssg/src/frontend/content/content-pipeline.md +82 -0
- package/assets/templates/ssg/src/frontend/content/css-playbook.md +68 -0
- package/assets/templates/ssg/src/frontend/content/hosting.md +48 -0
- package/assets/templates/ssg/src/frontend/pages/about/index.css +33 -0
- package/assets/templates/ssg/src/frontend/pages/about/index.html +60 -0
- package/assets/templates/ssg/src/frontend/pages/docs/index.css +505 -0
- package/assets/templates/ssg/src/frontend/pages/docs/index.html +52 -0
- package/assets/templates/ssg/src/frontend/pages/docs/index.ts +495 -0
- package/assets/templates/ssg/src/frontend/pages/home/index.css +91 -0
- package/assets/templates/ssg/src/frontend/pages/home/index.html +38 -0
- package/assets/templates/ssg/src/frontend/pages/home/tests/home.test.ts +24 -0
- package/assets/templates/ssg/src/frontend/tsconfig.json +13 -0
- package/package.json +41 -0
- package/scripts/pack-standalone.mjs +127 -0
- package/scripts/sync-assets.mjs +87 -0
- package/src/add-backend.ts +164 -0
- package/src/add.ts +112 -0
- package/src/api-watch.ts +84 -0
- package/src/backend-inspect.ts +45 -0
- package/src/backend-runtime.ts +286 -0
- package/src/build-plan.ts +12 -0
- package/src/build.ts +10 -0
- package/src/cli.ts +569 -0
- package/src/compile-tests.ts +61 -0
- package/src/dev-server.ts +393 -0
- package/src/enable-assets.ts +196 -0
- package/src/enable.ts +477 -0
- package/src/execute.ts +85 -0
- package/src/format.ts +254 -0
- package/src/frontend-watch.ts +145 -0
- package/src/full-watch.ts +80 -0
- package/src/index.ts +20 -0
- package/src/init-assets.ts +96 -0
- package/src/init.ts +339 -0
- package/src/paths.ts +26 -0
- package/src/providers.ts +88 -0
- package/src/publish.ts +8 -0
- package/src/refresh.ts +56 -0
- package/src/repair.ts +414 -0
- package/src/runtime.ts +48 -0
- package/src/smoke.ts +161 -0
- package/src/stop-signal.ts +26 -0
- package/src/test.ts +215 -0
- package/src/types.ts +29 -0
- package/src/watch-daemon-client.ts +171 -0
- package/src/watch-events.ts +195 -0
- package/src/watch.ts +66 -0
- package/src/workspace-watcher.ts +251 -0
- package/src/workspace.ts +55 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
|
|
3
|
+
const PORT = Number(process.env.PORT || 4321);
|
|
4
|
+
const server = http.createServer((req, res) => {
|
|
5
|
+
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
6
|
+
res.end('API server running');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
server.listen(PORT, '0.0.0.0', () => {
|
|
10
|
+
console.log(`API server running at http://localhost:${PORT}`);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export default server;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../base.tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"target": "ES2022",
|
|
5
|
+
"lib": ["ES2022"],
|
|
6
|
+
"outDir": "../../build/backend",
|
|
7
|
+
"rootDir": ".",
|
|
8
|
+
"baseUrl": ".",
|
|
9
|
+
"incremental": true,
|
|
10
|
+
"tsBuildInfoFile": "../../build/backend/.tsbuildinfo"
|
|
11
|
+
},
|
|
12
|
+
"include": ["**/*.ts", "../../types/**/*.d.ts"],
|
|
13
|
+
"exclude": ["node_modules", "../../build", "../../dist"],
|
|
14
|
+
"references": []
|
|
15
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/* Main App Stylesheet */
|
|
2
|
+
@import "./styles/reset.css";
|
|
3
|
+
|
|
4
|
+
/* Base body styles */
|
|
5
|
+
html, body {
|
|
6
|
+
margin: 0;
|
|
7
|
+
padding: 0;
|
|
8
|
+
height: 100%;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
body {
|
|
12
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
|
|
13
|
+
Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', Arial,
|
|
14
|
+
sans-serif;
|
|
15
|
+
line-height: 1.5;
|
|
16
|
+
color: #1f2937;
|
|
17
|
+
background: #f8fafc;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
main {
|
|
21
|
+
display: block;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/* Layout helpers */
|
|
25
|
+
.container {
|
|
26
|
+
width: 100%;
|
|
27
|
+
max-width: 1200px;
|
|
28
|
+
margin-left: auto;
|
|
29
|
+
margin-right: auto;
|
|
30
|
+
padding-left: 1rem;
|
|
31
|
+
padding-right: 1rem;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* Utility classes */
|
|
35
|
+
.sr-only {
|
|
36
|
+
position: absolute;
|
|
37
|
+
width: 1px;
|
|
38
|
+
height: 1px;
|
|
39
|
+
padding: 0;
|
|
40
|
+
margin: -1px;
|
|
41
|
+
overflow: hidden;
|
|
42
|
+
clip: rect(0, 0, 0, 0);
|
|
43
|
+
white-space: nowrap; /* added line */
|
|
44
|
+
border: 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* Basic typography */
|
|
48
|
+
h1, h2, h3, h4, h5, h6 {
|
|
49
|
+
color: #111827;
|
|
50
|
+
line-height: 1.25;
|
|
51
|
+
margin: 0 0 0.5rem 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
p {
|
|
55
|
+
margin: 0 0 1rem 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
a {
|
|
59
|
+
color: #2563eb;
|
|
60
|
+
text-decoration: none;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
a:hover {
|
|
64
|
+
text-decoration: underline;
|
|
65
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<meta name="description" content="Starter description for your Webstir app. Update it to summarize this page for search results.">
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<main> </main>
|
|
10
|
+
<script type="module" src="/hmr.js"></script>
|
|
11
|
+
<script src="/refresh.js" async></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// Global app initialization
|
|
2
|
+
|
|
3
|
+
type HotAsset = {
|
|
4
|
+
type: 'js' | 'css';
|
|
5
|
+
url: string;
|
|
6
|
+
relativePath: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
type HotModuleContext = {
|
|
10
|
+
changedFile: string | null;
|
|
11
|
+
modules: ReadonlyArray<HotAsset>;
|
|
12
|
+
styles: ReadonlyArray<HotAsset>;
|
|
13
|
+
cacheBuster: string;
|
|
14
|
+
timestamp: number;
|
|
15
|
+
asset?: HotAsset;
|
|
16
|
+
previousExports?: unknown;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type HotModuleHandlers = {
|
|
20
|
+
accept?: (moduleExports: unknown, context: HotModuleContext) => boolean | Promise<boolean>;
|
|
21
|
+
dispose?: (context: HotModuleContext) => void | Promise<void>;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type HotModuleRecord = HotModuleHandlers & {
|
|
25
|
+
currentExports?: unknown;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
declare global {
|
|
29
|
+
interface Window {
|
|
30
|
+
__webstirEventSource?: EventSource;
|
|
31
|
+
__webstirSetDevStatus?: (status: string, message?: string) => void;
|
|
32
|
+
__webstirOnHmrFallback?: (info: { reason?: string; payload?: unknown; details?: unknown }) => void;
|
|
33
|
+
__webstirRegisterHotModule?: (moduleId: string, handlers: HotModuleHandlers) => void;
|
|
34
|
+
__webstirDispose?: (asset: HotAsset | undefined, context: HotModuleContext) => boolean | Promise<boolean>;
|
|
35
|
+
__webstirAccept?: (moduleExports: unknown, context: HotModuleContext) => boolean | Promise<boolean>;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const hotModuleRegistry = new Map<string, HotModuleRecord>();
|
|
40
|
+
|
|
41
|
+
function ensureRecord(moduleId: string): HotModuleRecord {
|
|
42
|
+
const existing = hotModuleRegistry.get(moduleId);
|
|
43
|
+
if (existing) {
|
|
44
|
+
return existing;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const created: HotModuleRecord = {};
|
|
48
|
+
hotModuleRegistry.set(moduleId, created);
|
|
49
|
+
return created;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function normalizeModuleId(candidate?: string | null): string | null {
|
|
53
|
+
if (!candidate) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const url = new URL(candidate, window.location.origin);
|
|
59
|
+
return url.pathname;
|
|
60
|
+
} catch {
|
|
61
|
+
const index = candidate.indexOf('?');
|
|
62
|
+
return index === -1 ? candidate : candidate.slice(0, index);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isPromise<T = unknown>(value: unknown): value is Promise<T> {
|
|
67
|
+
return !!value && typeof (value as PromiseLike<T>).then === 'function';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function withHistoryContext(context: HotModuleContext, record: HotModuleRecord): HotModuleContext {
|
|
71
|
+
if (!record.currentExports) {
|
|
72
|
+
return context;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
...context,
|
|
77
|
+
previousExports: record.currentExports
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function evaluateHandlerResult(result: unknown): Promise<boolean> {
|
|
82
|
+
if (isPromise(result)) {
|
|
83
|
+
const resolved = await result;
|
|
84
|
+
return resolved !== false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return result !== false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Lazy-load error handler on first error
|
|
91
|
+
let errorHandlerLoaded = false;
|
|
92
|
+
|
|
93
|
+
async function loadErrorHandler() {
|
|
94
|
+
if (errorHandlerLoaded) return;
|
|
95
|
+
errorHandlerLoaded = true;
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const { install } = await import('./error');
|
|
99
|
+
install();
|
|
100
|
+
} catch (e) {
|
|
101
|
+
console.error('Failed to load error handler:', e);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function registerHotModule(moduleId: string, handlers: HotModuleHandlers): void {
|
|
106
|
+
const normalized = normalizeModuleId(moduleId);
|
|
107
|
+
if (!normalized) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const record = ensureRecord(normalized);
|
|
112
|
+
record.accept = handlers.accept;
|
|
113
|
+
record.dispose = handlers.dispose;
|
|
114
|
+
hotModuleRegistry.set(normalized, record);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
window.__webstirRegisterHotModule = registerHotModule;
|
|
118
|
+
|
|
119
|
+
window.__webstirDispose = async (asset, context) => {
|
|
120
|
+
const moduleId = normalizeModuleId(asset?.url ?? asset?.relativePath);
|
|
121
|
+
if (!moduleId) {
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const record = hotModuleRegistry.get(moduleId);
|
|
126
|
+
if (!record) {
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!record.dispose) {
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const contextWithHistory = withHistoryContext(context, record);
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const result = record.dispose(contextWithHistory);
|
|
138
|
+
if (isPromise(result)) {
|
|
139
|
+
await result;
|
|
140
|
+
}
|
|
141
|
+
return true;
|
|
142
|
+
} catch (error) {
|
|
143
|
+
console.error(`[webstir-hmr] Dispose handler failed for ${moduleId}.`, error);
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
window.__webstirAccept = async (moduleExports, context) => {
|
|
149
|
+
const moduleId = normalizeModuleId(context?.asset?.url ?? context?.asset?.relativePath);
|
|
150
|
+
if (!moduleId) {
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const record = ensureRecord(moduleId);
|
|
155
|
+
const contextWithHistory = withHistoryContext(context, record);
|
|
156
|
+
|
|
157
|
+
let accepted = true;
|
|
158
|
+
|
|
159
|
+
if (record.accept) {
|
|
160
|
+
try {
|
|
161
|
+
accepted = await evaluateHandlerResult(record.accept(moduleExports, contextWithHistory));
|
|
162
|
+
} catch (error) {
|
|
163
|
+
console.error(`[webstir-hmr] Accept handler failed for ${moduleId}.`, error);
|
|
164
|
+
accepted = false;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (accepted) {
|
|
169
|
+
record.currentExports = moduleExports;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
hotModuleRegistry.set(moduleId, record);
|
|
173
|
+
return accepted;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// Set up error listeners that will dynamically import the error handler
|
|
177
|
+
window.addEventListener('error', async () => {
|
|
178
|
+
await loadErrorHandler();
|
|
179
|
+
// The installed handler will catch subsequent errors
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
window.addEventListener('unhandledrejection', async () => {
|
|
183
|
+
await loadErrorHandler();
|
|
184
|
+
// The installed handler will catch subsequent rejections
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// Export for use by pages if needed
|
|
188
|
+
export { loadErrorHandler };
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// Global client-side error reporter (TypeScript)
|
|
2
|
+
let lastSentAt = 0;
|
|
3
|
+
let sentCount = 0;
|
|
4
|
+
const MAX_PER_SESSION = 20;
|
|
5
|
+
const MIN_INTERVAL_MS = 1000;
|
|
6
|
+
const DEDUPE_WINDOW_MS = 60_000; // 60s
|
|
7
|
+
const recent = new Map<string, number>(); // fingerprint -> timestamp
|
|
8
|
+
|
|
9
|
+
function cid(): string {
|
|
10
|
+
const w = window as any;
|
|
11
|
+
if (!w.__WEBSTIR_CID__) {
|
|
12
|
+
w.__WEBSTIR_CID__ = 'c-' + Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
13
|
+
}
|
|
14
|
+
return String(w.__WEBSTIR_CID__);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type Payload = {
|
|
18
|
+
type: 'error' | 'unhandledrejection';
|
|
19
|
+
message: string;
|
|
20
|
+
stack: string;
|
|
21
|
+
filename: string;
|
|
22
|
+
lineno: number;
|
|
23
|
+
colno: number;
|
|
24
|
+
pageUrl: string;
|
|
25
|
+
userAgent: string;
|
|
26
|
+
timestamp: string;
|
|
27
|
+
correlationId: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function toPayload(e: ErrorEvent | PromiseRejectionEvent): Payload {
|
|
31
|
+
const isRejection = !!e && e.type === 'unhandledrejection';
|
|
32
|
+
const reason: any = isRejection ? ((e as PromiseRejectionEvent).reason || {}) : {};
|
|
33
|
+
const err: any = (e as ErrorEvent)?.error || reason || {};
|
|
34
|
+
const message = (e as ErrorEvent)?.message || reason.message || err?.message || 'Unknown error';
|
|
35
|
+
const stack = String(err?.stack || reason.stack || '');
|
|
36
|
+
const filename = String((e as ErrorEvent)?.filename || '');
|
|
37
|
+
const lineno = Number((e as ErrorEvent)?.lineno || 0);
|
|
38
|
+
const colno = Number((e as ErrorEvent)?.colno || 0);
|
|
39
|
+
return {
|
|
40
|
+
type: isRejection ? 'unhandledrejection' : 'error',
|
|
41
|
+
message: String(message || ''),
|
|
42
|
+
stack,
|
|
43
|
+
filename,
|
|
44
|
+
lineno,
|
|
45
|
+
colno,
|
|
46
|
+
pageUrl: String(location.href),
|
|
47
|
+
userAgent: String(navigator.userAgent || ''),
|
|
48
|
+
timestamp: new Date().toISOString(),
|
|
49
|
+
correlationId: cid(),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Simple 32-bit FNV-1a
|
|
54
|
+
function hash(str: string): string {
|
|
55
|
+
let h = 2166136261 >>> 0;
|
|
56
|
+
for (let i = 0; i < str.length; i++) {
|
|
57
|
+
h ^= str.charCodeAt(i);
|
|
58
|
+
h = (h + (h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24)) >>> 0;
|
|
59
|
+
}
|
|
60
|
+
return h.toString(36);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function fingerprint(p: Payload): string {
|
|
64
|
+
return [
|
|
65
|
+
p.type || '',
|
|
66
|
+
p.message || '',
|
|
67
|
+
`${p.filename || ''}:${p.lineno || 0}:${p.colno || 0}`,
|
|
68
|
+
hash(p.stack || ''),
|
|
69
|
+
].join('|');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function shouldSend(p: Payload): boolean {
|
|
73
|
+
const now = Date.now();
|
|
74
|
+
if (now - lastSentAt < MIN_INTERVAL_MS) return false;
|
|
75
|
+
if (sentCount >= MAX_PER_SESSION) return false;
|
|
76
|
+
|
|
77
|
+
const fp = fingerprint(p);
|
|
78
|
+
const last = recent.get(fp) || 0;
|
|
79
|
+
|
|
80
|
+
// prune old entries opportunistically
|
|
81
|
+
if (recent.size > 100) {
|
|
82
|
+
recent.forEach((ts, k) => {
|
|
83
|
+
if (now - ts > DEDUPE_WINDOW_MS) recent.delete(k);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (now - last < DEDUPE_WINDOW_MS) return false;
|
|
88
|
+
recent.set(fp, now);
|
|
89
|
+
lastSentAt = now;
|
|
90
|
+
sentCount++;
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function report(e: ErrorEvent | PromiseRejectionEvent): void {
|
|
95
|
+
try {
|
|
96
|
+
const p = toPayload(e);
|
|
97
|
+
if (!shouldSend(p)) return;
|
|
98
|
+
const payload = JSON.stringify(p);
|
|
99
|
+
if (navigator.sendBeacon) {
|
|
100
|
+
const blob = new Blob([payload], { type: 'application/json' });
|
|
101
|
+
navigator.sendBeacon('/client-errors', blob);
|
|
102
|
+
} else {
|
|
103
|
+
fetch('/client-errors', {
|
|
104
|
+
method: 'POST',
|
|
105
|
+
headers: { 'Content-Type': 'application/json', 'X-Correlation-ID': cid() },
|
|
106
|
+
body: payload,
|
|
107
|
+
keepalive: true,
|
|
108
|
+
}).catch(() => { /* ignore */ });
|
|
109
|
+
}
|
|
110
|
+
} catch {
|
|
111
|
+
// ignore
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function install(): void {
|
|
116
|
+
const w = window as any;
|
|
117
|
+
if (w.__WEBSTIR_ERROR_HANDLER_INSTALLED__) return;
|
|
118
|
+
w.__WEBSTIR_ERROR_HANDLER_INSTALLED__ = true;
|
|
119
|
+
|
|
120
|
+
window.addEventListener('error', (e) => {
|
|
121
|
+
try { report(e); } catch { /* ignore */ }
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
window.addEventListener('unhandledrejection', (e) => {
|
|
125
|
+
try { report(e); } catch { /* ignore */ }
|
|
126
|
+
});
|
|
127
|
+
}
|