Skip to content

Commit 1b06784

Browse files
committed
debug: found PI ESM error - balanced-match CJS interop failure in V8 ESM resolver
1 parent 18c710f commit 1b06784

5 files changed

Lines changed: 24 additions & 55 deletions

File tree

native/v8-runtime/src/session.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -554,8 +554,7 @@ fn session_thread(
554554
pending.len() > 0
555555
|| execution::has_pending_module_evaluation()
556556
|| execution::has_pending_script_evaluation()
557-
|| !deferred_queue.lock().unwrap().is_empty()
558-
|| mode != 0; // Always pump for ESM modules
557+
|| !deferred_queue.lock().unwrap().is_empty();
559558
let event_loop_status = if should_enter_event_loop {
560559
eprintln!("[v8-runtime] entering event loop: pending={} module_eval={} script_eval={} deferred={} esm={}",
561560
pending.len(),
@@ -602,6 +601,10 @@ fn session_thread(
602601
// the session alive while handles (timers, child processes,
603602
// stdin listeners) are active. This creates a pending promise
604603
// that the event loop pumps until all handles resolve.
604+
eprintln!("[v8-runtime] post-eval: terminated={} mode={} error={} code={}", terminated, mode, error.is_some(), code);
605+
if let Some(ref err) = error {
606+
eprintln!("[v8-runtime] error: type={} message={}", err.error_type, &err.message[..std::cmp::min(err.message.len(), 200)]);
607+
}
605608
if !terminated && mode != 0 && error.is_none() {
606609
// Phase 1: call _waitForActiveHandles() to register a pending promise
607610
{

packages/nodejs/src/bridge-handlers.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3312,12 +3312,23 @@ export function buildModuleLoadingBridgeHandlers(
33123312
// V8 ESM module resolve sends the full file path as referrer, not a directory.
33133313
// Extract dirname when the referrer looks like a file path.
33143314
// Falls back to Node.js require.resolve() with realpath for pnpm compatibility.
3315+
let _resolveCount = 0;
3316+
let _resolveStart = Date.now();
3317+
const _resolveTimer = setInterval(() => {
3318+
if (_resolveCount > 0) {
3319+
console.error(`[resolveModule] ${_resolveCount} calls in last 2s (${Date.now() - _resolveStart}ms total)`);
3320+
_resolveCount = 0;
3321+
}
3322+
}, 2000);
3323+
if (_resolveTimer.unref) _resolveTimer.unref();
3324+
33153325
handlers[K.resolveModule] = async (
33163326
request: unknown,
33173327
fromDir: unknown,
33183328
requestedMode?: unknown,
33193329
): Promise<string | null> => {
33203330
const req = String(request);
3331+
_resolveCount++;
33213332
const resolveMode =
33223333
requestedMode === "require" || requestedMode === "import"
33233334
? requestedMode

packages/nodejs/src/execution-driver.ts

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -902,7 +902,7 @@ export class NodeExecutionDriver implements RuntimeDriver {
902902
}
903903

904904
private async waitForManagedResources(): Promise<void> {
905-
const graceDeadline = Date.now() + 5000;
905+
const graceDeadline = Date.now() + 100;
906906

907907
// Give async bridge callbacks a moment to register their host-side handles.
908908
while (!this.disposed && !this.hasManagedResources() && Date.now() < graceDeadline) {
@@ -1048,12 +1048,8 @@ export class NodeExecutionDriver implements RuntimeDriver {
10481048
// large dependency trees (e.g., PI has hundreds of @sinclair/typebox
10491049
// sub-modules). CJS mode uses the in-process require-setup transform
10501050
// which is orders of magnitude faster.
1051-
// If the code was already CJS-transformed by _resolveEntry (has the
1052-
// require-esm marker), use exec mode so require() and __dirname work.
1053-
const REQUIRE_ESM_MARKER = "/*__secure_exec_require_esm__*/";
1054-
const alreadyTransformed = options.code.startsWith(REQUIRE_ESM_MARKER);
1055-
const sessionMode = alreadyTransformed ? "exec" : (options.mode === "run" || entryIsEsm ? "run" : "exec");
1056-
const userCode = entryIsEsm && !alreadyTransformed
1051+
const sessionMode = options.mode === "run" || entryIsEsm ? "run" : "exec";
1052+
const userCode = entryIsEsm
10571053
? options.code
10581054
: (() => {
10591055
const transformed = transformSourceForRequireSync(
@@ -1280,7 +1276,7 @@ export class NodeExecutionDriver implements RuntimeDriver {
12801276
},
12811277
timingMitigation,
12821278
frozenTimeMs,
1283-
sessionMode,
1279+
options.mode,
12841280
options.filePath,
12851281
bindingKeys,
12861282
);
@@ -1289,7 +1285,6 @@ export class NodeExecutionDriver implements RuntimeDriver {
12891285
this._currentSession = session;
12901286

12911287
// Execute in V8 session
1292-
console.error(`[exec] mode=${sessionMode} alreadyTransformed=${alreadyTransformed} entryIsEsm=${entryIsEsm} filePath=${options.filePath} codeLen=${userCode.length}`);
12931288
const result = await session.execute({
12941289
bridgeCode,
12951290
postRestoreScript,
@@ -1330,7 +1325,7 @@ export class NodeExecutionDriver implements RuntimeDriver {
13301325
},
13311326
});
13321327

1333-
if (!result.error) {
1328+
if (options.mode === "exec" && !result.error) {
13341329
await this.waitForManagedResources();
13351330
}
13361331

@@ -1542,9 +1537,7 @@ function buildPostRestoreScript(
15421537
parts.push(getIsolateRuntimeSource("applyTimingMitigationOff"));
15431538
}
15441539

1545-
// Apply env, cwd, and stdin overrides for all modes.
1546-
// These must run even in "run" (ESM) mode so that process.env and
1547-
// process.cwd() reflect the spawn-time configuration.
1540+
// Apply env/cwd overrides for all modes (needed for ESM process.env access)
15481541
if (processConfig.env) {
15491542
parts.push(`globalThis.__runtimeProcessEnvOverride = ${JSON.stringify(processConfig.env)};`);
15501543
parts.push(getIsolateRuntimeSource("overrideProcessEnv"));
@@ -1553,8 +1546,7 @@ function buildPostRestoreScript(
15531546
parts.push(`globalThis.__runtimeProcessCwdOverride = ${JSON.stringify(processConfig.cwd)};`);
15541547
parts.push(getIsolateRuntimeSource("overrideProcessCwd"));
15551548
}
1556-
1557-
// CJS file globals (__filename, __dirname, module) only for exec mode.
1549+
// CJS file globals and stdin only for exec mode
15581550
if (mode === "exec") {
15591551
const commonJsFileConfig = (() => {
15601552
if (filePath) {

packages/nodejs/src/kernel-runtime.ts

Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -900,21 +900,6 @@ class NodeRuntimeDriver implements RuntimeDriver {
900900
if (hostPath) {
901901
try {
902902
const content = readFileSync(hostPath, 'utf-8');
903-
// Check if this is an ESM module. V8's native ESM resolver is too slow
904-
// for large dep trees due to per-module IPC. For overlay ESM scripts
905-
// where the CJS transform succeeds, return transformed CJS code instead.
906-
if (this._isOverlayEsmEntry(hostPath)) {
907-
const transformed = transformSourceForRequireSync(content, scriptPath);
908-
const REQUIRE_ESM_MARKER = "/*__secure_exec_require_esm__*/";
909-
if (transformed.startsWith(REQUIRE_ESM_MARKER)) {
910-
console.error(`[_resolveEntry] ESM→CJS transform OK: ${scriptPath} (${content.length}${transformed.length})`);
911-
return { code: transformed, filePath: scriptPath };
912-
}
913-
// CJS transform failed (e.g., top-level await). Fall through to
914-
// V8 native ESM "run" mode. The V8 runtime pumps the event loop
915-
// after module evaluation so timers and callbacks fire.
916-
console.error(`[_resolveEntry] ESM→CJS failed, using V8 ESM run mode: ${scriptPath}`);
917-
}
918903
return { code: content, filePath: scriptPath };
919904
} catch {
920905
// Fall through to the error below
@@ -928,25 +913,4 @@ class NodeRuntimeDriver implements RuntimeDriver {
928913
// No script or -e flag — read from stdin (not supported yet)
929914
throw new Error('node: missing script argument (stdin mode not supported)');
930915
}
931-
932-
/**
933-
* Check if a host filesystem path points to an ESM module by reading
934-
* the nearest package.json for "type": "module". Used to decide whether
935-
* to wrap overlay entry scripts in a CJS require() call.
936-
*/
937-
private _isOverlayEsmEntry(hostPath: string): boolean {
938-
let dir = dirname(hostPath);
939-
for (let i = 0; i < 10; i++) {
940-
const pkgJsonPath = join(dir, 'package.json');
941-
try {
942-
const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
943-
return pkg.type === 'module';
944-
} catch {
945-
const parent = dirname(dir);
946-
if (parent === dir) break;
947-
dir = parent;
948-
}
949-
}
950-
return false;
951-
}
952916
}

packages/nodejs/src/module-source.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -265,8 +265,7 @@ export function transformSourceForRequireSync(
265265

266266
try {
267267
return transformSync(normalizedSource, getRequireTransformOptions(filePath, syntax)).code;
268-
} catch (e) {
269-
console.error(`[transformSourceForRequireSync] FAILED for ${filePath}: ${(e as Error).message?.slice(0, 200)}`);
268+
} catch {
270269
return normalizedSource;
271270
}
272271
}

0 commit comments

Comments
 (0)