Skip to content

Commit 7bd50c4

Browse files
committed
feat: add development mode support and error overlay enhancements
1 parent a52e8f9 commit 7bd50c4

8 files changed

Lines changed: 182 additions & 17 deletions

File tree

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@ npx skills add yieldray/module-tsx
9696
<!-- ESM -->
9797
<script type="module" src="https://esm.sh/module-tsx"></script>
9898

99+
<!-- ESM (DEV mode) -->
100+
<script type="module" src="https://esm.sh/module-tsx/dev"></script>
101+
99102
<!-- ESM (self-contained, no external dependencies) -->
100103
<script
101104
type="module"

index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
</body>
1818
<!-- <script src="./dist/index.umd.js"></script> -->
1919
<script type="module">
20-
import { instance, ModuleTSX } from "./dist/index.cdn.mjs"; // the ESM module
20+
import { instance, ModuleTSX } from "./dist/index.dev.mjs"; // the ESM module
2121
// const { instance, ModuleTSX } = window.ModuleTSX; // the UMD global variable
2222
instance.addEventListener("*", (event) => {
2323
const { type, payload } = event.detail;

package.json

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,30 @@
66
"dist"
77
],
88
"main": "dist/index.mjs",
9-
"browser": "dist/index.cdn.mjs",
109
"types": "dist/index.d.ts",
10+
"exports": {
11+
".": {
12+
"types": "./dist/index.d.ts",
13+
"default": "./dist/index.cdn.mjs"
14+
},
15+
"./esm": {
16+
"types": "./dist/index.d.ts",
17+
"default": "./dist/index.mjs"
18+
},
19+
"./umd": {
20+
"types": "./dist/index.d.ts",
21+
"default": "./dist/index.umd.js"
22+
},
23+
"./dev": {
24+
"types": "./dist/index.d.ts",
25+
"default": "./dist/index.dev.mjs"
26+
}
27+
},
28+
"sideEffects": true,
1129
"scripts": {
1230
"dev": "tsdown --watch",
1331
"build": "tsdown",
32+
"lint": "pnpm dlx publint",
1433
"test": "node --test 'src/*.test.ts'"
1534
},
1635
"dependencies": {

skills/module-tsx/SKILL.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,21 @@ import "./style.css"; // injects a <style> tag into <head>
151151
import styles from "./button.module.css"; // returns { root: "abc123_root", ... }
152152
```
153153

154+
## Dev vs Production
155+
156+
Use the **dev** build during development for a rich error overlay (runtime errors,
157+
failed imports, source-mapped stack traces with code frames):
158+
159+
```html
160+
<script type="module" src="https://esm.sh/module-tsx/dev"></script>
161+
```
162+
163+
Use the **production** build for deployment (no error overlay, smaller footprint):
164+
165+
```html
166+
<script type="module" src="https://esm.sh/module-tsx"></script>
167+
```
168+
154169
## Common mistakes
155170

156171
- **`defer` attribute**: not supported. Use `async` or no attribute.

src/error-overlay.ts

Lines changed: 125 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,25 @@ const overlayStyle = /*css*/ `
3535
overflow-y: scroll;
3636
margin: 0;
3737
background: rgba(0, 0, 0, 0.66);
38+
scrollbar-width: thin;
39+
scrollbar-color: #444 #1a1a1a;
40+
}
41+
42+
.backdrop::-webkit-scrollbar {
43+
width: 8px;
44+
}
45+
46+
.backdrop::-webkit-scrollbar-track {
47+
background: #1a1a1a;
48+
}
49+
50+
.backdrop::-webkit-scrollbar-thumb {
51+
background: #444;
52+
border-radius: 4px;
53+
}
54+
55+
.backdrop::-webkit-scrollbar-thumb:hover {
56+
background: #666;
3857
}
3958
4059
.window {
@@ -61,31 +80,44 @@ pre {
6180
margin-top: 0;
6281
margin-bottom: 1em;
6382
overflow-x: scroll;
64-
scrollbar-width: none;
83+
scrollbar-width: thin;
84+
scrollbar-color: #444 transparent;
6585
}
6686
6787
pre::-webkit-scrollbar {
68-
display: none;
88+
height: 6px;
6989
}
7090
71-
pre.frame::-webkit-scrollbar {
72-
display: block;
73-
height: 5px;
91+
pre::-webkit-scrollbar-track {
92+
background: transparent;
7493
}
7594
76-
pre.frame::-webkit-scrollbar-thumb {
77-
background: #999;
78-
border-radius: 5px;
95+
pre::-webkit-scrollbar-thumb {
96+
background: #444;
97+
border-radius: 3px;
7998
}
8099
81-
pre.frame {
82-
scrollbar-width: thin;
100+
pre::-webkit-scrollbar-thumb:hover {
101+
background: #666;
102+
}
103+
104+
.header {
105+
display: flex;
106+
align-items: center;
107+
justify-content: space-between;
108+
gap: 1em;
109+
margin-bottom: 1em;
83110
}
84111
85112
.message {
86113
line-height: 1.3;
87114
font-weight: 600;
88115
white-space: pre-wrap;
116+
margin: 0;
117+
padding: 0;
118+
flex: 1;
119+
min-width: 0;
120+
overflow-x: hidden;
89121
}
90122
91123
.message-body {
@@ -156,6 +188,35 @@ code {
156188
color: var(--yellow);
157189
}
158190
191+
.copy-btn {
192+
flex-shrink: 0;
193+
padding: 6px 12px;
194+
font-family: var(--monospace);
195+
font-size: 12px;
196+
font-weight: 600;
197+
color: var(--dim);
198+
background: #2a2a2a;
199+
border: 1px solid #444;
200+
border-radius: 4px;
201+
cursor: pointer;
202+
transition: background 0.2s, color 0.2s, border-color 0.2s;
203+
}
204+
205+
.copy-btn:hover {
206+
background: #333;
207+
color: #fff;
208+
border-color: #666;
209+
}
210+
211+
.copy-btn:active {
212+
background: #444;
213+
}
214+
215+
.copy-btn.copied {
216+
color: #50fa7b;
217+
border-color: #50fa7b;
218+
}
219+
159220
kbd {
160221
line-height: 1.5;
161222
font-family: ui-monospace, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
@@ -173,12 +234,42 @@ kbd {
173234
`;
174235

175236
export function setupErrorOverlay(instance: ModuleTSX): void {
237+
/** If the error originates from a module-tsx blob URL, show the overlay and return true. */
238+
const handleRuntimeError = (error: Error): boolean => {
239+
const blobLocation = extractBlobLocation(error.stack || "", instance);
240+
if (!blobLocation) return false;
241+
242+
const sourceUrl = instance.getSourceUrlByBlob(blobLocation.blobUrl);
243+
if (!sourceUrl) return false;
244+
245+
showErrorOverlay(sourceUrl, error, instance);
246+
return true;
247+
};
248+
176249
instance.addEventListener("*", (event) => {
177250
const { type, payload } = (event as CustomEvent).detail;
178251
if (!type.endsWith(":error")) return;
179252

180253
showErrorOverlay(payload.id || payload.sourceUrl, payload.error, instance);
181254
});
255+
256+
// Catch runtime errors (e.g. from event handlers, timeouts) originating from module-tsx modules
257+
window.addEventListener("error", (event) => {
258+
if (event.error instanceof Error && handleRuntimeError(event.error)) {
259+
event.preventDefault();
260+
}
261+
});
262+
263+
// Catch unhandled promise rejections from module-tsx modules
264+
window.addEventListener("unhandledrejection", (event) => {
265+
const error = event.reason instanceof Error
266+
? event.reason
267+
: new Error(String(event.reason));
268+
269+
if (handleRuntimeError(error)) {
270+
event.preventDefault();
271+
}
272+
});
182273
}
183274

184275
const overlayId = "module-tsx-error-overlay";
@@ -617,7 +708,30 @@ async function showErrorOverlay(id: string, error: unknown, instance: ModuleTSX)
617708
innerHTML: "Click outside, press <kbd>Esc</kbd> key, or fix the code to dismiss.",
618709
});
619710

620-
window_.appendChild(messagePre);
711+
// Copy button
712+
const copyBtn = Object.assign(document.createElement("button"), {
713+
className: "copy-btn",
714+
textContent: "Copy",
715+
});
716+
copyBtn.addEventListener("click", () => {
717+
const text = [message, id, stack].filter(Boolean).join("\n\n");
718+
navigator.clipboard.writeText(text).then(() => {
719+
copyBtn.textContent = "Copied!";
720+
copyBtn.classList.add("copied");
721+
setTimeout(() => {
722+
copyBtn.textContent = "Copy";
723+
copyBtn.classList.remove("copied");
724+
}, 2000);
725+
});
726+
});
727+
728+
const header = Object.assign(document.createElement("div"), {
729+
className: "header",
730+
});
731+
header.appendChild(messagePre);
732+
header.appendChild(copyBtn);
733+
734+
window_.appendChild(header);
621735
window_.appendChild(filePre);
622736
if (codeFrameEl) {
623737
window_.appendChild(codeFrameEl);

src/index.dev.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export * from "./index.ts";
2+
import { instance } from "./index.ts";
3+
import { setupErrorOverlay } from "./error-overlay.ts";
4+
5+
setupErrorOverlay(instance);

src/index.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { ModuleTSX } from "./module-tsx.ts";
22
import { ModuleTSXError, warn } from "./error.ts";
3-
import { setupErrorOverlay } from "./error-overlay.ts";
43
import { ImportMap } from "./import-map.ts";
54
export { ImportMap } from "./import-map.ts";
65
export { ModuleTSX, ModuleTSXError };
@@ -10,8 +9,6 @@ export { ModuleTSX, ModuleTSXError };
109
*/
1110
export const instance = new ModuleTSX({ importMap: ImportMap.fromDOM() });
1211

13-
setupErrorOverlay(instance);
14-
1512
const TYPE_ATTRIBUTE_VALUE = "module-tsx";
1613
async function sideEffect() {
1714
const importScript = async (script: HTMLScriptElement) => {

tsdown.config.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,18 @@ export default defineConfig([
7373
entryFileNames: "[name].cdn.mjs",
7474
},
7575
},
76+
// index.dev.js — ESM with error overlay, unbundled, external deps rewritten to esm.sh
77+
{
78+
dts: false,
79+
entry: { index: "./src/index.dev.ts" },
80+
format: "esm",
81+
platform: "browser",
82+
target: ["esnext"],
83+
plugins: esmShPlugin,
84+
outputOptions: {
85+
entryFileNames: "[name].dev.mjs",
86+
},
87+
},
7688
// index.js — ESM, unbundled (external deps)
7789
{
7890
dts: true,
@@ -100,7 +112,7 @@ export default defineConfig([
100112
},
101113
// index.umd.js — UMD, bundled + minified
102114
{
103-
dts: false,
115+
dts: true,
104116
entry: { index: "./src/index.ts" },
105117
format: "umd",
106118
platform: "browser",

0 commit comments

Comments
 (0)