Skip to content

Commit b34258b

Browse files
V48 (impl-only): Load @vercel/sandbox via pure ESM to fix ERR_REQUIRE_ESM
Production deposit synthesis failed after sandbox auth when Next/CJS resolved dist/index.cjs → command.cjs require(@workflow/serde). Force the dist/index.js ESM entry with webpackIgnore, externalize sandbox/serde/oidc, and pin @vercel/sandbox to 2.6.0.
1 parent f956577 commit b34258b

7 files changed

Lines changed: 552 additions & 183 deletions

File tree

packages/generic-hosts/VercelSandbox/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
},
1616
"dependencies": {
1717
"@bitcode/host-generics": "workspace:*",
18-
"@vercel/sandbox": "^1.10.2"
18+
"@vercel/sandbox": "2.6.0"
1919
},
2020
"devDependencies": {
2121
"@types/jest": "^29.5.12",

packages/generic-hosts/VercelSandbox/src/__tests__/vercel-sandbox-host.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1+
import path from 'node:path';
2+
import { pathToFileURL } from 'node:url';
3+
14
import { buildAssetPackSandboxHostPlan } from '@bitcode/pipeline-hosts';
25
import {
6+
assertVercelSandboxAuthAvailable,
37
normalizeCreateOptions,
8+
resolveVercelSandboxEsmEntryHref,
49
VercelSandboxPipelineHost,
510
} from '../vercel-sandbox-host';
611
import type {
@@ -388,6 +393,44 @@ describe('VercelSandboxPipelineHost', () => {
388393
'Vercel Sandbox create did not complete within 5ms.'
389394
);
390395
});
396+
397+
it('resolves the pure-ESM @vercel/sandbox entry (not dist/index.cjs)', () => {
398+
const href = resolveVercelSandboxEsmEntryHref((id) => {
399+
if (id === '@vercel/sandbox/package.json') {
400+
return path.join('/virtual/node_modules/@vercel/sandbox', 'package.json');
401+
}
402+
throw new Error(`unexpected resolve: ${id}`);
403+
});
404+
expect(href).toBe(
405+
pathToFileURL(path.join('/virtual/node_modules/@vercel/sandbox/dist/index.js')).href,
406+
);
407+
expect(href.endsWith('/dist/index.js')).toBe(true);
408+
expect(href.includes('index.cjs')).toBe(false);
409+
});
410+
411+
it('accepts access-token auth when OIDC is absent', () => {
412+
const previous = {
413+
VERCEL_TOKEN: process.env.VERCEL_TOKEN,
414+
VERCEL_TEAM_ID: process.env.VERCEL_TEAM_ID,
415+
VERCEL_PROJECT_ID: process.env.VERCEL_PROJECT_ID,
416+
VERCEL_OIDC_TOKEN: process.env.VERCEL_OIDC_TOKEN,
417+
};
418+
try {
419+
delete process.env.VERCEL_OIDC_TOKEN;
420+
process.env.VERCEL_TOKEN = 'tok';
421+
process.env.VERCEL_TEAM_ID = 'team';
422+
process.env.VERCEL_PROJECT_ID = 'prj';
423+
expect(() => assertVercelSandboxAuthAvailable()).not.toThrow();
424+
425+
delete process.env.VERCEL_TOKEN;
426+
expect(() => assertVercelSandboxAuthAvailable()).toThrow(/Sandbox auth is not configured/);
427+
} finally {
428+
restoreEnv('VERCEL_TOKEN', previous.VERCEL_TOKEN);
429+
restoreEnv('VERCEL_TEAM_ID', previous.VERCEL_TEAM_ID);
430+
restoreEnv('VERCEL_PROJECT_ID', previous.VERCEL_PROJECT_ID);
431+
restoreEnv('VERCEL_OIDC_TOKEN', previous.VERCEL_OIDC_TOKEN);
432+
}
433+
});
391434
});
392435

