mirror of
https://github.com/pnpm/pnpm.git
synced 2026-03-31 05:22:00 -04:00
340 lines
11 KiB
TypeScript
340 lines
11 KiB
TypeScript
import path from 'path'
|
|
import {
|
|
readProjectManifestOnly,
|
|
tryReadProjectManifest,
|
|
} from '@pnpm/cli-utils'
|
|
import { type Config } from '@pnpm/config'
|
|
import { PnpmError } from '@pnpm/error'
|
|
import { filterPkgsBySelectorObjects } from '@pnpm/filter-workspace-packages'
|
|
import { arrayOfWorkspacePackagesToMap, findWorkspacePackages } from '@pnpm/workspace.find-packages'
|
|
import { type Lockfile } from '@pnpm/lockfile-types'
|
|
import { rebuildProjects } from '@pnpm/plugin-commands-rebuild'
|
|
import { createOrConnectStoreController, type CreateStoreControllerOptions } from '@pnpm/store-connection-manager'
|
|
import { type IncludedDependencies, type Project, type ProjectsGraph } from '@pnpm/types'
|
|
import {
|
|
install,
|
|
mutateModulesInSingleProject,
|
|
type MutateModulesOptions,
|
|
type WorkspacePackages,
|
|
} from '@pnpm/core'
|
|
import { logger } from '@pnpm/logger'
|
|
import { sequenceGraph } from '@pnpm/sort-packages'
|
|
import { createPkgGraph } from '@pnpm/workspace.pkgs-graph'
|
|
import isSubdir from 'is-subdir'
|
|
import { getOptionsFromRootManifest } from './getOptionsFromRootManifest'
|
|
import { getPinnedVersion } from './getPinnedVersion'
|
|
import { getSaveType } from './getSaveType'
|
|
import { getNodeExecPath } from './nodeExecPath'
|
|
import { recursive, createMatcher, matchDependencies, makeIgnorePatterns, type UpdateDepsMatcher } from './recursive'
|
|
import { updateToLatestSpecsFromManifest, createLatestSpecs } from './updateToLatestSpecsFromManifest'
|
|
import { createWorkspaceSpecs, updateToWorkspacePackagesFromManifest } from './updateWorkspaceDependencies'
|
|
|
|
const OVERWRITE_UPDATE_OPTIONS = {
|
|
allowNew: true,
|
|
update: false,
|
|
}
|
|
|
|
export type InstallDepsOptions = Pick<Config,
|
|
| 'allProjects'
|
|
| 'allProjectsGraph'
|
|
| 'autoInstallPeers'
|
|
| 'bail'
|
|
| 'bin'
|
|
| 'cliOptions'
|
|
| 'dedupePeerDependents'
|
|
| 'depth'
|
|
| 'dev'
|
|
| 'engineStrict'
|
|
| 'global'
|
|
| 'globalPnpmfile'
|
|
| 'hooks'
|
|
| 'ignoreCurrentPrefs'
|
|
| 'ignorePnpmfile'
|
|
| 'ignoreScripts'
|
|
| 'linkWorkspacePackages'
|
|
| 'lockfileDir'
|
|
| 'lockfileOnly'
|
|
| 'pnpmfile'
|
|
| 'production'
|
|
| 'rawLocalConfig'
|
|
| 'registries'
|
|
| 'save'
|
|
| 'saveDev'
|
|
| 'saveExact'
|
|
| 'saveOptional'
|
|
| 'savePeer'
|
|
| 'savePrefix'
|
|
| 'saveProd'
|
|
| 'saveWorkspaceProtocol'
|
|
| 'lockfileIncludeTarballUrl'
|
|
| 'scriptsPrependNodePath'
|
|
| 'scriptShell'
|
|
| 'selectedProjectsGraph'
|
|
| 'sideEffectsCache'
|
|
| 'sideEffectsCacheReadonly'
|
|
| 'sort'
|
|
| 'sharedWorkspaceLockfile'
|
|
| 'shellEmulator'
|
|
| 'tag'
|
|
| 'optional'
|
|
| 'workspaceConcurrency'
|
|
| 'workspaceDir'
|
|
| 'extraEnv'
|
|
| 'ignoreWorkspaceCycles'
|
|
> & CreateStoreControllerOptions & {
|
|
argv: {
|
|
original: string[]
|
|
}
|
|
allowNew?: boolean
|
|
forceFullResolution?: boolean
|
|
frozenLockfileIfExists?: boolean
|
|
include?: IncludedDependencies
|
|
includeDirect?: IncludedDependencies
|
|
latest?: boolean
|
|
/**
|
|
* If specified, the installation will only be performed for comparison of the
|
|
* wanted lockfile. The wanted lockfile will not be updated on disk and no
|
|
* modules will be linked.
|
|
*
|
|
* The given callback is passed the wanted lockfile before installation and
|
|
* after. This allows functions to reasonably determine whether the wanted
|
|
* lockfile will change on disk after installation. The lockfile arguments
|
|
* passed to this callback should not be mutated.
|
|
*/
|
|
lockfileCheck?: (prev: Lockfile, next: Lockfile) => void
|
|
update?: boolean
|
|
updateMatching?: (pkgName: string) => boolean
|
|
updatePackageManifest?: boolean
|
|
useBetaCli?: boolean
|
|
recursive?: boolean
|
|
dedupe?: boolean
|
|
workspace?: boolean
|
|
includeOnlyPackageFiles?: boolean
|
|
} & Partial<Pick<Config, 'pnpmHomeDir'>>
|
|
|
|
export async function installDeps (
|
|
opts: InstallDepsOptions,
|
|
params: string[]
|
|
) {
|
|
if (opts.workspace) {
|
|
if (opts.latest) {
|
|
throw new PnpmError('BAD_OPTIONS', 'Cannot use --latest with --workspace simultaneously')
|
|
}
|
|
if (!opts.workspaceDir) {
|
|
throw new PnpmError('WORKSPACE_OPTION_OUTSIDE_WORKSPACE', '--workspace can only be used inside a workspace')
|
|
}
|
|
if (!opts.linkWorkspacePackages && !opts.saveWorkspaceProtocol) {
|
|
if (opts.rawLocalConfig['save-workspace-protocol'] === false) {
|
|
throw new PnpmError('BAD_OPTIONS', 'This workspace has link-workspace-packages turned off, \
|
|
so dependencies are linked from the workspace only when the workspace protocol is used. \
|
|
Either set link-workspace-packages to true or don\'t use the --no-save-workspace-protocol option \
|
|
when running add/update with the --workspace option')
|
|
} else {
|
|
opts.saveWorkspaceProtocol = true
|
|
}
|
|
}
|
|
// @ts-expect-error
|
|
opts['preserveWorkspaceProtocol'] = !opts.linkWorkspacePackages
|
|
}
|
|
const includeDirect = opts.includeDirect ?? {
|
|
dependencies: true,
|
|
devDependencies: true,
|
|
optionalDependencies: true,
|
|
}
|
|
const forceHoistPattern = typeof opts.rawLocalConfig['hoist-pattern'] !== 'undefined' ||
|
|
typeof opts.rawLocalConfig['hoist'] !== 'undefined'
|
|
const forcePublicHoistPattern = typeof opts.rawLocalConfig['shamefully-hoist'] !== 'undefined' ||
|
|
typeof opts.rawLocalConfig['public-hoist-pattern'] !== 'undefined'
|
|
const allProjects = opts.allProjects ?? (
|
|
opts.workspaceDir ? await findWorkspacePackages(opts.workspaceDir, opts) : []
|
|
)
|
|
if (opts.workspaceDir) {
|
|
const selectedProjectsGraph = opts.selectedProjectsGraph ?? selectProjectByDir(allProjects, opts.dir)
|
|
if (selectedProjectsGraph != null) {
|
|
const sequencedGraph = sequenceGraph(selectedProjectsGraph)
|
|
// Check and warn if there are cyclic dependencies
|
|
if (!opts.ignoreWorkspaceCycles && !sequencedGraph.safe) {
|
|
const cyclicDependenciesInfo = sequencedGraph.cycles.length > 0
|
|
? `: ${sequencedGraph.cycles.map(deps => deps.join(', ')).join('; ')}`
|
|
: ''
|
|
logger.warn({
|
|
message: `There are cyclic workspace dependencies${cyclicDependenciesInfo}`,
|
|
prefix: opts.workspaceDir,
|
|
})
|
|
}
|
|
|
|
let allProjectsGraph!: ProjectsGraph
|
|
if (opts.dedupePeerDependents) {
|
|
allProjectsGraph = opts.allProjectsGraph ?? createPkgGraph(allProjects, {
|
|
linkWorkspacePackages: Boolean(opts.linkWorkspacePackages),
|
|
}).graph
|
|
} else {
|
|
allProjectsGraph = selectedProjectsGraph
|
|
if (!allProjectsGraph[opts.workspaceDir]) {
|
|
allProjectsGraph = {
|
|
...allProjectsGraph,
|
|
...selectProjectByDir(allProjects, opts.workspaceDir),
|
|
}
|
|
}
|
|
}
|
|
await recursive(allProjects,
|
|
params,
|
|
{
|
|
...opts,
|
|
forceHoistPattern,
|
|
forcePublicHoistPattern,
|
|
allProjectsGraph,
|
|
selectedProjectsGraph,
|
|
workspaceDir: opts.workspaceDir,
|
|
},
|
|
opts.update ? 'update' : (params.length === 0 ? 'install' : 'add')
|
|
)
|
|
return
|
|
}
|
|
}
|
|
// `pnpm install ""` is going to be just `pnpm install`
|
|
params = params.filter(Boolean)
|
|
|
|
const dir = opts.dir || process.cwd()
|
|
let workspacePackages!: WorkspacePackages
|
|
|
|
if (opts.workspaceDir) {
|
|
workspacePackages = arrayOfWorkspacePackagesToMap(allProjects) as WorkspacePackages
|
|
}
|
|
|
|
let { manifest, writeProjectManifest } = await tryReadProjectManifest(opts.dir, opts)
|
|
if (manifest === null) {
|
|
if (opts.update === true || params.length === 0) {
|
|
throw new PnpmError('NO_PKG_MANIFEST', `No package.json found in ${opts.dir}`)
|
|
}
|
|
manifest = {}
|
|
}
|
|
|
|
const store = await createOrConnectStoreController(opts)
|
|
const installOpts: Omit<MutateModulesOptions, 'allProjects'> = {
|
|
...opts,
|
|
...getOptionsFromRootManifest(manifest),
|
|
forceHoistPattern,
|
|
forcePublicHoistPattern,
|
|
// In case installation is done in a multi-package repository
|
|
// The dependencies should be built first,
|
|
// so ignoring scripts for now
|
|
ignoreScripts: !!workspacePackages || opts.ignoreScripts,
|
|
linkWorkspacePackagesDepth: opts.linkWorkspacePackages === 'deep' ? Infinity : opts.linkWorkspacePackages ? 0 : -1,
|
|
sideEffectsCacheRead: opts.sideEffectsCache ?? opts.sideEffectsCacheReadonly,
|
|
sideEffectsCacheWrite: opts.sideEffectsCache,
|
|
storeController: store.ctrl,
|
|
storeDir: store.dir,
|
|
workspacePackages,
|
|
}
|
|
if (opts.global && opts.pnpmHomeDir != null) {
|
|
const nodeExecPath = await getNodeExecPath()
|
|
if (isSubdir(opts.pnpmHomeDir, nodeExecPath)) {
|
|
installOpts['nodeExecPath'] = nodeExecPath
|
|
}
|
|
}
|
|
|
|
let updateMatch: UpdateDepsMatcher | null
|
|
if (opts.update) {
|
|
if (params.length === 0) {
|
|
const ignoreDeps = manifest.pnpm?.updateConfig?.ignoreDependencies
|
|
if (ignoreDeps?.length) {
|
|
params = makeIgnorePatterns(ignoreDeps)
|
|
}
|
|
}
|
|
updateMatch = params.length ? createMatcher(params) : null
|
|
} else {
|
|
updateMatch = null
|
|
}
|
|
if (updateMatch != null) {
|
|
params = matchDependencies(updateMatch, manifest, includeDirect)
|
|
if (params.length === 0) {
|
|
if (opts.latest) return
|
|
if (opts.depth === 0) {
|
|
throw new PnpmError('NO_PACKAGE_IN_DEPENDENCIES',
|
|
'None of the specified packages were found in the dependencies.')
|
|
}
|
|
}
|
|
}
|
|
|
|
if (opts.update && opts.latest) {
|
|
if (!params || (params.length === 0)) {
|
|
params = updateToLatestSpecsFromManifest(manifest, includeDirect)
|
|
} else {
|
|
params = createLatestSpecs(params, manifest)
|
|
}
|
|
}
|
|
if (opts.workspace) {
|
|
if (!params || (params.length === 0)) {
|
|
params = updateToWorkspacePackagesFromManifest(manifest, includeDirect, workspacePackages)
|
|
} else {
|
|
params = createWorkspaceSpecs(params, workspacePackages)
|
|
}
|
|
}
|
|
if (params?.length) {
|
|
const mutatedProject = {
|
|
allowNew: opts.allowNew,
|
|
binsDir: opts.bin,
|
|
dependencySelectors: params,
|
|
manifest,
|
|
mutation: 'installSome' as const,
|
|
peer: opts.savePeer,
|
|
pinnedVersion: getPinnedVersion(opts),
|
|
rootDir: opts.dir,
|
|
targetDependenciesField: getSaveType(opts),
|
|
}
|
|
const updatedImporter = await mutateModulesInSingleProject(mutatedProject, installOpts)
|
|
if (opts.save !== false) {
|
|
await writeProjectManifest(updatedImporter.manifest)
|
|
}
|
|
return
|
|
}
|
|
|
|
const updatedManifest = await install(manifest, installOpts)
|
|
if (opts.update === true && opts.save !== false) {
|
|
await writeProjectManifest(updatedManifest)
|
|
}
|
|
|
|
if (opts.linkWorkspacePackages && opts.workspaceDir) {
|
|
const { selectedProjectsGraph } = await filterPkgsBySelectorObjects(allProjects, [
|
|
{
|
|
excludeSelf: true,
|
|
includeDependencies: true,
|
|
parentDir: dir,
|
|
},
|
|
], {
|
|
workspaceDir: opts.workspaceDir,
|
|
})
|
|
await recursive(allProjects, [], {
|
|
...opts,
|
|
...OVERWRITE_UPDATE_OPTIONS,
|
|
allProjectsGraph: opts.allProjectsGraph!,
|
|
selectedProjectsGraph,
|
|
workspaceDir: opts.workspaceDir, // Otherwise TypeScript doesn't understand that is not undefined
|
|
}, 'install')
|
|
|
|
if (opts.ignoreScripts) return
|
|
|
|
await rebuildProjects(
|
|
[
|
|
{
|
|
buildIndex: 0,
|
|
manifest: await readProjectManifestOnly(opts.dir, opts),
|
|
rootDir: opts.dir,
|
|
},
|
|
], {
|
|
...opts,
|
|
pending: true,
|
|
storeController: store.ctrl,
|
|
storeDir: store.dir,
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
function selectProjectByDir (projects: Project[], searchedDir: string) {
|
|
const project = projects.find(({ dir }) => path.relative(dir, searchedDir) === '')
|
|
if (project == null) return undefined
|
|
return { [searchedDir]: { dependencies: [], package: project } }
|
|
}
|