@@ -9,8 +9,8 @@ import * as nodePath from 'path';
99 * @public
1010 */
1111export interface IRealNodeModulePathResolverOptions {
12- fs : Pick < typeof nodeFs , 'lstatSync' | 'readlinkSync' > ;
13- path : Pick < typeof nodePath , 'isAbsolute' | 'normalize ' | 'resolve' | 'sep' > ;
12+ fs ?: Partial < Pick < typeof nodeFs , 'lstatSync' | 'readlinkSync' > > ;
13+ path ?: Partial < Pick < typeof nodePath , 'isAbsolute' | 'join ' | 'resolve' | 'sep' > > ;
1414}
1515
1616/**
@@ -38,22 +38,33 @@ export class RealNodeModulePathResolver {
3838 public readonly realNodeModulePath : ( input : string ) => string ;
3939
4040 private readonly _cache : Map < string , string > ;
41- private readonly _fs : IRealNodeModulePathResolverOptions [ 'fs' ] ;
42-
43- public constructor (
44- options : IRealNodeModulePathResolverOptions = {
45- fs : nodeFs ,
46- path : nodePath
47- }
48- ) {
41+ private readonly _fs : Required < NonNullable < IRealNodeModulePathResolverOptions [ 'fs' ] > > ;
42+ private readonly _path : Required < NonNullable < IRealNodeModulePathResolverOptions [ 'path' ] > > ;
43+
44+ public constructor ( options : IRealNodeModulePathResolverOptions = { } ) {
45+ const {
46+ fs : { lstatSync = nodeFs . lstatSync , readlinkSync = nodeFs . readlinkSync } = nodeFs ,
47+ path : {
48+ isAbsolute = nodePath . isAbsolute ,
49+ join = nodePath . join ,
50+ resolve = nodePath . resolve ,
51+ sep = nodePath . sep
52+ } = nodePath
53+ } = options ;
4954 const cache : Map < string , string > = ( this . _cache = new Map ( ) ) ;
50- const { path, fs } = options ;
51- const { sep : pathSeparator } = path ;
52- this . _fs = fs ;
53-
54- const nodeModulesToken : string = `${ pathSeparator } node_modules${ pathSeparator } ` ;
55+ this . _fs = {
56+ lstatSync,
57+ readlinkSync
58+ } ;
59+ this . _path = {
60+ isAbsolute,
61+ join,
62+ resolve,
63+ sep
64+ } ;
5565
56- const tryReadLink : ( link : string ) => string | undefined = this . _tryReadLink . bind ( this ) ;
66+ const nodeModulesToken : string = `${ sep } node_modules${ sep } ` ;
67+ const self : this = this ;
5768
5869 function realNodeModulePathInternal ( input : string ) : string {
5970 // Find the last node_modules path segment
@@ -65,19 +76,24 @@ export class RealNodeModulePathResolver {
6576
6677 // First assume that the next path segment after node_modules is a symlink
6778 let linkStart : number = nodeModulesIndex + nodeModulesToken . length - 1 ;
68- let linkEnd : number = input . indexOf ( pathSeparator , linkStart + 1 ) ;
79+ let linkEnd : number = input . indexOf ( sep , linkStart + 1 ) ;
6980 // If the path segment starts with a '@', then it is a scoped package
7081 const isScoped : boolean = input . charAt ( linkStart + 1 ) === '@' ;
7182 if ( isScoped ) {
7283 // For a scoped package, the scope is an ordinary directory, so we need to find the next path segment
7384 if ( linkEnd < 0 ) {
7485 // Symlink missing, so see if anything before the last node_modules needs resolving,
7586 // and preserve the rest of the path
76- return `${ realNodeModulePathInternal ( input . slice ( 0 , nodeModulesIndex ) ) } ${ input . slice ( nodeModulesIndex ) } ` ;
87+ return join (
88+ realNodeModulePathInternal ( input . slice ( 0 , nodeModulesIndex ) ) ,
89+ input . slice ( nodeModulesIndex + 1 ) ,
90+ // Joining to `.` will clean up any extraneous trailing slashes
91+ '.'
92+ ) ;
7793 }
7894
7995 linkStart = linkEnd ;
80- linkEnd = input . indexOf ( pathSeparator , linkStart + 1 ) ;
96+ linkEnd = input . indexOf ( sep , linkStart + 1 ) ;
8197 }
8298
8399 // No trailing separator, so the link is the last path segment
@@ -87,13 +103,14 @@ export class RealNodeModulePathResolver {
87103
88104 const linkCandidate : string = input . slice ( 0 , linkEnd ) ;
89105 // Check if the link is a symlink
90- const linkTarget : string | undefined = tryReadLink ( linkCandidate ) ;
91- if ( linkTarget && path . isAbsolute ( linkTarget ) ) {
106+ const linkTarget : string | undefined = self . _tryReadLink ( linkCandidate ) ;
107+ if ( linkTarget && isAbsolute ( linkTarget ) ) {
92108 // Absolute path, combine the link target with any remaining path segments
93109 // Cache the resolution to avoid the readlink call in subsequent calls
94110 cache . set ( linkCandidate , linkTarget ) ;
95111 cache . set ( linkTarget , linkTarget ) ;
96- return `${ linkTarget } ${ input . slice ( linkEnd ) } ` ;
112+ // Joining to `.` will clean up any extraneous trailing slashes
113+ return join ( linkTarget , input . slice ( linkEnd + 1 ) , '.' ) ;
97114 }
98115
99116 // Relative path or does not exist
@@ -102,23 +119,26 @@ export class RealNodeModulePathResolver {
102119 const realpathBeforeNodeModules : string = realNodeModulePathInternal ( input . slice ( 0 , nodeModulesIndex ) ) ;
103120 if ( linkTarget ) {
104121 // Relative path in symbolic link. Should be resolved relative to real path of base path.
105- const resolvedTarget : string = path . resolve (
106- `${ realpathBeforeNodeModules } ${ input . slice ( nodeModulesIndex , linkStart ) } ` ,
122+ const resolvedTarget : string = resolve (
123+ realpathBeforeNodeModules ,
124+ input . slice ( nodeModulesIndex + 1 , linkStart ) ,
107125 linkTarget
108126 ) ;
109127 // Cache the result of the combined resolution to avoid the readlink call in subsequent calls
110128 cache . set ( linkCandidate , resolvedTarget ) ;
111129 cache . set ( resolvedTarget , resolvedTarget ) ;
112- return `${ resolvedTarget } ${ input . slice ( linkEnd ) } ` ;
130+ // Joining to `.` will clean up any extraneous trailing slashes
131+ return join ( resolvedTarget , input . slice ( linkEnd + 1 ) , '.' ) ;
113132 }
114133
115134 // No symlink, so just return the real path before the last node_modules combined with the
116135 // subsequent path segments
117- return `${ realpathBeforeNodeModules } ${ input . slice ( nodeModulesIndex ) } ` ;
136+ // Joining to `.` will clean up any extraneous trailing slashes
137+ return join ( realpathBeforeNodeModules , input . slice ( nodeModulesIndex + 1 ) , '.' ) ;
118138 }
119139
120140 this . realNodeModulePath = ( input : string ) => {
121- return realNodeModulePathInternal ( path . normalize ( input ) ) ;
141+ return realNodeModulePathInternal ( resolve ( input ) ) ;
122142 } ;
123143 }
124144
@@ -146,7 +166,9 @@ export class RealNodeModulePathResolver {
146166 // of an lstat call.
147167 const stat : nodeFs . Stats | undefined = this . _fs . lstatSync ( link ) ;
148168 if ( stat . isSymbolicLink ( ) ) {
149- return this . _fs . readlinkSync ( link , 'utf8' ) ;
169+ // path.join(x, '.') will trim trailing slashes, if applicable
170+ const result : string = this . _path . join ( this . _fs . readlinkSync ( link , 'utf8' ) , '.' ) ;
171+ return result ;
150172 }
151173 }
152174}
0 commit comments