393436
function restoreEnv(key: string, value: string | undefined): void {

packages/generic-hosts/VercelSandbox/src/vercel-sandbox-host.ts

Lines changed: 73 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
import fs from 'node:fs';
2+
import { createRequire } from 'node:module';
3+
import path from 'node:path';
4+
import { pathToFileURL } from 'node:url';
5+
16
import type {
27
PipelineHostCommand,
38
PipelineHostEvent,
@@ -487,12 +492,76 @@ export function assertVercelSandboxAuthAvailable(
487492
);
488493
}
489494

495+
/**
496+
* Resolve the pure-ESM entry of `@vercel/sandbox` as a file:// URL.
497+
*
498+
* Package root resolution under Next/CJS serverless can load `dist/index.cjs`
499+
* → `command.cjs`, which does `require('@workflow/serde')`. That package is
500+
* pure ESM, so Node throws ERR_REQUIRE_ESM on Vercel `/var/task`. The ESM
501+
* graph (`dist/index.js`) imports serde correctly.
502+
*/
503+
export function resolveVercelSandboxEsmEntryHref(
504+
resolveId: (id: string) => string = defaultResolvePackageId,
505+
): string {
506+
const root = resolveVercelSandboxPackageRoot(resolveId);
507+
const esmIndex = path.join(root, 'dist', 'index.js');
508+
return pathToFileURL(esmIndex).href;
509+
}
510+
511+
export function resolveVercelSandboxPackageRoot(
512+
resolveId: (id: string) => string = defaultResolvePackageId,
513+
): string {
514+
try {
515+
return path.dirname(resolveId('@vercel/sandbox/package.json'));
516+
} catch {
517+
// Incomplete exports / hoisting: resolve a runtime file and walk up until
518+
// package.json name is @vercel/sandbox.
519+
let dir = path.dirname(resolveId('@vercel/sandbox'));
520+
for (let i = 0; i < 8; i++) {
521+
const candidate = path.join(dir, 'package.json');
522+
try {
523+
const pkg = JSON.parse(fs.readFileSync(candidate, 'utf8')) as { name?: string };
524+
if (pkg.name === '@vercel/sandbox') return dir;
525+
} catch {
526+
// keep walking
527+
}
528+
const parent = path.dirname(dir);
529+
if (parent === dir) break;
530+
dir = parent;
531+
}
532+
throw new Error('Could not resolve @vercel/sandbox package root for ESM load.');
533+
}
534+
}
535+
536+
function defaultResolvePackageId(id: string): string {
537+
// createRequire needs a filepath anchor; cwd package.json works under Next,
538+
// Jest, and monorepo package tests without import.meta.
539+
const req = createRequire(path.join(process.cwd(), 'package.json'));
540+
return req.resolve(id);
541+
}
542+
490543
export async function loadVercelSandboxFactory(): Promise<SandboxFactory> {
491-
const module = await import('@vercel/sandbox') as { Sandbox?: SandboxFactory };
492-
if (!module.Sandbox?.create) {
493-
throw new Error('@vercel/sandbox did not expose Sandbox.create().');
544+
const esmHref = resolveVercelSandboxEsmEntryHref();
545+
// webpackIgnore: do not rewrite to a CJS interop chunk; load Node-native ESM.
546+
const module = (await import(
547+
/* webpackIgnore: true */
548+
esmHref
549+
)) as {
550+
Sandbox?: SandboxFactory;
551+
default?: SandboxFactory | { Sandbox?: SandboxFactory };
552+
};
553+
554+
const fromDefault =
555+
module.default && typeof module.default === 'object'
556+
? module.default.Sandbox
557+
: undefined;
558+
const Sandbox = module.Sandbox ?? fromDefault;
559+
if (!Sandbox?.create) {
560+
throw new Error(
561+
`@vercel/sandbox did not expose Sandbox.create() (ESM entry ${esmHref}).`,
562+
);
494563
}
495-
return module.Sandbox;
564+
return Sandbox;
496565
}
497566

498567
async function readCommandOutput(

packages/pipeline-hosts/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
"type-check": "pnpm run typecheck"
2424
},
2525
"dependencies": {
26-
"@vercel/sandbox": "^1.10.2",
26+
"@vercel/sandbox": "2.6.0",
2727
"@bitcode/host-generics": "workspace:*",
2828
"@bitcode/generic-hosts-local": "workspace:*",
2929
"@bitcode/generic-hosts-vercel-sandbox": "workspace:*"

0 commit comments

Comments
 (0)