fix(playground): replace esm.sh with jsDelivr +esm — fixes HTTP 500 on core.mjs - #35

Merged
joaodibba merged 1 commit into
TypeComposer:mainfrom
lucas-spin:fix/playground-cdn-jsdelivr-esm
Jun 12, 2026
Merged

fix(playground): replace esm.sh with jsDelivr +esm — fixes HTTP 500 on core.mjs#35
joaodibba merged 1 commit into
TypeComposer:mainfrom
lucas-spin:fix/playground-cdn-jsdelivr-esm

Conversation

@lucas-spin

Copy link
Copy Markdown
Contributor

Bug report (João)

GET https://esm.sh/typecomposer@0.1.56/es2022/core.mjs net::ERR_ABORTED 500
[Playground] Initialization failed: TypeError: Failed to fetch dynamically imported module:
https://esm.sh/typecomposer@0.1.56

Root cause

esm.sh auto-generates a split-entry bundle for typecomposer@0.1.56:

// https://esm.sh/typecomposer@0.1.56 ← 200, but…import"/typecomposer@0.1.56/es2022/core.mjs";// ← 500!import"/typecomposer@0.1.56/es2022/global.mjs";export*from"/typecomposer@0.1.56/es2022/typecomposer.mjs";

The generated core.mjs returns HTTP 500 with:

esbuild: No matching export in "node_modules/typecomposer/core/index.js" for import "Component"

Why?typecomposer's core/index.js re-exports from sub-packages (./element, ./components, …), and each of those sub-packages re-imports Component from "../.." (the root). This creates a circular dependency that esbuild — used internally by esm.sh — cannot resolve when building its split core.mjs entry.

The package has no "exports" map and uses "type": "module", so esm.sh tries to split on the bare subpath and fails.


Fix

Switch the import-map URL from esm.sh to jsDelivr +esm:

// Before (broken):consttypecomposerUrl=`https://esm.sh/typecomposer@${TYPECOMPOSER_VERSION}`;// After (working):consttypecomposerUrl=`https://cdn.jsdelivr.net/npm/typecomposer@${TYPECOMPOSER_VERSION}/+esm`;

jsDelivr's +esm endpoint uses Rollup to produce a single flat ES module from index.js directly, bypassing the circular subpath entirely. The result is a 117 KB file with all named exports (Component, VBox, DivElement, ButtonElement, etc.) and CORS headers.

CDNResponseNotes
esm.sh/typecomposer@0.1.56200 stub → imports core.mjs500Circular dep, esbuild chokes
cdn.jsdelivr.net/npm/typecomposer@0.1.56/+esm200, 117 KBRollup single-bundle, all exports present

Validation

# 1. CDN live check (from sandbox)
curl -sI https://cdn.jsdelivr.net/npm/typecomposer@0.1.56/+esm
→ HTTP/2 200, cache-control: public max-age=31536000 immutable, CORS: *
# 2. Named exports present
curl -s ... | grep -o "export{..."
→ export{..., V as Component, pn as HBox, Ge as DivElement, Se as ButtonElement, ...}
# 3. Build result
✓ 3486 modules transformed.
dist/assets/esbuild-BHljloGq.wasm 12,332.68 kB
dist/assets/index-C2_sis7Q.css 48.96 kB
dist/assets/index-Dgoy4g8A.js 1,754.62 kB ← contains jsdelivr URL (not esm.sh)
✓ built in 15.32s
# 4. No esm.sh URL in dist
grep -r "esm.sh" dist/ → (empty) ✓

Files changed

FileChange
src/views/playground/PlaygroundView.ts1 line: esm.shcdn.jsdelivr.net/…/+esm; updated JSDoc comment

Manual browser verification still needed

  1. Open deployed docs → Playground tab
  2. Confirm no net::ERR_ABORTED 500 in DevTools Network
  3. Confirm the demo AppPage renders (purple gradient + click button)

cc @zico15@joaodibba

esm.sh generates a split-entry bundle for typecomposer@0.1.56 that imports
a generated /es2022/core.mjs sub-path. That sub-path returns HTTP 500:
esbuild: No matching export in "node_modules/typecomposer/core/index.js"
for import "Component"
Root cause: typecomposer's core/* sub-modules re-import from "../.." (the
root index), creating a circular dependency that esbuild (used by esm.sh)
cannot resolve when building the split core.mjs entry.
Fix: switch the import map URL to jsDelivr's +esm endpoint, which uses
Rollup to produce a single-file ESM bundle from the root index.js directly.
The resulting bundle is a flat 117 KB file with all named exports present
and no circular dependency issue.
Before: https://esm.sh/typecomposer@${VERSION}
→ 200 stub → imports /es2022/core.mjs → 500
After: https://cdn.jsdelivr.net/npm/typecomposer@${VERSION}/+esm
→ 200 single-bundle ESM (Rollup, 117 KB, CORS: *)
Only PlaygroundView.ts is changed (1 line + updated comments).
Build: ✓ 3486 modules, esbuild.wasm in dist/assets/, 15.32s
@joaodibba
joaodibba merged commit d066bfe into TypeComposer:mainJun 12, 2026
1 check failed
@lucas-spin

Copy link
Copy Markdown
ContributorAuthor

Runtime Smoke Test — PR #35 (post-merge audit)

Tool: Playwright 1.60 + Chromium Headless Shell 148 (headless, no display)
Branch:lucas-spin:fix/playground-typecomposer-cdn @ 1846dcd
Server:npm run build + npx vite preview (production-like, port 4173)
Route tested:http://127.0.0.1:4173/ → click "Playground" link → http://127.0.0.1:4173/playground
Wait: 20–25 s for esbuild WASM init + compilation + iframe injection


✅ What the CDN fix got right

CheckResult
esm.sh in dist bundleABSENT — zero occurrences
esm.sh network requestsNONE — never fired
cdn.jsdelivr.net/npm/typecomposer@0.1.56/+esm HTTP status200 OK
Failed to fetch dynamically imported moduleNOT SEEN
esm.sh in console errorsNOT SEEN
Build succeeds✓ 3486 modules, 15.76s

The core CDN fix is correct. jsDelivr resolves the HTTP 500 that plagued esm.sh.


❌ New runtime blocker exposed by the fix

