@@ -26,7 +26,7 @@ export class PnpmPackageManager extends BasePackageManager {
2626 $hostInfo : IHostInfo ,
2727 private $httpClient : Server . IHttpClient ,
2828 private $logger : ILogger ,
29- $pacoteService : IPacoteService
29+ $pacoteService : IPacoteService ,
3030 ) {
3131 super ( $childProcess , $fs , $hostInfo , $pacoteService , "pnpm" ) ;
3232 }
@@ -35,7 +35,7 @@ export class PnpmPackageManager extends BasePackageManager {
3535 public async install (
3636 packageName : string ,
3737 pathToSave : string ,
38- config : INodePackageManagerInstallOptions
38+ config : INodePackageManagerInstallOptions ,
3939 ) : Promise < INpmInstallResultInfo > {
4040 if ( config . disableNpmInstall ) {
4141 return ;
@@ -44,13 +44,25 @@ export class PnpmPackageManager extends BasePackageManager {
4444 if ( config . ignoreScripts ) {
4545 config [ "ignore-scripts" ] = true ;
4646 }
47+ // CLI-internal options must never reach the command line: pnpm, unlike
48+ // npm, hard-fails on unknown options.
49+ delete config . ignoreScripts ;
50+ delete config . path ;
51+ delete config . frameworkPath ;
4752
4853 const packageJsonPath = path . join ( pathToSave , "package.json" ) ;
4954 const jsonContentBefore = this . $fs . readJson ( packageJsonPath ) ;
5055
5156 const flags = this . getFlagsString ( config , true ) ;
52- // With pnpm we need to install as "flat" or some imports wont be found
53- let params = [ "i" , "--shamefully-hoist" ] ;
57+ let params = [ "i" ] ;
58+ if ( ! this . projectManagesOwnHoisting ( pathToSave ) ) {
59+ // With pnpm's default isolated layout some imports won't be found, so
60+ // install "flat". Skipped when the project configures its own layout:
61+ // pnpm treats a hoisting flag that contradicts the stored install state
62+ // as a config change and rebuilds node_modules from scratch after a
63+ // prompt (aborting outright when there is no TTY).
64+ params . push ( "--shamefully-hoist" ) ;
65+ }
5466 const isInstallingAllDependencies = packageName === pathToSave ;
5567 if ( ! isInstallingAllDependencies ) {
5668 params . push ( packageName ) ;
@@ -63,7 +75,7 @@ export class PnpmPackageManager extends BasePackageManager {
6375 const result = await this . processPackageManagerInstall (
6476 packageName ,
6577 params ,
66- { cwd, isInstallingAllDependencies }
78+ { cwd, isInstallingAllDependencies } ,
6779 ) ;
6880 return result ;
6981 } catch ( e ) {
@@ -76,7 +88,7 @@ export class PnpmPackageManager extends BasePackageManager {
7688 public uninstall (
7789 packageName : string ,
7890 config ?: IDictionary < string | boolean > ,
79- cwd ?: string
91+ cwd ?: string ,
8092 ) : Promise < string > {
8193 // pnpm does not want save option in remove. It saves it by default
8294 delete config [ "save" ] ;
@@ -94,7 +106,7 @@ export class PnpmPackageManager extends BasePackageManager {
94106 let viewResult : any ;
95107 try {
96108 viewResult = await this . $childProcess . exec (
97- `pnpm info ${ packageName } ${ flags } `
109+ `pnpm info ${ packageName } ${ flags } ` ,
98110 ) ;
99111 } catch ( e ) {
100112 this . $errors . fail ( e . message ) ;
@@ -110,15 +122,15 @@ export class PnpmPackageManager extends BasePackageManager {
110122 @exported ( "pnpm" )
111123 public search (
112124 filter : string [ ] ,
113- config : IDictionary < string | boolean >
125+ config : IDictionary < string | boolean > ,
114126 ) : Promise < string > {
115127 const flags = this . getFlagsString ( config , false ) ;
116128 return this . $childProcess . exec ( `pnpm search ${ filter . join ( " " ) } ${ flags } ` ) ;
117129 }
118130
119131 public async searchNpms ( keyword : string ) : Promise < INpmsResult > {
120132 const httpRequestResult = await this . $httpClient . httpRequest (
121- `https://api.npms.io/v2/search?q=keywords:${ keyword } `
133+ `https://api.npms.io/v2/search?q=keywords:${ keyword } ` ,
122134 ) ;
123135 const result : INpmsResult = JSON . parse ( httpRequestResult . body ) ;
124136 return result ;
@@ -129,23 +141,60 @@ export class PnpmPackageManager extends BasePackageManager {
129141 const registry = await this . $childProcess . exec ( `pnpm config get registry` ) ;
130142 const url = `${ registry . trim ( ) } /${ packageName } ` ;
131143 this . $logger . trace (
132- `Trying to get data from pnpm registry for package ${ packageName } , url is: ${ url } `
144+ `Trying to get data from pnpm registry for package ${ packageName } , url is: ${ url } ` ,
133145 ) ;
134146 const responseData = ( await this . $httpClient . httpRequest ( url ) ) . body ;
135147 this . $logger . trace (
136- `Successfully received data from pnpm registry for package ${ packageName } . Response data is: ${ responseData } `
148+ `Successfully received data from pnpm registry for package ${ packageName } . Response data is: ${ responseData } ` ,
137149 ) ;
138150 const jsonData = JSON . parse ( responseData ) ;
139151 this . $logger . trace (
140- `Successfully parsed data from pnpm registry for package ${ packageName } .`
152+ `Successfully parsed data from pnpm registry for package ${ packageName } .` ,
141153 ) ;
142154 return jsonData ;
143155 }
144156
145157 @exported ( "pnpm" )
146158 public async getCachePath ( ) : Promise < string > {
147159 const cachePath = await this . $childProcess . exec ( `pnpm config get cache` ) ;
148- return path . join ( cachePath . trim ( ) , CACACHE_DIRECTORY_NAME ) ;
160+ const cacheDir = cachePath && cachePath . trim ( ) ;
161+ // pnpm has no `cache` config key of its own: modern versions print
162+ // "undefined" (older ones an empty string), which would yield a relative
163+ // garbage path. Derive a stable per-user location from the store instead.
164+ if ( cacheDir && cacheDir !== "undefined" && cacheDir !== "null" ) {
165+ return path . join ( cacheDir , CACACHE_DIRECTORY_NAME ) ;
166+ }
167+ const storePath = await this . $childProcess . exec ( `pnpm store path` ) ;
168+ return path . join ( path . dirname ( storePath . trim ( ) ) , CACACHE_DIRECTORY_NAME ) ;
169+ }
170+
171+ private projectManagesOwnHoisting ( installDir : string ) : boolean {
172+ // A pnpm-workspace.yaml (pnpm's config home since v10) or an .npmrc with
173+ // a layout key marks the node_modules layout as the project's own choice.
174+ const layoutKeyPattern =
175+ / ^ \s * ( s h a m e f u l l y - h o i s t | n o d e - l i n k e r | h o i s t | h o i s t - p a t t e r n | p u b l i c - h o i s t - p a t t e r n ) \s * [ = : ] / m;
176+ let dir = path . resolve ( installDir ) ;
177+ while ( true ) {
178+ if ( this . $fs . exists ( path . join ( dir , "pnpm-workspace.yaml" ) ) ) {
179+ return true ;
180+ }
181+ const npmrcPath = path . join ( dir , ".npmrc" ) ;
182+ if ( this . $fs . exists ( npmrcPath ) ) {
183+ try {
184+ const npmrcContent = this . $fs . readText ( npmrcPath ) ;
185+ if ( npmrcContent && layoutKeyPattern . test ( npmrcContent ) ) {
186+ return true ;
187+ }
188+ } catch ( err ) {
189+ this . $logger . trace ( `Unable to read ${ npmrcPath } . Error is: ` , err ) ;
190+ }
191+ }
192+ const parent = path . dirname ( dir ) ;
193+ if ( parent === dir ) {
194+ return false ;
195+ }
196+ dir = parent ;
197+ }
149198 }
150199}
151200
0 commit comments