diff --git a/.changeset/selfish-boxes-fry.md b/.changeset/selfish-boxes-fry.md new file mode 100644 index 00000000000..2719120cc8e --- /dev/null +++ b/.changeset/selfish-boxes-fry.md @@ -0,0 +1,50 @@ +--- +"@pnpm/resolve-dependencies": patch +"pnpm": patch +--- + +The location of an injected directory dependency should be correctly located, when there is a chain of local dependencies (declared via the `file:` protocol`). + +The next scenario was not working prior to the fix. There are 3 projects in the same folder: foo, bar, qar. + +`foo/package.json`: + +```json +{ + "name": "foo", + "dependencies": { + "bar": "file:../bar" + }, + "dependenciesMeta": { + "bar": { + "injected": true + } + } +} +``` + +`bar/package.json`: + +```json +{ + "name": "bar", + "dependencies": { + "qar": "file:../qar" + }, + "dependenciesMeta": { + "qar": { + "injected": true + } + } +} +``` + +`qar/package.json`: + +```json +{ + "name": "qar" +} +``` + +Related PR: [#4415](https://github.com/pnpm/pnpm/pull/4415). diff --git a/packages/core/test/install/injectLocalPackages.ts b/packages/core/test/install/injectLocalPackages.ts index 75048635f3b..14bf2583837 100644 --- a/packages/core/test/install/injectLocalPackages.ts +++ b/packages/core/test/install/injectLocalPackages.ts @@ -5,6 +5,7 @@ import { MutatedProject, mutateModules } from '@pnpm/core' import { preparePackages } from '@pnpm/prepare' import rimraf from '@zkochan/rimraf' import pathExists from 'path-exists' +import { sync as writeJsonFile } from 'write-json-file' import { testDefaults } from '../utils' test('inject local packages', async () => { @@ -224,6 +225,224 @@ test('inject local packages', async () => { } }) +test('inject local packages declared via file protocol', async () => { + const project1Manifest = { + name: 'project-1', + version: '1.0.0', + dependencies: { + 'is-negative': '1.0.0', + }, + devDependencies: { + 'dep-of-pkg-with-1-dep': '100.0.0', + }, + peerDependencies: { + 'is-positive': '>=1.0.0', + }, + } + const project2Manifest = { + name: 'project-2', + version: '1.0.0', + dependencies: { + 'project-1': 'file:../project-1', + }, + devDependencies: { + 'is-positive': '1.0.0', + }, + dependenciesMeta: { + 'project-1': { + injected: true, + }, + }, + } + const project3Manifest = { + name: 'project-3', + version: '1.0.0', + dependencies: { + 'project-2': 'file:../project-2', + }, + devDependencies: { + 'is-positive': '2.0.0', + }, + dependenciesMeta: { + 'project-2': { + injected: true, + }, + }, + } + const projects = preparePackages([ + { + location: 'project-1', + package: project1Manifest, + }, + { + location: 'project-2', + package: project2Manifest, + }, + { + location: 'project-3', + package: project3Manifest, + }, + ]) + + const importers: MutatedProject[] = [ + { + buildIndex: 0, + manifest: project1Manifest, + mutation: 'install', + rootDir: path.resolve('project-1'), + }, + { + buildIndex: 0, + manifest: project2Manifest, + mutation: 'install', + rootDir: path.resolve('project-2'), + }, + { + buildIndex: 0, + manifest: project3Manifest, + mutation: 'install', + rootDir: path.resolve('project-3'), + }, + ] + const workspacePackages = { + 'project-1': { + '1.0.0': { + dir: path.resolve('project-1'), + manifest: project1Manifest, + }, + }, + 'project-2': { + '1.0.0': { + dir: path.resolve('project-2'), + manifest: project2Manifest, + }, + }, + 'project-3': { + '1.0.0': { + dir: path.resolve('project-3'), + manifest: project2Manifest, + }, + }, + } + await mutateModules(importers, await testDefaults({ + workspacePackages, + })) + + await projects['project-1'].has('is-negative') + await projects['project-1'].has('dep-of-pkg-with-1-dep') + await projects['project-1'].hasNot('is-positive') + + await projects['project-2'].has('is-positive') + await projects['project-2'].has('project-1') + + await projects['project-3'].has('is-positive') + await projects['project-3'].has('project-2') + + expect(fs.readdirSync('node_modules/.pnpm').length).toBe(8) + + const rootModules = assertProject(process.cwd()) + { + const lockfile = await rootModules.readLockfile() + expect(lockfile.importers['project-2'].dependenciesMeta).toEqual({ + 'project-1': { + injected: true, + }, + }) + expect(lockfile.packages['file:project-1_is-positive@1.0.0']).toEqual({ + resolution: { + directory: 'project-1', + type: 'directory', + }, + id: 'file:project-1', + name: 'project-1', + version: '1.0.0', + peerDependencies: { + 'is-positive': '>=1.0.0', + }, + dependencies: { + 'is-negative': '1.0.0', + 'is-positive': '1.0.0', + }, + dev: false, + }) + expect(lockfile.packages['file:project-2_is-positive@2.0.0']).toEqual({ + resolution: { + directory: 'project-2', + type: 'directory', + }, + id: 'file:project-2', + name: 'project-2', + version: '1.0.0', + dependencies: { + 'project-1': 'file:project-1_is-positive@2.0.0', + }, + transitivePeerDependencies: ['is-positive'], + dev: false, + }) + + const modulesState = await rootModules.readModulesManifest() + expect(modulesState?.injectedDeps?.['project-1'].length).toEqual(2) + expect(modulesState?.injectedDeps?.['project-1'][0]).toContain(`node_modules${path.sep}.pnpm`) + expect(modulesState?.injectedDeps?.['project-1'][1]).toContain(`node_modules${path.sep}.pnpm`) + } + + await rimraf('node_modules') + await rimraf('project-1/node_modules') + await rimraf('project-2/node_modules') + await rimraf('project-3/node_modules') + + await mutateModules(importers, await testDefaults({ + frozenLockfile: true, + workspacePackages, + })) + + await projects['project-1'].has('is-negative') + await projects['project-1'].has('dep-of-pkg-with-1-dep') + await projects['project-1'].hasNot('is-positive') + + await projects['project-2'].has('is-positive') + await projects['project-2'].has('project-1') + + await projects['project-3'].has('is-positive') + await projects['project-3'].has('project-2') + + expect(fs.readdirSync('node_modules/.pnpm').length).toBe(8) + + // The injected project is updated when one of its dependencies needs to be updated + importers[0].manifest.dependencies!['is-negative'] = '2.0.0' + writeJsonFile('project-1/package.json', importers[0].manifest) + await mutateModules(importers, await testDefaults({ workspacePackages })) + { + const lockfile = await rootModules.readLockfile() + expect(lockfile.importers['project-2'].dependenciesMeta).toEqual({ + 'project-1': { + injected: true, + }, + }) + expect(lockfile.packages['file:project-1_is-positive@1.0.0']).toEqual({ + resolution: { + directory: 'project-1', + type: 'directory', + }, + id: 'file:project-1', + name: 'project-1', + version: '1.0.0', + peerDependencies: { + 'is-positive': '>=1.0.0', + }, + dependencies: { + 'is-negative': '2.0.0', + 'is-positive': '1.0.0', + }, + dev: false, + }) + const modulesState = await rootModules.readModulesManifest() + expect(modulesState?.injectedDeps?.['project-1'].length).toEqual(2) + expect(modulesState?.injectedDeps?.['project-1'][0]).toContain(`node_modules${path.sep}.pnpm`) + expect(modulesState?.injectedDeps?.['project-1'][1]).toContain(`node_modules${path.sep}.pnpm`) + } +}) + test('inject local packages and relink them after build', async () => { const project1Manifest = { name: 'project-1', diff --git a/packages/resolve-dependencies/src/resolveDependencies.ts b/packages/resolve-dependencies/src/resolveDependencies.ts index d8381791fed..1ad99d3fc58 100644 --- a/packages/resolve-dependencies/src/resolveDependencies.ts +++ b/packages/resolve-dependencies/src/resolveDependencies.ts @@ -619,7 +619,12 @@ async function resolveDependency ( lockfileDir: ctx.lockfileDir, preferredVersions: options.preferredVersions, preferWorkspacePackages: ctx.preferWorkspacePackages, - projectDir: options.currentDepth > 0 ? ctx.lockfileDir : ctx.prefix, + projectDir: ( + options.currentDepth > 0 && + !wantedDependency.pref.startsWith('file:') + ) + ? ctx.lockfileDir + : ctx.prefix, registry: wantedDependency.alias && pickRegistryForPackage(ctx.registries, wantedDependency.alias, wantedDependency.pref) || ctx.registries.default, // Unfortunately, even when run with --lockfile-only, we need the *real* package.json // so fetching of the tarball cannot be ever avoided. Related issue: https://github.com/pnpm/pnpm/issues/1176