[ERROR] [Playground] Initialization failed:
SecurityError: Failed to read the 'localStorage' property from 'Window':
The document is sandboxed and lacks the 'allow-same-origin' flag.
at <instance_members_initializer>
(https://cdn.jsdelivr.net/npm/typecomposer@0.1.56/+esm:7:5728)

Root cause (two-layer):

  1. typecomposer@0.1.56/index.js does import "./global" unconditionally. global/app.js defines class __App__ with two class field initializers:

    #t =ref(this.getThemePreferred());// calls localStorage.getItem(...)
    #n =ref(this.getLanguagePreferred());// calls localStorage.getItem(...)

    These run at class definition time (before any constructor), so localStorage is accessed the moment the module is evaluated.

  2. The playground iframe has sandbox="allow-scripts"withoutallow-same-origin. A blob: URL in such an iframe gets an opaque origin — the browser blocks localStorage, sessionStorage, indexedDB, and same-origin fetch. The class field initializer throws SecurityError synchronously.

Was this introduced by PR #35?No. PR #35 is exactly 1 line: esm.shcdn.jsdelivr.net/…/+esm. The sandbox="allow-scripts" attribute was set in PR #34 (caa0490). The localStorage call is inside typecomposer@0.1.56 itself.

Why wasn't it visible before? With esm.sh the module load failed with HTTP 500 before any JS ran — the error was hidden. With jsDelivr the module loads successfully and the localStorage call executes.

Effect: The iframe renders an error panel ("Initialization Error: SecurityError…") instead of the default AppPage demo (purple gradient + click button). The playground is non-functional despite the CDN fix being correct.


✅/❌ Full checklist

RequirementResult
Preview iframe renders default demo content❌ — error panel shown
No Runtime Error in console❌ — Runtime Error: SecurityError…
No Initialization failed in console❌ — [Playground] Initialization failed: SecurityError…
No SecurityError❌ — SecurityError: Failed to read localStorage
No Failed to fetch dynamically imported module
No esm.sh in console
No network requests to esm.sh/typecomposer
cdn.jsdelivr.net/npm/typecomposer@0.1.56/+esm → 200
Editing code triggers recompile + preview update❌ untestable (init fails)

Recommended follow-up fix

This needs one of:

Option A (playground-side, minimal): Wrap global/app.js localStorage reads in try/catch upstream in typecomposer@0.1.56. The getThemePreferred() / getLanguagePreferred() methods should silently fall back to defaults when localStorage is unavailable.

Option B (playground-side, immediate workaround): In PlaygroundView.ts, add allow-same-origin to the iframe sandbox — but this weakens isolation because the iframe would share origin with the docs page.

Option C (playground-side, cleanest): Use srcdoc= to load the iframe instead of blob: src. srcdoc iframes inherit the parent origin when there's no sandbox, but with sandbox="allow-scripts allow-same-origin" the user code runs isolated yet with localStorage available.

The right long-term fix is Option A — typecomposer's global initializer should be resilient to restricted environments (workers, sandboxed iframes, SSR). This is a separate issue from PR #35.


Verdict: PR #35 needs a companion fix. The CDN URL change itself is correct and should stay. The localStorage error is a pre-existing layered bug (typecomposer core + PR #34's sandbox choice) that the CDN fix has now surfaced.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@lucas-spin@joaodibba
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(playground): replace esm.sh with jsDelivr +esm — fixes HTTP 500 on core.mjs - #35

Merged
joaodibba merged 1 commit into
TypeComposer:mainfrom
lucas-spin:fix/playground-cdn-jsdelivr-esm
Jun 12, 2026
Merged

fix(playground): replace esm.sh with jsDelivr +esm — fixes HTTP 500 on core.mjs#35
joaodibba merged 1 commit into
TypeComposer:mainfrom
lucas-spin:fix/playground-cdn-jsdelivr-esm

Conversation

@lucas-spin

Copy link
Copy Markdown
Contributor

Bug report (João)

GET https://esm.sh/typecomposer@0.1.56/es2022/core.mjs net::ERR_ABORTED 500
[Playground] Initialization failed: TypeError: Failed to fetch dynamically imported module:
https://esm.sh/typecomposer@0.1.56

Root cause

esm.sh auto-generates a split-entry bundle for typecomposer@0.1.56:

// https://esm.sh/typecomposer@0.1.56 ← 200, but…import"/typecomposer@0.1.56/es2022/core.mjs";// ← 500!import"/typecomposer@0.1.56/es2022/global.mjs";export*from"/typecomposer@0.1.56/es2022/typecomposer.mjs";

The generated core.mjs returns HTTP 500 with:

esbuild: No matching export in "node_modules/typecomposer/core/index.js" for import "Component"

Why?typecomposer's core/index.js re-exports from sub-packages (./element, ./components, …), and each of those sub-packages re-imports Component from "../.." (the root). This creates a circular dependency that esbuild — used internally by esm.sh — cannot resolve when building its split core.mjs entry.

The package has no "exports" map and uses "type": "module", so esm.sh tries to split on the bare subpath and fails.


Fix

Switch the import-map URL from esm.sh to jsDelivr +esm:

// Before (broken):consttypecomposerUrl=`https://esm.sh/typecomposer@${TYPECOMPOSER_VERSION}`;// After (working):consttypecomposerUrl=`https://cdn.jsdelivr.net/npm/typecomposer@${TYPECOMPOSER_VERSION}/+esm`;

jsDelivr's +esm endpoint uses Rollup to produce a single flat ES module from index.js directly, bypassing the circular subpath entirely. The result is a 117 KB file with all named exports (Component, VBox, DivElement, ButtonElement, etc.) and CORS headers.

CDNResponseNotes
esm.sh/typecomposer@0.1.56200 stub → imports core.mjs500Circular dep, esbuild chokes
cdn.jsdelivr.net/npm/typecomposer@0.1.56/+esm200, 117 KBRollup single-bundle, all exports present

Validation

# 1. CDN live check (from sandbox)
curl -sI https://cdn.jsdelivr.net/npm/typecomposer@0.1.56/+esm
→ HTTP/2 200, cache-control: public max-age=31536000 immutable, CORS: *
# 2. Named exports present
curl -s ... | grep -o "export{..."
→ export{..., V as Component, pn as HBox, Ge as DivElement, Se as ButtonElement, ...}
# 3. Build result
✓ 3486 modules transformed.
dist/assets/esbuild-BHljloGq.wasm 12,332.68 kB
dist/assets/index-C2_sis7Q.css 48.96 kB
dist/assets/index-Dgoy4g8A.js 1,754.62 kB ← contains jsdelivr URL (not esm.sh)
✓ built in 15.32s
# 4. No esm.sh URL in dist
grep -r "esm.sh" dist/ → (empty) ✓

Files changed

FileChange
src/views/playground/PlaygroundView.ts1 line: esm.shcdn.jsdelivr.net/…/+esm; updated JSDoc comment

Manual browser verification still needed

  1. Open deployed docs → Playground tab
  2. Confirm no net::ERR_ABORTED 500 in DevTools Network
  3. Confirm the demo AppPage renders (purple gradient + click button)

cc @zico15@joaodibba

esm.sh generates a split-entry bundle for typecomposer@0.1.56 that imports
a generated /es2022/core.mjs sub-path. That sub-path returns HTTP 500:
esbuild: No matching export in "node_modules/typecomposer/core/index.js"
for import "Component"
Root cause: typecomposer's core/* sub-modules re-import from "../.." (the
root index), creating a circular dependency that esbuild (used by esm.sh)
cannot resolve when building the split core.mjs entry.
Fix: switch the import map URL to jsDelivr's +esm endpoint, which uses
Rollup to produce a single-file ESM bundle from the root index.js directly.
The resulting bundle is a flat 117 KB file with all named exports present
and no circular dependency issue.
Before: https://esm.sh/typecomposer@${VERSION}
→ 200 stub → imports /es2022/core.mjs → 500
After: https://cdn.jsdelivr.net/npm/typecomposer@${VERSION}/+esm
→ 200 single-bundle ESM (Rollup, 117 KB, CORS: *)
Only PlaygroundView.ts is changed (1 line + updated comments).
Build: ✓ 3486 modules, esbuild.wasm in dist/assets/, 15.32s
@joaodibba
joaodibba merged commit d066bfe into TypeComposer:mainJun 12, 2026
1 check failed
@lucas-spin

Copy link
Copy Markdown
ContributorAuthor

Runtime Smoke Test — PR #35 (post-merge audit)

Tool: Playwright 1.60 + Chromium Headless Shell 148 (headless, no display)
Branch:lucas-spin:fix/playground-typecomposer-cdn @ 1846dcd
Server:npm run build + npx vite preview (production-like, port 4173)
Route tested:http://127.0.0.1:4173/ → click "Playground" link → http://127.0.0.1:4173/playground
Wait: 20–25 s for esbuild WASM init + compilation + iframe injection


✅ What the CDN fix got right

CheckResult
esm.sh in dist bundleABSENT — zero occurrences
esm.sh network requestsNONE — never fired
cdn.jsdelivr.net/npm/typecomposer@0.1.56/+esm HTTP status200 OK
Failed to fetch dynamically imported moduleNOT SEEN
esm.sh in console errorsNOT SEEN
Build succeeds✓ 3486 modules, 15.76s

The core CDN fix is correct. jsDelivr resolves the HTTP 500 that plagued esm.sh.


❌ New runtime blocker exposed by the fix

