Skip to content

Conditional subpath with only "import" condition mis-traced — wrong file copied on Vercel deploy #599

Description

@ap-justin

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

  1. React Router v7 (framework mode) app deployed via @vercel/react-router preset.
  2. 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\".
  3. 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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions