diff --git a/.changeset/odd-peas-fetch.md b/.changeset/odd-peas-fetch.md new file mode 100644 index 00000000000..e2e8150a03f --- /dev/null +++ b/.changeset/odd-peas-fetch.md @@ -0,0 +1,8 @@ +--- +"@pnpm/core": patch +"@pnpm/manifest-utils": patch +"@pnpm/plugin-commands-installation": patch +"@pnpm/types": patch +--- + +`pnpm link --global ` should not change the type of the dependency diff --git a/packages/core/src/link/index.ts b/packages/core/src/link/index.ts index 4dd1ada50d9..4f5c39c2331 100644 --- a/packages/core/src/link/index.ts +++ b/packages/core/src/link/index.ts @@ -37,6 +37,7 @@ import { type LinkFunctionOptions = LinkOptions & { linkToBin?: string dir: string + targetDependenciesFieldMap?: Record } export { LinkFunctionOptions } @@ -74,12 +75,19 @@ export async function link ( throw new PnpmError('INVALID_PACKAGE_NAME', `Package in ${linkFromPath} must have a name field to be linked`) } + let targetDependencyType: string + if (opts.targetDependenciesFieldMap && Object.keys(opts.targetDependenciesFieldMap).length > 0 && typeof linkFrom === 'string') { + targetDependencyType = opts.targetDependenciesFieldMap?.[linkFrom] + } else if (opts.targetDependenciesField) { + targetDependencyType = opts.targetDependenciesField + } + specsToUpsert.push({ alias: manifest.name, pref: getPref(manifest.name, manifest.name, manifest.version, { pinnedVersion: opts.pinnedVersion, }), - saveType: (opts.targetDependenciesField ?? (ctx.manifest && guessDependencyType(manifest.name, ctx.manifest))) as DependenciesField, + saveType: (targetDependencyType! ?? (ctx.manifest && guessDependencyType(manifest.name, ctx.manifest))) as DependenciesField, }) const packagePath = normalize(path.relative(opts.dir, linkFromPath)) @@ -109,7 +117,7 @@ export async function link ( // TODO: cover with test that linking reports with correct dependency types const stu = specsToUpsert.find((s) => s.alias === manifest.name) await symlinkDirectRootDependency(path, destModules, alias, { - fromDependenciesField: stu?.saveType ?? opts.targetDependenciesField, + fromDependenciesField: stu?.saveType ?? opts.targetDependenciesFieldMap?.[path] ?? opts.targetDependenciesField, linkedPackage: manifest, prefix: opts.dir, }) @@ -122,7 +130,7 @@ export async function link ( }) let newPkg!: ProjectManifest - if (opts.targetDependenciesField) { + if (opts.targetDependenciesField ?? Object.keys(opts.targetDependenciesFieldMap ?? {}).length > 0) { newPkg = await updateProjectManifestObject(opts.dir, opts.manifest, specsToUpsert) for (const { alias } of specsToUpsert) { updatedWantedLockfile.importers[importerId].specifiers[alias] = getSpecFromPackageManifest(newPkg, alias) diff --git a/packages/core/src/link/options.ts b/packages/core/src/link/options.ts index e8e7828fc52..5a0ead797d8 100644 --- a/packages/core/src/link/options.ts +++ b/packages/core/src/link/options.ts @@ -22,6 +22,7 @@ interface StrictLinkOptions { storeDir: string reporter: ReporterFunction targetDependenciesField?: DependenciesField + targetDependenciesFieldMap?: Record dir: string preferSymlinkedExecutables: boolean diff --git a/packages/core/test/link.ts b/packages/core/test/link.ts index f661f3fec1f..a6eeb2fb2ac 100644 --- a/packages/core/test/link.ts +++ b/packages/core/test/link.ts @@ -6,7 +6,7 @@ import { link, } from '@pnpm/core' import { fixtures } from '@pnpm/test-fixtures' -import { prepareEmpty } from '@pnpm/prepare' +import { prepare, prepareEmpty } from '@pnpm/prepare' import { addDistTag } from '@pnpm/registry-mock' import { RootLog } from '@pnpm/core-loggers' import sinon from 'sinon' @@ -210,6 +210,37 @@ test('throws error is package name is not defined', async () => { } }) +test('link with option targetDependenciesFieldMap', async () => { + const project = prepare({ + devDependencies: { + '@pnpm.e2e/hello-world-js-bin': '*', + }, + }) + + const linkedPkgName = 'hello-world-js-bin' + const linkedPkgPath = path.resolve('..', linkedPkgName) + + f.copy(linkedPkgName, linkedPkgPath) + await link([`../${linkedPkgName}`], path.join(process.cwd(), 'node_modules'), await testDefaults({ + dir: process.cwd(), + manifest: { + devDependencies: { + '@pnpm.e2e/hello-world-js-bin': '*', + }, + }, + targetDependenciesFieldMap: { + [`../${linkedPkgName}`]: 'devDependencies', + }, + })) + + await project.isExecutable('.bin/hello-world-js-bin') + + const wantedLockfile = await project.readLockfile() + expect(wantedLockfile.devDependencies).toStrictEqual({ + '@pnpm.e2e/hello-world-js-bin': 'link:../hello-world-js-bin', + }) +}) + // test.skip('relative link when an external lockfile is used', async () => { // const projects = prepare(t, [ // { diff --git a/packages/manifest-utils/src/getAllDependenciesFromManifest.ts b/packages/manifest-utils/src/getAllDependenciesFromManifest.ts index 3097471dae1..de0d90c9db4 100644 --- a/packages/manifest-utils/src/getAllDependenciesFromManifest.ts +++ b/packages/manifest-utils/src/getAllDependenciesFromManifest.ts @@ -1,7 +1,7 @@ -import { Dependencies, ProjectManifest } from '@pnpm/types' +import { Dependencies, DependenciesField, ProjectManifest } from '@pnpm/types' export function getAllDependenciesFromManifest ( - pkg: Pick + pkg: Pick ): Dependencies { return { ...pkg.devDependencies, diff --git a/packages/manifest-utils/src/getDependencyTypeFromManifest.ts b/packages/manifest-utils/src/getDependencyTypeFromManifest.ts new file mode 100644 index 00000000000..e5286a26ae9 --- /dev/null +++ b/packages/manifest-utils/src/getDependencyTypeFromManifest.ts @@ -0,0 +1,12 @@ +import { ProjectManifest, DependenciesManifestField } from '@pnpm/types' + +export const getDependencyTypeFromManifest = ( + manifest: Pick, + depName: string +): DependenciesManifestField | null => { + if (manifest.optionalDependencies?.[depName]) return 'optionalDependencies' + else if (manifest.peerDependencies?.[depName]) return 'peerDependencies' + else if (manifest.dependencies?.[depName]) return 'dependencies' + else if (manifest.devDependencies?.[depName]) return 'devDependencies' + else return null +} diff --git a/packages/manifest-utils/src/getSpecFromPackageManifest.ts b/packages/manifest-utils/src/getSpecFromPackageManifest.ts index c0195c29ca0..49750f6cae7 100644 --- a/packages/manifest-utils/src/getSpecFromPackageManifest.ts +++ b/packages/manifest-utils/src/getSpecFromPackageManifest.ts @@ -1,7 +1,7 @@ -import { ProjectManifest } from '@pnpm/types' +import { ProjectManifest, DependenciesManifestField } from '@pnpm/types' export function getSpecFromPackageManifest ( - manifest: Pick, + manifest: Pick, depName: string ) { return manifest.optionalDependencies?.[depName] ?? diff --git a/packages/manifest-utils/src/index.ts b/packages/manifest-utils/src/index.ts index c5891307b27..7882e90919c 100644 --- a/packages/manifest-utils/src/index.ts +++ b/packages/manifest-utils/src/index.ts @@ -7,6 +7,7 @@ import { getSpecFromPackageManifest } from './getSpecFromPackageManifest' export * from './getPref' export * from './updateProjectManifestObject' +export * from './getDependencyTypeFromManifest' export { getSpecFromPackageManifest } diff --git a/packages/manifest-utils/test/getDependencyTypeFromManifest.test.ts b/packages/manifest-utils/test/getDependencyTypeFromManifest.test.ts new file mode 100644 index 00000000000..07150d88cbd --- /dev/null +++ b/packages/manifest-utils/test/getDependencyTypeFromManifest.test.ts @@ -0,0 +1,38 @@ +import { getDependencyTypeFromManifest } from '@pnpm/manifest-utils' + +test('getDependencyTypeFromManifest()', () => { + expect( + getDependencyTypeFromManifest({ + dependencies: { + foo: '1.0.0', + }, + }, 'foo')).toEqual('dependencies') + + expect( + getDependencyTypeFromManifest({ + devDependencies: { + foo: '1.0.0', + }, + }, 'foo')).toEqual('devDependencies') + + expect( + getDependencyTypeFromManifest({ + optionalDependencies: { + foo: '1.0.0', + }, + }, 'foo')).toEqual('optionalDependencies') + + expect( + getDependencyTypeFromManifest({ + peerDependencies: { + foo: '1.0.0', + }, + }, 'foo')).toEqual('peerDependencies') + + expect( + getDependencyTypeFromManifest({ + peerDependencies: { + foo: '1.0.0', + }, + }, 'bar')).toEqual(null) +}) diff --git a/packages/plugin-commands-installation/src/link.ts b/packages/plugin-commands-installation/src/link.ts index 83e838d45ca..f8b47024cb2 100644 --- a/packages/plugin-commands-installation/src/link.ts +++ b/packages/plugin-commands-installation/src/link.ts @@ -21,6 +21,8 @@ import { LinkFunctionOptions, WorkspacePackages, } from '@pnpm/core' +import { getDependencyTypeFromManifest } from '@pnpm/manifest-utils' +import { DependenciesManifestField } from '@pnpm/types' import pLimit from 'p-limit' import pathAbsolute from 'path-absolute' import pick from 'ramda/src/pick' @@ -160,6 +162,9 @@ export async function handler ( })) ) + const { manifest, writeProjectManifest } = await readProjectManifest(linkCwdDir, opts) + const targetDependenciesFieldMap: Record = {} + if (pkgNames.length > 0) { let globalPkgNames!: string[] if (opts.workspaceDir) { @@ -178,11 +183,14 @@ export async function handler ( globalPkgNames = pkgNames } const globalPkgPath = pathAbsolute(opts.dir) - globalPkgNames.forEach((pkgName) => pkgPaths.push(path.join(globalPkgPath, 'node_modules', pkgName))) + globalPkgNames.forEach((pkgName) => { + const pkgPath = path.join(globalPkgPath, 'node_modules', pkgName) + pkgPaths.push(pkgPath) + const dependencyType = getDependencyTypeFromManifest(manifest, pkgName) + targetDependenciesFieldMap[pkgPath] = dependencyType ?? linkOpts.targetDependenciesField + }) } - const { manifest, writeProjectManifest } = await readProjectManifest(linkCwdDir, opts) - const linkConfig = await getConfig( { ...opts.cliOptions, dir: cwd }, { @@ -195,6 +203,7 @@ export async function handler ( const newManifest = await link(pkgPaths, path.join(linkCwdDir, 'node_modules'), { ...linkConfig, targetDependenciesField: linkOpts.targetDependenciesField, + targetDependenciesFieldMap, storeController: storeL.ctrl, storeDir: storeL.dir, manifest, diff --git a/packages/plugin-commands-installation/test/link.ts b/packages/plugin-commands-installation/test/link.ts index 4176f85d2a2..0a93e6d0266 100644 --- a/packages/plugin-commands-installation/test/link.ts +++ b/packages/plugin-commands-installation/test/link.ts @@ -4,6 +4,7 @@ import readYamlFile from 'read-yaml-file' import { install, link } from '@pnpm/plugin-commands-installation' import { prepare, preparePackages } from '@pnpm/prepare' import { assertProject, isExecutable } from '@pnpm/assert-project' +import { readProjectManifest } from '@pnpm/cli-utils' import { fixtures } from '@pnpm/test-fixtures' import PATH from 'path-name' import writePkg from 'write-pkg' @@ -145,6 +146,56 @@ test('link a global package to the specified directory', async function () { await project.has('global-package-with-bin') }) +test('do not change the type of the dependency when linking a global package', async function () { + prepare({ + devDependencies: { + 'global-package-with-bin': '*', + }, + }) + process.chdir('..') + + const globalDir = path.resolve('global') + const globalBin = path.join(globalDir, 'bin') + const oldPath = process.env[PATH] + process.env[PATH] = `${globalBin}${path.delimiter}${oldPath ?? ''}` + await fs.mkdir(globalBin, { recursive: true }) + + await writePkg('global-package-with-bin', { name: 'global-package-with-bin', version: '1.0.0', bin: 'bin.js' }) + await fs.writeFile('global-package-with-bin/bin.js', '#!/usr/bin/env node\nconsole.log(/hi/)\n', 'utf8') + + process.chdir('global-package-with-bin') + + // link to global + await link.handler({ + ...DEFAULT_OPTS, + cliOptions: { + global: true, + }, + bin: globalBin, + dir: globalDir, + }) + + process.chdir('../project') + + // link from global + await link.handler({ + ...DEFAULT_OPTS, + cliOptions: { + global: true, + }, + bin: globalBin, + dir: globalDir, + }, ['global-package-with-bin']) + + const { manifest } = await readProjectManifest(process.cwd(), {}) + + process.env[PATH] = oldPath + + expect(manifest.devDependencies).toStrictEqual({ + 'global-package-with-bin': '^1.0.0', + }) +}) + test('relative link', async () => { const project = prepare({ dependencies: { diff --git a/packages/types/src/misc.ts b/packages/types/src/misc.ts index 7990cd76917..578b091b0b3 100644 --- a/packages/types/src/misc.ts +++ b/packages/types/src/misc.ts @@ -1,5 +1,7 @@ export type DependenciesField = 'optionalDependencies' | 'dependencies' | 'devDependencies' +export type DependenciesManifestField = DependenciesField | 'peerDependencies' + // NOTE: The order in this array is important. export const DEPENDENCIES_FIELDS: DependenciesField[] = [ 'optionalDependencies',