[ERROR] [Playground] Initialization failed:
SecurityError: Failed to read the 'localStorage' property from 'Window':
The document is sandboxed and lacks the 'allow-same-origin' flag.
at <instance_members_initializer>
(https://cdn.jsdelivr.net/npm/typecomposer@0.1.56/+esm:7:5728)

Root cause (two-layer):

  1. typecomposer@0.1.56/index.js does import "./global" unconditionally. global/app.js defines class __App__ with two class field initializers:

    #t =ref(this.getThemePreferred());// calls localStorage.getItem(...)
    #n =ref(this.getLanguagePreferred());// calls localStorage.getItem(...)

    These run at class definition time (before any constructor), so localStorage is accessed the moment the module is evaluated.

  2. The playground iframe has sandbox="allow-scripts"withoutallow-same-origin. A blob: URL in such an iframe gets an opaque origin — the browser blocks localStorage, sessionStorage, indexedDB, and same-origin fetch. The class field initializer throws SecurityError synchronously.

Was this introduced by PR #35?No. PR #35 is exactly 1 line: esm.shcdn.jsdelivr.net/…/+esm. The sandbox="allow-scripts" attribute was set in PR #34 (caa0490). The localStorage call is inside typecomposer@0.1.56 itself.

Why wasn't it visible before? With esm.sh the module load failed with HTTP 500 before any JS ran — the error was hidden. With jsDelivr the module loads successfully and the localStorage call executes.

Effect: The iframe renders an error panel ("Initialization Error: SecurityError…") instead of the default AppPage demo (purple gradient + click button). The playground is non-functional despite the CDN fix being correct.


✅/❌ Full checklist

RequirementResult
Preview iframe renders default demo content❌ — error panel shown
No Runtime Error in console❌ — Runtime Error: SecurityError…
No Initialization failed in console❌ — [Playground] Initialization failed: SecurityError…
No SecurityError❌ — SecurityError: Failed to read localStorage
No Failed to fetch dynamically imported module
No esm.sh in console
No network requests to esm.sh/typecomposer
cdn.jsdelivr.net/npm/typecomposer@0.1.56/+esm → 200
Editing code triggers recompile + preview update❌ untestable (init fails)

Recommended follow-up fix

This needs one of:

Option A (playground-side, minimal): Wrap global/app.js localStorage reads in try/catch upstream in typecomposer@0.1.56. The getThemePreferred() / getLanguagePreferred() methods should silently fall back to defaults when localStorage is unavailable.

Option B (playground-side, immediate workaround): In PlaygroundView.ts, add allow-same-origin to the iframe sandbox — but this weakens isolation because the iframe would share origin with the docs page.

Option C (playground-side, cleanest): Use srcdoc= to load the iframe instead of blob: src. srcdoc iframes inherit the parent origin when there's no sandbox, but with sandbox="allow-scripts allow-same-origin" the user code runs isolated yet with localStorage available.

The right long-term fix is Option A — typecomposer's global initializer should be resilient to restricted environments (workers, sandboxed iframes, SSR). This is a separate issue from PR #35.


Verdict: PR #35 needs a companion fix. The CDN URL change itself is correct and should stay. The localStorage error is a pre-existing layered bug (typecomposer core + PR #34's sandbox choice) that the CDN fix has now surfaced.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@lucas-spin@joaodibba
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(playground): replace esm.sh with jsDelivr +esm — fixes HTTP 500 on core.mjs - #35

Merged
joaodibba merged 1 commit into
TypeComposer:mainfrom
lucas-spin:fix/playground-cdn-jsdelivr-esm
Jun 12, 2026
Merged

fix(playground): replace esm.sh with jsDelivr +esm — fixes HTTP 500 on core.mjs#35
joaodibba merged 1 commit into
TypeComposer:mainfrom
lucas-spin:fix/playground-cdn-jsdelivr-esm

Conversation

@lucas-spin

Copy link
Copy Markdown
Contributor

Bug report (João)

GET https://esm.sh/typecomposer@0.1.56/es2022/core.mjs net::ERR_ABORTED 500
[Playground] Initialization failed: TypeError: Failed to fetch dynamically imported module:
https://esm.sh/typecomposer@0.1.56

Root cause

esm.sh auto-generates a split-entry bundle for typecomposer@0.1.56:

// https://esm.sh/typecomposer@0.1.56 ← 200, but…import"/typecomposer@0.1.56/es2022/core.mjs";// ← 500!import"/typecomposer@0.1.56/es2022/global.mjs";export*from"/typecomposer@0.1.56/es2022/typecomposer.mjs";

The generated core.mjs returns HTTP 500 with:

esbuild: No matching export in "node_modules/typecomposer/core/index.js" for import "Component"

Why?typecomposer's core/index.js re-exports from sub-packages (./element, ./components, …), and each of those sub-packages re-imports Component from "../.." (the root). This creates a circular dependency that esbuild — used internally by esm.sh — cannot resolve when building its split core.mjs entry.

The package has no "exports" map and uses "type": "module", so esm.sh tries to split on the bare subpath and fails.


Fix

Switch the import-map URL from esm.sh to jsDelivr +esm:

// Before (broken):consttypecomposerUrl=`https://esm.sh/typecomposer@${TYPECOMPOSER_VERSION}`;// After (working):consttypecomposerUrl=`https://cdn.jsdelivr.net/npm/typecomposer@${TYPECOMPOSER_VERSION}/+esm`;

jsDelivr's +esm endpoint uses Rollup to produce a single flat ES module from index.js directly, bypassing the circular subpath entirely. The result is a 117 KB file with all named exports (Component, VBox, DivElement, ButtonElement, etc.) and CORS headers.

CDNResponseNotes
esm.sh/typecomposer@0.1.56200 stub → imports core.mjs500Circular dep, esbuild chokes
cdn.jsdelivr.net/npm/typecomposer@0.1.56/+esm200, 117 KBRollup single-bundle, all exports present

Validation

# 1. CDN live check (from sandbox)
curl -sI https://cdn.jsdelivr.net/npm/typecomposer@0.1.56/+esm
→ HTTP/2 200, cache-control: public max-age=31536000 immutable, CORS: *
# 2. Named exports present
curl -s ... | grep -o "export{..."
→ export{..., V as Component, pn as HBox, Ge as DivElement, Se as ButtonElement, ...}
# 3. Build result
✓ 3486 modules transformed.
dist/assets/esbuild-BHljloGq.wasm 12,332.68 kB
dist/assets/index-C2_sis7Q.css 48.96 kB
dist/assets/index-Dgoy4g8A.js 1,754.62 kB ← contains jsdelivr URL (not esm.sh)
✓ built in 15.32s
# 4. No esm.sh URL in dist
grep -r "esm.sh" dist/ → (empty) ✓

Files changed

FileChange
src/views/playground/PlaygroundView.ts1 line: esm.shcdn.jsdelivr.net/…/+esm; updated JSDoc comment

Manual browser verification still needed

  1. Open deployed docs → Playground tab
  2. Confirm no net::ERR_ABORTED 500 in DevTools Network
  3. Confirm the demo AppPage renders (purple gradient + click button)

cc @zico15@joaodibba

esm.sh generates a split-entry bundle for typecomposer@0.1.56 that imports
a generated /es2022/core.mjs sub-path. That sub-path returns HTTP 500:
esbuild: No matching export in "node_modules/typecomposer/core/index.js"
for import "Component"
Root cause: typecomposer's core/* sub-modules re-import from "../.." (the
root index), creating a circular dependency that esbuild (used by esm.sh)
cannot resolve when building the split core.mjs entry.
Fix: switch the import map URL to jsDelivr's +esm endpoint, which uses
Rollup to produce a single-file ESM bundle from the root index.js directly.
The resulting bundle is a flat 117 KB file with all named exports present
and no circular dependency issue.
Before: https://esm.sh/typecomposer@${VERSION}
→ 200 stub → imports /es2022/core.mjs → 500
After: https://cdn.jsdelivr.net/npm/typecomposer@${VERSION}/+esm
→ 200 single-bundle ESM (Rollup, 117 KB, CORS: *)
Only PlaygroundView.ts is changed (1 line + updated comments).
Build: ✓ 3486 modules, esbuild.wasm in dist/assets/, 15.32s
@joaodibba
joaodibba merged commit d066bfe into TypeComposer:mainJun 12, 2026
1 check failed
@lucas-spin

Copy link
Copy Markdown
ContributorAuthor

Runtime Smoke Test — PR #35 (post-merge audit)

Tool: Playwright 1.60 + Chromium Headless Shell 148 (headless, no display)
Branch:lucas-spin:fix/playground-typecomposer-cdn @ 1846dcd
Server:npm run build + npx vite preview (production-like, port 4173)
Route tested:http://127.0.0.1:4173/ → click "Playground" link → http://127.0.0.1:4173/playground
Wait: 20–25 s for esbuild WASM init + compilation + iframe injection


✅ What the CDN fix got right

CheckResult
esm.sh in dist bundleABSENT — zero occurrences
esm.sh network requestsNONE — never fired
cdn.jsdelivr.net/npm/typecomposer@0.1.56/+esm HTTP status200 OK
Failed to fetch dynamically imported moduleNOT SEEN
esm.sh in console errorsNOT SEEN
Build succeeds✓ 3486 modules, 15.76s

The core CDN fix is correct. jsDelivr resolves the HTTP 500 that plagued esm.sh.


❌ New runtime blocker exposed by the fix

