mirror of
https://github.com/pnpm/pnpm.git
synced 2026-03-11 11:48:08 -04:00
296 lines
10 KiB
TypeScript
296 lines
10 KiB
TypeScript
import pathExists from 'path-exists'
|
|
import path from 'path'
|
|
import {
|
|
type Lockfile,
|
|
type PackageSnapshot,
|
|
type ProjectSnapshot,
|
|
} from '@pnpm/lockfile-file'
|
|
import {
|
|
nameVerFromPkgSnapshot,
|
|
packageIdFromSnapshot,
|
|
pkgSnapshotToResolution,
|
|
} from '@pnpm/lockfile-utils'
|
|
import { type IncludedDependencies } from '@pnpm/modules-yaml'
|
|
import { packageIsInstallable } from '@pnpm/package-is-installable'
|
|
import { safeReadPackageJsonFromDir } from '@pnpm/read-package-json'
|
|
import { type SupportedArchitectures, type PatchFile, type Registries } from '@pnpm/types'
|
|
import {
|
|
type FetchPackageToStoreFunction,
|
|
type StoreController,
|
|
} from '@pnpm/store-controller-types'
|
|
import { hoist, type HoistingLimits, type HoisterResult } from '@pnpm/real-hoist'
|
|
import * as dp from '@pnpm/dependency-path'
|
|
import {
|
|
type DependenciesGraph,
|
|
type DepHierarchy,
|
|
type DirectDependenciesByImporterId,
|
|
type LockfileToDepGraphResult,
|
|
} from '@pnpm/deps.graph-builder'
|
|
|
|
export interface LockfileToHoistedDepGraphOptions {
|
|
autoInstallPeers: boolean
|
|
engineStrict: boolean
|
|
force: boolean
|
|
hoistingLimits?: HoistingLimits
|
|
externalDependencies?: Set<string>
|
|
importerIds: string[]
|
|
include: IncludedDependencies
|
|
ignoreScripts: boolean
|
|
currentHoistedLocations?: Record<string, string[]>
|
|
lockfileDir: string
|
|
nodeVersion: string
|
|
pnpmVersion: string
|
|
registries: Registries
|
|
patchedDependencies?: Record<string, PatchFile>
|
|
sideEffectsCacheRead: boolean
|
|
skipped: Set<string>
|
|
storeController: StoreController
|
|
storeDir: string
|
|
virtualStoreDir: string
|
|
supportedArchitectures?: SupportedArchitectures
|
|
}
|
|
|
|
export async function lockfileToHoistedDepGraph (
|
|
lockfile: Lockfile,
|
|
currentLockfile: Lockfile | null,
|
|
opts: LockfileToHoistedDepGraphOptions
|
|
): Promise<LockfileToDepGraphResult> {
|
|
let prevGraph!: DependenciesGraph
|
|
if (currentLockfile?.packages != null) {
|
|
prevGraph = (await _lockfileToHoistedDepGraph(currentLockfile, {
|
|
...opts,
|
|
force: true,
|
|
skipped: new Set(),
|
|
})).graph
|
|
} else {
|
|
prevGraph = {}
|
|
}
|
|
return {
|
|
...(await _lockfileToHoistedDepGraph(lockfile, opts)),
|
|
prevGraph,
|
|
}
|
|
}
|
|
|
|
async function _lockfileToHoistedDepGraph (
|
|
lockfile: Lockfile,
|
|
opts: LockfileToHoistedDepGraphOptions
|
|
): Promise<Omit<LockfileToDepGraphResult, 'prevGraph'>> {
|
|
const tree = hoist(lockfile, {
|
|
hoistingLimits: opts.hoistingLimits,
|
|
externalDependencies: opts.externalDependencies,
|
|
autoInstallPeers: opts.autoInstallPeers,
|
|
})
|
|
const graph: DependenciesGraph = {}
|
|
const modulesDir = path.join(opts.lockfileDir, 'node_modules')
|
|
const fetchDepsOpts = {
|
|
...opts,
|
|
lockfile,
|
|
graph,
|
|
pkgLocationsByDepPath: {},
|
|
hoistedLocations: {} as Record<string, string[]>,
|
|
}
|
|
const hierarchy = {
|
|
[opts.lockfileDir]: await fetchDeps(fetchDepsOpts, modulesDir, tree.dependencies),
|
|
}
|
|
const directDependenciesByImporterId: DirectDependenciesByImporterId = {
|
|
'.': directDepsMap(Object.keys(hierarchy[opts.lockfileDir]), graph),
|
|
}
|
|
const symlinkedDirectDependenciesByImporterId: DirectDependenciesByImporterId = { '.': {} }
|
|
await Promise.all(
|
|
Array.from(tree.dependencies).map(async (rootDep) => {
|
|
const reference = Array.from(rootDep.references)[0]
|
|
if (reference.startsWith('workspace:')) {
|
|
const importerId = reference.replace('workspace:', '')
|
|
const projectDir = path.join(opts.lockfileDir, importerId)
|
|
const modulesDir = path.join(projectDir, 'node_modules')
|
|
const nextHierarchy = (await fetchDeps(fetchDepsOpts, modulesDir, rootDep.dependencies))
|
|
hierarchy[projectDir] = nextHierarchy
|
|
|
|
const importer = lockfile.importers[importerId]
|
|
const importerDir = path.join(opts.lockfileDir, importerId)
|
|
symlinkedDirectDependenciesByImporterId[importerId] = pickLinkedDirectDeps(importer, importerDir, opts.include)
|
|
directDependenciesByImporterId[importerId] = directDepsMap(Object.keys(nextHierarchy), graph)
|
|
}
|
|
})
|
|
)
|
|
return {
|
|
directDependenciesByImporterId,
|
|
graph,
|
|
hierarchy,
|
|
pkgLocationsByDepPath: fetchDepsOpts.pkgLocationsByDepPath,
|
|
symlinkedDirectDependenciesByImporterId,
|
|
hoistedLocations: fetchDepsOpts.hoistedLocations,
|
|
}
|
|
}
|
|
|
|
function directDepsMap (directDepDirs: string[], graph: DependenciesGraph): Record<string, string> {
|
|
return directDepDirs.reduce((acc, dir) => {
|
|
acc[graph[dir].alias!] = dir
|
|
return acc
|
|
}, {} as Record<string, string>)
|
|
}
|
|
|
|
function pickLinkedDirectDeps (
|
|
importer: ProjectSnapshot,
|
|
importerDir: string,
|
|
include: IncludedDependencies
|
|
): Record<string, string> {
|
|
const rootDeps = {
|
|
...(include.devDependencies ? importer.devDependencies : {}),
|
|
...(include.dependencies ? importer.dependencies : {}),
|
|
...(include.optionalDependencies ? importer.optionalDependencies : {}),
|
|
}
|
|
return Object.entries(rootDeps)
|
|
.reduce((directDeps, [alias, ref]) => {
|
|
if (ref.startsWith('link:')) {
|
|
directDeps[alias] = path.resolve(importerDir, ref.slice(5))
|
|
}
|
|
return directDeps
|
|
}, {} as Record<string, string>)
|
|
}
|
|
|
|
async function fetchDeps (
|
|
opts: {
|
|
graph: DependenciesGraph
|
|
lockfile: Lockfile
|
|
pkgLocationsByDepPath: Record<string, string[]>
|
|
hoistedLocations: Record<string, string[]>
|
|
} & LockfileToHoistedDepGraphOptions,
|
|
modules: string,
|
|
deps: Set<HoisterResult>
|
|
): Promise<DepHierarchy> {
|
|
const depHierarchy: Record<string, DepHierarchy> = {}
|
|
await Promise.all(Array.from(deps).map(async (dep) => {
|
|
const depPath = Array.from(dep.references)[0]
|
|
if (opts.skipped.has(depPath) || depPath.startsWith('workspace:')) return
|
|
const pkgSnapshot = opts.lockfile.packages![depPath]
|
|
if (!pkgSnapshot) {
|
|
// it is a link
|
|
return
|
|
}
|
|
const { name: pkgName, version: pkgVersion } = nameVerFromPkgSnapshot(depPath, pkgSnapshot)
|
|
const packageId = packageIdFromSnapshot(depPath, pkgSnapshot)
|
|
|
|
const pkg = {
|
|
name: pkgName,
|
|
version: pkgVersion,
|
|
engines: pkgSnapshot.engines,
|
|
cpu: pkgSnapshot.cpu,
|
|
os: pkgSnapshot.os,
|
|
libc: pkgSnapshot.libc,
|
|
}
|
|
if (!opts.force &&
|
|
packageIsInstallable(packageId, pkg, {
|
|
engineStrict: opts.engineStrict,
|
|
lockfileDir: opts.lockfileDir,
|
|
nodeVersion: opts.nodeVersion,
|
|
optional: pkgSnapshot.optional === true,
|
|
supportedArchitectures: opts.supportedArchitectures,
|
|
}) === false
|
|
) {
|
|
opts.skipped.add(depPath)
|
|
return
|
|
}
|
|
const dir = path.join(modules, dep.name)
|
|
const depLocation = path.relative(opts.lockfileDir, dir)
|
|
const resolution = pkgSnapshotToResolution(depPath, pkgSnapshot, opts.registries)
|
|
let fetchResponse!: ReturnType<FetchPackageToStoreFunction>
|
|
// We check for the existence of the package inside node_modules.
|
|
// It will only be missing if the user manually removed it.
|
|
// That shouldn't normally happen but Bit CLI does remove node_modules in component directories:
|
|
// https://github.com/teambit/bit/blob/5e1eed7cd122813ad5ea124df956ee89d661d770/scopes/dependencies/dependency-resolver/dependency-installer.ts#L169
|
|
//
|
|
// We also verify that the package that is present has the expected version.
|
|
// This check is required because there is no guarantee the modules manifest and current lockfile were
|
|
// successfully saved after node_modules was changed during installation.
|
|
const skipFetch = opts.currentHoistedLocations?.[depPath]?.includes(depLocation) &&
|
|
await dirHasPackageJsonWithVersion(path.join(opts.lockfileDir, depLocation), pkgVersion)
|
|
const pkgResolution = {
|
|
id: packageId,
|
|
resolution,
|
|
}
|
|
if (skipFetch) {
|
|
const { filesIndexFile } = opts.storeController.getFilesIndexFilePath({
|
|
ignoreScripts: opts.ignoreScripts,
|
|
pkg: pkgResolution,
|
|
})
|
|
fetchResponse = { filesIndexFile } as any // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
} else {
|
|
try {
|
|
fetchResponse = opts.storeController.fetchPackage({
|
|
force: false,
|
|
lockfileDir: opts.lockfileDir,
|
|
ignoreScripts: opts.ignoreScripts,
|
|
pkg: pkgResolution,
|
|
expectedPkg: {
|
|
name: pkgName,
|
|
version: pkgVersion,
|
|
},
|
|
}) as any // eslint-disable-line
|
|
if (fetchResponse instanceof Promise) fetchResponse = await fetchResponse
|
|
} catch (err: any) { // eslint-disable-line
|
|
if (pkgSnapshot.optional) return
|
|
throw err
|
|
}
|
|
}
|
|
opts.graph[dir] = {
|
|
alias: dep.name,
|
|
children: {},
|
|
depPath,
|
|
dir,
|
|
fetching: fetchResponse.fetching,
|
|
filesIndexFile: fetchResponse.filesIndexFile,
|
|
hasBin: pkgSnapshot.hasBin === true,
|
|
hasBundledDependencies: pkgSnapshot.bundledDependencies != null,
|
|
modules,
|
|
name: pkgName,
|
|
optional: !!pkgSnapshot.optional,
|
|
optionalDependencies: new Set(Object.keys(pkgSnapshot.optionalDependencies ?? {})),
|
|
patchFile: opts.patchedDependencies?.[`${pkgName}@${pkgVersion}`],
|
|
}
|
|
if (!opts.pkgLocationsByDepPath[depPath]) {
|
|
opts.pkgLocationsByDepPath[depPath] = []
|
|
}
|
|
opts.pkgLocationsByDepPath[depPath].push(dir)
|
|
depHierarchy[dir] = await fetchDeps(opts, path.join(dir, 'node_modules'), dep.dependencies)
|
|
if (!opts.hoistedLocations[depPath]) {
|
|
opts.hoistedLocations[depPath] = []
|
|
}
|
|
opts.hoistedLocations[depPath].push(depLocation)
|
|
opts.graph[dir].children = getChildren(pkgSnapshot, opts.pkgLocationsByDepPath, opts)
|
|
}))
|
|
return depHierarchy
|
|
}
|
|
|
|
async function dirHasPackageJsonWithVersion (dir: string, expectedVersion?: string): Promise<boolean> {
|
|
if (!expectedVersion) return pathExists(dir)
|
|
try {
|
|
const manifest = await safeReadPackageJsonFromDir(dir)
|
|
return manifest?.version === expectedVersion
|
|
} catch (err: any) { // eslint-disable-line
|
|
if (err.code === 'ENOENT') {
|
|
return pathExists(dir)
|
|
}
|
|
throw err
|
|
}
|
|
}
|
|
|
|
function getChildren (
|
|
pkgSnapshot: PackageSnapshot,
|
|
pkgLocationsByDepPath: Record<string, string[]>,
|
|
opts: { include: IncludedDependencies }
|
|
): Record<string, string> {
|
|
const allDeps = {
|
|
...pkgSnapshot.dependencies,
|
|
...(opts.include.optionalDependencies ? pkgSnapshot.optionalDependencies : {}),
|
|
}
|
|
const children: Record<string, string> = {}
|
|
for (const [childName, childRef] of Object.entries(allDeps)) {
|
|
const childDepPath = dp.refToRelative(childRef, childName)
|
|
if (childDepPath && pkgLocationsByDepPath[childDepPath]) {
|
|
children[childName] = pkgLocationsByDepPath[childDepPath][0]
|
|
}
|
|
}
|
|
return children
|
|
}
|