mirror of
https://github.com/pnpm/pnpm.git
synced 2026-04-10 18:18:56 -04:00
fix: hoisting with global virtual store (#9648)
This commit is contained in:
8
.changeset/flat-insects-act.md
Normal file
8
.changeset/flat-insects-act.md
Normal 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).
|
||||
5
.changeset/odd-mammals-battle.md
Normal file
5
.changeset/odd-mammals-battle.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@pnpm/deps.graph-builder": minor
|
||||
---
|
||||
|
||||
New option added: includeUnchangedDeps.
|
||||
8
deps/graph-builder/src/lockfileToDepGraph.ts
vendored
8
deps/graph-builder/src/lockfileToDepGraph.ts
vendored
@@ -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 })
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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' },
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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 = {}
|
||||
}
|
||||
|
||||
@@ -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:",
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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
15
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user