Summary
When a package's exports field declares a subpath whose only conditions are "types" + "import" (no "default", no "require"), nft appears to mis-trace it during Vercel function packaging. At runtime, Node's ESM loader resolves the bare specifier to a different file than the subpath should yield — specifically, it ends up loading the package's main entry instead of the subpath entry — and module instantiation fails because the expected named export is missing.
Repro
Package: @paypal/paypal-js@10.0.1.
Its exports map:
{
".": {
"types": "./types/index.d.ts",
"import": "./dist/esm/paypal-js.js",
"require": "./dist/cjs/paypal-js.js"
},
"./sdk-v6": {
"types": "./types/v6/index.d.ts",
"import": "./dist/v6/esm/paypal-js.js"
}
}
The ./sdk-v6 subpath has no default/require condition.
dist/v6/esm/paypal-js.js exports loadCoreSdkScript, version.
dist/esm/paypal-js.js (the . entry) exports loadScript, loadCustomScript, version — no loadCoreSdkScript.
What I did
- React Router v7 (framework mode) app deployed via
@vercel/react-router preset.
- SSR module statically imports
loadCoreSdkScript from @paypal/paypal-js/sdk-v6. Vite externalizes the package, so the SSR bundle literally contains import { loadCoreSdkScript } from \"@paypal/paypal-js/sdk-v6\".
- Deploy to Vercel (Node 24 runtime).
Expected
Function loads, Node resolves @paypal/paypal-js/sdk-v6 to dist/v6/esm/paypal-js.js, named import loadCoreSdkScript binds successfully.
Actual
Function crashes on instantiation:
SyntaxError: The requested module '@paypal/paypal-js/sdk-v6' does not
provide an export named 'loadCoreSdkScript'
at #asyncInstantiate (node:internal/modules/esm/module_job:319:21)
This is Node's named-export validation failing — the resolved module exists and loads, but doesn't have loadCoreSdkScript. The export shape matches the v5 entry (.), strongly suggesting nft caused the deployed function's resolution to fall through to the main entry instead of the ./sdk-v6 subpath.
Local sanity checks (all pass)
node --input-type=module -e \"import('@paypal/paypal-js/sdk-v6').then(m => console.log(Object.keys(m)))\" → ['loadCoreSdkScript', 'version']
- Fresh
npm i @paypal/paypal-js@10.0.1 in an empty dir + ESM import works.
- Same on Node 24.
Hypothesis
When nft walks the package and a subpath lacks "default"/"require" conditions, the trace step misses or drops the v6 file (or the package.json is rewritten such that Node falls back to legacy pkg + subpath.js / main resolution). Either way, the deployed function ends up with the wrong file.
Workaround
Convert the static import to a dynamic await import(\"@paypal/paypal-js/sdk-v6\") inside a client-only code path so nft never has to trace the subpath through the SSR graph.
Why this matters
This is a spec-valid exports shape (subpath conditions are optional and "import" alone is legitimate for ESM-only subpaths). Any package can ship this and silently break on Vercel. PayPal's v6 SDK is one example; this will recur as more libraries publish ESM-only subpaths.
Environment
- Vercel Node.js runtime (Node 24)
@vercel/react-router@1.3.1 preset
- React Router v7 framework mode (Vite SSR)
@paypal/paypal-js@10.0.1
Happy to put together a tighter minimal repro repo if useful — let me know the shape you'd prefer (bare nft CLI invocation vs. full Vercel deploy).
Summary
When a package's
exportsfield declares a subpath whose only conditions are"types"+"import"(no"default", no"require"), nft appears to mis-trace it during Vercel function packaging. At runtime, Node's ESM loader resolves the bare specifier to a different file than the subpath should yield — specifically, it ends up loading the package's main entry instead of the subpath entry — and module instantiation fails because the expected named export is missing.Repro
Package:
@paypal/paypal-js@10.0.1.Its
exportsmap:{ ".": { "types": "./types/index.d.ts", "import": "./dist/esm/paypal-js.js", "require": "./dist/cjs/paypal-js.js" }, "./sdk-v6": { "types": "./types/v6/index.d.ts", "import": "./dist/v6/esm/paypal-js.js" } }The
./sdk-v6subpath has nodefault/requirecondition.dist/v6/esm/paypal-js.jsexportsloadCoreSdkScript, version.dist/esm/paypal-js.js(the.entry) exportsloadScript, loadCustomScript, version— noloadCoreSdkScript.What I did
@vercel/react-routerpreset.loadCoreSdkScriptfrom@paypal/paypal-js/sdk-v6. Vite externalizes the package, so the SSR bundle literally containsimport { loadCoreSdkScript } from \"@paypal/paypal-js/sdk-v6\".Expected
Function loads, Node resolves
@paypal/paypal-js/sdk-v6todist/v6/esm/paypal-js.js, named importloadCoreSdkScriptbinds successfully.Actual
Function crashes on instantiation:
This is Node's named-export validation failing — the resolved module exists and loads, but doesn't have
loadCoreSdkScript. The export shape matches the v5 entry (.), strongly suggesting nft caused the deployed function's resolution to fall through to the main entry instead of the./sdk-v6subpath.Local sanity checks (all pass)
node --input-type=module -e \"import('@paypal/paypal-js/sdk-v6').then(m => console.log(Object.keys(m)))\"→['loadCoreSdkScript', 'version']npm i @paypal/paypal-js@10.0.1in an empty dir + ESM import works.Hypothesis
When nft walks the package and a subpath lacks
"default"/"require"conditions, the trace step misses or drops the v6 file (or the package.json is rewritten such that Node falls back to legacypkg + subpath.js/ main resolution). Either way, the deployed function ends up with the wrong file.Workaround
Convert the static import to a dynamic
await import(\"@paypal/paypal-js/sdk-v6\")inside a client-only code path so nft never has to trace the subpath through the SSR graph.Why this matters
This is a spec-valid
exportsshape (subpath conditions are optional and"import"alone is legitimate for ESM-only subpaths). Any package can ship this and silently break on Vercel. PayPal's v6 SDK is one example; this will recur as more libraries publish ESM-only subpaths.Environment
@vercel/react-router@1.3.1preset@paypal/paypal-js@10.0.1Happy to put together a tighter minimal repro repo if useful — let me know the shape you'd prefer (bare nft CLI invocation vs. full Vercel deploy).