-
-
Notifications
You must be signed in to change notification settings - Fork 57
Description
What version of pkg are you using?
6.13.1
What version of Node.js are you using?
24.11.1
What operating system are you using?
Windows
What CPU architecture are you using?
x86_64
What Node versions, OSs and CPU architectures are you building for?
node24
Describe the Bug
Description
After the ESM support introduced in v6.13.0 (#192), packages that are dual-format (shipping both .js ESM and .cjs CommonJS files) with subpath exports fail to resolve when require()'d via a subpath.
The ESM-aware resolver correctly resolves the path using the exports map, but the result is discarded because the resolved .cjs file is not considered ESM. The code then falls back to the legacy resolve package which doesn't understand the exports field at all.
Reproduction
Package: @langchain/langgraph@1.0.15
Code being packaged:
const { ToolNode, toolsCondition } = require('@langchain/langgraph/prebuilt');Relevant package.json of @langchain/langgraph:
{
"type": "module",
"main": "./dist/index.js",
"exports": {
"./prebuilt": {
"import": {
"types": "./dist/prebuilt/index.d.ts",
"default": "./dist/prebuilt/index.js"
},
"require": {
"types": "./dist/prebuilt/index.d.cts",
"default": "./dist/prebuilt/index.cjs"
}
}
}
}Actual file structure (both files exist on disk):
node_modules/@langchain/langgraph/
dist/
prebuilt/
index.js ← ESM (import/export syntax)
index.cjs ← CJS (require/exports syntax)
Root Cause Analysis
I traced through the resolution logic and found three compounding bugs:
Bug 1: Correct ESM resolution result is discarded in follow.ts
resolver.ts correctly resolves the subpath:
require('@langchain/langgraph/prebuilt')
→ tryResolveESM()
→ resolveWithExports(pkg, './prebuilt', { require: true })
→ resolves to: dist/prebuilt/index.cjs ✅
Then at resolver.ts:158-161, isESMFile() is called on the resolved .cjs file:
return {
resolved: esmResolved, // dist/prebuilt/index.cjs
isESM: isESMFile(esmResolved), // .cjs → always returns false
};Back in follow.ts:87:
if (result.isESM) {
// SKIPPED — because .cjs → isESM = false
// The correctly resolved path is never used!
resolve(result.resolved);
return;
}
// Falls through...The resolver found the right file, but follow.ts throws the answer away because it gates on result.isESM rather than "did the exports-aware resolver succeed?"
Bug 2: CJS fallback doesn't understand exports field
After discarding the correct result, the code falls to follow.ts:148 which uses the resolve npm package (sync()). This legacy resolver only understands the main field — it has no concept of exports subpath mappings. It tries to find prebuilt/index.js at the package root and fails.
Bug 3: Synthetic main patching misses nested condition objects
The walker's package.json patch at walker.ts:1039-1043 checks:
if (typeof exports['.'].require === 'string' && exports['.'].require) {
mainEntry = exports['.'].require;
}But in this package, exports['.'].require is an object ({ types: "...", default: "..." }), not a string. So the synthetic main field is never generated, even for the root export.
Additionally, the synthetic main logic only handles exports['.'] — it does nothing for subpath exports like exports['./prebuilt'].
Resolution Flow Diagram
require('@langchain/langgraph/prebuilt')
│
▼
ESM Resolver ──→ resolves to dist/prebuilt/index.cjs ✅
│
▼
isESMFile('.cjs') ──→ false (correct — .cjs IS commonjs)
│
▼
result.isESM = false
│
▼
follow.ts:87 → if(result.isESM) ──→ SKIPPED ❌ (resolved path discarded!)
│
▼
CJS fallback sync() ──→ doesn't understand "exports" map ❌
│
▼
MODULE_NOT_FOUND 💥
Expected Behavior
When a package defines subpath exports with a require condition in its package.json:
"./prebuilt": {
"require": {
"default": "./dist/prebuilt/index.cjs"
}
}Then require('@langchain/langgraph/prebuilt') should resolve to dist/prebuilt/index.cjs inside the packaged executable, just as it does in normal Node.js runtime.
The exports-aware resolver already finds this file correctly — the result should be used regardless of whether the resolved file is .cjs or .js, since the exports field is a package-level feature, not an ESM-only feature.
To Reproduce
Mention on the #Reproduction paragraph in description.