From e89315ce979311aefc006779b59497d0236a9d3b Mon Sep 17 00:00:00 2001 From: spacedevin Date: Wed, 7 Oct 2026 17:20:52 -0700 Subject: [PATCH] fix: mark the compiled server packages as ES modules tish writes the compiled packages under .orbit/server/.../node_modules (@tishlang/orbit, tishdoc-*) without a package.json. Node never looks past a node_modules folder for one, so it takes them for CommonJS; Vercel's Node 22 runtime then failed every server route with "Named export 'Fragment' not found" (moo.moi's /callback and 404). compileEsm now writes {"type": "module"} into every package folder under any node_modules in the compiled tree. --- src/build/tish.mjs | 34 ++++++++++++++++++++++++++++++++++ test/e2e.mjs | 4 ++++ 2 files changed, 38 insertions(+) diff --git a/src/build/tish.mjs b/src/build/tish.mjs index 0cc4267..ba97935 100644 --- a/src/build/tish.mjs +++ b/src/build/tish.mjs @@ -36,5 +36,39 @@ export async function compileEsm(root, entry, outDir, extra = []) { const { out } = await tish(root, ['build', entry, '-o', outDir, '--target', 'js', '--format', 'esm', '--jsx-import-source', '@tishlang/orbit', ...extra]) const m = /Entry:\s*(.+)/.exec(out) if (!m) throw new Error('tish build did not report an entry file:\n' + out) + markEsm(outDir) return path.resolve(root, m[1].trim()) } + +// The compiled packages under /node_modules (@tishlang/orbit, …) are ES modules, but tish +// writes them without a package.json. Node never looks past a node_modules folder for one, so it +// takes them for CommonJS, and stricter loaders (Vercel's Node runtime) then fail on their named +// exports ("Named export 'Fragment' not found"). Give each package folder {"type": "module"}. +function markEsm(outDir) { + const mark = dir => { + const pj = path.join(dir, 'package.json') + if (!fs.existsSync(pj)) fs.writeFileSync(pj, JSON.stringify({ type: 'module' }) + '\n') + } + const isDir = p => fs.statSync(p).isDirectory() + const walk = dir => { + for (const name of fs.readdirSync(dir)) { + const p = path.join(dir, name) + if (!isDir(p)) continue + if (name === 'node_modules') { + for (const pkg of fs.readdirSync(p)) { + const pp = path.join(p, pkg) + if (!isDir(pp)) continue + if (pkg.startsWith('@')) { + for (const sub of fs.readdirSync(pp)) if (isDir(path.join(pp, sub))) { mark(path.join(pp, sub)); walk(path.join(pp, sub)) } + } else { + mark(pp) + walk(pp) + } + } + } else { + walk(p) + } + } + } + walk(outDir) +} diff --git a/test/e2e.mjs b/test/e2e.mjs index 72d1f30..94c959d 100644 --- a/test/e2e.mjs +++ b/test/e2e.mjs @@ -77,6 +77,10 @@ console.log('trailingSlash: true (default)') check(cfg.version === 3, 'vercel config v3', cfg.version) check(cfg.routes.some(x => x.status === 308 && x.headers?.Location === '/$1/'), 'vercel adds the trailing slash', null) check(fs.existsSync(path.join(dir, '.vercel', 'output', 'functions', '_orbit.func', 'index.mjs')), 'vercel server function', null) + const unmarked = [] + const scan = d => { for (const n of fs.readdirSync(d)) { const p = path.join(d, n); if (!fs.statSync(p).isDirectory()) continue; if (path.basename(d) === 'node_modules' || path.basename(path.dirname(d)) === 'node_modules' && path.basename(d).startsWith('@')) { if (!n.startsWith('@') && !fs.existsSync(path.join(p, 'package.json'))) unmarked.push(p) } scan(p) } } + scan(path.join(dir, '.vercel', 'output', 'functions', '_orbit.func', 'server')) + check(unmarked.length === 0, 'compiled packages are marked as ES modules', unmarked) check(fs.existsSync(path.join(dir, '.vercel', 'output', 'static', 'index.html')), 'vercel static pages', null) // The function Vercel runs, behind a plain Node server. const fn = (await import(path.join(dir, '.vercel', 'output', 'functions', '_orbit.func', 'index.mjs'))).default