fix: hoisting with global virtual store (#9648)

This commit is contained in:
Zoltan Kochan
2025-06-22 22:03:29 +02:00
committed by GitHub
parent 1907f2abaf
commit b982a0d6dc
11 changed files with 303 additions and 179 deletions

View File

@@ -0,0 +1,8 @@
---
"@pnpm/hoist": major
"@pnpm/headless": patch
"@pnpm/core": patch
"pnpm": patch
---
Fixed hoisting with `enableGlobalVirtualStore` set to `true` [#9648](https://github.com/pnpm/pnpm/pull/9648).

View File

@@ -0,0 +1,5 @@
---
"@pnpm/deps.graph-builder": minor
---
New option added: includeUnchangedDeps.

View File

@@ -58,6 +58,7 @@ export interface LockfileToDepGraphOptions {
force: boolean
importerIds: ProjectId[]
include: IncludedDependencies
includeUnchangedDeps?: boolean
ignoreScripts: boolean
lockfileDir: string
nodeVersion: string
@@ -190,9 +191,12 @@ async function buildGraphFromPackages (
locationByDepPath[depPath] = dir
let dirExists: boolean | undefined
if (depIsPresent &&
if (
depIsPresent &&
isEmpty(currentPackages[depPath].optionalDependencies ?? {}) &&
isEmpty(pkgSnapshot.optionalDependencies ?? {})) {
isEmpty(pkgSnapshot.optionalDependencies ?? {}) &&
!opts.includeUnchangedDeps
) {
dirExists = await pathExists(dir)
if (dirExists) return
brokenModulesLogger.debug({ missing: dir })

View File

@@ -35,7 +35,6 @@ import pathExists from 'path-exists'
import equals from 'ramda/src/equals'
import isEmpty from 'ramda/src/isEmpty'
import difference from 'ramda/src/difference'
import omit from 'ramda/src/omit'
import pick from 'ramda/src/pick'
import pickBy from 'ramda/src/pickBy'
import props from 'ramda/src/props'
@@ -214,35 +213,33 @@ export async function linkPackages (projects: ImporterToUpdate[], depGraph: Depe
if (opts.hoistPattern == null && opts.publicHoistPattern == null) {
newHoistedDependencies = {}
} else if (newDepPaths.length > 0 || removedDepPaths.size > 0) {
// It is important to keep the skipped packages in the lockfile which will be saved as the "current lockfile".
// pnpm is comparing the current lockfile to the wanted one and they should match.
// But for hoisting, we need a version of the lockfile w/o the skipped packages, so we're making a copy.
const hoistLockfile = {
...currentLockfile,
packages: currentLockfile.packages != null ? omit(Array.from(opts.skipped), currentLockfile.packages) : {},
}
newHoistedDependencies = await hoist({
extraNodePath: opts.extraNodePaths,
lockfile: hoistLockfile,
importerIds: projectIds,
privateHoistedModulesDir: opts.hoistedModulesDir,
privateHoistPattern: opts.hoistPattern ?? [],
publicHoistedModulesDir: opts.rootModulesDir,
publicHoistPattern: opts.publicHoistPattern ?? [],
virtualStoreDir: opts.virtualStoreDir,
virtualStoreDirMaxLength: opts.virtualStoreDirMaxLength,
hoistedWorkspacePackages: opts.hoistWorkspacePackages
? projects.reduce((hoistedWorkspacePackages, project) => {
if (project.manifest.name && project.id !== '.') {
hoistedWorkspacePackages[project.id] = {
dir: project.rootDir,
name: project.manifest.name,
newHoistedDependencies = {
...opts.hoistedDependencies,
...await hoist({
extraNodePath: opts.extraNodePaths,
graph: depGraph,
directDepsByImporterId: opts.dependenciesByProjectId,
importerIds: projectIds,
privateHoistedModulesDir: opts.hoistedModulesDir,
privateHoistPattern: opts.hoistPattern ?? [],
publicHoistedModulesDir: opts.rootModulesDir,
publicHoistPattern: opts.publicHoistPattern ?? [],
virtualStoreDir: opts.virtualStoreDir,
virtualStoreDirMaxLength: opts.virtualStoreDirMaxLength,
hoistedWorkspacePackages: opts.hoistWorkspacePackages
? projects.reduce((hoistedWorkspacePackages, project) => {
if (project.manifest.name && project.id !== '.') {
hoistedWorkspacePackages[project.id] = {
dir: project.rootDir,
name: project.manifest.name,
}
}
}
return hoistedWorkspacePackages
}, {} as Record<string, HoistedWorkspaceProject>)
: undefined,
})
return hoistedWorkspacePackages
}, {} as Record<string, HoistedWorkspaceProject>)
: undefined,
skipped: opts.skipped,
}),
}
} else {
newHoistedDependencies = opts.hoistedDependencies
}

View File

@@ -16,11 +16,11 @@ test('using a global virtual store', async () => {
await install(manifest, testDefaults({
enableGlobalVirtualStore: true,
virtualStoreDir: globalVirtualStoreDir,
privateHoistPattern: '*',
hoistPattern: ['*'],
}))
{
expect(fs.existsSync(path.resolve('node_modules/.pnpm/node_modules/@pnpm.e2e/dep-of-pkg-with-1-dep')))
expect(fs.existsSync(path.resolve('node_modules/.pnpm/node_modules/@pnpm.e2e/dep-of-pkg-with-1-dep/package.json'))).toBeTruthy()
expect(fs.existsSync(path.resolve('node_modules/.pnpm/lock.yaml'))).toBeTruthy()
const files = fs.readdirSync(path.join(globalVirtualStoreDir, '@pnpm.e2e/pkg-with-1-dep/100.0.0'))
expect(files.length).toBe(1)
@@ -34,10 +34,11 @@ test('using a global virtual store', async () => {
enableGlobalVirtualStore: true,
virtualStoreDir: globalVirtualStoreDir,
frozenLockfile: true,
hoistPattern: ['*'],
}))
{
expect(fs.existsSync(path.resolve('node_modules/.pnpm/node_modules/@pnpm.e2e/dep-of-pkg-with-1-dep')))
expect(fs.existsSync(path.resolve('node_modules/.pnpm/node_modules/@pnpm.e2e/dep-of-pkg-with-1-dep/package.json'))).toBeTruthy()
expect(fs.existsSync(path.resolve('node_modules/.pnpm/lock.yaml'))).toBeTruthy()
const files = fs.readdirSync(path.join(globalVirtualStoreDir, '@pnpm.e2e/pkg-with-1-dep/100.0.0'))
expect(files.length).toBe(1)

View File

@@ -512,6 +512,7 @@ test('hoist when updating in one of the workspace projects', async () => {
const modulesManifest = rootModules.readModulesManifest()
expect(modulesManifest?.hoistedDependencies).toStrictEqual({
'@pnpm.e2e/dep-of-pkg-with-1-dep@100.0.0': { '@pnpm.e2e/dep-of-pkg-with-1-dep': 'private' },
'@pnpm.e2e/foo@100.0.0': { '@pnpm.e2e/foo': 'private' },
})
}
})

View File

@@ -139,6 +139,8 @@ export interface HeadlessOptions {
hoistedDependencies: HoistedDependencies
hoistPattern?: string[]
publicHoistPattern?: string[]
currentHoistPattern?: string[]
currentPublicHoistPattern?: string[]
currentHoistedLocations?: Record<string, string[]>
lockfileDir: string
modulesDir?: string
@@ -331,6 +333,8 @@ export async function headlessInstall (opts: HeadlessOptions): Promise<Installat
nodeVersion: opts.currentEngine.nodeVersion,
pnpmVersion: opts.currentEngine.pnpmVersion,
supportedArchitectures: opts.supportedArchitectures,
includeUnchangedDeps: (!equals(opts.currentHoistPattern, opts.hoistPattern ?? undefined)) ||
(!equals(opts.currentPublicHoistPattern, opts.publicHoistPattern ?? undefined)),
} as LockfileToDepGraphOptions
const {
directDependenciesByImporterId,
@@ -434,36 +438,37 @@ export async function headlessInstall (opts: HeadlessOptions): Promise<Installat
})
if (opts.ignorePackageManifest !== true && (opts.hoistPattern != null || opts.publicHoistPattern != null)) {
// It is important to keep the skipped packages in the lockfile which will be saved as the "current lockfile".
// pnpm is comparing the current lockfile to the wanted one and they should match.
// But for hoisting, we need a version of the lockfile w/o the skipped packages, so we're making a copy.
const hoistLockfile = {
...filteredLockfile,
packages: filteredLockfile.packages != null ? omit(Array.from(skipped), filteredLockfile.packages) : {},
}
newHoistedDependencies = await hoist({
extraNodePath: opts.extraNodePaths,
lockfile: hoistLockfile,
importerIds,
preferSymlinkedExecutables: opts.preferSymlinkedExecutables,
privateHoistedModulesDir: hoistedModulesDir,
privateHoistPattern: opts.hoistPattern ?? [],
publicHoistedModulesDir,
publicHoistPattern: opts.publicHoistPattern ?? [],
virtualStoreDir,
virtualStoreDirMaxLength: opts.virtualStoreDirMaxLength,
hoistedWorkspacePackages: opts.hoistWorkspacePackages
? Object.values(opts.allProjects).reduce((hoistedWorkspacePackages, project) => {
if (project.manifest.name && project.id !== '.') {
hoistedWorkspacePackages[project.id] = {
dir: project.rootDir,
name: project.manifest.name,
newHoistedDependencies = {
...opts.hoistedDependencies,
...await hoist({
extraNodePath: opts.extraNodePaths,
graph,
directDepsByImporterId: Object.fromEntries(Object.entries(directDependenciesByImporterId).map(([projectId, deps]) => [
projectId,
new Map(Object.entries(deps)),
])),
importerIds,
preferSymlinkedExecutables: opts.preferSymlinkedExecutables,
privateHoistedModulesDir: hoistedModulesDir,
privateHoistPattern: opts.hoistPattern ?? [],
publicHoistedModulesDir,
publicHoistPattern: opts.publicHoistPattern ?? [],
virtualStoreDir,
virtualStoreDirMaxLength: opts.virtualStoreDirMaxLength,
hoistedWorkspacePackages: opts.hoistWorkspacePackages
? Object.values(opts.allProjects).reduce((hoistedWorkspacePackages, project) => {
if (project.manifest.name && project.id !== '.') {
hoistedWorkspacePackages[project.id] = {
dir: project.rootDir,
name: project.manifest.name,
}
}
}
return hoistedWorkspacePackages
}, {} as Record<string, HoistedWorkspaceProject>)
: undefined,
})
return hoistedWorkspacePackages
}, {} as Record<string, HoistedWorkspaceProject>)
: undefined,
skipped: opts.skipped,
}),
}
} else {
newHoistedDependencies = {}
}

View File

@@ -35,11 +35,7 @@
"dependencies": {
"@pnpm/constants": "workspace:*",
"@pnpm/core-loggers": "workspace:*",
"@pnpm/dependency-path": "workspace:*",
"@pnpm/link-bins": "workspace:*",
"@pnpm/lockfile.types": "workspace:*",
"@pnpm/lockfile.utils": "workspace:*",
"@pnpm/lockfile.walker": "workspace:*",
"@pnpm/matcher": "workspace:*",
"@pnpm/types": "workspace:*",
"@pnpm/util.lex-comparator": "catalog:",

View File

@@ -3,37 +3,46 @@ import path from 'path'
import { linkLogger } from '@pnpm/core-loggers'
import { WANTED_LOCKFILE } from '@pnpm/constants'
import { linkBinsOfPkgsByAliases, type WarnFunction } from '@pnpm/link-bins'
import {
type LockfileObject,
nameVerFromPkgSnapshot,
} from '@pnpm/lockfile.utils'
import { lockfileWalker, type LockfileWalkerStep } from '@pnpm/lockfile.walker'
import { logger } from '@pnpm/logger'
import { createMatcher } from '@pnpm/matcher'
import { type DepPath, type HoistedDependencies, type ProjectId } from '@pnpm/types'
import { type DepPath, type HoistedDependencies, type ProjectId, type DependenciesField } from '@pnpm/types'
import { lexCompare } from '@pnpm/util.lex-comparator'
import * as dp from '@pnpm/dependency-path'
import isSubdir from 'is-subdir'
import mapObjIndexed from 'ramda/src/mapObjIndexed'
import resolveLinkTarget from 'resolve-link-target'
import symlinkDir from 'symlink-dir'
export interface DependenciesGraphNode<T extends string> {
dir: string
children: Record<string, T>
optionalDependencies: Set<string>
hasBin: boolean
name: string
depPath: DepPath
}
export type DependenciesGraph<T extends string> = Record<T, DependenciesGraphNode<T>>
export interface DirectDependenciesByImporterId<T extends string> {
[importerId: string]: Map<string, T>
}
const hoistLogger = logger('hoist')
export interface HoistOpts extends GetHoistedDependenciesOpts {
export interface HoistOpts<T extends string> extends GetHoistedDependenciesOpts<T> {
extraNodePath?: string[]
preferSymlinkedExecutables?: boolean
virtualStoreDir: string
virtualStoreDirMaxLength: number
}
export async function hoist (opts: HoistOpts): Promise<HoistedDependencies> {
export async function hoist<T extends string> (opts: HoistOpts<T>): Promise<HoistedDependencies | null> {
const result = getHoistedDependencies(opts)
if (!result) return {}
const { hoistedDependencies, hoistedAliasesWithBins } = result
if (!result) return null
const { hoistedDependencies, hoistedAliasesWithBins, hoistedDependenciesByNodeId } = result
await symlinkHoistedDependencies(hoistedDependencies, {
lockfile: opts.lockfile,
await symlinkHoistedDependencies(hoistedDependenciesByNodeId, {
graph: opts.graph,
directDepsByImporterId: opts.directDepsByImporterId,
privateHoistedModulesDir: opts.privateHoistedModulesDir,
publicHoistedModulesDir: opts.publicHoistedModulesDir,
virtualStoreDir: opts.virtualStoreDir,
@@ -55,8 +64,10 @@ export async function hoist (opts: HoistOpts): Promise<HoistedDependencies> {
return hoistedDependencies
}
export interface GetHoistedDependenciesOpts {
lockfile: LockfileObject
export interface GetHoistedDependenciesOpts<T extends string> {
graph: DependenciesGraph<T>
skipped: Set<DepPath>
directDepsByImporterId: DirectDependenciesByImporterId<T>
importerIds?: ProjectId[]
privateHoistPattern: string[]
privateHoistedModulesDir: string
@@ -70,12 +81,11 @@ export interface HoistedWorkspaceProject {
dir: string
}
export function getHoistedDependencies (opts: GetHoistedDependenciesOpts): HoistGraphResult | null {
if (opts.lockfile.packages == null) return null
const { directDeps, step } = lockfileWalker(
opts.lockfile,
opts.importerIds ?? Object.keys(opts.lockfile.importers) as ProjectId[]
export function getHoistedDependencies<T extends string> (opts: GetHoistedDependenciesOpts<T>): HoistGraphResult<T> | null {
if (Object.keys(opts.graph ?? {}).length === 0) return null
const { directDeps, step } = graphWalker(
opts.graph,
opts.directDepsByImporterId
)
// We want to hoist all the workspace packages, not only those that are in the dependencies
// of any other workspace packages.
@@ -85,19 +95,19 @@ export function getHoistedDependencies (opts: GetHoistedDependenciesOpts): Hoist
Object.entries(opts.hoistedWorkspacePackages ?? {})
.map(([id, { name }]) => [name, id as ProjectId])
)
const deps: Dependency[] = [
const deps: Array<Dependency<T>> = [
{
children: {
...hoistedWorkspaceDeps,
...directDeps
.reduce((acc, { alias, depPath }) => {
.reduce((acc, { alias, nodeId }) => {
if (!acc[alias]) {
acc[alias] = depPath
acc[alias] = nodeId
}
return acc
}, {} as Record<string, DepPath>),
}, {} as Record<string, T>),
},
depPath: '',
nodeId: '' as T,
depth: -1,
},
...getDependencies(0, step),
@@ -105,9 +115,10 @@ export function getHoistedDependencies (opts: GetHoistedDependenciesOpts): Hoist
const getAliasHoistType = createGetAliasHoistType(opts.publicHoistPattern, opts.privateHoistPattern)
return hoistGraph(deps, opts.lockfile.importers['.' as ProjectId]?.specifiers ?? {}, {
return hoistGraph(deps, opts.directDepsByImporterId['.' as ProjectId] ?? new Map(), {
getAliasHoistType,
lockfile: opts.lockfile,
graph: opts.graph,
skipped: opts.skipped,
})
}
@@ -154,20 +165,16 @@ async function linkAllBins (modulesDir: string, opts: LinkAllBinsOptions): Promi
}
}
function getDependencies (
function getDependencies<T extends string> (
depth: number,
step: LockfileWalkerStep
): Dependency[] {
const deps: Dependency[] = []
const nextSteps: LockfileWalkerStep[] = []
for (const { pkgSnapshot, depPath, next } of step.dependencies) {
const allDeps: Record<string, string> = {
...pkgSnapshot.dependencies,
...pkgSnapshot.optionalDependencies,
}
step: GraphWalkerStep<T>
): Array<Dependency<T>> {
const deps: Array<Dependency<T>> = []
const nextSteps: Array<GraphWalkerStep<T>> = []
for (const { node, nodeId, next } of step.dependencies) {
deps.push({
children: mapObjIndexed(dp.refToRelative, allDeps) as Record<string, DepPath>,
depPath,
children: node.children,
nodeId,
depth,
})
@@ -182,42 +189,47 @@ function getDependencies (
return [
...deps,
...nextSteps.flatMap(getDependencies.bind(null, depth + 1)),
...(nextSteps.flatMap(getDependencies.bind(null, depth + 1)) as Array<Dependency<T>>),
]
}
export interface Dependency {
children: Record<string, DepPath | ProjectId>
depPath: string
export interface Dependency<T extends string> {
children: Record<string, T | ProjectId>
nodeId: T
depth: number
}
interface HoistGraphResult {
interface HoistGraphResult<T extends string> {
hoistedDependencies: HoistedDependencies
hoistedDependenciesByNodeId: HoistedDependenciesByNodeId<T>
hoistedAliasesWithBins: string[]
}
function hoistGraph (
depNodes: Dependency[],
currentSpecifiers: Record<string, string>,
type HoistedDependenciesByNodeId<T extends string> = Map<T | ProjectId, Record<string, 'public' | 'private'>>
function hoistGraph<T extends string> (
depNodes: Array<Dependency<T>>,
currentSpecifiers: Map<string, T>,
opts: {
getAliasHoistType: GetAliasHoistType
lockfile: LockfileObject
graph: DependenciesGraph<T>
skipped: Set<DepPath>
}
): HoistGraphResult {
const hoistedAliases = new Set(Object.keys(currentSpecifiers))
const hoistedDependencies: HoistedDependencies = {}
): HoistGraphResult<T> {
const hoistedAliases = new Set(currentSpecifiers.keys())
const hoistedDependencies: HoistedDependencies = Object.create(null)
const hoistedDependenciesByNodeId: HoistedDependenciesByNodeId<T> = new Map()
const hoistedAliasesWithBins = new Set<string>()
depNodes
// sort by depth and then alphabetically
.sort((a, b) => {
const depthDiff = a.depth - b.depth
return depthDiff === 0 ? lexCompare(a.depPath, b.depPath) : depthDiff
return depthDiff === 0 ? lexCompare(a.nodeId, b.nodeId) : depthDiff
})
// build the alias map and the id map
.forEach((depNode) => {
for (const [childAlias, childPath] of Object.entries<DepPath | ProjectId>(depNode.children)) {
for (const [childAlias, childNodeId] of Object.entries<T | ProjectId>(depNode.children)) {
const hoist = opts.getAliasHoistType(childAlias)
if (!hoist) continue
const childAliasNormalized = childAlias.toLowerCase()
@@ -225,24 +237,37 @@ function hoistGraph (
if (hoistedAliases.has(childAliasNormalized)) {
continue
}
if (opts.lockfile.packages?.[childPath as DepPath]?.hasBin) {
if (!hoistedDependenciesByNodeId.has(childNodeId)) {
hoistedDependenciesByNodeId.set(childNodeId, {})
}
hoistedDependenciesByNodeId.get(childNodeId)![childAlias] = hoist
const node = opts.graph[childNodeId as T]
if (node?.depPath == null || opts.skipped.has(node.depPath)) {
continue
}
if (node.hasBin) {
hoistedAliasesWithBins.add(childAlias)
}
hoistedAliases.add(childAliasNormalized)
if (!hoistedDependencies[childPath]) {
hoistedDependencies[childPath] = {}
if (!hoistedDependencies[node.depPath]) {
hoistedDependencies[node.depPath] = {}
}
hoistedDependencies[childPath][childAlias] = hoist
hoistedDependencies[node.depPath][childAlias] = hoist
}
})
return { hoistedDependencies, hoistedAliasesWithBins: Array.from(hoistedAliasesWithBins) }
return {
hoistedDependencies,
hoistedDependenciesByNodeId,
hoistedAliasesWithBins: Array.from(hoistedAliasesWithBins),
}
}
async function symlinkHoistedDependencies (
hoistedDependencies: HoistedDependencies,
async function symlinkHoistedDependencies<T extends string> (
hoistedDependenciesByNodeId: HoistedDependenciesByNodeId<T>,
opts: {
lockfile: LockfileObject
graph: DependenciesGraph<T>
directDepsByImporterId: DirectDependenciesByImporterId<T>
privateHoistedModulesDir: string
publicHoistedModulesDir: string
virtualStoreDir: string
@@ -251,32 +276,31 @@ async function symlinkHoistedDependencies (
}
): Promise<void> {
const symlink = symlinkHoistedDependency.bind(null, opts)
await Promise.all(
Object.entries(hoistedDependencies)
.map(async ([hoistedDepId, pkgAliases]) => {
const pkgSnapshot = opts.lockfile.packages![hoistedDepId as DepPath]
let depLocation!: string
if (pkgSnapshot) {
const pkgName = nameVerFromPkgSnapshot(hoistedDepId, pkgSnapshot).name
const modules = path.join(opts.virtualStoreDir, dp.depPathToFilename(hoistedDepId, opts.virtualStoreDirMaxLength), 'node_modules')
depLocation = path.join(modules, pkgName as string)
} else {
if (!opts.lockfile.importers[hoistedDepId as ProjectId]) {
// This dependency is probably a skipped optional dependency.
hoistLogger.debug({ hoistFailedFor: hoistedDepId })
return
}
depLocation = opts.hoistedWorkspacePackages![hoistedDepId].dir
const promises: Array<Promise<void>> = []
for (const [hoistedDepNodeId, pkgAliases] of hoistedDependenciesByNodeId.entries()) {
promises.push((async () => {
const node = opts.graph[hoistedDepNodeId as T]
let depLocation!: string
if (node) {
depLocation = node.dir
} else {
if (!opts.directDepsByImporterId[hoistedDepNodeId as ProjectId]) {
// This dependency is probably a skipped optional dependency.
hoistLogger.debug({ hoistFailedFor: hoistedDepNodeId })
return
}
await Promise.all(Object.entries(pkgAliases).map(async ([pkgAlias, hoistType]) => {
const targetDir = hoistType === 'public'
? opts.publicHoistedModulesDir
: opts.privateHoistedModulesDir
const dest = path.join(targetDir, pkgAlias)
return symlink(depLocation, dest)
}))
})
)
depLocation = opts.hoistedWorkspacePackages![hoistedDepNodeId].dir
}
await Promise.all(Object.entries(pkgAliases).map(async ([pkgAlias, hoistType]) => {
const targetDir = hoistType === 'public'
? opts.publicHoistedModulesDir
: opts.privateHoistedModulesDir
const dest = path.join(targetDir, pkgAlias)
return symlink(depLocation, dest)
}))
})())
}
await Promise.all(promises)
}
async function symlinkHoistedDependency (
@@ -313,3 +337,107 @@ async function symlinkHoistedDependency (
await symlinkDir(depLocation, dest)
linkLogger.debug({ target: dest, link: depLocation })
}
export function graphWalker<T extends string> (
graph: DependenciesGraph<T>,
directDepsByImporterId: DirectDependenciesByImporterId<T>,
opts?: {
include?: { [dependenciesField in DependenciesField]: boolean }
skipped?: Set<DepPath>
}
): GraphWalker<T> {
const startNodeIds = [] as T[]
const allDirectDeps = [] as Array<{ alias: string, nodeId: T }>
for (const directDeps of Object.values(directDepsByImporterId)) {
for (const [alias, nodeId] of directDeps.entries()) {
const depNode = graph[nodeId]
if (depNode == null) continue
startNodeIds.push(nodeId)
allDirectDeps.push({ alias, nodeId })
}
}
const visited = new Set<T>()
return {
directDeps: allDirectDeps,
step: makeStep({
includeOptionalDependencies: opts?.include?.optionalDependencies !== false,
graph,
visited,
skipped: opts?.skipped,
}, startNodeIds),
}
}
function makeStep<T extends string> (
ctx: {
includeOptionalDependencies: boolean
graph: DependenciesGraph<T>
visited: Set<T>
skipped?: Set<DepPath>
},
nextNodeIds: T[]
): GraphWalkerStep<T> {
const result: GraphWalkerStep<T> = {
dependencies: [],
links: [],
missing: [],
}
const _next = collectChildNodeIds.bind(null, {
includeOptionalDependencies: ctx.includeOptionalDependencies,
})
for (const nodeId of nextNodeIds) {
if (ctx.visited.has(nodeId)) continue
ctx.visited.add(nodeId)
const node = ctx.graph[nodeId]
if (node == null) {
if (nodeId.startsWith('link:')) {
result.links.push(nodeId)
continue
}
result.missing.push(nodeId)
continue
}
if (ctx.skipped?.has(node.depPath)) continue
result.dependencies.push({
nodeId,
next: () => makeStep<T>(ctx, _next(node) as T[]),
node,
})
}
return result
}
function collectChildNodeIds<T extends string> (opts: { includeOptionalDependencies: boolean }, nextPkg: DependenciesGraphNode<T>): T[] {
if (opts.includeOptionalDependencies) {
return Object.values(nextPkg.children)
} else {
const nextNodeIds: T[] = []
for (const [alias, nodeId] of Object.entries(nextPkg.children)) {
if (!nextPkg.optionalDependencies.has(alias)) {
nextNodeIds.push(nodeId)
}
}
return nextNodeIds
}
}
export interface GraphWalker<T extends string> {
directDeps: Array<{
alias: string
nodeId: T
}>
step: GraphWalkerStep<T>
}
export interface GraphWalkerStep<T extends string> {
dependencies: Array<GraphDependency<T>>
links: string[]
missing: string[]
}
export interface GraphDependency<T extends string> {
nodeId: T
node: DependenciesGraphNode<T>
next: () => GraphWalkerStep<T>
}

View File

@@ -12,24 +12,12 @@
{
"path": "../../config/matcher"
},
{
"path": "../../lockfile/types"
},
{
"path": "../../lockfile/utils"
},
{
"path": "../../lockfile/walker"
},
{
"path": "../../packages/constants"
},
{
"path": "../../packages/core-loggers"
},
{
"path": "../../packages/dependency-path"
},
{
"path": "../../packages/logger"
},

15
pnpm-lock.yaml generated
View File

@@ -33,6 +33,9 @@ catalogs:
'@pnpm/log.group':
specifier: 3.0.1
version: 3.0.1
'@pnpm/logger':
specifier: '>=1001.0.0 <1002.0.0'
version: 1001.0.0
'@pnpm/meta-updater':
specifier: 2.0.6
version: 2.0.6
@@ -5025,21 +5028,9 @@ importers:
'@pnpm/core-loggers':
specifier: workspace:*
version: link:../../packages/core-loggers
'@pnpm/dependency-path':
specifier: workspace:*
version: link:../../packages/dependency-path
'@pnpm/link-bins':
specifier: workspace:*
version: link:../link-bins
'@pnpm/lockfile.types':
specifier: workspace:*
version: link:../../lockfile/types
'@pnpm/lockfile.utils':
specifier: workspace:*
version: link:../../lockfile/utils
'@pnpm/lockfile.walker':
specifier: workspace:*
version: link:../../lockfile/walker
'@pnpm/matcher':
specifier: workspace:*
version: link:../../config/matcher