Reproduction link or steps
https://stackblitz.com/edit/github-va9a7t9q?file=tsdown.config.ts
deps.skipNodeModulesBundle is documented as "Skip bundling all node_modules dependencies." But for any dependency published without an exports field (legacy main/types only), it gets bundled anyway whenever platform is not "node". For the dts build this causes an error when the inlined package transitively references a CJS .d.ts.
This issue requires a combination of deps.skipNodeModulesBundle: true + a non-node platform + a dependency without an exports field.
What is expected?
deps.skipNodeModulesBundle: true should externalize every bare / non-relative node_modules import, regardless of whether tsdown's resolver can resolve it and regardless of the configured platform.
What is actually happening?
Under a non-node platform, rolldown's resolver does not fall back to the legacy main/types fields, so packages without an exports field don't get resolved. skipNodeModulesBundle only externalizes when that resolve succeeds, so these deps get inlined.
Any additional comments?
The following was generated by Claude:
In DepsPlugin (tsdown/dist/format-*.mjs), externalStrategy only externalizes a node_modules import when the prior this.resolve(...) succeeded:
// resolveId handler
const resolved = await this.resolve(id, importer, { ...extraOptions, skipSelf: true })
let shouldExternal = await externalStrategy(id, importer, resolved)
// externalStrategy
async function externalStrategy(id, importer, resolved) {
// ...
if (skipNodeModulesBundle && resolved && (resolved.external || RE_NODE_MODULES.test(resolved.id))) {
const resolvedDep = await resolveDepSubpath(id, resolved)
return resolvedDep ? [true, resolvedDep] : true
}
// ...
return false // resolve failed -> NOT externalized -> bundled
}
Under platform: "neutral", resolved is null for a no-exports package, so the function falls through to return false and the dep is bundled.
Workaround
deps.neverBundle feeds rolldown's native external (matched on the specifier, no resolution required), so it externalizes correctly. Note it is tested against both the raw specifier and the resolved absolute path, so internal/absolute paths must be excluded:
export default defineConfig({
entry: ["./src/index.ts"],
dts: true,
platform: "neutral",
deps: {
skipNodeModulesBundle: true,
neverBundle: (id) => !path.isAbsolute(id) && !id.startsWith(".") && !id.startsWith("#"),
},
})
Reproduction link or steps
https://stackblitz.com/edit/github-va9a7t9q?file=tsdown.config.ts
deps.skipNodeModulesBundleis documented as "Skip bundling allnode_modulesdependencies." But for any dependency published without anexportsfield (legacymain/typesonly), it gets bundled anyway wheneverplatformis not"node". For thedtsbuild this causes an error when the inlined package transitively references a CJS.d.ts.This issue requires a combination of
deps.skipNodeModulesBundle: true+ a non-nodeplatform+ a dependency without anexportsfield.What is expected?
deps.skipNodeModulesBundle: trueshould externalize every bare / non-relativenode_modulesimport, regardless of whether tsdown's resolver can resolve it and regardless of the configuredplatform.What is actually happening?
Under a non-
nodeplatform, rolldown's resolver does not fall back to the legacymain/typesfields, so packages without anexportsfield don't get resolved.skipNodeModulesBundleonly externalizes when that resolve succeeds, so these deps get inlined.Any additional comments?
The following was generated by Claude:
In
DepsPlugin(tsdown/dist/format-*.mjs),externalStrategyonly externalizes anode_modulesimport when the priorthis.resolve(...)succeeded:Under
platform: "neutral",resolvedisnullfor a no-exportspackage, so the function falls through toreturn falseand the dep is bundled.Workaround
deps.neverBundlefeeds rolldown's nativeexternal(matched on the specifier, no resolution required), so it externalizes correctly. Note it is tested against both the raw specifier and the resolved absolute path, so internal/absolute paths must be excluded: