Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 34 additions & 11 deletions src/features/deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,9 +185,15 @@ export function DepsPlugin(
tsdownBundle: TsdownBundle,
): Plugin {
const deps = pkg && Array.from(getProductionDeps(pkg))
// Keyed by package.json path. Every subpath import of a package reads the
// same file, so read it once per build.
const hasExportsCache: HasExportsCache = new Map()

return {
name: 'tsdown:deps',
buildStart() {
hasExportsCache.clear()
},
resolveId: {
filter: [include(and(id(/^[^.]/), importerId(/./)))],
async handler(id, importer, extraOptions) {
Expand Down Expand Up @@ -382,7 +388,8 @@ export function DepsPlugin(
}
if (RE_PACKAGE_SPECIFIER.test(id)) {
const resolvedDep =
shouldResolveDepSubpath && (await resolveDepSubpath(id, resolve))
shouldResolveDepSubpath &&
(await resolveDepSubpath(id, resolve, hasExportsCache))
return resolvedDep ? [true, resolvedDep] : true
}
const resolved = await resolve()
Expand All @@ -394,7 +401,8 @@ export function DepsPlugin(
if (deps) {
if (deps.includes(id) || deps.some((dep) => id.startsWith(`${dep}/`))) {
const resolvedDep =
shouldResolveDepSubpath && (await resolveDepSubpath(id, resolve))
shouldResolveDepSubpath &&
(await resolveDepSubpath(id, resolve, hasExportsCache))
return resolvedDep ? [true, resolvedDep] : true
}

Expand Down Expand Up @@ -494,7 +502,24 @@ export function getTypesPackageName(id: string): string | undefined {
return `@types/${name.replace(/^@/, '').replace('/', '__')}`
}

async function resolveDepSubpath(id: string, resolve: ResolveFn) {
// `undefined` when the package.json can't be read or parsed
type HasExportsCache = Map<string, Promise<boolean | undefined>>

async function readHasExports(
packageJsonPath: string,
): Promise<boolean | undefined> {
try {
return !!JSON.parse(await readFile(packageJsonPath, 'utf8')).exports
} catch {
return undefined
}
}

async function resolveDepSubpath(
id: string,
resolve: ResolveFn,
hasExportsCache: HasExportsCache,
) {
const parts = id.split('/')
// ignore scope
if (parts[0][0] === '@') parts.shift()
Expand All @@ -504,15 +529,13 @@ async function resolveDepSubpath(id: string, resolve: ResolveFn) {
const resolved = await resolve()
if (!resolved?.packageJsonPath) return

let pkgJson: Record<string, any>
try {
pkgJson = JSON.parse(await readFile(resolved.packageJsonPath, 'utf8'))
} catch {
return
let hasExports = hasExportsCache.get(resolved.packageJsonPath)
if (!hasExports) {
hasExports = readHasExports(resolved.packageJsonPath)
hasExportsCache.set(resolved.packageJsonPath, hasExports)
}

// no `exports` field
if (pkgJson.exports) return
// only rewrite packages that have a readable package.json without `exports`
if ((await hasExports) !== false) return

const parsed = parseNodeModulesPath(resolved.id)
if (!parsed) return
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
## index.mjs

```mjs
import { lt } from "my-dep/functions/lt.js";
import { a } from "with-exports/a";
import { gt } from "my-dep/functions/gt.js";
import { b } from "with-exports/b";
export { a, b, gt, lt };

```
38 changes: 38 additions & 0 deletions tests/e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1486,6 +1486,44 @@ describe('resolve dep subpath without exports field', () => {

expect(fileMap['index.mjs']).toContain('my-dep/folder/index.js')
})

test('each package is checked for exports separately', async (context) => {
const { fileMap } = await testBuild({
context,
files: {
'node_modules/my-dep/package.json': JSON.stringify({
name: 'my-dep',
main: 'index.js',
}),
'node_modules/my-dep/functions/lt.js': `export const lt = () => {}`,
'node_modules/my-dep/functions/gt.js': `export const gt = () => {}`,
'node_modules/with-exports/package.json': JSON.stringify({
name: 'with-exports',
exports: { './*': './dist/*.js' },
}),
'node_modules/with-exports/dist/a.js': `export const a = 1`,
'node_modules/with-exports/dist/b.js': `export const b = 2`,
'index.ts': `export * from './one'
export * from './two'`,
'one.ts': `export { lt } from 'my-dep/functions/lt'
export { a } from 'with-exports/a'`,
'two.ts': `export { gt } from 'my-dep/functions/gt'
export { b } from 'with-exports/b'`,
'package.json': JSON.stringify({
name: 'test-pkg',
dependencies: { 'my-dep': '^1.0.0', 'with-exports': '^1.0.0' },
}),
},
options: {
deps: { resolveDepSubpath: true },
},
})

expect(fileMap['index.mjs']).toContain('my-dep/functions/lt.js')
expect(fileMap['index.mjs']).toContain('my-dep/functions/gt.js')
expect(fileMap['index.mjs']).toMatch(/from ["']with-exports\/a["']/)
expect(fileMap['index.mjs']).toMatch(/from ["']with-exports\/b["']/)
})
})

describe('neverBundle: true', () => {
Expand Down
Loading