[ERROR] [Playground] Initialization failed:
SecurityError: Failed to read the 'localStorage' property from 'Window':
The document is sandboxed and lacks the 'allow-same-origin' flag.
at <instance_members_initializer>
(https://cdn.jsdelivr.net/npm/typecomposer@0.1.56/+esm:7:5728)

Root cause (two-layer):

  1. typecomposer@0.1.56/index.js does import "./global" unconditionally. global/app.js defines class __App__ with two class field initializers:

    #t =ref(this.getThemePreferred());// calls localStorage.getItem(...)
    #n =ref(this.getLanguagePreferred());// calls localStorage.getItem(...)

    These run at class definition time (before any constructor), so localStorage is accessed the moment the module is evaluated.

  2. The playground iframe has sandbox="allow-scripts"withoutallow-same-origin. A blob: URL in such an iframe gets an opaque origin — the browser blocks localStorage, sessionStorage, indexedDB, and same-origin fetch. The class field initializer throws SecurityError synchronously.

Was this introduced by PR #35?No. PR #35 is exactly 1 line: esm.shcdn.jsdelivr.net/…/+esm. The sandbox="allow-scripts" attribute was set in PR #34 (caa0490). The localStorage call is inside typecomposer@0.1.56 itself.

Why wasn't it visible before? With esm.sh the module load failed with HTTP 500 before any JS ran — the error was hidden. With jsDelivr the module loads successfully and the localStorage call executes.

Effect: The iframe renders an error panel ("Initialization Error: SecurityError…") instead of the default AppPage demo (purple gradient + click button). The playground is non-functional despite the CDN fix being correct.


✅/❌ Full checklist

RequirementResult
Preview iframe renders default demo content❌ — error panel shown
No Runtime Error in console❌ — Runtime Error: SecurityError…
No Initialization failed in console❌ — [Playground] Initialization failed: SecurityError…
No SecurityError❌ — SecurityError: Failed to read localStorage
No Failed to fetch dynamically imported module
No esm.sh in console
No network requests to esm.sh/typecomposer
cdn.jsdelivr.net/npm/typecomposer@0.1.56/+esm → 200
Editing code triggers recompile + preview update❌ untestable (init fails)

Recommended follow-up fix

This needs one of:

Option A (playground-side, minimal): Wrap global/app.js localStorage reads in try/catch upstream in typecomposer@0.1.56. The getThemePreferred() / getLanguagePreferred() methods should silently fall back to defaults when localStorage is unavailable.

Option B (playground-side, immediate workaround): In PlaygroundView.ts, add allow-same-origin to the iframe sandbox — but this weakens isolation because the iframe would share origin with the docs page.

Option C (playground-side, cleanest): Use srcdoc= to load the iframe instead of blob: src. srcdoc iframes inherit the parent origin when there's no sandbox, but with sandbox="allow-scripts allow-same-origin" the user code runs isolated yet with localStorage available.

The right long-term fix is Option A — typecomposer's global initializer should be resilient to restricted environments (workers, sandboxed iframes, SSR). This is a separate issue from PR #35.


Verdict: PR #35 needs a companion fix. The CDN URL change itself is correct and should stay. The localStorage error is a pre-existing layered bug (typecomposer core + PR #34's sandbox choice) that the CDN fix has now surfaced.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@lucas-spin@joaodibba
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(playground): replace esm.sh with jsDelivr +esm — fixes HTTP 500 on core.mjs - #35

Merged
joaodibba merged 1 commit into
TypeComposer:mainfrom
lucas-spin:fix/playground-cdn-jsdelivr-esm
Jun 12, 2026
Merged

fix(playground): replace esm.sh with jsDelivr +esm — fixes HTTP 500 on core.mjs#35
joaodibba merged 1 commit into
TypeComposer:mainfrom
lucas-spin:fix/playground-cdn-jsdelivr-esm

Conversation

@lucas-spin

Copy link
Copy Markdown
Contributor

Bug report (João)

GET https://esm.sh/typecomposer@0.1.56/es2022/core.mjs net::ERR_ABORTED 500
[Playground] Initialization failed: TypeError: Failed to fetch dynamically imported module:
https://esm.sh/typecomposer@0.1.56

Root cause

esm.sh auto-generates a split-entry bundle for typecomposer@0.1.56:

// https://esm.sh/typecomposer@0.1.56 ← 200, but…import"/typecomposer@0.1.56/es2022/core.mjs";// ← 500!import"/typecomposer@0.1.56/es2022/global.mjs";export*from"/typecomposer@0.1.56/es2022/typecomposer.mjs";

The generated core.mjs returns HTTP 500 with:

esbuild: No matching export in "node_modules/typecomposer/core/index.js" for import "Component"

Why?typecomposer's core/index.js re-exports from sub-packages (./element, ./components, …), and each of those sub-packages re-imports Component from "../.." (the root). This creates a circular dependency that esbuild — used internally by esm.sh — cannot resolve when building its split core.mjs entry.

The package has no "exports" map and uses "type": "module", so esm.sh tries to split on the bare subpath and fails.


Fix

Switch the import-map URL from esm.sh to jsDelivr +esm:

// Before (broken):consttypecomposerUrl=`https://esm.sh/typecomposer@${TYPECOMPOSER_VERSION}`;// After (working):consttypecomposerUrl=`https://cdn.jsdelivr.net/npm/typecomposer@${TYPECOMPOSER_VERSION}/+esm`;

jsDelivr's +esm endpoint uses Rollup to produce a single flat ES module from index.js directly, bypassing the circular subpath entirely. The result is a 117 KB file with all named exports (Component, VBox, DivElement, ButtonElement, etc.) and CORS headers.

CDNResponseNotes
esm.sh/typecomposer@0.1.56200 stub → imports core.mjs500Circular dep, esbuild chokes
cdn.jsdelivr.net/npm/typecomposer@0.1.56/+esm200, 117 KBRollup single-bundle, all exports present

Validation

# 1. CDN live check (from sandbox)
curl -sI https://cdn.jsdelivr.net/npm/typecomposer@0.1.56/+esm
→ HTTP/2 200, cache-control: public max-age=31536000 immutable, CORS: *
# 2. Named exports present
curl -s ... | grep -o "export{..."
→ export{..., V as Component, pn as HBox, Ge as DivElement, Se as ButtonElement, ...}
# 3. Build result
✓ 3486 modules transformed.
dist/assets/esbuild-BHljloGq.wasm 12,332.68 kB
dist/assets/index-C2_sis7Q.css 48.96 kB
dist/assets/index-Dgoy4g8A.js 1,754.62 kB ← contains jsdelivr URL (not esm.sh)
✓ built in 15.32s
# 4. No esm.sh URL in dist
grep -r "esm.sh" dist/ → (empty) ✓

Files changed

FileChange
src/views/playground/PlaygroundView.ts1 line: esm.shcdn.jsdelivr.net/…/+esm; updated JSDoc comment

Manual browser verification still needed

  1. Open deployed docs → Playground tab
  2. Confirm no net::ERR_ABORTED 500 in DevTools Network
  3. Confirm the demo AppPage renders (purple gradient + click button)

cc @zico15@joaodibba

esm.sh generates a split-entry bundle for typecomposer@0.1.56 that imports
a generated /es2022/core.mjs sub-path. That sub-path returns HTTP 500:
esbuild: No matching export in "node_modules/typecomposer/core/index.js"
for import "Component"
Root cause: typecomposer's core/* sub-modules re-import from "../.." (the
root index), creating a circular dependency that esbuild (used by esm.sh)
cannot resolve when building the split core.mjs entry.
Fix: switch the import map URL to jsDelivr's +esm endpoint, which uses
Rollup to produce a single-file ESM bundle from the root index.js directly.
The resulting bundle is a flat 117 KB file with all named exports present
and no circular dependency issue.
Before: https://esm.sh/typecomposer@${VERSION}
→ 200 stub → imports /es2022/core.mjs → 500
After: https://cdn.jsdelivr.net/npm/typecomposer@${VERSION}/+esm
→ 200 single-bundle ESM (Rollup, 117 KB, CORS: *)
Only PlaygroundView.ts is changed (1 line + updated comments).
Build: ✓ 3486 modules, esbuild.wasm in dist/assets/, 15.32s
@joaodibba
joaodibba merged commit d066bfe into TypeComposer:mainJun 12, 2026
1 check failed
@lucas-spin

Copy link
Copy Markdown
ContributorAuthor

Runtime Smoke Test — PR #35 (post-merge audit)

Tool: Playwright 1.60 + Chromium Headless Shell 148 (headless, no display)
Branch:lucas-spin:fix/playground-typecomposer-cdn @ 1846dcd
Server:npm run build + npx vite preview (production-like, port 4173)
Route tested:http://127.0.0.1:4173/ → click "Playground" link → http://127.0.0.1:4173/playground
Wait: 20–25 s for esbuild WASM init + compilation + iframe injection


✅ What the CDN fix got right

CheckResult
esm.sh in dist bundleABSENT — zero occurrences
esm.sh network requestsNONE — never fired
cdn.jsdelivr.net/npm/typecomposer@0.1.56/+esm HTTP status200 OK
Failed to fetch dynamically imported moduleNOT SEEN
esm.sh in console errorsNOT SEEN
Build succeeds✓ 3486 modules, 15.76s

The core CDN fix is correct. jsDelivr resolves the HTTP 500 that plagued esm.sh.


❌ New runtime blocker exposed by the fix

