Skip to content

fix(deps): externalize unresolvable bare deps with skipNodeModulesBundle#1009

Open
m2na7 wants to merge 2 commits into
rolldown:mainfrom
m2na7:fix/skip-node-modules-bundle-unresolvable
Open

fix(deps): externalize unresolvable bare deps with skipNodeModulesBundle#1009
m2na7 wants to merge 2 commits into
rolldown:mainfrom
m2na7:fix/skip-node-modules-bundle-unresolvable

Conversation

@m2na7

@m2na7 m2na7 commented Jul 9, 2026

Copy link
Copy Markdown

Description

deps.skipNodeModulesBundle: true is documented as "Skip bundling all node_modules dependencies", but a bare dependency the resolver can't resolve is bundled instead of externalized.

This happens for a dependency published without an exports field on a non-node platform: the resolver doesn't fall back to the legacy main/types fields, so this.resolve(...) returns null. In externalStrategy (src/features/deps.ts) the skipNodeModulesBundle branch was gated on resolved being truthy, so a null resolution fell through to return false and the bare import got bundled.

It's most visible in the dts build, where a types-only dependency ends up inlined into the emitted .d.ts instead of being kept as an external import:

Before

//#region node_modules/my-dep/index.d.ts
interface Foo { a: number; }
//#endregion
export type { Foo };

After

import { Foo } from "my-dep";
export type { Foo };

The fix externalizes the bare specifier in this case too — skipNodeModulesBundle should never bundle a node_modules import whether or not it resolves. Absolute paths and # subpath imports are left untouched, and the change is fully scoped to skipNodeModulesBundle, so default resolution and the existing "Module not found" path (without this option) are unaffected.

Added a regression test in tests/e2e.test.ts (types-only dep + dts: true + platform: 'neutral' + skipNodeModulesBundle: true) that fails without the change and passes with it. pnpm test (full suite), tsc --noEmit, eslint --max-warnings 0, and prettier --check all pass.

Linked Issues

fixes #993

Additional context

The bug reproduces only with the combination in the test: a non-node platform + a dependency lacking an exports field (so the resolver returns null). On a node platform the same dependency resolves via main, so skipNodeModulesBundle already externalizes it correctly.

@netlify

netlify Bot commented Jul 9, 2026

Copy link
Copy Markdown

Deploy Preview for tsdown-main ready!

Name Link
🔨 Latest commit 3d783d2
🔍 Latest deploy log https://app.netlify.com/projects/tsdown-main/deploys/6a54aaa2b0184e0008207f7f
😎 Deploy Preview https://deploy-preview-1009--tsdown-main.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@pkg-pr-new

pkg-pr-new Bot commented Jul 9, 2026

Copy link
Copy Markdown

Open in StackBlitz

tsdown

pnpm add https://pkg.pr.new/tsdown@1009 -D
npm i https://pkg.pr.new/tsdown@1009 -D
yarn add https://pkg.pr.new/tsdown@1009.tgz -D

create-tsdown

pnpm add https://pkg.pr.new/create-tsdown@1009 -D
npm i https://pkg.pr.new/create-tsdown@1009 -D
yarn add https://pkg.pr.new/create-tsdown@1009.tgz -D

@tsdown/css

pnpm add https://pkg.pr.new/@tsdown/css@1009 -D
npm i https://pkg.pr.new/@tsdown/css@1009 -D
yarn add https://pkg.pr.new/@tsdown/css@1009.tgz -D

@tsdown/exe

pnpm add https://pkg.pr.new/@tsdown/exe@1009 -D
npm i https://pkg.pr.new/@tsdown/exe@1009 -D
yarn add https://pkg.pr.new/@tsdown/exe@1009.tgz -D

tsdown-migrate

pnpm add https://pkg.pr.new/tsdown-migrate@1009 -D
npm i https://pkg.pr.new/tsdown-migrate@1009 -D
yarn add https://pkg.pr.new/tsdown-migrate@1009.tgz -D

commit: 3d783d2

@m2na7 m2na7 force-pushed the fix/skip-node-modules-bundle-unresolvable branch from c654edb to b367a20 Compare July 9, 2026 04:50
When `deps.skipNodeModulesBundle` is enabled, a bare dependency that the
resolver can't resolve was bundled instead of externalized. This happens
for a package without an `exports` field on a non-`node` platform, where
the legacy `main`/`types` fields aren't consulted, so resolution returns
null. It is most visible in the dts build, where a types-only dependency
gets inlined into the emitted `.d.ts` instead of kept as an external
import.

Externalize the bare specifier in that case too, since
skipNodeModulesBundle should never bundle a node_modules import whether or
not it resolves. Absolute paths and `#` subpath imports are left untouched.

Fixes rolldown#993
Keep the existing externalization check untouched and add the
unresolvable-bare-specifier case as a sibling guard, matching the flat
early-return style of externalStrategy.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

skipNodeModulesBundle bundles deps it can't resolve (non-node platform + dependency without an exports field)

1 participant