@ossido-labs/ossido-ui 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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +19 -0
  3. package/dist/esm/components/BaseStyles.d.ts +8 -0
  4. package/dist/esm/components/BaseStyles.js +17 -0
  5. package/dist/esm/components/BaseStyles.js.map +1 -0
  6. package/dist/esm/components/DefaultError.d.ts +9 -0
  7. package/dist/esm/components/DefaultError.js +30 -0
  8. package/dist/esm/components/DefaultError.js.map +1 -0
  9. package/dist/esm/components/DefaultLoading.d.ts +6 -0
  10. package/dist/esm/components/DefaultLoading.js +13 -0
  11. package/dist/esm/components/DefaultLoading.js.map +1 -0
  12. package/dist/esm/components/DefaultScreen.d.ts +19 -0
  13. package/dist/esm/components/DefaultScreen.js +96 -0
  14. package/dist/esm/components/DefaultScreen.js.map +1 -0
  15. package/dist/esm/components/DevBuildErrorContent.d.ts +11 -0
  16. package/dist/esm/components/DevBuildErrorContent.js +56 -0
  17. package/dist/esm/components/DevBuildErrorContent.js.map +1 -0
  18. package/dist/esm/components/DevErrorContent.d.ts +4 -0
  19. package/dist/esm/components/DevErrorContent.js +192 -0
  20. package/dist/esm/components/DevErrorContent.js.map +1 -0
  21. package/dist/esm/components/DevErrorOverlay.d.ts +13 -0
  22. package/dist/esm/components/DevErrorOverlay.js +53 -0
  23. package/dist/esm/components/DevErrorOverlay.js.map +1 -0
  24. package/dist/esm/components/DevErrorOverlayHost.d.ts +2 -0
  25. package/dist/esm/components/DevErrorOverlayHost.js +295 -0
  26. package/dist/esm/components/DevErrorOverlayHost.js.map +1 -0
  27. package/dist/esm/components/DevErrorReporter.d.ts +13 -0
  28. package/dist/esm/components/DevErrorReporter.js +28 -0
  29. package/dist/esm/components/DevErrorReporter.js.map +1 -0
  30. package/dist/esm/components/base.js +6 -0
  31. package/dist/esm/components/base.js.map +1 -0
  32. package/dist/esm/components/devErrorSource.d.ts +96 -0
  33. package/dist/esm/components/devErrorSource.js +321 -0
  34. package/dist/esm/components/devErrorSource.js.map +1 -0
  35. package/dist/esm/components/devErrorStore.d.ts +105 -0
  36. package/dist/esm/components/devErrorStore.js +186 -0
  37. package/dist/esm/components/devErrorStore.js.map +1 -0
  38. package/dist/esm/components/devErrorStyles.d.ts +6 -0
  39. package/dist/esm/components/devErrorStyles.js +426 -0
  40. package/dist/esm/components/devErrorStyles.js.map +1 -0
  41. package/dist/esm/index.d.ts +11 -0
  42. package/dist/esm/index.js +11 -0
  43. package/dist/esm/types.d.ts +26 -0
  44. package/dist/esm/vite/error-overlay.d.ts +32 -0
  45. package/dist/esm/vite/error-overlay.js +49 -0
  46. package/dist/esm/vite/error-overlay.js.map +1 -0
  47. package/package.json +81 -0