[ERROR] [Playground] Initialization failed:
SecurityError: Failed to read the 'localStorage' property from 'Window':
The document is sandboxed and lacks the 'allow-same-origin' flag.
at <instance_members_initializer>
(https://cdn.jsdelivr.net/npm/typecomposer@0.1.56/+esm:7:5728)

Root cause (two-layer):

  1. typecomposer@0.1.56/index.js does import "./global" unconditionally. global/app.js defines class __App__ with two class field initializers:

    #t =ref(this.getThemePreferred());// calls localStorage.getItem(...)
    #n =ref(this.getLanguagePreferred());// calls localStorage.getItem(...)

    These run at class definition time (before any constructor), so localStorage is accessed the moment the module is evaluated.

  2. The playground iframe has sandbox="allow-scripts"withoutallow-same-origin. A blob: URL in such an iframe gets an opaque origin — the browser blocks localStorage, sessionStorage, indexedDB, and same-origin fetch. The class field initializer throws SecurityError synchronously.

Was this introduced by PR #35?No. PR #35 is exactly 1 line: esm.shcdn.jsdelivr.net/…/+esm. The sandbox="allow-scripts" attribute was set in PR #34 (caa0490). The localStorage call is inside typecomposer@0.1.56 itself.

Why wasn't it visible before? With esm.sh the module load failed with HTTP 500 before any JS ran — the error was hidden. With jsDelivr the module loads successfully and the localStorage call executes.

Effect: The iframe renders an error panel ("Initialization Error: SecurityError…") instead of the default AppPage demo (purple gradient + click button). The playground is non-functional despite the CDN fix being correct.


✅/❌ Full checklist

RequirementResult
Preview iframe renders default demo content❌ — error panel shown
No Runtime Error in console❌ — Runtime Error: SecurityError…
No Initialization failed in console❌ — [Playground] Initialization failed: SecurityError…
No SecurityError❌ — SecurityError: Failed to read localStorage
No Failed to fetch dynamically imported module
No esm.sh in console
No network requests to esm.sh/typecomposer
cdn.jsdelivr.net/npm/typecomposer@0.1.56/+esm → 200
Editing code triggers recompile + preview update❌ untestable (init fails)

Recommended follow-up fix

This needs one of:

Option A (playground-side, minimal): Wrap global/app.js localStorage reads in try/catch upstream in typecomposer@0.1.56. The getThemePreferred() / getLanguagePreferred() methods should silently fall back to defaults when localStorage is unavailable.

Option B (playground-side, immediate workaround): In PlaygroundView.ts, add allow-same-origin to the iframe sandbox — but this weakens isolation because the iframe would share origin with the docs page.

Option C (playground-side, cleanest): Use srcdoc= to load the iframe instead of blob: src. srcdoc iframes inherit the parent origin when there's no sandbox, but with sandbox="allow-scripts allow-same-origin" the user code runs isolated yet with localStorage available.

The right long-term fix is Option A — typecomposer's global initializer should be resilient to restricted environments (workers, sandboxed iframes, SSR). This is a separate issue from PR #35.


Verdict: PR #35 needs a companion fix. The CDN URL change itself is correct and should stay. The localStorage error is a pre-existing layered bug (typecomposer core + PR #34's sandbox choice) that the CDN fix has now surfaced.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@lucas-spin@joaodibba
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(playground): replace esm.sh with jsDelivr +esm — fixes HTTP 500 on core.mjs - #35

Merged
joaodibba merged 1 commit into
TypeComposer:mainfrom
lucas-spin:fix/playground-cdn-jsdelivr-esm
Jun 12, 2026
Merged

fix(playground): replace esm.sh with jsDelivr +esm — fixes HTTP 500 on core.mjs#35
joaodibba merged 1 commit into
TypeComposer:mainfrom
lucas-spin:fix/playground-cdn-jsdelivr-esm

Conversation

@lucas-spin

Copy link
Copy Markdown
Contributor

Bug report (João)

GET https://esm.sh/typecomposer@0.1.56/es2022/core.mjs net::ERR_ABORTED 500
[Playground] Initialization failed: TypeError: Failed to fetch dynamically imported module:
https://esm.sh/typecomposer@0.1.56

Root cause

esm.sh auto-generates a split-entry bundle for typecomposer@0.1.56:

// https://esm.sh/typecomposer@0.1.56 ← 200, but…import"/typecomposer@0.1.56/es2022/core.mjs";// ← 500!import"/typecomposer@0.1.56/es2022/global.mjs";export*from"/typecomposer@0.1.56/es2022/typecomposer.mjs";

The generated core.mjs returns HTTP 500 with:

esbuild: No matching export in "node_modules/typecomposer/core/index.js" for import "Component"

Why?typecomposer's core/index.js re-exports from sub-packages (./element, ./components, …), and each of those sub-packages re-imports Component from "../.." (the root). This creates a circular dependency that esbuild — used internally by esm.sh — cannot resolve when building its split core.mjs entry.

The package has no "exports" map and uses "type": "module", so esm.sh tries to split on the bare subpath and fails.


Fix

Switch the import-map URL from esm.sh to jsDelivr +esm:

// Before (broken):consttypecomposerUrl=`https://esm.sh/typecomposer@${TYPECOMPOSER_VERSION}`;// After (working):consttypecomposerUrl=`https://cdn.jsdelivr.net/npm/typecomposer@${TYPECOMPOSER_VERSION}/+esm`;

jsDelivr's +esm endpoint uses Rollup to produce a single flat ES module from index.js directly, bypassing the circular subpath entirely. The result is a 117 KB file with all named exports (Component, VBox, DivElement, ButtonElement, etc.) and CORS headers.

CDNResponseNotes
esm.sh/typecomposer@0.1.56200 stub → imports core.mjs500Circular dep, esbuild chokes
cdn.jsdelivr.net/npm/typecomposer@0.1.56/+esm200, 117 KBRollup single-bundle, all exports present

Validation

# 1. CDN live check (from sandbox)
curl -sI https://cdn.jsdelivr.net/npm/typecomposer@0.1.56/+esm
→ HTTP/2 200, cache-control: public max-age=31536000 immutable, CORS: *
# 2. Named exports present
curl -s ... | grep -o "export{..."
→ export{..., V as Component, pn as HBox, Ge as DivElement, Se as ButtonElement, ...}
# 3. Build result
✓ 3486 modules transformed.
dist/assets/esbuild-BHljloGq.wasm 12,332.68 kB
dist/assets/index-C2_sis7Q.css 48.96 kB
dist/assets/index-Dgoy4g8A.js 1,754.62 kB ← contains jsdelivr URL (not esm.sh)
✓ built in 15.32s
# 4. No esm.sh URL in dist
grep -r "esm.sh" dist/ → (empty) ✓

Files changed

FileChange
src/views/playground/PlaygroundView.ts1 line: esm.shcdn.jsdelivr.net/…/+esm; updated JSDoc comment

Manual browser verification still needed

  1. Open deployed docs → Playground tab
  2. Confirm no net::ERR_ABORTED 500 in DevTools Network
  3. Confirm the demo AppPage renders (purple gradient + click button)

cc @zico15@joaodibba

esm.sh generates a split-entry bundle for typecomposer@0.1.56 that imports
a generated /es2022/core.mjs sub-path. That sub-path returns HTTP 500:
esbuild: No matching export in "node_modules/typecomposer/core/index.js"
for import "Component"
Root cause: typecomposer's core/* sub-modules re-import from "../.." (the
root index), creating a circular dependency that esbuild (used by esm.sh)
cannot resolve when building the split core.mjs entry.
Fix: switch the import map URL to jsDelivr's +esm endpoint, which uses
Rollup to produce a single-file ESM bundle from the root index.js directly.
The resulting bundle is a flat 117 KB file with all named exports present
and no circular dependency issue.
Before: https://esm.sh/typecomposer@${VERSION}
→ 200 stub → imports /es2022/core.mjs → 500
After: https://cdn.jsdelivr.net/npm/typecomposer@${VERSION}/+esm
→ 200 single-bundle ESM (Rollup, 117 KB, CORS: *)
Only PlaygroundView.ts is changed (1 line + updated comments).
Build: ✓ 3486 modules, esbuild.wasm in dist/assets/, 15.32s
@joaodibba
joaodibba merged commit d066bfe into TypeComposer:mainJun 12, 2026
1 check failed
@lucas-spin

Copy link
Copy Markdown
ContributorAuthor

Runtime Smoke Test — PR #35 (post-merge audit)

Tool: Playwright 1.60 + Chromium Headless Shell 148 (headless, no display)
Branch:lucas-spin:fix/playground-typecomposer-cdn @ 1846dcd
Server:npm run build + npx vite preview (production-like, port 4173)
Route tested:http://127.0.0.1:4173/ → click "Playground" link → http://127.0.0.1:4173/playground
Wait: 20–25 s for esbuild WASM init + compilation + iframe injection


✅ What the CDN fix got right

CheckResult
esm.sh in dist bundleABSENT — zero occurrences
esm.sh network requestsNONE — never fired
cdn.jsdelivr.net/npm/typecomposer@0.1.56/+esm HTTP status200 OK
Failed to fetch dynamically imported moduleNOT SEEN
esm.sh in console errorsNOT SEEN
Build succeeds✓ 3486 modules, 15.76s

The core CDN fix is correct. jsDelivr resolves the HTTP 500 that plagued esm.sh.


❌ New runtime blocker exposed by the fix

