Skip to content

Commit a13cbea

Browse files
committed
fix(server): bundle superjson into a single ESM file for the browser
Root cause of ALL client-side JS failing: superjson internally imports \`copy-anything\` which imports \`is-what\` — both as bare specifiers. The browser's import map only had an entry for \`superjson\` itself, so the transitive imports failed with: Uncaught TypeError: Failed to resolve module specifier "copy-anything" Since webjs/core statically re-exports richFetch which statically imports superjson, the ENTIRE core module failed to load. No components registered, no events fired, no interactivity. Fix: instead of serving superjson's raw dist/ files (which contain bare imports to its deps), the server now bundles superjson + all transitive deps into a single self-contained ESM file via esbuild on first request. Result is cached in-process. 11KB minified, zero bare-specifier leaks. Import map simplified: 'superjson' → '/__webjs/vendor/superjson.js' (single bundled file) Removed the old /__webjs/vendor/superjson/* wildcard handler (no longer needed — one file serves everything). Also: fixed counter.ts that was broken by a bad sed in the previous debug commit (stray closing brace). Verified in production mode (webjs start): - /__webjs/vendor/superjson.js → 200 (11KB) - All pages 200 - Counter module 200 - No console errors - 70/70 framework tests pass
1 parent 5ae7d71 commit a13cbea

3 files changed

Lines changed: 49 additions & 17 deletions

File tree

examples/blog/components/counter.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,6 @@ export class Counter extends WebComponent {
88
static tag = 'my-counter';
99
static properties = { count: { type: Number } };
1010
count = 0;
11-
connectedCallback() {
12-
super.connectedCallback();
13-
console.log('[webjs] counter connected — JS is alive');
14-
}
1511
static styles = css`
1612
:host {
1713
display: inline-flex;

packages/server/src/dev.js

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -299,15 +299,11 @@ async function handleCore(req, ctx) {
299299
return fileResponse(abs, { dev, immutable: !dev });
300300
}
301301

302-
// Vendored packages (only superjson in v1) — served from node_modules so
303-
// browsers can resolve bare specifiers in the import map.
304-
if (path.startsWith('/__webjs/vendor/superjson/')) {
305-
const rel = path.slice('/__webjs/vendor/superjson/'.length);
306-
const root = locatePackageDir(appDir, 'superjson');
307-
if (!root) return new Response('superjson not installed', { status: 404 });
308-
const abs = resolve(root, rel);
309-
if (!abs.startsWith(root)) return new Response('forbidden', { status: 403 });
310-
return fileResponse(abs, { dev, immutable: !dev });
302+
// Vendored superjson — served as a single pre-bundled ESM file so its
303+
// transitive deps (copy-anything, is-what) don't need their own
304+
// import-map entries. Bundled lazily on first request via esbuild.
305+
if (path === '/__webjs/vendor/superjson.js') {
306+
return serveBundledSuperjson(appDir, dev);
311307
}
312308

313309
// Prod bundle (if present)
@@ -687,6 +683,45 @@ async function exists(p) {
687683
* @param {string} abs
688684
* @param {boolean} dev
689685
*/
686+
/** @type {string | null} */
687+
let _superjsonBundle = null;
688+
689+
/**
690+
* Serve superjson as a single self-contained ESM file. All transitive deps
691+
* (copy-anything, is-what) are inlined by esbuild so the browser only ever
692+
* fetches one file and no bare-specifier imports leak.
693+
*/
694+
async function serveBundledSuperjson(appDir, dev) {
695+
if (!_superjsonBundle) {
696+
let build;
697+
try { ({ build } = await import('esbuild')); }
698+
catch {
699+
return new Response('/* esbuild missing */', {
700+
status: 500,
701+
headers: { 'content-type': 'application/javascript; charset=utf-8' },
702+
});
703+
}
704+
const entryPoint = locatePackageDir(appDir, 'superjson');
705+
if (!entryPoint) return new Response('superjson not found', { status: 404 });
706+
const result = await build({
707+
entryPoints: [join(entryPoint, 'dist', 'index.js')],
708+
bundle: true,
709+
format: 'esm',
710+
target: 'es2022',
711+
platform: 'browser',
712+
write: false,
713+
minify: !dev,
714+
});
715+
_superjsonBundle = result.outputFiles[0].text;
716+
}
717+
return new Response(_superjsonBundle, {
718+
headers: {
719+
'content-type': 'application/javascript; charset=utf-8',
720+
'cache-control': dev ? 'no-cache' : 'public, max-age=31536000, immutable',
721+
},
722+
});
723+
}
724+
690725
async function tsResponse(abs, dev) {
691726
let esbuild;
692727
try { ({ transform: esbuild } = await import('esbuild')); }

packages/server/src/importmap.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
/**
22
* Build the import map JSON injected into every SSR HTML document.
3-
* Lets users write bare specifiers (`import { html } from 'webjs'`,
4-
* `import { parse } from 'superjson'`) without relative paths or a bundler.
3+
*
4+
* superjson is mapped to a single pre-bundled ESM file (its transitive deps
5+
* copy-anything + is-what are inlined by esbuild on first request) so only
6+
* ONE fetch is needed and no extra bare-specifier entries leak into the map.
57
*/
68
export function buildImportMap() {
79
return {
810
imports: {
911
'webjs': '/__webjs/core/index.js',
1012
'webjs/': '/__webjs/core/src/',
11-
'superjson': '/__webjs/vendor/superjson/dist/index.js',
12-
'superjson/': '/__webjs/vendor/superjson/dist/',
13+
'superjson': '/__webjs/vendor/superjson.js',
1314
},
1415
};
1516
}

0 commit comments

Comments
 (0)