@@ -0,0 +1,321 @@
1
+ //#region src/components/devErrorSource.ts
2
+ const SOURCE_CONTEXT = 5;
3
+ /** Map a source file extension to the Shiki grammar (language) to load. */
4
+ function langForFile(file) {
5
+ switch (extensionOf(file)) {
6
+ case ".tsx":
7
+ case ".jsx": return "tsx";
8
+ case ".ts": return "typescript";
9
+ case ".rs": return "rust";
10
+ default: return "javascript";
11
+ }
12
+ }
13
+ const V8_LINE = /^(?:(.*?)\s+)?\(?([^()]+):(\d+):(\d+)\)?$/;
14
+ const SPIDERMONKEY_LINE = /^(.*?)@(.+):(\d+):(\d+)$/;
15
+ const RUST_FRAME = /^\d+:\s+(.+)$/;
16
+ function parseStack(stack) {
17
+ if (!stack) return [];
18
+ const frames = [];
19
+ let pendingFn;
20
+ const flushPending = () => {
21
+ if (pendingFn !== void 0) {
22
+ frames.push({ fn: pendingFn });
23
+ pendingFn = void 0;
24
+ }
25
+ };
26
+ for (const rawLine of stack.split("\n")) {
27
+ const line = rawLine.trim();
28
+ if (!line) continue;
29
+ const rustFrame = line.match(RUST_FRAME);
30
+ if (rustFrame) {
31
+ flushPending();
32
+ pendingFn = rustFrame[1];
33
+ continue;
34
+ }
35
+ if (line.startsWith("at ")) {
36
+ const match = line.slice(3).trim().match(V8_LINE);
37
+ if (match) frames.push({
38
+ fn: pendingFn ?? (match[1]?.trim() || void 0),
39
+ file: match[2],
40
+ line: Number(match[3]),
41
+ column: Number(match[4])
42
+ });
43
+ else frames.push({ fn: pendingFn ?? line.slice(3).trim() });
44
+ pendingFn = void 0;
45
+ continue;
46
+ }
47
+ const match = line.match(SPIDERMONKEY_LINE);
48
+ if (match) {
49
+ flushPending();
50
+ frames.push({
51
+ fn: match[1] || void 0,
52
+ file: match[2],
53
+ line: Number(match[3]),
54
+ column: Number(match[4])
55
+ });
56
+ }
57
+ }
58
+ flushPending();
59
+ return frames;
60
+ }
61
+ /**
62
+ * The most specific label for an error: its constructor (class) name when it is
63
+ * more descriptive than the generic `Error`. A subclass like `TestError` keeps
64
+ * `error.name === 'Error'` unless it explicitly sets `name`, but its
65
+ * constructor name is still `TestError`.
66
+ */
67
+ function errorLabel(error) {
68
+ const constructorName = error.constructor?.name;
69
+ if (constructorName && constructorName !== "Error" && constructorName !== "Object") return constructorName;
70
+ return error.name || "Error";
71
+ }
72
+ function isApplicationFile(file) {
73
+ if (!file) return false;
74
+ return !file.includes("node_modules") && !file.includes("/deps/") && !file.startsWith("node:") && !file.includes("/rustc/") && !file.includes("/rustlib/") && !file.includes("/.cargo/registry");
75
+ }
76
+ /** Collapse `foo/../` segments (e.g. Rust's `.ossido/../src/...` → `src/...`). */
77
+ function collapseParentDirs(path) {
78
+ const out = [];
79
+ for (const part of path.split("/")) {
80
+ const prev = out[out.length - 1];
81
+ if (part === ".." && prev !== void 0 && prev !== "" && prev !== "..") out.pop();
82
+ else out.push(part);
83
+ }
84
+ return out.join("/");
85
+ }
86
+ /** Strip origin and query for a readable path. */
87
+ function prettyPath(file) {
88
+ const withoutQuery = file.replace(/\?.*$/, "");
89
+ let pathname = withoutQuery;
90
+ try {
91
+ pathname = new URL(withoutQuery).pathname;
92
+ } catch {}
93
+ return collapseParentDirs(pathname.replace(/^\/vite-server/, "").replace(/^\/@fs/, ""));
94
+ }
95
+ /** Resolve a sourcemap `source` (relative) against the served module path. */
96
+ function resolveSourcePath(frameFile, source) {
97
+ const clean = frameFile.replace(/\?.*$/, "");
98
+ try {
99
+ const url = new URL(clean);
100
+ return new URL(source, `https://host${url.pathname}`).pathname.replace(/^\/vite-server/, "").replace(/^\/@fs/, "");
101
+ } catch {
102
+ return source;
103
+ }
104
+ }
105
+ function extensionOf(file) {
106
+ const match = file.replace(/\?.*$/, "").match(/\.(tsx|ts|jsx|js|mjs|cjs|rs)$/);
107
+ return match ? `.${match[1]}` : ".js";
108
+ }
109
+ /** Immediate (unmapped) frames for the first paint before async resolution. */
110
+ function initialFrames(stack) {
111
+ return parseStack(stack).map((frame) => ({
112
+ fn: frame.fn,
113
+ file: frame.file ? prettyPath(frame.file) : void 0,
114
+ line: frame.line,
115
+ column: frame.column,
116
+ isApp: isApplicationFile(frame.file)
117
+ }));
118
+ }
119
+ async function loadSourceMap(moduleUrl, cache, TraceMapCtor) {
120
+ const key = moduleUrl.replace(/\?.*$/, "");
121
+ const cached = cache.get(key);
122
+ if (cached !== void 0) return cached;
123
+ let map = null;
124
+ try {
125
+ let json = null;
126
+ const mapResponse = await fetch(`${key}.map`);
127
+ if (mapResponse.ok) json = await mapResponse.json();
128
+ else {
129
+ const moduleResponse = await fetch(moduleUrl);
130
+ if (moduleResponse.ok) {
131
+ const inline = (await moduleResponse.text()).match(/sourceMappingURL=data:application\/json;(?:charset=[^;,]+;)?base64,([A-Za-z0-9+/=]+)/);
132
+ if (inline?.[1]) json = JSON.parse(atob(inline[1]));
133
+ }
134
+ }
135
+ if (json) map = new TraceMapCtor(json);
136
+ } catch {
137
+ map = null;
138
+ }
139
+ cache.set(key, map);
140
+ return map;
141
+ }
142
+ /**
143
+ * Pull the per-line `<span class="line">` elements out of Shiki's `codeToHast`
144
+ * output (`root > pre > code > span.line*`). Shiki already delimits lines, so no
145
+ * manual line-splitting is needed.
146
+ */
147
+ function extractLineNodes(tree) {
148
+ return ((tree.children.find((node) => node.type === "element" && node.tagName === "pre")?.children.find((node) => node.type === "element" && node.tagName === "code"))?.children ?? []).filter((node) => node.type === "element" && node.properties?.["class"] === "line").map((line) => line.children);
149
+ }
150
+ /**
151
+ * Slice the source into the context window around the throwing line, as plain
152
+ * text. Synchronous and dependency-free, so the excerpt can render immediately;
153
+ * syntax highlighting then swaps in over the same layout (see
154
+ * {@link highlightExcerpt}).
155
+ */
156
+ function extractExcerpt(src) {
157
+ const allLines = src.content.split("\n");
158
+ const start = Math.max(1, src.line - SOURCE_CONTEXT);
159
+ const end = Math.min(allLines.length, src.line + SOURCE_CONTEXT);
160
+ const lines = [];
161
+ for (let number = start; number <= end; number++) lines.push({
162
+ number,
163
+ text: allLines[number - 1] ?? "",
164
+ isErrorLine: number === src.line
165
+ });
166
+ return {
167
+ file: src.file,
168
+ line: src.line,
169
+ column: src.column,
170
+ lines
171
+ };
172
+ }
173
+ function buildExcerpt(highlighter, toHtml, source, file, errorLine, errorColumn) {
174
+ const lines = extractLineNodes(highlighter.codeToHast(source, {
175
+ lang: langForFile(file),
176
+ theme: "github-dark"
177
+ }));
178
+ const start = Math.max(1, errorLine - SOURCE_CONTEXT);
179
+ const end = Math.min(lines.length, errorLine + SOURCE_CONTEXT);
180
+ const highlighted = [];
181
+ for (let number = start; number <= end; number++) highlighted.push({
182
+ number,
183
+ html: toHtml({
184
+ type: "root",
185
+ children: lines[number - 1] ?? []
186
+ }),
187
+ isErrorLine: number === errorLine
188
+ });
189
+ return {
190
+ file,
191
+ line: errorLine,
192
+ column: errorColumn,
193
+ lines: highlighted
194
+ };
195
+ }
196
+ let highlighterPromise = null;
197
+ /**
198
+ * Build a fine-grained Shiki highlighter: the tree-shakeable core, the pure-JS
199
+ * regex engine (no oniguruma WASM), and only the four grammars the overlay needs
200
+ * plus one theme. This keeps the dev-only chunk small and avoids a WASM fetch on
201
+ * the dev critical path.
202
+ */
203
+ async function getHighlighter() {
204
+ if (!highlighterPromise) highlighterPromise = (async () => {
205
+ const [{ createHighlighterCore }, { createJavaScriptRegexEngine }] = await Promise.all([import("shiki/core"), import("shiki/engine/javascript")]);
206
+ const [tsx, ts, js, rust, theme] = await Promise.all([
207
+ import("@shikijs/langs/tsx"),
208
+ import("@shikijs/langs/typescript"),
209
+ import("@shikijs/langs/javascript"),
210
+ import("@shikijs/langs/rust"),
211
+ import("@shikijs/themes/github-dark")
212
+ ]);
213
+ return createHighlighterCore({
214
+ themes: [theme.default],
215
+ langs: [
216
+ tsx.default,
217
+ ts.default,
218
+ js.default,
219
+ rust.default
220
+ ],
221
+ engine: createJavaScriptRegexEngine()
222
+ });
223
+ })();
224
+ return highlighterPromise;
225
+ }
226
+ /**
227
+ * Eagerly kick off the heavy, dev-only imports (Shiki core + engine + grammars,
228
+ * trace-mapping, hast-util-to-html) so the first error's source excerpt renders
229
+ * without waiting on library loading. Safe to call repeatedly — the imports are
230
+ * cached. Call it when the overlay host mounts.
231
+ */
232
+ function warmDevErrorSource() {
233
+ getHighlighter();
234
+ import("@jridgewell/trace-mapping");
235
+ import("hast-util-to-html");
236
+ }
237
+ /**
238
+ * Syntax-highlight a source excerpt (Shiki picks the grammar from the file
239
+ * extension, e.g. `.tsx` or `.rs`). Returns `null` if the dev-only libraries
240
+ * fail to load — the caller keeps showing the plain excerpt.
241
+ */
242
+ async function highlightExcerpt(src) {
243
+ try {
244
+ const [highlighter, htmlModule] = await Promise.all([getHighlighter(), import("hast-util-to-html")]);
245
+ const toHtml = htmlModule.toHtml;
246
+ return buildExcerpt(highlighter, toHtml, src.content, src.file, src.line, src.column);
247
+ } catch {
248
+ return null;
249
+ }
250
+ }
251
+ /**
252
+ * Resolve stack frames to original locations (via each module's sourcemap) and
253
+ * return the raw source content of the top application frame — WITHOUT
254
+ * highlighting it, so the caller can render a plain excerpt immediately and
255
+ * highlight it asynchronously. Falls back to unmapped frames if trace-mapping
256
+ * fails to load.
257
+ */
258
+ async function resolveDevErrorFrames(stack) {
259
+ const rawFrames = parseStack(stack);
260
+ try {
261
+ const { TraceMap, originalPositionFor, sourceContentFor } = await import("@jridgewell/trace-mapping");
262
+ const mapCache = /* @__PURE__ */ new Map();
263
+ const frames = [];
264
+ let source = null;
265
+ for (const frame of rawFrames) {
266
+ if (!frame.file || frame.line == null) {
267
+ frames.push({
268
+ fn: frame.fn,
269
+ isApp: false
270
+ });
271
+ continue;
272
+ }
273
+ const isApp = isApplicationFile(frame.file);
274
+ let file = prettyPath(frame.file);
275
+ let line = frame.line;
276
+ let column = frame.column ?? 1;
277
+ let content = null;
278
+ if (isApp) {
279
+ const map = await loadSourceMap(frame.file, mapCache, TraceMap);
280
+ if (map) {
281
+ const original = originalPositionFor(map, {
282
+ line: frame.line,
283
+ column: (frame.column ?? 1) - 1
284
+ });
285
+ if (original.source != null && original.line != null) {
286
+ file = resolveSourcePath(frame.file, original.source);
287
+ line = original.line;
288
+ column = (original.column ?? 0) + 1;
289
+ content = sourceContentFor(map, original.source);
290
+ }
291
+ }
292
+ }
293
+ frames.push({
294
+ fn: frame.fn,
295
+ file,
296
+ line,
297
+ column,
298
+ isApp
299
+ });
300
+ if (!source && isApp && content) source = {
301
+ content,
302
+ file,
303
+ line,
304
+ column
305
+ };
306
+ }
307
+ return {
308
+ frames,
309
+ source
310
+ };
311
+ } catch {
312
+ return {
313
+ frames: initialFrames(stack),
314
+ source: null
315
+ };
316
+ }
317
+ }
318
+
319
+ //#endregion
320
+ export { errorLabel, extractExcerpt, highlightExcerpt, initialFrames, parseStack, prettyPath, resolveDevErrorFrames, warmDevErrorSource };
321
+ //# sourceMappingURL=devErrorSource.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"devErrorSource.js","names":[],"sources":["../../../src/components/devErrorSource.ts"],"sourcesContent":["import type { TraceMap } from '@jridgewell/trace-mapping';\nimport type { HighlighterCore } from 'shiki/core';\n\n// Minimal hast shapes (avoids a direct dependency on `@types/hast`).\ninterface HastText {\n type: 'text';\n value: string;\n}\ninterface HastElement {\n type: 'element';\n tagName: string;\n properties?: Record<string, unknown>;\n children: Array<HastNode>;\n}\ntype HastNode = HastText | HastElement;\ntype ToHtml = (tree: { type: 'root'; children: Array<HastNode> }) => string;\n\n/**\n * Dev-only pipeline for the error overlay. Resolves a browser stack trace to\n * original source locations (via each module's sourcemap), then extracts and\n * syntax-highlights (Shiki) the source excerpt around the throwing expression.\n * Everything here is dynamically imported so it never ships in the production\n * client bundle.\n */\n\ninterface RawFrame {\n fn?: string;\n file?: string;\n line?: number;\n column?: number;\n}\n\nexport interface ResolvedFrame {\n fn?: string;\n file?: string;\n line?: number;\n column?: number;\n isApp: boolean;\n}\n\nexport interface HighlightedLine {\n number: number;\n /** Pre-highlighted HTML for the line (from Shiki, inline-styled). */\n html: string;\n isErrorLine: boolean;\n}\n\n/** A single source line as plain text — shown before highlighting is ready. */\nexport interface PlainLine {\n number: number;\n text: string;\n isErrorLine: boolean;\n}\n\nexport interface SourceExcerpt {\n file: string;\n line: number;\n column: number;\n lines: Array<HighlightedLine>;\n}\n\n/** The excerpt in plain text, before Shiki has highlighted it. */\nexport interface PlainExcerpt {\n file: string;\n line: number;\n column: number;\n lines: Array<PlainLine>;\n}\n\n/** Source content around a throwing location — the input to an excerpt. */\nexport interface RawExcerptSource {\n content: string;\n file: string;\n line: number;\n column: number;\n}\n\nconst SOURCE_CONTEXT = 5;\n\n/** Map a source file extension to the Shiki grammar (language) to load. */\nfunction langForFile(\n file: string,\n): 'tsx' | 'typescript' | 'javascript' | 'rust' {\n switch (extensionOf(file)) {\n case '.tsx':\n case '.jsx':\n return 'tsx';\n case '.ts':\n return 'typescript';\n case '.rs':\n return 'rust';\n default:\n return 'javascript';\n }\n}\n\n// V8 / Chrome: `fn (url:line:col)` or `url:line:col` (after the `at ` prefix).\nconst V8_LINE = /^(?:(.*?)\\s+)?\\(?([^()]+):(\\d+):(\\d+)\\)?$/;\n// SpiderMonkey / JavaScriptCore (Firefox, Safari): `fn@url:line:col`\n// (`fn` may be empty). The url itself can contain `@` (vite's `@fs`), so the\n// function name is only the part before the FIRST `@`.\nconst SPIDERMONKEY_LINE = /^(.*?)@(.+):(\\d+):(\\d+)$/;\n// Rust backtrace frame header: `<n>: <symbol>`, e.g. `10: my_app::route::handler`.\n// Its `at <file>:<line>:<col>` location, when present, is on the following line.\nconst RUST_FRAME = /^\\d+:\\s+(.+)$/;\n\nexport function parseStack(stack: string | undefined): Array<RawFrame> {\n if (!stack) return [];\n\n const frames: Array<RawFrame> = [];\n // A Rust frame symbol awaiting its (optional) `at <file>:<line>:<col>` line.\n let pendingFn: string | undefined;\n\n const flushPending = (): void => {\n if (pendingFn !== undefined) {\n frames.push({ fn: pendingFn });\n pendingFn = undefined;\n }\n };\n\n for (const rawLine of stack.split('\\n')) {\n const line = rawLine.trim();\n if (!line) continue;\n\n const rustFrame = line.match(RUST_FRAME);\n if (rustFrame) {\n // A new symbol: the previous one had no location line, emit it as-is.\n flushPending();\n pendingFn = rustFrame[1];\n continue;\n }\n\n if (line.startsWith('at ')) {\n const match = line.slice(3).trim().match(V8_LINE);\n if (match) {\n frames.push({\n // Prefer a Rust symbol captured from the preceding frame header.\n fn: pendingFn ?? (match[1]?.trim() || undefined),\n file: match[2],\n line: Number(match[3]),\n column: Number(match[4]),\n });\n } else {\n frames.push({ fn: pendingFn ?? line.slice(3).trim() });\n }\n pendingFn = undefined;\n continue;\n }\n\n const match = line.match(SPIDERMONKEY_LINE);\n if (match) {\n flushPending();\n frames.push({\n fn: match[1] || undefined,\n file: match[2],\n line: Number(match[3]),\n column: Number(match[4]),\n });\n }\n }\n\n flushPending();\n return frames;\n}\n\n/**\n * The most specific label for an error: its constructor (class) name when it is\n * more descriptive than the generic `Error`. A subclass like `TestError` keeps\n * `error.name === 'Error'` unless it explicitly sets `name`, but its\n * constructor name is still `TestError`.\n */\nexport function errorLabel(error: Error): string {\n const constructorName = error.constructor?.name;\n if (\n constructorName &&\n constructorName !== 'Error' &&\n constructorName !== 'Object'\n ) {\n return constructorName;\n }\n return error.name || 'Error';\n}\n\nfunction isApplicationFile(file: string | undefined): boolean {\n if (!file) return false;\n return (\n !file.includes('node_modules') &&\n !file.includes('/deps/') &&\n !file.startsWith('node:') &&\n // Rust stdlib / toolchain / registry frames are vendor noise, not app code.\n !file.includes('/rustc/') &&\n !file.includes('/rustlib/') &&\n !file.includes('/.cargo/registry')\n );\n}\n\n/** Collapse `foo/../` segments (e.g. Rust's `.ossido/../src/...` → `src/...`). */\nfunction collapseParentDirs(path: string): string {\n const out: Array<string> = [];\n for (const part of path.split('/')) {\n const prev = out[out.length - 1];\n if (part === '..' && prev !== undefined && prev !== '' && prev !== '..') {\n out.pop();\n } else {\n out.push(part);\n }\n }\n return out.join('/');\n}\n\n/** Strip origin and query for a readable path. */\nexport function prettyPath(file: string): string {\n const withoutQuery = file.replace(/\\?.*$/, '');\n let pathname = withoutQuery;\n try {\n pathname = new URL(withoutQuery).pathname;\n } catch {\n // already a path\n }\n return collapseParentDirs(\n pathname.replace(/^\\/vite-server/, '').replace(/^\\/@fs/, ''),\n );\n}\n\n/** Resolve a sourcemap `source` (relative) against the served module path. */\nfunction resolveSourcePath(frameFile: string, source: string): string {\n const clean = frameFile.replace(/\\?.*$/, '');\n try {\n const url = new URL(clean);\n const resolved = new URL(source, `https://host${url.pathname}`).pathname;\n return resolved.replace(/^\\/vite-server/, '').replace(/^\\/@fs/, '');\n } catch {\n return source;\n }\n}\n\nfunction extensionOf(file: string): string {\n const match = file\n .replace(/\\?.*$/, '')\n .match(/\\.(tsx|ts|jsx|js|mjs|cjs|rs)$/);\n return match ? `.${match[1]}` : '.js';\n}\n\n/** Immediate (unmapped) frames for the first paint before async resolution. */\nexport function initialFrames(stack: string | undefined): Array<ResolvedFrame> {\n return parseStack(stack).map((frame) => ({\n fn: frame.fn,\n file: frame.file ? prettyPath(frame.file) : undefined,\n line: frame.line,\n column: frame.column,\n isApp: isApplicationFile(frame.file),\n }));\n}\n\nasync function loadSourceMap(\n moduleUrl: string,\n cache: Map<string, TraceMap | null>,\n TraceMapCtor: typeof TraceMap,\n): Promise<TraceMap | null> {\n const key = moduleUrl.replace(/\\?.*$/, '');\n const cached = cache.get(key);\n if (cached !== undefined) return cached;\n\n let map: TraceMap | null = null;\n try {\n let json: unknown = null;\n const mapResponse = await fetch(`${key}.map`);\n if (mapResponse.ok) {\n json = await mapResponse.json();\n } else {\n const moduleResponse = await fetch(moduleUrl);\n if (moduleResponse.ok) {\n const text = await moduleResponse.text();\n const inline = text.match(\n /sourceMappingURL=data:application\\/json;(?:charset=[^;,]+;)?base64,([A-Za-z0-9+/=]+)/,\n );\n if (inline?.[1]) json = JSON.parse(atob(inline[1]));\n }\n }\n if (json)\n map = new TraceMapCtor(json as ConstructorParameters<typeof TraceMap>[0]);\n } catch {\n map = null;\n }\n\n cache.set(key, map);\n return map;\n}\n\n/**\n * Pull the per-line `<span class=\"line\">` elements out of Shiki's `codeToHast`\n * output (`root > pre > code > span.line*`). Shiki already delimits lines, so no\n * manual line-splitting is needed.\n */\nfunction extractLineNodes(tree: {\n children: Array<HastNode>;\n}): Array<Array<HastNode>> {\n const pre = tree.children.find(\n (node): node is HastElement =>\n node.type === 'element' && node.tagName === 'pre',\n );\n const code = pre?.children.find(\n (node): node is HastElement =>\n node.type === 'element' && node.tagName === 'code',\n );\n // Shiki tags each line as `<span class=\"line\">` (a raw `class` string, not the\n // hast-normalized `className` array), separated by `\\n` text nodes.\n const lineNodes = (code?.children ?? []).filter(\n (node): node is HastElement =>\n node.type === 'element' && node.properties?.['class'] === 'line',\n );\n return lineNodes.map((line) => line.children);\n}\n\n/**\n * Slice the source into the context window around the throwing line, as plain\n * text. Synchronous and dependency-free, so the excerpt can render immediately;\n * syntax highlighting then swaps in over the same layout (see\n * {@link highlightExcerpt}).\n */\nexport function extractExcerpt(src: RawExcerptSource): PlainExcerpt {\n const allLines = src.content.split('\\n');\n const start = Math.max(1, src.line - SOURCE_CONTEXT);\n const end = Math.min(allLines.length, src.line + SOURCE_CONTEXT);\n const lines: Array<PlainLine> = [];\n for (let number = start; number <= end; number++) {\n lines.push({\n number,\n text: allLines[number - 1] ?? '',\n isErrorLine: number === src.line,\n });\n }\n return { file: src.file, line: src.line, column: src.column, lines };\n}\n\nfunction buildExcerpt(\n highlighter: HighlighterCore,\n toHtml: ToHtml,\n source: string,\n file: string,\n errorLine: number,\n errorColumn: number,\n): SourceExcerpt {\n const tree = highlighter.codeToHast(source, {\n lang: langForFile(file),\n theme: 'github-dark',\n }) as unknown as { children: Array<HastNode> };\n const lines = extractLineNodes(tree);\n\n const start = Math.max(1, errorLine - SOURCE_CONTEXT);\n const end = Math.min(lines.length, errorLine + SOURCE_CONTEXT);\n\n const highlighted: Array<HighlightedLine> = [];\n for (let number = start; number <= end; number++) {\n highlighted.push({\n number,\n html: toHtml({ type: 'root', children: lines[number - 1] ?? [] }),\n isErrorLine: number === errorLine,\n });\n }\n\n return { file, line: errorLine, column: errorColumn, lines: highlighted };\n}\n\nlet highlighterPromise: Promise<HighlighterCore> | null = null;\n\n/**\n * Build a fine-grained Shiki highlighter: the tree-shakeable core, the pure-JS\n * regex engine (no oniguruma WASM), and only the four grammars the overlay needs\n * plus one theme. This keeps the dev-only chunk small and avoids a WASM fetch on\n * the dev critical path.\n */\nasync function getHighlighter(): Promise<HighlighterCore> {\n if (!highlighterPromise) {\n highlighterPromise = (async (): Promise<HighlighterCore> => {\n const [{ createHighlighterCore }, { createJavaScriptRegexEngine }] =\n await Promise.all([\n import('shiki/core'),\n import('shiki/engine/javascript'),\n ]);\n const [tsx, ts, js, rust, theme] = await Promise.all([\n import('@shikijs/langs/tsx'),\n import('@shikijs/langs/typescript'),\n import('@shikijs/langs/javascript'),\n import('@shikijs/langs/rust'),\n import('@shikijs/themes/github-dark'),\n ]);\n return createHighlighterCore({\n themes: [theme.default],\n langs: [tsx.default, ts.default, js.default, rust.default],\n engine: createJavaScriptRegexEngine(),\n });\n })();\n }\n return highlighterPromise;\n}\n\n/**\n * Eagerly kick off the heavy, dev-only imports (Shiki core + engine + grammars,\n * trace-mapping, hast-util-to-html) so the first error's source excerpt renders\n * without waiting on library loading. Safe to call repeatedly — the imports are\n * cached. Call it when the overlay host mounts.\n */\nexport function warmDevErrorSource(): void {\n void getHighlighter();\n void import('@jridgewell/trace-mapping');\n void import('hast-util-to-html');\n}\n\n/**\n * Syntax-highlight a source excerpt (Shiki picks the grammar from the file\n * extension, e.g. `.tsx` or `.rs`). Returns `null` if the dev-only libraries\n * fail to load — the caller keeps showing the plain excerpt.\n */\nexport async function highlightExcerpt(\n src: RawExcerptSource,\n): Promise<SourceExcerpt | null> {\n try {\n const [highlighter, htmlModule] = await Promise.all([\n getHighlighter(),\n import('hast-util-to-html'),\n ]);\n const toHtml = htmlModule.toHtml as unknown as ToHtml;\n return buildExcerpt(\n highlighter,\n toHtml,\n src.content,\n src.file,\n src.line,\n src.column,\n );\n } catch {\n return null;\n }\n}\n\n/**\n * Resolve stack frames to original locations (via each module's sourcemap) and\n * return the raw source content of the top application frame — WITHOUT\n * highlighting it, so the caller can render a plain excerpt immediately and\n * highlight it asynchronously. Falls back to unmapped frames if trace-mapping\n * fails to load.\n */\nexport async function resolveDevErrorFrames(\n stack: string | undefined,\n): Promise<{ frames: Array<ResolvedFrame>; source: RawExcerptSource | null }> {\n const rawFrames = parseStack(stack);\n\n try {\n const { TraceMap, originalPositionFor, sourceContentFor } =\n await import('@jridgewell/trace-mapping');\n\n const mapCache = new Map<string, TraceMap | null>();\n const frames: Array<ResolvedFrame> = [];\n let source: RawExcerptSource | null = null;\n\n for (const frame of rawFrames) {\n if (!frame.file || frame.line == null) {\n frames.push({ fn: frame.fn, isApp: false });\n continue;\n }\n\n const isApp = isApplicationFile(frame.file);\n let file = prettyPath(frame.file);\n let line = frame.line;\n let column = frame.column ?? 1;\n let content: string | null | undefined = null;\n\n // Only application frames are worth mapping (vendor frames are noise).\n if (isApp) {\n const map = await loadSourceMap(frame.file, mapCache, TraceMap);\n if (map) {\n const original = originalPositionFor(map, {\n line: frame.line,\n column: (frame.column ?? 1) - 1,\n });\n if (original.source != null && original.line != null) {\n file = resolveSourcePath(frame.file, original.source);\n line = original.line;\n column = (original.column ?? 0) + 1;\n content = sourceContentFor(map, original.source);\n }\n }\n }\n\n frames.push({ fn: frame.fn, file, line, column, isApp });\n\n // Keep the top application frame's source for the excerpt.\n if (!source && isApp && content) {\n source = { content, file, line, column };\n }\n }\n\n return { frames, source };\n } catch {\n return { frames: initialFrames(stack), source: null };\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Development-only store that aggregates every kind of dev error behind one
3
+ * overlay: React render errors (and SSR/Rust panics) caught by the route error
4
+ * boundary, uncaught `window` errors, unhandled promise rejections, and Vite
5
+ * build/compile errors. {@link DevErrorOverlayHost} subscribes to it via
6
+ * `useSyncExternalStore` and renders a persistent dev indicator + browsable
7
+ * error overlay.
8
+ *
9
+ * The store is a module singleton (not React state) so it exists before the
10
+ * overlay mounts and independently of which subtree threw. It is also published
11
+ * on `window.__OSSIDO_DEV_ERRORS__` so the Vite HMR client — patched by
12
+ * `ErrorOverlayVitePlugin`, which runs outside the module system — can report
13
+ * build errors into it without needing `import.meta.hot`.
14
+ */
15
+ /** The distinct sources an aggregated error can come from. */
16
+ export type DevErrorKind = 'runtime' | 'uncaught' | 'unhandledrejection' | 'build';
17
+ /**
18
+ * A Vite build/compile error (a subset of Vite's `ErrorPayload['err']`). Kept
19
+ * structural rather than importing from `vite` so this module carries no build
20
+ * dependency.
21
+ */
22
+ export interface DevBuildError {
23
+ message: string;
24
+ stack?: string;
25
+ id?: string;
26
+ frame?: string;
27
+ plugin?: string;
28
+ loc?: {
29
+ file?: string;
30
+ line?: number;
31
+ column?: number;
32
+ };
33
+ }
34
+ export interface DevErrorEntry {
35
+ id: number;
36
+ kind: DevErrorKind;
37
+ /** Present for JS errors (`runtime` / `uncaught` / `unhandledrejection`). */
38
+ error?: Error;
39
+ /** Present for `build` errors. */
40
+ build?: DevBuildError;
41
+ /** Only in-place-resettable errors (the route boundary) carry a reset. */
42
+ reset?: () => void;
43
+ /** How many times this identical error has been reported (dedup counter). */
44
+ occurrences: number;
45
+ /** Dedup key — identical errors collapse to a single entry. */
46
+ signature: string;
47
+ }
48
+ /** Which screen corner the dev indicator sits in. */
49
+ export type DevOverlayCorner = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
50
+ export declare const DEV_OVERLAY_CORNERS: ReadonlyArray<DevOverlayCorner>;
51
+ export interface DevErrorState {
52
+ entries: ReadonlyArray<DevErrorEntry>;
53
+ /** Index of the entry currently shown in the pager. */
54
+ activeIndex: number;
55
+ /** The full error overlay is open (only meaningful when there are errors). */
56
+ overlayOpen: boolean;
57
+ /** The indicator's menu popover is open. */
58
+ menuOpen: boolean;
59
+ /** The user hid the idle indicator for this session (errors still force it). */
60
+ badgeHidden: boolean;
61
+ /** Which corner the indicator sits in (persisted across reloads). */
62
+ position: DevOverlayCorner;
63
+ }
64
+ declare class DevErrorStore {
65
+ private entries;
66
+ private activeIndex;
67
+ private overlayOpen;
68
+ private menuOpen;
69
+ private badgeHidden;
70
+ private position;
71
+ private nextId;
72
+ private listeners;
73
+ private snapshot;
74
+ constructor();
75
+ subscribe: (listener: () => void) => (() => void);
76
+ getSnapshot: () => DevErrorState;
77
+ getServerSnapshot: () => DevErrorState;
78
+ private commit;
79
+ /** Add (or refresh) a JS error. Returns its id so the reporter can remove it. */
80
+ addJsError: (kind: Exclude<DevErrorKind, "build">, error: Error, reset?: () => void) => number;
81
+ /** Report a Vite build error — replaces any previous one. */
82
+ addBuildError: (build: DevBuildError) => number;
83
+ /** Clear the current build error (called when Vite reports a successful update). */
84
+ clearBuildErrors: () => void;
85
+ removeById: (id: number) => void;
86
+ setActiveIndex: (index: number) => void;
87
+ next: () => void;
88
+ prev: () => void;
89
+ /** Open the full error overlay (from the indicator menu's "View errors"). */
90
+ openOverlay: () => void;
91
+ /** Close the overlay back to the indicator (X / Esc / click-away). */
92
+ closeOverlay: () => void;
93
+ toggleMenu: () => void;
94
+ closeMenu: () => void;
95
+ /** Hide the idle indicator for this session (a new error still forces it back). */
96
+ hideBadge: () => void;
97
+ setPosition: (corner: DevOverlayCorner) => void;
98
+ /**
99
+ * Publish the store on `window` and drain any errors buffered by the Vite
100
+ * client before this module loaded. Safe to call repeatedly.
101
+ */
102
+ installBridge(): void;
103
+ }
104
+ export declare const devErrorStore: DevErrorStore;
105
+ export {};
@@ -0,0 +1,186 @@
1
+ //#region src/components/devErrorStore.ts
2
+ const DEV_OVERLAY_CORNERS = [
3
+ "top-left",
4
+ "top-right",
5
+ "bottom-left",
6
+ "bottom-right"
7
+ ];
8
+ const DEFAULT_CORNER = "bottom-left";
9
+ const CORNER_STORAGE_KEY = "ossido:dev-overlay-corner";
10
+ /** The corner is a persisted preference; hiding the badge is per-session. */
11
+ function loadCorner() {
12
+ try {
13
+ const stored = globalThis.localStorage?.getItem(CORNER_STORAGE_KEY);
14
+ if (stored && DEV_OVERLAY_CORNERS.includes(stored)) return stored;
15
+ } catch {}
16
+ return DEFAULT_CORNER;
17
+ }
18
+ function saveCorner(corner) {
19
+ try {
20
+ globalThis.localStorage?.setItem(CORNER_STORAGE_KEY, corner);
21
+ } catch {}
22
+ }
23
+ const EMPTY_STATE = {
24
+ entries: [],
25
+ activeIndex: 0,
26
+ overlayOpen: false,
27
+ menuOpen: false,
28
+ badgeHidden: false,
29
+ position: DEFAULT_CORNER
30
+ };
31
+ const BRIDGE_KEY = "__OSSIDO_DEV_ERRORS__";
32
+ const BUFFER_KEY = "__OSSIDO_DEV_ERRORS_BUFFER__";
33
+ function jsSignature(error) {
34
+ return `${error.name}:${error.message}:${(error.stack ?? "").slice(0, 200)}`;
35
+ }
36
+ const BUILD_SIGNATURE = "build";
37
+ var DevErrorStore = class {
38
+ constructor() {
39
+ this.entries = [];
40
+ this.activeIndex = 0;
41
+ this.overlayOpen = false;
42
+ this.menuOpen = false;
43
+ this.badgeHidden = false;
44
+ this.position = loadCorner();
45
+ this.nextId = 1;
46
+ this.listeners = /* @__PURE__ */ new Set();
47
+ this.snapshot = EMPTY_STATE;
48
+ this.subscribe = (listener) => {
49
+ this.listeners.add(listener);
50
+ return () => {
51
+ this.listeners.delete(listener);
52
+ };
53
+ };
54
+ this.getSnapshot = () => this.snapshot;
55
+ this.getServerSnapshot = () => EMPTY_STATE;
56
+ this.addJsError = (kind, error, reset) => {
57
+ const signature = jsSignature(error);
58
+ const existing = this.entries.find((e) => e.signature === signature);
59
+ if (existing) {
60
+ if (kind === "runtime") {
61
+ existing.kind = "runtime";
62
+ existing.reset = reset ?? existing.reset;
63
+ }
64
+ existing.occurrences += 1;
65
+ this.activeIndex = this.entries.indexOf(existing);
66
+ this.overlayOpen = true;
67
+ this.commit();
68
+ return existing.id;
69
+ }
70
+ const entry = {
71
+ id: this.nextId++,
72
+ kind,
73
+ error,
74
+ reset,
75
+ occurrences: 1,
76
+ signature
77
+ };
78
+ this.entries.push(entry);
79
+ this.activeIndex = this.entries.length - 1;
80
+ this.overlayOpen = true;
81
+ this.commit();
82
+ return entry.id;
83
+ };
84
+ this.addBuildError = (build) => {
85
+ this.entries = this.entries.filter((e) => e.kind !== "build");
86
+ const entry = {
87
+ id: this.nextId++,
88
+ kind: "build",
89
+ build,
90
+ occurrences: 1,
91
+ signature: BUILD_SIGNATURE
92
+ };
93
+ this.entries.push(entry);
94
+ this.activeIndex = this.entries.length - 1;
95
+ this.overlayOpen = true;
96
+ this.commit();
97
+ return entry.id;
98
+ };
99
+ this.clearBuildErrors = () => {
100
+ if (!this.entries.some((e) => e.kind === "build")) return;
101
+ this.entries = this.entries.filter((e) => e.kind !== "build");
102
+ this.commit();
103
+ };
104
+ this.removeById = (id) => {
105
+ const next = this.entries.filter((e) => e.id !== id);
106
+ if (next.length === this.entries.length) return;
107
+ this.entries = next;
108
+ this.commit();
109
+ };
110
+ this.setActiveIndex = (index) => {
111
+ this.activeIndex = index;
112
+ this.commit();
113
+ };
114
+ this.next = () => {
115
+ if (this.entries.length === 0) return;
116
+ this.activeIndex = (this.activeIndex + 1) % this.entries.length;
117
+ this.commit();
118
+ };
119
+ this.prev = () => {
120
+ if (this.entries.length === 0) return;
121
+ this.activeIndex = (this.activeIndex - 1 + this.entries.length) % this.entries.length;
122
+ this.commit();
123
+ };
124
+ this.openOverlay = () => {
125
+ this.overlayOpen = true;
126
+ this.menuOpen = false;
127
+ this.commit();
128
+ };
129
+ this.closeOverlay = () => {
130
+ this.overlayOpen = false;
131
+ this.commit();
132
+ };
133
+ this.toggleMenu = () => {
134
+ this.menuOpen = !this.menuOpen;
135
+ this.commit();
136
+ };
137
+ this.closeMenu = () => {
138
+ if (!this.menuOpen) return;
139
+ this.menuOpen = false;
140
+ this.commit();
141
+ };
142
+ this.hideBadge = () => {
143
+ this.badgeHidden = true;
144
+ this.menuOpen = false;
145
+ this.commit();
146
+ };
147
+ this.setPosition = (corner) => {
148
+ this.position = corner;
149
+ saveCorner(corner);
150
+ this.commit();
151
+ };
152
+ this.commit();
153
+ }
154
+ commit() {
155
+ this.activeIndex = Math.min(Math.max(this.activeIndex, 0), Math.max(this.entries.length - 1, 0));
156
+ this.snapshot = {
157
+ entries: this.entries.slice(),
158
+ activeIndex: this.activeIndex,
159
+ overlayOpen: this.overlayOpen && this.entries.length > 0,
160
+ menuOpen: this.menuOpen,
161
+ badgeHidden: this.badgeHidden,
162
+ position: this.position
163
+ };
164
+ for (const listener of this.listeners) listener();
165
+ }
166
+ /**
167
+ * Publish the store on `window` and drain any errors buffered by the Vite
168
+ * client before this module loaded. Safe to call repeatedly.
169
+ */
170
+ installBridge() {
171
+ if (typeof window === "undefined") return;
172
+ const globalWindow = window;
173
+ globalWindow[BRIDGE_KEY] = this;
174
+ const buffered = globalWindow[BUFFER_KEY];
175
+ if (buffered?.length) {
176
+ for (const build of buffered) this.addBuildError(build);
177
+ globalWindow[BUFFER_KEY] = [];
178
+ }
179
+ }
180
+ };
181
+ const devErrorStore = new DevErrorStore();
182
+ devErrorStore.installBridge();
183
+
184
+ //#endregion
185
+ export { DEV_OVERLAY_CORNERS, devErrorStore };
186
+ //# sourceMappingURL=devErrorStore.js.map