[ERROR] [Playground] Initialization failed:
SecurityError: Failed to read the 'localStorage' property from 'Window':
The document is sandboxed and lacks the 'allow-same-origin' flag.
at <instance_members_initializer>
(https://cdn.jsdelivr.net/npm/typecomposer@0.1.56/+esm:7:5728)

Root cause (two-layer):

  1. typecomposer@0.1.56/index.js does import "./global" unconditionally. global/app.js defines class __App__ with two class field initializers:

    #t =ref(this.getThemePreferred());// calls localStorage.getItem(...)
    #n =ref(this.getLanguagePreferred());// calls localStorage.getItem(...)

    These run at class definition time (before any constructor), so localStorage is accessed the moment the module is evaluated.

  2. The playground iframe has sandbox="allow-scripts"withoutallow-same-origin. A blob: URL in such an iframe gets an opaque origin — the browser blocks localStorage, sessionStorage, indexedDB, and same-origin fetch. The class field initializer throws SecurityError synchronously.

Was this introduced by PR #35?No. PR #35 is exactly 1 line: esm.shcdn.jsdelivr.net/…/+esm. The sandbox="allow-scripts" attribute was set in PR #34 (caa0490). The localStorage call is inside typecomposer@0.1.56 itself.

Why wasn't it visible before? With esm.sh the module load failed with HTTP 500 before any JS ran — the error was hidden. With jsDelivr the module loads successfully and the localStorage call executes.

Effect: The iframe renders an error panel ("Initialization Error: SecurityError…") instead of the default AppPage demo (purple gradient + click button). The playground is non-functional despite the CDN fix being correct.


✅/❌ Full checklist

RequirementResult
Preview iframe renders default demo content❌ — error panel shown
No Runtime Error in console❌ — Runtime Error: SecurityError…
No Initialization failed in console❌ — [Playground] Initialization failed: SecurityError…
No SecurityError❌ — SecurityError: Failed to read localStorage
No Failed to fetch dynamically imported module
No esm.sh in console
No network requests to esm.sh/typecomposer
cdn.jsdelivr.net/npm/typecomposer@0.1.56/+esm → 200
Editing code triggers recompile + preview update❌ untestable (init fails)

Recommended follow-up fix

This needs one of:

Option A (playground-side, minimal): Wrap global/app.js localStorage reads in try/catch upstream in typecomposer@0.1.56. The getThemePreferred() / getLanguagePreferred() methods should silently fall back to defaults when localStorage is unavailable.

Option B (playground-side, immediate workaround): In PlaygroundView.ts, add allow-same-origin to the iframe sandbox — but this weakens isolation because the iframe would share origin with the docs page.

Option C (playground-side, cleanest): Use srcdoc= to load the iframe instead of blob: src. srcdoc iframes inherit the parent origin when there's no sandbox, but with sandbox="allow-scripts allow-same-origin" the user code runs isolated yet with localStorage available.

The right long-term fix is Option A — typecomposer's global initializer should be resilient to restricted environments (workers, sandboxed iframes, SSR). This is a separate issue from PR #35.


Verdict: PR #35 needs a companion fix. The CDN URL change itself is correct and should stay. The localStorage error is a pre-existing layered bug (typecomposer core + PR #34's sandbox choice) that the CDN fix has now surfaced.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@lucas-spin@joaodibba
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(playground): replace esm.sh with jsDelivr +esm — fixes HTTP 500 on core.mjs - #35

Merged
joaodibba merged 1 commit into
TypeComposer:mainfrom
lucas-spin:fix/playground-cdn-jsdelivr-esm
Jun 12, 2026
Merged

fix(playground): replace esm.sh with jsDelivr +esm — fixes HTTP 500 on core.mjs#35
joaodibba merged 1 commit into
TypeComposer:mainfrom
lucas-spin:fix/playground-cdn-jsdelivr-esm

Conversation

@lucas-spin

Copy link
Copy Markdown
Contributor

Bug report (João)

GET https://esm.sh/typecomposer@0.1.56/es2022/core.mjs net::ERR_ABORTED 500
[Playground] Initialization failed: TypeError: Failed to fetch dynamically imported module:
https://esm.sh/typecomposer@0.1.56

Root cause

esm.sh auto-generates a split-entry bundle for typecomposer@0.1.56:

// https://esm.sh/typecomposer@0.1.56 ← 200, but…import"/typecomposer@0.1.56/es2022/core.mjs";// ← 500!import"/typecomposer@0.1.56/es2022/global.mjs";export*from"/typecomposer@0.1.56/es2022/typecomposer.mjs";

The generated core.mjs returns HTTP 500 with:

esbuild: No matching export in "node_modules/typecomposer/core/index.js" for import "Component"

Why?typecomposer's core/index.js re-exports from sub-packages (./element, ./components, …), and each of those sub-packages re-imports Component from "../.." (the root). This creates a circular dependency that esbuild — used internally by esm.sh — cannot resolve when building its split core.mjs entry.

The package has no "exports" map and uses "type": "module", so esm.sh tries to split on the bare subpath and fails.


Fix

Switch the import-map URL from esm.sh to jsDelivr +esm:

// Before (broken):consttypecomposerUrl=`https://esm.sh/typecomposer@${TYPECOMPOSER_VERSION}`;// After (working):consttypecomposerUrl=`https://cdn.jsdelivr.net/npm/typecomposer@${TYPECOMPOSER_VERSION}/+esm`;

jsDelivr's +esm endpoint uses Rollup to produce a single flat ES module from index.js directly, bypassing the circular subpath entirely. The result is a 117 KB file with all named exports (Component, VBox, DivElement, ButtonElement, etc.) and CORS headers.

CDNResponseNotes
esm.sh/typecomposer@0.1.56200 stub → imports core.mjs500Circular dep, esbuild chokes
cdn.jsdelivr.net/npm/typecomposer@0.1.56/+esm200, 117 KBRollup single-bundle, all exports present

Validation

# 1. CDN live check (from sandbox)
curl -sI https://cdn.jsdelivr.net/npm/typecomposer@0.1.56/+esm
→ HTTP/2 200, cache-control: public max-age=31536000 immutable, CORS: *
# 2. Named exports present
curl -s ... | grep -o "export{..."
→ export{..., V as Component, pn as HBox, Ge as DivElement, Se as ButtonElement, ...}
# 3. Build result
✓ 3486 modules transformed.
dist/assets/esbuild-BHljloGq.wasm 12,332.68 kB
dist/assets/index-C2_sis7Q.css 48.96 kB
dist/assets/index-Dgoy4g8A.js 1,754.62 kB ← contains jsdelivr URL (not esm.sh)
✓ built in 15.32s
# 4. No esm.sh URL in dist
grep -r "esm.sh" dist/ → (empty) ✓

Files changed

FileChange
src/views/playground/PlaygroundView.ts1 line: esm.shcdn.jsdelivr.net/…/+esm; updated JSDoc comment

Manual browser verification still needed

  1. Open deployed docs → Playground tab
  2. Confirm no net::ERR_ABORTED 500 in DevTools Network
  3. Confirm the demo AppPage renders (purple gradient + click button)

cc @zico15@joaodibba

esm.sh generates a split-entry bundle for typecomposer@0.1.56 that imports
a generated /es2022/core.mjs sub-path. That sub-path returns HTTP 500:
esbuild: No matching export in "node_modules/typecomposer/core/index.js"
for import "Component"
Root cause: typecomposer's core/* sub-modules re-import from "../.." (the
root index), creating a circular dependency that esbuild (used by esm.sh)
cannot resolve when building the split core.mjs entry.
Fix: switch the import map URL to jsDelivr's +esm endpoint, which uses
Rollup to produce a single-file ESM bundle from the root index.js directly.
The resulting bundle is a flat 117 KB file with all named exports present
and no circular dependency issue.
Before: https://esm.sh/typecomposer@${VERSION}
→ 200 stub → imports /es2022/core.mjs → 500
After: https://cdn.jsdelivr.net/npm/typecomposer@${VERSION}/+esm
→ 200 single-bundle ESM (Rollup, 117 KB, CORS: *)
Only PlaygroundView.ts is changed (1 line + updated comments).
Build: ✓ 3486 modules, esbuild.wasm in dist/assets/, 15.32s
@joaodibba
joaodibba merged commit d066bfe into TypeComposer:mainJun 12, 2026
1 check failed
@lucas-spin

Copy link
Copy Markdown
ContributorAuthor

Runtime Smoke Test — PR #35 (post-merge audit)

Tool: Playwright 1.60 + Chromium Headless Shell 148 (headless, no display)
Branch:lucas-spin:fix/playground-typecomposer-cdn @ 1846dcd
Server:npm run build + npx vite preview (production-like, port 4173)
Route tested:http://127.0.0.1:4173/ → click "Playground" link → http://127.0.0.1:4173/playground
Wait: 20–25 s for esbuild WASM init + compilation + iframe injection


✅ What the CDN fix got right

CheckResult
esm.sh in dist bundleABSENT — zero occurrences
esm.sh network requestsNONE — never fired
cdn.jsdelivr.net/npm/typecomposer@0.1.56/+esm HTTP status200 OK
Failed to fetch dynamically imported moduleNOT SEEN
esm.sh in console errorsNOT SEEN
Build succeeds✓ 3486 modules, 15.76s

The core CDN fix is correct. jsDelivr resolves the HTTP 500 that plagued esm.sh.


❌ New runtime blocker exposed by the fix

