Files
pnpm/pkg-manager/plugin-commands-installation/src/link.ts
2024-10-28 04:01:43 +01:00

188 lines
5.9 KiB
TypeScript

import path from 'path'
import {
docsUrl,
tryReadProjectManifest,
type ReadProjectManifestOpts,
} from '@pnpm/cli-utils'
import { UNIVERSAL_OPTIONS } from '@pnpm/common-cli-options-help'
import { type Config, types as allTypes } from '@pnpm/config'
import { DEPENDENCIES_FIELDS, type ProjectManifest, type Project } from '@pnpm/types'
import { PnpmError } from '@pnpm/error'
import { arrayOfWorkspacePackagesToMap } from '@pnpm/get-context'
import { findWorkspacePackages } from '@pnpm/workspace.find-packages'
import { writeProjectManifest } from '@pnpm/write-project-manifest'
import {
type WorkspacePackages,
} from '@pnpm/core'
import { logger } from '@pnpm/logger'
import pick from 'ramda/src/pick'
import partition from 'ramda/src/partition'
import renderHelp from 'render-help'
import { getSaveType } from './getSaveType'
import * as install from './install'
// @ts-expect-error
const isWindows = process.platform === 'win32' || global['FAKE_WINDOWS']
const isFilespec = isWindows ? /^(?:[.]|~[/]|[/\\]|[a-zA-Z]:)/ : /^(?:[.]|~[/]|[/]|[a-zA-Z]:)/
type LinkOpts = Pick<Config,
| 'bin'
| 'cliOptions'
| 'engineStrict'
| 'rootProjectManifest'
| 'rootProjectManifestDir'
| 'saveDev'
| 'saveOptional'
| 'saveProd'
| 'workspaceDir'
| 'workspacePackagePatterns'
| 'sharedWorkspaceLockfile'
| 'globalPkgDir'
> & Partial<Pick<Config, 'linkWorkspacePackages'>> & install.InstallCommandOptions
export const rcOptionsTypes = cliOptionsTypes
export function cliOptionsTypes (): Record<string, unknown> {
return pick([
'global-dir',
'global',
'only',
'package-import-method',
'production',
'registry',
'reporter',
'save-dev',
'save-exact',
'save-optional',
'save-prefix',
'unsafe-perm',
], allTypes)
}
export const commandNames = ['link', 'ln']
export function help (): string {
return renderHelp({
aliases: ['ln'],
descriptionLists: [
{
title: 'Options',
list: UNIVERSAL_OPTIONS,
},
],
url: docsUrl('link'),
usages: [
'pnpm link <dir|pkg name>',
'pnpm link',
],
})
}
async function checkPeerDeps (linkCwdDir: string, opts: LinkOpts) {
const { manifest } = await tryReadProjectManifest(linkCwdDir, opts)
if (manifest?.peerDependencies && Object.keys(manifest.peerDependencies).length > 0) {
const packageName = manifest.name ?? path.basename(linkCwdDir) // Assuming the name property exists in newManifest
const peerDeps = Object.entries(manifest.peerDependencies)
.map(([key, value]) => ` - ${key}@${String(value)}`)
.join(', ')
logger.warn({
message: `The package ${packageName}, which you have just pnpm linked, has the following peerDependencies specified in its package.json:
${peerDeps}
The linked in dependency will not resolve the peer dependencies from the target node_modules.
This might cause issues in your project. To resolve this, you may use the "file:" protocol to reference the local dependency.`,
prefix: opts.dir,
})
}
}
export async function handler (
opts: LinkOpts,
params?: string[]
): Promise<void> {
let workspacePackagesArr: Project[]
let workspacePackages!: WorkspacePackages
if (opts.workspaceDir) {
workspacePackagesArr = await findWorkspacePackages(opts.workspaceDir, {
...opts,
patterns: opts.workspacePackagePatterns,
})
workspacePackages = arrayOfWorkspacePackagesToMap(workspacePackagesArr) as WorkspacePackages
} else {
workspacePackages = new Map()
}
const linkOpts = Object.assign(opts, {
targetDependenciesField: getSaveType(opts),
workspacePackages,
binsDir: opts.bin,
})
if (opts.cliOptions?.global && !opts.bin) {
throw new PnpmError('NO_GLOBAL_BIN_DIR', 'Unable to find the global bin directory', {
hint: 'Run "pnpm setup" to create it automatically, or set the global-bin-dir setting, or the PNPM_HOME env variable. The global bin directory should be in the PATH.',
})
}
// pnpm link
if ((params == null) || (params.length === 0)) {
const cwd = process.cwd()
if (path.relative(linkOpts.dir, cwd) === '') {
throw new PnpmError('LINK_BAD_PARAMS', 'You must provide a parameter')
}
await checkPeerDeps(cwd, opts)
const newManifest = opts.rootProjectManifest ?? {}
await addLinkToManifest(opts, newManifest, cwd, linkOpts.dir)
await writeProjectManifest(path.join(opts.rootProjectManifestDir, 'package.json'), newManifest)
await install.handler({
...linkOpts,
frozenLockfileIfExists: false,
rootProjectManifest: newManifest,
})
return
}
const [pkgPaths, pkgNames] = partition((inp) => isFilespec.test(inp), params)
pkgNames.forEach((pkgName) => pkgPaths.push(path.join(opts.globalPkgDir, 'node_modules', pkgName)))
const newManifest = opts.rootProjectManifest ?? {}
await Promise.all(
pkgPaths.map(async (dir) => {
await addLinkToManifest(opts, newManifest, dir, opts.dir)
await checkPeerDeps(dir, opts)
})
)
await writeProjectManifest(path.join(opts.rootProjectManifestDir, 'package.json'), newManifest)
await install.handler({
...linkOpts,
frozenLockfileIfExists: false,
rootProjectManifest: newManifest,
})
}
async function addLinkToManifest (opts: ReadProjectManifestOpts, manifest: ProjectManifest, linkedDepDir: string, dependentDir: string) {
if (!manifest.pnpm) {
manifest.pnpm = {
overrides: {},
}
} else if (!manifest.pnpm.overrides) {
manifest.pnpm.overrides = {}
}
const { manifest: linkedManifest } = await tryReadProjectManifest(linkedDepDir, opts)
const linkedPkgName = linkedManifest?.name ?? path.basename(linkedDepDir)
const linkedPkgSpec = `link:${path.relative(dependentDir, linkedDepDir)}`
manifest.pnpm.overrides![linkedPkgName] = linkedPkgSpec
if (DEPENDENCIES_FIELDS.every((depField) => manifest[depField]?.[linkedPkgName] == null)) {
manifest.dependencies = manifest.dependencies ?? {}
manifest.dependencies[linkedPkgName] = linkedPkgSpec
}
}