@@ -18,14 +18,9 @@ import type { TsdownBundle } from '../utils/chunks.ts'
1818import type { Logger } from '../utils/logger.ts'
1919import type { Arrayable } from '../utils/types.ts'
2020import type { PackageJson } from 'pkg-types'
21- import type {
22- ExternalOption ,
23- Plugin ,
24- PluginContext ,
25- ResolveIdExtraOptions ,
26- } from 'rolldown'
21+ import type { ExternalOption , Plugin , ResolvedId } from 'rolldown'
2722
28- const debug = createDebug ( 'tsdown:dep ' )
23+ const debug = createDebug ( 'tsdown:deps ' )
2924
3025export type NoExternalFn = (
3126 id : string ,
@@ -162,37 +157,7 @@ export function resolveDepsConfig(
162157 }
163158}
164159
165- async function parseBundledDep (
166- moduleId : string ,
167- ) : Promise < { name : string ; pkgName : string ; version : string } | undefined > {
168- const slashed = slash ( moduleId )
169- const lastNmIdx = slashed . lastIndexOf ( '/node_modules/' )
170- if ( lastNmIdx === - 1 ) return
171-
172- const afterNm = slashed . slice ( lastNmIdx + 14 /* '/node_modules/'.length */ )
173- const parts = afterNm . split ( '/' )
174-
175- let name : string
176- if ( parts [ 0 ] [ 0 ] === '@' ) {
177- name = `${ parts [ 0 ] } /${ parts [ 1 ] } `
178- } else {
179- name = parts [ 0 ]
180- }
181-
182- const root = slashed . slice (
183- 0 ,
184- lastNmIdx + 14 /* '/node_modules/'.length */ + name . length ,
185- )
186-
187- try {
188- const json = JSON . parse (
189- await readFile ( path . join ( root , 'package.json' ) , 'utf8' ) ,
190- )
191- return { name, pkgName : json . name , version : json . version }
192- } catch { }
193- }
194-
195- export function DepPlugin (
160+ export function DepsPlugin (
196161 {
197162 pkg,
198163 deps : { alwaysBundle, onlyBundle, skipNodeModulesBundle } ,
@@ -204,28 +169,40 @@ export function DepPlugin(
204169 const deps = pkg && Array . from ( getProductionDeps ( pkg ) )
205170
206171 return {
207- name : 'tsdown:external ' ,
172+ name : 'tsdown:deps ' ,
208173 resolveId : {
209174 filter : [ include ( and ( id ( / ^ [ ^ . ] / ) , importerId ( / ./ ) ) ) ] ,
210175 async handler ( id , importer , extraOptions ) {
211176 if ( extraOptions . isEntry ) return
212177 typeAssert ( importer )
213178
214- const shouldExternal = await externalStrategy (
215- this ,
216- id ,
217- importer ,
218- extraOptions ,
219- )
179+ const resolved = await this . resolve ( id , importer , {
180+ ...extraOptions ,
181+ skipSelf : true ,
182+ } )
183+ let shouldExternal = await externalStrategy ( id , importer , resolved )
184+ if ( Array . isArray ( shouldExternal ) ) {
185+ debug ( 'custom resolved id for %o -> %o' , id , shouldExternal [ 1 ] )
186+ id = shouldExternal [ 1 ]
187+ shouldExternal = shouldExternal [ 0 ]
188+ }
220189 const nodeBuiltinModule = isBuiltin ( id )
190+ const moduleSideEffects = nodeBuiltinModule ? false : undefined
221191
222192 debug ( 'shouldExternal: %o = %o' , id , shouldExternal )
223193
224194 if ( shouldExternal === true || shouldExternal === 'absolute' ) {
225195 return {
226196 id,
227197 external : shouldExternal ,
228- moduleSideEffects : nodeBuiltinModule ? false : undefined ,
198+ moduleSideEffects,
199+ }
200+ }
201+
202+ if ( resolved ) {
203+ return {
204+ ...resolved ,
205+ moduleSideEffects,
229206 }
230207 }
231208 } ,
@@ -311,35 +288,34 @@ export function DepPlugin(
311288
312289 /**
313290 * - `true`: always external
291+ * - `[true, resolvedId]`: external with custom resolved ID
314292 * - `false`: skip, let other plugins handle it
315293 * - `'absolute'`: external as absolute path
316294 * - `'no-external'`: skip, but mark as non-external for inlineOnly check
317295 */
318296 async function externalStrategy (
319- context : PluginContext ,
320297 id : string ,
321298 importer : string | undefined ,
322- extraOptions : ResolveIdExtraOptions ,
323- ) : Promise < boolean | 'absolute' | 'no-external' > {
299+ resolved : ResolvedId | null ,
300+ ) : Promise < boolean | [ true , string ] | 'absolute' | 'no-external' > {
324301 if ( id === shimFile ) return false
325302
326303 if ( alwaysBundle ?.( id , importer ) ) {
327304 return 'no-external'
328305 }
329306
330- if ( skipNodeModulesBundle ) {
331- const resolved = await context . resolve ( id , importer , extraOptions )
332- if (
333- resolved &&
334- ( resolved . external || RE_NODE_MODULES . test ( resolved . id ) )
335- ) {
336- return true
337- }
307+ if (
308+ skipNodeModulesBundle &&
309+ resolved &&
310+ ( resolved . external || RE_NODE_MODULES . test ( resolved . id ) )
311+ ) {
312+ return true
338313 }
339314
340315 if ( deps ) {
341316 if ( deps . includes ( id ) || deps . some ( ( dep ) => id . startsWith ( `${ dep } /` ) ) ) {
342- return true
317+ const resolvedDep = await resolveDepPath ( id , resolved )
318+ return resolvedDep ? [ true , resolvedDep ] : true
343319 }
344320
345321 if ( importer && RE_DTS . test ( importer ) && ! id . startsWith ( '@types/' ) ) {
@@ -354,6 +330,75 @@ export function DepPlugin(
354330 }
355331}
356332
333+ function parseDepPath (
334+ id : string ,
335+ ) : [ name : string , subpath : string , root : string ] | undefined {
336+ const slashed = slash ( id )
337+ const lastNmIdx = slashed . lastIndexOf ( '/node_modules/' )
338+ if ( lastNmIdx === - 1 ) return
339+
340+ const afterNm = slashed . slice ( lastNmIdx + 14 /* '/node_modules/'.length */ )
341+ const parts = afterNm . split ( '/' )
342+
343+ let name : string
344+ if ( parts [ 0 ] [ 0 ] === '@' ) {
345+ name = `${ parts [ 0 ] } /${ parts [ 1 ] } `
346+ } else {
347+ name = parts [ 0 ]
348+ }
349+
350+ const root = slashed . slice (
351+ 0 ,
352+ lastNmIdx + 14 /* '/node_modules/'.length */ + name . length ,
353+ )
354+
355+ return [ name , afterNm . slice ( name . length ) , root ]
356+ }
357+
358+ async function parseBundledDep (
359+ moduleId : string ,
360+ ) : Promise < { name : string ; pkgName : string ; version : string } | undefined > {
361+ const parsed = parseDepPath ( moduleId )
362+ if ( ! parsed ) return
363+
364+ const [ name , , root ] = parsed
365+
366+ try {
367+ const json = JSON . parse (
368+ await readFile ( path . join ( root , 'package.json' ) , 'utf8' ) ,
369+ )
370+ return { name, pkgName : json . name , version : json . version }
371+ } catch { }
372+ }
373+
374+ async function resolveDepPath ( id : string , resolved : ResolvedId | null ) {
375+ if ( ! resolved ?. packageJsonPath ) return
376+
377+ const parts = id . split ( '/' )
378+ // ignore scope
379+ if ( parts [ 0 ] [ 0 ] === '@' ) parts . shift ( )
380+ // ignore no subpath or file imports
381+ if ( parts . length === 1 || parts . at ( - 1 ) ! . includes ( '.' ) ) return
382+
383+ let pkgJson : Record < string , any >
384+ try {
385+ pkgJson = JSON . parse ( await readFile ( resolved . packageJsonPath , 'utf8' ) )
386+ } catch {
387+ return
388+ }
389+
390+ // no `exports` field
391+ if ( pkgJson . exports ) return
392+
393+ const parsed = parseDepPath ( resolved . id )
394+ if ( ! parsed ) return
395+
396+ const result = parsed [ 0 ] + parsed [ 1 ]
397+ if ( result === id ) return
398+
399+ return result
400+ }
401+
357402/*
358403 * Production deps should be excluded from the bundle
359404 */
0 commit comments