[ERROR] [Playground] Initialization failed:
SecurityError: Failed to read the 'localStorage' property from 'Window':
The document is sandboxed and lacks the 'allow-same-origin' flag.
at <instance_members_initializer>
(https://cdn.jsdelivr.net/npm/typecomposer@0.1.56/+esm:7:5728)

Root cause (two-layer):

  1. typecomposer@0.1.56/index.js does import "./global" unconditionally. global/app.js defines class __App__ with two class field initializers:

    #t =ref(this.getThemePreferred());// calls localStorage.getItem(...)
    #n =ref(this.getLanguagePreferred());// calls localStorage.getItem(...)

    These run at class definition time (before any constructor), so localStorage is accessed the moment the module is evaluated.

  2. The playground iframe has sandbox="allow-scripts"withoutallow-same-origin. A blob: URL in such an iframe gets an opaque origin — the browser blocks localStorage, sessionStorage, indexedDB, and same-origin fetch. The class field initializer throws SecurityError synchronously.

Was this introduced by PR #35?No. PR #35 is exactly 1 line: esm.shcdn.jsdelivr.net/…/+esm. The sandbox="allow-scripts" attribute was set in PR #34 (caa0490). The localStorage call is inside typecomposer@0.1.56 itself.

Why wasn't it visible before? With esm.sh the module load failed with HTTP 500 before any JS ran — the error was hidden. With jsDelivr the module loads successfully and the localStorage call executes.

Effect: The iframe renders an error panel ("Initialization Error: SecurityError…") instead of the default AppPage demo (purple gradient + click button). The playground is non-functional despite the CDN fix being correct.


✅/❌ Full checklist

RequirementResult
Preview iframe renders default demo content❌ — error panel shown
No Runtime Error in console❌ — Runtime Error: SecurityError…
No Initialization failed in console❌ — [Playground] Initialization failed: SecurityError…
No SecurityError❌ — SecurityError: Failed to read localStorage
No Failed to fetch dynamically imported module
No esm.sh in console
No network requests to esm.sh/typecomposer
cdn.jsdelivr.net/npm/typecomposer@0.1.56/+esm → 200
Editing code triggers recompile + preview update❌ untestable (init fails)

Recommended follow-up fix

This needs one of:

Option A (playground-side, minimal): Wrap global/app.js localStorage reads in try/catch upstream in typecomposer@0.1.56. The getThemePreferred() / getLanguagePreferred() methods should silently fall back to defaults when localStorage is unavailable.

Option B (playground-side, immediate workaround): In PlaygroundView.ts, add allow-same-origin to the iframe sandbox — but this weakens isolation because the iframe would share origin with the docs page.

Option C (playground-side, cleanest): Use srcdoc= to load the iframe instead of blob: src. srcdoc iframes inherit the parent origin when there's no sandbox, but with sandbox="allow-scripts allow-same-origin" the user code runs isolated yet with localStorage available.

The right long-term fix is Option A — typecomposer's global initializer should be resilient to restricted environments (workers, sandboxed iframes, SSR). This is a separate issue from PR #35.


Verdict: PR #35 needs a companion fix. The CDN URL change itself is correct and should stay. The localStorage error is a pre-existing layered bug (typecomposer core + PR #34's sandbox choice) that the CDN fix has now surfaced.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@lucas-spin@joaodibba
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(playground): replace esm.sh with jsDelivr +esm — fixes HTTP 500 on core.mjs - #35

Merged
joaodibba merged 1 commit into
TypeComposer:mainfrom
lucas-spin:fix/playground-cdn-jsdelivr-esm
Jun 12, 2026
Merged

fix(playground): replace esm.sh with jsDelivr +esm — fixes HTTP 500 on core.mjs#35
joaodibba merged 1 commit into
TypeComposer:mainfrom
lucas-spin:fix/playground-cdn-jsdelivr-esm

Conversation

@lucas-spin

Copy link
Copy Markdown
Contributor

Bug report (João)

GET https://esm.sh/typecomposer@0.1.56/es2022/core.mjs net::ERR_ABORTED 500
[Playground] Initialization failed: TypeError: Failed to fetch dynamically imported module:
https://esm.sh/typecomposer@0.1.56

Root cause

esm.sh auto-generates a split-entry bundle for typecomposer@0.1.56:

// https://esm.sh/typecomposer@0.1.56 ← 200, but…import"/typecomposer@0.1.56/es2022/core.mjs";// ← 500!import"/typecomposer@0.1.56/es2022/global.mjs";export*from"/typecomposer@0.1.56/es2022/typecomposer.mjs";

The generated core.mjs returns HTTP 500 with:

esbuild: No matching export in "node_modules/typecomposer/core/index.js" for import "Component"

Why?typecomposer's core/index.js re-exports from sub-packages (./element, ./components, …), and each of those sub-packages re-imports Component from "../.." (the root). This creates a circular dependency that esbuild — used internally by esm.sh — cannot resolve when building its split core.mjs entry.

The package has no "exports" map and uses "type": "module", so esm.sh tries to split on the bare subpath and fails.


Fix

Switch the import-map URL from esm.sh to jsDelivr +esm:

// Before (broken):consttypecomposerUrl=`https://esm.sh/typecomposer@${TYPECOMPOSER_VERSION}`;// After (working):consttypecomposerUrl=`https://cdn.jsdelivr.net/npm/typecomposer@${TYPECOMPOSER_VERSION}/+esm`;

jsDelivr's +esm endpoint uses Rollup to produce a single flat ES module from index.js directly, bypassing the circular subpath entirely. The result is a 117 KB file with all named exports (Component, VBox, DivElement, ButtonElement, etc.) and CORS headers.

CDNResponseNotes
esm.sh/typecomposer@0.1.56200 stub → imports core.mjs500Circular dep, esbuild chokes
cdn.jsdelivr.net/npm/typecomposer@0.1.56/+esm200, 117 KBRollup single-bundle, all exports present

Validation

# 1. CDN live check (from sandbox)
curl -sI https://cdn.jsdelivr.net/npm/typecomposer@0.1.56/+esm
→ HTTP/2 200, cache-control: public max-age=31536000 immutable, CORS: *
# 2. Named exports present
curl -s ... | grep -o "export{..."
→ export{..., V as Component, pn as HBox, Ge as DivElement, Se as ButtonElement, ...}
# 3. Build result
✓ 3486 modules transformed.
dist/assets/esbuild-BHljloGq.wasm 12,332.68 kB
dist/assets/index-C2_sis7Q.css 48.96 kB
dist/assets/index-Dgoy4g8A.js 1,754.62 kB ← contains jsdelivr URL (not esm.sh)
✓ built in 15.32s
# 4. No esm.sh URL in dist
grep -r "esm.sh" dist/ → (empty) ✓

Files changed

FileChange
src/views/playground/PlaygroundView.ts1 line: esm.shcdn.jsdelivr.net/…/+esm; updated JSDoc comment

Manual browser verification still needed

  1. Open deployed docs → Playground tab
  2. Confirm no net::ERR_ABORTED 500 in DevTools Network
  3. Confirm the demo AppPage renders (purple gradient + click button)

cc @zico15@joaodibba

esm.sh generates a split-entry bundle for typecomposer@0.1.56 that imports
a generated /es2022/core.mjs sub-path. That sub-path returns HTTP 500:
esbuild: No matching export in "node_modules/typecomposer/core/index.js"
for import "Component"
Root cause: typecomposer's core/* sub-modules re-import from "../.." (the
root index), creating a circular dependency that esbuild (used by esm.sh)
cannot resolve when building the split core.mjs entry.
Fix: switch the import map URL to jsDelivr's +esm endpoint, which uses
Rollup to produce a single-file ESM bundle from the root index.js directly.
The resulting bundle is a flat 117 KB file with all named exports present
and no circular dependency issue.
Before: https://esm.sh/typecomposer@${VERSION}
→ 200 stub → imports /es2022/core.mjs → 500
After: https://cdn.jsdelivr.net/npm/typecomposer@${VERSION}/+esm
→ 200 single-bundle ESM (Rollup, 117 KB, CORS: *)
Only PlaygroundView.ts is changed (1 line + updated comments).
Build: ✓ 3486 modules, esbuild.wasm in dist/assets/, 15.32s
@joaodibba
joaodibba merged commit d066bfe into TypeComposer:mainJun 12, 2026
1 check failed
@lucas-spin

Copy link
Copy Markdown
ContributorAuthor

Runtime Smoke Test — PR #35 (post-merge audit)

Tool: Playwright 1.60 + Chromium Headless Shell 148 (headless, no display)
Branch:lucas-spin:fix/playground-typecomposer-cdn @ 1846dcd
Server:npm run build + npx vite preview (production-like, port 4173)
Route tested:http://127.0.0.1:4173/ → click "Playground" link → http://127.0.0.1:4173/playground
Wait: 20–25 s for esbuild WASM init + compilation + iframe injection


✅ What the CDN fix got right

CheckResult
esm.sh in dist bundleABSENT — zero occurrences
esm.sh network requestsNONE — never fired
cdn.jsdelivr.net/npm/typecomposer@0.1.56/+esm HTTP status200 OK
Failed to fetch dynamically imported moduleNOT SEEN
esm.sh in console errorsNOT SEEN
Build succeeds✓ 3486 modules, 15.76s

The core CDN fix is correct. jsDelivr resolves the HTTP 500 that plagued esm.sh.


❌ New runtime blocker exposed by the fix

