1- import * as lockfile from '@yarnpkg/lockfile' ;
2- import { promises as fs } from 'fs' ;
1+ import { promises as fs , exists } from 'fs' ;
32import * as path from 'path' ;
3+ import * as lockfile from '@yarnpkg/lockfile' ;
44import { hoistDependencies } from './hoisting' ;
55import { PackageJson , PackageLock , PackageLockEntry , PackageLockPackage , YarnLock } from './types' ;
66
@@ -42,7 +42,7 @@ export async function generateShrinkwrap(options: ShrinkwrapOptions): Promise<Pa
4242
4343 if ( options . outputFile ) {
4444 // Write the shrinkwrap file
45- await fs . writeFile ( options . outputFile , JSON . stringify ( lock , undefined , 2 ) , { encoding : 'utf8' } ) ;
45+ await fs . writeFile ( options . outputFile , JSON . stringify ( lock , undefined , 2 ) , { encoding : 'utf8' } ) ;
4646 }
4747
4848 return lock ;
@@ -66,9 +66,9 @@ async function dependenciesFor(deps: Record<string, string>, yarnLock: YarnLock,
6666 rootDir = await fs . realpath ( rootDir ) ;
6767
6868 for ( const [ depName , versionRange ] of Object . entries ( deps ) ) {
69- const depPkgJsonFile = require . resolve ( `${ depName } /package.json` , { paths : [ rootDir ] } ) ;
69+ const depDir = await findPackageDir ( depName , rootDir ) ;
70+ const depPkgJsonFile = path . join ( depDir , 'package.json' ) ;
7071 const depPkgJson = await loadPackageJson ( depPkgJsonFile ) ;
71- const depDir = path . dirname ( depPkgJsonFile ) ;
7272 const yarnKey = `${ depName } @${ versionRange } ` ;
7373
7474 // Sanity check
@@ -150,4 +150,40 @@ export function formatPackageLock(entry: PackageLockEntry) {
150150 recurse ( [ ...names , depName ] , depEntry ) ;
151151 }
152152 }
153+ }
154+
155+ /**
156+ * Find package directory
157+ *
158+ * Do this by walking upwards in the directory tree until we find
159+ * `<dir>/node_modules/<package>/package.json`.
160+ *
161+ * -------
162+ *
163+ * Things that we tried but don't work:
164+ *
165+ * 1. require.resolve(`${depName}/package.json`, { paths: [rootDir] });
166+ *
167+ * Breaks with ES Modules if `package.json` has not been exported, which is
168+ * being enforced starting Node12.
169+ *
170+ * 2. findPackageJsonUpwardFrom(require.resolve(depName, { paths: [rootDir] }))
171+ *
172+ * Breaks if a built-in NodeJS package name conflicts with an NPM package name
173+ * (in Node15 `string_decoder` is introduced...)
174+ */
175+ async function findPackageDir ( depName : string , rootDir : string ) {
176+ let prevDir ;
177+ let dir = rootDir ;
178+ while ( dir !== prevDir ) {
179+ const candidateDir = path . join ( dir , 'node_modules' , depName ) ;
180+ if ( await new Promise ( ok => exists ( path . join ( candidateDir , 'package.json' ) , ok ) ) ) {
181+ return candidateDir ;
182+ }
183+
184+ prevDir = dir ;
185+ dir = path . dirname ( dir ) ; // dirname('/') -> '/', dirname('c:\\') -> 'c:\\'
186+ }
187+
188+ throw new Error ( `Did not find '${ depName } ' upwards of '${ rootDir } '` ) ;
153189}
0 commit comments