[ERROR] [Playground] Initialization failed:
SecurityError: Failed to read the 'localStorage' property from 'Window':
The document is sandboxed and lacks the 'allow-same-origin' flag.
at <instance_members_initializer>
(https://cdn.jsdelivr.net/npm/typecomposer@0.1.56/+esm:7:5728)

Root cause (two-layer):

  1. typecomposer@0.1.56/index.js does import "./global" unconditionally. global/app.js defines class __App__ with two class field initializers:

    #t =ref(this.getThemePreferred());// calls localStorage.getItem(...)
    #n =ref(this.getLanguagePreferred());// calls localStorage.getItem(...)

    These run at class definition time (before any constructor), so localStorage is accessed the moment the module is evaluated.

  2. The playground iframe has sandbox="allow-scripts"withoutallow-same-origin. A blob: URL in such an iframe gets an opaque origin — the browser blocks localStorage, sessionStorage, indexedDB, and same-origin fetch. The class field initializer throws SecurityError synchronously.

Was this introduced by PR #35?No. PR #35 is exactly 1 line: esm.shcdn.jsdelivr.net/…/+esm. The sandbox="allow-scripts" attribute was set in PR #34 (caa0490). The localStorage call is inside typecomposer@0.1.56 itself.

Why wasn't it visible before? With esm.sh the module load failed with HTTP 500 before any JS ran — the error was hidden. With jsDelivr the module loads successfully and the localStorage call executes.

Effect: The iframe renders an error panel ("Initialization Error: SecurityError…") instead of the default AppPage demo (purple gradient + click button). The playground is non-functional despite the CDN fix being correct.


✅/❌ Full checklist

RequirementResult
Preview iframe renders default demo content❌ — error panel shown
No Runtime Error in console❌ — Runtime Error: SecurityError…
No Initialization failed in console❌ — [Playground] Initialization failed: SecurityError…
No SecurityError❌ — SecurityError: Failed to read localStorage
No Failed to fetch dynamically imported module
No esm.sh in console
No network requests to esm.sh/typecomposer
cdn.jsdelivr.net/npm/typecomposer@0.1.56/+esm → 200
Editing code triggers recompile + preview update❌ untestable (init fails)

Recommended follow-up fix

This needs one of:

Option A (playground-side, minimal): Wrap global/app.js localStorage reads in try/catch upstream in typecomposer@0.1.56. The getThemePreferred() / getLanguagePreferred() methods should silently fall back to defaults when localStorage is unavailable.

Option B (playground-side, immediate workaround): In PlaygroundView.ts, add allow-same-origin to the iframe sandbox — but this weakens isolation because the iframe would share origin with the docs page.

Option C (playground-side, cleanest): Use srcdoc= to load the iframe instead of blob: src. srcdoc iframes inherit the parent origin when there's no sandbox, but with sandbox="allow-scripts allow-same-origin" the user code runs isolated yet with localStorage available.

The right long-term fix is Option A — typecomposer's global initializer should be resilient to restricted environments (workers, sandboxed iframes, SSR). This is a separate issue from PR #35.


Verdict: PR #35 needs a companion fix. The CDN URL change itself is correct and should stay. The localStorage error is a pre-existing layered bug (typecomposer core + PR #34's sandbox choice) that the CDN fix has now surfaced.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@lucas-spin@joaodibba
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(playground): replace esm.sh with jsDelivr +esm — fixes HTTP 500 on core.mjs - #35

Merged
joaodibba merged 1 commit into
TypeComposer:mainfrom
lucas-spin:fix/playground-cdn-jsdelivr-esm
Jun 12, 2026
Merged

fix(playground): replace esm.sh with jsDelivr +esm — fixes HTTP 500 on core.mjs#35
joaodibba merged 1 commit into
TypeComposer:mainfrom
lucas-spin:fix/playground-cdn-jsdelivr-esm

Conversation

@lucas-spin

Copy link
Copy Markdown
Contributor

Bug report (João)

GET https://esm.sh/typecomposer@0.1.56/es2022/core.mjs net::ERR_ABORTED 500
[Playground] Initialization failed: TypeError: Failed to fetch dynamically imported module:
https://esm.sh/typecomposer@0.1.56

Root cause

esm.sh auto-generates a split-entry bundle for typecomposer@0.1.56:

// https://esm.sh/typecomposer@0.1.56 ← 200, but…import"/typecomposer@0.1.56/es2022/core.mjs";// ← 500!import"/typecomposer@0.1.56/es2022/global.mjs";export*from"/typecomposer@0.1.56/es2022/typecomposer.mjs";

The generated core.mjs returns HTTP 500 with:

esbuild: No matching export in "node_modules/typecomposer/core/index.js" for import "Component"

Why?typecomposer's core/index.js re-exports from sub-packages (./element, ./components, …), and each of those sub-packages re-imports Component from "../.." (the root). This creates a circular dependency that esbuild — used internally by esm.sh — cannot resolve when building its split core.mjs entry.

The package has no "exports" map and uses "type": "module", so esm.sh tries to split on the bare subpath and fails.


Fix

Switch the import-map URL from esm.sh to jsDelivr +esm:

// Before (broken):consttypecomposerUrl=`https://esm.sh/typecomposer@${TYPECOMPOSER_VERSION}`;// After (working):consttypecomposerUrl=`https://cdn.jsdelivr.net/npm/typecomposer@${TYPECOMPOSER_VERSION}/+esm`;

jsDelivr's +esm endpoint uses Rollup to produce a single flat ES module from index.js directly, bypassing the circular subpath entirely. The result is a 117 KB file with all named exports (Component, VBox, DivElement, ButtonElement, etc.) and CORS headers.

CDNResponseNotes
esm.sh/typecomposer@0.1.56200 stub → imports core.mjs500Circular dep, esbuild chokes
cdn.jsdelivr.net/npm/typecomposer@0.1.56/+esm200, 117 KBRollup single-bundle, all exports present

Validation

# 1. CDN live check (from sandbox)
curl -sI https://cdn.jsdelivr.net/npm/typecomposer@0.1.56/+esm
→ HTTP/2 200, cache-control: public max-age=31536000 immutable, CORS: *
# 2. Named exports present
curl -s ... | grep -o "export{..."
→ export{..., V as Component, pn as HBox, Ge as DivElement, Se as ButtonElement, ...}
# 3. Build result
✓ 3486 modules transformed.
dist/assets/esbuild-BHljloGq.wasm 12,332.68 kB
dist/assets/index-C2_sis7Q.css 48.96 kB
dist/assets/index-Dgoy4g8A.js 1,754.62 kB ← contains jsdelivr URL (not esm.sh)
✓ built in 15.32s
# 4. No esm.sh URL in dist
grep -r "esm.sh" dist/ → (empty) ✓

Files changed

FileChange
src/views/playground/PlaygroundView.ts1 line: esm.shcdn.jsdelivr.net/…/+esm; updated JSDoc comment

Manual browser verification still needed

  1. Open deployed docs → Playground tab
  2. Confirm no net::ERR_ABORTED 500 in DevTools Network
  3. Confirm the demo AppPage renders (purple gradient + click button)

cc @zico15@joaodibba

esm.sh generates a split-entry bundle for typecomposer@0.1.56 that imports
a generated /es2022/core.mjs sub-path. That sub-path returns HTTP 500:
esbuild: No matching export in "node_modules/typecomposer/core/index.js"
for import "Component"
Root cause: typecomposer's core/* sub-modules re-import from "../.." (the
root index), creating a circular dependency that esbuild (used by esm.sh)
cannot resolve when building the split core.mjs entry.
Fix: switch the import map URL to jsDelivr's +esm endpoint, which uses
Rollup to produce a single-file ESM bundle from the root index.js directly.
The resulting bundle is a flat 117 KB file with all named exports present
and no circular dependency issue.
Before: https://esm.sh/typecomposer@${VERSION}
→ 200 stub → imports /es2022/core.mjs → 500
After: https://cdn.jsdelivr.net/npm/typecomposer@${VERSION}/+esm
→ 200 single-bundle ESM (Rollup, 117 KB, CORS: *)
Only PlaygroundView.ts is changed (1 line + updated comments).
Build: ✓ 3486 modules, esbuild.wasm in dist/assets/, 15.32s
@joaodibba
joaodibba merged commit d066bfe into TypeComposer:mainJun 12, 2026
1 check failed
@lucas-spin

Copy link
Copy Markdown
ContributorAuthor

Runtime Smoke Test — PR #35 (post-merge audit)

Tool: Playwright 1.60 + Chromium Headless Shell 148 (headless, no display)
Branch:lucas-spin:fix/playground-typecomposer-cdn @ 1846dcd
Server:npm run build + npx vite preview (production-like, port 4173)
Route tested:http://127.0.0.1:4173/ → click "Playground" link → http://127.0.0.1:4173/playground
Wait: 20–25 s for esbuild WASM init + compilation + iframe injection


✅ What the CDN fix got right

CheckResult
esm.sh in dist bundleABSENT — zero occurrences
esm.sh network requestsNONE — never fired
cdn.jsdelivr.net/npm/typecomposer@0.1.56/+esm HTTP status200 OK
Failed to fetch dynamically imported moduleNOT SEEN
esm.sh in console errorsNOT SEEN
Build succeeds✓ 3486 modules, 15.76s

The core CDN fix is correct. jsDelivr resolves the HTTP 500 that plagued esm.sh.


❌ New runtime blocker exposed by the fix

[ERROR] [Playground] Initialization failed:
SecurityError: Failed to read the 'localStorage' property from 'Window':
The document is sandboxed and lacks the 'allow-same-origin' flag.
at <instance_members_initializer>
(https://cdn.jsdelivr.net/npm/typecomposer@0.1.56/+esm:7:5728)

Root cause (two-layer):

  1. typecomposer@0.1.56/index.js does import "./global" unconditionally. global/app.js defines class __App__ with two class field initializers:

    #t =ref(this.getThemePreferred());// calls localStorage.getItem(...)
    #n =ref(this.getLanguagePreferred());// calls localStorage.getItem(...)

    These run at class definition time (before any constructor), so localStorage is accessed the moment the module is evaluated.

  2. The playground iframe has sandbox="allow-scripts"withoutallow-same-origin. A blob: URL in such an iframe gets an opaque origin — the browser blocks localStorage, sessionStorage, indexedDB, and same-origin fetch. The class field initializer throws SecurityError synchronously.

Was this introduced by PR #35?No. PR #35 is exactly 1 line: esm.shcdn.jsdelivr.net/…/+esm. The sandbox="allow-scripts" attribute was set in PR #34 (caa0490). The localStorage call is inside typecomposer@0.1.56 itself.

Why wasn't it visible before? With esm.sh the module load failed with HTTP 500 before any JS ran — the error was hidden. With jsDelivr the module loads successfully and the localStorage call executes.

Effect: The iframe renders an error panel ("Initialization Error: SecurityError…") instead of the default AppPage demo (purple gradient + click button). The playground is non-functional despite the CDN fix being correct.


✅/❌ Full checklist

RequirementResult
Preview iframe renders default demo content❌ — error panel shown
No Runtime Error in console❌ — Runtime Error: SecurityError…
No Initialization failed in console❌ — [Playground] Initialization failed: SecurityError…
No SecurityError❌ — SecurityError: Failed to read localStorage
No Failed to fetch dynamically imported module
No esm.sh in console
No network requests to esm.sh/typecomposer
cdn.jsdelivr.net/npm/typecomposer@0.1.56/+esm → 200
Editing code triggers recompile + preview update❌ untestable (init fails)

Recommended follow-up fix

This needs one of:

Option A (playground-side, minimal): Wrap global/app.js localStorage reads in try/catch upstream in typecomposer@0.1.56. The getThemePreferred() / getLanguagePreferred() methods should silently fall back to defaults when localStorage is unavailable.

Option B (playground-side, immediate workaround): In PlaygroundView.ts, add allow-same-origin to the iframe sandbox — but this weakens isolation because the iframe would share origin with the docs page.

Option C (playground-side, cleanest): Use srcdoc= to load the iframe instead of blob: src. srcdoc iframes inherit the parent origin when there's no sandbox, but with sandbox="allow-scripts allow-same-origin" the user code runs isolated yet with localStorage available.

The right long-term fix is Option A — typecomposer's global initializer should be resilient to restricted environments (workers, sandboxed iframes, SSR). This is a separate issue from PR #35.


Verdict: PR #35 needs a companion fix. The CDN URL change itself is correct and should stay. The localStorage error is a pre-existing layered bug (typecomposer core + PR #34's sandbox choice) that the CDN fix has now surfaced.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@lucas-spin@joaodibba